diff --git a/lib/inventory/build-inventory/default.nix b/lib/inventory/build-inventory/default.nix index 21a3a4ae7..14614be7a 100644 --- a/lib/inventory/build-inventory/default.nix +++ b/lib/inventory/build-inventory/default.nix @@ -66,6 +66,27 @@ let serviceName: serviceConfigs: let supportedRoles = clan-core.lib.modules.getRoles inventory.modules serviceName; + + firstRole = import (getRoleFile (builtins.head supportedRoles)); + + loadModuleForClassCheck = + m: + if lib.isFunction m then + let + args = lib.functionArgs m; + in + m args + else + m; + + isClanModule = + let + module = loadModuleForClassCheck firstRole; + in + if module ? _class then module._class == "clan" else false; + + getRoleFile = role: inventory.modules.${serviceName} + "/roles/${role}.nix"; + resolvedRolesPerInstance = lib.mapAttrs ( instanceName: instanceConfig: let @@ -111,105 +132,113 @@ let # ) (lib.filterAttrs (_: ms: builtins.elem machineName ms) machinesRoles); # CompiledService :: { machineImports :: []; machineRoles :: [ String ] } { - inherit machinesRoles matchedRoles resolvedRolesPerInstance; + inherit + machinesRoles + matchedRoles + resolvedRolesPerInstance + firstRole + isClanModule + ; # TODO: Add other attributes - machineImports = ( - lib.foldlAttrs ( - # [ Modules ], String, ServiceConfig - acc2: instanceName: serviceConfig: - let - resolvedRoles = lib.genAttrs supportedRoles ( - roleName: - resolveTags { - members = serviceConfig.roles.${roleName} or { }; - inherit - serviceName - instanceName - roleName - inventory - ; - } - ); - - isInService = builtins.any (members: builtins.elem machineName members.machines) ( - builtins.attrValues resolvedRoles - ); - - # all roles where the machine is present - machineRoles = builtins.attrNames ( - lib.filterAttrs (_role: roleConfig: builtins.elem machineName roleConfig.machines) resolvedRoles - ); - machineServiceConfig = (serviceConfig.machines.${machineName} or { }).config or { }; - globalConfig = serviceConfig.config or { }; - - globalExtraModules = serviceConfig.extraModules or [ ]; - machineExtraModules = serviceConfig.machines.${machineName}.extraModules or [ ]; - roleServiceExtraModules = builtins.foldl' ( - acc: role: acc ++ serviceConfig.roles.${role}.extraModules or [ ] - ) [ ] machineRoles; - - # TODO: maybe optimize this dont lookup the role in inverse roles. Imports are not lazy - roleModules = builtins.map ( - role: - if builtins.elem role supportedRoles && inventory.modules ? ${serviceName} then - inventory.modules.${serviceName} + "/roles/${role}.nix" - else - throw "Module ${serviceName} doesn't have role: '${role}'. Role: ${ - inventory.modules.${serviceName} - }/roles/${role}.nix not found." - ) machineRoles; - - roleServiceConfigs = builtins.filter (m: m != { }) ( - builtins.map (role: serviceConfig.roles.${role}.config or { }) machineRoles - ); - - extraModules = map (s: if builtins.typeOf s == "string" then "${directory}/${s}" else s) ( - globalExtraModules ++ machineExtraModules ++ roleServiceExtraModules - ); - - nonExistingRoles = builtins.filter (role: !(builtins.elem role supportedRoles)) ( - builtins.attrNames (serviceConfig.roles or { }) - ); - - constraintAssertions = clan-core.lib.modules.checkConstraints { - moduleName = serviceName; - allModules = inventory.modules; - inherit resolvedRoles instanceName; - }; - in - if (nonExistingRoles != [ ]) then - throw "Roles ${builtins.toString nonExistingRoles} are not defined in the service ${serviceName}." - else if !(serviceConfig.enabled or true) then - acc2 - else if isInService then - acc2 - ++ [ - { - imports = roleModules ++ extraModules; - - clan.inventory.assertions = constraintAssertions; - clan.inventory.services.${serviceName}.${instanceName} = { - roles = resolvedRoles; - # TODO: Add inverseRoles to the service config if needed - # inherit inverseRoles; - }; - } - (lib.optionalAttrs (globalConfig != { } || machineServiceConfig != { } || roleServiceConfigs != [ ]) - { - clan.${serviceName} = lib.mkMerge ( - [ - globalConfig - machineServiceConfig - ] - ++ roleServiceConfigs - ); + machineImports = + if isClanModule then + throw "Clan modules are not supported yet." + else + (lib.foldlAttrs ( + # [ Modules ], String, ServiceConfig + acc2: instanceName: serviceConfig: + let + resolvedRoles = lib.genAttrs supportedRoles ( + roleName: + resolveTags { + members = serviceConfig.roles.${roleName} or { }; + inherit + serviceName + instanceName + roleName + inventory + ; } - ) - ] - else - acc2 - ) [ ] (serviceConfigs) - ); + ); + + isInService = builtins.any (members: builtins.elem machineName members.machines) ( + builtins.attrValues resolvedRoles + ); + + # all roles where the machine is present + machineRoles = builtins.attrNames ( + lib.filterAttrs (_role: roleConfig: builtins.elem machineName roleConfig.machines) resolvedRoles + ); + machineServiceConfig = (serviceConfig.machines.${machineName} or { }).config or { }; + globalConfig = serviceConfig.config or { }; + + globalExtraModules = serviceConfig.extraModules or [ ]; + machineExtraModules = serviceConfig.machines.${machineName}.extraModules or [ ]; + roleServiceExtraModules = builtins.foldl' ( + acc: role: acc ++ serviceConfig.roles.${role}.extraModules or [ ] + ) [ ] machineRoles; + + # TODO: maybe optimize this dont lookup the role in inverse roles. Imports are not lazy + roleModules = builtins.map ( + role: + if builtins.elem role supportedRoles && inventory.modules ? ${serviceName} then + getRoleFile role + else + throw "Module ${serviceName} doesn't have role: '${role}'. Role: ${ + inventory.modules.${serviceName} + }/roles/${role}.nix not found." + ) machineRoles; + + roleServiceConfigs = builtins.filter (m: m != { }) ( + builtins.map (role: serviceConfig.roles.${role}.config or { }) machineRoles + ); + + extraModules = map (s: if builtins.typeOf s == "string" then "${directory}/${s}" else s) ( + globalExtraModules ++ machineExtraModules ++ roleServiceExtraModules + ); + + nonExistingRoles = builtins.filter (role: !(builtins.elem role supportedRoles)) ( + builtins.attrNames (serviceConfig.roles or { }) + ); + + constraintAssertions = clan-core.lib.modules.checkConstraints { + moduleName = serviceName; + allModules = inventory.modules; + inherit resolvedRoles instanceName; + }; + in + if (nonExistingRoles != [ ]) then + throw "Roles ${builtins.toString nonExistingRoles} are not defined in the service ${serviceName}." + else if !(serviceConfig.enabled or true) then + acc2 + else if isInService then + acc2 + ++ [ + { + imports = roleModules ++ extraModules; + + clan.inventory.assertions = constraintAssertions; + clan.inventory.services.${serviceName}.${instanceName} = { + roles = resolvedRoles; + # TODO: Add inverseRoles to the service config if needed + # inherit inverseRoles; + }; + } + (lib.optionalAttrs (globalConfig != { } || machineServiceConfig != { } || roleServiceConfigs != [ ]) + { + clan.${serviceName} = lib.mkMerge ( + [ + globalConfig + machineServiceConfig + ] + ++ roleServiceConfigs + ); + } + ) + ] + else + acc2 + ) [ ] (serviceConfigs)); assertions = lib.mapAttrs' (name: value: { name = "checkservice.${serviceName}.${name}"; @@ -244,33 +273,55 @@ let */ buildInventory = { inventory, directory }: - { - machines = builtins.mapAttrs ( - machineName: machineConfig: - let - compiledServices = compileServicesForMachine { - inherit - machineName - inventory - directory - ; - }; - compiledMachine = compileMachine { - inherit - machineConfig - ; - }; - machineImports = - compiledMachine.machineImports - ++ builtins.foldl' (acc: service: acc ++ service.machineImports) [ ] ( - builtins.attrValues compiledServices - ); - in + (lib.evalModules { + modules = [ + ./internal.nix { - inherit machineImports compiledServices compiledMachine; + machines = builtins.mapAttrs ( + machineName: machineConfig: + let + compiledServices = compileServicesForMachine { + inherit + machineName + inventory + directory + ; + }; + compiledMachine = compileMachine { + inherit + machineConfig + ; + }; + + machineImports = + compiledMachine.machineImports + ++ builtins.foldl' ( + acc: service: + let + failedAssertions = (lib.filterAttrs (_: v: !v.assertion) service.assertions); + failedAssertionsImports = + if failedAssertions != { } then + [ + { + clan.inventory.assertions = failedAssertions; + } + ] + else + [ ]; + in + acc + ++ service.machineImports + # Import failed assertions + ++ failedAssertionsImports + ) [ ] (builtins.attrValues compiledServices); + in + { + inherit machineImports compiledServices compiledMachine; + } + ) (inventory.machines or { }); } - ) (inventory.machines or { }); - }; + ]; + }).config; in { inherit buildInventory; diff --git a/lib/inventory/build-inventory/interface.nix b/lib/inventory/build-inventory/interface.nix index f8b347aa9..030da790b 100644 --- a/lib/inventory/build-inventory/interface.nix +++ b/lib/inventory/build-inventory/interface.nix @@ -92,7 +92,6 @@ let }; in { - imports = [ ./assertions.nix ]; diff --git a/lib/inventory/build-inventory/internal.nix b/lib/inventory/build-inventory/internal.nix new file mode 100644 index 000000000..58fa59075 --- /dev/null +++ b/lib/inventory/build-inventory/internal.nix @@ -0,0 +1,24 @@ +{ lib, ... }: +let + inherit (lib) types mkOption; + submodule = m: types.submoduleWith { modules = [ m ]; }; +in +{ + options = { + machines = mkOption { + type = types.attrsOf (submodule { + options = { + compiledMachine = mkOption { + type = types.raw; + }; + compiledServices = mkOption { + type = types.raw; + }; + machineImports = mkOption { + type = types.raw; + }; + }; + }); + }; + }; +} diff --git a/lib/inventory/tests/clanModule/README.md b/lib/inventory/tests/clanModule/README.md new file mode 100644 index 000000000..aa87ee5c8 --- /dev/null +++ b/lib/inventory/tests/clanModule/README.md @@ -0,0 +1,4 @@ +--- +features = [ "inventory" ] +--- +Description \ No newline at end of file diff --git a/lib/inventory/tests/clanModule/roles/default.nix b/lib/inventory/tests/clanModule/roles/default.nix new file mode 100644 index 000000000..6d0c67be4 --- /dev/null +++ b/lib/inventory/tests/clanModule/roles/default.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + _class = "clan"; + perInstance = { }; + perService = { }; +} diff --git a/lib/inventory/tests/default.nix b/lib/inventory/tests/default.nix index d29c4cc0a..8424b3973 100644 --- a/lib/inventory/tests/default.nix +++ b/lib/inventory/tests/default.nix @@ -9,6 +9,44 @@ let inherit (inventory) buildInventory; in { + test_inventory_a = + let + compiled = buildInventory { + inventory = { + machines = { + A = { }; + }; + services = { + clanModule = { }; + legacyModule = { }; + }; + modules = { + clanModule = ./clanModule; + legacyModule = ./legacyModule; + }; + }; + directory = ./.; + }; + in + { + expr = { + clanModule = lib.filterAttrs ( + name: _: name == "isClanModule" + ) compiled.machines.A.compiledServices.clanModule; + legacyModule = lib.filterAttrs ( + name: _: name == "isClanModule" + ) compiled.machines.A.compiledServices.legacyModule; + }; + expected = { + clanModule = { + isClanModule = true; + }; + legacyModule = { + isClanModule = false; + }; + }; + }; + test_inventory_empty = let compiled = buildInventory { diff --git a/lib/inventory/tests/legacyModule/README.md b/lib/inventory/tests/legacyModule/README.md new file mode 100644 index 000000000..aa87ee5c8 --- /dev/null +++ b/lib/inventory/tests/legacyModule/README.md @@ -0,0 +1,4 @@ +--- +features = [ "inventory" ] +--- +Description \ No newline at end of file diff --git a/lib/inventory/tests/legacyModule/roles/default.nix b/lib/inventory/tests/legacyModule/roles/default.nix new file mode 100644 index 000000000..853ff22fe --- /dev/null +++ b/lib/inventory/tests/legacyModule/roles/default.nix @@ -0,0 +1,10 @@ +{ + lib, + config, + clan-core, + ... +}: +{ + # Just some random stuff + config.user.user = lib.mapAttrs clan-core.users.root; +}