From 4421ce006eb228e8e45be7ef58cf05f71e784244 Mon Sep 17 00:00:00 2001 From: DavHau Date: Tue, 24 Jun 2025 23:06:26 +0700 Subject: [PATCH] docs: add clan options search page This provides a simpler and more intuitive search over a flat list of possible options. Styling still to be improved --- docs/mkdocs.yml | 1 + docs/nix/default.nix | 2 + docs/nix/flake-module.nix | 5 +- docs/nix/options/flake-module.nix | 167 ++++++++++++++++++ docs/overrides/options.html | 14 ++ docs/site/options.md | 6 + flake.lock | 84 ++++++++- flake.nix | 3 + lib/inventory/build-inventory/interface.nix | 165 ++++++----------- .../build-inventory/roles-interface.nix | 86 +++++++++ 10 files changed, 421 insertions(+), 112 deletions(-) create mode 100644 docs/nix/options/flake-module.nix create mode 100644 docs/overrides/options.html create mode 100644 docs/site/options.md create mode 100644 lib/inventory/build-inventory/roles-interface.nix diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index a64b567e6..761fb575a 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -179,6 +179,7 @@ nav: - 04-fetching-nix-from-python: decisions/04-fetching-nix-from-python.md - 05-deployment-parameters: decisions/05-deployment-parameters.md - Template: decisions/_template.md + - Options: options.md docs_dir: site site_dir: out diff --git a/docs/nix/default.nix b/docs/nix/default.nix index d59b8b1ea..2f62debce 100644 --- a/docs/nix/default.nix +++ b/docs/nix/default.nix @@ -7,6 +7,7 @@ asciinema-player-css, roboto, fira-code, + docs-options, ... }: let @@ -55,5 +56,6 @@ pkgs.stdenv.mkDerivation { installPhase = '' cp -a out/ $out/ + cp -r ${docs-options} $out/options-page ''; } diff --git a/docs/nix/flake-module.nix b/docs/nix/flake-module.nix index 7b7b1fb67..815653dd3 100644 --- a/docs/nix/flake-module.nix +++ b/docs/nix/flake-module.nix @@ -1,5 +1,8 @@ { inputs, self, ... }: { + imports = [ + ./options/flake-module.nix + ]; perSystem = { config, @@ -124,7 +127,7 @@ packages = { docs = pkgs.python3.pkgs.callPackage ./default.nix { clan-core = self; - inherit (self'.packages) clan-cli-docs inventory-api-docs; + inherit (self'.packages) clan-cli-docs docs-options inventory-api-docs; inherit (inputs) nixpkgs; inherit module-docs; inherit asciinema-player-js; diff --git a/docs/nix/options/flake-module.nix b/docs/nix/options/flake-module.nix new file mode 100644 index 000000000..b21e71bd1 --- /dev/null +++ b/docs/nix/options/flake-module.nix @@ -0,0 +1,167 @@ +{ self, config, ... }: +{ + perSystem = + { + inputs', + lib, + ... + }: + let + inherit (lib) + mapAttrsToList + flip + mapAttrs + mkOption + types + splitString + stringLength + substring + ; + inherit (self) clanLib; + + serviceModules = self.clan.modules; + + baseHref = "/options-page/"; + + evalService = + serviceModule: + lib.evalModules { + modules = [ + { + imports = [ + serviceModule + ../../../lib/inventory/distributed-service/service-module.nix + ]; + } + ]; + }; + + getRoles = module: (evalService module).config.roles; + + getManifest = module: (evalService module).config.manifest; + + loadFile = file: if builtins.pathExists file then builtins.readFile file else ""; + + settingsModules = + module: flip mapAttrs (getRoles module) (_roleName: roleConfig: roleConfig.interface); + + # Map each letter to its capitalized version + capitalizeChar = + char: + { + a = "A"; + b = "B"; + c = "C"; + d = "D"; + e = "E"; + f = "F"; + g = "G"; + h = "H"; + i = "I"; + j = "J"; + k = "K"; + l = "L"; + m = "M"; + n = "N"; + o = "O"; + p = "P"; + q = "Q"; + r = "R"; + s = "S"; + t = "T"; + u = "U"; + v = "V"; + w = "W"; + x = "X"; + y = "Y"; + z = "Z"; + } + .${char}; + + title = + name: + let + # split by - + parts = splitString "-" name; + # capitalize first letter of each part + capitalize = part: (capitalizeChar (substring 0 1 part)) + substring 1 (stringLength part) part; + capitalizedParts = map capitalize parts; + in + builtins.concatStringsSep " " capitalizedParts; + + fakeInstanceOptions = + name: module: + let + manifest = getManifest module; + description = '' + # ${title name} (Clan Service) + + **${manifest.description}** + + ${loadFile (module._file + "/../README.md")} + + ${ + if manifest.categories != [ ] then + "Categories: " + builtins.concatStringsSep ", " manifest.categories + else + "No categories defined" + } + + ''; + in + { + options = { + _ = mkOption { + type = types.raw; + }; + instances.${name} = lib.mkOption { + inherit description; + type = types.submodule { + options.roles = flip mapAttrs (settingsModules module) ( + roleName: roleSettingsModule: + mkOption { + type = types.submodule { + imports = [ + (import ../../../lib/inventory/build-inventory/roles-interface.nix { + inherit clanLib; + nestedSettingsOption = mkOption { + type = types.raw; + description = '' + See [instances.${name}.roles.${roleName}.settings](${baseHref}?option_scope=0&option=instances.${name}.roles.${roleName}.settings) + ''; + }; + settingsOption = mkOption { + type = types.submoduleWith { + modules = [ roleSettingsModule ]; + }; + }; + }) + ]; + }; + } + ); + }; + }; + }; + }; + + mkScope = name: modules: { + inherit name; + modules = [ + (import ../../../lib/inventory/build-inventory/interface.nix { + inherit clanLib; + noInstanceOptions = true; + }) + ] ++ mapAttrsToList fakeInstanceOptions modules; + urlPrefix = "https://github.com/nix-community/dream2nix/blob/main/"; + }; + in + { + packages.docs-options = inputs'.nuschtos.packages.mkMultiSearch { + inherit baseHref; + title = "Clan Options"; + # scopes = mapAttrsToList mkScope serviceModules; + scopes = [ (mkScope "Clan Inventory" serviceModules) ]; + }; + }; +} diff --git a/docs/overrides/options.html b/docs/overrides/options.html new file mode 100644 index 000000000..45544c55a --- /dev/null +++ b/docs/overrides/options.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} {% block extrahead %} + +{% endblock %} {% block site_nav %}{% endblock %} {% block content %} {{ +page.content }} {% endblock %} diff --git a/docs/site/options.md b/docs/site/options.md new file mode 100644 index 000000000..ee666e02d --- /dev/null +++ b/docs/site/options.md @@ -0,0 +1,6 @@ +--- +template: options.html +--- + + + diff --git a/flake.lock b/flake.lock index e9ce8289e..91f8a32c0 100644 --- a/flake.lock +++ b/flake.lock @@ -67,6 +67,50 @@ "type": "github" } }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "ixx": { + "inputs": { + "flake-utils": [ + "nuschtos", + "flake-utils" + ], + "nixpkgs": [ + "nuschtos", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1748294338, + "narHash": "sha256-FVO01jdmUNArzBS7NmaktLdGA5qA3lUMJ4B7a05Iynw=", + "owner": "NuschtOS", + "repo": "ixx", + "rev": "cc5f390f7caf265461d4aab37e98d2292ebbdb85", + "type": "github" + }, + "original": { + "owner": "NuschtOS", + "ref": "v0.0.8", + "repo": "ixx", + "type": "github" + } + }, "nix-darwin": { "inputs": { "nixpkgs": [ @@ -128,6 +172,28 @@ "url": "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz" } }, + "nuschtos": { + "inputs": { + "flake-utils": "flake-utils", + "ixx": "ixx", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1749730855, + "narHash": "sha256-L3x2nSlFkXkM6tQPLJP3oCBMIsRifhIDPMQQdHO5xWo=", + "owner": "NuschtOS", + "repo": "search", + "rev": "8dfe5879dd009ff4742b668d9c699bc4b9761742", + "type": "github" + }, + "original": { + "owner": "NuschtOS", + "repo": "search", + "type": "github" + } + }, "root": { "inputs": { "data-mesher": "data-mesher", @@ -137,8 +203,9 @@ "nix-select": "nix-select", "nixos-facter-modules": "nixos-facter-modules", "nixpkgs": "nixpkgs", + "nuschtos": "nuschtos", "sops-nix": "sops-nix", - "systems": "systems", + "systems": "systems_2", "treefmt-nix": "treefmt-nix" } }, @@ -177,6 +244,21 @@ "type": "github" } }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "treefmt-nix": { "inputs": { "nixpkgs": [ diff --git a/flake.nix b/flake.nix index 7907da570..9d62fa441 100644 --- a/flake.nix +++ b/flake.nix @@ -34,6 +34,9 @@ treefmt-nix.follows = "treefmt-nix"; }; }; + + nuschtos.url = "github:NuschtOS/search"; + nuschtos.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = diff --git a/lib/inventory/build-inventory/interface.nix b/lib/inventory/build-inventory/interface.nix index ed3b4f488..f56c165a0 100644 --- a/lib/inventory/build-inventory/interface.nix +++ b/lib/inventory/build-inventory/interface.nix @@ -1,4 +1,8 @@ -{ clanLib }: +{ + clanLib, + # workaround for docs rendering to include fake instance options + noInstanceOptions ? false, +}: { lib, config, @@ -370,121 +374,62 @@ in ); }; - instances = lib.mkOption { - description = "Multi host service module instances"; - type = types.attrsOf ( - types.submoduleWith { - modules = [ - ( - { name, ... }: - { - options = { - # ModuleSpec - module = lib.mkOption { - type = types.submodule { - options.input = lib.mkOption { - type = types.nullOr types.str; - default = null; - defaultText = "Name of the input. Default to 'null' which means the module is local"; - description = '' - Name of the input. Default to 'null' which means the module is local - ''; - }; - options.name = lib.mkOption { - type = types.str; - default = name; - defaultText = ""; - description = '' - Attribute of the clan service module imported from the chosen input. - - Defaults to the name of the instance. - ''; - }; - }; - default = { }; - }; - roles = lib.mkOption { - default = { }; - type = types.attrsOf ( - types.submodule { - options = { - # TODO: deduplicate - machines = lib.mkOption { - type = types.attrsOf ( - types.submodule { - options.settings = lib.mkOption { - default = { }; - type = clanLib.types.uniqueDeferredSerializableModule; - }; - } - ); - default = { }; - }; - tags = lib.mkOption { - type = types.attrsOf (types.submodule { }); - default = { }; - }; - settings = lib.mkOption { - default = { }; - type = clanLib.types.uniqueDeferredSerializableModule; - }; - extraModules = lib.mkOption { + instances = + if noInstanceOptions then + { } + else + lib.mkOption { + description = "Multi host service module instances"; + type = types.attrsOf ( + types.submoduleWith { + modules = [ + ( + { name, ... }: + { + options = { + # ModuleSpec + module = lib.mkOption { + type = types.submodule { + options.input = lib.mkOption { + type = types.nullOr types.str; + default = null; + defaultText = "Name of the input. Default to 'null' which means the module is local"; description = '' - List of additionally imported `.nix` expressions. - - Supported types: - - - **Strings**: Interpreted relative to the 'directory' passed to buildClan. - - **Paths**: should be relative to the current file. - - **Any**: Nix expression must be serializable to JSON. - - !!! Note - **The import only happens if the machine is part of the service or role.** - - Other types are passed through to the nixos configuration. - - ???+ Example - To import the `special.nix` file - - ``` - . Clan Directory - ├── flake.nix - ... - └── modules - ├── special.nix - └── ... - ``` - - ```nix - { - extraModules = [ "modules/special.nix" ]; - } - ``` + Name of the input. Default to 'null' which means the module is local + ''; + }; + options.name = lib.mkOption { + type = types.str; + default = name; + defaultText = ""; + description = '' + Attribute of the clan service module imported from the chosen input. + + Defaults to the name of the instance. ''; - apply = value: if lib.isString value then value else builtins.seq (builtins.toJSON value) value; - default = [ ]; - type = types.listOf ( - types.oneOf [ - types.str - types.path - (types.attrsOf types.anything) - ] - ); }; }; - } - ); - }; - }; - } - ) - ]; - } - ); - default = { }; - }; + }; + roles = lib.mkOption { + default = { }; + type = types.attrsOf ( + types.submodule { + imports = [ (import ./roles-interface.nix { inherit clanLib; }) ]; + } + ); + }; + }; + } + ) + ]; + } + ); + default = { }; + }; services = lib.mkOption { + # services are deprecated in favor of `instances` + visible = false; description = '' Services of the inventory. diff --git a/lib/inventory/build-inventory/roles-interface.nix b/lib/inventory/build-inventory/roles-interface.nix new file mode 100644 index 000000000..61481f15e --- /dev/null +++ b/lib/inventory/build-inventory/roles-interface.nix @@ -0,0 +1,86 @@ +{ + clanLib, + settingsOption ? null, + nestedSettingsOption ? null, +}: +{ lib, ... }: +let + inherit (lib) + types + ; +in +{ + options = { + # TODO: deduplicate + machines = lib.mkOption { + type = types.attrsOf ( + types.submodule { + options.settings = + if nestedSettingsOption != null then + nestedSettingsOption + else + lib.mkOption { + default = { }; + type = clanLib.types.uniqueDeferredSerializableModule; + }; + } + ); + default = { }; + }; + tags = lib.mkOption { + type = types.attrsOf (types.submodule { }); + default = { }; + }; + settings = + if settingsOption != null then + settingsOption + else + lib.mkOption { + default = { }; + type = clanLib.types.uniqueDeferredSerializableModule; + }; + extraModules = lib.mkOption { + description = '' + List of additionally imported `.nix` expressions. + + Supported types: + + - **Strings**: Interpreted relative to the 'directory' passed to buildClan. + - **Paths**: should be relative to the current file. + - **Any**: Nix expression must be serializable to JSON. + + !!! Note + **The import only happens if the machine is part of the service or role.** + + Other types are passed through to the nixos configuration. + + ???+ Example + To import the `special.nix` file + + ``` + . Clan Directory + ├── flake.nix + ... + └── modules + ├── special.nix + └── ... + ``` + + ```nix + { + extraModules = [ "modules/special.nix" ]; + } + ``` + ''; + apply = value: if lib.isString value then value else builtins.seq (builtins.toJSON value) value; + default = [ ]; + type = types.listOf ( + types.oneOf [ + types.str + types.path + (types.attrsOf types.anything) + ] + ); + }; + }; +}