Merge pull request 'Docs for clan service options' (#3670) from service-docs into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3670
This commit is contained in:
hsjobeki
2025-05-16 13:02:28 +00:00
6 changed files with 252 additions and 18 deletions

View File

@@ -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";
};
};
};

View File

@@ -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

View File

@@ -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;

View File

@@ -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 =

View File

@@ -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 = """<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">"""
for cat in categories:
color = cat_info[cat]["color"]
# description = cat_info[cat]["description"]
color = categories_info[cat]["color"]
res += f"""
<div style="background-color: {color}; color: white; padding: 10px; border-radius: 20px; text-align: center;">
{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()

View File

@@ -0,0 +1,76 @@
# Using `clanServices`
Clans `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.<instance_name>
```
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 Clans 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.
---
## Whats Next?
* [Author your own clanService →](../authoring/clanServices/index.md)
* [Migrate from clanModules →](../guides/migrate-inventory-services.md)
<!-- TODO: * [Understand the architecture →](../explanation/clan-architecture.md) -->