[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
List of records in G-Exp
From: |
Martin Baulig |
Subject: |
List of records in G-Exp |
Date: |
Tue, 25 Jul 2023 05:47:11 +0000 |
Hello,
I'm currently working on a somewhat complex service; there are multiple
daemons, each running as its own user, and some "secrets" that need to be
installed (passwords, X.509 certificate keys). After getting a prototype up and
running a couple of weeks ago, I'm now trying to polish things up and remove
some of the ugly hacks that I had used in my first implementation.
To make this modular and extensible, there will be several services that build
on top of each other:
- The service-account-service-type at the bottom, extending both
account-service-type and activation-service-type to create the user accounts
and their data directories (each daemon will have its own log file, pid file
and scratch storage, etc.)
- The secrets-service-type extends that to install some files that only the
daemons can access.
- The actual services sit on top of that - and extend the first two based on
their configuration.
I got a prototype working, but my code looks rather ugly, so I was wondering
whether there might be a better way.
I defined these two record types:
> (define-record-type* <service-account-service>
> service-account-service
> make-service-account-service
> service-account-service?
>
> (root service-account-service-root (default "/etc/private"))
> (entries service-account-service-entries (default '())))
>
> (define-record-type* <service-account>
> service-account
> make-service-account
> service-account?
>
> (id service-account-id)
> (user service-account-user (default "root"))
> (group service-account-group (default "root"))
> (create-account? service-account-create-account (default #f))
> (working-directory service-account-working-directory (default #f))
> (has-daemon? service-account-has-daemon? (default #t))
> (pid-directory service-account-pid-directory (default #f))
> (log-directory service-account-log-directory (default #f))
> (log-file service-account-log-file (default #f)))
And then I declare my service type:
> (define-public service-account-service-type
> (service-type (name 'service-account)
> (extensions
> (list (service-extension activation-service-type
> service-account-service-activation)))
> (compose concatenate)
> (extend extend-service-account-entries)
> (description
> "Service-Account service activation.")
> (default-value (service-account-service))))
and I can then extend it via something like
> (define secrets-service-account
> (match-lambda
> (($ <secrets-service> _ _ entries)
> (map secrets-entry-account entries))))
>
> (define-public secrets-service-type
> (service-type (name 'secrets)
> (extensions
> (list (service-extension activation-service-type
> secrets-service-activation)
> (service-extension service-account-service-type
> secrets-service-account)))
> (compose concatenate)
> (extend (lambda (config extended-secrets)
> (match-record config <secrets-service>
> (database root-directory entries)
> (secrets-service
> (database database)
> (root-directory root-directory)
> (entries (append entries extended-secrets))))))
> (description
> "Secrets service activation.")
> (default-value (secrets-service))))
This is all pretty much unspectacular.
The problem arises with the activation service implementation:
> (extensions
> (list (service-extension activation-service-type
> service-account-service-activation)))
If I understand this correctly, then my service-account-service-activation
needs to be a function that takes an instance of my <service-account-service>
record as its only parameter and returns a G-Exp.
So ...
> (define (service-account-service-activation configuration)
>
> #~(begin
>
> (format #t "Activating service accounts: ~a~%" #$configuration)))
Well, to - that's going to produce an error complaining about my
<service-account-service> record not being a valid G-Exp input.
I can access its two string fields easily - but how do I iterate over the list
of <service-account> records? I'd have to use ungexp inside that inner block -
but that only seems to work with stuff that can be "lowered".
Of course, I could "cheat" and do something like ...
> #$@(map some-func (service-account-service-entries configuration))
... and that both compiles and runs (provided that I put that some-func into an
imported module) - but that some-func will be invoked multiple times throughout
a "guix system reconfigure".
But I want to do it the right way.
Here's what I came up with:
First, we define a function that turns a <service-account> into a list of
primitive types (strings, integers, booleans):
> (define translate-account
> (match-lambda
> (($ <service-account> id user group create-account? working-dir
> has-daemon? pid-dir log-dir log-file)
> `(list ',id ,user ,group ,create-account? ,working-dir
>
> ,pid-dir ,log-dir ,log-file))))
Then, I define a G-Exp compiler for <service-account-service>:
> (define-gexp-compiler (service-account-service-compiler
> (config <service-account-service>)
> system target)
> (with-monad %store-monad
> (match config
> (($ <service-account-service> root entries)
> (return `(list ,root
> ,@(map (lambda (account)
> (translate-account
> (resolve-service-account root (cdr account))))
>
> entries)))))))
And finally, in my service-account-service-activation, I pattern-match against
these lists:
> (define (service-account-service-activation configuration)
> (with-imported-modules '((baulig build utils))
> #~(begin
> (use-modules (baulig build utils))
> (match-let (((root . entries) #$configuration))
> (format #t "Activating service accounts: ~a~%" root)
> (map (match-lambda
> ((id user group create-account? working-dir pid-dir log-dir log-file)
> (format #t "Activating accound: ~a - ~a / ~a - ~a - ~a - ~a - ~a / ~a~%"
> id user group create-account? working-dir pid-dir log-dir log-file)))
>
> entries)))))
Well, it compiles and runs - but it's still quite a bit of boilerplate.
And ideally, I'd like to have a function that takes an instance of a
<service-account> record and returns a G-Exp containing all the mkdir-p, chown,
etc. commands for it. These cannot return a "normal" derivation / store item -
because their outputs need to be installed with special ownership / file
permissions, as they will contain passwords / keys required by the daemons to
run.
But since there will be multiple instances of that record, I'd get some
function returning a list of G-Exp's. To sequentially execute them, my gut
feeling tells me that I'll need to use the monadic nature of the Store - is
there something like Haskell's Monadic map functions for G-Exp's?
Full code:
-
[account.scm](https://gitlab.com/martin-baulig/config-and-setup/guix-packages/-/blob/662a8142562942306bb4746a8058e13109dee1a8/packages/baulig/services/account.scm)
-
[secrets.scm](https://gitlab.com/martin-baulig/config-and-setup/guix-packages/-/blob/662a8142562942306bb4746a8058e13109dee1a8/packages/baulig/services/secrets.scm)
And I'm trying to replace [this
monstrosity](https://gitlab.com/martin-baulig/config-and-setup/guix-packages/-/blob/662a8142562942306bb4746a8058e13109dee1a8/packages/baulig/services/bacula.scm).
I'm still fairly new to GNU Guix, so any feedback would be greatly appreciated.
Best regards,
Martin
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- List of records in G-Exp,
Martin Baulig <=