From a89502e47f39399cee9955314b87c3039c0d6ade Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Tue, 1 Jul 2025 16:54:19 +0200 Subject: [PATCH] clanServices: add flake level exports --- clanModules/borgbackup/roles/client.nix | 1 - lib/modules/clan/interface.nix | 35 ++++ lib/modules/clan/module.nix | 7 +- .../all-services-wrapper.nix | 75 ++++++++ .../distributed-service/inventory-adapter.nix | 53 ++++-- .../distributed-service/service-module.nix | 36 ++++ .../distributed-service/tests/default.nix | 8 +- .../distributed-service/tests/exports.nix | 170 ++++++++++++++++++ .../tests/per_instance_args.nix | 12 +- .../tests/per_machine_args.nix | 8 +- 10 files changed, 373 insertions(+), 32 deletions(-) create mode 100644 lib/modules/inventory/distributed-service/all-services-wrapper.nix create mode 100644 lib/modules/inventory/distributed-service/tests/exports.nix diff --git a/clanModules/borgbackup/roles/client.nix b/clanModules/borgbackup/roles/client.nix index 35d6526c2..e2cb8e591 100644 --- a/clanModules/borgbackup/roles/client.nix +++ b/clanModules/borgbackup/roles/client.nix @@ -185,7 +185,6 @@ in ]; clan.core.vars.generators.borgbackup = { - files."borgbackup.ssh.pub".secret = false; files."borgbackup.ssh" = { }; files."borgbackup.repokey" = { }; diff --git a/lib/modules/clan/interface.nix b/lib/modules/clan/interface.nix index 02cbc7eb4..643be1d79 100644 --- a/lib/modules/clan/interface.nix +++ b/lib/modules/clan/interface.nix @@ -67,6 +67,41 @@ in ''; }; + # TODO: make this writable by moving the options from inventoryClass into clan. + exports = lib.mkOption { + readOnly = true; + }; + + exportsModule = lib.mkOption { + type = types.deferredModule; + # can be set only once + readOnly = true; + description = '' + A module that is used to define the module of flake level exports - + + such as 'exports.machines.' and 'exports.instances.' + + Example: + + ```nix + { + options.vars.generators = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submoduleWith { + modules = [ + { + options.script = lib.mkOption { type = lib.types.str; }; + } + ]; + } + ); + default = { }; + }; + } + ``` + ''; + }; + specialArgs = lib.mkOption { type = types.attrsOf types.raw; default = { }; diff --git a/lib/modules/clan/module.nix b/lib/modules/clan/module.nix index 6f47050cb..917468e0f 100644 --- a/lib/modules/clan/module.nix +++ b/lib/modules/clan/module.nix @@ -224,6 +224,8 @@ in inherit nixosConfigurations; inherit darwinConfigurations; + exports = config.clanInternals.inventoryClass.distributedServices.servicesEval.config.exports; + clanInternals = { inventoryClass = let @@ -244,10 +246,13 @@ in inherit inventory directory; } ( + let + clanConfig = config; + in { config, ... }: { distributedServices = clanLib.inventory.mapInstances { - inherit (config) inventory; + inherit (clanConfig) inventory exportsModule; inherit flakeInputs; clanCoreModules = clan-core.clan.modules; prefix = [ "distributedServices" ]; diff --git a/lib/modules/inventory/distributed-service/all-services-wrapper.nix b/lib/modules/inventory/distributed-service/all-services-wrapper.nix new file mode 100644 index 000000000..0f345da9c --- /dev/null +++ b/lib/modules/inventory/distributed-service/all-services-wrapper.nix @@ -0,0 +1,75 @@ +# Wraps all services in one fixed point module +{ + lib, + config, + specialArgs, + _ctx, + ... +}: +let + inherit (lib) mkOption types; + inherit (types) attrsWith submoduleWith; +in +{ + # TODO: merge these options into clan options + options = { + exportsModule = mkOption { + type = types.deferredModule; + readOnly = true; + }; + mappedServices = mkOption { + visible = false; + type = attrsWith { + placeholder = "mappedServiceName"; + elemType = submoduleWith { + modules = [ + ( + { name, ... }: + { + _module.args._ctx = [ name ]; + _module.args.exports' = config.exports; + } + ) + ./service-module.nix + # feature modules + (lib.modules.importApply ./api-feature.nix { + inherit (specialArgs) clanLib; + prefix = _ctx; + }) + ]; + }; + }; + default = { }; + }; + exports = mkOption { + type = submoduleWith { + modules = [ + { + options = { + instances = lib.mkOption { + # instances.... + type = types.attrsOf (submoduleWith { + modules = [ + config.exportsModule + ]; + }); + }; + # instances.... + machines = lib.mkOption { + type = types.attrsOf (submoduleWith { + modules = [ + config.exportsModule + ]; + }); + }; + }; + } + ] ++ lib.mapAttrsToList (_: service: service.exports) config.mappedServices; + }; + default = { }; + }; + debug = mkOption { + default = lib.mapAttrsToList (_: service: service.exports) config.mappedServices; + }; + }; +} diff --git a/lib/modules/inventory/distributed-service/inventory-adapter.nix b/lib/modules/inventory/distributed-service/inventory-adapter.nix index 15120bed3..b21dcc31c 100644 --- a/lib/modules/inventory/distributed-service/inventory-adapter.nix +++ b/lib/modules/inventory/distributed-service/inventory-adapter.nix @@ -26,6 +26,7 @@ in inventory, clanCoreModules, prefix ? [ ], + exportsModule, }: let # machineHasTag = machineName: tagName: lib.elem tagName inventory.machines.${machineName}.tags; @@ -89,23 +90,6 @@ in } ) inventory.instances or { }; - # TODO: Eagerly check the _class of the resolved module - importedModulesEvaluated = lib.mapAttrs ( - module_ident: instances: - clanLib.evalService { - prefix = prefix ++ [ module_ident ]; - modules = - [ - # Import the resolved module. - # i.e. clan.modules.admin - (builtins.head instances).instance.resolvedModule - ] # Include all the instances that correlate to the resolved module - ++ (builtins.map (v: { - instances.${v.instanceName}.roles = v.instance.instanceRoles; - }) instances); - } - ) grouped; - # Group the instances by the module they resolve to # This is necessary to evaluate the module in a single pass # :: { _ :: [ { name, value } ] } @@ -133,9 +117,44 @@ in acc ++ [ eval.config.result.final.${machineName}.nixosModule or { } ] ) [ ] importedModulesEvaluated; }) inventory.machines or { }; + + evalServices = + { modules, prefix }: + lib.evalModules { + specialArgs = { + inherit clanLib; + _ctx = prefix; + }; + modules = [ + ./all-services-wrapper.nix + ] ++ modules; + }; + + servicesEval = evalServices { + inherit prefix; + modules = [ + { + inherit exportsModule; + mappedServices = lib.mapAttrs (_module_ident: instances: { + imports = + [ + # Import the resolved module. + # i.e. clan.modules.admin + (builtins.head instances).instance.resolvedModule + ] # Include all the instances that correlate to the resolved module + ++ (builtins.map (v: { + instances.${v.instanceName}.roles = v.instance.instanceRoles; + }) instances); + }) grouped; + } + ]; + }; + importedModulesEvaluated = servicesEval.config.mappedServices; + in { inherit + servicesEval importedModuleWithInstances grouped allMachines diff --git a/lib/modules/inventory/distributed-service/service-module.nix b/lib/modules/inventory/distributed-service/service-module.nix index dc597fdf4..f088d7883 100644 --- a/lib/modules/inventory/distributed-service/service-module.nix +++ b/lib/modules/inventory/distributed-service/service-module.nix @@ -384,6 +384,10 @@ in type = types.deferredModuleWith { staticModules = [ ({ + options.exports = mkOption { + type = types.deferredModule; + default = { }; + }; options.nixosModule = mkOption { type = types.deferredModule; default = { }; @@ -514,6 +518,10 @@ in type = types.deferredModuleWith { staticModules = [ ({ + options.exports = mkOption { + type = types.deferredModule; + default = { }; + }; options.nixosModule = mkOption { type = types.deferredModule; default = { }; @@ -608,6 +616,34 @@ in modules = [ v ]; }).config; }; + + exports = mkOption { + default = { }; + type = types.submoduleWith { + # Static modules + modules = + [ + { + options.instances = mkOption { + type = types.attrsOf types.deferredModule; + }; + } + { + options.machines = mkOption { + type = types.attrsOf types.deferredModule; + }; + } + ] + ++ lib.mapAttrsToList (_roleName: role: { + instances = lib.mapAttrs (_instanceName: instance: { + imports = lib.mapAttrsToList (_machineName: v: v.exports) instance.allMachines; + }) role.allInstances; + }) config.result.allRoles + ++ lib.mapAttrsToList (machineName: machine: { + machines.${machineName} = machine.exports; + }) config.result.allMachines; + }; + }; # --- # Place the result in _module.result to mark them as "internal" and discourage usage/overrides # diff --git a/lib/modules/inventory/distributed-service/tests/default.nix b/lib/modules/inventory/distributed-service/tests/default.nix index 0ccbe3d7f..c8a3e1583 100644 --- a/lib/modules/inventory/distributed-service/tests/default.nix +++ b/lib/modules/inventory/distributed-service/tests/default.nix @@ -48,9 +48,11 @@ let clanCoreModules = { }; flakeInputs = flakeInputsFixture; inherit inventory; + exportsModule = { }; }; in { + exports = import ./exports.nix { inherit lib clanLib; }; resolve_module_spec = import ./import_module_spec.nix { inherit lib callInventoryAdapter; }; test_simple = let @@ -171,7 +173,7 @@ in { # Test that the module is mapped into the output # We might change the attribute name in the future - expr = lib.attrNames res.importedModulesEvaluated.self-A.config.instances; + expr = lib.attrNames res.importedModulesEvaluated.self-A.instances; expected = [ "instance_bar" "instance_foo" @@ -227,7 +229,7 @@ in { # Test that the module is mapped into the output # We might change the attribute name in the future - expr = lib.attrNames res.importedModulesEvaluated.self-A.config.result.allMachines; + expr = lib.attrNames res.importedModulesEvaluated.self-A.result.allMachines; expected = [ "jon" "sara" @@ -279,7 +281,7 @@ in { # Test that the module is mapped into the output # We might change the attribute name in the future - expr = lib.attrNames res.importedModulesEvaluated.self-A.config.result.allMachines; + expr = lib.attrNames res.importedModulesEvaluated.self-A.result.allMachines; expected = [ "jon" "sara" diff --git a/lib/modules/inventory/distributed-service/tests/exports.nix b/lib/modules/inventory/distributed-service/tests/exports.nix new file mode 100644 index 000000000..bc9c6cb54 --- /dev/null +++ b/lib/modules/inventory/distributed-service/tests/exports.nix @@ -0,0 +1,170 @@ +{ lib, clanLib }: +let + clan = clanLib.clan { + self = { }; + directory = ./.; + + exportsModule = { + options.vars.generators = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submoduleWith { + # TODO: import the vars submodule here + modules = [ + { + options.script = lib.mkOption { type = lib.types.str; }; + } + ]; + } + ); + default = { }; + }; + }; + + machines.jon = { }; + machines.sara = { }; + # A module that adds exports perMachine + modules.A = + { exports', ... }: + { + manifest.name = "A"; + roles.peer.perInstance = + { machine, ... }: + { + # Cross reference a perMachine exports + exports.vars.generators."${machine.name}-network-ip".script = + "A:" + exports'.machines.${machine.name}.vars.generators.key.script; + # Cross reference a perInstance exports from a different service + exports.vars.generators."${machine.name}-full-hostname".script = + "A:" + exports'.instances."B-1".vars.generators.hostname.script; + }; + roles.server = { }; + perMachine = + { machine, ... }: + { + exports = { + vars.generators.key.script = machine.name; + }; + }; + }; + # A module that adds exports perInstance + modules.B = { + manifest.name = "B"; + roles.peer.perInstance = + { instanceName, ... }: + { + exports = { + vars.generators.hostname.script = instanceName; + }; + }; + }; + + inventory = { + instances.B-1 = { + module.name = "B"; + module.input = "self"; + roles.peer.tags.all = { }; + }; + instances.B-2 = { + module.name = "B"; + module.input = "self"; + roles.peer.tags.all = { }; + }; + instances.A-1 = { + module.name = "A"; + module.input = "self"; + roles.peer.tags.all = { }; + roles.server.tags.all = { }; + }; + instances.A-2 = { + module.name = "A"; + module.input = "self"; + roles.peer.tags.all = { }; + roles.server.tags.all = { }; + }; + }; + }; +in +{ + test_1 = { + inherit clan; + expr = clan.config.exports; + expected = { + instances = { + A-1 = { + vars = { + generators = { + jon-full-hostname = { + script = "A:B-1"; + }; + jon-network-ip = { + script = "A:jon"; + }; + sara-full-hostname = { + script = "A:B-1"; + }; + sara-network-ip = { + script = "A:sara"; + }; + }; + }; + }; + A-2 = { + vars = { + generators = { + jon-full-hostname = { + script = "A:B-1"; + }; + jon-network-ip = { + script = "A:jon"; + }; + sara-full-hostname = { + script = "A:B-1"; + }; + sara-network-ip = { + script = "A:sara"; + }; + }; + }; + }; + B-1 = { + vars = { + generators = { + hostname = { + script = "B-1"; + }; + }; + }; + }; + B-2 = { + vars = { + generators = { + hostname = { + script = "B-2"; + }; + }; + }; + }; + }; + machines = { + jon = { + vars = { + generators = { + key = { + script = "jon"; + }; + }; + }; + }; + sara = { + vars = { + generators = { + key = { + script = "sara"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/lib/modules/inventory/distributed-service/tests/per_instance_args.nix b/lib/modules/inventory/distributed-service/tests/per_instance_args.nix index b94697c59..05e1b16a2 100644 --- a/lib/modules/inventory/distributed-service/tests/per_instance_args.nix +++ b/lib/modules/inventory/distributed-service/tests/per_instance_args.nix @@ -106,7 +106,7 @@ in test_per_instance_arguments = { expr = { instanceName = - res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName; + res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName; # settings are specific. # Below we access: @@ -114,11 +114,11 @@ in # roles = peer # machines = jon settings = - res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings; + res.importedModulesEvaluated.self-A.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; + res.importedModulesEvaluated.self-A.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; + res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.roles; }; expected = { instanceName = "instance_foo"; @@ -161,9 +161,9 @@ in # TODO: Cannot be tested like this anymore test_per_instance_settings_vendoring = { - x = res.importedModulesEvaluated.self-A.config; + x = res.importedModulesEvaluated.self-A; expr = - res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.vendoredSettings; + res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.vendoredSettings; expected = { timeout = "config.thing"; }; diff --git a/lib/modules/inventory/distributed-service/tests/per_machine_args.nix b/lib/modules/inventory/distributed-service/tests/per_machine_args.nix index 444b94569..ac68631d9 100644 --- a/lib/modules/inventory/distributed-service/tests/per_machine_args.nix +++ b/lib/modules/inventory/distributed-service/tests/per_machine_args.nix @@ -81,7 +81,7 @@ in inherit res; expr = { hasMachineSettings = - res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon + res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon ? settings; # settings are specific. @@ -89,10 +89,10 @@ in # instance = instance_foo # roles = peer # machines = jon - specificMachineSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon.settings; + specificMachineSettings = filterInternals res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon.settings; hasRoleSettings = - res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer + res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer ? settings; # settings are specific. @@ -100,7 +100,7 @@ in # instance = instance_foo # roles = peer # machines = * - specificRoleSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.settings; + specificRoleSettings = filterInternals res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.settings; }; expected = { hasMachineSettings = true;