diff --git a/README.md b/README.md index 754f2b3..29b5049 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # finix - modular services -Support for [NixOS modular services](https://nixos.org/manual/nixos/unstable/#modular-services) for `finix`. +Support for [Modular services](https://nixos.org/manual/nixos/unstable/#modular-services) for `finix`. There is a Matrix room for discussions: diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..89a8a7a --- /dev/null +++ b/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "finix": { + "locked": { + "lastModified": 1788403395, + "narHash": "sha256-nqsj0NmmYJ1aFj6hHUFQuKnyHSjqyMxcBNI99mMbrfw=", + "owner": "finix-community", + "repo": "finix", + "rev": "f765fafde5e21b9f456827d1bf84a42417abe67d", + "type": "github" + }, + "original": { + "owner": "finix-community", + "repo": "finix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1788404924, + "narHash": "sha256-lhEhY8X5EgkQ/eg6IFz4cc8jRuSYSvnxx1al7d1dvZ0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0968519e14f7aa7d3e9b389682bd74d2b51c8ce8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "finix": "finix", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 4bad867..64c726d 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,79 @@ { - description = "Support for NixOS modular services in finix"; - - outputs = _: { - nixosModules.default = import ./modules; + description = "Support for Modular services in finix"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + finix.url = "github:finix-community/finix"; }; + + outputs = + { + self, + nixpkgs, + finix, + }: + let + finixSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + forAllFinixSystems = + f: + nixpkgs.lib.genAttrs finixSystems ( + system: + let + pkgs = import nixpkgs { inherit system; }; + in + f pkgs system + ); + in + { + nixosModules.default = import ./modules/service.nix; + + checks = forAllFinixSystems ( + pkgs: system: { + default = pkgs.testers.modularServiceCompliance { + callReload = path: "initctl reload ${path}"; + sharedDir = ".finix-test"; + evalConfig = + { services, ... }: + let + machine = finix.lib.finixSystem { + inherit (pkgs) lib; + modules = [ + { + nixpkgs.pkgs = nixpkgs.lib.mkDefault pkgs; + } + { + system.services = services; + } + self.nixosModules.default + ]; + }; + in + { + config = machine.config.system.services; + checkDrv = machine.config.system.build.toplevel; + }; + mkTest = + { + name, + services, + testExe, + }: + finix.lib.mkTest { + inherit name; + nodes.machine = { + imports = [ self.nixosModules.default ]; + system.services = services; + }; + testScript = '' + machine.wait_for_condition("service/syslogd/ready") + machine.succeed("${testExe}") + ''; + }; + }; + } + ); + }; } diff --git a/modules/default.nix b/modules/default.nix deleted file mode 100644 index 90f213a..0000000 --- a/modules/default.nix +++ /dev/null @@ -1,47 +0,0 @@ -{ - config, - options, - pkgs, - lib, - ... -}: -let - portable-lib = import ./portable/lib.nix { inherit lib; }; - - modularServiceConfiguration = portable-lib.configure { - serviceManagerPkgs = pkgs; - extraRootModules = [ - ./finit/service.nix - ]; - }; -in -{ - imports = [ - ./finit/system.nix - ]; - - options = { - system.services = lib.mkOption { - type = lib.types.attrsOf modularServiceConfiguration.serviceSubmodule; - default = { }; - description = '' - A collection of modular services. - ''; - visible = "shallow"; - }; - }; - - config = { - assertions = lib.concatLists ( - lib.mapAttrsToList ( - name: cfg: portable-lib.getAssertions (options.system.services.loc ++ [ name ]) cfg - ) config.system.services - ); - - warnings = lib.concatLists ( - lib.mapAttrsToList ( - name: cfg: portable-lib.getWarnings (options.system.services.loc ++ [ name ]) cfg - ) config.system.services - ); - }; -} diff --git a/modules/finit/service.nix b/modules/finit/service.nix deleted file mode 100644 index cea0143..0000000 --- a/modules/finit/service.nix +++ /dev/null @@ -1,33 +0,0 @@ -{ config, lib, ... }: -{ - imports = [ (lib.mkAliasOptionModule [ "finit" "service" ] [ "finit" "services" "" ]) ]; - - options = { - finit.services = lib.mkOption { - type = with lib.types; lazyAttrsOf deferredModule; - default = { }; - description = '' - This module configures Finit services. - ''; - }; - - # Import this logic into sub-services also. - # Extends the portable `services` option. - services = lib.mkOption { - type = lib.types.attrsOf ( - lib.types.submoduleWith { - class = "service"; - modules = [ - ./service.nix - ]; - } - ); - }; - }; - - config = { - finit.services."" = { - command = lib.escapeShellArgs config.process.argv; - }; - }; -} diff --git a/modules/finit/system.nix b/modules/finit/system.nix deleted file mode 100644 index afbc20c..0000000 --- a/modules/finit/system.nix +++ /dev/null @@ -1,29 +0,0 @@ -{ config, lib, ... }: -let - makeServices = - prefixes: service: - lib.concatMapAttrs ( - name: module: - let - label = if name == "" then prefixes else prefixes ++ [ name ]; - in - { - "${lib.concatStringsSep "-" label}" = - { ... }: - { - imports = [ module ]; - }; - } - ) service.finit.services - // lib.concatMapAttrs ( - subServiceName: subService: makeServices (prefixes ++ [ subServiceName ]) subService - ) service.services; -in -{ - # Assert Finit services for those defined in isolation to the system. - config = { - finit.services = lib.concatMapAttrs ( - topLevelName: topLevelService: makeServices [ topLevelName ] topLevelService - ) config.system.services; - }; -} diff --git a/modules/portable/assertions.nix b/modules/portable/assertions.nix deleted file mode 100644 index c904625..0000000 --- a/modules/portable/assertions.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ lib, ... }: -{ - options = { - assertions = lib.mkOption { - type = with lib.types; listOf unspecified; - internal = true; - default = [ ]; - example = [ - { - assertion = false; - message = "you can't enable this for that reason"; - } - ]; - description = '' - This option allows modules to express conditions that must - hold for the evaluation of the system configuration to - succeed, along with associated error messages for the user. - ''; - }; - - warnings = lib.mkOption { - internal = true; - default = [ ]; - type = with lib.types; listOf str; - example = [ "The `foo' service is deprecated and will go away soon!" ]; - description = '' - This option allows modules to show warnings to users during - the evaluation of the system configuration. - ''; - }; - }; -} diff --git a/modules/portable/config-data-item.nix b/modules/portable/config-data-item.nix deleted file mode 100644 index 6ae7eb1..0000000 --- a/modules/portable/config-data-item.nix +++ /dev/null @@ -1,64 +0,0 @@ -# This file is a function that returns a module. -pkgs: -{ - lib, - name, - config, - options, - ... -}: -let - inherit (lib) mkOption types; -in -{ - options = { - enable = mkOption { - type = types.bool; - default = true; - description = '' - Whether this configuration file should be generated. - This option allows specific configuration files to be disabled. - ''; - }; - - name = mkOption { - type = types.str; - description = '' - Name of the configuration file (relative to the service's configuration directory). Defaults to the attribute name. - ''; - }; - - path = mkOption { - type = types.str; - readOnly = true; - description = '' - The actual path where this configuration file will be available. - This is determined by the service manager implementation. - - On NixOS it is an absolute path. - Other service managers may provide a relative path, in order to be unprivileged and/or relocatable. - ''; - }; - - text = mkOption { - default = null; - type = types.nullOr types.lines; - description = "Text content of the configuration file."; - }; - - source = mkOption { - type = types.path; - description = "Path of the source file."; - }; - }; - - config = { - name = lib.mkDefault name; - source = lib.mkIf (config.text != null) ( - let - name' = "service-configdata-" + lib.replaceStrings [ "/" ] [ "-" ] name; - in - lib.mkDerivedConfig options.text (pkgs.writeText name') - ); - }; -} diff --git a/modules/portable/config-data.nix b/modules/portable/config-data.nix deleted file mode 100644 index 870a6e2..0000000 --- a/modules/portable/config-data.nix +++ /dev/null @@ -1,45 +0,0 @@ -# Non-modular context provided by the modular services integration. -{ pkgs }: - -# Configuration data support for portable services -# This module provides configData for services, enabling configuration reloading -# without terminating and restarting the service process. -{ - lib, - ... -}: -let - inherit (lib) mkOption types; - inherit (lib.modules) importApply; -in -{ - options = { - configData = mkOption { - default = { }; - example = lib.literalExpression '' - { - "server.conf" = { - text = ''' - port = 8080 - workers = 4 - '''; - }; - "ssl/cert.pem" = { - source = ./cert.pem; - }; - } - ''; - description = '' - Configuration data files for the service - - These files are made available to the service and can be updated without restarting the service process, enabling configuration reloading. - The service manager implementation determines how these files are exposed to the service (e.g., via a specific directory path). - This path is available in the `path` sub-option for each `configData.` entry. - - This is particularly useful for services that support configuration reloading via signals (e.g., SIGHUP) or which pick up changes automatically, so that no downtime is required in order to reload the service. - ''; - - type = types.lazyAttrsOf (types.submodule (importApply ./config-data-item.nix pkgs)); - }; - }; -} diff --git a/modules/portable/lib.nix b/modules/portable/lib.nix deleted file mode 100644 index 88db80f..0000000 --- a/modules/portable/lib.nix +++ /dev/null @@ -1,77 +0,0 @@ -{ lib, ... }: -let - inherit (lib) - concatLists - mapAttrsToList - showOption - types - ; -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 - ); - - /** - This is the entrypoint for the portable part of modular services. - - It provides the various options that are consumed by service manager implementations. - - # Inputs - - `serviceManagerPkgs`: A Nixpkgs instance which will be used for built-in logic such as converting `configData..text` to a store path. - - `extraRootModules`: Modules to be loaded into the "root" service submodule, but not into its sub-`services`. That's the modules' own responsibility. - - `extraRootSpecialArgs`: Fixed module arguments that are provided in a similar manner to `extraRootModules`. - - # Output - - An attribute set. - - `serviceSubmodule`: a Module System option type which is a `submodule` with the portable modules and this function's inputs loaded into it. - */ - configure = - { - serviceManagerPkgs, - extraRootModules ? [ ], - extraRootSpecialArgs ? { }, - }: - let - modules = [ - (lib.modules.importApply ./service.nix { pkgs = serviceManagerPkgs; }) - ]; - serviceSubmodule = types.submoduleWith { - class = "service"; - modules = modules ++ extraRootModules; - specialArgs = extraRootSpecialArgs; - }; - in - { - inherit serviceSubmodule; - }; -} diff --git a/modules/portable/service.nix b/modules/portable/service.nix deleted file mode 100644 index c5dde7f..0000000 --- a/modules/portable/service.nix +++ /dev/null @@ -1,54 +0,0 @@ -# Non-module arguments -# These are separate from the module arguments to avoid implicit dependencies. -# This makes service modules self-contained, allowing mixing of Nixpkgs versions. -{ pkgs }: - -# The module -{ - lib, - ... -}: -let - inherit (lib) mkOption types; - pathOrStr = types.coercedTo types.path (x: "${x}") types.str; -in -{ - # https://nixos.org/manual/nixos/unstable/#modular-services - _class = "service"; - imports = [ - (pkgs.path + "/modules/generic/meta-maintainers.nix") - ./assertions.nix - (lib.modules.importApply ./config-data.nix { inherit pkgs; }) - ]; - options = { - services = mkOption { - type = types.attrsOf ( - types.submoduleWith { - modules = [ - (lib.modules.importApply ./service.nix { inherit pkgs; }) - ]; - } - ); - 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 = { - argv = lib.mkOption { - type = types.listOf pathOrStr; - example = lib.literalExpression ''[ (lib.getExe config.package) "--nobackground" ]''; - description = '' - Command filename and arguments for starting this service. - This is a raw command-line that should not contain any shell escaping. - If expansion of environmental variables is required then use - a shell script or `importas` from `pkgs.execline`. - ''; - }; - }; - }; -} diff --git a/modules/service.nix b/modules/service.nix new file mode 100644 index 0000000..11c7e52 --- /dev/null +++ b/modules/service.nix @@ -0,0 +1,101 @@ +{ config, lib, ... }: +let + inherit (lib) mkOption types; +in + { + imports = [ + (lib.mkAliasOptionModule [ "finit" "service" ] [ "system" "services" ]) + (lib.mkAliasOptionModule [ "finit" "services" ] [ "system" "services" ]) + ]; + + options = { + finit.services = lib.mkOption { + type = with lib.types; lazyAttrsOf deferredModule; + default = { }; + description = '' + This module configures Finit services. + ''; + }; + + finit.command = mkOption { + type = types.str; + default = lib.escapeShellArgs config.process.argv; + defaultText = lib.literalExpression "lib.escapeShellArgs config.process.argv"; + description = '' + Starting Command for finix. + + This option sets the primary starting entry, and is the way to extend the + command line derived from {option}`process.argv`. + + By default, it is set to `lib.escapeShellArgs {option}process.argv`. Because + {option}`process.argv` is already a command line (not an argument + list), it is used verbatim so that references like `$MAINPID` are preserved. + When {option}`process.argv` is unset, this option is `null` and no + `finit.command` is emitted; a service may then set + `finit.command` itself. + + To extend {option}`process.argv`, you can append + to the command line: + ```nix + finit.command = + config.process.argv + ""; + ``` + ''; + }; + + finit.reload = mkOption { + type = types.nullOr types.str; + default = if config.process.reloadCommand != null then config.process.reloadCommand else null; + defaultText = lib.literalExpression "config.process.reloadCommand"; + description = '' + Reload Command for finix. + + This option sets the primary reload entry, and is the way to extend the + command line derived from {option}`process.reloadCommand`. + + By default, it is set to {option}`process.reloadCommand`. Because + {option}`process.reloadCommand` is already a command line (not an argument + list), it is used verbatim so that references like `$MAINPID` are preserved. + When {option}`process.reloadCommand` is unset, this option is `null` and no + `finit.reload` is emitted; a service may then set + `finit.reload` itself. + + To extend {option}`process.reloadCommand`, you can append + to the command line: + ```nix + finit.reload = + config.process.reloadCommand + ""; + ``` + ''; + }; + + # Import this logic into sub-services also. + # Extends the portable `services` option. + services = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submoduleWith { + class = "service"; + modules = [ + ./service.nix + ]; + } + ); + }; + }; + + config = { + finit.services."" = { + conditions = [ "service/syslogd/ready" ]; + command = config.finit.command; + reload = config.finit.reload; + notify = + if config.notificationProtocol.systemd then + "systemd" + else if config.notificationProtocol.s6 then + "s6" + else + null; + }; + + }; +} diff --git a/modules/system.nix b/modules/system.nix new file mode 100644 index 0000000..2a3c176 --- /dev/null +++ b/modules/system.nix @@ -0,0 +1,67 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + portable-lib = import "${pkgs.path}/lib/services/lib.nix" { inherit lib; }; + modularServiceConfiguration = portable-lib.configure { + serviceManagerPkgs = pkgs; + extraRootModules = [ + ./finit/service.nix + ]; + }; + + makeServices = + prefixes: service: + lib.concatMapAttrs ( + name: module: + let + label = if name == "" then prefixes else prefixes ++ [ name ]; + in + { + "${lib.concatStringsSep "-" label}" = + { ... }: + { + imports = [ module ]; + }; + } + ) service.finit.services + // lib.concatMapAttrs ( + subServiceName: subService: makeServices (prefixes ++ [ subServiceName ]) subService + ) service.services; +in +{ + + options = { + system.services = lib.mkOption { + type = lib.types.attrsOf modularServiceConfiguration.serviceSubmodule; + default = { }; + description = '' + A collection of modular services. + ''; + visible = "shallow"; + }; + }; + + # Assert Finit services for those defined in isolation to the system. + config = { + finit.services = lib.concatMapAttrs ( + topLevelName: topLevelService: makeServices [ topLevelName ] topLevelService + ) config.system.services; + + assertions = lib.concatLists ( + lib.mapAttrsToList ( + name: cfg: portable-lib.getAssertions (config.system.services.loc ++ [ name ]) cfg + ) config.system.services + ); + + warnings = lib.concatLists ( + lib.mapAttrsToList ( + name: cfg: portable-lib.getWarnings (config.system.services.loc ++ [ name ]) cfg + ) config.system.services + ); + }; +}