Merge pull request 'Inventory: init external modules support' (#2466) from hsjobeki/clan-core:inventory-modules into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/2466
This commit is contained in:
@@ -6,10 +6,8 @@ This site will guide you through authoring your first module. Explaining which c
|
||||
Under construction
|
||||
:fontawesome-solid-road-barrier: :fontawesome-solid-road-barrier: :fontawesome-solid-road-barrier:
|
||||
|
||||
!!! Note
|
||||
Currently ClanModules should be contributed to the [clan-core repository](https://git.clan.lol/clan/clan-core) via a PR.
|
||||
|
||||
Ad-hoc loading of custom modules is not recommended / supported yet.
|
||||
!!! Tip
|
||||
External ClanModules can be ad-hoc loaded via [`clan.inventory.modules`](../reference/nix-api/inventory.md#modules)
|
||||
|
||||
## Bootstrapping the `clanModule`
|
||||
|
||||
@@ -43,13 +41,37 @@ clanModules/borgbackup
|
||||
|
||||
The `roles` folder is strictly required for `features = [ "inventory" ]`.
|
||||
|
||||
The clanModule must be registered via the `clanModules` attribute in `clan-core`
|
||||
=== "User module"
|
||||
|
||||
```nix title="clanModules/flake-module.nix"
|
||||
--8<-- "clanModules/flake-module.nix:0:6"
|
||||
# Register your new module here
|
||||
If the module should be ad-hoc loaded.
|
||||
It can be made avilable in any project via the [`clan.inventory.modules`](../reference/nix-api/inventory.md#modules) attribute.
|
||||
|
||||
```nix title="flake.nix"
|
||||
# ...
|
||||
```
|
||||
buildClan {
|
||||
# 1. Add the module to the avilable inventory modules
|
||||
inventory.modules = {
|
||||
custom-module = ./modules/my_module;
|
||||
};
|
||||
# 2. Use the module in the inventory
|
||||
inventory.services = {
|
||||
custom-module.instance_1 = {
|
||||
roles.default.machines = [ "machineA" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
=== "Upstream module"
|
||||
|
||||
If the module will be contributed to [`clan-core`](https://git.clan.lol/clan-core)
|
||||
The clanModule must be registered within the `clanModules` attribute in `clan-core`
|
||||
|
||||
```nix title="clanModules/flake-module.nix"
|
||||
--8<-- "clanModules/flake-module.nix:0:5"
|
||||
# Register our new module here
|
||||
# ...
|
||||
```
|
||||
|
||||
## Readme
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ in
|
||||
# We don't specify the type here, for better performance.
|
||||
inventory = lib.mkOption { type = lib.types.raw; };
|
||||
inventoryFile = lib.mkOption { type = lib.types.raw; };
|
||||
serviceConfigs = lib.mkOption { type = lib.types.raw; };
|
||||
clanModules = lib.mkOption { type = lib.types.raw; };
|
||||
source = lib.mkOption { type = lib.types.raw; };
|
||||
meta = lib.mkOption { type = lib.types.raw; };
|
||||
|
||||
@@ -159,7 +159,7 @@ in
|
||||
inventory.machines = lib.mapAttrs (_n: _: { }) config.machines;
|
||||
}
|
||||
# Merge the meta attributes from the buildClan function
|
||||
#
|
||||
{ inventory.modules = clan-core.clanModules; }
|
||||
# config.inventory.meta <- config.meta
|
||||
{ inventory.meta = config.meta; }
|
||||
# Set default for computed tags
|
||||
@@ -169,6 +169,7 @@ in
|
||||
inherit nixosConfigurations;
|
||||
|
||||
clanInternals = {
|
||||
inherit serviceConfigs;
|
||||
inherit (clan-core) clanModules;
|
||||
inherit inventoryFile;
|
||||
inventory = config.inventory;
|
||||
|
||||
@@ -24,8 +24,8 @@ in
|
||||
"${moduleName}.${instanceName}.roles.${roleName}.min" = {
|
||||
assertion = memberCount >= roleConstraints.min;
|
||||
message = ''
|
||||
The ${moduleName} module requires at least ${builtins.toString roleConstraints.min} '${roleName}'s
|
||||
but found '${builtins.toString memberCount}' within instance '${instanceName}':
|
||||
The ${moduleName} module requires at least ${builtins.toString roleConstraints.min} members of the '${roleName}' role
|
||||
but found '${builtins.toString memberCount}' members within instance '${instanceName}':
|
||||
|
||||
${lib.concatLines members}
|
||||
'';
|
||||
@@ -36,8 +36,8 @@ in
|
||||
"${moduleName}.${instanceName}.roles.${roleName}.max" = {
|
||||
assertion = memberCount <= roleConstraints.max;
|
||||
message = ''
|
||||
The ${moduleName} module allows at most for ${builtins.toString roleConstraints.max} '${roleName}'s
|
||||
but found '${builtins.toString memberCount}' within instance '${instanceName}':
|
||||
The ${moduleName} module allows at most for ${builtins.toString roleConstraints.max} members of the '${roleName}' role
|
||||
but found '${builtins.toString memberCount}' members within instance '${instanceName}':
|
||||
|
||||
${lib.concatLines members}
|
||||
'';
|
||||
|
||||
@@ -16,5 +16,5 @@ in
|
||||
facts = import ./facts.nix { inherit lib; };
|
||||
inventory = import ./inventory { inherit lib clan-core; };
|
||||
jsonschema = import ./jsonschema { inherit lib; };
|
||||
modules = import ./frontmatter { inherit clan-core lib; };
|
||||
modules = import ./frontmatter { inherit lib; };
|
||||
}
|
||||
|
||||
@@ -53,16 +53,16 @@ let
|
||||
}
|
||||
*/
|
||||
evalClanModulesWithRoles =
|
||||
clanModules:
|
||||
allModules:
|
||||
let
|
||||
res = builtins.mapAttrs (
|
||||
moduleName: module:
|
||||
let
|
||||
frontmatter = clan-core.lib.modules.getFrontmatter moduleName;
|
||||
frontmatter = clan-core.lib.modules.getFrontmatter allModules.${moduleName} moduleName;
|
||||
roles =
|
||||
if builtins.elem "inventory" frontmatter.features or [ ] then
|
||||
assert lib.isPath module;
|
||||
clan-core.lib.modules.getRoles moduleName
|
||||
clan-core.lib.modules.getRoles allModules moduleName
|
||||
else
|
||||
[ ];
|
||||
in
|
||||
@@ -83,7 +83,7 @@ let
|
||||
}).options.clan.${moduleName} or { };
|
||||
}) roles
|
||||
)
|
||||
) clanModules;
|
||||
) allModules;
|
||||
in
|
||||
res;
|
||||
in
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{ clan-core, lib }:
|
||||
{ lib }:
|
||||
let
|
||||
# Trim the .nix extension from a filename
|
||||
trimExtension = name: builtins.substring 0 (builtins.stringLength name - 4) name;
|
||||
@@ -8,18 +8,20 @@ let
|
||||
moduleName,
|
||||
instanceName,
|
||||
resolvedRoles,
|
||||
allModules,
|
||||
}:
|
||||
lib.evalModules {
|
||||
specialArgs = {
|
||||
inherit moduleName resolvedRoles instanceName;
|
||||
allRoles = getRoles moduleName;
|
||||
allRoles = getRoles allModules moduleName;
|
||||
};
|
||||
modules = [
|
||||
(getFrontmatter moduleName)
|
||||
(getFrontmatter allModules.${moduleName} moduleName)
|
||||
./interface.nix
|
||||
];
|
||||
};
|
||||
|
||||
# For Documentation purposes only
|
||||
frontmatterOptions =
|
||||
(lib.evalModules {
|
||||
specialArgs = {
|
||||
@@ -32,26 +34,24 @@ let
|
||||
}).options;
|
||||
|
||||
getRoles =
|
||||
serviceName:
|
||||
allModules: serviceName:
|
||||
lib.mapAttrsToList (name: _value: trimExtension name) (
|
||||
lib.filterAttrs (name: type: type == "regular" && lib.hasSuffix ".nix" name) (
|
||||
builtins.readDir (
|
||||
if clan-core.clanModules ? ${serviceName} then
|
||||
clan-core.clanModules.${serviceName} + "/roles"
|
||||
if allModules ? ${serviceName} then
|
||||
allModules.${serviceName} + "/roles"
|
||||
else
|
||||
throw "ClanModule not found: '${serviceName}'. Make sure the module is added in the 'clanModules' attribute of clan-core."
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
getConstraints = modulename: (getFrontmatter modulename).constraints;
|
||||
|
||||
checkConstraints = args: (evalFrontmatter args).config.constraints.assertions;
|
||||
|
||||
getReadme =
|
||||
modulename:
|
||||
modulepath: modulename:
|
||||
let
|
||||
readme = "${clan-core}/clanModules/${modulename}/README.md";
|
||||
readme = modulepath + "/README.md";
|
||||
readmeContents =
|
||||
if (builtins.pathExists readme) then
|
||||
(builtins.readFile readme)
|
||||
@@ -61,9 +61,9 @@ let
|
||||
readmeContents;
|
||||
|
||||
getFrontmatter =
|
||||
modulename:
|
||||
modulepath: modulename:
|
||||
let
|
||||
content = getReadme modulename;
|
||||
content = getReadme modulepath modulename;
|
||||
parts = lib.splitString "---" content;
|
||||
# Partition the parts into the first part (the readme content) and the rest (the metadata)
|
||||
parsed = builtins.partition ({ index, ... }: if index >= 2 then false else true) (
|
||||
@@ -89,12 +89,10 @@ let
|
||||
in
|
||||
{
|
||||
inherit
|
||||
evalFrontmatter
|
||||
frontmatterOptions
|
||||
|
||||
getFrontmatter
|
||||
getReadme
|
||||
getConstraints
|
||||
|
||||
checkConstraints
|
||||
getRoles
|
||||
;
|
||||
|
||||
@@ -38,8 +38,9 @@ let
|
||||
};
|
||||
|
||||
checkService =
|
||||
serviceName:
|
||||
builtins.elem "inventory" (clan-core.lib.modules.getFrontmatter serviceName).features or [ ];
|
||||
modulepath: serviceName:
|
||||
builtins.elem "inventory"
|
||||
(clan-core.lib.modules.getFrontmatter modulepath serviceName).features or [ ];
|
||||
|
||||
extendMachine =
|
||||
{ machineConfig, inventory }:
|
||||
@@ -53,7 +54,7 @@ let
|
||||
acc
|
||||
++ [
|
||||
{
|
||||
assertion = checkService serviceName;
|
||||
assertion = checkService inventory.modules.${serviceName} serviceName;
|
||||
message = ''
|
||||
Service ${serviceName} cannot be used in inventory. It does not declare the 'inventory' feature.
|
||||
|
||||
@@ -94,7 +95,7 @@ let
|
||||
acc2: instanceName: serviceConfig:
|
||||
|
||||
let
|
||||
roles = clan-core.lib.modules.getRoles serviceName;
|
||||
roles = clan-core.lib.modules.getRoles inventory.modules serviceName;
|
||||
|
||||
resolvedRoles = lib.genAttrs roles (
|
||||
roleName:
|
||||
@@ -129,11 +130,11 @@ let
|
||||
# TODO: maybe optimize this dont lookup the role in inverse roles. Imports are not lazy
|
||||
roleModules = builtins.map (
|
||||
role:
|
||||
if builtins.elem role roles && clan-core.clanModules ? ${serviceName} then
|
||||
clan-core.clanModules.${serviceName} + "/roles/${role}.nix"
|
||||
if builtins.elem role roles && inventory.modules ? ${serviceName} then
|
||||
inventory.modules.${serviceName} + "/roles/${role}.nix"
|
||||
else
|
||||
throw "Module ${serviceName} doesn't have role: '${role}'. Role: ${
|
||||
clan-core.clanModules.${serviceName}
|
||||
inventory.modules.${serviceName}
|
||||
}/roles/${role}.nix not found."
|
||||
) machineRoles;
|
||||
|
||||
@@ -151,6 +152,7 @@ let
|
||||
|
||||
constraintAssertions = clan-core.lib.modules.checkConstraints {
|
||||
moduleName = serviceName;
|
||||
allModules = inventory.modules;
|
||||
inherit resolvedRoles instanceName;
|
||||
};
|
||||
in
|
||||
|
||||
@@ -92,6 +92,42 @@ in
|
||||
./assertions.nix
|
||||
];
|
||||
options = {
|
||||
modules = lib.mkOption {
|
||||
type = types.attrsOf types.path;
|
||||
default = { };
|
||||
defaultText = "clanModules of clan-core";
|
||||
description = ''
|
||||
A mapping of module names to their path.
|
||||
|
||||
Each module can be referenced by its `attributeName` in the `inventory.services` attribute set.
|
||||
|
||||
!!! Important
|
||||
Each module MUST fullfill the following requirements to be usable with the inventory:
|
||||
|
||||
- The module MUST have a `README.md` file with a `description`.
|
||||
- The module MUST have at least `features = [ "inventory" ]` in the frontmatter section.
|
||||
- The module MUST have a subfolder `roles` with at least one `{roleName}.nix` file.
|
||||
|
||||
For further information see: [Module Authoring Guide](../../clanmodules/index.md).
|
||||
|
||||
???+ example
|
||||
```nix
|
||||
buildClan {
|
||||
# 1. Add the module to the avilable inventory modules
|
||||
inventory.modules = {
|
||||
custom-module = ./modules/my_module;
|
||||
};
|
||||
# 2. Use the module in the inventory
|
||||
inventory.services = {
|
||||
custom-module.instance_1 = {
|
||||
roles.default.machines = [ "machineA" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
'';
|
||||
};
|
||||
|
||||
assertions = lib.mkOption {
|
||||
type = types.listOf types.unspecified;
|
||||
internal = true;
|
||||
|
||||
@@ -6,7 +6,6 @@ let
|
||||
inherit lib clan-core;
|
||||
}
|
||||
);
|
||||
|
||||
inherit (inventory) buildInventory;
|
||||
in
|
||||
{
|
||||
@@ -23,6 +22,7 @@ in
|
||||
configs = buildInventory {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
modules = clan-core.clanModules;
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.server.machines = [ "backup_server" ];
|
||||
@@ -64,6 +64,7 @@ in
|
||||
configs = buildInventory {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
modules = clan-core.clanModules;
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.client.tags = [ "backup" ];
|
||||
@@ -103,6 +104,7 @@ in
|
||||
configs = buildInventory {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
modules = clan-core.clanModules;
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.client.machines = [ "machine_1" ];
|
||||
@@ -132,6 +134,7 @@ in
|
||||
configs = buildInventory {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
modules = clan-core.clanModules;
|
||||
services = {
|
||||
fanatasy.instance_1 = {
|
||||
roles.default.machines = [ "machine_1" ];
|
||||
@@ -156,6 +159,7 @@ in
|
||||
configs = buildInventory {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
modules = clan-core.clanModules;
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.roleXYZ.machines = [ "machine_1" ];
|
||||
@@ -179,6 +183,7 @@ in
|
||||
configs = buildInventory {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
modules = clan-core.clanModules;
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.client.machines = [ "machine_1" ];
|
||||
@@ -205,6 +210,7 @@ in
|
||||
configs = buildInventory {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
modules = clan-core.clanModules;
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
enabled = false;
|
||||
|
||||
@@ -72,7 +72,8 @@ rec {
|
||||
{ }
|
||||
else if opt ? defaultText then
|
||||
{
|
||||
default = "<thunk>";
|
||||
# dont add default to jsonschema. It seems to alter the type
|
||||
# default = "<thunk>";
|
||||
}
|
||||
else
|
||||
lib.optionalAttrs (opt ? default) {
|
||||
@@ -93,10 +94,17 @@ rec {
|
||||
};
|
||||
|
||||
makeModuleInfo =
|
||||
{ path }:
|
||||
{
|
||||
"$exportedModuleInfo" = {
|
||||
path,
|
||||
defaultText ? null,
|
||||
}:
|
||||
{
|
||||
"$exportedModuleInfo" =
|
||||
{
|
||||
inherit path;
|
||||
}
|
||||
// lib.optionalAttrs (defaultText != null) {
|
||||
inherit defaultText;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -172,6 +180,7 @@ rec {
|
||||
};
|
||||
exposedModuleInfo = makeModuleInfo {
|
||||
path = option.loc;
|
||||
defaultText = option.defaultText or null;
|
||||
};
|
||||
in
|
||||
# either type
|
||||
|
||||
@@ -35,5 +35,6 @@ Service = dict[str, Any]
|
||||
class Inventory:
|
||||
meta: Meta
|
||||
machines: dict[str, Machine] = field(default_factory = dict)
|
||||
modules: dict[str, str] = field(default_factory = dict)
|
||||
services: dict[str, Service] = field(default_factory = dict)
|
||||
tags: dict[str, list[str]] = field(default_factory = dict)
|
||||
|
||||
Reference in New Issue
Block a user