[Top][All Lists]

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH v2] doc: Add Writing Service Configuration section.

From: Andrew Tropin
Subject: [PATCH v2] doc: Add Writing Service Configuration section.
Date: Thu, 23 Dec 2021 16:22:25 +0300

* guix.texi (Writing Service Configuration): New section.
 doc/guix.texi | 252 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 248 insertions(+), 4 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 333cb4117a..29d85d3dc5 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -10363,6 +10363,7 @@ compiling modules.  It can be @code{#f}, @code{#t}, or 
 The other arguments are as for @code{derivation} (@pxref{Derivations}).
 @end deffn
+@anchor{file-like objects}
 @cindex file-like objects
 The @code{local-file}, @code{plain-file}, @code{computed-file},
 @code{program-file}, and @code{scheme-file} procedures below return
@@ -15942,6 +15943,7 @@ symlink:
 Return a service that sets the host name to @var{name}.
 @end deffn
 @defvr {Scheme Variable} console-font-service-type
 Install the given fonts on the specified ttys (fonts are per
 virtual console on the kernel Linux).  The value of this service is a list of
@@ -33717,6 +33719,7 @@ a daemon that can execute application bundles 
(sometimes referred to as
 @end defvr
 @deftp {Data Type} docker-configuration
 This is the data type representing the configuration of Docker and Containerd.
@@ -35652,10 +35655,11 @@ them in an @code{operating-system} declaration.  But 
how do we define
 them in the first place?  And what is a service anyway?
-* Service Composition::         The model for composing services.
-* Service Types and Services::  Types and services.
-* Service Reference::           API reference.
-* Shepherd Services::           A particular type of service.
+* Service Composition::            The model for composing services.
+* Service Types and Services::     Types and services.
+* Writing Service Configurations:: A guideline for writing guix services.
+* Service Reference::              API reference.
+* Shepherd Services::              A particular type of service.
 @end menu
 @node Service Composition
@@ -35851,6 +35855,245 @@ There can be only one instance of an extensible 
service type such as
 Still here?  The next section provides a reference of the programming
 interface for services.
+@node Writing Service Configurations
+@subsection Writing Service Configurations
+Guix already contains a wide variety of system and home services, but
+sometimes users might want to add new services.  This section contains
+tips for simplifying this process, and should help to make service
+configurations and their implementations more consistent.
+@quotation Note
+If you find any exceptions or patterns missing in this section, please
+send a patch with additions/changes to @email{}
+mailing list or just start a discussion/ask a question.
+@end quotation
+@subsubheading Configuration Itself
+As we know from previous sections, a Guix service can accept a service
+value, usually some kind of configuration record and optionally, be
+extended with additional values by other services (@pxref{Service
+When being extended, most services take some kind of configuration
+record or a list thereof, but in some cases a simpler value is all
+that is necessary.
+There are some cases, when the service accepts a list of pairs or some
+other non-record values.  For example, @code{console-font-service-type}
+(@pxref{console-font-service-type}) accepts an
+association list, and @code{etc-service-type} (@pxref{etc-service-type})
+accepts a list of lists.  Those services are kinda special, they do
+auxiliary work of setting up some part of the operating system or home
+environment, or just an intermediate helpers used by other Guix
+services.  For example @code{etc-service-type} is not that useful on its
+own, but it helps other services to create files in /etc directory, when
+it necessary.
+However, in most cases a Guix service is wrapping some software, which
+consists of one or more packages, and configuration file or files.
+Therefore, the value for such service is quite complicated and it's hard
+to represent it with just a list or basic data type, in such cases we
+use a record.  Each such record (@pxref{SRFI-9 Records, Scheme Records,,
+guile, GNU Guile Reference Manual}) have @samp{-configuration} suffix,
+for example, the @code{docker-service-type} should accept a record type
+named @code{docker-configuration}, which contains fields used to
+configure Docker.  Configuration records for home services should also
+have a @code{home-} prefix in their name.
+There is a module @code{gnu service configuration}, which contains
+helpers simplifying configuration definition process.  Take a look at
+@code{gnu services docker} module or grep for
+@code{define-configuration} to find usage examples.
+@c Provide some examples, tips, and rationale behind @code{gnu service
+@c configuration} module.
+After a configuration record has been properly named and defined let's
+discuss how to name and define the fields, and which approach to use for
+implementing the serialization code related to them.
+In this context, the @dfn{serialization} is a process of converting
+values of the fields defined in service configuration into a string or
+strings of a target config format, which will be put to the
+configuration file or files used by the program.
+@subsubheading Configuration Record Fields
+It's a good idea to have one or more fields for specifying the package
+or packages that will be installed by a service.  For example,
+@code{docker-configuration} has @code{docker}, @code{docker-cli},
+@code{containerd} fields (@pxref{docker-configuration}).  Sometimes it
+make sense to make a field, which accepts a list of packages for cases,
+where an arbitrary list of plugins can be passed to the configuration.
+There are some services, which provide a field called @code{package} in
+their configuration, which is ok, but the way it done in
+@code{docker-configuration} is more flexible and thus preferable.
+Fields for configuration files, should be name the same as target
+configuration file name, but in kebab-case@footnote{The case used for
+identifiers in languages of Lisp family, example:
+@code{this-is-kebab-case}.}: @code{bashrc} for @file{.bashrc},
+@code{bash-profile} for @file{.bash_profile},
+@code{tmux-conf} for @file{tmux.conf}, etc.  The implementation
+for such fields will be discussed in the next subsubsection.
+Other fields in most cases add some boilerplates/reasonable defaults to
+configuration files, enable/disable installation of some packages or
+provide other custom behavior, for example @code{guix-defaults?} or
+@code{aliases} fields in @code{home-bash-configuration}
+(@pxref{home-bash-configuration}).  There is no any special requirements
+or recommendations here, but it's necessary to make it possible to
+disable all the effects of such fields to provide a user with an empty
+configuration and let them generate it from scratch with only field for
+configuration file.  For example, setting @code{guix-defaults?} to
+@code{#f} and @code{aliases} to @code{'()} will give user an ability to
+control the content of @file{.bashrc} solely by setting the value of
+@code{bashrc} field.
+@end enumerate
+@subsubheading Fields for Configuration Files
+The field should accept a data structure (preferably a combination of
+simple lists, alists, @ref{Vectors, vectors,, guile,},
+@ref{G-Expressions, gexps} and basic data types), which will be
+serialized to target configuration format, in other words, it should
+provide an alternative Lisp syntax, which can be later translated to a
+target one, like SXML to XML.  Such approach is quite flexible and
+simple, it requires to write serializer once for one configuration
+format and can be reused multiple times in different Guix services.
+Let's take a look at JSON: we implement serialization function, which
+converts vectors to arrays, alists to objects (AKA dictionaries or
+associative arrays), numbers to numbers, gexps to the strings,
+@ref{file-like objects} (@pxref{G-Expressions}) to the strings, which
+contains the path to the file in the store, @code{#t} to @code{true} and
+so on, and now we have all programs using JSON as a format for
+configurations covered.  Maybe some fine-tunning will be needed for
+particular application, but the primary serialization part is already
+The pros and cons of such approach is inherited from open-world
+assumption.  It doesn't matter if the underlying applications provides
+new configuration options, we don't need to change anything in the
+service configuration and its serialization code, it will work perfectly
+fine.  On the other hand, it is harder to type check and structure check
+at ``compile-time'', and we can end up with a configuration, which won't
+be accepted by the target program due to unexisting, misspelled or
+wrongly-typed options.  It's possible to add those checks, but we will
+get the drawbacks of closed-world assumption: we need to keep the
+service implementation in-sync with app config options, and it will make
+impossible to use the same service with older/newer package version,
+which has a slightly different list of available options and will add an
+excessive maintanence load.
+However, for some applications with really stable configuration those
+checks can be helpful and should be implemented if possible, for some
+others we can implement them only partially.
+The alternative approach applied in some exitsting services is to use
+records for defining the structure of configuration field, it has the
+same downsides of closed-world assumption and a few more problems:
+It has to replicate all the available options for the app (sometimes
+hundreds or thousands) to allow the user express any configuration they
+Having a few records adds one more layer of abstraction between service
+configuration and resulting app config, including different field
+casing, new semantic units.
+@c provide examples?
+It harder to implement optional settings, serialization becomes very
+ad-hoc and hard to reuse among other services with the same target
+config format.
+@end enumerate
+Exceptions can exist, but the overall idea is to provide a lispy syntax
+for target configuration.  Take a look at Sway example configuration
+(which also can be used for i3).  The following value of @code{config}
+field of @code{home-sway-configuration}:
+`((include ,(local-file "./sway/config"))
+  (bindsym $mod+Ctrl+Shift+a exec emacsclient -c --eval "'(eshell)'")
+  (bindsym $mod+Ctrl+Shift+o "[class=\"IceCat\"]" kill)
+  (input * ((xkb_layout us,ru)
+            (xkb_variant dvorak,))))
+@end example
+would yield something like:
+include /gnu/store/408jwvh6wxxn1j85lj95fniih05gx5xj-config
+bindsym $mod+Ctrl+Shift+a exec emacsclient -c --eval '(eshell)'
+bindsym $mod+Ctrl+Shift+o [class="IceCat"] kill
+input * @{
+    xkb_layout us,ru
+    xkb_variant dvorak,
+@end example
+The mapping between Scheme code and resulting configuration is quite
+obvious.  The serialization code with some type and structure checks
+takes less than 70 lines and every possible Sway/i3 configuration can be
+expressed using this field.
+@subsubheading Let User Escape
+Sometimes a user already has a configuration file for an program, make
+sure that it is possible to reuse it directly without rewriting.  In the
+example above, the following snippet allows one to include already an
+existing config to the newly generated one utilizing @code{include}
+directive of i3/Sway config language:
+(include ,(local-file "./sway/config"))
+@end lisp
+When building a resulting config the file-like objects are substituted
+with a path of the file in the store and Sway's @code{include} loads
+this file during startup.  The way file-like objects are treated here
+also allows one to specify paths to plugins or other binary files like:
+(load-plugin ,(file-append plugin-package "/share/"))
+@end lisp
+(the example value for imaginary service configuration config file
+In some cases the target configuration language may not have such an
+@code{include} directive and can't provide such a functionallity, to
+workaround it we can do the following trick:
+   #$(local-file "./sway/config")
+   (@@ (ice-9 textual-ports) get-string-all))
+@end lisp
+The ‘get-string-all’ procedure will read the contents of the
+@file{./sway/config} file (to be more preciese the copy of this file
+placed in the store), and return a string containing the contents.  Once
+serialized, the G-expression will thus be turn into the contents of the
+Sway configuration file in @file{./sway/config}.  This code can be
+easily combined with the rest of Sway's configuration, additionally, we
+can control the place where the content of @file{./sway/config} will
+appear in resulting file by moving this snippet around.
+Following these simple rules will help to make simple, consistent and
+maintainable service configurations, and will let users express any
+possible needs and reuse existing configuration files.
 @node Service Reference
 @subsection Service Reference
@@ -36076,6 +36319,7 @@ The type of the ``boot service'', which produces the 
@dfn{boot script}.
 The boot script is what the initial RAM disk runs when booting.
 @end defvr
 @defvr {Scheme Variable} etc-service-type
 The type of the @file{/etc} service.  This service is used to create
 files under @file{/etc} and can be extended by

Attachment: signature.asc
Description: PGP signature

reply via email to

[Prev in Thread] Current Thread [Next in Thread]