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)
+ ]
+ );
+ };
+ };
+}