diff --git a/lib/inventory/distributed-service/api-feature.nix b/lib/inventory/distributed-service/api-feature.nix index f745251f0..e5d4a8113 100644 --- a/lib/inventory/distributed-service/api-feature.nix +++ b/lib/inventory/distributed-service/api-feature.nix @@ -10,6 +10,7 @@ in { lib, config, ... }: { options.result.api = lib.mkOption { + visible = false; default = { }; type = lib.types.submodule ({ options.schema = lib.mkOption { diff --git a/lib/inventory/distributed-service/service-module.nix b/lib/inventory/distributed-service/service-module.nix index efdd5fe93..8dccc7c4e 100644 --- a/lib/inventory/distributed-service/service-module.nix +++ b/lib/inventory/distributed-service/service-module.nix @@ -26,13 +26,6 @@ let ${builtins.toJSON (lib.attrNames config.roles)} ''; - # checkInstanceSettings = - # instanceName: instanceSettings: - # let - # unmatchedRoles = 1; - # in - # unmatchedRoles; - /** Merges the role- and machine-settings using the role interface @@ -154,11 +147,29 @@ let in { options = { + # TODO: deduplicate this with inventory.instances + # Although inventory has stricter constraints instances = mkOption { + # Instances are created in the inventory + visible = false; + defaultText = "Throws: 'The service must define its instances' when not defined"; default = throw '' The clan service module ${config.manifest.name} doesn't define any instances. - Did you forget to create instances via 'inventory.instances' ? + Did you forget to create instances via 'inventory.instances'? + ''; + description = '' + Instances of the service. + + An Instance is a user-specific deployment or configuration of a service. + It represents the active usage of the service configured to the user's settings or use case. + The `` of the instance is arbitrary, but must be unique. + + A common best practice is to name the instance after the 'service' and the 'use-case'. + + For example: + + - 'instances.zerotier-homelab = ...' for a zerotier instance that connects all machines of a homelab ''; type = attrsWith { @@ -174,6 +185,21 @@ in # apply = v: lib.seq (checkInstanceSettings name v) v; # }; options.roles = mkOption { + description = '' + Roles of the instance. + + A role is a specific behavior or configuration of the service. + It defines how the service should behave in the context of this instance. + The `` must match one of the roles defined in the service + + For example: + + - 'roles.client = ...' for a client role that connects to the service + - 'roles.server = ...' for a server role that provides the service + + Throws an error if empty, since this would mean that the service has no members. + ''; + defaultText = "Throws: 'The service must define members via roles' when not defined"; default = throw '' Instance '${name}' of service '${config.manifest.name}' mut define members via 'roles'. @@ -184,26 +210,35 @@ in placeholder = "roleName"; elemType = submoduleWith { modules = [ - ( - { ... }: - { - # instances.{instanceName}.roles.{roleName}.machines - options.machines = mkOption { - type = attrsWith { - placeholder = "machineName"; - elemType = submoduleWith { - modules = [ - (m: { - options.settings = mkOption { - type = types.raw; - description = "Settings of '${name}-machine': ${m.name}."; - default = { }; - }; - }) - ]; - }; + ({ + # instances.{instanceName}.roles.{roleName}.machines + options.machines = mkOption { + description = '' + Machines of the role. + + A machine is a physical or virtual machine that is part of the instance. + The `` must match the name of any machine defined in the clan. + + For example: + + - 'machines.my-machine = { ...; }' for a machine that is part of the instance + - 'machines.my-other-machine = { ...; }' for another machine that is part of the instance + ''; + type = attrsWith { + placeholder = "machineName"; + elemType = submoduleWith { + modules = [ + (m: { + options.settings = mkOption { + type = types.raw; + description = "Settings of '${name}-machine': ${m.name or ""}."; + default = { }; + }; + }) + ]; }; }; + }; # instances.{instanceName}.roles.{roleName}.settings # options._settings = mkOption { }; @@ -242,6 +277,22 @@ in }; }; roles = mkOption { + description = '' + Roles of the service. + + A role is a specific behavior or configuration of the service. + It defines how the service should behave in the context of the clan. + + The ``s of the service are defined here. Later usage of the roles must match one of the `roleNames`. + + For example: + + - 'roles.client = ...' for a client role that connects to the service + - 'roles.server = ...' for a server role that provides the service + + Throws an error if empty, since this would mean that the service has no way of adding members. + ''; + defaultText = "Throws: 'The service must define its roles' when not defined"; default = throw '' Role behavior of service '${config.manifest.name}' must be defined. A 'clan.service' module should always define its behavior via 'roles' @@ -263,32 +314,138 @@ in in { options.interface = mkOption { + description = '' + Abstract interface of the role. + + This is an abstract module which should define 'options' for the role's settings. + + Example: + + ```nix + { + options.timeout = mkOption { + type = types.int; + default = 30; + description = "Timeout in seconds"; + }; + } + ``` + + Note: + + - `machine.config` is not available here, since the role is definition is abstract. + - *defaults* that depend on the *machine* or *instance* should be added to *settings* later in 'perInstance' or 'perMachine' + ''; type = types.deferredModule; # TODO: Default to an empty module # need to test that an the empty module can be evaluated to empty settings default = { }; }; options.perInstance = mkOption { - type = types.deferredModuleWith { - staticModules = [ - # Common output format - # As described by adr - # { nixosModule, services, ... } - ( - { ... }: + description = '' + Per-instance configuration of the role. + + This option is used to define instance-specific behavior for the service-role. (Example below) + + Although the type is a `deferredModule`, it helps to think of it as a function. + The 'function' takes the `instance-name` and some other `arguments`. + + *Arguments*: + + - `instanceName` (`string`): The name of the instance. + - `machine`: Machine information, containing: + ```nix { - options.nixosModule = mkOption { default = { }; }; - options.services = mkOption { - type = attrsWith { - placeholder = "serviceName"; - elemType = submoduleWith { - modules = [ ./service-module.nix ]; + name = "machineName"; + roles = ["client" "server" ... ]; + } + ``` + - `roles`: Attribute set of all roles of the instance, in the form: + ```nix + roles = { + client = { + machines = { + jon = { + settings = { + timeout = 60; }; }; - default = { }; + # ... }; - } - ) + settings = { + timeout = 30; + }; + }; + # ... + }; + ``` + + - `settings`: The settings of the role, as defined in `inventory` + ```nix + { + timeout = 30; + } + ``` + - `extendSettings`: A function that takes a module and returns a new module with extended settings. + ```nix + extendSettings { + timeout = mkForce 60; + }; + -> + { + timeout = 60; + } + ``` + + *Returns* an `attribute set` containing: + + - `nixosModule`: The NixOS module for the instance. + + ''; + type = types.deferredModuleWith { + staticModules = [ + ({ + options.nixosModule = mkOption { + type = types.deferredModule; + default = { }; + description = '' + This module is later imported to configure the machine with the config derived from service's settings. + + Example: + + ```nix + roles.client.perInstance = { instanceName, ... }: + { + # Keep in mind that this module is produced once per-instance + # Meaning you might end up with multiple of these modules. + # Make sure they can be imported all together without conflicts + # + # ↓ nixos-config + nixosModule = { config ,... }: { + # create one systemd service per instance + # It is a common practice to concatenate the *service-name* and *instance-name* + # To ensure globally unique systemd-units for the target machine + systemd.services."webly-''${instanceName}" = { + ... + }; + }; + } + ``` + ''; + }; + # TODO: Recursive services + options.services = mkOption { + visible = false; + type = attrsWith { + placeholder = "serviceName"; + elemType = submoduleWith { + modules = [ ./service-module.nix ]; + }; + }; + apply = _: throw "Not implemented yet"; + default = { }; + }; + }) ]; }; default = { }; @@ -333,26 +490,78 @@ in }; perMachine = mkOption { + description = '' + Per-machine configuration of the service. + + This option is used to define machine-specific settings for the service **once**, if any service-instance is used. + + Although the type is a `deferredModule`, it helps to think of it as a function. + The 'function' takes the `machine-name` and some other 'arguments' + + *Arguments*: + + - `machine`: `{ name :: string; roles :: listOf String }` + - `instances`: The scope of the machine, containing all instances and roles that the machine is part of. + ```nix + { + instances = { + = { + roles = { + = { + # Per-machine settings + machines = { = { settings = { ... }; }; }; }; + # Per-role settings + settings = { ... }; + }; + }; + }; + } + ``` + + *Returns* an `attribute set` containing: + + - `nixosModule`: The NixOS module for the machine. + + ''; type = types.deferredModuleWith { staticModules = [ - # Common output format - # As described by adr - # { nixosModule, services, ... } - ( - { ... }: - { - options.nixosModule = mkOption { default = { }; }; - options.services = mkOption { - type = attrsWith { - placeholder = "serviceName"; - elemType = submoduleWith { - modules = [ ./service-module.nix ]; - }; + ({ + options.nixosModule = mkOption { + type = types.deferredModule; + default = { }; + description = '' + A single NixOS module for the machine. + + This module is later imported to configure the machine with the config derived from service's settings. + + Example: + + ```nix + # ↓ machine.roles ... + perMachine = { machine, ... }: + { # ↓ nixos-config + nixosModule = { config ,... }: { + systemd.services.foo = { + enable = true; + }; + } + } + ``` + ''; + }; + # TODO: Recursive services + options.services = mkOption { + visible = false; + type = attrsWith { + placeholder = "serviceName"; + elemType = submoduleWith { + modules = [ ./service-module.nix ]; }; - default = { }; }; - } - ) + apply = _: throw "Not implemented yet"; + default = { }; + }; + }) ]; }; default = { }; @@ -428,6 +637,7 @@ in } */ result.allRoles = mkOption { + visible = false; readOnly = true; default = lib.mapAttrs (roleName: roleCfg: { allInstances = lib.mapAttrs (instanceName: instanceCfg: { @@ -454,10 +664,12 @@ in result.assertions = mkOption { default = { }; + visible = false; type = types.attrsOf types.raw; }; result.allMachines = mkOption { + visible = false; readOnly = true; default = let @@ -505,6 +717,7 @@ in }; result.final = mkOption { + visible = false; readOnly = true; default = lib.mapAttrs ( machineName: machineResult: diff --git a/lib/inventory/distributed-service/tests/per_instance_args.nix b/lib/inventory/distributed-service/tests/per_instance_args.nix index d27fe473f..ceb12ffa2 100644 --- a/lib/inventory/distributed-service/tests/per_instance_args.nix +++ b/lib/inventory/distributed-service/tests/per_instance_args.nix @@ -37,17 +37,19 @@ let }; in { - nixosModule = { - inherit - instanceName - settings - machine - roles - ; + options.passthru = lib.mkOption { + default = { + inherit + instanceName + settings + machine + roles + ; - # We are double vendoring the settings - # To test that we can do it indefinitely - vendoredSettings = finalSettings; + # We are double vendoring the settings + # To test that we can do it indefinitely + vendoredSettings = finalSettings; + }; }; }; }; @@ -101,25 +103,22 @@ in { # settings should evaluate test_per_instance_arguments = { - expr = - let - m = ( - unwrapModule - res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.nixosModule - ); - in - { - instanceName = m.instanceName; + expr = { + instanceName = + res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName; - # settings are specific. - # Below we access: - # instance = instance_foo - # roles = peer - # machines = jon - settings = m.settings; - machine = m.machine; - roles = m.roles; - }; + # settings are specific. + # Below we access: + # instance = instance_foo + # roles = peer + # machines = jon + settings = + res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings; + machine = + res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.machine; + roles = + res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.roles; + }; expected = { instanceName = "instance_foo"; settings = { @@ -162,9 +161,8 @@ in # TODO: Cannot be tested like this anymore test_per_instance_settings_vendoring = { expr = - (unwrapModule - res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.nixosModule - ).vendoredSettings; + + res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.vendoredSettings; expected = { timeout = "config.thing"; }; diff --git a/lib/inventory/distributed-service/tests/per_machine_args.nix b/lib/inventory/distributed-service/tests/per_machine_args.nix index bca087a4f..34fe6c38d 100644 --- a/lib/inventory/distributed-service/tests/per_machine_args.nix +++ b/lib/inventory/distributed-service/tests/per_machine_args.nix @@ -28,8 +28,10 @@ let perMachine = { instances, machine, ... }: { - nixosModule = { - inherit instances machine; + options.passthru = lib.mkOption { + default = { + inherit instances machine; + }; }; }; }; @@ -76,7 +78,7 @@ in inherit res; expr = { hasMachineSettings = - res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer.machines.jon + res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon ? settings; # settings are specific. @@ -84,10 +86,10 @@ in # instance = instance_foo # roles = peer # machines = jon - specificMachineSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer.machines.jon.settings; + specificMachineSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon.settings; hasRoleSettings = - res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer + res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer ? settings; # settings are specific. @@ -95,7 +97,7 @@ in # instance = instance_foo # roles = peer # machines = * - specificRoleSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer.settings; + specificRoleSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.settings; }; expected = { hasMachineSettings = true; diff --git a/lib/inventory/flake-module.nix b/lib/inventory/flake-module.nix index 4c36fb9ee..34b76890a 100644 --- a/lib/inventory/flake-module.nix +++ b/lib/inventory/flake-module.nix @@ -43,6 +43,16 @@ in } ); + legacyPackages.clan-service-module-interface = + (pkgs.nixosOptionsDoc { + options = + (self.clanLib.inventory.evalClanService { + modules = [ ]; + prefix = [ ]; + }).options; + warningsAreErrors = true; + }).optionsJSON; + # Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests legacyPackages.evalTests-inventory = import ./tests { inherit lib; diff --git a/lib/types/flake-module.nix b/lib/types/flake-module.nix index 2b75953c5..e0e0b58a6 100644 --- a/lib/types/flake-module.nix +++ b/lib/types/flake-module.nix @@ -20,6 +20,5 @@ in { imports = [ test-types-module ]; - legacyPackages.xxx = { }; }; }