Compare commits

..

1 Commits

Author SHA1 Message Date
pinpox
5f05e36c9d Remove outdated facts message
One every executino of `clan machines update` a message referring to
facts is still being printed. While there is more code to be cleaned up
around facts, this simple change removes the prominent message out of
user's attention.
2025-10-28 15:32:14 +01:00
48 changed files with 903 additions and 1460 deletions

View File

@@ -32,15 +32,17 @@
}; };
perInstance = perInstance =
{ {
instanceName, roles,
settings, lib,
machine,
... ...
}: }:
{ {
exports.networking = {
exports."internet/${instanceName}/default/${machine.name}".networking = { # TODO add user space network support to clan-cli
hosts = [ settings.host ]; peers = lib.mapAttrs (_name: machine: {
host.plain = machine.settings.host;
SSHOptions = map (_x: "-J x") machine.settings.jumphosts;
}) roles.default.machines;
}; };
}; };
}; };

View File

@@ -39,7 +39,6 @@
... ...
}: }:
let let
uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list);
# Collect searchDomains from all servers in this instance # Collect searchDomains from all servers in this instance
allServerSearchDomains = lib.flatten ( allServerSearchDomains = lib.flatten (
lib.mapAttrsToList (_name: machineConfig: machineConfig.settings.certificate.searchDomains or [ ]) ( lib.mapAttrsToList (_name: machineConfig: machineConfig.settings.certificate.searchDomains or [ ]) (
@@ -47,7 +46,7 @@
) )
); );
# Merge client's searchDomains with all servers' searchDomains # Merge client's searchDomains with all servers' searchDomains
searchDomains = uniqueStrings (settings.certificate.searchDomains ++ allServerSearchDomains); searchDomains = lib.uniqueStrings (settings.certificate.searchDomains ++ allServerSearchDomains);
in in
{ {
clan.core.vars.generators.openssh-ca = lib.mkIf (searchDomains != [ ]) { clan.core.vars.generators.openssh-ca = lib.mkIf (searchDomains != [ ]) {

View File

@@ -1 +0,0 @@
This a test README just to appease the eval warnings if we don't have one

View File

@@ -1,111 +0,0 @@
/*
Set up a CA chain for the clan. There will be one root CA for each instance
of the ssl service, then each host has its own host CA that is signed by the
instance-wide root CA.
Trusting the root CA, will result in also trusting the individual host CAs,
as they are signed by it.
Hosts can then use their respective host CAs to expose SSL secured services.
*/
{
exports,
config,
lib,
...
}:
{
_class = "clan.service";
manifest.name = "clan-core/ssl";
manifest.description = "Set up a CA infrastucture for your clan";
manifest.readme = builtins.readFile ./README.md;
# Generate a root CA for each instances of the ssl module.
exports = lib.mapAttrs' (instanceName: _: {
"ssl/${instanceName}///".vars.generators.ssl-root-ca =
{ config, ... }:
{
files.key = { };
files.cert.secret = false;
runtimeInputs = [
config.pkgs.pkgs.openssl
];
script = ''
# Generate CA private key (4096-bit RSA)
openssl genrsa -out "$out/key" 4096
# Generate self-signed CA certificate (valid for 10 years)
openssl req -new -x509 \
-key "$out/key" \
-out "$out/cert" \
-days 3650 \
-subj "/C=US/ST=State/L=City/O=Organization/OU=IT/CN=Root CA" \
-sha256
'';
};
}) config.instances;
roles.default = {
description = "Generate a host CA, signed by the root CA and trust the root CA";
perInstance =
{
instanceName,
machine,
...
}:
{
# Generate a host CA, which depends on (is signed by) the root CA
exports = {
"ssl/${instanceName}/default/${machine.name}/".vars.generators.ssl-host-ca =
{ config, ... }:
{
dependencies = {
ssl-root-ca = exports."ssl/${instanceName}///".vars.generators.ssl-root-ca;
};
files.key = { };
files.cert.secret = false;
runtimeInputs = [
config.pkgs.pkgs.openssl
];
script = ''
# Generate intermediate CA private key (4096-bit RSA)
openssl genrsa -out "$out/key" 4096
# Generate Certificate Signing Request (CSR) for intermediate CA
openssl req -new \
-key "$out/key" \
-out "$out/csr" \
-subj "/C=US/ST=State/L=City/O=Organization/OU=IT/CN=Host CA"
# Sign the CSR with the root CA to create the intermediate certificate
openssl x509 -req \
-in "$out/csr" \
-CA "$dependencies/ssl-root-ca/cert" \
-CAkey "$dependencies/ssl-root-ca/key" \
-CAcreateserial \
-out "$out/cert" \
-days 3650 \
-sha256 \
-extfile <(printf "basicConstraints=CA:TRUE\nkeyUsage=keyCertSign,cRLSign")
'';
};
};
nixosModule =
{ ... }:
{
# We trust the (public) root CA certificate on all machines with this role
security.pki.certificateFiles = [
exports."ssl/${instanceName}///".vars.generators.ssl-root-ca.files.cert.path
];
};
};
};
}

View File

@@ -1,47 +0,0 @@
{
self,
inputs,
lib,
...
}:
let
module = ./default.nix;
in
{
clan.modules = {
ssl = module;
};
perSystem =
{ ... }:
let
# Module that contains the tests
# This module adds:
# - legacyPackages.<system>.eval-tests-ssl
# - checks.<system>.eval-tests-ssl
# unit-test-module = (
# self.clanLib.test.flakeModules.makeEvalChecks {
# inherit module;
# inherit inputs;
# fileset = lib.fileset.unions [
# # The ssl service being tested
# ../../clanServices/ssl
# # Required modules
# ../../nixosModules/clanCore
# ];
# testName = "ssl";
# tests = ./tests/eval-tests.nix;
# # Optional arguments passed to the test
# testArgs = { };
# }
# );
in
{
# imports = [ unit-test-module ];
clan.nixosTests.ssl = {
imports = [ ./tests/vm/default.nix ];
clan.modules.ssl = module;
};
};
}

View File

@@ -1,23 +0,0 @@
{
name = "ssl";
clan = {
directory = ./.;
inventory = {
machines.peer1 = { };
machines.peer2 = { };
instances."test" = {
module.name = "ssl";
module.input = "self";
roles.default.machines.peer1 = { };
};
};
};
testScript =
{ ... }:
''
start_all()
'';
}

View File

@@ -41,14 +41,14 @@ let
# In this case it is 'self-zerotier-redux' # In this case it is 'self-zerotier-redux'
# This is usually only used internally, but we can use it to test the evaluation of service module in isolation # This is usually only used internally, but we can use it to test the evaluation of service module in isolation
# evaluatedService = # evaluatedService =
# testFlake.clanInternals.inventoryClass.distributedServices.servicesEval.config.mappedServices.self-zerotier-redux.config; # testFlake.clanInternals.inventoryClass.distributedServices.importedModulesEvaluated.self-zerotier-redux.config;
in in
{ {
test_simple = { test_simple = {
inherit testFlake; inherit testFlake;
expr = expr =
testFlake.config.clan.clanInternals.inventoryClass.distributedServices.servicesEval.config.mappedServices.self-wifi.config; testFlake.config.clan.clanInternals.inventoryClass.distributedServices.importedModulesEvaluated.self-wifi.config;
expected = 1; expected = 1;
# expr = { # expr = {

View File

@@ -74,20 +74,13 @@
# TODO make it nicer @lassulus, @picnoir wants microlens # TODO make it nicer @lassulus, @picnoir wants microlens
# Get a list of all exported IPs from all VPN modules # Get a list of all exported IPs from all VPN modules
# exportedPeerIPs = builtins.foldl' ( exportedPeerIPs = builtins.foldl' (
# acc: e: acc: e:
# if e == { } then if e == { } then
# acc acc
# else else
# acc ++ (lib.flatten (builtins.filter (s: s != "") (lib.attrValues (select' "peers.*.plain" e)))) acc ++ (lib.flatten (builtins.filter (s: s != "") (lib.attrValues (select' "peers.*.plain" e))))
# ) [ ] (lib.attrValues (select' "*.networking.?peers.*.host.?plain" exports)); ) [ ] (lib.attrValues (select' "instances.*.networking.?peers.*.host.?plain" exports));
# exports."internet/${instanceName}/default/${machine.name}".networking = {
# hosts = [ settings.host ];
# };
# exportedPeerIPs = (select' "*".networking.hosts exports);
exportedPeerIPs = lib.flatten (builtins.attrValues (select' "*.networking.hosts" exports));
# Construct a list of peers in yggdrasil format # Construct a list of peers in yggdrasil format
exportedPeers = lib.flatten (map mkPeers exportedPeerIPs); exportedPeers = lib.flatten (map mkPeers exportedPeerIPs);

View File

@@ -21,16 +21,9 @@
# Peers are set form exports of the internet service # Peers are set form exports of the internet service
instances."internet" = { instances."internet" = {
module.name = "internet"; module.name = "internet";
roles.default.machines.peer1.settings.host = "peer1-internet"; roles.default.machines.peer1.settings.host = "peer1";
roles.default.machines.peer2.settings.host = "peer2-internet"; roles.default.machines.peer2.settings.host = "peer2";
}; };
instances."zerotier" = {
module.name = "zerotier";
roles.controller.machines.peer1 = { };
roles.peer.machines.peer2 = { };
};
}; };
}; };

View File

@@ -1,18 +0,0 @@
{
"data": "ENC[AES256_GCM,data:ZkirPKTvLpV3+aMklbRIkafGCMISIRrqgFu8B0A1nQEdeqRR0bexoRuzLopuj95mqPKYHWT9ArF8zDqVW9t4UgazTgprK/coFlKk/2wO8dO2JmVcFlGZou2Hz6JVvt8xuELU350lpF+o4k1xmAqswqaRQyqgAIvVDnym/jZPj9hBZpSXr/IcUnH4cXcNv51Xt82Zvo132RoaU1warlNk1p3dr1DRHU56KtEwhkj9YxoIcS4K4BaEl9L87REXnFEBu5p8FeO1f3bp/ZFOxL7bYKROFHYhK4mIlSTVmYJg4a1CP0M7v842xm83C37Y6xgN8SltC/ld9TuxBNVhfzmHHotpBXvAbwxkCJE6ChJI,iv:M4jqMRvbjODcWGjJUMc3ys4Tra0KBwVXOVMoeXcAXuQ=,tag:irDJqWEeXlIXOv/DMZWlGQ==,type:str]",
"sops": {
"age": [
{
"recipient": "age1p8trv2dmpanl3gnzj294c4t5uysu7d6rfjncp5lmn6redyda8fns6p7kca",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwVGJGWlZOb05QL3AzSzFM\nUG4vV3RFK2RjVEhVd2QzQ3pTMUl0UmFLaURnCkRORDBuK0xUM1pYSFRFZXlpK1Na\nUHp6b3pWeEl0SkF2ZERaa3gyczh0RlkKLS0tIHFoanBkS1Jhc3ovQlJFV0lCQVpY\nUEUrcmZlbkhQa0lac3pqenBXWkpDZTgKNQ6Lu4L6zHKTN4pe2T3eg7lvTeZQ2/mf\nD33YfN15W/yuOb+LzVTwSj6wPgQuSaVRlgbCm/t1adzTnUZmruWxuA==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1YVMrSEkybzJpdXVHQWtP\nMzQ2QXZmQXJNL05ORDRobWZmQmdrTWtiVDJZCk9Wckg4eVJiU21BcFQ4MDhjTzlw\nVnh6b25NM3ZSNXRIQUEwd0RaSjg1MW8KLS0tICtqVWxpN09CSC9kcUdvRmw1RmRh\nOHlWQXEwYWFPY2VsM0Q0RzJyL2FWNUUK3f7t64UBdGtzxo0upCugNvA2vKUXL6gb\n0CJq4MG1s+lgFpvenRlozsaG3I8IxPHkFWuTA6OuUCCwaJqb0eT4ZA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-10-31T14:34:48Z",
"mac": "ENC[AES256_GCM,data:+mMvTo1+4f9rQm1U6td5Sx7NYeuKJQeXcTpFOooAV8wt75XX2VhX059/S3krFJ8vIsMUqQ0PqPLipCNTaTi8cxkqHfsVQEGCcALGtisk5bnHWgipnFoaO6Ao9TKkmFBcQo9za9+Z40stNIzThOHWaZonvp9KWIVj92CFic62UT8=,iv:HhVf1rhN6Ocp6Bif1oXQScJUe4ndFw3Rv/obVYDx5aA=,tag:9M5iMVcj3ore3DQtwdJuMQ==,type:str]",
"version": "3.11.0"
}
}

View File

@@ -1 +0,0 @@
fd06:8020:2351:b57:2899:9306:8020:2351

View File

@@ -1,18 +0,0 @@
{
"data": "ENC[AES256_GCM,data:gzHNCz/yRXD9sXRvqpGC18ZUF1JLvpBO44klfRjl6WzCPHLrC9Mp6cGFa+U3CZL2i/0JGKOtQGH+82Ra6oAkOiWEcSRN/xmAmcZaoVPTnvZ2tF7vvlRfR5hq+p/ZQw4+Y4V1TIuYj2dLNrVIIGYmWSabqI0mgVTTjyRsDJSB4YgqGTYismvZ9QXICSDxwROIrC2xl0Xx+MYWhxR1PVJ3B1HbJ8KEQCuBVq46Wki/INe0bD+ODlxCv9GCGPgaNjMwACOwQXo5WGP9zSDq2HEkTeg5YUmX1o1G6LwkG2fY/Hr5XMiLGU6G0remP/WbCOoLRXdB/Luevg/rTlQ/dNDawPARsbZZSjLmk/BHUOUJ,iv:zPeIyZi2ckbEcbX4FFhyN3ryWf4eoRu4XIafeAje28E=,tag:8/Vn0m+/wMGY706fYX55Vg==,type:str]",
"sops": {
"age": [
{
"recipient": "age107mprppm3r9u7f26e6t5mhtdny0h5ugfmfjy8kac2tw9nrh9a3ksex0xca",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDYlU4cG1KYXZodFJYYXNo\ndjhNbUFzNEhySzI2NmduR0EwOUhENFRZN3o4CmtSNG5ObkM2bDJXaXk1QlFVWURK\nV1lRa1VVV0hNZlh0eVJpVHFqU3FXMzgKLS0tIFhtUjZnZVdMczNFVUMrL2Q0b1Rz\nRFlzTUFXVWZwM2gwRW1LTzd0a2lhQTAKHyakwS8kB4Gg4Vjs3PJsbF3VHzJjAbOR\nR+y6op3zPjQpr5QfsRn4MoES/ViGDPZWLYxXUSMctGVDxIfgdZxP9A==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1dHBaY015Q2J2NlRyRGEy\nRGtRcm1YckhYSm5mbU5GaGFaTjhRa1UraWpRCnFWSDBSYURFS21QYUYxVXdKdGVi\nY1hiN3c3eTlJUWo2dXZXUk9TN3g3ZVkKLS0tIGJneUlaMU1KeVVBcXN5L3FIMjNP\nYkpWTVA3d2k1a3Y5Yk9kUUF3SFo2V2sKGLQYVmX8HnDqX5K/tdbfgYnpVmaTArIY\nuhw+CtrXmEHhksZqgGCcjEoCz7cDMzMA42kVdqh/OfFzJNxrRfJjPA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-10-31T14:59:28Z",
"mac": "ENC[AES256_GCM,data:MWpzOKUYXkmw2DX6YsN5pPIF9Y6GZ4rPnwq3uaOnFm40SOXPN2/JXSL7E9bGgaBeboUbChNwiGmBBRQX+7d2Te/NoItJAPw4YJTtquA+Rb7+sgPUoL6kYP7YZfjw1Z2hi61YMYXZH0/q4tBx6SNukt7o/uRYLu2LjyO09251uO4=,iv:YVXr5u2xwVEOlG+xYguAO1ZsCXvMx6rhXBV24CkFPv8=,tag:AOK4Pi2YYx4w0je9gALDLw==,type:str]",
"version": "3.11.0"
}
}

View File

@@ -1 +0,0 @@
fd06:8020:2351:b57:2899:9340:7f3b:e1b3

View File

@@ -1,6 +1,5 @@
{ {
clanLib, clanLib,
directory,
... ...
}: }:
{ {
@@ -17,23 +16,21 @@
instanceName, instanceName,
roles, roles,
lib, lib,
machine,
... ...
}: }:
{ {
exports.networking = {
exports."internet/${instanceName}/peer/${machine.name}".networking = { priority = lib.mkDefault 900;
hosts = lib.flatten [ # TODO add user space network support to clan-cli
(clanLib.vars.getPublicValue { module = "clan_lib.network.zerotier";
flake = directory; peers = lib.mapAttrs (name: _machine: {
machine = machine.name; host.var = {
machine = name;
generator = "zerotier"; generator = "zerotier";
file = "zerotier-ip"; file = "zerotier-ip";
# default = throw "kaputt"; };
}) }) roles.peer.machines;
];
}; };
nixosModule = nixosModule =
{ {
config, config,
@@ -143,9 +140,6 @@
pkgs, pkgs,
... ...
}: }:
let
uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list);
in
{ {
imports = [ imports = [
(import ./shared.nix { (import ./shared.nix {
@@ -162,7 +156,7 @@
config = { config = {
systemd.services.zerotier-inventory-autoaccept = systemd.services.zerotier-inventory-autoaccept =
let let
machines = uniqueStrings ( machines = lib.uniqueStrings (
(lib.optionals (roles ? moon) (lib.attrNames roles.moon.machines)) (lib.optionals (roles ? moon) (lib.attrNames roles.moon.machines))
++ (lib.optionals (roles ? controller) (lib.attrNames roles.controller.machines)) ++ (lib.optionals (roles ? controller) (lib.attrNames roles.controller.machines))
++ (lib.optionals (roles ? peer) (lib.attrNames roles.peer.machines)) ++ (lib.optionals (roles ? peer) (lib.attrNames roles.peer.machines))

12
devFlake/flake.lock generated
View File

@@ -105,11 +105,11 @@
}, },
"nixpkgs-dev": { "nixpkgs-dev": {
"locked": { "locked": {
"lastModified": 1761748483, "lastModified": 1761631514,
"narHash": "sha256-v7fttCB5lJ22Ok7+N7ZbLhDeM89QIz9YWtQP4XN7xgA=", "narHash": "sha256-VsXz+2W4DFBozzppbF9SXD9pNcv17Z+c/lYXvPJi/eI=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "061c55856b29b8b9360e14231a0986c7f85f1130", "rev": "a0b0d4b52b5f375658ca8371dc49bff171dbda91",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -128,11 +128,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1761730856, "lastModified": 1760652422,
"narHash": "sha256-t1i5p/vSWwueZSC0Z2BImxx3BjoUDNKyC2mk24krcMY=", "narHash": "sha256-C88Pgz38QIl9JxQceexqL2G7sw9vodHWx1Uaq+NRJrw=",
"owner": "NuschtOS", "owner": "NuschtOS",
"repo": "search", "repo": "search",
"rev": "e29de6db0cb3182e9aee75a3b1fd1919d995d85b", "rev": "3ebeebe8b6a49dfb11f771f761e0310f7c48d726",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -40,9 +40,6 @@ lib.fix (
# TODO: Flatten our lib functions like this: # TODO: Flatten our lib functions like this:
resolveModule = clanLib.callLib ./resolve-module { }; resolveModule = clanLib.callLib ./resolve-module { };
# Functions to help define exports
exports = clanLib.callLib ./exports.nix { };
fs = { fs = {
inherit (builtins) pathExists readDir; inherit (builtins) pathExists readDir;
}; };

View File

@@ -1,88 +0,0 @@
{ lib }:
let
/**
Creates a scope string for global exports
At least one of serviceName or machineName must be set.
The scope string has the format:
"/SERVICE/INSTANCE/ROLE/MACHINE"
If the parameter is not set, the corresponding part is left empty.
Semantically this means "all".
Examples:
mkScope { serviceName = "A"; }
-> "/A///"
mkScope { machineName = "jon"; }
-> "///jon"
mkScope { serviceName = "A"; instanceName = "i1"; roleName = "peer"; machineName = "jon"; }
-> "/A/i1/peer/jon"
*/
mkScope =
{
serviceName ? "",
instanceName ? "",
roleName ? "",
machineName ? "",
}:
let
parts = [
serviceName
instanceName
roleName
machineName
];
checkedParts = lib.map (
part:
lib.throwIf (builtins.match ".?/.?" part != null) ''
clanLib.exports.mkScope: ${part} cannot contain the "/" character
''
) parts;
in
lib.throwIf ((serviceName == "" && machineName == "")) ''
clanLib.exports.mkScope requires at least 'serviceName' or 'machineName' to be set
In case your use case requires neither
'' (lib.join "/" checkedParts);
/**
Parses a scope string into its components
Returns an attribute set with the keys:
- serviceName
- instanceName
- roleName
- machineName
Example:
parseScope "A/i1/peer/jon"
->
{
serviceName = "A";
instanceName = "i1";
roleName = "peer";
machineName = "jon";
}
*/
parseScope =
scopeStr:
let
parts = lib.splitString "/" scopeStr;
checkedParts = lib.throwIf (lib.length parts != 4) ''
clanLib.exports.parseScope: invalid scope string format, expected 4 parts separated by 3 "/"
'' (parts);
in
{
serviceName = lib.elemAt 0 checkedParts;
instanceName = lib.elemAt 1 checkedParts;
roleName = lib.elemAt 2 checkedParts;
machineName = lib.elemAt 3 checkedParts;
};
in
{
inherit mkScope parseScope;
}

View File

@@ -103,11 +103,6 @@ rec {
inherit lib; inherit lib;
clan-core = self; clan-core = self;
}; };
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests-build-clan
legacyPackages.eval-exports = import ./new_exports.nix {
inherit lib;
clan-core = self;
};
checks = { checks = {
eval-lib-build-clan = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } '' eval-lib-build-clan = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
export HOME="$(realpath .)" export HOME="$(realpath .)"

View File

@@ -2,7 +2,11 @@
lib, lib,
clanLib, clanLib,
}: }:
let
services = clanLib.callLib ./distributed-service/inventory-adapter.nix { };
in
{ {
inherit (services) mapInstances;
inventoryModule = { inventoryModule = {
_file = "clanLib.inventory.module"; _file = "clanLib.inventory.module";
imports = [ imports = [

View File

@@ -2,7 +2,6 @@
{ {
# TODO: consume directly from clan.config # TODO: consume directly from clan.config
directory, directory,
exports,
}: }:
{ {
lib, lib,
@@ -18,10 +17,10 @@ in
{ {
# TODO: merge these options into clan options # TODO: merge these options into clan options
options = { options = {
# exportsModule = mkOption { exportsModule = mkOption {
# type = types.deferredModule; type = types.deferredModule;
# readOnly = true; readOnly = true;
# }; };
mappedServices = mkOption { mappedServices = mkOption {
visible = false; visible = false;
type = attrsWith { type = attrsWith {
@@ -29,17 +28,19 @@ in
elemType = submoduleWith { elemType = submoduleWith {
class = "clan.service"; class = "clan.service";
specialArgs = { specialArgs = {
exports = config.exports;
directory = directory;
clanLib = specialArgs.clanLib; clanLib = specialArgs.clanLib;
inherit
exports
directory
;
}; };
modules = [ modules = [
( (
{ name, ... }: { name, ... }:
{ {
_module.args._ctx = [ name ]; _module.args._ctx = [ name ];
_module.args.clanLib = specialArgs.clanLib;
_module.args.exports = config.exports;
_module.args.directory = directory;
} }
) )
./service-module.nix ./service-module.nix
@@ -54,13 +55,34 @@ in
default = { }; default = { };
}; };
exports = mkOption { exports = mkOption {
type = types.lazyAttrsOf types.deferredModule; type = submoduleWith {
modules = [
# collect exports from all services {
# zipAttrs is needed until we use the record type. options = {
default = lib.zipAttrsWith (_name: values: { imports = values; }) ( instances = lib.mkOption {
lib.mapAttrsToList (_name: service: service.exports) config.mappedServices default = { };
); # instances.<instanceName>...
type = types.attrsOf (submoduleWith {
modules = [
config.exportsModule
];
});
};
# instances.<machineName>...
machines = lib.mkOption {
default = { };
type = types.attrsOf (submoduleWith {
modules = [
config.exportsModule
];
});
};
};
}
]
++ lib.mapAttrsToList (_: service: service.exports) config.mappedServices;
};
default = { };
}; };
}; };
} }

View File

@@ -0,0 +1,171 @@
# Adapter function between the inventory.instances and the clan.service module
#
# Data flow:
# - inventory.instances -> Adapter -> clan.service module -> Service Resources (i.e. NixosModules per Machine, Vars per Service, etc.)
#
# What this file does:
#
# - Resolves the [Module] to an actual module-path and imports it.
# - Groups together all the same modules into a single import and creates all instances for it.
# - Resolves the inventory tags into machines. Tags don't exist at the service level.
# Also combines the settings for 'machines' and 'tags'.
{
lib,
clanLib,
...
}:
{
mapInstances =
{
# This is used to resolve the module imports from 'flake.inputs'
flakeInputs,
# The clan inventory
inventory,
directory,
clanCoreModules,
prefix ? [ ],
exportsModule,
}:
let
# machineHasTag = machineName: tagName: lib.elem tagName inventory.machines.${machineName}.tags;
# map the instances into the module
importedModuleWithInstances = lib.mapAttrs (
instanceName: instance:
let
resolvedModule = clanLib.resolveModule {
moduleSpec = instance.module;
inherit flakeInputs clanCoreModules;
};
# Every instance includes machines via roles
# :: { client :: ... }
instanceRoles = lib.mapAttrs (
roleName: role:
let
resolvedMachines = clanLib.inventory.resolveTags {
members = {
# Explicit members
machines = lib.attrNames role.machines;
# Resolved Members
tags = lib.attrNames role.tags;
};
inherit (inventory) machines;
inherit instanceName roleName;
};
in
# instances.<instanceName>.roles.<roleName> =
# Remove "tags", they are resolved into "machines"
(removeAttrs role [ "tags" ])
// {
machines = lib.genAttrs resolvedMachines.machines (
machineName:
let
machineSettings = instance.roles.${roleName}.machines.${machineName}.settings or { };
in
# TODO: tag settings
# Wait for this feature until option introspection for 'settings' is done.
# This might get too complex to handle otherwise.
# settingsViaTags = lib.filterAttrs (
# tagName: _: machineHasTag machineName tagName
# ) instance.roles.${roleName}.tags;
{
# TODO: Do we want to wrap settings with
# setDefaultModuleLocation "inventory.instances.${instanceName}.roles.${roleName}.tags.${tagName}";
settings = {
imports = [
machineSettings
]; # ++ lib.attrValues (lib.mapAttrs (_tagName: v: v.settings) settingsViaTags);
};
}
);
}
) instance.roles;
in
{
inherit (instance) module;
inherit resolvedModule instanceRoles;
}
) inventory.instances or { };
# Group the instances by the module they resolve to
# This is necessary to evaluate the module in a single pass
# :: { <module.input>_<module.name> :: [ { name, value } ] }
# Since 'perMachine' needs access to all the instances we should include them as a whole
grouped = lib.foldlAttrs (
acc: instanceName: instance:
let
inputName = if instance.module.input == null then "<clan-core>" else instance.module.input;
id = inputName + "-" + instance.module.name;
in
acc
// {
${id} = acc.${id} or [ ] ++ [
{
inherit instanceName instance;
}
];
}
) { } importedModuleWithInstances;
# servicesEval.config.mappedServices.self-A.result.final.jon.nixosModule
allMachines = lib.mapAttrs (machineName: _: {
# This is the list of nixosModules for each machine
machineImports = lib.foldlAttrs (
acc: _module_ident: serviceModule:
acc ++ [ serviceModule.result.final.${machineName}.nixosModule or { } ]
) [ ] servicesEval.config.mappedServices;
}) inventory.machines or { };
evalServices =
{ modules, prefix }:
lib.evalModules {
class = "clan";
specialArgs = {
inherit clanLib;
_ctx = prefix;
};
modules = [
(import ./all-services-wrapper.nix { inherit directory; })
]
++ modules;
};
servicesEval = evalServices {
inherit prefix;
modules = [
{
inherit exportsModule;
mappedServices = lib.mapAttrs (_module_ident: instances: {
imports = [
# Import the resolved module.
# i.e. clan.modules.admin
{
options.module = lib.mkOption {
type = lib.types.raw;
default = (builtins.head instances).instance.module;
};
}
(builtins.head instances).instance.resolvedModule
] # Include all the instances that correlate to the resolved module
++ (builtins.map (v: {
instances.${v.instanceName}.roles = v.instance.instanceRoles;
}) instances);
}) grouped;
}
];
};
importedModulesEvaluated = servicesEval.config.mappedServices;
in
{
inherit
servicesEval
importedModuleWithInstances
# Exposed for testing
grouped
allMachines
importedModulesEvaluated
;
};
}

View File

@@ -7,14 +7,10 @@
... ...
}: }:
let let
inherit (lib) mkOption types; inherit (lib) mkOption types uniqueStrings;
inherit (types) attrsWith submoduleWith; inherit (types) attrsWith submoduleWith;
errorContext = "Error context: ${lib.concatStringsSep "." _ctx}"; errorContext = "Error context: ${lib.concatStringsSep "." _ctx}";
# TODO:
# Remove once this gets merged upstream; performs in O(n*log(n) instead of O(n^2))
# https://github.com/NixOS/nixpkgs/pull/355616/files
uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list);
/** /**
Merges the role- and machine-settings using the role interface Merges the role- and machine-settings using the role interface
@@ -504,7 +500,7 @@ in
staticModules = [ staticModules = [
({ ({
options.exports = mkOption { options.exports = mkOption {
type = types.lazyAttrsOf types.deferredModule; type = types.deferredModule;
default = { }; default = { };
description = '' description = ''
!!! Danger "Experimental Feature" !!! Danger "Experimental Feature"
@@ -634,16 +630,8 @@ in
type = types.deferredModuleWith { type = types.deferredModuleWith {
staticModules = [ staticModules = [
({ ({
# exports."///".generator.name = { _file ... import = []; _type = }
# exports."///".networking = { _file ... import = []; }
# generators."///".name = { name, ...}: { _file ... import = [];}
# networks."///" = { _file ... import = []; }
# { _file ... import = []; }
# { _file ... import = []; }
options.exports = mkOption { options.exports = mkOption {
type = types.lazyAttrsOf types.deferredModule; type = types.deferredModule;
default = { }; default = { };
description = '' description = ''
!!! Danger "Experimental Feature" !!! Danger "Experimental Feature"
@@ -775,38 +763,79 @@ in
``` ```
''; '';
default = { }; default = { };
type = types.lazyAttrsOf ( type = types.submoduleWith {
types.deferredModuleWith { # Static modules
# staticModules = []; modules = [
# lib.concatLists ( {
# lib.concatLists ( options.instances = mkOption {
# lib.mapAttrsToList ( type = types.attrsOf types.deferredModule;
# _roleName: role: description = ''
# lib.mapAttrsToList ( export modules defined in 'perInstance'
# _instanceName: instance: lib.mapAttrsToList (_machineName: v: v.exports) instance.allMachines mapped to their instance name
# ) role.allInstances
# ) config.result.allRoles Example
# )
# ) with instances:
# ++
} ```nix
); instances.A = { ... };
# # Lazy default via imports instances.B= { ... };
# # should probably be moved to deferredModuleWith { staticModules = [ ]; }
# imports = roles.peer.perInstance = { instanceName, machine, ... }:
# if config._docs_rendering then {
# [ ] exports.foo = 1;
# else }
# lib.mapAttrsToList (_roleName: role: {
# instances = lib.mapAttrs (_instanceName: instance: { This yields all other services can access these exports
# imports = lib.mapAttrsToList (_machineName: v: v.exports) instance.allMachines; =>
# }) role.allInstances; exports.instances.A.foo = 1;
# }) config.result.allRoles exports.instances.B.foo = 1;
# ++ lib.mapAttrsToList (machineName: machine: { ```
# machines.${machineName} = machine.exports; '';
# }) config.result.allMachines; };
# } options.machines = mkOption {
# ]; type = types.attrsOf types.deferredModule;
description = ''
export modules defined in 'perMachine'
mapped to their machine name
Example
with machines:
```nix
instances.A = { roles.peer.machines.jon = ... };
instances.B = { roles.peer.machines.jon = ... };
perMachine = { machine, ... }:
{
exports.foo = 1;
}
This yields all other services can access these exports
=>
exports.machines.jon.foo = 1;
exports.machines.sara.foo = 1;
```
'';
};
# Lazy default via imports
# should probably be moved to deferredModuleWith { staticModules = [ ]; }
imports =
if config._docs_rendering then
[ ]
else
lib.mapAttrsToList (_roleName: role: {
instances = lib.mapAttrs (_instanceName: instance: {
imports = lib.mapAttrsToList (_machineName: v: v.exports) instance.allMachines;
}) role.allInstances;
}) config.result.allRoles
++ lib.mapAttrsToList (machineName: machine: {
machines.${machineName} = machine.exports;
}) config.result.allMachines;
}
];
};
}; };
# --- # ---
# Place the result in _module.result to mark them as "internal" and discourage usage/overrides # Place the result in _module.result to mark them as "internal" and discourage usage/overrides
@@ -991,39 +1020,5 @@ in
} }
) config.result.allMachines; ) config.result.allMachines;
}; };
debug = mkOption {
default = lib.zipAttrsWith (_name: values: { imports = values; }) (
lib.mapAttrsToList (_machineName: machine: machine.exports) config.result.allMachines
);
};
}; };
imports = [
{
# collect exports from all machines
# zipAttrs is needed until we use the record type.
exports = lib.zipAttrsWith (_name: values: { imports = values; }) (
lib.mapAttrsToList (_machineName: machine: machine.exports) config.result.allMachines
);
}
{
# collect exports from all instances, roles and machines
# zipAttrs is needed until we use the record type.
exports = lib.zipAttrsWith (_name: values: { imports = values; }) (
lib.concatLists (
lib.concatLists (
lib.mapAttrsToList (
_roleName: role:
lib.mapAttrsToList (
_instanceName: instance: lib.mapAttrsToList (_machineName: v: v.exports) instance.allMachines
) role.allInstances
) config.result.allRoles
)
)
);
}
];
} }

View File

@@ -4,53 +4,63 @@
... ...
}: }:
let let
inherit (lib)
evalModules
;
flakeInputsFixture = { evalInventory =
upstream.clan.modules = { m:
uzzi = { (evalModules {
_class = "clan.service"; # Static modules
manifest = { modules = [
name = "uzzi-from-upstream"; clanLib.inventory.inventoryModule
}; {
}; _file = "test file";
}; tags.all = [ ];
}; tags.nixos = [ ];
tags.darwin = [ ];
}
{
modules.test = { };
}
m
];
}).config;
createTestClan = callInventoryAdapter =
testClan: inventoryModule:
let let
res = clanLib.clan ({ inventory = evalInventory inventoryModule;
# Static / mocked flakeInputsFixture = {
specialArgs = { self.clan.modules = inventoryModule.modules or { };
clan-core = { # Example upstream module
clan.modules = { }; upstream.clan.modules = {
uzzi = {
_class = "clan.service";
manifest = {
name = "uzzi-from-upstream";
};
}; };
}; };
self.inputs = flakeInputsFixture // { };
self.clan = res.config;
};
directory = ./.;
exportsModule = { };
imports = [
testClan
];
});
in in
res; clanLib.inventory.mapInstances {
directory = ./.;
clanCoreModules = { };
flakeInputs = flakeInputsFixture;
inherit inventory;
exportsModule = { };
};
in in
{ {
extraModules = import ./extraModules.nix { inherit clanLib; }; extraModules = import ./extraModules.nix { inherit clanLib; };
exports = import ./exports.nix { inherit lib clanLib; }; exports = import ./exports.nix { inherit lib clanLib; };
settings = import ./settings.nix { inherit lib createTestClan; }; settings = import ./settings.nix { inherit lib callInventoryAdapter; };
specialArgs = import ./specialArgs.nix { inherit lib createTestClan; }; specialArgs = import ./specialArgs.nix { inherit lib callInventoryAdapter; };
resolve_module_spec = import ./import_module_spec.nix { resolve_module_spec = import ./import_module_spec.nix { inherit lib callInventoryAdapter; };
inherit lib createTestClan;
};
test_simple = test_simple =
let let
res = createTestClan { res = callInventoryAdapter {
# Authored module # Authored module
# A minimal module looks like this # A minimal module looks like this
# It isn't exactly doing anything but it's a valid module that produces an output # It isn't exactly doing anything but it's a valid module that produces an output
@@ -61,7 +71,7 @@ in
}; };
}; };
# User config # User config
inventory.instances."instance_foo" = { instances."instance_foo" = {
module = { module = {
name = "simple-module"; name = "simple-module";
}; };
@@ -71,7 +81,7 @@ in
{ {
# Test that the module is mapped into the output # Test that the module is mapped into the output
# We might change the attribute name in the future # We might change the attribute name in the future
expr = res.config._services.mappedServices ? "<clan-core>-simple-module"; expr = res.importedModulesEvaluated ? "<clan-core>-simple-module";
expected = true; expected = true;
inherit res; inherit res;
}; };
@@ -82,7 +92,7 @@ in
# All instances should be included within one evaluation to make all of them available # All instances should be included within one evaluation to make all of them available
test_module_grouping = test_module_grouping =
let let
res = createTestClan { res = callInventoryAdapter {
# Authored module # Authored module
# A minimal module looks like this # A minimal module looks like this
# It isn't exactly doing anything but it's a valid module that produces an output # It isn't exactly doing anything but it's a valid module that produces an output
@@ -102,19 +112,18 @@ in
perMachine = { }: { }; perMachine = { }: { };
}; };
# User config # User config
inventory.instances."instance_foo" = { instances."instance_foo" = {
module = { module = {
name = "A"; name = "A";
}; };
}; };
inventory.instances."instance_bar" = { instances."instance_bar" = {
module = { module = {
name = "B"; name = "B";
}; };
}; };
inventory.instances."instance_baz" = { instances."instance_baz" = {
module = { module = {
name = "A"; name = "A";
}; };
@@ -124,16 +133,16 @@ in
{ {
# Test that the module is mapped into the output # Test that the module is mapped into the output
# We might change the attribute name in the future # We might change the attribute name in the future
expr = lib.attrNames res.config._services.mappedServices; expr = lib.mapAttrs (_n: v: builtins.length v) res.grouped;
expected = [ expected = {
"<clan-core>-A" "<clan-core>-A" = 2;
"<clan-core>-B" "<clan-core>-B" = 1;
]; };
}; };
test_creates_all_instances = test_creates_all_instances =
let let
res = createTestClan { res = callInventoryAdapter {
# Authored module # Authored module
# A minimal module looks like this # A minimal module looks like this
# It isn't exactly doing anything but it's a valid module that produces an output # It isn't exactly doing anything but it's a valid module that produces an output
@@ -145,24 +154,22 @@ in
perMachine = { }: { }; perMachine = { }: { };
}; };
inventory = { instances."instance_foo" = {
instances."instance_foo" = { module = {
module = { name = "A";
name = "A"; input = "self";
input = "self";
};
}; };
instances."instance_bar" = { };
module = { instances."instance_bar" = {
name = "A"; module = {
input = "self"; name = "A";
}; input = "self";
}; };
instances."instance_zaza" = { };
module = { instances."instance_zaza" = {
name = "B"; module = {
input = null; name = "B";
}; input = null;
}; };
}; };
}; };
@@ -170,7 +177,7 @@ in
{ {
# Test that the module is mapped into the output # Test that the module is mapped into the output
# We might change the attribute name in the future # We might change the attribute name in the future
expr = lib.attrNames res.config._services.mappedServices.self-A.instances; expr = lib.attrNames res.importedModulesEvaluated.self-A.instances;
expected = [ expected = [
"instance_bar" "instance_bar"
"instance_foo" "instance_foo"
@@ -180,7 +187,7 @@ in
# Membership via roles # Membership via roles
test_add_machines_directly = test_add_machines_directly =
let let
res = createTestClan { res = callInventoryAdapter {
# Authored module # Authored module
# A minimal module looks like this # A minimal module looks like this
# It isn't exactly doing anything but it's a valid module that produces an output # It isn't exactly doing anything but it's a valid module that produces an output
@@ -195,40 +202,38 @@ in
# perMachine = {}: {}; # perMachine = {}: {};
}; };
inventory = { machines = {
machines = { jon = { };
jon = { }; sara = { };
sara = { }; hxi = { };
hxi = { }; };
instances."instance_foo" = {
module = {
name = "A";
input = "self";
}; };
instances."instance_foo" = { roles.peer.machines.jon = { };
module = { };
name = "A"; instances."instance_bar" = {
input = "self"; module = {
}; name = "A";
roles.peer.machines.jon = { }; input = "self";
}; };
instances."instance_bar" = { roles.peer.machines.sara = { };
module = { };
name = "A"; instances."instance_zaza" = {
input = "self"; module = {
}; name = "B";
roles.peer.machines.sara = { }; input = null;
};
instances."instance_zaza" = {
module = {
name = "B";
input = null;
};
roles.peer.tags.all = { };
}; };
roles.peer.tags.all = { };
}; };
}; };
in in
{ {
# Test that the module is mapped into the output # Test that the module is mapped into the output
# We might change the attribute name in the future # We might change the attribute name in the future
expr = lib.attrNames res.config._services.mappedServices.self-A.result.allMachines; expr = lib.attrNames res.importedModulesEvaluated.self-A.result.allMachines;
expected = [ expected = [
"jon" "jon"
"sara" "sara"
@@ -238,7 +243,7 @@ in
# Membership via tags # Membership via tags
test_add_machines_via_tags = test_add_machines_via_tags =
let let
res = createTestClan { res = callInventoryAdapter {
# Authored module # Authored module
# A minimal module looks like this # A minimal module looks like this
# It isn't exactly doing anything but it's a valid module that produces an output # It isn't exactly doing anything but it's a valid module that produces an output
@@ -252,37 +257,35 @@ in
# perMachine = {}: {}; # perMachine = {}: {};
}; };
inventory = { machines = {
machines = { jon = {
jon = { tags = [ "foo" ];
tags = [ "foo" ];
};
sara = {
tags = [ "foo" ];
};
hxi = { };
}; };
instances."instance_foo" = { sara = {
module = { tags = [ "foo" ];
name = "A";
input = "self";
};
roles.peer.tags.foo = { };
}; };
instances."instance_zaza" = { hxi = { };
module = { };
name = "B"; instances."instance_foo" = {
input = null; module = {
}; name = "A";
roles.peer.tags.all = { }; input = "self";
}; };
roles.peer.tags.foo = { };
};
instances."instance_zaza" = {
module = {
name = "B";
input = null;
};
roles.peer.tags.all = { };
}; };
}; };
in in
{ {
# Test that the module is mapped into the output # Test that the module is mapped into the output
# We might change the attribute name in the future # We might change the attribute name in the future
expr = lib.attrNames res.config._services.mappedServices.self-A.result.allMachines; expr = lib.attrNames res.importedModulesEvaluated.self-A.result.allMachines;
expected = [ expected = [
"jon" "jon"
"sara" "sara"
@@ -290,9 +293,6 @@ in
}; };
machine_imports = import ./machine_imports.nix { inherit lib clanLib; }; machine_imports = import ./machine_imports.nix { inherit lib clanLib; };
per_machine_args = import ./per_machine_args.nix { inherit lib createTestClan; }; per_machine_args = import ./per_machine_args.nix { inherit lib callInventoryAdapter; };
per_instance_args = import ./per_instance_args.nix { per_instance_args = import ./per_instance_args.nix { inherit lib callInventoryAdapter; };
inherit lib;
callInventoryAdapter = createTestClan;
};
} }

View File

@@ -1,4 +1,4 @@
{ createTestClan, ... }: { callInventoryAdapter, ... }:
let let
# Authored module # Authored module
# A minimal module looks like this # A minimal module looks like this
@@ -23,13 +23,10 @@ let
resolve = resolve =
spec: spec:
createTestClan { callInventoryAdapter {
inherit modules; inherit modules machines;
inventory = { instances."instance_foo" = {
inherit machines; module = spec;
instances."instance_foo" = {
module = spec;
};
}; };
}; };
in in
@@ -39,16 +36,25 @@ in
(resolve { (resolve {
name = "A"; name = "A";
input = "self"; input = "self";
}).config._services.mappedServices.self-A.manifest.name; }).importedModuleWithInstances.instance_foo.resolvedModule;
expected = "network"; expected = {
_class = "clan.service";
manifest = {
name = "network";
};
};
}; };
test_import_remote_module_by_name = { test_import_remote_module_by_name = {
expr = expr =
(resolve { (resolve {
name = "uzzi"; name = "uzzi";
input = "upstream"; input = "upstream";
}).config._services.mappedServices.upstream-uzzi.manifest.name; }).importedModuleWithInstances.instance_foo.resolvedModule;
expected = "uzzi-from-upstream"; expected = {
_class = "clan.service";
manifest = {
name = "uzzi-from-upstream";
};
};
}; };
} }

View File

@@ -58,43 +58,39 @@ let
sara = { }; sara = { };
}; };
res = callInventoryAdapter { res = callInventoryAdapter {
inherit modules; inherit modules machines;
instances."instance_foo" = {
inventory = { module = {
inherit machines; name = "A";
instances."instance_foo" = { input = "self";
module = {
name = "A";
input = "self";
};
roles.peer.machines.jon = {
settings.timeout = lib.mkForce "foo-peer-jon";
};
roles.peer = {
settings.timeout = "foo-peer";
};
roles.controller.machines.jon = { };
}; };
instances."instance_bar" = { roles.peer.machines.jon = {
module = { settings.timeout = lib.mkForce "foo-peer-jon";
name = "A";
input = "self";
};
roles.peer.machines.jon = {
settings.timeout = "bar-peer-jon";
};
}; };
# TODO: move this into a seperate test. roles.peer = {
# Seperate out the check that this module is never imported settings.timeout = "foo-peer";
# import the module "B" (undefined)
# All machines have this instance
instances."instance_zaza" = {
module = {
name = "B";
input = null;
};
roles.peer.tags.all = { };
}; };
roles.controller.machines.jon = { };
};
instances."instance_bar" = {
module = {
name = "A";
input = "self";
};
roles.peer.machines.jon = {
settings.timeout = "bar-peer-jon";
};
};
# TODO: move this into a seperate test.
# Seperate out the check that this module is never imported
# import the module "B" (undefined)
# All machines have this instance
instances."instance_zaza" = {
module = {
name = "B";
input = null;
};
roles.peer.tags.all = { };
}; };
}; };
@@ -109,10 +105,9 @@ in
{ {
# settings should evaluate # settings should evaluate
test_per_instance_arguments = { test_per_instance_arguments = {
inherit res;
expr = { expr = {
instanceName = instanceName =
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName; res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName;
# settings are specific. # settings are specific.
# Below we access: # Below we access:
@@ -120,11 +115,11 @@ in
# roles = peer # roles = peer
# machines = jon # machines = jon
settings = settings =
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings; res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings;
machine = machine =
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.machine; res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.machine;
roles = roles =
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.roles; res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.roles;
}; };
expected = { expected = {
instanceName = "instance_foo"; instanceName = "instance_foo";
@@ -165,9 +160,9 @@ in
# TODO: Cannot be tested like this anymore # TODO: Cannot be tested like this anymore
test_per_instance_settings_vendoring = { test_per_instance_settings_vendoring = {
x = res.config._services.mappedServices.self-A; x = res.importedModulesEvaluated.self-A;
expr = expr =
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.vendoredSettings; res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.vendoredSettings;
expected = { expected = {
timeout = "config.thing"; timeout = "config.thing";
}; };

View File

@@ -1,4 +1,4 @@
{ lib, createTestClan }: { lib, callInventoryAdapter }:
let let
# Authored module # Authored module
# A minimal module looks like this # A minimal module looks like this
@@ -39,40 +39,36 @@ let
jon = { }; jon = { };
sara = { }; sara = { };
}; };
res = createTestClan { res = callInventoryAdapter {
inherit modules; inherit modules machines;
inventory = { instances."instance_foo" = {
module = {
inherit machines; name = "A";
instances."instance_foo" = { input = "self";
module = {
name = "A";
input = "self";
};
roles.peer.machines.jon = {
settings.timeout = lib.mkForce "foo-peer-jon";
};
roles.peer = {
settings.timeout = "foo-peer";
};
}; };
instances."instance_bar" = { roles.peer.machines.jon = {
module = { settings.timeout = lib.mkForce "foo-peer-jon";
name = "A";
input = "self";
};
roles.peer.machines.jon = {
settings.timeout = "bar-peer-jon";
};
}; };
instances."instance_zaza" = { roles.peer = {
module = { settings.timeout = "foo-peer";
name = "B";
input = null;
};
roles.peer.tags.all = { };
}; };
}; };
instances."instance_bar" = {
module = {
name = "A";
input = "self";
};
roles.peer.machines.jon = {
settings.timeout = "bar-peer-jon";
};
};
instances."instance_zaza" = {
module = {
name = "B";
input = null;
};
roles.peer.tags.all = { };
};
}; };
in in
@@ -83,7 +79,7 @@ in
inherit res; inherit res;
expr = { expr = {
hasMachineSettings = hasMachineSettings =
res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon
? settings; ? settings;
# settings are specific. # settings are specific.
@@ -92,10 +88,10 @@ in
# roles = peer # roles = peer
# machines = jon # machines = jon
specificMachineSettings = specificMachineSettings =
res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon.settings; res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon.settings;
hasRoleSettings = hasRoleSettings =
res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer
? settings; ? settings;
# settings are specific. # settings are specific.
@@ -104,7 +100,7 @@ in
# roles = peer # roles = peer
# machines = * # machines = *
specificRoleSettings = specificRoleSettings =
res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer; res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer;
}; };
expected = { expected = {
hasMachineSettings = true; hasMachineSettings = true;

View File

@@ -1,6 +1,6 @@
{ createTestClan, lib, ... }: { callInventoryAdapter, lib, ... }:
let let
res = createTestClan { res = callInventoryAdapter {
modules."A" = { modules."A" = {
_class = "clan.service"; _class = "clan.service";
manifest = { manifest = {
@@ -21,31 +21,28 @@ let
}; };
}; };
}; };
inventory = { machines = {
jon = { };
machines = { sara = { };
jon = { }; };
sara = { }; instances."instance_foo" = {
module = {
name = "A";
input = "self";
}; };
instances."instance_foo" = { # Settings for both jon and sara
module = { roles.peer.settings = {
name = "A"; timeout = 40;
input = "self";
};
# Settings for both jon and sara
roles.peer.settings = {
timeout = 40;
};
# Jon overrides timeout
roles.peer.machines.jon = {
settings.timeout = lib.mkForce 42;
};
roles.peer.machines.sara = { };
}; };
# Jon overrides timeout
roles.peer.machines.jon = {
settings.timeout = lib.mkForce 42;
};
roles.peer.machines.sara = { };
}; };
}; };
config = res.config._services.mappedServices.self-A; config = res.servicesEval.config.mappedServices.self-A;
# #
applySettings = applySettings =

View File

@@ -1,6 +1,6 @@
{ createTestClan, lib, ... }: { callInventoryAdapter, lib, ... }:
let let
res = createTestClan { res = callInventoryAdapter {
modules."A" = m: { modules."A" = m: {
_class = "clan.service"; _class = "clan.service";
config = { config = {
@@ -14,21 +14,19 @@ let
default = m; default = m;
}; };
}; };
inventory = { machines = {
machines = { jon = { };
jon = { }; };
}; instances."instance_foo" = {
instances."instance_foo" = { module = {
module = { name = "A";
name = "A"; input = "self";
input = "self";
};
roles.peer.machines.jon = { };
}; };
roles.peer.machines.jon = { };
}; };
}; };
specialArgs = lib.attrNames res.config._services.mappedServices.self-A.test.specialArgs; specialArgs = lib.attrNames res.servicesEval.config.mappedServices.self-A.test.specialArgs;
in in
{ {
test_simple = { test_simple = {

View File

@@ -1,221 +0,0 @@
{
clan-core,
lib,
}:
# TODO: TEST: define a clan without machines
{
test_simple =
let
eval = clan-core.clanLib.clan {
exports."///".foo = lib.mkForce eval.config.exports."///".bar;
directory = ./.;
self = {
clan = eval.config;
inputs = { };
};
machines.jon = { };
machines.sara = { };
exportsModule =
{ lib, ... }:
{
options.foo = lib.mkOption {
type = lib.types.number;
default = 0;
};
options.bar = lib.mkOption {
type = lib.types.number;
default = 0;
};
};
####### Service module "A"
modules.service-A =
{ ... }:
{
# config.exports
manifest.name = "A";
roles.default = {
# TODO: Remove automapping
# Currently exports are automapped
# scopes "/service=A/instance=hello/role=default/machine=jon"
# perInstance.exports.foo = 7;
# New style:
# Explizit scope
# perInstance.exports."service=A/instance=hello/role=default/machine=jon".foo = 7;
perInstance =
{ instanceName, machine, exports, ... }:
{
exports."A/${instanceName}/default/${machine.name}" = {
foo = 7;
# define export depending on B
bar = exports."B/B/default/${machine.name}".foo + 35;
};
# exports."A/${instanceName}/default/${machine.name}".
# default behavior
# exports = scope.mkExports { foo = 7; };
# We want to export things for different scopes from this scope;
# If this scope is used.
#
# Explicit scope; different from the function scope above
# exports = clanLib.scopedExport {
# # Different role export
# role = "peer";
# serviceName = config.manifest.name;
# inherit instanceName machineName;
# } { foo = 7; };
};
};
perMachine =
{ ... }:
{
#
# exports = scope.mkExports { foo = 7; };
# exports."A///${machine.name}".foo = 42;
# exports."B///".foo = 42;
};
# scope "/service=A/instance=??/role=??/machine=jon"
# perMachine.exports.foo = 42;
# scope "/service=A/instance=??/role=??/machine=??"
# exports."///".foo = 10;
};
####### Service module "A"
modules.service-B =
{ exports, ... }:
{
# config.exports
manifest.name = "B";
roles.default = {
# TODO: Remove automapping
# Currently exports are automapped
# scopes "/service=A/instance=hello/role=default/machine=jon"
# perInstance.exports.foo = 7;
# New style:
# Explizit scope
# perInstance.exports."service=A/instance=hello/role=default/machine=jon".foo = 7;
perInstance =
{ instanceName, machine, ... }:
{
# TODO: Test non-existing scope
# define export depending on A
exports."B/${instanceName}/default/${machine.name}".foo = exports."///".foo + exports."A/A/default/${machine.name}".foo;
# exports."B/B/default/jon".foo = exports."A/A/default/jon".foo;
# default behavior
# exports = scope.mkExports { foo = 7; };
# We want to export things for different scopes from this scope;
# If this scope is used.
#
# Explicit scope; different from the function scope above
# exports = clanLib.scopedExport {
# # Different role export
# role = "peer";
# serviceName = config.manifest.name;
# inherit instanceName machineName;
# } { foo = 7; };
};
};
perMachine =
{ ... }:
{
# exports = scope.mkExports { foo = 7; };
# exports."A///${machine.name}".foo = 42;
# exports."B///".foo = 42;
};
# scope "/service=A/instance=??/role=??/machine=jon"
# perMachine.exports.foo = 42;
# scope "/service=A/instance=??/role=??/machine=??"
exports."///".foo = 10;
};
#######
inventory = {
instances.A = {
module.name = "service-A";
module.input = "self";
roles.default.tags = [ "all" ];
};
instances.B = {
module.name = "service-B";
module.input = "self";
roles.default.tags = [ "all" ];
};
};
# <- inventory
#
# -> exports
/**
Current state
{
instances = {
hello = { networking = null; };
};
machines = {
jon = { networking = null; };
};
}
*/
/**
Target state: (Flat attribute set)
tdlr;
# roles / instance level definitions may not exist on their own
# role and instance names are completely arbitrary.
# For example what does it mean: this is a export for all "peer" roles of all service-instances? That would be magic on the roleName.
# Or exports for all instances with name "ifoo" ? That would be magic on the instanceName.
# Practical combinations
# always include either the service name or the machine name
exports = {
# Clan level (1)
"///" networks generators
# Service anchored (8) : min 1 instance is needed ; machines may not exist
"A///" <- service specific
"A/instance//" <- instance of a service
"A//peer/" <- role of a service
"A/instance/peer/" <- instance+role of a service
"A///machine" <- machine of a service
"A/instance//machine" <- machine + instance of a service
"A//role/machine" <- machine + role of a service
"A/instance/role/machine" <- machine + role + instance of a service
# Machine anchored (1 or 2)
"///jon" <- this machine
"A///jon" <- role on a machine (dupped with service anchored)
# Unpractical; probably not needed (5)
"//peer/jon" <- role on a machine
"/instance//jon" <- role on a machine
"/instance//" <- instance: All "foo" instances everywhere?
"//role/" <- role: All "peer" roles everywhere?
"/instance/role/" <- instance role: Applies to all services, whose instance name has "ifoo" and role is "peer" (double magic)
# TODO: lazyattrs poc
}
*/
};
in
{
inherit eval;
expr = eval;
expected = 42;
};
}

View File

@@ -1,21 +1,4 @@
{ lib, ... }: { lib, ... }:
let
inherit (lib)
mapAttrs
attrNames
showOption
setDefaultModuleLocation
mkOptionType
isAttrs
filterAttrs
intersectAttrs
mapAttrsToList
mkOptionDefault
zipAttrsWith
seq
fix
;
in
{ {
/** /**
A custom type for deferred modules that guarantee to be JSON serializable. A custom type for deferred modules that guarantee to be JSON serializable.
@@ -29,7 +12,7 @@ in
- Enforces that the definition is JSON serializable - Enforces that the definition is JSON serializable
- Disallows nested imports - Disallows nested imports
*/ */
uniqueDeferredSerializableModule = fix ( uniqueDeferredSerializableModule = lib.fix (
self: self:
let let
checkDef = checkDef =
@@ -40,18 +23,19 @@ in
def; def;
in in
# Essentially the "raw" type, but with a custom name and check # Essentially the "raw" type, but with a custom name and check
mkOptionType { lib.mkOptionType {
name = "deferredModule"; name = "deferredModule";
description = "deferred custom module. Must be JSON serializable."; description = "deferred custom module. Must be JSON serializable.";
descriptionClass = "noun"; descriptionClass = "noun";
# Unfortunately, tryEval doesn't catch JSON errors # Unfortunately, tryEval doesn't catch JSON errors
check = value: seq (builtins.toJSON value) (isAttrs value); check = value: lib.seq (builtins.toJSON value) (lib.isAttrs value);
merge = lib.options.mergeUniqueOption { merge = lib.options.mergeUniqueOption {
message = "------"; message = "------";
merge = loc: defs: { merge = loc: defs: {
imports = map ( imports = map (
def: def:
seq (checkDef loc def) setDefaultModuleLocation "${def.file}, via option ${showOption loc}" lib.seq (checkDef loc def) lib.setDefaultModuleLocation
"${def.file}, via option ${lib.showOption loc}"
def.value def.value
) defs; ) defs;
}; };
@@ -64,113 +48,4 @@ in
}; };
} }
); );
/**
New submodule type that allows merging at the attribute level.
:::note
'record' type adopted from https://github.com/NixOS/nixpkgs/pull/334680
:::
It applies additional constraints to immediate child options:
- No support for 'readOnly'
- No support for 'apply'
- No support for type-merging: That means the modules options must be pre-declared directly.
*/
record =
{
optional ? { },
required ? { },
wildcardType ? null,
}:
mkOptionType {
name = "record";
description =
if wildcardType == null then "record" else "open record of ${wildcardType.description}";
descriptionClass = if wildcardType == null then "noun" else "composite";
check = isAttrs;
merge.v2 =
{ loc, defs }:
let
pushPositions = map (
def:
mapAttrs (_n: v: {
inherit (def) file;
value = v;
}) def.value
);
# Checks
intersection = intersectAttrs optional required;
optionalDefault = filterAttrs (_: opt: opt ? default) optional;
# Definitions + option defaults
allDefs =
defs
++ (mapAttrsToList (name: opt: {
file = (builtins.unsafeGetAttrPos name required).file or "<unknown-file>";
value = {
${name} = mkOptionDefault opt.default;
};
}) (filterAttrs (_n: opt: opt ? default) required));
merged = zipAttrsWith (
name: defs:
let
elemType = optional.${name}.type or required.${name}.type or wildcardType;
in
lib.modules.mergeDefinitions (loc ++ [ name ]) elemType defs
) (pushPositions allDefs);
in
{
headError =
if intersection != { } then
{
message = "The following attributes of '${showOption loc}' are both declared in 'optional' and in 'required': ${lib.concatStringsSep ", " (attrNames intersection)}";
}
else if optionalDefault != { } then
{
message = "The following attributes of '${showOption loc}' are declared in 'optional' cannot have a default value: ${lib.concatStringsSep ", " (attrNames optionalDefault)}";
}
else
null;
# TODO: expose fields, fieldValues and extraValues
valueMeta = {
attrs = mapAttrs (_n: v: v.checkedAndMerged.valueMeta) merged;
};
value = mapAttrs (
name: v:
let
elemType = optional.${name}.type or required.${name}.type or wildcardType;
in
if required ? ${name} then
# Non-optional, lazy ?
v.mergedValue
else
# Optional, lazy
v.optionalValue.value or elemType.emptyValue.value or v.mergedValue
) merged;
};
nestedTypes = lib.optionalAttrs (wildcardType != null) {
inherit wildcardType;
};
getSubOptions =
prefix:
# Since this type doesn't support type merging, we can safely use the original attrs to display documentation.
mapAttrs (
name: opt:
(
opt
// {
loc = prefix ++ [ name ];
inherit name;
declarations = [
(builtins.unsafeGetAttrPos name optional).file or (builtins.unsafeGetAttrPos name required).file
or "<unknown-file>"
];
}
)
) (optional // required);
};
} }

View File

@@ -1,44 +0,0 @@
{ lib, clanLib, ... }:
let
inherit (lib) evalModules mkOption;
inherit (clanLib.types) record;
in
{
test_simple =
let
eval = evalModules {
modules = [
{
options.foo = mkOption {
type = record { };
default = { };
};
}
];
};
in
{
inherit eval;
expr = eval.config.foo;
expected = { };
};
test_wildcard =
let
eval = evalModules {
modules = [
{
options.foo = mkOption {
type = record { };
default = { };
};
}
];
};
in
{
inherit eval;
expr = eval.config.foo;
expected = { };
};
}

View File

@@ -1,5 +1,92 @@
{ lib, clanLib, ... }: { lib, clanLib, ... }:
let
evalSettingsModule =
m:
lib.evalModules {
modules = [
{
options.foo = lib.mkOption {
type = clanLib.types.uniqueDeferredSerializableModule;
};
}
m
];
};
in
{ {
unique = import ./unique_tests.nix { inherit lib clanLib; }; test_simple =
record = import ./record_tests.nix { inherit lib clanLib; }; let
eval = evalSettingsModule {
foo = { };
};
in
{
inherit eval;
expr = eval.config.foo;
expected = {
# Foo has imports
# This can only ever be one module due to the type of foo
imports = [
{
# This is the result of 'setDefaultModuleLocation'
# Which also returns exactly one module
_file = "<unknown-file>, via option foo";
imports = [
{ }
];
}
];
};
};
test_no_nested_imports =
let
eval = evalSettingsModule {
foo = {
imports = [ ];
};
};
in
{
inherit eval;
expr = eval.config.foo;
expectedError = {
type = "ThrownError";
message = "*nested imports";
};
};
test_no_function_modules =
let
eval = evalSettingsModule {
foo =
{ ... }:
{
};
};
in
{
inherit eval;
expr = eval.config.foo;
expectedError = {
type = "TypeError";
message = "cannot convert a function to JSON";
};
};
test_non_attrs_module =
let
eval = evalSettingsModule {
foo = "foo.nix";
};
in
{
inherit eval;
expr = eval.config.foo;
expectedError = {
type = "ThrownError";
message = ".*foo.* is not of type";
};
};
} }

View File

@@ -1,92 +0,0 @@
{ lib, clanLib, ... }:
let
evalSettingsModule =
m:
lib.evalModules {
modules = [
{
options.foo = lib.mkOption {
type = clanLib.types.uniqueDeferredSerializableModule;
};
}
m
];
};
in
{
test_not_defined =
let
eval = evalSettingsModule {
foo = { };
};
in
{
inherit eval;
expr = eval.config.foo;
expected = {
# Foo has imports
# This can only ever be one module due to the type of foo
imports = [
{
# This is the result of 'setDefaultModuleLocation'
# Which also returns exactly one module
_file = "<unknown-file>, via option foo";
imports = [
{ }
];
}
];
};
};
test_no_nested_imports =
let
eval = evalSettingsModule {
foo = {
imports = [ ];
};
};
in
{
inherit eval;
expr = eval.config.foo;
expectedError = {
type = "ThrownError";
message = "*nested imports";
};
};
test_no_function_modules =
let
eval = evalSettingsModule {
foo =
{ ... }:
{
};
};
in
{
inherit eval;
expr = eval.config.foo;
expectedError = {
type = "TypeError";
message = "cannot convert a function to JSON";
};
};
test_non_attrs_module =
let
eval = evalSettingsModule {
foo = "foo.nix";
};
in
{
inherit eval;
expr = eval.config.foo;
expectedError = {
type = "ThrownError";
message = ".*foo.* is not of type";
};
};
}

View File

@@ -19,7 +19,6 @@
imports = [ imports = [
./top-level-interface.nix ./top-level-interface.nix
./module.nix ./module.nix
./distributed-services.nix
./checks.nix ./checks.nix
]; ];
} }

View File

@@ -1,163 +0,0 @@
{
lib,
clanLib,
config,
clan-core,
...
}:
let
inherit (lib) mkOption types;
# Keep a reference to top-level
clanConfig = config;
inventory = clanConfig.inventory;
flakeInputs = clanConfig.self.inputs;
clanCoreModules = clan-core.clan.modules;
grouped = lib.foldlAttrs (
acc: instanceName: instance:
let
inputName = if instance.module.input == null then "<clan-core>" else instance.module.input;
id = inputName + "-" + instance.module.name;
in
acc
// {
${id} = acc.${id} or [ ] ++ [
{
inherit instanceName instance;
}
];
}
) { } importedModuleWithInstances;
importedModuleWithInstances = lib.mapAttrs (
instanceName: instance:
let
resolvedModule = clanLib.resolveModule {
moduleSpec = instance.module;
inherit flakeInputs clanCoreModules;
};
# Every instance includes machines via roles
# :: { client :: ... }
instanceRoles = lib.mapAttrs (
roleName: role:
let
resolvedMachines = clanLib.inventory.resolveTags {
members = {
# Explicit members
machines = lib.attrNames role.machines;
# Resolved Members
tags = lib.attrNames role.tags;
};
inherit (inventory) machines;
inherit instanceName roleName;
};
in
# instances.<instanceName>.roles.<roleName> =
# Remove "tags", they are resolved into "machines"
(removeAttrs role [ "tags" ])
// {
machines = lib.genAttrs resolvedMachines.machines (
machineName:
let
machineSettings = instance.roles.${roleName}.machines.${machineName}.settings or { };
in
# TODO: tag settings
# Wait for this feature until option introspection for 'settings' is done.
# This might get too complex to handle otherwise.
# settingsViaTags = lib.filterAttrs (
# tagName: _: machineHasTag machineName tagName
# ) instance.roles.${roleName}.tags;
{
# TODO: Do we want to wrap settings with
# setDefaultModuleLocation "inventory.instances.${instanceName}.roles.${roleName}.tags.${tagName}";
settings = {
imports = [
machineSettings
]; # ++ lib.attrValues (lib.mapAttrs (_tagName: v: v.settings) settingsViaTags);
};
}
);
}
) instance.roles;
in
{
inherit (instance) module;
inherit resolvedModule instanceRoles;
}
) inventory.instances or { };
in
{
_class = "clan";
options._services = mkOption {
visible = false;
description = ''
All service instances
!!! Danger "Internal API"
Do not rely on this API yet.
- Will be renamed to just 'services' in the future.
Once the name can be claimed again.
- Structure will change.
API will be declared as public after beeing simplified.
'';
type = types.submoduleWith {
# TODO: Remove specialArgs
specialArgs = {
inherit clanLib;
};
modules = [
(import ../../lib/inventory/distributed-service/all-services-wrapper.nix {
inherit (clanConfig) directory exports;
})
# Dependencies
{
# exportsModule = clanConfig.exportsModule;
}
{
# TODO: Rename to "allServices"
# All services
mappedServices = lib.mapAttrs (_module_ident: instances: {
imports = [
# Import the resolved module.
# i.e. clan.modules.admin
{
options.module = lib.mkOption {
type = lib.types.raw;
default = (builtins.head instances).instance.module;
};
}
(builtins.head instances).instance.resolvedModule
] # Include all the instances that correlate to the resolved module
++ (builtins.map (v: {
instances.${v.instanceName}.roles = v.instance.instanceRoles;
}) instances);
}) grouped;
}
];
};
default = { };
};
options._allMachines = mkOption {
internal = true;
type = types.raw;
default = lib.mapAttrs (machineName: _: {
# This is the list of nixosModules for each machine
machineImports = lib.foldlAttrs (
acc: _module_ident: serviceModule:
acc ++ [ serviceModule.result.final.${machineName}.nixosModule or { } ]
) [ ] config._services.mappedServices;
}) inventory.machines or { };
};
config = {
clanInternals.inventoryClass.machines = config._allMachines;
# clanInternals.inventoryClass.distributedServices = config._services;
# Exports from distributed services
exports = config._services.exports;
};
}

View File

@@ -3,16 +3,12 @@
lib, lib,
clanModule, clanModule,
clanLib, clanLib,
clan-core,
}: }:
let let
eval = lib.evalModules { eval = lib.evalModules {
modules = [ modules = [
clanModule clanModule
]; ];
specialArgs = {
self = clan-core;
};
}; };
evalDocs = pkgs.nixosOptionsDoc { evalDocs = pkgs.nixosOptionsDoc {

View File

@@ -12,7 +12,6 @@ in
}: }:
let let
jsonDocs = import ./eval-docs.nix { jsonDocs = import ./eval-docs.nix {
clan-core = self;
inherit inherit
pkgs pkgs
lib lib

View File

@@ -219,6 +219,8 @@ in
inherit nixosConfigurations; inherit nixosConfigurations;
inherit darwinConfigurations; inherit darwinConfigurations;
exports = config.clanInternals.inventoryClass.distributedServices.servicesEval.config.exports;
clanInternals = { clanInternals = {
inventoryClass = inventoryClass =
let let
@@ -252,9 +254,21 @@ in
exportsModule = config.exportsModule; exportsModule = config.exportsModule;
} }
( (
{ ... }: { config, ... }:
{ {
staticModules = clan-core.clan.modules; staticModules = clan-core.clan.modules;
distributedServices = clanLib.inventory.mapInstances {
inherit (config)
inventory
directory
flakeInputs
exportsModule
;
clanCoreModules = clan-core.clan.modules;
prefix = [ "distributedServices" ];
};
machines = config.distributedServices.allMachines;
} }
) )
]; ];

View File

@@ -110,7 +110,9 @@ in
# TODO: make this writable by moving the options from inventoryClass into clan. # TODO: make this writable by moving the options from inventoryClass into clan.
exports = lib.mkOption { exports = lib.mkOption {
type = types.lazyAttrsOf (types.submoduleWith { modules = [ config.exportsModule ]; }); readOnly = true;
visible = false;
internal = true;
}; };
exportsModule = lib.mkOption { exportsModule = lib.mkOption {
@@ -118,86 +120,84 @@ in
visible = false; visible = false;
type = types.deferredModule; type = types.deferredModule;
default = { default = {
options.networking = { options.networking = lib.mkOption {
default = null;
priority = lib.mkOption { type = lib.types.nullOr (
type = lib.types.int; lib.types.submodule {
default = 1000; options = {
description = '' priority = lib.mkOption {
priority with which this network should be tried. type = lib.types.int;
higher priority means it gets used earlier in the chain default = 1000;
''; description = ''
}; priority with which this network should be tried.
module = lib.mkOption { higher priority means it gets used earlier in the chain
# type = lib.types.enum [ '';
# "clan_lib.network.direct" };
# "clan_lib.network.tor" module = lib.mkOption {
# ]; # type = lib.types.enum [
type = lib.types.str; # "clan_lib.network.direct"
default = "clan_lib.network.direct"; # "clan_lib.network.tor"
description = '' # ];
the technology this network uses to connect to the target type = lib.types.str;
This is used for userspace networking with socks proxies. default = "clan_lib.network.direct";
''; description = ''
}; the technology this network uses to connect to the target
# should we call this machines? hosts? This is used for userspace networking with socks proxies.
'';
hosts = lib.mkOption { };
type = lib.types.listOf lib.types.str; # should we call this machines? hosts?
default = [ ]; peers = lib.mkOption {
}; # <name>
type = lib.types.attrsOf (
# peers = lib.mkOption { lib.types.submodule (
# { name, ... }:
# # <name> {
# type = lib.types.attrsOf ( options = {
# lib.types.submodule ( name = lib.mkOption {
# { name, ... }: type = lib.types.str;
# { default = name;
# options = { };
# name = lib.mkOption { SSHOptions = lib.mkOption {
# type = lib.types.str; type = lib.types.listOf lib.types.str;
# default = name; default = [ ];
# }; };
# SSHOptions = lib.mkOption { host = lib.mkOption {
# type = lib.types.listOf lib.types.str; description = '''';
# default = [ ]; type = lib.types.attrTag {
# }; plain = lib.mkOption {
# type = lib.types.str;
# host = lib.mkOption { description = ''
# description = ''''; a plain value, which can be read directly from the config
# type = lib.types.attrTag { '';
# plain = lib.mkOption { };
# type = lib.types.str; var = lib.mkOption {
# description = '' type = lib.types.submodule {
# a plain value, which can be read directly from the config options = {
# ''; machine = lib.mkOption {
# }; type = lib.types.str;
# var = lib.mkOption { example = "jon";
# type = lib.types.submodule { };
# options = { generator = lib.mkOption {
# machine = lib.mkOption { type = lib.types.str;
# type = lib.types.str; example = "tor-ssh";
# example = "jon"; };
# }; file = lib.mkOption {
# generator = lib.mkOption { type = lib.types.str;
# type = lib.types.str; example = "hostname";
# example = "tor-ssh"; };
# }; };
# file = lib.mkOption { };
# type = lib.types.str; };
# example = "hostname"; };
# }; };
# }; };
# }; }
# }; )
# }; );
# }; };
# }; };
# } }
# ) );
# );
# };
}; };
}; };
description = '' description = ''

View File

@@ -67,6 +67,9 @@ in
type = types.raw; type = types.raw;
}; };
distributedServices = mkOption {
type = types.raw;
};
inventory = mkOption { inventory = mkOption {
type = types.raw; type = types.raw;
}; };

View File

@@ -225,7 +225,9 @@ def generate_facts(
raise ClanError(msg) raise ClanError(msg)
if not was_regenerated and len(machines) > 0: if not was_regenerated and len(machines) > 0:
log.info("All secrets and facts are already up to date") pass
# Remove message until facts has been propertly deleted
# log.info("All secrets and facts are already up to date")
return was_regenerated return was_regenerated

View File

@@ -64,9 +64,6 @@
''; '';
in in
{ {
legacyPackages = {
inherit jsonDocs clanModulesViaService;
};
packages = { packages = {
inherit module-docs; inherit module-docs;
}; };

View File

@@ -11,10 +11,151 @@
... ...
}: }:
let let
inherit (lib)
mapAttrsToList
mapAttrs
mkOption
types
splitString
stringLength
substring
;
inherit (self) clanLib;
serviceModules = self.clan.modules;
baseHref = "/option-search/"; baseHref = "/option-search/";
getRoles =
module:
(clanLib.evalService {
modules = [ module ];
prefix = [ ];
}).config.roles;
getManifest =
module:
(clanLib.evalService {
modules = [ module ];
prefix = [ ];
}).config.manifest;
settingsModules = module: mapAttrs (_roleName: roleConfig: roleConfig.interface) (getRoles module);
# Map each letter to its capitalized version # Map each letter to its capitalized version
capitalizeChar =
char:
{
a = "A";
b = "B";
c = "C";
d = "D";
e = "E";
f = "F";
g = "G";
h = "H";
i = "I";
j = "J";
k = "K";
l = "L";
m = "M";
n = "N";
o = "O";
p = "P";
q = "Q";
r = "R";
s = "S";
t = "T";
u = "U";
v = "V";
w = "W";
x = "X";
y = "Y";
z = "Z";
}
.${char};
title =
name:
let
# split by -
parts = splitString "-" name;
# capitalize first letter of each part
capitalize = part: (capitalizeChar (substring 0 1 part)) + substring 1 (stringLength part) part;
capitalizedParts = map capitalize parts;
in
builtins.concatStringsSep " " capitalizedParts;
fakeInstanceOptions =
name: module:
let
manifest = getManifest module;
description = ''
# ${title name} (Clan Service)
**${manifest.description}**
${lib.optionalString (manifest ? readme) manifest.readme}
${
if manifest.categories != [ ] then
"Categories: " + builtins.concatStringsSep ", " manifest.categories
else
"No categories defined"
}
'';
in
{
options = {
instances.${name} = lib.mkOption {
inherit description;
type = types.submodule {
options.roles = mapAttrs (
roleName: roleSettingsModule:
mkOption {
type = types.submodule {
_file = "docs flake-module";
imports = [
{ _module.args = { inherit clanLib; }; }
(import ../../modules/inventoryClass/role.nix {
nestedSettingsOption = mkOption {
type = types.raw;
description = ''
See [instances.${name}.roles.${roleName}.settings](${baseHref}?option_scope=0&option=inventory.instances.${name}.roles.${roleName}.settings)
'';
};
settingsOption = mkOption {
type = types.submoduleWith {
modules = [ roleSettingsModule ];
};
};
})
];
};
}
) (settingsModules module);
};
};
};
};
docModules = [
{
inherit self;
}
self.modules.clan.default
{
options.inventory = lib.mkOption {
type = types.submoduleWith {
modules = [
{ noInstanceOptions = true; }
]
++ mapAttrsToList fakeInstanceOptions serviceModules;
};
};
}
];
baseModule = baseModule =
# Module # Module
@@ -67,6 +208,12 @@
title = "Clan Options"; title = "Clan Options";
# scopes = mapAttrsToList mkScope serviceModules; # scopes = mapAttrsToList mkScope serviceModules;
scopes = [ scopes = [
{
inherit baseHref;
name = "Flake Options (clan.nix file)";
modules = docModules;
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
}
{ {
name = "Machine Options (clan.core NixOS options)"; name = "Machine Options (clan.core NixOS options)";
optionsJSON = "${coreOptions}/share/doc/nixos/options.json"; optionsJSON = "${coreOptions}/share/doc/nixos/options.json";