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.."
|
||||
features = [ "inventory" ]
|
||||
|
||||
constraints.roles.controller.eq = 1
|
||||
constraints.roles.moon.max = 7
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -45,18 +45,11 @@ in
|
||||
|
||||
config = {
|
||||
assertions = [
|
||||
# TODO: This should also be checked via frontmatter constraints
|
||||
{
|
||||
assertion = builtins.length instanceNames == 1;
|
||||
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;
|
||||
|
||||
@@ -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`.
|
||||
|
||||
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 }:
|
||||
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 =
|
||||
modulename:
|
||||
let
|
||||
@@ -38,4 +96,13 @@ rec {
|
||||
---
|
||||
...rest of your README.md...
|
||||
'';
|
||||
in
|
||||
{
|
||||
inherit
|
||||
getFrontmatter
|
||||
getReadme
|
||||
getRoles
|
||||
getConstraints
|
||||
checkConstraints
|
||||
;
|
||||
}
|
||||
|
||||
@@ -55,21 +55,7 @@ let
|
||||
evalClanModulesWithRoles =
|
||||
clanModules:
|
||||
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
|
||||
[ ];
|
||||
getRoles = clan-core.lib.modules.getRoles;
|
||||
res = builtins.mapAttrs (
|
||||
moduleName: module:
|
||||
let
|
||||
|
||||
@@ -42,6 +42,7 @@ let
|
||||
builtins.elem "inventory" (clan-core.lib.modules.getFrontmatter serviceName).features or [ ];
|
||||
|
||||
trimExtension = name: builtins.substring 0 (builtins.stringLength name - 4) name;
|
||||
|
||||
/*
|
||||
Returns a NixOS configuration for every machine in the inventory.
|
||||
|
||||
@@ -126,6 +127,10 @@ let
|
||||
nonExistingRoles = builtins.filter (role: !(builtins.elem role roles)) (
|
||||
builtins.attrNames (serviceConfig.roles or { })
|
||||
);
|
||||
constraintAssertions = clan-core.lib.modules.checkConstraints {
|
||||
moduleName = serviceName;
|
||||
inherit resolvedRoles;
|
||||
};
|
||||
in
|
||||
if (nonExistingRoles != [ ]) then
|
||||
throw "Roles ${builtins.toString nonExistingRoles} are not defined in the service ${serviceName}."
|
||||
@@ -149,6 +154,7 @@ let
|
||||
}
|
||||
)
|
||||
({
|
||||
assertions = constraintAssertions;
|
||||
clan.inventory.services.${serviceName}.${instanceName} = {
|
||||
roles = resolvedRoles;
|
||||
# TODO: Add inverseRoles to the service config if needed
|
||||
|
||||
@@ -25,6 +25,7 @@ class Frontmatter:
|
||||
description: str
|
||||
categories: list[str] = field(default_factory=lambda: ["Uncategorized"])
|
||||
features: list[str] = field(default_factory=list)
|
||||
constraints: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def categories_info(self) -> dict[str, CategoryInfo]:
|
||||
|
||||
Reference in New Issue
Block a user