Merge pull request 'inventory modules: expose module schemas at runtime' (#2469) from hsjobeki/clan-core:inventory-modules into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/2469
This commit is contained in:
hsjobeki
2024-11-22 12:56:27 +00:00
7 changed files with 80 additions and 19 deletions

View File

@@ -41,6 +41,8 @@ clanModules/borgbackup
The `roles` folder is strictly required for `features = [ "inventory" ]`. The `roles` folder is strictly required for `features = [ "inventory" ]`.
## Registering the module
=== "User module" === "User module"
If the module should be ad-hoc loaded. If the module should be ad-hoc loaded.
@@ -134,6 +136,56 @@ Adds the roles: `client` and `server`
} }
``` ```
## Adding configuration options
While we recommend to keep the interface as minimal as possible and deriving all required information from the `roles` model it might sometimes be required or convinient to expose customization options beyond `roles`.
The following shows how to add options to your module.
**It is important to understand that every module has its own namespace where it should declare options**
**`clan.{moduleName}`**
???+ Example
The following example shows how to register options in the module interface
and how it can be set via the inventory
```nix title="/default.nix"
custom-module = ./modules/custom-module;
```
Since the module is called `custom-module` all of its exposed options should be added to `options.clan.custom-module.*...*`
```nix title="custom-module/roles/default.nix"
{
options = {
clan.custom-module.foo = mkOption {
type = types.str;
default = "bar";
};
};
}
```
If the module is [registered](#registering-the-module).
Configuration can be set as follows.
```nix title="flake.nix"
buildClan {
inventory.services = {
custom-module.instance_1 = {
roles.default.machines = [ "machineA" ];
roles.default.config = {
# All configuration here is scoped to `clan.custom-module`
foo = "foobar";
};
};
};
}
```
## Organizing the ClanModule ## Organizing the ClanModule
Each `{role}.nix` is included into the machine if the machine is declared to have the role. Each `{role}.nix` is included into the machine if the machine is declared to have the role.

View File

@@ -101,8 +101,12 @@ in
# Those options are interfaced by the CLI # Those options are interfaced by the CLI
# We don't specify the type here, for better performance. # We don't specify the type here, for better performance.
inventory = lib.mkOption { type = lib.types.raw; }; inventory = lib.mkOption { type = lib.types.raw; };
# all inventory module schemas
moduleSchemas = lib.mkOption { type = lib.types.raw; };
inventoryFile = lib.mkOption { type = lib.types.raw; }; inventoryFile = lib.mkOption { type = lib.types.raw; };
# The machine 'imports' generated by the inventory per machine
serviceConfigs = lib.mkOption { type = lib.types.raw; }; serviceConfigs = lib.mkOption { type = lib.types.raw; };
# clan-core's modules
clanModules = lib.mkOption { type = lib.types.raw; }; clanModules = lib.mkOption { type = lib.types.raw; };
source = lib.mkOption { type = lib.types.raw; }; source = lib.mkOption { type = lib.types.raw; };
meta = lib.mkOption { type = lib.types.raw; }; meta = lib.mkOption { type = lib.types.raw; };

View File

@@ -169,6 +169,7 @@ in
inherit nixosConfigurations; inherit nixosConfigurations;
clanInternals = { clanInternals = {
moduleSchemas = clan-core.lib.modules.getModulesSchema config.inventory.modules;
inherit serviceConfigs; inherit serviceConfigs;
inherit (clan-core) clanModules; inherit (clan-core) clanModules;
inherit inventoryFile; inherit inventoryFile;

View File

@@ -16,5 +16,8 @@ in
facts = import ./facts.nix { inherit lib; }; facts = import ./facts.nix { inherit lib; };
inventory = import ./inventory { inherit lib clan-core; }; inventory = import ./inventory { inherit lib clan-core; };
jsonschema = import ./jsonschema { inherit lib; }; jsonschema = import ./jsonschema { inherit lib; };
modules = import ./frontmatter { inherit lib; }; modules = import ./frontmatter {
inherit lib;
self = clan-core;
};
} }

View File

@@ -1,8 +1,20 @@
{ lib }: { lib, self }:
let let
# Trim the .nix extension from a filename # Trim the .nix extension from a filename
trimExtension = name: builtins.substring 0 (builtins.stringLength name - 4) name; trimExtension = name: builtins.substring 0 (builtins.stringLength name - 4) name;
jsonWithoutHeader = self.lib.jsonschema {
includeDefaults = true;
header = { };
};
getModulesSchema =
modules:
lib.mapAttrs (
_moduleName: rolesOptions:
lib.mapAttrs (_roleName: options: jsonWithoutHeader.parseOptions options { }) rolesOptions
) (self.lib.evalClanModulesWithRoles modules);
evalFrontmatter = evalFrontmatter =
{ {
moduleName, moduleName,
@@ -90,7 +102,7 @@ in
{ {
inherit inherit
frontmatterOptions frontmatterOptions
getModulesSchema
getFrontmatter getFrontmatter
checkConstraints checkConstraints

View File

@@ -2,25 +2,14 @@
self, self,
self', self',
pkgs, pkgs,
lib,
... ...
}: }:
let let
includeDefaults = true;
# { mName :: { roleName :: Options } } modulesSchema = self.lib.modules.getModulesSchema self.clanModules;
modulesRolesOptions = self.lib.evalClanModulesWithRoles self.clanModules;
modulesSchema = lib.mapAttrs (
_moduleName: rolesOptions:
lib.mapAttrs (_roleName: options: jsonWithoutHeader.parseOptions options { }) rolesOptions
) modulesRolesOptions;
jsonLib = self.lib.jsonschema { inherit includeDefaults; }; jsonLib = self.lib.jsonschema { inherit includeDefaults; };
includeDefaults = true;
jsonWithoutHeader = self.lib.jsonschema {
inherit includeDefaults;
header = { };
};
frontMatterSchema = jsonLib.parseOptions self.lib.modules.frontmatterOptions { }; frontMatterSchema = jsonLib.parseOptions self.lib.modules.frontmatterOptions { };

View File

@@ -145,7 +145,7 @@ class ModuleInfo:
def get_modules(base_path: str) -> dict[str, str]: def get_modules(base_path: str) -> dict[str, str]:
cmd = nix_eval( cmd = nix_eval(
[ [
f"{base_path}#clanInternals.clanModules", f"{base_path}#clanInternals.inventory.modules",
"--json", "--json",
] ]
) )
@@ -153,11 +153,11 @@ def get_modules(base_path: str) -> dict[str, str]:
proc = run_no_stdout(cmd) proc = run_no_stdout(cmd)
res = proc.stdout.strip() res = proc.stdout.strip()
except ClanCmdError as e: except ClanCmdError as e:
msg = "clanInternals might not have clanModules attributes" msg = "clanInternals might not have inventory.modules attributes"
raise ClanError( raise ClanError(
msg, msg,
location=f"list_modules {base_path}", location=f"list_modules {base_path}",
description="Evaluation failed on clanInternals.clanModules attribute", description="Evaluation failed on clanInternals.inventory.modules attribute",
) from e ) from e
modules: dict[str, str] = json.loads(res) modules: dict[str, str] = json.loads(res)
return modules return modules