From 2df96d3a9b5330e03e004a26616dd333bc493b97 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 1 Oct 2025 18:28:11 +0200 Subject: [PATCH] inventory: Add roles..description option and a warning if it is not set --- clanServices/borgbackup/default.nix | 3 ++- .../distributed-service/inventory-adapter.nix | 6 +++++ .../distributed-service/service-module.nix | 20 +++++++++++++++ .../service-list-from-inputs.nix | 2 +- .../src/workflows/Service/Service.stories.tsx | 25 +++++++++++++++---- pkgs/clan-cli/clan_lib/services/modules.py | 22 +++++++++++++--- .../clan_lib/services/modules_test.py | 7 ++++++ 7 files changed, 75 insertions(+), 10 deletions(-) diff --git a/clanServices/borgbackup/default.nix b/clanServices/borgbackup/default.nix index 29f20136c..fa7d575cb 100644 --- a/clanServices/borgbackup/default.nix +++ b/clanServices/borgbackup/default.nix @@ -9,7 +9,7 @@ # TODO: a client can only be in one instance, add constraint roles.server = { - + description = "A borgbackup server that stores the backups of clients."; interface = { lib, ... }: { @@ -62,6 +62,7 @@ }; roles.client = { + description = "A borgbackup client that backs up to one or more borgbackup servers."; interface = { lib, diff --git a/lib/modules/inventory/distributed-service/inventory-adapter.nix b/lib/modules/inventory/distributed-service/inventory-adapter.nix index 8edab9be0..66786b375 100644 --- a/lib/modules/inventory/distributed-service/inventory-adapter.nix +++ b/lib/modules/inventory/distributed-service/inventory-adapter.nix @@ -140,6 +140,12 @@ imports = [ # Import the resolved module. # i.e. clan.modules.admin + { + options.module = lib.mkOption { + type = lib.types.raw; + default = (builtins.head instances).instance.module; + }; + } (builtins.head instances).instance.resolvedModule ] # Include all the instances that correlate to the resolved module ++ (builtins.map (v: { diff --git a/lib/modules/inventory/distributed-service/service-module.nix b/lib/modules/inventory/distributed-service/service-module.nix index cbed6b04f..b578b976b 100644 --- a/lib/modules/inventory/distributed-service/service-module.nix +++ b/lib/modules/inventory/distributed-service/service-module.nix @@ -381,6 +381,13 @@ in roleName = name; 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 { description = '' Abstract interface of the role. @@ -959,8 +966,21 @@ in ( let failedAssertions = (lib.filterAttrs (_: v: !v.assertion) config.result.assertions); + formatModule = + if config.module.input != null then + "${config.module.input}/${config.module.name}" + else + "/${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 { + warnings = (lib.filter (v: v != null) warningsWithNull); assertions = lib.attrValues failedAssertions; } ) diff --git a/lib/modules/inventoryClass/service-list-from-inputs.nix b/lib/modules/inventoryClass/service-list-from-inputs.nix index 1ba0c4fab..bfeff1c2b 100644 --- a/lib/modules/inventoryClass/service-list-from-inputs.nix +++ b/lib/modules/inventoryClass/service-list-from-inputs.nix @@ -19,7 +19,7 @@ let in { manifest = eval.config.manifest; - roles = lib.mapAttrs (_n: _v: { }) eval.config.roles; + roles = lib.mapAttrs (_n: v: { inherit (v) description; }) eval.config.roles; }; in { diff --git a/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx b/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx index 843a97b0b..c38a72681 100644 --- a/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx +++ b/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx @@ -37,8 +37,14 @@ const mockFetcher: Fetcher = ( description: "This is module A", }, roles: { - client: null, - server: null, + client: { + name: "client", + description: null, + }, + server: { + name: "server", + description: null, + }, }, }, }, @@ -52,9 +58,18 @@ const mockFetcher: Fetcher = ( description: "This is module B", }, roles: { - peer: null, - moon: null, - controller: null, + peer: { + name: "peer", + description: null, + }, + moon: { + name: "moon", + description: null, + }, + controller: { + name: "controller", + description: null, + }, }, }, }, diff --git a/pkgs/clan-cli/clan_lib/services/modules.py b/pkgs/clan-cli/clan_lib/services/modules.py index d47f63463..3f002a1c4 100644 --- a/pkgs/clan-cli/clan_lib/services/modules.py +++ b/pkgs/clan-cli/clan_lib/services/modules.py @@ -147,10 +147,16 @@ def extract_frontmatter[T]( ) +@dataclass(frozen=True, eq=True) +class Role: + name: str + description: str | None = None + + @dataclass class ModuleInfo: manifest: ModuleManifest - roles: dict[str, None] + roles: dict[str, Role] @dataclass @@ -242,6 +248,9 @@ def list_service_modules(flake: Flake) -> ClanModules: "input": None if input_name == clan_input_name else input_name, } ) + + roles = module_info.get("roles", {}) + res.append( Module( instance_refs=find_instance_refs_for_module( @@ -249,7 +258,13 @@ def list_service_modules(flake: Flake) -> ClanModules: ), usage_ref=module_ref, 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"]), ), native=(input_name == clan_input_name), @@ -462,7 +477,8 @@ def set_service_instance( raise ClanError(msg) 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: if role_name not in allowed_roles: diff --git a/pkgs/clan-cli/clan_lib/services/modules_test.py b/pkgs/clan-cli/clan_lib/services/modules_test.py index ae2c4590e..27a32f70c 100644 --- a/pkgs/clan-cli/clan_lib/services/modules_test.py +++ b/pkgs/clan-cli/clan_lib/services/modules_test.py @@ -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("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 def test_list_service_modules(