diff --git a/clanServices/hello-world/default.nix b/clanServices/hello-world/default.nix index 86bfbdb1e..15e53a7b3 100644 --- a/clanServices/hello-world/default.nix +++ b/clanServices/hello-world/default.nix @@ -3,6 +3,7 @@ { _class = "clan.service"; manifest.name = "clan-core/hello-word"; + manifest.description = "This is a test"; roles.peer = { interface = @@ -10,6 +11,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..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 @@ -85,6 +86,11 @@ 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/index.md + - 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..1451ef24b 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,83 @@ 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) + + 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) + + 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 +552,27 @@ 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" + + # 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) + output += render_categories(frontmatter.categories, frontmatter.categories_info) output += "\n---\n\n" # 3. README.md content @@ -785,7 +897,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 +941,6 @@ if __name__ == "__main__": # produce_inventory_docs() produce_clan_modules_docs() + produce_clan_service_docs() produce_clan_modules_frontmatter_docs() 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) +