Modules/constraints: init constraints checking for inventory compatible modules
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
description = "Configures [Zerotier VPN](https://zerotier.com) secure and efficient networking within a Clan.."
|
description = "Configures [Zerotier VPN](https://zerotier.com) secure and efficient networking within a Clan.."
|
||||||
features = [ "inventory" ]
|
features = [ "inventory" ]
|
||||||
|
|
||||||
|
constraints.roles.controller.eq = 1
|
||||||
|
constraints.roles.moon.max = 7
|
||||||
---
|
---
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|||||||
@@ -45,18 +45,11 @@ in
|
|||||||
|
|
||||||
config = {
|
config = {
|
||||||
assertions = [
|
assertions = [
|
||||||
|
# TODO: This should also be checked via frontmatter constraints
|
||||||
{
|
{
|
||||||
assertion = builtins.length instanceNames == 1;
|
assertion = builtins.length instanceNames == 1;
|
||||||
message = "The zerotier module currently only supports one instance per machine, but found ${builtins.toString instanceNames}";
|
message = "The zerotier module currently only supports one instance per machine, but found ${builtins.toString instanceNames}";
|
||||||
}
|
}
|
||||||
{
|
|
||||||
assertion = builtins.length roles.controller.machines == 1;
|
|
||||||
message = "The zerotier module requires exactly one controller, but found ${builtins.toString roles.controller.machines}";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = builtins.length roles.moons.machines <= 7;
|
|
||||||
message = "The zerotier module allows at most for seven moons , but found ${builtins.toString roles.moons.machines}";
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
clan.core.networking.zerotier.networkId = networkId;
|
clan.core.networking.zerotier.networkId = networkId;
|
||||||
|
|||||||
@@ -192,3 +192,17 @@ Assuming that there is a common code path or a common interface between `server`
|
|||||||
Many modules use `roles/default.nix` which registers the role `default`.
|
Many modules use `roles/default.nix` which registers the role `default`.
|
||||||
|
|
||||||
If you are a clan module author and your module has only one role where you cannot determine the name, then we would like you to follow the convention.
|
If you are a clan module author and your module has only one role where you cannot determine the name, then we would like you to follow the convention.
|
||||||
|
|
||||||
|
|
||||||
|
`constraints.roles.<roleName>.<constraintType>` (Optional `int`) (Experimental)
|
||||||
|
: Contraints for the module
|
||||||
|
|
||||||
|
The following example requires exactly one `server`
|
||||||
|
and supports up to `7` clients
|
||||||
|
|
||||||
|
```md
|
||||||
|
---
|
||||||
|
constraints.roles.server.eq = 1
|
||||||
|
constraints.roles.client.max = 7
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|||||||
54
lib/constraints/default.nix
Normal file
54
lib/constraints/default.nix
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
resolvedRoles,
|
||||||
|
moduleName,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./interface.nix
|
||||||
|
];
|
||||||
|
config.assertions = lib.foldl' (
|
||||||
|
ass: roleName:
|
||||||
|
let
|
||||||
|
roleConstraints = config.roles.${roleName};
|
||||||
|
members = resolvedRoles.${roleName}.machines;
|
||||||
|
memberCount = builtins.length members;
|
||||||
|
# Checks
|
||||||
|
eqCheck =
|
||||||
|
if roleConstraints.eq != null then
|
||||||
|
[
|
||||||
|
{
|
||||||
|
assertion = memberCount == roleConstraints.eq;
|
||||||
|
message = "The ${moduleName} module requires exactly ${builtins.toString roleConstraints.eq} '${roleName}', but found ${builtins.toString memberCount}: ${builtins.toString members}";
|
||||||
|
}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[ ];
|
||||||
|
|
||||||
|
minCheck =
|
||||||
|
if roleConstraints.min > 0 then
|
||||||
|
[
|
||||||
|
{
|
||||||
|
assertion = memberCount >= roleConstraints.min;
|
||||||
|
message = "The ${moduleName} module requires at least ${builtins.toString roleConstraints.min} '${roleName}'s, but found ${builtins.toString memberCount}: ${builtins.toString members}";
|
||||||
|
}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[ ];
|
||||||
|
|
||||||
|
maxCheck =
|
||||||
|
if roleConstraints.max != null then
|
||||||
|
[
|
||||||
|
{
|
||||||
|
assertion = memberCount <= roleConstraints.max;
|
||||||
|
message = "The ${moduleName} module allows at most for ${builtins.toString roleConstraints.max} '${roleName}'s, but found ${builtins.toString memberCount}: ${builtins.toString members}";
|
||||||
|
}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[ ];
|
||||||
|
in
|
||||||
|
eqCheck ++ minCheck ++ maxCheck ++ ass
|
||||||
|
) [ ] (lib.attrNames config.roles);
|
||||||
|
}
|
||||||
54
lib/constraints/interface.nix
Normal file
54
lib/constraints/interface.nix
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{ lib, allRoles, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib) mkOption types;
|
||||||
|
rolesAttrs = builtins.groupBy lib.id allRoles;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.roles = lib.mapAttrs (
|
||||||
|
_name: _:
|
||||||
|
mkOption {
|
||||||
|
default = { };
|
||||||
|
type = types.submoduleWith {
|
||||||
|
modules = [
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
max = mkOption {
|
||||||
|
type = types.nullOr types.int;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
min = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 0;
|
||||||
|
};
|
||||||
|
eq = mkOption {
|
||||||
|
type = types.nullOr types.int;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) rolesAttrs;
|
||||||
|
|
||||||
|
# The resulting assertions
|
||||||
|
options.assertions = mkOption {
|
||||||
|
default = [ ];
|
||||||
|
type = types.listOf (
|
||||||
|
types.submoduleWith {
|
||||||
|
modules = [
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
assertion = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
};
|
||||||
|
message = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,63 @@
|
|||||||
{ clan-core, lib }:
|
{ clan-core, lib }:
|
||||||
rec {
|
let
|
||||||
|
getRoles =
|
||||||
|
modulePath:
|
||||||
|
let
|
||||||
|
rolesDir = modulePath + "/roles";
|
||||||
|
in
|
||||||
|
if builtins.pathExists rolesDir then
|
||||||
|
lib.pipe rolesDir [
|
||||||
|
builtins.readDir
|
||||||
|
(lib.filterAttrs (_n: v: v == "regular"))
|
||||||
|
lib.attrNames
|
||||||
|
(lib.filter (fileName: lib.hasSuffix ".nix" fileName))
|
||||||
|
(map (fileName: lib.removeSuffix ".nix" fileName))
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[ ];
|
||||||
|
|
||||||
|
getConstraints =
|
||||||
|
modulename:
|
||||||
|
let
|
||||||
|
eval = lib.evalModules {
|
||||||
|
specialArgs = {
|
||||||
|
allRoles = getRoles clan-core.clanModules.${modulename};
|
||||||
|
};
|
||||||
|
modules = [
|
||||||
|
./constraints/interface.nix
|
||||||
|
(getFrontmatter modulename).constraints
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
eval.config.roles;
|
||||||
|
|
||||||
|
checkConstraints =
|
||||||
|
{ moduleName, resolvedRoles }:
|
||||||
|
let
|
||||||
|
eval = lib.evalModules {
|
||||||
|
specialArgs = {
|
||||||
|
inherit moduleName;
|
||||||
|
allRoles = getRoles clan-core.clanModules.${moduleName};
|
||||||
|
resolvedRoles = {
|
||||||
|
controller = {
|
||||||
|
machines = [ "test-inventory-machine" ];
|
||||||
|
};
|
||||||
|
moon = {
|
||||||
|
machines = [ ];
|
||||||
|
};
|
||||||
|
peer = {
|
||||||
|
machines = [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
modules = [
|
||||||
|
./constraints/default.nix
|
||||||
|
((getFrontmatter moduleName).constraints or { })
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
eval.config.assertions;
|
||||||
|
|
||||||
getReadme =
|
getReadme =
|
||||||
modulename:
|
modulename:
|
||||||
let
|
let
|
||||||
@@ -38,4 +96,13 @@ rec {
|
|||||||
---
|
---
|
||||||
...rest of your README.md...
|
...rest of your README.md...
|
||||||
'';
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit
|
||||||
|
getFrontmatter
|
||||||
|
getReadme
|
||||||
|
getRoles
|
||||||
|
getConstraints
|
||||||
|
checkConstraints
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,21 +55,7 @@ let
|
|||||||
evalClanModulesWithRoles =
|
evalClanModulesWithRoles =
|
||||||
clanModules:
|
clanModules:
|
||||||
let
|
let
|
||||||
getRoles =
|
getRoles = clan-core.lib.modules.getRoles;
|
||||||
modulePath:
|
|
||||||
let
|
|
||||||
rolesDir = "${modulePath}/roles";
|
|
||||||
in
|
|
||||||
if builtins.pathExists rolesDir then
|
|
||||||
lib.pipe rolesDir [
|
|
||||||
builtins.readDir
|
|
||||||
(lib.filterAttrs (_n: v: v == "regular"))
|
|
||||||
lib.attrNames
|
|
||||||
(lib.filter (fileName: lib.hasSuffix ".nix" fileName))
|
|
||||||
(map (fileName: lib.removeSuffix ".nix" fileName))
|
|
||||||
]
|
|
||||||
else
|
|
||||||
[ ];
|
|
||||||
res = builtins.mapAttrs (
|
res = builtins.mapAttrs (
|
||||||
moduleName: module:
|
moduleName: module:
|
||||||
let
|
let
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ let
|
|||||||
builtins.elem "inventory" (clan-core.lib.modules.getFrontmatter serviceName).features or [ ];
|
builtins.elem "inventory" (clan-core.lib.modules.getFrontmatter serviceName).features or [ ];
|
||||||
|
|
||||||
trimExtension = name: builtins.substring 0 (builtins.stringLength name - 4) name;
|
trimExtension = name: builtins.substring 0 (builtins.stringLength name - 4) name;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Returns a NixOS configuration for every machine in the inventory.
|
Returns a NixOS configuration for every machine in the inventory.
|
||||||
|
|
||||||
@@ -126,6 +127,10 @@ let
|
|||||||
nonExistingRoles = builtins.filter (role: !(builtins.elem role roles)) (
|
nonExistingRoles = builtins.filter (role: !(builtins.elem role roles)) (
|
||||||
builtins.attrNames (serviceConfig.roles or { })
|
builtins.attrNames (serviceConfig.roles or { })
|
||||||
);
|
);
|
||||||
|
constraintAssertions = clan-core.lib.modules.checkConstraints {
|
||||||
|
moduleName = serviceName;
|
||||||
|
inherit resolvedRoles;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
if (nonExistingRoles != [ ]) then
|
if (nonExistingRoles != [ ]) then
|
||||||
throw "Roles ${builtins.toString nonExistingRoles} are not defined in the service ${serviceName}."
|
throw "Roles ${builtins.toString nonExistingRoles} are not defined in the service ${serviceName}."
|
||||||
@@ -149,6 +154,7 @@ let
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
({
|
({
|
||||||
|
assertions = constraintAssertions;
|
||||||
clan.inventory.services.${serviceName}.${instanceName} = {
|
clan.inventory.services.${serviceName}.${instanceName} = {
|
||||||
roles = resolvedRoles;
|
roles = resolvedRoles;
|
||||||
# TODO: Add inverseRoles to the service config if needed
|
# TODO: Add inverseRoles to the service config if needed
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class Frontmatter:
|
|||||||
description: str
|
description: str
|
||||||
categories: list[str] = field(default_factory=lambda: ["Uncategorized"])
|
categories: list[str] = field(default_factory=lambda: ["Uncategorized"])
|
||||||
features: list[str] = field(default_factory=list)
|
features: list[str] = field(default_factory=list)
|
||||||
|
constraints: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def categories_info(self) -> dict[str, CategoryInfo]:
|
def categories_info(self) -> dict[str, CategoryInfo]:
|
||||||
|
|||||||
Reference in New Issue
Block a user