clanModules: remove unused code

This commit is contained in:
Johannes Kirschbauer
2025-08-16 11:48:13 +02:00
parent 62805c66ff
commit 308a10d6e6
14 changed files with 6 additions and 1070 deletions

View File

@@ -139,33 +139,6 @@ in
nixosTests
// flakeOutputs
// {
# TODO: Automatically provide this check to downstream users to check their modules
clan-modules-json-compatible =
let
allSchemas = lib.mapAttrs (
_n: m:
let
schema =
(self.clanLib.evalService {
modules = [ m ];
prefix = [
"checks"
system
];
}).config.result.api.schema;
in
schema
) self.clan.modules;
in
pkgs.runCommand "combined-result"
{
schemaFile = builtins.toFile "schemas.json" (builtins.toJSON allSchemas);
}
''
mkdir -p $out
cat $schemaFile > $out/allSchemas.json
'';
clan-core-for-checks = pkgs.runCommand "clan-core-for-checks" { } ''
cp -r ${privateInputs.clan-core-for-checks} $out
chmod -R +w $out

View File

@@ -19,11 +19,6 @@
clan-core = self;
inherit pkgs;
evalClanModules = self.clanLib.evalClan.evalClanModules;
modulesRolesOptions = self.clanLib.evalClan.evalClanModulesWithRoles {
allModules = self.clanModules;
inherit pkgs;
clan-core = self;
};
};
# Frontmatter for clanModules
@@ -35,10 +30,6 @@
};
in
docs.optionsJSON;
# Options available when imported via ` inventory.${moduleName}....${rolesName} `
clanModulesViaRoles = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaRoles);
# clan service options
clanModulesViaService = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaService);
@@ -89,7 +80,6 @@
}
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
# A file that contains the links to all clanModule docs
export CLAN_MODULES_VIA_ROLES=${clanModulesViaRoles}
export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService}
export CLAN_SERVICE_INTERFACE=${self'.legacyPackages.clan-service-module-interface}/share/doc/nixos/options.json
# Frontmatter format for clanModules
@@ -107,7 +97,6 @@
legacyPackages = {
inherit
jsonDocs
clanModulesViaRoles
clanModulesViaService
;
};

View File

@@ -1,5 +1,4 @@
{
modulesRolesOptions,
nixosOptionsDoc,
evalClanModules,
lib,
@@ -12,19 +11,6 @@ let
transformOptions = stripStorePathsFromDeclarations;
in
{
clanModulesViaRoles = lib.mapAttrs (
_moduleName: rolesOptions:
lib.mapAttrs (
_roleName: options:
(nixosOptionsDoc {
inherit options;
warningsAreErrors = true;
inherit transformOptions;
}).optionsJSON
) rolesOptions
) modulesRolesOptions;
# Test with:
# nix build .\#legacyPackages.x86_64-linux.clanModulesViaService
clanModulesViaService = lib.mapAttrs (

View File

@@ -33,8 +33,6 @@ from clan_lib.errors import ClanError
from clan_lib.services.modules import (
CategoryInfo,
Frontmatter,
extract_frontmatter,
get_roles,
)
# Get environment variables
@@ -43,12 +41,6 @@ CLAN_CORE_DOCS = Path(os.environ["CLAN_CORE_DOCS"])
CLAN_MODULES_FRONTMATTER_DOCS = os.environ.get("CLAN_MODULES_FRONTMATTER_DOCS")
BUILD_CLAN_PATH = os.environ.get("BUILD_CLAN_PATH")
## Clan modules ##
# Some modules can be imported via nix natively
CLAN_MODULES_VIA_NIX = os.environ.get("CLAN_MODULES_VIA_NIX")
# Some modules can be imported via inventory
CLAN_MODULES_VIA_ROLES = os.environ.get("CLAN_MODULES_VIA_ROLES")
# Options how to author clan.modules
# perInstance, perMachine, ...
CLAN_SERVICE_INTERFACE = os.environ.get("CLAN_SERVICE_INTERFACE")
@@ -505,154 +497,6 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
of.write(output)
def produce_clan_modules_docs() -> None:
if not CLAN_MODULES_VIA_NIX:
msg = f"Environment variables are not set correctly: $CLAN_MODULES_VIA_NIX={CLAN_MODULES_VIA_NIX}"
raise ClanError(msg)
if not CLAN_MODULES_VIA_ROLES:
msg = f"Environment variables are not set correctly: $CLAN_MODULES_VIA_ROLES={CLAN_MODULES_VIA_ROLES}"
raise ClanError(msg)
if not CLAN_CORE_PATH:
msg = f"Environment variables are not set correctly: $CLAN_CORE_PATH={CLAN_CORE_PATH}"
raise ClanError(msg)
if not OUT:
msg = f"Environment variables are not set correctly: $out={OUT}"
raise ClanError(msg)
modules_index = "# Modules Overview\n\n"
modules_index += clan_modules_descr
modules_index += "## Overview\n\n"
modules_index += '<div class="grid cards" markdown>\n\n'
with Path(CLAN_MODULES_VIA_ROLES).open() as f2:
role_links: dict[str, dict[str, str]] = json.load(f2)
with Path(CLAN_MODULES_VIA_NIX).open() as f:
links: dict[str, str] = json.load(f)
for module_name, options_file in links.items():
print(f"Rendering ClanModule: {module_name}")
readme_file = CLAN_CORE_PATH / "clanModules" / module_name / "README.md"
with readme_file.open() as f:
readme = f.read()
frontmatter: Frontmatter
frontmatter, readme_content = extract_frontmatter(readme, str(readme_file))
# skip if experimental feature enabled
if "experimental" in frontmatter.features:
print(f"Skipping {module_name}: Experimental feature")
continue
modules_index += build_option_card(module_name, frontmatter)
##### Print module documentation #####
# 1. Header
output = module_header(module_name, "inventory" in frontmatter.features)
# 2. Description from README.md
if frontmatter.description:
output += f"*{frontmatter.description}*\n\n"
# 2. Deprecation note if the module is deprecated
if "deprecated" in frontmatter.features:
output += f"""
!!! Warning "Deprecated"
The `{module_name}` module is deprecated.*
Use 'clanServices/{module_name}' or a similar successor instead
"""
else:
output += f"""
!!! Warning "Will be deprecated"
The `{module_name}` module might eventually be migrated to 'clanServices'*
See: [clanServices](../../guides/clanServices.md)
"""
# 3. Categories from README.md
output += "## Categories\n\n"
output += render_categories(frontmatter.categories, frontmatter.categories_info)
output += "\n---\n\n"
# 3. README.md content
output += f"{readme_content}\n"
# 4. Usage
##### Print usage via Inventory #####
# get_roles(str) -> list[str] | None
# if not isinstance(options_file, str):
roles = get_roles(CLAN_CORE_PATH / "clanModules" / module_name)
if roles:
# Render inventory usage
output += """## Usage via Inventory\n\n"""
output += render_roles(roles, module_name)
for role in roles:
role_options_file = role_links[module_name][role]
# Abort if the options file is not found
if not isinstance(role_options_file, str):
print(
f"Error: module: {module_name} in role: {role} - options file not found, Got {role_options_file}"
)
exit(1)
no_options = f"""### Options of `{role}` role
**The `{module_name}` `{role}` doesnt offer / require any options to be set.**
"""
heading = f"""### Options of `{role}` role
The following options are available when using the `{role}` role.
"""
output += print_options(
role_options_file,
heading,
no_options,
replace_prefix=f"clan.{module_name}",
)
else:
# No roles means no inventory usage
output += """## Usage via Inventory
**This module cannot be used via the inventory interface.**
"""
##### Print usage via Nix / nixos #####
if not isinstance(options_file, str):
print(
f"Skipping {module_name}: Cannot be used via import clanModules.{module_name}"
)
output += """## Usage via Nix
**This module cannot be imported directly in your nixos configuration.**
"""
else:
output += module_nix_usage(module_name)
no_options = "** This module doesnt require any options to be set.**"
output += print_options(options_file, options_head, no_options)
outfile = Path(OUT) / f"clanModules/{module_name}.md"
outfile.parent.mkdir(
parents=True,
exist_ok=True,
)
with outfile.open("w") as of:
of.write(output)
modules_index += "</div>"
modules_index += "\n"
modules_outfile = Path(OUT) / "clanModules/index.md"
with modules_outfile.open("w") as of:
of.write(modules_index)
def build_option_card(module_name: str, frontmatter: Frontmatter) -> str:
"""
Build the overview index card for each reference target option.
@@ -863,8 +707,6 @@ if __name__ == "__main__": #
produce_clan_core_docs()
produce_clan_service_author_docs()
# produce_clan_modules_docs()
produce_clan_service_docs()
# produce_clan_modules_frontmatter_docs()

View File

@@ -67,7 +67,6 @@
clan = {
meta.name = "clan-core";
inventory = {
services = { };
machines = {
"test-darwin-machine" = {
machineClass = "darwin";

View File

@@ -52,57 +52,8 @@ let
# ''
evaled;
/*
This function takes a list of module names and evaluates them
Returns a set of interfaces as described below:
Fn :: { ${moduleName} = Module; } -> {
${moduleName} :: {
${roleName}: JSONSchema
}
}
*/
evalClanModulesWithRoles =
{
allModules,
clan-core,
pkgs,
}:
let
res = builtins.mapAttrs (
moduleName: module:
let
frontmatter = clanLib.modules.getFrontmatter allModules.${moduleName} moduleName;
roles =
if builtins.elem "inventory" frontmatter.features or [ ] then
assert lib.isPath module;
clan-core.clanLib.modules.getRoles "Documentation: inventory.modules" allModules moduleName
else
[ ];
in
lib.listToAttrs (
lib.map (role: {
name = role;
value =
(lib.evalModules {
class = "nixos";
modules = [
(baseModule { inherit pkgs; })
clan-core.nixosModules.clanCore
{
clan.core.settings.directory = clan-core;
}
# Role interface
(module + "/roles/${role}.nix")
];
}).options.clan.${moduleName} or { };
}) roles
)
) allModules;
in
res;
in
{
evalClanModules = evalClanModulesLegacy;
inherit evalClanModulesWithRoles;
}

View File

@@ -3,51 +3,6 @@ let
# Trim the .nix extension from a filename
trimExtension = name: builtins.substring 0 (builtins.stringLength name - 4) name;
jsonWithoutHeader = clanLib.jsonschema {
includeDefaults = true;
header = { };
};
getModulesSchema =
{
modules,
clan-core,
pkgs,
}:
lib.mapAttrs
(
_moduleName: rolesOptions:
lib.mapAttrs (_roleName: options: jsonWithoutHeader.parseOptions options { }) rolesOptions
)
(
clanLib.evalClan.evalClanModulesWithRoles {
allModules = modules;
inherit pkgs clan-core;
}
);
evalFrontmatter =
{
moduleName,
instanceName,
resolvedRoles,
allModules,
}:
lib.evalModules {
modules = [
(getFrontmatter allModules.${moduleName} moduleName)
./interface.nix
{
constraints.imports = [
(lib.modules.importApply ../constraints {
inherit moduleName resolvedRoles instanceName;
allRoles = getRoles "inventory.modules" allModules moduleName;
})
];
}
];
};
# For Documentation purposes only
frontmatterOptions =
(lib.evalModules {
@@ -119,17 +74,12 @@ let
builtins.readDir (checkedPath)
)
);
checkConstraints = args: (evalFrontmatter args).config.constraints.assertions;
getFrontmatter = _modulepath: _modulename: "clanModules are removed!";
in
{
inherit
frontmatterOptions
getModulesSchema
getFrontmatter
checkConstraints
getRoles
;
}

View File

@@ -1,19 +1,11 @@
{
self,
self',
lib,
pkgs,
flakeOptions,
...
}:
let
modulesSchema = self.clanLib.modules.getModulesSchema {
modules = self.clanModules;
inherit pkgs;
clan-core = self;
};
jsonLib = self.clanLib.jsonschema { inherit includeDefaults; };
includeDefaults = true;
@@ -38,13 +30,6 @@ let
];
clanSchema = jsonLib.parseOptions (lib.filterAttrs (n: _v: lib.elem n include) clanOpts) { };
renderSchema = pkgs.writers.writePython3Bin "render-schema" {
flakeIgnore = [
"F401"
"E501"
];
} ./render_schema.py;
clan-schema-abstract = pkgs.stdenv.mkDerivation {
name = "clan-schema-files";
buildInputs = [ pkgs.cue ];
@@ -66,26 +51,6 @@ in
frontMatterSchema
clanSchema
inventorySchema
modulesSchema
renderSchema
clan-schema-abstract
;
# Inventory schema, with the modules schema added per role
inventory =
pkgs.runCommand "rendered"
{
buildInputs = [
pkgs.python3
self'.packages.clan-cli
];
}
''
export INVENTORY_SCHEMA_PATH=${builtins.toFile "inventory-schema.json" (builtins.toJSON inventorySchema)}
export MODULES_SCHEMA_PATH=${builtins.toFile "modules-schema.json" (builtins.toJSON modulesSchema)}
mkdir $out
# The python script will place the schemas in the output directory
exec python3 ${renderSchema}/bin/render-schema
'';
}

View File

@@ -1,162 +0,0 @@
"""
Python script to join the abstract inventory schema, with the concrete clan modules
Inventory has slots which are 'Any' type.
We dont want to evaluate the clanModules interface in nix, when evaluating the inventory
"""
import json
import os
from pathlib import Path
from typing import Any
from clan_lib.errors import ClanError
# Get environment variables
INVENTORY_SCHEMA_PATH = Path(os.environ["INVENTORY_SCHEMA_PATH"])
# { [moduleName] :: { [roleName] :: SCHEMA }}
MODULES_SCHEMA_PATH = Path(os.environ["MODULES_SCHEMA_PATH"])
OUT = os.environ.get("out")
if not INVENTORY_SCHEMA_PATH:
msg = f"Environment variables are not set correctly: INVENTORY_SCHEMA_PATH={INVENTORY_SCHEMA_PATH}."
raise ClanError(msg)
if not MODULES_SCHEMA_PATH:
msg = f"Environment variables are not set correctly: MODULES_SCHEMA_PATH={MODULES_SCHEMA_PATH}."
raise ClanError(msg)
if not OUT:
msg = f"Environment variables are not set correctly: OUT={OUT}."
raise ClanError(msg)
def service_roles_to_schema(
schema: dict[str, Any],
service_name: str,
roles: list[str],
roles_schemas: dict[str, dict[str, Any]],
# Original service properties: {'config': Schema, 'machines': Schema, 'meta': Schema, 'extraModules': Schema, ...?}
orig: dict[str, Any],
) -> dict[str, Any]:
"""
Add roles to the service schema
"""
# collect all the roles for the service, to form a type union
all_roles_schema: list[dict[str, Any]] = []
for role_name, role_schema in roles_schemas.items():
role_schema["title"] = f"{module_name}-config-role-{role_name}"
all_roles_schema.append(role_schema)
role_schema = {}
for role in roles:
role_schema[role] = {
"type": "object",
"additionalProperties": False,
"properties": {
**orig["roles"]["additionalProperties"]["properties"],
"config": {
**roles_schemas.get(role, {}),
"title": f"{service_name}-config-role-{role}",
"type": "object",
"default": {},
"additionalProperties": False,
},
},
}
machines_schema = {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
**orig["machines"]["additionalProperties"]["properties"],
"config": {
"title": f"{service_name}-config",
"oneOf": all_roles_schema,
"type": "object",
"default": {},
"additionalProperties": False,
},
},
},
}
services["properties"][service_name] = {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": False,
"properties": {
# Original inventory schema
**orig,
# Inject the roles schemas
"roles": {
"title": f"{service_name}-roles",
"type": "object",
"properties": role_schema,
"additionalProperties": False,
},
"machines": machines_schema,
"config": {
"title": f"{service_name}-config",
"oneOf": all_roles_schema,
"type": "object",
"default": {},
"additionalProperties": False,
},
},
},
}
return schema
if __name__ == "__main__":
print("Joining inventory schema with modules schema")
print(f"Inventory schema path: {INVENTORY_SCHEMA_PATH}")
print(f"Modules schema path: {MODULES_SCHEMA_PATH}")
modules_schema = {}
with Path.open(MODULES_SCHEMA_PATH) as f:
modules_schema = json.load(f)
inventory_schema = {}
with Path.open(INVENTORY_SCHEMA_PATH) as f:
inventory_schema = json.load(f)
services = inventory_schema["properties"]["services"]
original_service_props = services["additionalProperties"]["additionalProperties"][
"properties"
].copy()
# Init the outer services schema
# Properties (service names) will be filled in the next step
services = {
"type": "object",
"properties": {
# Service names
},
"additionalProperties": False,
}
for module_name, roles_schemas in modules_schema.items():
# Add the roles schemas to the service schema
roles = list(roles_schemas.keys())
if roles:
services = service_roles_to_schema(
services,
module_name,
roles,
roles_schemas,
original_service_props,
)
inventory_schema["properties"]["services"] = services
outpath = Path(OUT)
with (outpath / "schema.json").open("w") as f:
json.dump(inventory_schema, f, indent=2)
with (outpath / "modules_schemas.json").open("w") as f:
json.dump(modules_schema, f, indent=2)

View File

@@ -1,78 +0,0 @@
# Integrity validation of the inventory
{ config, lib, ... }:
{
# Assertion must be of type
# { assertion :: bool, message :: string, severity :: "error" | "warning" }
imports = [
# Check that each machine used in a service is defined in the top-level machines
{
assertions = lib.foldlAttrs (
ass1: serviceName: c:
ass1
++ lib.foldlAttrs (
ass2: instanceName: instanceConfig:
let
topLevelMachines = lib.attrNames config.machines;
# All machines must be defined in the top-level machines
assertions = lib.foldlAttrs (
assertions: roleName: role:
assertions
++ builtins.filter (a: !a.assertion) (
builtins.map (m: {
assertion = builtins.elem m topLevelMachines;
message = ''
Machine '${m}' is not defined in the inventory. This might still work, if the machine is defined via nix.
Defined in service: '${serviceName}' instance: '${instanceName}' role: '${roleName}'.
Inventory machines:
${builtins.concatStringsSep "\n" (map (n: "'${n}'") topLevelMachines)}
'';
severity = "warning";
}) role.machines
)
) [ ] instanceConfig.roles;
in
ass2 ++ assertions
) [ ] c
) [ ] config.services;
}
# Check that each tag used in a role is defined in at least one machines tags
{
assertions = lib.foldlAttrs (
ass1: serviceName: c:
ass1
++ lib.foldlAttrs (
ass2: instanceName: instanceConfig:
let
allTags = lib.foldlAttrs (
tags: _machineName: machine:
tags ++ machine.tags
) [ ] config.machines;
# All machines must be defined in the top-level machines
assertions = lib.foldlAttrs (
assertions: roleName: role:
assertions
++ builtins.filter (a: !a.assertion) (
builtins.map (m: {
assertion = builtins.elem m allTags;
message = ''
Tag '${m}' is not defined in the inventory.
Defined in service: '${serviceName}' instance: '${instanceName}' role: '${roleName}'.
Available tags:
${builtins.concatStringsSep "\n" (map (n: "'${n}'") allTags)}
'';
severity = "error";
}) role.tags
)
) [ ] instanceConfig.roles;
in
ass2 ++ assertions
) [ ] c
) [ ] config.services;
}
];
}

View File

@@ -1,268 +1,5 @@
{
lib,
config,
clanLib,
...
}:
let
inherit (config) inventory directory;
resolveTags =
# Inventory, { machines :: [string], tags :: [string] }
{
serviceName,
instanceName,
roleName,
inventory,
members,
}:
{
machines =
members.machines or [ ]
++ (builtins.foldl' (
acc: tag:
let
# For error printing
availableTags = lib.foldlAttrs (
acc: _: v:
v.tags or [ ] ++ acc
) [ ] (inventory.machines);
tagMembers = builtins.attrNames (
lib.filterAttrs (_n: v: builtins.elem tag v.tags or [ ]) inventory.machines
);
in
if tagMembers == [ ] then
lib.warn ''
inventory.services.${serviceName}.${instanceName}: - ${roleName} tags: no machine with tag '${tag}' found.
Available tags: ${builtins.toJSON (lib.unique availableTags)}
'' [ ]
else
acc ++ tagMembers
) [ ] members.tags or [ ]);
};
checkService =
modulepath: serviceName:
builtins.elem "inventory" (clanLib.modules.getFrontmatter modulepath serviceName).features or [ ];
compileMachine =
{ machineConfig }:
{
machineImports = [
(lib.optionalAttrs (machineConfig.deploy.targetHost or null != null) {
config.clan.core.networking.targetHost = lib.mkForce machineConfig.deploy.targetHost;
})
(lib.optionalAttrs (machineConfig.deploy.buildHost or null != null) {
config.clan.core.networking.buildHost = lib.mkForce machineConfig.deploy.buildHost;
})
];
assertions = { };
};
resolveImports =
{
supportedRoles,
resolvedRolesPerInstance,
serviceConfigs,
serviceName,
machineName,
getRoleFile,
}:
(lib.foldlAttrs (
# : [ Modules ] -> String -> ServiceConfig -> [ Modules ]
acc2: instanceName: serviceConfig:
let
resolvedRoles = resolvedRolesPerInstance.${instanceName};
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 don't 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
);
features =
(clanLib.modules.getFrontmatter inventory.modules.${serviceName} serviceName).features or [ ];
deprecationWarning = lib.optionalAttrs (builtins.elem "deprecated" features) {
warnings = [
''
The '${serviceName}' module has been migrated from `inventory.services` to `inventory.instances`
See https://docs.clan.lol/guides/clanServices/ for usage.
''
];
};
in
if !(serviceConfig.enabled or true) then
acc2
else if isInService then
acc2
++ [
deprecationWarning
{
imports = roleModules ++ extraModules;
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));
in
{
imports = [
./interface.nix
];
config = {
machines = builtins.mapAttrs (
machineName: machineConfig: m:
let
compiledServices = lib.mapAttrs (
_: serviceConfigs:
(
{ config, ... }:
let
serviceName = config.serviceName;
getRoleFile = role: builtins.seq role inventory.modules.${serviceName} + "/roles/${role}.nix";
in
{
_file = "inventory/builder.nix";
_module.args = {
inherit
resolveTags
inventory
clanLib
machineName
serviceConfigs
;
};
imports = [
./roles.nix
];
machineImports = resolveImports {
supportedRoles = config.supportedRoles;
resolvedRolesPerInstance = config.resolvedRolesPerInstance;
inherit
serviceConfigs
serviceName
machineName
getRoleFile
;
};
# Assertions
assertions = {
"checkservice.${serviceName}" = {
assertion = checkService inventory.modules.${serviceName} serviceName;
message = ''
Service ${serviceName} cannot be used in inventory. It does not declare the 'inventory' feature.
To allow it add the following to the beginning of the README.md of the module:
---
...
features = [ "inventory" ]
---
Also make sure to test the module with the 'inventory' feature enabled.
'';
};
};
}
)
) (config.inventory.services or { });
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
[
{
clan.inventory.assertions = {
"alive.assertion.inventory" = {
assertion = true;
message = ''
No failed assertions found for machine ${machineName}. This will never be displayed.
It is here for testing purposes.
'';
};
};
}
];
in
acc
++ service.machineImports
# Import failed assertions
++ failedAssertionsImports
) [ ] (builtins.attrValues m.config.compiledServices)
);
in
{
inherit machineImports compiledServices compiledMachine;
}
) (inventory.machines or { });
};
}

View File

@@ -1,65 +0,0 @@
{
lib,
config,
resolveTags,
inventory,
clanLib,
machineName,
serviceConfigs,
...
}:
let
serviceName = config.serviceName;
in
{
# Roles resolution
# : List String
supportedRoles = clanLib.modules.getRoles "inventory.modules" inventory.modules serviceName;
matchedRoles = builtins.attrNames (
lib.filterAttrs (_: ms: builtins.elem machineName ms) config.machinesRoles
);
resolvedRolesPerInstance = lib.mapAttrs (
instanceName: instanceConfig:
let
resolvedRoles = lib.genAttrs config.supportedRoles (
roleName:
resolveTags {
members = instanceConfig.roles.${roleName} or { };
inherit
instanceName
serviceName
roleName
inventory
;
}
);
usedRoles = builtins.attrNames instanceConfig.roles;
unmatchedRoles = builtins.filter (role: !builtins.elem role config.supportedRoles) usedRoles;
in
if unmatchedRoles != [ ] then
throw ''
Roles ${builtins.toJSON unmatchedRoles} are not defined in the service ${serviceName}.
Instance: '${instanceName}'
Please use one of available roles: ${builtins.toJSON config.supportedRoles}
''
else
resolvedRoles
) serviceConfigs;
machinesRoles = builtins.zipAttrsWith (
_n: vs:
let
flat = builtins.foldl' (acc: s: acc ++ s.machines) [ ] vs;
in
lib.unique flat
) (builtins.attrValues config.resolvedRolesPerInstance);
assertions = lib.concatMapAttrs (
instanceName: resolvedRoles:
clanLib.modules.checkConstraints {
moduleName = serviceName;
allModules = inventory.modules;
inherit resolvedRoles instanceName;
}
) config.resolvedRolesPerInstance;
}

View File

@@ -94,7 +94,10 @@ let
in
{
imports = [
./assertions.nix
(lib.mkRemovedOptionModule [ "services" ] ''
The `inventory.services` option has been removed. Use `inventory.instances` instead.
See: https://docs.clan.lol/concepts/inventory/#services
'')
];
options = {
# Internal things
@@ -415,160 +418,5 @@ in
);
default = { };
};
services = lib.mkOption {
# TODO: deprecate these options
# services are deprecated in favor of `instances`
# visible = false;
description = ''
Services of the inventory.
- The first `<name>` is the moduleName. It must be a valid clanModule name.
- The second `<name>` is an arbitrary instance name.
???+ Example
```nix
# ClanModule name. See the module documentation for the available modules.
# Instance name, can be anything, some services might use it as a unique identifier.
services.borgbackup."instance_1" = {
roles.client.machines = ["machineA"];
};
```
!!! Note
Services MUST be added to machines via `roles` exclusively.
See [`roles.<rolename>.machines`](#inventory.services.roles.machines) or [`roles.<rolename>.tags`](#inventory.services.roles.tags) for more information.
'';
default = { };
type = types.attrsOf (
types.attrsOf (
types.submodule (
# instance name
{ name, ... }:
{
options.enabled = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Enable or disable the complete service.
If the service is disabled, it will not be added to any machine.
!!! Note
This flag is primarily used to temporarily disable a service.
I.e. A 'backup service' without any 'server' might be incomplete and would cause failure if enabled.
'';
};
options.meta = metaOptionsWith name;
options.extraModules = extraModulesOption;
options.config = moduleConfig // {
description = ''
Configuration of the specific clanModule.
!!! Note
Configuration is passed to the nixos configuration scoped to the module.
```nix
clan.<serviceName> = { ... # Config }
```
???+ Example
For `services.borgbackup` the config is the passed to the machine with the prefix of `clan.borgbackup`.
This means all config values are mapped to the `borgbackup` clanModule exclusively (`config.clan.borgbackup`).
```nix
{
services.borgbackup."instance_1".config = {
destinations = [ ... ];
# See the 'borgbackup' module docs for all options
};
}
```
!!! Note
The module author is responsible for supporting multiple instance configurations in different roles.
See each clanModule's documentation for more information.
'';
};
options.machines = lib.mkOption {
description = ''
Attribute set of machines specific config for the service.
Will be merged with other service configs, such as the role config and the global config.
For machine specific overrides use `mkForce` or other higher priority methods.
???+ Example
```{.nix hl_lines="4-7"}
services.borgbackup."instance_1" = {
roles.client.machines = ["machineA"];
machines.machineA.config = {
# Additional specific config for the machine
# This is merged with all other config places
};
};
```
'';
default = { };
type = types.attrsOf (
types.submodule {
options.extraModules = extraModulesOption;
options.config = moduleConfig // {
description = ''
Additional configuration of the specific machine.
See how [`service.<name>.<name>.config`](#inventory.services.config) works in general for further information.
'';
};
}
);
};
options.roles = lib.mkOption {
default = { };
type = types.attrsOf (
types.submodule {
options.machines = lib.mkOption {
default = [ ];
type = types.listOf types.str;
example = [ "machineA" ];
description = ''
List of machines which are part of the role.
The machines are referenced by their `attributeName` in the `inventory.machines` attribute set.
Memberships are declared here to determine which machines are part of the service.
Alternatively, `tags` can be used to determine the membership, more dynamically.
'';
};
options.tags = lib.mkOption {
default = [ ];
apply = lib.unique;
type = types.listOf types.str;
description = ''
List of tags which are used to determine the membership of the role.
The tags are matched against the `inventory.machines.<machineName>.tags` attribute set.
If a machine has at least one tag of the role, it is part of the role.
'';
};
options.config = moduleConfig // {
description = ''
Additional configuration of the specific role.
See how [`service.<name>.<name>.config`](#inventory.services.config) works in general for further information.
'';
};
options.extraModules = extraModulesOption;
}
);
};
}
)
)
);
};
};
}

View File

@@ -11,6 +11,7 @@
default =
builtins.removeAttrs (clanLib.introspection.getPrios { options = config.inventory.options; })
# tags are freeformType which is not supported yet.
[ "tags" ];
# services is removed and throws an error if accessed.
[ "tags" "services"];
};
}