build-inventory: move inventory and inventoryClass into explizitly different folders

This commit is contained in:
Johannes Kirschbauer
2025-06-25 17:45:10 +02:00
parent 68ed393c87
commit c91b5fb3db
42 changed files with 49 additions and 139 deletions

View File

@@ -0,0 +1,78 @@
# Integrity validation of the inventory
{ config, lib, ... }:
{
# Assertion must be of type
# { assertion :: bool, message :: string, severity :: "error" | "warning" }
imports = [
# Check that each machine used in a service is defined in the top-level machines
{
assertions = lib.foldlAttrs (
ass1: serviceName: c:
ass1
++ lib.foldlAttrs (
ass2: instanceName: instanceConfig:
let
topLevelMachines = lib.attrNames config.machines;
# All machines must be defined in the top-level machines
assertions = lib.foldlAttrs (
assertions: roleName: role:
assertions
++ builtins.filter (a: !a.assertion) (
builtins.map (m: {
assertion = builtins.elem m topLevelMachines;
message = ''
Machine '${m}' is not defined in the inventory. This might still work, if the machine is defined via nix.
Defined in service: '${serviceName}' instance: '${instanceName}' role: '${roleName}'.
Inventory machines:
${builtins.concatStringsSep "\n" (map (n: "'${n}'") topLevelMachines)}
'';
severity = "warning";
}) role.machines
)
) [ ] instanceConfig.roles;
in
ass2 ++ assertions
) [ ] c
) [ ] config.services;
}
# Check that each tag used in a role is defined in at least one machines tags
{
assertions = lib.foldlAttrs (
ass1: serviceName: c:
ass1
++ lib.foldlAttrs (
ass2: instanceName: instanceConfig:
let
allTags = lib.foldlAttrs (
tags: _machineName: machine:
tags ++ machine.tags
) [ ] config.machines;
# All machines must be defined in the top-level machines
assertions = lib.foldlAttrs (
assertions: roleName: role:
assertions
++ builtins.filter (a: !a.assertion) (
builtins.map (m: {
assertion = builtins.elem m allTags;
message = ''
Tag '${m}' is not defined in the inventory.
Defined in service: '${serviceName}' instance: '${instanceName}' role: '${roleName}'.
Available tags:
${builtins.concatStringsSep "\n" (map (n: "'${n}'") allTags)}
'';
severity = "error";
}) role.tags
)
) [ ] instanceConfig.roles;
in
ass2 ++ assertions
) [ ] c
) [ ] config.services;
}
];
}

View File

@@ -0,0 +1,268 @@
{
lib,
config,
clanLib,
...
}:
let
inherit (config) inventory directory;
resolveTags =
# Inventory, { machines :: [string], tags :: [string] }
{
serviceName,
instanceName,
roleName,
inventory,
members,
}:
{
machines =
members.machines or [ ]
++ (builtins.foldl' (
acc: tag:
let
# For error printing
availableTags = lib.foldlAttrs (
acc: _: v:
v.tags or [ ] ++ acc
) [ ] (inventory.machines);
tagMembers = builtins.attrNames (
lib.filterAttrs (_n: v: builtins.elem tag v.tags or [ ]) inventory.machines
);
in
if tagMembers == [ ] then
lib.warn ''
inventory.services.${serviceName}.${instanceName}: - ${roleName} tags: no machine with tag '${tag}' found.
Available tags: ${builtins.toJSON (lib.unique availableTags)}
'' [ ]
else
acc ++ tagMembers
) [ ] members.tags or [ ]);
};
checkService =
modulepath: serviceName:
builtins.elem "inventory" (clanLib.modules.getFrontmatter modulepath serviceName).features or [ ];
compileMachine =
{ machineConfig }:
{
machineImports = [
(lib.optionalAttrs (machineConfig.deploy.targetHost or null != null) {
config.clan.core.networking.targetHost = lib.mkForce machineConfig.deploy.targetHost;
})
(lib.optionalAttrs (machineConfig.deploy.buildHost or null != null) {
config.clan.core.networking.buildHost = lib.mkForce machineConfig.deploy.buildHost;
})
];
assertions = { };
};
resolveImports =
{
supportedRoles,
resolvedRolesPerInstance,
serviceConfigs,
serviceName,
machineName,
getRoleFile,
}:
(lib.foldlAttrs (
# : [ Modules ] -> String -> ServiceConfig -> [ Modules ]
acc2: instanceName: serviceConfig:
let
resolvedRoles = resolvedRolesPerInstance.${instanceName};
isInService = builtins.any (members: builtins.elem machineName members.machines) (
builtins.attrValues resolvedRoles
);
# all roles where the machine is present
machineRoles = builtins.attrNames (
lib.filterAttrs (_role: roleConfig: builtins.elem machineName roleConfig.machines) resolvedRoles
);
machineServiceConfig = (serviceConfig.machines.${machineName} or { }).config or { };
globalConfig = serviceConfig.config or { };
globalExtraModules = serviceConfig.extraModules or [ ];
machineExtraModules = serviceConfig.machines.${machineName}.extraModules or [ ];
roleServiceExtraModules = builtins.foldl' (
acc: role: acc ++ serviceConfig.roles.${role}.extraModules or [ ]
) [ ] machineRoles;
# TODO: maybe optimize this don't lookup the role in inverse roles. Imports are not lazy
roleModules = builtins.map (
role:
if builtins.elem role supportedRoles && inventory.modules ? ${serviceName} then
getRoleFile role
else
throw "Module ${serviceName} doesn't have role: '${role}'. Role: ${
inventory.modules.${serviceName}
}/roles/${role}.nix not found."
) machineRoles;
roleServiceConfigs = builtins.filter (m: m != { }) (
builtins.map (role: serviceConfig.roles.${role}.config or { }) machineRoles
);
extraModules = map (s: if builtins.typeOf s == "string" then "${directory}/${s}" else s) (
globalExtraModules ++ machineExtraModules ++ roleServiceExtraModules
);
features =
(clanLib.modules.getFrontmatter inventory.modules.${serviceName} serviceName).features or [ ];
deprecationWarning = lib.optionalAttrs (builtins.elem "deprecated" features) {
warnings = [
''
The '${serviceName}' module has been migrated from `inventory.services` to `inventory.instances`
See https://docs.clan.lol/guides/clanServices/ for usage.
''
];
};
in
if !(serviceConfig.enabled or true) then
acc2
else if isInService then
acc2
++ [
deprecationWarning
{
imports = roleModules ++ extraModules;
clan.inventory.services.${serviceName}.${instanceName} = {
roles = resolvedRoles;
# TODO: Add inverseRoles to the service config if needed
# inherit inverseRoles;
};
}
(lib.optionalAttrs (globalConfig != { } || machineServiceConfig != { } || roleServiceConfigs != [ ])
{
clan.${serviceName} = lib.mkMerge (
[
globalConfig
machineServiceConfig
]
++ roleServiceConfigs
);
}
)
]
else
acc2
) [ ] (serviceConfigs));
in
{
imports = [
./interface.nix
];
config = {
machines = builtins.mapAttrs (
machineName: machineConfig: m:
let
compiledServices = lib.mapAttrs (
_: serviceConfigs:
(
{ config, ... }:
let
serviceName = config.serviceName;
getRoleFile = role: builtins.seq role inventory.modules.${serviceName} + "/roles/${role}.nix";
in
{
_file = "inventory/builder.nix";
_module.args = {
inherit
resolveTags
inventory
clanLib
machineName
serviceConfigs
;
};
imports = [
./roles.nix
];
machineImports = resolveImports {
supportedRoles = config.supportedRoles;
resolvedRolesPerInstance = config.resolvedRolesPerInstance;
inherit
serviceConfigs
serviceName
machineName
getRoleFile
;
};
# Assertions
assertions = {
"checkservice.${serviceName}" = {
assertion = checkService inventory.modules.${serviceName} serviceName;
message = ''
Service ${serviceName} cannot be used in inventory. It does not declare the 'inventory' feature.
To allow it add the following to the beginning of the README.md of the module:
---
...
features = [ "inventory" ]
---
Also make sure to test the module with the 'inventory' feature enabled.
'';
};
};
}
)
) (config.inventory.services or { });
compiledMachine = compileMachine {
inherit
machineConfig
;
};
machineImports = (
compiledMachine.machineImports
++ builtins.foldl' (
acc: service:
let
failedAssertions = (lib.filterAttrs (_: v: !v.assertion) service.assertions);
failedAssertionsImports =
if failedAssertions != { } then
[
{
clan.inventory.assertions = failedAssertions;
}
]
else
[
{
clan.inventory.assertions = {
"alive.assertion.inventory" = {
assertion = true;
message = ''
No failed assertions found for machine ${machineName}. This will never be displayed.
It is here for testing purposes.
'';
};
};
}
];
in
acc
++ service.machineImports
# Import failed assertions
++ failedAssertionsImports
) [ ] (builtins.attrValues m.config.compiledServices)
);
in
{
inherit machineImports compiledServices compiledMachine;
}
) (inventory.machines or { });
};
}

View File

@@ -0,0 +1,91 @@
{ lib, ... }:
let
inherit (lib) types mkOption;
submodule = m: types.submoduleWith { modules = [ m ]; };
in
{
options = {
directory = mkOption {
type = types.path;
};
distributedServices = mkOption {
type = types.raw;
};
inventory = mkOption {
type = types.raw;
};
machines = mkOption {
type = types.attrsOf (
submodule (
{ name, ... }:
let
machineName = name;
in
{
options = {
compiledMachine = mkOption {
type = types.raw;
};
compiledServices = mkOption {
# type = types.attrsOf;
type = types.attrsOf (
types.submoduleWith {
modules = [
(
{ name, ... }:
let
serviceName = name;
in
{
options = {
machineName = mkOption {
default = machineName;
readOnly = true;
};
serviceName = mkOption {
default = serviceName;
readOnly = true;
};
# Outputs
machineImports = mkOption {
type = types.listOf types.raw;
};
supportedRoles = mkOption {
type = types.listOf types.str;
};
matchedRoles = mkOption {
type = types.listOf types.str;
};
machinesRoles = mkOption {
type = types.attrsOf (types.listOf types.str);
};
resolvedRolesPerInstance = mkOption {
type = types.attrsOf (
types.attrsOf (submodule {
options.machines = mkOption {
type = types.listOf types.str;
};
})
);
};
assertions = mkOption {
type = types.attrsOf types.raw;
};
};
}
)
];
}
);
};
machineImports = mkOption {
type = types.listOf types.raw;
};
};
}
)
);
};
};
}

View File

@@ -0,0 +1,65 @@
{
lib,
config,
resolveTags,
inventory,
clanLib,
machineName,
serviceConfigs,
...
}:
let
serviceName = config.serviceName;
in
{
# Roles resolution
# : List String
supportedRoles = clanLib.modules.getRoles "inventory.modules" inventory.modules serviceName;
matchedRoles = builtins.attrNames (
lib.filterAttrs (_: ms: builtins.elem machineName ms) config.machinesRoles
);
resolvedRolesPerInstance = lib.mapAttrs (
instanceName: instanceConfig:
let
resolvedRoles = lib.genAttrs config.supportedRoles (
roleName:
resolveTags {
members = instanceConfig.roles.${roleName} or { };
inherit
instanceName
serviceName
roleName
inventory
;
}
);
usedRoles = builtins.attrNames instanceConfig.roles;
unmatchedRoles = builtins.filter (role: !builtins.elem role config.supportedRoles) usedRoles;
in
if unmatchedRoles != [ ] then
throw ''
Roles ${builtins.toJSON unmatchedRoles} are not defined in the service ${serviceName}.
Instance: '${instanceName}'
Please use one of available roles: ${builtins.toJSON config.supportedRoles}
''
else
resolvedRoles
) serviceConfigs;
machinesRoles = builtins.zipAttrsWith (
_n: vs:
let
flat = builtins.foldl' (acc: s: acc ++ s.machines) [ ] vs;
in
lib.unique flat
) (builtins.attrValues config.resolvedRolesPerInstance);
assertions = lib.concatMapAttrs (
instanceName: resolvedRoles:
clanLib.modules.checkConstraints {
moduleName = serviceName;
allModules = inventory.modules;
inherit resolvedRoles instanceName;
}
) config.resolvedRolesPerInstance;
}

View File

@@ -0,0 +1,49 @@
# Generate partial NixOS configurations for every machine in the inventory
# This function is responsible for generating the module configuration for every machine in the inventory.
{ lib, clanLib }:
let
/*
Returns a set with NixOS configuration for every machine in the inventory.
machinesFromInventory :: Inventory -> { ${machine_name} :: NixOSConfiguration }
*/
buildInventory =
{
inventory,
directory,
flakeInputs,
prefix ? [ ],
localModuleSet ? { },
}:
(lib.evalModules {
# TODO: move clanLib from specialArgs to options
specialArgs = {
inherit clanLib;
};
modules = [
./builder/default.nix
(lib.modules.importApply ./service-list-from-inputs.nix {
inherit flakeInputs clanLib localModuleSet;
})
{ inherit directory inventory; }
(
# config.distributedServices.allMachines.${name} or [ ];
{ config, ... }:
{
distributedServices = clanLib.inventory.mapInstances {
inherit (config) inventory;
inherit localModuleSet;
inherit flakeInputs;
prefix = prefix ++ [ "distributedServices" ];
};
machines = config.distributedServices.allMachines;
}
)
(lib.modules.importApply ./inventory-introspection.nix { inherit clanLib; })
];
}).config;
in
{
inherit buildInventory;
}

View File

@@ -0,0 +1,598 @@
{
lib,
clanLib,
config,
options,
...
}:
let
types = lib.types;
metaOptionsWith = name: {
name = lib.mkOption {
type = types.str;
default = name;
description = ''
Name of the machine or service
'';
};
description = lib.mkOption {
default = null;
type = types.nullOr types.str;
description = ''
Optional freeform description
'';
};
icon = lib.mkOption {
default = null;
type = types.nullOr types.str;
description = ''
Under construction, will be used for the UI
'';
};
};
moduleConfig = lib.mkOption {
default = { };
# TODO: use types.deferredModule
# clan.borgbackup MUST be defined as submodule
type = types.attrsOf types.anything;
description = ''
Configuration of the specific clanModule.
!!! Note
Configuration is passed to the nixos configuration scoped to the module.
```nix
clan.<serviceName> = { ... # Config }
```
'';
};
extraModulesOption = 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.anything
]
);
};
in
{
imports = [
./assertions.nix
];
options = {
# Internal things
_inventoryFile = lib.mkOption {
type = types.path;
readOnly = true;
internal = true;
visible = false;
};
_legacyModules = lib.mkOption {
internal = true;
visible = false;
default = { };
};
noInstanceOptions = lib.mkOption {
type = types.bool;
internal = true;
visible = false;
default = false;
};
options = lib.mkOption {
internal = true;
visible = false;
type = types.raw;
default = options;
};
# ---------------------------
modules = lib.mkOption {
# Don't define the type yet
# We manually transform the value with types.deferredModule.merge later to keep them serializable
type = types.attrsOf types.raw;
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 fulfill 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](../../guides/authoring/clanServices/index.md).
???+ example
```nix
buildClan {
# 1. Add the module to the available 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" ];
};
};
};
```
'';
apply =
moduleSet:
let
allowedNames = lib.attrNames config._legacyModules;
in
if builtins.all (moduleName: builtins.elem moduleName allowedNames) (lib.attrNames moduleSet) then
moduleSet
else
lib.warn ''
`inventory.modules` will be deprecated soon.
Please migrate the following modules into `clan.service` modules
and register them in `clan.modules`
${lib.concatStringsSep "\n" (
map (m: "'${m}'") (lib.attrNames (lib.filterAttrs (n: _v: !builtins.elem n allowedNames) moduleSet))
)}
See: https://docs.clan.lol/guides/clanServices/
And: https://docs.clan.lol/guides/authoring/clanServices/
'' moduleSet;
};
assertions = lib.mkOption {
type = types.listOf types.unspecified;
internal = true;
visible = false;
default = [ ];
};
meta = lib.mkOption {
type = lib.types.submoduleWith {
modules = [
./meta-interface.nix
];
};
};
tags = lib.mkOption {
default = { };
description = ''
Tags of the inventory are used to group machines together.
It is recommended to use [`machine.tags`](#inventory.machines.tags) to define the tags of the machines.
This can be used to define custom tags that are either statically set or dynamically computed.
#### Static Tags
???+ example "Static Tag Example"
```nix
inventory.tags = {
foo = [ "machineA" "machineB" ];
};
```
The tag `foo` will always be added to `machineA` and `machineB`.
#### Dynamic Tags
It is possible to compute tags based on the machines properties or based on other tags.
!!! danger
This is a powerful feature and should be used with caution.
It is possible to cause infinite recursion by computing tags based on the machines properties or based on other tags.
???+ example "Dynamic Tag Example"
allButFoo is a computed tag. It will be added to all machines except 'foo'
`all` is a predefined tag. See the docs of [`tags.all`](#inventory.tags.all).
```nix
# inventory.tags inventory.machines
inventory.tags = {config, machines...}: {
# The "all" tag
allButFoo = builtins.filter (name: name != "foo") config.all;
};
```
!!! warning
Do NOT compute `tags` from `machine.tags` this will cause infinite recursion.
'';
type = types.submoduleWith {
specialArgs = {
inherit (config) machines;
};
modules = [
{
freeformType = with lib.types; lazyAttrsOf (listOf str);
# Reserved tags
# Defined as options here to show them in advance
options = {
# 'All machines' tag
all = lib.mkOption {
type = with lib.types; listOf str;
defaultText = "[ <All Machines> ]";
description = ''
!!! example "Predefined Tag"
Will be added to all machines
```nix
inventory.machines.machineA.tags = [ "all" ];
```
'';
};
nixos = lib.mkOption {
type = with lib.types; listOf str;
defaultText = "[ <All NixOS Machines> ]";
description = ''
!!! example "Predefined Tag"
Will be added to all machines that set `machineClass = "nixos"`
```nix
inventory.machines.machineA.tags = [ "nixos" ];
```
'';
};
darwin = lib.mkOption {
type = with lib.types; listOf str;
defaultText = "[ <All Darwin Machines> ]";
description = ''
!!! example "Predefined Tag"
Will be added to all machines that set `machineClass = "darwin"`
```nix
inventory.machines.machineA.tags = [ "darwin" ];
```
'';
};
};
}
];
};
};
machines = lib.mkOption {
description = ''
Machines in the inventory.
Each machine declared here can be referencd via its `attributeName` by the `inventory.service`s `roles`.
'';
default = { };
type = types.lazyAttrsOf (
types.submoduleWith ({
modules = [
(
{ name, ... }:
{
tags = builtins.attrNames (
# config.tags
lib.filterAttrs (_t: tagMembers: builtins.elem name tagMembers) config.tags
);
}
)
(
{ name, ... }:
{
options = {
inherit (metaOptionsWith name) name description icon;
machineClass = lib.mkOption {
default = "nixos";
type = types.enum [
"nixos"
"darwin"
];
description = ''
The module system that should be used to construct the machine
Set this to `darwin` for macOS machines
'';
};
tags = lib.mkOption {
description = ''
List of tags for the machine.
The machine can be referenced by its tags in `inventory.services`
???+ Example
```nix
inventory.machines.machineA.tags = [ "tag1" "tag2" ];
```
```nix
services.borgbackup."instance_1".roles.client.tags = [ "tag1" ];
```
!!! Note
Tags can be used to determine the membership of the machine in the services.
Without changing the service configuration, the machine can be added to a service by adding the correct tags to the machine.
'';
default = [ ];
apply = lib.unique;
type = types.listOf types.str;
};
deploy.targetHost = lib.mkOption {
description = "SSH address of the host to deploy the machine to";
default = null;
type = types.nullOr types.str;
};
deploy.buildHost = lib.mkOption {
description = "SSH address of the host to build the machine on";
default = null;
type = types.nullOr types.str;
};
};
}
)
];
})
);
};
instances =
if config.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 = ''
Name of the input. Default to 'null' which means the module is local
'';
};
options.name = lib.mkOption {
type = types.str;
default = name;
defaultText = "<Name of the Instance>";
description = ''
Attribute of the clan service module imported from the chosen input.
Defaults to the name of the instance.
'';
};
};
};
roles = lib.mkOption {
default = { };
type = types.attrsOf (
types.submodule {
imports = [
{
_file = "inventory/interface";
_module.args = {
inherit clanLib;
};
}
(import ./roles-interface.nix { })
];
}
);
};
};
}
)
];
}
);
default = { };
};
services = lib.mkOption {
# TODO: deprecate these options
# services are deprecated in favor of `instances`
# visible = false;
description = ''
Services of the inventory.
- The first `<name>` is the moduleName. It must be a valid clanModule name.
- The second `<name>` is an arbitrary instance name.
???+ Example
```nix
# ClanModule name. See the module documentation for the available modules.
# Instance name, can be anything, some services might use it as a unique identifier.
services.borgbackup."instance_1" = {
roles.client.machines = ["machineA"];
};
```
!!! Note
Services MUST be added to machines via `roles` exclusively.
See [`roles.<rolename>.machines`](#inventory.services.roles.machines) or [`roles.<rolename>.tags`](#inventory.services.roles.tags) for more information.
'';
default = { };
type = types.attrsOf (
types.attrsOf (
types.submodule (
# instance name
{ name, ... }:
{
options.enabled = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Enable or disable the complete service.
If the service is disabled, it will not be added to any machine.
!!! Note
This flag is primarily used to temporarily disable a service.
I.e. A 'backup service' without any 'server' might be incomplete and would cause failure if enabled.
'';
};
options.meta = metaOptionsWith name;
options.extraModules = extraModulesOption;
options.config = moduleConfig // {
description = ''
Configuration of the specific clanModule.
!!! Note
Configuration is passed to the nixos configuration scoped to the module.
```nix
clan.<serviceName> = { ... # Config }
```
???+ Example
For `services.borgbackup` the config is the passed to the machine with the prefix of `clan.borgbackup`.
This means all config values are mapped to the `borgbackup` clanModule exclusively (`config.clan.borgbackup`).
```nix
{
services.borgbackup."instance_1".config = {
destinations = [ ... ];
# See the 'borgbackup' module docs for all options
};
}
```
!!! Note
The module author is responsible for supporting multiple instance configurations in different roles.
See each clanModule's documentation for more information.
'';
};
options.machines = lib.mkOption {
description = ''
Attribute set of machines specific config for the service.
Will be merged with other service configs, such as the role config and the global config.
For machine specific overrides use `mkForce` or other higher priority methods.
???+ Example
```{.nix hl_lines="4-7"}
services.borgbackup."instance_1" = {
roles.client.machines = ["machineA"];
machines.machineA.config = {
# Additional specific config for the machine
# This is merged with all other config places
};
};
```
'';
default = { };
type = types.attrsOf (
types.submodule {
options.extraModules = extraModulesOption;
options.config = moduleConfig // {
description = ''
Additional configuration of the specific machine.
See how [`service.<name>.<name>.config`](#inventory.services.config) works in general for further information.
'';
};
}
);
};
options.roles = lib.mkOption {
default = { };
type = types.attrsOf (
types.submodule {
options.machines = lib.mkOption {
default = [ ];
type = types.listOf types.str;
example = [ "machineA" ];
description = ''
List of machines which are part of the role.
The machines are referenced by their `attributeName` in the `inventory.machines` attribute set.
Memberships are declared here to determine which machines are part of the service.
Alternatively, `tags` can be used to determine the membership, more dynamically.
'';
};
options.tags = lib.mkOption {
default = [ ];
apply = lib.unique;
type = types.listOf types.str;
description = ''
List of tags which are used to determine the membership of the role.
The tags are matched against the `inventory.machines.<machineName>.tags` attribute set.
If a machine has at least one tag of the role, it is part of the role.
'';
};
options.config = moduleConfig // {
description = ''
Additional configuration of the specific role.
See how [`service.<name>.<name>.config`](#inventory.services.config) works in general for further information.
'';
};
options.extraModules = extraModulesOption;
}
);
};
}
)
)
);
};
};
}

View File

@@ -0,0 +1,16 @@
{
config,
lib,
clanLib,
...
}:
{
options.introspection = lib.mkOption {
readOnly = true;
# TODO: use options.inventory instead of the evaluate config attribute
default =
builtins.removeAttrs (clanLib.introspection.getPrios { options = config.inventory.options; })
# tags are freeformType which is not supported yet.
[ "tags" ];
};
}

View File

@@ -0,0 +1,35 @@
{ lib, ... }:
let
types = lib.types;
metaOptions = {
name = lib.mkOption {
type = types.strMatching "[a-zA-Z0-9_-]*";
example = "my_clan";
description = ''
Name of the clan.
Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
Should only contain alphanumeric characters, `_` and `-`.
'';
};
description = lib.mkOption {
default = null;
type = types.nullOr types.str;
description = ''
Optional freeform description
'';
};
icon = lib.mkOption {
default = null;
type = types.nullOr types.str;
description = ''
Under construction, will be used for the UI
'';
};
};
in
{
options = metaOptions;
}

View File

@@ -0,0 +1,85 @@
{
settingsOption ? null,
nestedSettingsOption ? null,
}:
{ lib, clanLib, ... }:
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)
]
);
};
};
}

View File

@@ -0,0 +1,43 @@
{
flakeInputs,
clanLib,
localModuleSet,
}:
{ lib, config, ... }:
let
inspectModule =
inputName: moduleName: module:
let
eval = clanLib.inventory.evalClanService {
modules = [ module ];
prefix = [
inputName
"clan"
"modules"
moduleName
];
};
in
{
manifest = eval.config.manifest;
roles = lib.mapAttrs (_n: _v: { }) eval.config.roles;
};
in
{
options.modulesPerSource = lib.mkOption {
# { sourceName :: { moduleName :: {} }}
default =
let
inputsWithModules = lib.filterAttrs (_inputName: v: v ? clan.modules) flakeInputs;
in
lib.mapAttrs (
inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules
) inputsWithModules;
};
options.localModules = lib.mkOption {
default = lib.mapAttrs (inspectModule "self") localModuleSet;
};
}