docs(service-modules): add description and docs for options

This commit is contained in:
Johannes Kirschbauer
2025-06-04 12:33:46 +02:00
parent 8a9843e7ca
commit 0180013e68
6 changed files with 319 additions and 96 deletions

View File

@@ -10,6 +10,7 @@ in
{ lib, config, ... }: { lib, config, ... }:
{ {
options.result.api = lib.mkOption { options.result.api = lib.mkOption {
visible = false;
default = { }; default = { };
type = lib.types.submodule ({ type = lib.types.submodule ({
options.schema = lib.mkOption { options.schema = lib.mkOption {

View File

@@ -26,13 +26,6 @@ let
${builtins.toJSON (lib.attrNames config.roles)} ${builtins.toJSON (lib.attrNames config.roles)}
''; '';
# checkInstanceSettings =
# instanceName: instanceSettings:
# let
# unmatchedRoles = 1;
# in
# unmatchedRoles;
/** /**
Merges the role- and machine-settings using the role interface Merges the role- and machine-settings using the role interface
@@ -154,11 +147,29 @@ let
in in
{ {
options = { options = {
# TODO: deduplicate this with inventory.instances
# Although inventory has stricter constraints
instances = mkOption { instances = mkOption {
# Instances are created in the inventory
visible = false;
defaultText = "Throws: 'The service must define its instances' when not defined";
default = throw '' default = throw ''
The clan service module ${config.manifest.name} doesn't define any instances. 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 `<instanceName>` 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 { type = attrsWith {
@@ -174,6 +185,21 @@ in
# apply = v: lib.seq (checkInstanceSettings name v) v; # apply = v: lib.seq (checkInstanceSettings name v) v;
# }; # };
options.roles = mkOption { 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 `<roleName>` 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 '' default = throw ''
Instance '${name}' of service '${config.manifest.name}' mut define members via 'roles'. Instance '${name}' of service '${config.manifest.name}' mut define members via 'roles'.
@@ -184,26 +210,35 @@ in
placeholder = "roleName"; placeholder = "roleName";
elemType = submoduleWith { elemType = submoduleWith {
modules = [ modules = [
( ({
{ ... }: # instances.{instanceName}.roles.{roleName}.machines
{ options.machines = mkOption {
# instances.{instanceName}.roles.{roleName}.machines description = ''
options.machines = mkOption { Machines of the role.
type = attrsWith {
placeholder = "machineName"; A machine is a physical or virtual machine that is part of the instance.
elemType = submoduleWith { The `<machineName>` must match the name of any machine defined in the clan.
modules = [
(m: { For example:
options.settings = mkOption {
type = types.raw; - 'machines.my-machine = { ...; }' for a machine that is part of the instance
description = "Settings of '${name}-machine': ${m.name}."; - 'machines.my-other-machine = { ...; }' for another machine that is part of the instance
default = { }; '';
}; type = attrsWith {
}) placeholder = "machineName";
]; elemType = submoduleWith {
}; modules = [
(m: {
options.settings = mkOption {
type = types.raw;
description = "Settings of '${name}-machine': ${m.name or "<machineName>"}.";
default = { };
};
})
];
}; };
}; };
};
# instances.{instanceName}.roles.{roleName}.settings # instances.{instanceName}.roles.{roleName}.settings
# options._settings = mkOption { }; # options._settings = mkOption { };
@@ -242,6 +277,22 @@ in
}; };
}; };
roles = mkOption { 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 `<roleName>`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 '' default = throw ''
Role behavior of service '${config.manifest.name}' must be defined. Role behavior of service '${config.manifest.name}' must be defined.
A 'clan.service' module should always define its behavior via 'roles' A 'clan.service' module should always define its behavior via 'roles'
@@ -263,32 +314,138 @@ in
in in
{ {
options.interface = mkOption { 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; type = types.deferredModule;
# TODO: Default to an empty module # TODO: Default to an empty module
# need to test that an the empty module can be evaluated to empty settings # need to test that an the empty module can be evaluated to empty settings
default = { }; default = { };
}; };
options.perInstance = mkOption { options.perInstance = mkOption {
type = types.deferredModuleWith { description = ''
staticModules = [ Per-instance configuration of the role.
# Common output format
# As described by adr This option is used to define instance-specific behavior for the service-role. (Example below)
# { nixosModule, services, ... }
( 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 = { }; }; name = "machineName";
options.services = mkOption { roles = ["client" "server" ... ];
type = attrsWith { }
placeholder = "serviceName"; ```
elemType = submoduleWith { - `roles`: Attribute set of all roles of the instance, in the form:
modules = [ ./service-module.nix ]; ```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 = { }; default = { };
@@ -333,26 +490,78 @@ in
}; };
perMachine = mkOption { 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 = {
<instanceName> = {
roles = {
<roleName> = {
# Per-machine settings
machines = { <machineName> = { settings = { ... }; }; }; };
# Per-role settings
settings = { ... };
};
};
};
}
```
*Returns* an `attribute set` containing:
- `nixosModule`: The NixOS module for the machine.
'';
type = types.deferredModuleWith { type = types.deferredModuleWith {
staticModules = [ staticModules = [
# Common output format ({
# As described by adr options.nixosModule = mkOption {
# { nixosModule, services, ... } type = types.deferredModule;
( default = { };
{ ... }: description = ''
{ A single NixOS module for the machine.
options.nixosModule = mkOption { default = { }; };
options.services = mkOption { This module is later imported to configure the machine with the config derived from service's settings.
type = attrsWith {
placeholder = "serviceName"; Example:
elemType = submoduleWith {
modules = [ ./service-module.nix ]; ```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 = { }; default = { };
@@ -428,6 +637,7 @@ in
} }
*/ */
result.allRoles = mkOption { result.allRoles = mkOption {
visible = false;
readOnly = true; readOnly = true;
default = lib.mapAttrs (roleName: roleCfg: { default = lib.mapAttrs (roleName: roleCfg: {
allInstances = lib.mapAttrs (instanceName: instanceCfg: { allInstances = lib.mapAttrs (instanceName: instanceCfg: {
@@ -454,10 +664,12 @@ in
result.assertions = mkOption { result.assertions = mkOption {
default = { }; default = { };
visible = false;
type = types.attrsOf types.raw; type = types.attrsOf types.raw;
}; };
result.allMachines = mkOption { result.allMachines = mkOption {
visible = false;
readOnly = true; readOnly = true;
default = default =
let let
@@ -505,6 +717,7 @@ in
}; };
result.final = mkOption { result.final = mkOption {
visible = false;
readOnly = true; readOnly = true;
default = lib.mapAttrs ( default = lib.mapAttrs (
machineName: machineResult: machineName: machineResult:

View File

@@ -37,17 +37,19 @@ let
}; };
in in
{ {
nixosModule = { options.passthru = lib.mkOption {
inherit default = {
instanceName inherit
settings instanceName
machine settings
roles machine
; roles
;
# We are double vendoring the settings # We are double vendoring the settings
# To test that we can do it indefinitely # To test that we can do it indefinitely
vendoredSettings = finalSettings; vendoredSettings = finalSettings;
};
}; };
}; };
}; };
@@ -101,25 +103,22 @@ in
{ {
# settings should evaluate # settings should evaluate
test_per_instance_arguments = { test_per_instance_arguments = {
expr = expr = {
let instanceName =
m = ( res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName;
unwrapModule
res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.nixosModule
);
in
{
instanceName = m.instanceName;
# settings are specific. # settings are specific.
# Below we access: # Below we access:
# instance = instance_foo # instance = instance_foo
# roles = peer # roles = peer
# machines = jon # machines = jon
settings = m.settings; settings =
machine = m.machine; res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings;
roles = m.roles; 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 = { expected = {
instanceName = "instance_foo"; instanceName = "instance_foo";
settings = { settings = {
@@ -162,9 +161,8 @@ in
# TODO: Cannot be tested like this anymore # TODO: Cannot be tested like this anymore
test_per_instance_settings_vendoring = { test_per_instance_settings_vendoring = {
expr = expr =
(unwrapModule
res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.nixosModule res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.vendoredSettings;
).vendoredSettings;
expected = { expected = {
timeout = "config.thing"; timeout = "config.thing";
}; };

View File

@@ -28,8 +28,10 @@ let
perMachine = perMachine =
{ instances, machine, ... }: { instances, machine, ... }:
{ {
nixosModule = { options.passthru = lib.mkOption {
inherit instances machine; default = {
inherit instances machine;
};
}; };
}; };
}; };
@@ -76,7 +78,7 @@ in
inherit res; inherit res;
expr = { expr = {
hasMachineSettings = 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;
# settings are specific. # settings are specific.
@@ -84,10 +86,10 @@ in
# instance = instance_foo # instance = instance_foo
# roles = peer # roles = peer
# machines = jon # 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 = 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;
# settings are specific. # settings are specific.
@@ -95,7 +97,7 @@ in
# instance = instance_foo # instance = instance_foo
# roles = peer # roles = peer
# machines = * # 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 = { expected = {
hasMachineSettings = true; hasMachineSettings = true;

View File

@@ -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 # Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests
legacyPackages.evalTests-inventory = import ./tests { legacyPackages.evalTests-inventory = import ./tests {
inherit lib; inherit lib;

View File

@@ -20,6 +20,5 @@
in in
{ {
imports = [ test-types-module ]; imports = [ test-types-module ];
legacyPackages.xxx = { };
}; };
} }