From c7183924644473be64913c12ba8a2cee2e997e1f Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 8 Jan 2025 14:35:28 +0100 Subject: [PATCH 1/6] Add modular services, system.services --- nixos/modules/module-list.nix | 2 + nixos/modules/system/service/README.md | 28 ++++++ .../system/service/portable/service.nix | 51 +++++++++++ .../modules/system/service/portable/test.nix | 89 +++++++++++++++++++ .../system/service/systemd/service.nix | 67 ++++++++++++++ .../modules/system/service/systemd/system.nix | 65 ++++++++++++++ nixos/modules/system/service/systemd/test.nix | 70 +++++++++++++++ nixos/modules/system/service/systemd/user.nix | 3 + nixos/tests/all-tests.nix | 10 +++ 9 files changed, 385 insertions(+) create mode 100644 nixos/modules/system/service/README.md create mode 100644 nixos/modules/system/service/portable/service.nix create mode 100644 nixos/modules/system/service/portable/test.nix create mode 100644 nixos/modules/system/service/systemd/service.nix create mode 100644 nixos/modules/system/service/systemd/system.nix create mode 100644 nixos/modules/system/service/systemd/test.nix create mode 100644 nixos/modules/system/service/systemd/user.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index b5b6fd8c0e2fe..a4dcabae32d7f 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1715,6 +1715,8 @@ ./system/boot/tmp.nix ./system/boot/uvesafb.nix ./system/etc/etc-activation.nix + ./system/service/systemd/system.nix + ./system/service/systemd/user.nix ./tasks/auto-upgrade.nix ./tasks/bcache.nix ./tasks/cpu-freq.nix diff --git a/nixos/modules/system/service/README.md b/nixos/modules/system/service/README.md new file mode 100644 index 0000000000000..ed0a247267eb0 --- /dev/null +++ b/nixos/modules/system/service/README.md @@ -0,0 +1,28 @@ + +# Modular Services + +This directory defines a modular service infrastructure for NixOS. +See the [Modular Services chapter] in the manual [[source]](../../doc/manual/development/modular-services.md). + +[Modular Services chapter]: https://nixos.org/manual/nixos/unstable/#modular-services + +# Design decision log + +- `system.services.`. Alternatives considered + - `systemServices`: similar to does not allow importing a composition of services into `system`. Not sure if that's a good idea in the first place, but I've kept the possibility open. + - `services.abstract`: used in https://github.com/NixOS/nixpkgs/pull/267111, but too weird. Service modules should fit naturally into the configuration system. + Also "abstract" is wrong, because it has submodules - in other words, evalModules results, concrete services - not abstract at all. + - `services.modular`: only slightly better than `services.abstract`, but still weird + +- No `daemon.*` options. https://github.com/NixOS/nixpkgs/pull/267111/files#r1723206521 + +- For now, do not add an `enable` option, because it's ambiguous. Does it disable at the Nix level (not generate anything) or at the systemd level (generate a service that is disabled)? + +- Move all process options into a `process` option tree. Putting this at the root is messy, because we also have sub-services at that level. Those are rather distinct. Grouping them "by kind" should raise fewer questions. + +- `modules/system/service/systemd/system.nix` has `system` twice. Not great, but + - they have different meanings + 1. These are system-provided modules, provided by the configuration manager + 2. `systemd/system` configures SystemD _system units_. + - This reserves `modules/service` for actual service modules, at least until those are lifted out of NixOS, potentially + diff --git a/nixos/modules/system/service/portable/service.nix b/nixos/modules/system/service/portable/service.nix new file mode 100644 index 0000000000000..76c0441ab3a0e --- /dev/null +++ b/nixos/modules/system/service/portable/service.nix @@ -0,0 +1,51 @@ +{ lib, config, options, ... }: +let + inherit (lib) mkOption types; + pathOrStr = types.coercedTo types.path (x: "${x}") types.str; + program = + types.coercedTo + (types.package // { + # require mainProgram for this conversion + check = v: v.type or null == "derivation" && v?meta.mainProgram; + }) + lib.getExe + pathOrStr + // { + description = "main program, path or command"; + descriptionClass = "conjunction"; + }; +in +{ + options = { + services = mkOption { + type = types.attrsOf (types.submoduleWith { + modules = [ + ./service.nix + ]; + }); + description = '' + A collection of [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured in one go. + + You could consider the sub-service relationship to be an ownership relation. + It **does not** automatically create any other relationship between services (e.g. systemd slices), unless perhaps such a behavior is explicitly defined and enabled in another option. + ''; + default = { }; + visible = "shallow"; + }; + process = { + executable = mkOption { + type = program; + description = '' + The path to the executable that will be run when the service is started. + ''; + }; + args = lib.mkOption { + type = types.listOf pathOrStr; + description = '' + Arguments to pass to the `executable`. + ''; + default = []; + }; + }; + }; +} diff --git a/nixos/modules/system/service/portable/test.nix b/nixos/modules/system/service/portable/test.nix new file mode 100644 index 0000000000000..82f2cdba16287 --- /dev/null +++ b/nixos/modules/system/service/portable/test.nix @@ -0,0 +1,89 @@ +# Run: +# nix-instantiate --eval nixos/modules/system/service/portable/test.nix +let + lib = import ../../../../../lib; + + inherit (lib) mkOption types; + + dummyPkg = + name: + derivation { + system = "dummy"; + name = name; + builder = "/bin/false"; + }; + + exampleConfig = { + _file = "${__curPos.file}:${toString __curPos.line}"; + services = { + service1 = { + process = { + executable = "/usr/bin/echo"; # *giggles* + args = [ "hello" ]; + }; + }; + service2 = { + process = { + # No meta.mainProgram, because it's supposedly an executable script _file_, + # not a directory with a bin directory containing the main program. + executable = dummyPkg "cowsay.sh"; + args = [ "world" ]; + }; + }; + service3 = { + process = { + executable = dummyPkg "cowsay-ng" // { meta.mainProgram = "cowsay"; }; + args = [ "!" ]; + }; + }; + }; + }; + + exampleEval = lib.evalModules { + modules = [ + { + options.services = mkOption { + type = types.attrsOf (types.submoduleWith { + class = "service"; + modules = [ + ./service.nix + ]; + }); + }; + } + exampleConfig + ]; + }; + + test = + assert + exampleEval.config == { + services = { + service1 = { + process = { + executable = "/usr/bin/echo"; + args = [ "hello" ]; + }; + services = { }; + }; + service2 = { + process = { + executable = "${dummyPkg "cowsay.sh"}"; + args = [ "world" ]; + }; + services = { }; + }; + service3 = { + process = { + executable = "${dummyPkg "cowsay-ng"}/bin/cowsay"; + args = [ "!" ]; + }; + services = { }; + }; + }; + }; + + "ok"; + +in +test diff --git a/nixos/modules/system/service/systemd/service.nix b/nixos/modules/system/service/systemd/service.nix new file mode 100644 index 0000000000000..01698f784e44b --- /dev/null +++ b/nixos/modules/system/service/systemd/service.nix @@ -0,0 +1,67 @@ +{ lib, config, ... }: +let + inherit (lib) mkOption types; +in +{ + imports = [ + ../portable/service.nix + (lib.mkAliasOptionModule ["systemd" "service"] ["systemd" "services" ""]) + (lib.mkAliasOptionModule ["systemd" "socket"] ["systemd" "sockets" ""]) + ]; + options = { + systemd.services = mkOption { + description = '' + This module configures systemd services, with the notable difference that their unit names will be prefixed with the abstract service name. + + This option's value is not suitable for reading, but you can define a module here that interacts with just the unit configuration in the host system configuration. + + Note that this option contains _deferred_ modules. + This means that the module has not been combined with the system configuration yet, no values can be read from this option. + What you can do instead is define a module that reads from the module arguments (such as `config`) that are available when the module is merged into the system configuration. + ''; + type = + types.lazyAttrsOf + (types.deferredModuleWith { + staticModules = [ + # TODO: Add modules for the purpose of generating documentation? + ]; + }); + default = {}; + }; + systemd.sockets = mkOption { + description = '' + Declares systemd socket units. Names will be prefixed by the service name / path. + + See {option}`systemd.services`. + ''; + type = types.lazyAttrsOf types.deferredModule; + default = {}; + }; + + # Also import systemd logic into sub-services + # extends the portable `services` option + services = mkOption { + type = types.attrsOf (types.submoduleWith { + class = "service"; + modules = [ + ./service.nix + ]; + }); + }; + }; + config = { + # Note that this is the systemd.services option above, not the system one. + systemd.services."" = { + # TODO description; + wantedBy = lib.mkDefault [ "multi-user.target" ]; + serviceConfig = { + Type = lib.mkDefault "simple"; + Restart = lib.mkDefault "always"; + RestartSec = lib.mkDefault "5"; + ExecStart = [ + (lib.escapeShellArgs ([ config.process.executable ] ++ config.process.args)) + ]; + }; + }; + }; +} diff --git a/nixos/modules/system/service/systemd/system.nix b/nixos/modules/system/service/systemd/system.nix new file mode 100644 index 0000000000000..5c6e544ea9633 --- /dev/null +++ b/nixos/modules/system/service/systemd/system.nix @@ -0,0 +1,65 @@ +{ lib, config, pkgs, ... }: + +let + inherit (lib) concatMapAttrs mkOption types; + + dash = before: after: + if after == "" + then before + else if before == "" + then after + else "${before}-${after}"; + + makeUnits = unitType: prefix: service: + concatMapAttrs + (unitName: unitModule: { + "${dash prefix unitName}" = { ... }: { + imports = [ unitModule ]; + }; + }) + service.systemd.${unitType} + // concatMapAttrs + (subServiceName: subService: + makeUnits unitType (dash prefix subServiceName) subService + ) + service.services; +in +{ + # First half of the magic: mix systemd logic into the otherwise abstract services + options = { + system.services = mkOption { + description = '' + A collection of NixOS [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured as systemd services. + ''; + type = types.attrsOf (types.submoduleWith { + class = "service"; + modules = [ + ./service.nix + ]; + specialArgs = { + # perhaps: features."systemd" = { }; + inherit pkgs; + }; + }); + default = { }; + visible = "shallow"; + }; + }; + + # Second half of the magic: siphon units that were defined in isolation to the system + config = { + systemd.services = + concatMapAttrs + (serviceName: topLevelService: + makeUnits "services" serviceName topLevelService + ) + config.system.services; + + systemd.sockets = + concatMapAttrs + (serviceName: topLevelService: + makeUnits "sockets" serviceName topLevelService + ) + config.system.services; + }; +} diff --git a/nixos/modules/system/service/systemd/test.nix b/nixos/modules/system/service/systemd/test.nix new file mode 100644 index 0000000000000..76eaebe19069d --- /dev/null +++ b/nixos/modules/system/service/systemd/test.nix @@ -0,0 +1,70 @@ +# Run: +# nix-build -A nixosTests.modularService + +{ evalSystem, runCommand, hello, ... }: + +let + machine = evalSystem ({ lib, ... }: { + + # Test input + + system.services.foo = { + process = { + executable = hello; + args = [ "--greeting" "hoi" ]; + }; + }; + system.services.bar = { + process = { + executable = hello; + args = [ "--greeting" "hoi" ]; + }; + systemd.service = { + serviceConfig.X-Bar = "lol crossbar whatever"; + }; + services.db = { + process = { + executable = hello; + args = [ "--greeting" "Hi, I'm a database, would you believe it" ]; + }; + systemd.service = { + serviceConfig.RestartSec = "42"; + }; + }; + }; + + # irrelevant stuff + system.stateVersion = "25.05"; + fileSystems."/".device = "/test/dummy"; + boot.loader.grub.enable = false; + }); + + inherit (machine.config.system.build) toplevel; +in + runCommand "test-modular-service-systemd-units" { + passthru = { + inherit + machine + toplevel + ; + }; + } '' + echo ${toplevel}/etc/systemd/system/foo.service: + cat -n ${toplevel}/etc/systemd/system/foo.service + ( + set -x + grep -F 'ExecStart=${hello}/bin/hello --greeting hoi' ${toplevel}/etc/systemd/system/foo.service >/dev/null + + grep -F 'ExecStart=${hello}/bin/hello --greeting hoi' ${toplevel}/etc/systemd/system/bar.service >/dev/null + grep -F 'X-Bar=lol crossbar whatever' ${toplevel}/etc/systemd/system/bar.service >/dev/null + + grep 'ExecStart=${hello}/bin/hello --greeting .*database.*' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null + grep -F 'RestartSec=42' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null + + [[ ! -e ${toplevel}/etc/systemd/system/foo.socket ]] + [[ ! -e ${toplevel}/etc/systemd/system/bar.socket ]] + [[ ! -e ${toplevel}/etc/systemd/system/bar-db.socket ]] + ) + echo 🐬👍 + touch $out + '' diff --git a/nixos/modules/system/service/systemd/user.nix b/nixos/modules/system/service/systemd/user.nix new file mode 100644 index 0000000000000..514731233b09f --- /dev/null +++ b/nixos/modules/system/service/systemd/user.nix @@ -0,0 +1,3 @@ +# TBD, analogous to system.nix but for user units +{ +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index be077353de00c..41cbc8d658d7b 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -42,6 +42,15 @@ let featureFlags.minimalModules = {}; }; evalMinimalConfig = module: nixosLib.evalModules { modules = [ module ]; }; + evalSystem = module: + import ../lib/eval-config.nix { + system = null; + modules = [ + ../modules/misc/nixpkgs/read-only.nix + { nixpkgs.pkgs = pkgs; } + module + ]; + }; inherit (rec { @@ -618,6 +627,7 @@ in { mjolnir = handleTest ./matrix/mjolnir.nix {}; mobilizon = handleTest ./mobilizon.nix {}; mod_perl = handleTest ./mod_perl.nix {}; + modularService = pkgs.callPackage ../modules/system/service/systemd/test.nix { inherit evalSystem; }; molly-brown = handleTest ./molly-brown.nix {}; mollysocket = handleTest ./mollysocket.nix { }; monado = handleTest ./monado.nix {}; From cd93c596365171122edf1c1e51c53c472fc2d03f Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 8 Jan 2025 17:09:34 +0100 Subject: [PATCH 2/6] format --- .../system/service/portable/service.nix | 33 +++++--- .../modules/system/service/portable/test.nix | 18 ++-- .../system/service/systemd/service.nix | 36 ++++---- .../modules/system/service/systemd/system.nix | 82 ++++++++++--------- nixos/modules/system/service/systemd/test.nix | 73 +++++++++++------ 5 files changed, 138 insertions(+), 104 deletions(-) diff --git a/nixos/modules/system/service/portable/service.nix b/nixos/modules/system/service/portable/service.nix index 76c0441ab3a0e..58772320abb6a 100644 --- a/nixos/modules/system/service/portable/service.nix +++ b/nixos/modules/system/service/portable/service.nix @@ -1,15 +1,20 @@ -{ lib, config, options, ... }: +{ + lib, + config, + options, + ... +}: let inherit (lib) mkOption types; pathOrStr = types.coercedTo types.path (x: "${x}") types.str; program = - types.coercedTo - (types.package // { + types.coercedTo ( + types.package + // { # require mainProgram for this conversion - check = v: v.type or null == "derivation" && v?meta.mainProgram; - }) - lib.getExe - pathOrStr + check = v: v.type or null == "derivation" && v ? meta.mainProgram; + } + ) lib.getExe pathOrStr // { description = "main program, path or command"; descriptionClass = "conjunction"; @@ -18,11 +23,13 @@ in { options = { services = mkOption { - type = types.attrsOf (types.submoduleWith { - modules = [ - ./service.nix - ]; - }); + type = types.attrsOf ( + types.submoduleWith { + modules = [ + ./service.nix + ]; + } + ); description = '' A collection of [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured in one go. @@ -44,7 +51,7 @@ in description = '' Arguments to pass to the `executable`. ''; - default = []; + default = [ ]; }; }; }; diff --git a/nixos/modules/system/service/portable/test.nix b/nixos/modules/system/service/portable/test.nix index 82f2cdba16287..b4a29122fca79 100644 --- a/nixos/modules/system/service/portable/test.nix +++ b/nixos/modules/system/service/portable/test.nix @@ -32,7 +32,9 @@ let }; service3 = { process = { - executable = dummyPkg "cowsay-ng" // { meta.mainProgram = "cowsay"; }; + executable = dummyPkg "cowsay-ng" // { + meta.mainProgram = "cowsay"; + }; args = [ "!" ]; }; }; @@ -43,12 +45,14 @@ let modules = [ { options.services = mkOption { - type = types.attrsOf (types.submoduleWith { - class = "service"; - modules = [ - ./service.nix - ]; - }); + type = types.attrsOf ( + types.submoduleWith { + class = "service"; + modules = [ + ./service.nix + ]; + } + ); }; } exampleConfig diff --git a/nixos/modules/system/service/systemd/service.nix b/nixos/modules/system/service/systemd/service.nix index 01698f784e44b..bf5473e04caf3 100644 --- a/nixos/modules/system/service/systemd/service.nix +++ b/nixos/modules/system/service/systemd/service.nix @@ -5,8 +5,8 @@ in { imports = [ ../portable/service.nix - (lib.mkAliasOptionModule ["systemd" "service"] ["systemd" "services" ""]) - (lib.mkAliasOptionModule ["systemd" "socket"] ["systemd" "sockets" ""]) + (lib.mkAliasOptionModule [ "systemd" "service" ] [ "systemd" "services" "" ]) + (lib.mkAliasOptionModule [ "systemd" "socket" ] [ "systemd" "sockets" "" ]) ]; options = { systemd.services = mkOption { @@ -19,14 +19,14 @@ in This means that the module has not been combined with the system configuration yet, no values can be read from this option. What you can do instead is define a module that reads from the module arguments (such as `config`) that are available when the module is merged into the system configuration. ''; - type = - types.lazyAttrsOf - (types.deferredModuleWith { - staticModules = [ - # TODO: Add modules for the purpose of generating documentation? - ]; - }); - default = {}; + type = types.lazyAttrsOf ( + types.deferredModuleWith { + staticModules = [ + # TODO: Add modules for the purpose of generating documentation? + ]; + } + ); + default = { }; }; systemd.sockets = mkOption { description = '' @@ -35,18 +35,20 @@ in See {option}`systemd.services`. ''; type = types.lazyAttrsOf types.deferredModule; - default = {}; + default = { }; }; # Also import systemd logic into sub-services # extends the portable `services` option services = mkOption { - type = types.attrsOf (types.submoduleWith { - class = "service"; - modules = [ - ./service.nix - ]; - }); + type = types.attrsOf ( + types.submoduleWith { + class = "service"; + modules = [ + ./service.nix + ]; + } + ); }; }; config = { diff --git a/nixos/modules/system/service/systemd/system.nix b/nixos/modules/system/service/systemd/system.nix index 5c6e544ea9633..24ea41d77ccaf 100644 --- a/nixos/modules/system/service/systemd/system.nix +++ b/nixos/modules/system/service/systemd/system.nix @@ -1,28 +1,34 @@ -{ lib, config, pkgs, ... }: +{ + lib, + config, + pkgs, + ... +}: let inherit (lib) concatMapAttrs mkOption types; - dash = before: after: - if after == "" - then before - else if before == "" - then after - else "${before}-${after}"; + dash = + before: after: + if after == "" then + before + else if before == "" then + after + else + "${before}-${after}"; - makeUnits = unitType: prefix: service: - concatMapAttrs - (unitName: unitModule: { - "${dash prefix unitName}" = { ... }: { + makeUnits = + unitType: prefix: service: + concatMapAttrs (unitName: unitModule: { + "${dash prefix unitName}" = + { ... }: + { imports = [ unitModule ]; }; - }) - service.systemd.${unitType} - // concatMapAttrs - (subServiceName: subService: - makeUnits unitType (dash prefix subServiceName) subService - ) - service.services; + }) service.systemd.${unitType} + // concatMapAttrs ( + subServiceName: subService: makeUnits unitType (dash prefix subServiceName) subService + ) service.services; in { # First half of the magic: mix systemd logic into the otherwise abstract services @@ -31,16 +37,18 @@ in description = '' A collection of NixOS [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured as systemd services. ''; - type = types.attrsOf (types.submoduleWith { - class = "service"; - modules = [ - ./service.nix - ]; - specialArgs = { - # perhaps: features."systemd" = { }; - inherit pkgs; - }; - }); + type = types.attrsOf ( + types.submoduleWith { + class = "service"; + modules = [ + ./service.nix + ]; + specialArgs = { + # perhaps: features."systemd" = { }; + inherit pkgs; + }; + } + ); default = { }; visible = "shallow"; }; @@ -48,18 +56,12 @@ in # Second half of the magic: siphon units that were defined in isolation to the system config = { - systemd.services = - concatMapAttrs - (serviceName: topLevelService: - makeUnits "services" serviceName topLevelService - ) - config.system.services; + systemd.services = concatMapAttrs ( + serviceName: topLevelService: makeUnits "services" serviceName topLevelService + ) config.system.services; - systemd.sockets = - concatMapAttrs - (serviceName: topLevelService: - makeUnits "sockets" serviceName topLevelService - ) - config.system.services; + systemd.sockets = concatMapAttrs ( + serviceName: topLevelService: makeUnits "sockets" serviceName topLevelService + ) config.system.services; }; } diff --git a/nixos/modules/system/service/systemd/test.nix b/nixos/modules/system/service/systemd/test.nix index 76eaebe19069d..b393763edbe53 100644 --- a/nixos/modules/system/service/systemd/test.nix +++ b/nixos/modules/system/service/systemd/test.nix @@ -1,54 +1,73 @@ # Run: # nix-build -A nixosTests.modularService -{ evalSystem, runCommand, hello, ... }: +{ + evalSystem, + runCommand, + hello, + ... +}: let - machine = evalSystem ({ lib, ... }: { + machine = evalSystem ( + { lib, ... }: + { - # Test input + # Test input - system.services.foo = { - process = { - executable = hello; - args = [ "--greeting" "hoi" ]; - }; - }; - system.services.bar = { - process = { - executable = hello; - args = [ "--greeting" "hoi" ]; - }; - systemd.service = { - serviceConfig.X-Bar = "lol crossbar whatever"; + system.services.foo = { + process = { + executable = hello; + args = [ + "--greeting" + "hoi" + ]; + }; }; - services.db = { + system.services.bar = { process = { executable = hello; - args = [ "--greeting" "Hi, I'm a database, would you believe it" ]; + args = [ + "--greeting" + "hoi" + ]; }; systemd.service = { - serviceConfig.RestartSec = "42"; + serviceConfig.X-Bar = "lol crossbar whatever"; + }; + services.db = { + process = { + executable = hello; + args = [ + "--greeting" + "Hi, I'm a database, would you believe it" + ]; + }; + systemd.service = { + serviceConfig.RestartSec = "42"; + }; }; }; - }; - # irrelevant stuff - system.stateVersion = "25.05"; - fileSystems."/".device = "/test/dummy"; - boot.loader.grub.enable = false; - }); + # irrelevant stuff + system.stateVersion = "25.05"; + fileSystems."/".device = "/test/dummy"; + boot.loader.grub.enable = false; + } + ); inherit (machine.config.system.build) toplevel; in - runCommand "test-modular-service-systemd-units" { +runCommand "test-modular-service-systemd-units" + { passthru = { inherit machine toplevel ; }; - } '' + } + '' echo ${toplevel}/etc/systemd/system/foo.service: cat -n ${toplevel}/etc/systemd/system/foo.service ( From b07d4c66b175dfe7dabeaa880dd2cc99bdd6a904 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 8 Jan 2025 15:38:25 +0100 Subject: [PATCH 3/6] Add assertions and warnings to modular services --- nixos/modules/misc/assertions.nix | 4 +- nixos/modules/system/service/portable/lib.nix | 31 +++++++ .../system/service/portable/service.nix | 5 ++ .../modules/system/service/portable/test.nix | 89 +++++++++++++++++-- .../modules/system/service/systemd/system.nix | 24 ++++- 5 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 nixos/modules/system/service/portable/lib.nix diff --git a/nixos/modules/misc/assertions.nix b/nixos/modules/misc/assertions.nix index 5853f64329c1e..ae8fa710b2b8c 100644 --- a/nixos/modules/misc/assertions.nix +++ b/nixos/modules/misc/assertions.nix @@ -32,5 +32,7 @@ }; }; - # impl of assertions is in + # impl of assertions is in + # - + # - } diff --git a/nixos/modules/system/service/portable/lib.nix b/nixos/modules/system/service/portable/lib.nix new file mode 100644 index 0000000000000..5e5b8b662b68f --- /dev/null +++ b/nixos/modules/system/service/portable/lib.nix @@ -0,0 +1,31 @@ +{ lib, ... }: +let + inherit (lib) concatLists mapAttrsToList showOption; +in +rec { + flattenMapServicesConfigToList = f: loc: config: + f loc config + ++ concatLists + (mapAttrsToList + (k: v: + flattenMapServicesConfigToList + f + (loc ++ ["services" k]) + v) + config.services); + + getWarnings = + flattenMapServicesConfigToList + (loc: config: + map (msg: "in ${showOption loc}: ${msg}") config.warnings); + + getAssertions = + flattenMapServicesConfigToList + (loc: config: + map + (ass: { + message = "in ${showOption loc}: ${ass.message}"; + assertion = ass.assertion; + }) + config.assertions); +} diff --git a/nixos/modules/system/service/portable/service.nix b/nixos/modules/system/service/portable/service.nix index 58772320abb6a..d000398b8a6ac 100644 --- a/nixos/modules/system/service/portable/service.nix +++ b/nixos/modules/system/service/portable/service.nix @@ -21,6 +21,11 @@ let }; in { + # https://nixos.org/manual/nixos/unstable/#modular-services + _class = "service"; + imports = [ + ../../../misc/assertions.nix + ]; options = { services = mkOption { type = types.attrsOf ( diff --git a/nixos/modules/system/service/portable/test.nix b/nixos/modules/system/service/portable/test.nix index b4a29122fca79..7901ea6965e98 100644 --- a/nixos/modules/system/service/portable/test.nix +++ b/nixos/modules/system/service/portable/test.nix @@ -5,6 +5,8 @@ let inherit (lib) mkOption types; + portable-lib = import ./lib.nix { inherit lib; }; + dummyPkg = name: derivation { @@ -21,6 +23,15 @@ let executable = "/usr/bin/echo"; # *giggles* args = [ "hello" ]; }; + assertions = [ + { + assertion = false; + message = "you can't enable this for that reason"; + } + ]; + warnings = [ + "The `foo' service is deprecated and will go away soon!" + ]; }; service2 = { process = { @@ -32,10 +43,25 @@ let }; service3 = { process = { - executable = dummyPkg "cowsay-ng" // { - meta.mainProgram = "cowsay"; + executable = "/bin/false"; + args = [ ]; + }; + services.exclacow = { + process = { + executable = dummyPkg "cowsay-ng" // { + meta.mainProgram = "cowsay"; + }; + args = [ "!" ]; }; - args = [ "!" ]; + assertions = [ + { + assertion = false; + message = "you can't enable this for such reason"; + } + ]; + warnings = [ + "The `bar' service is deprecated and will go away soon!" + ]; }; }; }; @@ -69,6 +95,15 @@ let args = [ "hello" ]; }; services = { }; + assertions = [ + { + assertion = false; + message = "you can't enable this for that reason"; + } + ]; + warnings = [ + "The `foo' service is deprecated and will go away soon!" + ]; }; service2 = { process = { @@ -76,17 +111,59 @@ let args = [ "world" ]; }; services = { }; + assertions = [ ]; + warnings = [ ]; }; service3 = { process = { - executable = "${dummyPkg "cowsay-ng"}/bin/cowsay"; - args = [ "!" ]; + executable = "/bin/false"; + args = [ ]; }; - services = { }; + services.exclacow = { + process = { + executable = "${dummyPkg "cowsay-ng"}/bin/cowsay"; + args = [ "!" ]; + }; + services = { }; + assertions = [ + { + assertion = false; + message = "you can't enable this for such reason"; + } + ]; + warnings = [ "The `bar' service is deprecated and will go away soon!" ]; + }; + assertions = [ ]; + warnings = [ ]; }; }; }; + assert + portable-lib.getWarnings [ "service1" ] exampleEval.config.services.service1 == [ + "in service1: The `foo' service is deprecated and will go away soon!" + ]; + + assert + portable-lib.getAssertions [ "service1" ] exampleEval.config.services.service1 == [ + { + message = "in service1: you can't enable this for that reason"; + assertion = false; + } + ]; + + assert + portable-lib.getWarnings [ "service3" ] exampleEval.config.services.service3 == [ + "in service3.services.exclacow: The `bar' service is deprecated and will go away soon!" + ]; + assert + portable-lib.getAssertions [ "service3" ] exampleEval.config.services.service3 == [ + { + message = "in service3.services.exclacow: you can't enable this for such reason"; + assertion = false; + } + ]; + "ok"; in diff --git a/nixos/modules/system/service/systemd/system.nix b/nixos/modules/system/service/systemd/system.nix index 24ea41d77ccaf..8ecc7bcd31453 100644 --- a/nixos/modules/system/service/systemd/system.nix +++ b/nixos/modules/system/service/systemd/system.nix @@ -1,12 +1,21 @@ { lib, config, + options, pkgs, ... }: let - inherit (lib) concatMapAttrs mkOption types; + inherit (lib) + concatMapAttrs + mkOption + types + concatLists + mapAttrsToList + ; + + portable-lib = import ../portable/lib.nix { inherit lib; }; dash = before: after: @@ -56,6 +65,19 @@ in # Second half of the magic: siphon units that were defined in isolation to the system config = { + + assertions = concatLists ( + mapAttrsToList ( + name: cfg: portable-lib.getAssertions (options.system.services.loc ++ [ name ]) cfg + ) config.system.services + ); + + warnings = concatLists ( + mapAttrsToList ( + name: cfg: portable-lib.getWarnings (options.system.services.loc ++ [ name ]) cfg + ) config.system.services + ); + systemd.services = concatMapAttrs ( serviceName: topLevelService: makeUnits "services" serviceName topLevelService ) config.system.services; From 30c57d53bcba05f37eaebea761c25f81fcd6371e Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 8 Jan 2025 17:10:49 +0100 Subject: [PATCH 4/6] format --- nixos/modules/system/service/portable/lib.nix | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/nixos/modules/system/service/portable/lib.nix b/nixos/modules/system/service/portable/lib.nix index 5e5b8b662b68f..0529f81f8561f 100644 --- a/nixos/modules/system/service/portable/lib.nix +++ b/nixos/modules/system/service/portable/lib.nix @@ -3,29 +3,31 @@ let inherit (lib) concatLists mapAttrsToList showOption; in rec { - flattenMapServicesConfigToList = f: loc: config: + flattenMapServicesConfigToList = + f: loc: config: f loc config - ++ concatLists - (mapAttrsToList - (k: v: - flattenMapServicesConfigToList - f - (loc ++ ["services" k]) - v) - config.services); + ++ concatLists ( + mapAttrsToList ( + k: v: + flattenMapServicesConfigToList f ( + loc + ++ [ + "services" + k + ] + ) v + ) config.services + ); - getWarnings = - flattenMapServicesConfigToList - (loc: config: - map (msg: "in ${showOption loc}: ${msg}") config.warnings); + getWarnings = flattenMapServicesConfigToList ( + loc: config: map (msg: "in ${showOption loc}: ${msg}") config.warnings + ); - getAssertions = - flattenMapServicesConfigToList - (loc: config: - map - (ass: { - message = "in ${showOption loc}: ${ass.message}"; - assertion = ass.assertion; - }) - config.assertions); + getAssertions = flattenMapServicesConfigToList ( + loc: config: + map (ass: { + message = "in ${showOption loc}: ${ass.message}"; + assertion = ass.assertion; + }) config.assertions + ); } From 398f612640aee23a5aa6e895e2f701d6929ee178 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 8 Jan 2025 14:42:51 +0100 Subject: [PATCH 5/6] ghostunnel.services.default: init --- nixos/tests/all-tests.nix | 1 + nixos/tests/ghostunnel-modular.nix | 120 +++++++++++++ pkgs/by-name/gh/ghostunnel/package.nix | 6 + pkgs/by-name/gh/ghostunnel/service.nix | 229 +++++++++++++++++++++++++ 4 files changed, 356 insertions(+) create mode 100644 nixos/tests/ghostunnel-modular.nix create mode 100644 pkgs/by-name/gh/ghostunnel/service.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 41cbc8d658d7b..fa2b8c88cfa37 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -390,6 +390,7 @@ in { gerrit = handleTest ./gerrit.nix {}; geth = handleTest ./geth.nix {}; ghostunnel = handleTest ./ghostunnel.nix {}; + ghostunnel-modular = runTest ./ghostunnel-modular.nix; gitdaemon = handleTest ./gitdaemon.nix {}; gitea = handleTest ./gitea.nix { giteaPackage = pkgs.gitea; }; github-runner = handleTest ./github-runner.nix {}; diff --git a/nixos/tests/ghostunnel-modular.nix b/nixos/tests/ghostunnel-modular.nix new file mode 100644 index 0000000000000..a1f17bc034005 --- /dev/null +++ b/nixos/tests/ghostunnel-modular.nix @@ -0,0 +1,120 @@ +{ hostPkgs, lib, ... }: +{ + _class = "nixosTest"; + name = "ghostunnel"; + nodes = { + backend = + { pkgs, ... }: + { + services.nginx.enable = true; + services.nginx.virtualHosts."backend".root = pkgs.runCommand "webroot" { } '' + mkdir $out + echo hi >$out/hi.txt + ''; + networking.firewall.allowedTCPPorts = [ 80 ]; + }; + service = + { pkgs, ... }: + { + system.services."ghostunnel-plain-old" = { + imports = [ pkgs.ghostunnel.services.default ]; + ghostunnel = { + listen = "0.0.0.0:443"; + cert = "/root/service-cert.pem"; + key = "/root/service-key.pem"; + disableAuthentication = true; + target = "backend:80"; + unsafeTarget = true; + }; + }; + system.services."ghostunnel-client-cert" = { + imports = [ pkgs.ghostunnel.services.default ]; + ghostunnel = { + listen = "0.0.0.0:1443"; + cert = "/root/service-cert.pem"; + key = "/root/service-key.pem"; + cacert = "/root/ca.pem"; + target = "backend:80"; + allowCN = [ "client" ]; + unsafeTarget = true; + }; + }; + networking.firewall.allowedTCPPorts = [ + 443 + 1443 + ]; + }; + client = + { pkgs, ... }: + { + environment.systemPackages = [ + pkgs.curl + ]; + }; + }; + + testScript = '' + + # prepare certificates + + def cmd(command): + print(f"+{command}") + r = os.system(command) + if r != 0: + raise Exception(f"Command {command} failed with exit code {r}") + + # Create CA + cmd("${hostPkgs.openssl}/bin/openssl genrsa -out ca-key.pem 4096") + cmd("${hostPkgs.openssl}/bin/openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -subj '/C=NL/ST=Zuid-Holland/L=The Hague/O=Stevige Balken en Planken B.V./OU=OpSec/CN=Certificate Authority' -out ca.pem") + + # Create service + cmd("${hostPkgs.openssl}/bin/openssl genrsa -out service-key.pem 4096") + cmd("${hostPkgs.openssl}/bin/openssl req -subj '/CN=service' -sha256 -new -key service-key.pem -out service.csr") + cmd("echo subjectAltName = DNS:service,IP:127.0.0.1 >> extfile.cnf") + cmd("echo extendedKeyUsage = serverAuth >> extfile.cnf") + cmd("${hostPkgs.openssl}/bin/openssl x509 -req -days 365 -sha256 -in service.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out service-cert.pem -extfile extfile.cnf") + + # Create client + cmd("${hostPkgs.openssl}/bin/openssl genrsa -out client-key.pem 4096") + cmd("${hostPkgs.openssl}/bin/openssl req -subj '/CN=client' -new -key client-key.pem -out client.csr") + cmd("echo extendedKeyUsage = clientAuth > extfile-client.cnf") + cmd("${hostPkgs.openssl}/bin/openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile extfile-client.cnf") + + cmd("ls -al") + + start_all() + + # Configuration + service.copy_from_host("ca.pem", "/root/ca.pem") + service.copy_from_host("service-cert.pem", "/root/service-cert.pem") + service.copy_from_host("service-key.pem", "/root/service-key.pem") + client.copy_from_host("ca.pem", "/root/ca.pem") + client.copy_from_host("service-cert.pem", "/root/service-cert.pem") + client.copy_from_host("client-cert.pem", "/root/client-cert.pem") + client.copy_from_host("client-key.pem", "/root/client-key.pem") + + backend.wait_for_unit("nginx.service") + service.wait_for_unit("multi-user.target") + service.wait_for_unit("multi-user.target") + client.wait_for_unit("multi-user.target") + + # Check assumptions before the real test + client.succeed("bash -c 'diff <(curl -v --no-progress-meter http://backend/hi.txt) <(echo hi)'") + + # Plain old simple TLS can connect, ignoring cert + client.succeed("bash -c 'diff <(curl -v --no-progress-meter --insecure https://service/hi.txt) <(echo hi)'") + + # Plain old simple TLS provides correct signature with its cert + client.succeed("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service/hi.txt) <(echo hi)'") + + # Client can authenticate with certificate + client.succeed("bash -c 'diff <(curl -v --no-progress-meter --cert /root/client-cert.pem --key /root/client-key.pem --cacert /root/ca.pem https://service:1443/hi.txt) <(echo hi)'") + + # Client must authenticate with certificate + client.fail("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service:1443/hi.txt) <(echo hi)'") + ''; + + meta.maintainers = with lib.maintainers; [ + roberth + ]; +} diff --git a/pkgs/by-name/gh/ghostunnel/package.nix b/pkgs/by-name/gh/ghostunnel/package.nix index e5946c5c682f4..665184d1415f4 100644 --- a/pkgs/by-name/gh/ghostunnel/package.nix +++ b/pkgs/by-name/gh/ghostunnel/package.nix @@ -4,6 +4,7 @@ fetchFromGitHub, lib, nixosTests, + ghostunnel, }: buildGoModule rec { @@ -33,6 +34,11 @@ buildGoModule rec { podman = nixosTests.podman-tls-ghostunnel; }; + passthru.services.default = { + imports = [ ./service.nix ]; + ghostunnel.package = ghostunnel; # FIXME: finalAttrs.finalPackage + }; + meta = with lib; { broken = stdenv.hostPlatform.isDarwin; description = "TLS proxy with mutual authentication support for securing non-TLS backend applications"; diff --git a/pkgs/by-name/gh/ghostunnel/service.nix b/pkgs/by-name/gh/ghostunnel/service.nix new file mode 100644 index 0000000000000..dbc49478a6cb8 --- /dev/null +++ b/pkgs/by-name/gh/ghostunnel/service.nix @@ -0,0 +1,229 @@ +{ + lib, + config, + options, + pkgs, + ... +}: +let + inherit (lib) + concatStringsSep + escapeShellArg + mkDefault + mkIf + mkOption + optional + types + ; + cfg = config.ghostunnel; + +in +{ + # https://nixos.org/manual/nixos/unstable/#modular-services + _class = "service"; + options = { + ghostunnel = { + package = mkOption { + description = "Package to use for ghostunnel"; + type = types.package; + }; + + listen = mkOption { + description = '' + Address and port to listen on (can be HOST:PORT, unix:PATH). + ''; + type = types.str; + }; + + target = mkOption { + description = '' + Address to forward connections to (can be HOST:PORT or unix:PATH). + ''; + type = types.str; + }; + + keystore = mkOption { + description = '' + Path to keystore (combined PEM with cert/key, or PKCS12 keystore). + + NB: storepass is not supported because it would expose credentials via `/proc/*/cmdline`. + + Specify this or `cert` and `key`. + ''; + type = types.nullOr types.str; + default = null; + }; + + cert = mkOption { + description = '' + Path to certificate (PEM with certificate chain). + + Not required if `keystore` is set. + ''; + type = types.nullOr types.str; + default = null; + }; + + key = mkOption { + description = '' + Path to certificate private key (PEM with private key). + + Not required if `keystore` is set. + ''; + type = types.nullOr types.str; + default = null; + }; + + cacert = mkOption { + description = '' + Path to CA bundle file (PEM/X509). Uses system trust store if `null`. + ''; + type = types.nullOr types.str; + }; + + disableAuthentication = mkOption { + description = '' + Disable client authentication, no client certificate will be required. + ''; + type = types.bool; + default = false; + }; + + allowAll = mkOption { + description = '' + If true, allow all clients, do not check client cert subject. + ''; + type = types.bool; + default = false; + }; + + allowCN = mkOption { + description = '' + Allow client if common name appears in the list. + ''; + type = types.listOf types.str; + default = [ ]; + }; + + allowOU = mkOption { + description = '' + Allow client if organizational unit name appears in the list. + ''; + type = types.listOf types.str; + default = [ ]; + }; + + allowDNS = mkOption { + description = '' + Allow client if DNS subject alternative name appears in the list. + ''; + type = types.listOf types.str; + default = [ ]; + }; + + allowURI = mkOption { + description = '' + Allow client if URI subject alternative name appears in the list. + ''; + type = types.listOf types.str; + default = [ ]; + }; + + extraArguments = mkOption { + description = "Extra arguments to pass to `ghostunnel server` (shell syntax)"; + type = types.separatedString " "; + default = ""; + }; + + unsafeTarget = mkOption { + description = '' + If set, does not limit target to localhost, 127.0.0.1, [::1], or UNIX sockets. + + This is meant to protect against accidental unencrypted traffic on + untrusted networks. + ''; + type = types.bool; + default = false; + }; + }; + }; + + config = { + assertions = [ + { + message = '' + At least one access control flag is required. + Set at least one of: + - ${options.ghostunnel.disableAuthentication} + - ${options.ghostunnel.allowAll} + - ${options.ghostunnel.allowCN} + - ${options.ghostunnel.allowOU} + - ${options.ghostunnel.allowDNS} + - ${options.ghostunnel.allowURI} + ''; + assertion = + cfg.disableAuthentication + || cfg.allowAll + || cfg.allowCN != [ ] + || cfg.allowOU != [ ] + || cfg.allowDNS != [ ] + || cfg.allowURI != [ ]; + } + ]; + + ghostunnel = { + # Clients should not be authenticated with the public root certificates + # (afaict, it doesn't make sense), so we only provide that default when + # client cert auth is disabled. + cacert = mkIf cfg.disableAuthentication (mkDefault null); + }; + + # TODO assertions + + process = { + executable = pkgs.writeScriptBin "run-ghostunnel" '' + #!${pkgs.runtimeShell} + exec ${lib.getExe cfg.package} ${ + concatStringsSep " " ( + optional (cfg.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore" + ++ optional (cfg.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert" + ++ optional (cfg.key != null) "--key=$CREDENTIALS_DIRECTORY/key" + ++ optional (cfg.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert" + ++ [ + "server" + "--listen" + cfg.listen + "--target" + cfg.target + ] + ++ optional cfg.allowAll "--allow-all" + ++ map (v: "--allow-cn=${escapeShellArg v}") cfg.allowCN + ++ map (v: "--allow-ou=${escapeShellArg v}") cfg.allowOU + ++ map (v: "--allow-dns=${escapeShellArg v}") cfg.allowDNS + ++ map (v: "--allow-uri=${escapeShellArg v}") cfg.allowURI + ++ optional cfg.disableAuthentication "--disable-authentication" + ++ optional cfg.unsafeTarget "--unsafe-target" + ++ [ cfg.extraArguments ] + ) + } + ''; + }; + + # refine the service + systemd.service = { + after = [ "network.target" ]; + wants = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Restart = "always"; + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + DynamicUser = true; + LoadCredential = + optional (cfg.keystore != null) "keystore:${cfg.keystore}" + ++ optional (cfg.cert != null) "cert:${cfg.cert}" + ++ optional (cfg.key != null) "key:${cfg.key}" + ++ optional (cfg.cacert != null) "cacert:${cfg.cacert}"; + }; + }; + }; +} From 1ee1ac47cc528474fecba733adab4b436b12b79f Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 8 Jan 2025 16:22:49 +0100 Subject: [PATCH 6/6] nixos/doc: Add modular services section --- nixos/doc/manual/default.nix | 28 ++++++ nixos/doc/manual/development/development.md | 1 + .../manual/development/modular-services.md | 91 +++++++++++++++++++ nixos/doc/manual/redirects.json | 18 ++++ .../src/nixos_render_docs/redirects.py | 2 +- 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 nixos/doc/manual/development/modular-services.md diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix index 5b171420b24d4..56e0ce5826eb0 100644 --- a/nixos/doc/manual/default.nix +++ b/nixos/doc/manual/default.nix @@ -13,6 +13,7 @@ let inherit (pkgs) buildPackages runCommand docbook_xsl_ns; inherit (pkgs.lib) + evalModules hasPrefix removePrefix flip @@ -97,8 +98,35 @@ let ${testOptionsDoc.optionsJSON}/${common.outputPath}/options.json sed -e '/@PYTHON_MACHINE_METHODS@/ {' -e 'r ${testDriverMachineDocstrings}/machine-methods.md' -e 'd' -e '}' \ -i ./development/writing-nixos-tests.section.md + substituteInPlace ./development/modular-services.md \ + --replace-fail \ + '@PORTABLE_SERVICE_OPTIONS@' \ + ${portableServiceOptions.optionsJSON}/${common.outputPath}/options.json + substituteInPlace ./development/modular-services.md \ + --replace-fail \ + '@SYSTEMD_SERVICE_OPTIONS@' \ + ${systemdServiceOptions.optionsJSON}/${common.outputPath}/options.json ''; + portableServiceOptions = buildPackages.nixosOptionsDoc { + inherit (evalModules { modules = [ ../../modules/system/service/portable/service.nix ]; }) options; + inherit revision warningsAreErrors; + transformOptions = opt: opt // { + # Clean up declaration sites to not refer to the NixOS source tree. + declarations = map stripAnyPrefixes opt.declarations; + }; + }; + + systemdServiceOptions = buildPackages.nixosOptionsDoc { + inherit (evalModules { modules = [ ../../modules/system/service/systemd/service.nix ]; }) options; + # TODO: filter out options that are not systemd-specific, maybe also change option prefix to just `service-opt-`? + inherit revision warningsAreErrors; + transformOptions = opt: opt // { + # Clean up declaration sites to not refer to the NixOS source tree. + declarations = map stripAnyPrefixes opt.declarations; + }; + }; + in rec { inherit (optionsDoc) optionsJSON optionsNix optionsDocBook; diff --git a/nixos/doc/manual/development/development.md b/nixos/doc/manual/development/development.md index 76f405c3b29cc..37762afb41dab 100644 --- a/nixos/doc/manual/development/development.md +++ b/nixos/doc/manual/development/development.md @@ -12,4 +12,5 @@ writing-documentation.chapter.md nixos-tests.chapter.md developing-the-test-driver.chapter.md testing-installer.chapter.md +modular-services.md ``` diff --git a/nixos/doc/manual/development/modular-services.md b/nixos/doc/manual/development/modular-services.md new file mode 100644 index 0000000000000..d0443081a4880 --- /dev/null +++ b/nixos/doc/manual/development/modular-services.md @@ -0,0 +1,91 @@ + +# Modular Services {#modular-services} + +Status: in development. This functionality is new in NixOS 25.05, and significant changes should be expected. We'd love to hear your feedback in + +Traditionally, NixOS services were defined using sets of options *in* modules, not *as* modules. This made them non-modular, resulting in problems with composability, reuse, and portability. + +A *modular service* is a [module] that defines values for a core set of options, including which program to run. + +NixOS provides two options into which such modules can be plugged: + +- `system.services.` +- an option for user services (TBD) + +Crucially, these options have the type [`attrsOf`] [`submodule`]. +The name of the service is the attribute name corresponding to `attrsOf`. + +The `submodule` is pre-loaded with two modules: +- a generic module that is intended to be portable +- a module with systemd-specific options, whose values or defaults derive from the generic module's option values. + +So note that the default value of `system.services.` is not a complete service. It requires that the user provide a value, and this is typically done by importing a module. For example: + + +```nix +{ + system.services.httpd = { + imports = [ nixpkgs.modules.services.foo ]; + foo.settings = { + # ... + }; + }; +} +``` + +## Portability {#modular-service-portability} + +It is possible to write service modules that are portable. This is done by either avoiding the `systemd` option tree, or by defining process-manager-specific definitions in an optional way: + +```nix +{ config, options, lib, ... }: { + _class = "service"; + config = { + process.executable = "${lib.getExe config.foo.program}"; + } // lib.optionalAttrs (options?systemd) { + # ... systemd-specific definitions ... + }; +} +``` + +This way, the module can be loaded into a configuration manager that does not use systemd, and the `systemd` definitions will be ignored. +Similarly, other configuration managers can declare their own options for services to customize. + +## Composition and Ownership {#modular-service-composition} + +Compared to traditional services, modular services are inherently more composable, by virtue of being modules and receiving a user-provided name when imported. +However, composition can not end there, because services need to be able to interact with each other. +This can be achieved in two ways: +1. Users can link services together by providing the necessary NixOS configuration. +2. Services can be compositions of other services. + +These aren't mutually exclusive. In fact, it is a good practice when developing services to first write them as individual services, and then compose them into a higher-level composition. Each of these services is a valid modular service, including their composition. + +## Migration {#modular-service-migration} + +Many services could be migrated to the modular service system, but even when the modular service system is mature, it is not necessary to migrate all services. +For instance, many system-wide services are a mandatory part of a desktop system, and it doesn't make sense to have multiple instances of them. +Moving their logic into separate Nix files may still be beneficial for the efficient evaluation of configurations that don't use those services, but that is a rather minor benefit, unless modular services potentially become the standard way to define services. + + + +## Portable Service Options {#modular-service-options-portable} + +```{=include=} options +id-prefix: service-opt- +list-id: service-options +source: @PORTABLE_SERVICE_OPTIONS@ +``` + +## Systemd-specific Service Options {#modular-service-options-systemd} + +```{=include=} options +id-prefix: systemd-service-opt- +list-id: systemd-service-options +source: @SYSTEMD_SERVICE_OPTIONS@ +``` + +[module]: https://nixos.org/manual/nixpkgs/stable/index.html#module-system + +[`attrsOf`]: #sec-option-types-composed +[`submodule`]: #sec-option-types-submodule diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index a7ebb49b62b8a..1a3ce92bf7fd4 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -2,6 +2,24 @@ "book-nixos-manual": [ "index.html#book-nixos-manual" ], + "modular-service-composition": [ + "index.html#modular-service-composition" + ], + "modular-service-migration": [ + "index.html#modular-service-migration" + ], + "modular-service-options-portable": [ + "index.html#modular-service-options-portable" + ], + "modular-service-options-systemd": [ + "index.html#modular-service-options-systemd" + ], + "modular-service-portability": [ + "index.html#modular-service-portability" + ], + "modular-services": [ + "index.html#modular-services" + ], "module-services-crab-hole": [ "index.html#module-services-crab-hole" ], diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py index 1a891a1af238e..554295aff50b9 100644 --- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py +++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py @@ -114,7 +114,7 @@ def validate(self, initial_xref_targets: dict[str, XrefTarget]): - The first element of an identifier's redirects list must denote its current location. """ xref_targets = {} - ignored_identifier_patterns = ("opt-", "auto-generated-", "function-library-") + ignored_identifier_patterns = ("opt-", "auto-generated-", "function-library-", "service-opt-", "systemd-service-opt") for id, target in initial_xref_targets.items(): # filter out automatically generated identifiers from module options and library documentation if id.startswith(ignored_identifier_patterns):