inventory: Add roles.<name>.description option and a warning if it is not set

This commit is contained in:
Qubasa
2025-10-01 18:28:11 +02:00
parent b344db021b
commit 2df96d3a9b
7 changed files with 75 additions and 10 deletions

View File

@@ -9,7 +9,7 @@
# TODO: a client can only be in one instance, add constraint # TODO: a client can only be in one instance, add constraint
roles.server = { roles.server = {
description = "A borgbackup server that stores the backups of clients.";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
@@ -62,6 +62,7 @@
}; };
roles.client = { roles.client = {
description = "A borgbackup client that backs up to one or more borgbackup servers.";
interface = interface =
{ {
lib, lib,

View File

@@ -140,6 +140,12 @@
imports = [ imports = [
# Import the resolved module. # Import the resolved module.
# i.e. clan.modules.admin # i.e. clan.modules.admin
{
options.module = lib.mkOption {
type = lib.types.raw;
default = (builtins.head instances).instance.module;
};
}
(builtins.head instances).instance.resolvedModule (builtins.head instances).instance.resolvedModule
] # Include all the instances that correlate to the resolved module ] # Include all the instances that correlate to the resolved module
++ (builtins.map (v: { ++ (builtins.map (v: {

View File

@@ -381,6 +381,13 @@ in
roleName = name; roleName = name;
in in
{ {
options.description = mkOption {
type = lib.types.nullOr types.str;
description = "A short description of the role '${name}', explaining it's effect on the supplied machine.";
example = "Connects the supplied machine as a '${name}' to the 'example' service.";
default = null;
};
options.interface = mkOption { options.interface = mkOption {
description = '' description = ''
Abstract interface of the role. Abstract interface of the role.
@@ -959,8 +966,21 @@ in
( (
let let
failedAssertions = (lib.filterAttrs (_: v: !v.assertion) config.result.assertions); failedAssertions = (lib.filterAttrs (_: v: !v.assertion) config.result.assertions);
formatModule =
if config.module.input != null then
"${config.module.input}/${config.module.name}"
else
"<clan-core>/${config.module.name}";
warningsWithNull = lib.mapAttrsToList (
roleName: roleConfig:
if (roleConfig.description == null) then
"Missing description for role '${roleName}' of clanService '${formatModule}'"
else
null
) config.roles;
in in
{ {
warnings = (lib.filter (v: v != null) warningsWithNull);
assertions = lib.attrValues failedAssertions; assertions = lib.attrValues failedAssertions;
} }
) )

View File

@@ -19,7 +19,7 @@ let
in in
{ {
manifest = eval.config.manifest; manifest = eval.config.manifest;
roles = lib.mapAttrs (_n: _v: { }) eval.config.roles; roles = lib.mapAttrs (_n: v: { inherit (v) description; }) eval.config.roles;
}; };
in in
{ {

View File

@@ -37,8 +37,14 @@ const mockFetcher: Fetcher = <K extends OperationNames>(
description: "This is module A", description: "This is module A",
}, },
roles: { roles: {
client: null, client: {
server: null, name: "client",
description: null,
},
server: {
name: "server",
description: null,
},
}, },
}, },
}, },
@@ -52,9 +58,18 @@ const mockFetcher: Fetcher = <K extends OperationNames>(
description: "This is module B", description: "This is module B",
}, },
roles: { roles: {
peer: null, peer: {
moon: null, name: "peer",
controller: null, description: null,
},
moon: {
name: "moon",
description: null,
},
controller: {
name: "controller",
description: null,
},
}, },
}, },
}, },

View File

@@ -147,10 +147,16 @@ def extract_frontmatter[T](
) )
@dataclass(frozen=True, eq=True)
class Role:
name: str
description: str | None = None
@dataclass @dataclass
class ModuleInfo: class ModuleInfo:
manifest: ModuleManifest manifest: ModuleManifest
roles: dict[str, None] roles: dict[str, Role]
@dataclass @dataclass
@@ -242,6 +248,9 @@ def list_service_modules(flake: Flake) -> ClanModules:
"input": None if input_name == clan_input_name else input_name, "input": None if input_name == clan_input_name else input_name,
} }
) )
roles = module_info.get("roles", {})
res.append( res.append(
Module( Module(
instance_refs=find_instance_refs_for_module( instance_refs=find_instance_refs_for_module(
@@ -249,7 +258,13 @@ def list_service_modules(flake: Flake) -> ClanModules:
), ),
usage_ref=module_ref, usage_ref=module_ref,
info=ModuleInfo( info=ModuleInfo(
roles=module_info.get("roles", {}), roles={
rname: Role(
name=rname,
description=roles[rname].get("description", None),
)
for rname in roles
},
manifest=ModuleManifest.from_dict(module_info["manifest"]), manifest=ModuleManifest.from_dict(module_info["manifest"]),
), ),
native=(input_name == clan_input_name), native=(input_name == clan_input_name),
@@ -462,7 +477,8 @@ def set_service_instance(
raise ClanError(msg) raise ClanError(msg)
module = resolve_service_module_ref(flake, module_ref) module = resolve_service_module_ref(flake, module_ref)
allowed_roles = module.info.roles.keys()
allowed_roles = list(module.info.roles)
for role_name in roles: for role_name in roles:
if role_name not in allowed_roles: if role_name not in allowed_roles:

View File

@@ -62,6 +62,13 @@ def test_list_service_instances(
assert instances["baz"].resolved.usage_ref.get("input") is None assert instances["baz"].resolved.usage_ref.get("input") is None
assert instances["baz"].resolved.usage_ref.get("name") == "sshd" assert instances["baz"].resolved.usage_ref.get("name") == "sshd"
borgbackup_service = next(
m for m in service_modules.modules if m.usage_ref.get("name") == "borgbackup"
)
# Module has roles with descriptions
assert borgbackup_service.info.roles["client"].description is not None
assert borgbackup_service.info.roles["server"].description is not None
@pytest.mark.with_core @pytest.mark.with_core
def test_list_service_modules( def test_list_service_modules(