diff --git a/decisions/01-ClanModules.md b/decisions/01-ClanModules.md new file mode 100644 index 000000000..f132fc9ef --- /dev/null +++ b/decisions/01-ClanModules.md @@ -0,0 +1,181 @@ +# Interface for module authors + +Status: proposed + +## Context + +ClanModules define cross `machine` `services`. +Services can be `instantiated` multiple times. +To make configuration more natural each service instance has multiple `roles` (.i.e. `client` `server`) + +Overall this makes it a 4-Dimensional problem where n-`machines` can have m-`services`, m-`services` can have k-`instances` and k-`instances` can have l-`roles`. + +### Problems + +Problems with the current way of writing clanModules. + +1. No way to retrieve the config of a single service instance + 2. -> We can't really support multiple instances even though the nix module *could* support multiple instances + 3. the "config" of a service MUST be evaluated against the "interface" outside of any machine. + -> Solution: evaluate each instance config and store it in seperate attributes +3. Can't merge multiple config instances -> Example: https://pad.lassul.us/i9uSspVbRUmiBmsJ8OR1ug# + -> Propsed change: feed the instance config into a helper that defines the merge behavior +5. Writing modules for multiple instances is cumbersome + -> Propsed change: Helper utility such as "perInstance" +6. No clearly separated, role based interface that is usable via json based APIs + 1. Currently we just import the module into a test machine and try to figure out what changes + -> Proposed change: enforce machine agnostic inventory interface definitions + +### Some immediate ideas + +#### simple idea + +Just use another nested attribute set + +- `clan.something.borgbackup.instance1 = config` +- `clan.something.borgbackup.instance1 = config` + +Problems solved: + +- each instance has its own config + +Problems not solved: + +- Machine independent seperated interface +- Still cumbersome to merge the instances +- Unclear how to get to each config because the interface also depends on the role + +#### Improved Idea + +- PerInstance: utility that maps over all instances and defines what needs to be set for each instance and receives the instanceConfig as argument. +- GlobalConfig: Some stuff that needs to be set statically on a machine if the service is enabled. + +## Proposed Change + +Change the ClanModule interface + +`client.nix` +```nix +{ + # Allows backwards compatibility + _class = "clan"; + + # Allows importing other service modules + module.zerotier = { + # function that receives each instance along with its config and resolved roles + perInstance = + { + # serviceConfig is the config of this instance + # as described in interface attribute below. + # Merged without differing priority: + # service..config + # + role..config + # + machine..config + serviceConfig, + # : string + instanceName, + # machine :: { name :: string , roles :: listOf string; } + machine, + # : { {roleName} :: { machines :: listOf string; } } + roles, + ... }: + { + nixosModule = { + config.debug."${instanceName}-client" = serviceConfig; + }; + }; + # Function that is appplied once for this role + perService = { + nixosModule = + { lib, ... }: + { + # Some shared code should be put into a shared file + # Which is then imported into all/some roles + imports = [ + ../shared.nix + ]; + options.debug = lib.mkOption { + type = lib.types.attrsOf lib.types.raw; + }; + }; + }; + + # Describes the settings how a `client` can be configured. + # These are the `options` of serviceConfig of a `client`. (Since: This file is called client.nix) + interface = + { lib, ... }: + { + options.foo = lib.mkOption { + type = lib.types.str; + default = "bar"; + }; + }; + }; +} +``` + +## Not Changed! (existing inventory.services for api configuration of clanModules) + +```nix +{ + inventory.services = { + test."instance1" = { + roles.client.tags = [ "all" ]; + machines.foo.config = { ... }; + roles.client.config = { ... }; + + }; + test."instance2" = { + roles.server.tags = [ "all" ]; + config = { ... }; + }; + }; +} +``` + +We don't propse to change anything here. Because we had no problems with that interface yet. +We even like it because it is very fast to evaluate and can be configured via UI. + +### Example of borgbackup before and after + +```nix +# client.nix (BEFORE) +{config, ...}: +let + instances = config.clan.inventory.services.borgbackup or { }; + # roles = { ${role_name} :: { machines :: [string] } } + allServers = lib.foldlAttrs ( + acc: _instanceName: instanceConfig: + acc + ++ ( + if builtins.elem machineName instanceConfig.roles.client.machines then + instanceConfig.roles.server.machines + else + [ ] + ) + ) [ ] instances; + + machineName = config.clan.core.settings.machine.name; + + cfg = config.clan.borgbackup; +in +{ +# ... Some nixos config +} +``` + +```nix +# client.nix (AFTER) +{ + _class = "clan"; + perInstance = {instanceName, serviceConfig, machine, roles }: { + # allServers => roles.server; + # machineName => machine.name; + # cfg => serviceConfig; + nixosModule = {config, ...}: { + # ...some nixos config + } + # Extendable. I.e. Instances can also define 'vars' or other things later. + }; +} +```