From da3c1ceea3f6d4309a449a8081589c3245981f48 Mon Sep 17 00:00:00 2001 From: pinpox Date: Thu, 15 May 2025 22:20:01 +0200 Subject: [PATCH 1/2] Add clanServices to docs Renders the documentation for clanServices. Options for the modules are extracted and rendered the same way as for the existing clanModules. Additionally tweaks the typography for the documentation of options slightly --- clanServices/hello-world/default.nix | 15 ++++ docs/mkdocs.yml | 4 ++ docs/nix/flake-module.nix | 11 ++- docs/nix/get-module-docs.nix | 27 +++++++ docs/nix/render_options/__init__.py | 104 ++++++++++++++++++++++----- 5 files changed, 143 insertions(+), 18 deletions(-) diff --git a/clanServices/hello-world/default.nix b/clanServices/hello-world/default.nix index 86bfbdb1e..fca39d13d 100644 --- a/clanServices/hello-world/default.nix +++ b/clanServices/hello-world/default.nix @@ -3,6 +3,19 @@ { _class = "clan.service"; manifest.name = "clan-core/hello-word"; + manifest.description = "This is a test"; + + roles.test = { + interface = + { lib, ... }: + { + options.foo = lib.mkOption { + type = lib.types.str; + # default = ""; + description = "Some option"; + }; + }; + }; roles.peer = { interface = @@ -10,6 +23,8 @@ { options.foo = lib.mkOption { type = lib.types.str; + # default = ""; + description = "Some option"; }; }; }; diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 7512e3578..19c931449 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -85,6 +85,10 @@ nav: - 02-clan-api: decisions/02-clan-api.md - 03-adr-numbering-process: decisions/03-adr-numbering-process.md - Template: decisions/_template.md + - Clan Services: + - reference/clanServices/admin.md + - reference/clanServices/hello-world.md + - reference/clanServices/wifi.md - Clan Modules: - Overview: - reference/clanModules/index.md diff --git a/docs/nix/flake-module.nix b/docs/nix/flake-module.nix index 767d7c4e1..bfba3e72b 100644 --- a/docs/nix/flake-module.nix +++ b/docs/nix/flake-module.nix @@ -36,6 +36,9 @@ # 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); + # Simply evaluated options (JSON) renderOptions = pkgs.runCommand "render-options" @@ -85,6 +88,7 @@ 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_MODULES_VIA_NIX=${clanModulesViaNix} # Frontmatter format for clanModules export CLAN_MODULES_FRONTMATTER_DOCS=${clanModulesFrontmatter}/share/doc/nixos/options.json @@ -100,7 +104,12 @@ in { legacyPackages = { - inherit jsonDocs clanModulesViaNix clanModulesViaRoles; + inherit + jsonDocs + clanModulesViaNix + clanModulesViaRoles + clanModulesViaService + ; }; devShells.docs = pkgs.callPackage ./shell.nix { inherit (self'.packages) docs clan-cli-docs inventory-api-docs; diff --git a/docs/nix/get-module-docs.nix b/docs/nix/get-module-docs.nix index 076a7f650..13cd80b43 100644 --- a/docs/nix/get-module-docs.nix +++ b/docs/nix/get-module-docs.nix @@ -36,6 +36,33 @@ ) rolesOptions ) modulesRolesOptions; + # Test with: + # nix build .\#legacyPackages.x86_64-linux.clanModulesViaService + clanModulesViaService = lib.mapAttrs ( + _moduleName: moduleValue: + let + evaluatedService = clan-core.clanLib.inventory.evalClanService { + modules = [ moduleValue ]; + prefix = [ ]; + }; + in + { + roles = lib.mapAttrs ( + _roleName: role: + + (nixosOptionsDoc { + transformOptions = + opt: if lib.strings.hasPrefix "_" opt.name then opt // { visible = false; } else opt; + options = (lib.evalModules { modules = [ role.interface ]; }).options; + warningsAreErrors = true; + }).optionsJSON + ) evaluatedService.config.roles; + + manifest = evaluatedService.config.manifest; + + } + ) clan-core.clan.modules; + clanCore = (nixosOptionsDoc { options = diff --git a/docs/nix/render_options/__init__.py b/docs/nix/render_options/__init__.py index fa63c2592..57f56c280 100644 --- a/docs/nix/render_options/__init__.py +++ b/docs/nix/render_options/__init__.py @@ -30,7 +30,12 @@ from pathlib import Path from typing import Any from clan_cli.errors import ClanError -from clan_lib.api.modules import Frontmatter, extract_frontmatter, get_roles +from clan_lib.api.modules import ( + CategoryInfo, + Frontmatter, + extract_frontmatter, + get_roles, +) # Get environment variables CLAN_CORE_PATH = Path(os.environ["CLAN_CORE_PATH"]) @@ -44,6 +49,7 @@ 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") +CLAN_MODULES_VIA_SERVICE = os.environ.get("CLAN_MODULES_VIA_SERVICE") OUT = os.environ.get("out") @@ -58,7 +64,8 @@ def replace_store_path(text: str) -> tuple[str, str]: res = "https://git.clan.lol/clan/clan-core/src/branch/main/" + str( Path(*Path(text).parts[4:]) ) - name = Path(res).name + # name = Path(res).name + name = str(Path(*Path(text).parts[4:])) return (res, name) @@ -149,8 +156,12 @@ def render_option( decls = option.get("declarations", []) if decls: source_path, name = replace_store_path(decls[0]) + + name = name.split(",")[0] + source_path = source_path.split(",")[0] + res += f""" -:simple-git: [{name}]({source_path}) +:simple-git: Declared in: [{name}]({source_path}) """ res += "\n\n" @@ -221,7 +232,8 @@ def produce_clan_modules_frontmatter_docs() -> None: # header output = """# Frontmatter -Every clan module has a `frontmatter` section within its readme. It provides machine readable metadata about the module. +Every clan module has a `frontmatter` section within its readme. It provides +machine readable metadata about the module. !!! example @@ -246,7 +258,8 @@ Every clan module has a `frontmatter` section within its readme. It provides mac output += """## Overview -This provides an overview of the available attributes of the `frontmatter` within the `README.md` of a clan module. +This provides an overview of the available attributes of the `frontmatter` +within the `README.md` of a clan module. """ # for option_name, info in options.items(): @@ -331,7 +344,7 @@ def produce_clan_core_docs() -> None: def render_roles(roles: list[str] | None, module_name: str) -> str: if roles: - roles_list = "\n".join([f" - `{r}`" for r in roles]) + roles_list = "\n".join([f"- `{r}`" for r in roles]) return ( f""" ### Roles @@ -341,7 +354,7 @@ This module can be used via predefined roles {roles_list} """ """ -Every role has its own configuration options. Which are each listed below. +Every role has its own configuration options, which are each listed below. For more information, see the [inventory guide](../../manual/inventory.md). @@ -350,8 +363,10 @@ For more information, see the [inventory guide](../../manual/inventory.md). `clan.admin.allowedkeys` - This means there are two equivalent ways to set the `allowedkeys` option. Either via a nixos module or via the inventory interface. - **But it is recommended to keep together `imports` and `config` to preserve locality of the module configuration.** + This means there are two equivalent ways to set the `allowedkeys` option. + Either via a nixos module or via the inventory interface. + **But it is recommended to keep together `imports` and `config` to preserve + locality of the module configuration.** === "Inventory" @@ -383,7 +398,11 @@ For more information, see the [inventory guide](../../manual/inventory.md). return "" -clan_modules_descr = """Clan modules are [NixOS modules](https://wiki.nixos.org/wiki/NixOS_modules) which have been enhanced with additional features provided by Clan, with certain option types restricted to enable configuration through a graphical interface. +clan_modules_descr = """ +Clan modules are [NixOS modules](https://wiki.nixos.org/wiki/NixOS_modules) +which have been enhanced with additional features provided by Clan, with +certain option types restricted to enable configuration through a graphical +interface. !!! note "๐Ÿ”น" Modules with this indicator support the [inventory](../../manual/inventory.md) feature. @@ -391,12 +410,12 @@ clan_modules_descr = """Clan modules are [NixOS modules](https://wiki.nixos.org/ """ -def render_categories(categories: list[str], frontmatter: Frontmatter) -> str: - cat_info = frontmatter.categories_info +def render_categories( + categories: list[str], categories_info: dict[str, CategoryInfo] +) -> str: res = """
""" for cat in categories: - color = cat_info[cat]["color"] - # description = cat_info[cat]["description"] + color = categories_info[cat]["color"] res += f"""
{cat} @@ -406,6 +425,56 @@ def render_categories(categories: list[str], frontmatter: Frontmatter) -> str: return res +def produce_clan_service_docs() -> None: + if not CLAN_MODULES_VIA_SERVICE: + msg = f"Environment variables are not set correctly: $CLAN_MODULES_VIA_SERVICE={CLAN_MODULES_VIA_SERVICE}" + 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) + + with Path(CLAN_MODULES_VIA_SERVICE).open() as f3: + service_links: dict[str, dict[str, dict[str, Any]]] = json.load(f3) + + for module_name, module_info in service_links.items(): + output = f"# {module_name}\n\n" + # output += f"`clan.modules.{module_name}`\n" + output += f"*{module_info['manifest']['description']}*\n" + + fm = Frontmatter("") + # output += "## Categories\n\n" + output += render_categories( + module_info["manifest"]["categories"], fm.categories_info + ) + output += "\n---\n\n## Roles\n" + + output += f"The {module_name} module has the following roles:\n\n" + + for role_name, _ in module_info["roles"].items(): + output += f"- {role_name}\n" + + for role_name, role_filename in module_info["roles"].items(): + output += print_options( + role_filename, + f"## Options for the `{role_name}` role", + "This role has no configuration", + replace_prefix=f"clan.{module_name}", + ) + + outfile = Path(OUT) / f"clanServices/{module_name}.md" + outfile.parent.mkdir( + parents=True, + exist_ok=True, + ) + with outfile.open("w") as of: + 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}" @@ -456,11 +525,11 @@ def produce_clan_modules_docs() -> None: # 2. Description from README.md if frontmatter.description: - output += f"**{frontmatter.description}**\n\n" + output += f"*{frontmatter.description}*\n\n" # 3. Categories from README.md output += "## Categories\n\n" - output += render_categories(frontmatter.categories, frontmatter) + output += render_categories(frontmatter.categories, frontmatter.categories_info) output += "\n---\n\n" # 3. README.md content @@ -785,7 +854,7 @@ def options_docs_from_tree( root: Option, init_level: int = 1, prefix: list[str] | None = None ) -> str: """ - Render the options from the tree structure. + eender the options from the tree structure. Args: root (Option): The root option node. @@ -829,5 +898,6 @@ if __name__ == "__main__": # produce_inventory_docs() produce_clan_modules_docs() + produce_clan_service_docs() produce_clan_modules_frontmatter_docs() From 62d2c08dd53cf719e4d49864a003e5ca3ae83688 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 16 May 2025 13:56:33 +0200 Subject: [PATCH 2/2] Docs: improve clanService docs --- clanServices/hello-world/default.nix | 12 ----- docs/mkdocs.yml | 2 + docs/nix/render_options/__init__.py | 43 ++++++++++++++++ docs/site/guides/clanServices.md | 76 ++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 docs/site/guides/clanServices.md diff --git a/clanServices/hello-world/default.nix b/clanServices/hello-world/default.nix index fca39d13d..15e53a7b3 100644 --- a/clanServices/hello-world/default.nix +++ b/clanServices/hello-world/default.nix @@ -5,18 +5,6 @@ manifest.name = "clan-core/hello-word"; manifest.description = "This is a test"; - roles.test = { - interface = - { lib, ... }: - { - options.foo = lib.mkOption { - type = lib.types.str; - # default = ""; - description = "Some option"; - }; - }; - }; - roles.peer = { interface = { lib, ... }: diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 19c931449..bc5a99ce8 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -54,6 +54,7 @@ nav: - Deploy Machine: getting-started/deploy.md - Continuous Integration: getting-started/check.md - Guides: + - clanServices: guides/clanServices.md - Disk Encryption: getting-started/disk-encryption.md - Mesh VPN: getting-started/mesh-vpn.md - Backup & Restore: getting-started/backups.md @@ -86,6 +87,7 @@ nav: - 03-adr-numbering-process: decisions/03-adr-numbering-process.md - Template: decisions/_template.md - Clan Services: + - reference/clanServices/index.md - reference/clanServices/admin.md - reference/clanServices/hello-world.md - reference/clanServices/wifi.md diff --git a/docs/nix/render_options/__init__.py b/docs/nix/render_options/__init__.py index 57f56c280..1451ef24b 100644 --- a/docs/nix/render_options/__init__.py +++ b/docs/nix/render_options/__init__.py @@ -438,6 +438,33 @@ def produce_clan_service_docs() -> None: msg = f"Environment variables are not set correctly: $out={OUT}" raise ClanError(msg) + indexfile = Path(OUT) / "clanServices/index.md" + indexfile.parent.mkdir( + parents=True, + exist_ok=True, + ) + index = "# Clan Services\n\n" + index += """ +**`clanServices`** are modular building blocks that simplify the configuration and orchestration of multi-host services. + +Each `clanService`: + +* Is a module of class **`clan.service`** +* Can define **roles** (e.g., `client`, `server`) +* Uses **`inventory.instances`** to configure where and how it is deployed +* Replaces the legacy `clanModules` and `inventory.services` system altogether + +!!! Note + `clanServices` are part of Clan's next-generation service model and are intended to replace `clanModules`. + + See [Migration Guide](../../guides/migrate-inventory-services.md) for help on migrating. + +Learn how to use `clanServices` in practice in the [Using clanServices guide](../../guides/clanServices.md). +""" + + with indexfile.open("w") as of: + of.write(index) + with Path(CLAN_MODULES_VIA_SERVICE).open() as f3: service_links: dict[str, dict[str, dict[str, Any]]] = json.load(f3) @@ -527,6 +554,22 @@ def produce_clan_modules_docs() -> None: 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}](../clanServices/{module_name}.md) 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) diff --git a/docs/site/guides/clanServices.md b/docs/site/guides/clanServices.md new file mode 100644 index 000000000..fb1cd2f88 --- /dev/null +++ b/docs/site/guides/clanServices.md @@ -0,0 +1,76 @@ +# Using `clanServices` + +Clanโ€™s `clanServices` system is a composable way to define and deploy services across machines. It replaces the legacy `clanModules` approach and introduces better structure, flexibility, and reuse. + +This guide shows how to **instantiate** a `clanService`, explains how service definitions are structured in your inventory, and how to pick or create services from modules exposed by flakes. + +--- + +## Overview + +A `clanService` is used in: + +```nix +inventory.instances. +``` + +Each instance includes a reference to a **module specification** โ€” this is how Clan knows which service module to use and where it came from. +You can reference services from any flake input, allowing you to compose services from multiple flake sources. + +--- + +## Basic Example + +Example of instantiating a `borgbackup` service using `clan-core`: + +```nix +inventory.instances = { + # Arbitrary unique name for this 'borgbackup' instance + borgbackup-example = { + module = { + name = "borgbackup"; # <-- Name of the module + input = "clan-core"; # <-- The flake input where the service is defined + }; + # Participation of the machines is defined via roles + roles.client.machines."machine-a" = {}; + roles.server.machines."backup-host" = {}; + }; +} +``` + +If you used `clan-core` as an input attribute for your flake: + +```nix + # โ†“ module.input = "clan-core" +inputs.clan-core.url = "git+https://git.clan.lol/clan/clan-core" +``` + +## Picking a clanService + +You can use services exposed by Clanโ€™s core module library, `clan-core`. + +๐Ÿ”— See: [List of Available Services in clan-core](../reference/clanServices/index.md) + +## Defining Your Own Service + +You can also author your own `clanService` modules. + +๐Ÿ”— Learn how to write your own service: [Authoring a clanService](../authoring/clanServices/index.md) + +You might expose your service module from your flake โ€” this makes it easy for other people to also use your module in their clan. + +--- + +## ๐Ÿ’ก Tips for Working with clanServices + +* You can add multiple inputs to your flake (`clan-core`, `your-org-modules`, etc.) to mix and match services. +* Each service instance is isolated by its key in `inventory.instances`, allowing you to deploy multiple versions or roles of the same service type. +* Roles can target different machines or be scoped dynamically. + +--- + +## Whatโ€™s Next? + +* [Author your own clanService โ†’](../authoring/clanServices/index.md) +* [Migrate from clanModules โ†’](../guides/migrate-inventory-services.md) +