Compare commits

..

1 Commits

Author SHA1 Message Date
Johannes Kirschbauer
63aceeeb4e WIP exports: draft endpoints for services along with a 'firwewall' consumer 2025-10-21 16:25:21 +02:00
242 changed files with 3905 additions and 10479 deletions

View File

@@ -1,10 +1,8 @@
clanServices/.* @pinpox @kenji clanServices/.* @pinpox @kenji
lib/test/container-test-driver/.* @DavHau @mic92 lib/test/container-test-driver/.* @DavHau @mic92
lib/inventory/.* @hsjobeki lib/modules/inventory/.* @hsjobeki
lib/inventoryClass/.* @hsjobeki lib/modules/inventoryClass/.* @hsjobeki
modules/.* @hsjobeki
pkgs/clan-app/ui/.* @hsjobeki @brianmcgee pkgs/clan-app/ui/.* @hsjobeki @brianmcgee
pkgs/clan-app/clan_app/.* @qubasa @hsjobeki pkgs/clan-app/clan_app/.* @qubasa @hsjobeki

View File

@@ -87,7 +87,6 @@ in
# Container Tests # Container Tests
nixos-test-container = self.clanLib.test.containerTest ./container nixosTestArgs; nixos-test-container = self.clanLib.test.containerTest ./container nixosTestArgs;
nixos-systemd-abstraction = self.clanLib.test.containerTest ./systemd-abstraction nixosTestArgs; nixos-systemd-abstraction = self.clanLib.test.containerTest ./systemd-abstraction nixosTestArgs;
nixos-llm-test = self.clanLib.test.containerTest ./llm nixosTestArgs;
nixos-test-user-firewall-iptables = self.clanLib.test.containerTest ./user-firewall/iptables.nix nixosTestArgs; nixos-test-user-firewall-iptables = self.clanLib.test.containerTest ./user-firewall/iptables.nix nixosTestArgs;
nixos-test-user-firewall-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs; nixos-test-user-firewall-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs;
nixos-test-extra-python-packages = self.clanLib.test.containerTest ./test-extra-python-packages nixosTestArgs; nixos-test-extra-python-packages = self.clanLib.test.containerTest ./test-extra-python-packages nixosTestArgs;

View File

@@ -1,82 +0,0 @@
{ self, pkgs, ... }:
let
cli = self.packages.${pkgs.hostPlatform.system}.clan-cli-full;
ollama-model = pkgs.callPackage ./qwen3-4b-instruct.nix { };
in
{
name = "llm";
nodes = {
peer1 =
{ pkgs, ... }:
{
users.users.text-user = {
isNormalUser = true;
linger = true;
uid = 1000;
extraGroups = [ "systemd-journal" ];
};
# Set environment variables for user systemd
environment.extraInit = ''
if [ "$(id -u)" = "1000" ]; then
export XDG_RUNTIME_DIR="/run/user/1000"
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
ollama_dir="$HOME/.ollama"
mkdir -p "$ollama_dir"
ln -sf ${ollama-model}/models "$ollama_dir"/models
fi
'';
# Enable PAM for user systemd sessions
security.pam.services.systemd-user = {
startSession = true;
# Workaround for containers - use pam_permit to avoid helper binary issues
text = pkgs.lib.mkForce ''
account required pam_permit.so
session required pam_permit.so
session required pam_env.so conffile=/etc/pam/environment readenv=0
session required ${pkgs.systemd}/lib/security/pam_systemd.so
'';
};
environment.systemPackages = [
cli
pkgs.ollama
(cli.pythonRuntime.withPackages (
ps: with ps; [
pytest
pytest-xdist
(cli.pythonRuntime.pkgs.toPythonModule cli)
self.legacyPackages.${pkgs.hostPlatform.system}.nixosTestLib
]
))
];
};
};
testScript =
{ ... }:
''
start_all()
peer1.wait_for_unit("multi-user.target")
peer1.wait_for_unit("user@1000.service")
# Fix user journal permissions so text-user can read their own logs
peer1.succeed("chown text-user:systemd-journal /var/log/journal/*/user-1000.journal*")
peer1.succeed("chmod 640 /var/log/journal/*/user-1000.journal*")
# the -o adopts="" is needed to overwrite any args coming from pyproject.toml
# -p no:cacheprovider disables pytest's cacheprovider which tries to write to the nix store in this case
cmd = "su - text-user -c 'pytest -s -n0 -m service_runner -p no:cacheprovider -o addopts="" ${cli.passthru.sourceWithTests}/clan_lib/llm'"
print("Running tests with command: " + cmd)
# Run tests as text-user (environment variables are set automatically)
peer1.succeed(cmd)
'';
}

View File

@@ -1,70 +0,0 @@
{ pkgs }:
let
# Got them from https://github.com/Gholamrezadar/ollama-direct-downloader
# Download manifest
manifest = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/manifests/4b-instruct";
# You'll need to calculate this hash - run the derivation once and it will tell you the correct hash
hash = "sha256-Dtze80WT6sGqK+nH0GxDLc+BlFrcpeyi8nZiwY8Wi6A=";
};
# Download blobs
blob1 = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:b72accf9724e93698c57cbd3b1af2d3341b3d05ec2089d86d273d97964853cd2";
hash = "sha256-tyrM+XJOk2mMV8vTsa8tM0Gz0F7CCJ2G0nPZeWSFPNI=";
};
blob2 = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:85e4a5b7b8ef0e48af0e8658f5aaab9c2324c76c1641493f4d1e25fce54b18b9";
hash = "sha256-heSlt7jvDkivDoZY9aqrnCMkx2wWQUk/TR4l/OVLGLk=";
};
blob3 = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:eade0a07cac7712787bbce23d12f9306adb4781d873d1df6e16f7840fa37afec";
hash = "sha256-6t4KB8rHcSeHu84j0S+TBq20eB2HPR324W94QPo3r+w=";
};
blob4 = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:d18a5cc71b84bc4af394a31116bd3932b42241de70c77d2b76d69a314ec8aa12";
hash = "sha256-0YpcxxuEvErzlKMRFr05MrQiQd5wx30rdtaaMU7IqhI=";
};
blob5 = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:0914c7781e001948488d937994217538375b4fd8c1466c5e7a625221abd3ea7a";
hash = "sha256-CRTHeB4AGUhIjZN5lCF1ODdbT9jBRmxeemJSIavT6no=";
};
in
pkgs.stdenv.mkDerivation {
pname = "ollama-qwen3-4b-instruct";
version = "1.0";
dontUnpack = true;
buildPhase = ''
mkdir -p $out/models/manifests/registry.ollama.ai/library/qwen3
mkdir -p $out/models/blobs
# Copy manifest
cp ${manifest} $out/models/manifests/registry.ollama.ai/library/qwen3/4b-instruct
# Copy blobs with correct names
cp ${blob1} $out/models/blobs/sha256-b72accf9724e93698c57cbd3b1af2d3341b3d05ec2089d86d273d97964853cd2
cp ${blob2} $out/models/blobs/sha256-85e4a5b7b8ef0e48af0e8658f5aaab9c2324c76c1641493f4d1e25fce54b18b9
cp ${blob3} $out/models/blobs/sha256-eade0a07cac7712787bbce23d12f9306adb4781d873d1df6e16f7840fa37afec
cp ${blob4} $out/models/blobs/sha256-d18a5cc71b84bc4af394a31116bd3932b42241de70c77d2b76d69a314ec8aa12
cp ${blob5} $out/models/blobs/sha256-0914c7781e001948488d937994217538375b4fd8c1466c5e7a625221abd3ea7a
'';
installPhase = ''
# buildPhase already created everything in $out
:
'';
meta = with pkgs.lib; {
description = "Qwen3 4B Instruct model for Ollama";
license = "apache-2.0";
platforms = platforms.all;
};
}

View File

@@ -62,6 +62,6 @@ in
peer1.succeed("chmod 640 /var/log/journal/*/user-1000.journal*") peer1.succeed("chmod 640 /var/log/journal/*/user-1000.journal*")
# Run tests as text-user (environment variables are set automatically) # Run tests as text-user (environment variables are set automatically)
peer1.succeed("su - text-user -c 'pytest -p no:cacheprovider -o addopts="" -s -n0 ${cli.passthru.sourceWithTests}/clan_lib/service_runner'") peer1.succeed("su - text-user -c 'pytest -s -n0 ${cli}/${cli.pythonRuntime.sitePackages}/clan_lib/service_runner'")
''; '';
} }

View File

@@ -1,7 +1,4 @@
{ { ... }:
clanLib,
...
}:
let let
sharedInterface = sharedInterface =
{ lib, ... }: { lib, ... }:
@@ -54,15 +51,15 @@ let
builtins.foldl' ( builtins.foldl' (
urls: name: urls: name:
let let
ip = clanLib.vars.getPublicValue { ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value";
flake = config.clan.core.settings.directory;
machine = name;
generator = "zerotier";
file = "zerotier-ip";
default = null;
};
in in
if ip != null then urls ++ [ "[${ip}]:${builtins.toString settings.network.port}" ] else urls if builtins.pathExists ipPath then
let
ip = builtins.readFile ipPath;
in
urls ++ [ "[${ip}]:${builtins.toString settings.network.port}" ]
else
urls
) [ ] (builtins.attrNames ((roles.admin.machines or { }) // (roles.signer.machines or { }))) ) [ ] (builtins.attrNames ((roles.admin.machines or { }) // (roles.signer.machines or { })))
); );
@@ -159,14 +156,9 @@ in
readHostKey = readHostKey =
machine: machine:
let let
publicKey = clanLib.vars.getPublicValue { path = "${config.clan.core.settings.directory}/vars/per-machine/${machine}/data-mesher-host-key/public_key/value";
flake = config.clan.core.settings.directory;
inherit machine;
generator = "data-mesher-host-key";
file = "public_key";
};
in in
builtins.elemAt (lib.splitString "\n" publicKey) 1; builtins.elemAt (lib.splitString "\n" (builtins.readFile path)) 1;
in in
{ {
enable = true; enable = true;

View File

@@ -0,0 +1,34 @@
{ clanLib, inventory, ... }:
{
manifest.name = "clan-core/firewall";
manifest.description = "Configures firewall rules based on exported endpoints from other services";
roles.default.description = "Configures firewall rules based on exported endpoints from other services";
perMachine =
# firewall instances
{
exports,
machine,
lib,
...
}:
let
instances = clanLib.resolveInstances machine inventory;
instancesTcpPorts = builtins.concatLists (
map (
instanceName:
lib.mapAttrsToList (_endpointName: cfg: cfg.port) exports.instances.${instanceName}.endpoints
) instances
);
machineTcpPorts = lib.mapAttrsToList (
_endpointName: cfg: cfg.port
) exports.instances.${machine.name}.endpoints;
allowedPorts = instancesTcpPorts ++ machineTcpPorts;
in
{
nixosModule.networking.firewall.allowedTCPPorts = allowedPorts;
};
}

View File

@@ -45,6 +45,12 @@
... ...
}: }:
{ {
exports.endpoints.greeting = {
port = 80;
};
exports.endpoints.uptime = {
port = 80;
};
# Analog to 'perSystem' of flake-parts. # Analog to 'perSystem' of flake-parts.
# For every instance of this service we will add a nixosModule to a morning-machine # For every instance of this service we will add a nixosModule to a morning-machine
nixosModule = nixosModule =
@@ -89,6 +95,9 @@
perMachine = perMachine =
{ machine, ... }: { machine, ... }:
{ {
exports.endpoints.core-online = {
port = 8080;
};
nixosModule = nixosModule =
{ pkgs, ... }: { pkgs, ... }:
{ {

View File

@@ -16,7 +16,6 @@
options = { options = {
host = lib.mkOption { host = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "";
description = '' description = ''
ip address or hostname (domain) of the machine ip address or hostname (domain) of the machine
''; '';
@@ -32,15 +31,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

@@ -44,10 +44,8 @@
pkgs.openssl pkgs.openssl
]; ];
# TODO: Implement automated certificate rotation instead of using a 100-year expiration
script = '' script = ''
openssl req -x509 -nodes -newkey rsa:4096 \ openssl req -x509 -nodes -newkey rsa:4096 \
-days 36500 \
-keyout "$out"/key \ -keyout "$out"/key \
-out "$out"/crt \ -out "$out"/crt \
-subj "/C=US/ST=CA/L=San Francisco/O=Example Corp/OU=IT/CN=example.com" -subj "/C=US/ST=CA/L=San Francisco/O=Example Corp/OU=IT/CN=example.com"

View File

@@ -1,33 +1,33 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
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 k1PNWTkgLsXO8DNkCudxcvPElXfmaw6zwaLrZys=
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

@@ -1,18 +1,19 @@
{ {
"data": "ENC[AES256_GCM,data:Ho1AvJoI17OVQY/Usmjn4yDLFVVGI6wJLr/e8/GZXnYqnY5/oSQEwN+91nuF2MOa4qu9WjO6HCu9jMDVZdTnbXTGFM56rU17TOdn6z7RSB3fMRq3+dbSuSKHo71SLG6vg9H85im39uuz5crzTy+uJtJaF6bC2sqfq1feZTlylhiA3TD4w1t7pny6M8/i1MF0xCcEXFc30FA3leArhnDiKrANDa2xhQydoneOUVAvCXzmPTneHLQV9L4ga5AOf0aYe4AvJO4193N5mqUm8kUc0RbMinHf5XT9umXZXQbpOHvnFEf8vMxO9uZHVxdidMEehGeIxjJnlhiAQ2FiIMmtd8VjH6Ue6ecN2b5sX93ii020XcwjFzgLRj+YxXuio02T99KaKtS3u6MqIpgD589/DpycjV7mp/V78y6l8ULCCSqrhWnlO54BbPAqHcFUezoukbwfg7oJuVCOtQDFrDvZ6HPozh/63rOFsEqRQuINcG1yGjLgyni95WaQ/fE8X0EPWjewLV64c3T4ZV+1ypkIpI/qnfjMFv2CKZmEiCCyqtTOoe/Z8LBiwRACzCPf7vHQ5zkIcvtKQrBXMdb26cYElIlLt9olsUkf6/UjZUra+w7V9plS3FSWD0SfjvLFCuLZe+rVqNkymZXpg2gbLudpkNKs2pAk/fsqnf5SYJkCUXrViOnBZozPrSCeJfUJ8O3nYeWnxkI1lHgiP1TGzjI8EIrEM/Df1qWkxWdmO8lmYivEP1uBLXpB0O74EV94xrtYKZs5QRaaQPD6mJdcdY3hjcVJDRCpGwcnGmhvTanB4pK9rDTtCJT5WjDlgFgFMnLUh69Hy+q6vbcqvGimvKuTyTgn+idM+baQwG27/aTJj9SagDjyyaqNIrTtnRTkI1EphP7mqzs8TtBryP+I3ig7VlL1O+6Qr5wd/3o8qyUusGhxG+hFEGOnECaVXdyBOzbVS0pYTgWyw80Kd3KgybR5BsTYa9rTgelXPkbe1cRPdTjkwn1oyfBcF7RcairMGVDv7+FKx0WTypASce2PUyD577PFZSQaFzn+4oYfFWh4mOOx0ilQEj2YRWzZBcAz8oHzsTT9AVmt+TYFdDgFKk1M+DNJjvASRZRB1LL+h70wH3IbmnoxlVeOIKSIvZ/sqArBglmBij8O9ZIKlKzT3fg6Xwpcjl09a7kkOtKaKNkGHYpM+h4H355P3dija/cjkHjQL5cvBpBHKIgC/lzPuC9J9t6xP/0GROQwAd7+8Gwrj1LpFsqGLIQwGmz77R1eUNTfdZ5cXH69p6fU7AsHgp37cJq/QsBu6A7AjaU7whhoDNBTHM1+DoH0ufrqmxMfkgSaw3VzuEjOZhOeuqVLM9zHGX3ol/6OPEcniKad7WcKv/81njeFvZyeMVrELbHYre9zqjSwo6lwKUSmO9nUWjcKhiRKVKWd49Ftnv2tm1LnerhvoWhmPF1vPCGSuU9ms34RRZsMGbpWI1fkAWgV7DtqmxBehck1HhXJ7zA/CwESj94a+BTYZaqE4ZvjqfXbUVnlf0ttKOxRFM0cZJUhFj/vjeE4sLm+zu79xEnSsx6ZrzCd+09RBovx5obEqOsWBVyo7VvBMCzfa+9XjYIYyoNPm9HAMCWm0xvVE5xy9gPqiXVr4kWUroPpIdxDaTfib3cFfN5d+Ks7Mmz0KtN+JembjziwqS7jCHUFjSx9QM4dHzhnxDRCCARC4fVTR3EJakk903NN1NXwNqZJbySTK5vniQwuSD9hx0KyVyXxsWTnlyJxu0zAc+rKOVQ/vELw2lxaTVnbRwhFYkHO2WmO8AVN6ScAxhMXoNXI4tBYaomlrPZakPn8kqPgXBhzJBRIcXgOj8ijM6OT4FDnky7kKotfkTtHn3/IlAJ8j8lyz1RIAW2lVlRaGlaWbdOu7ETgNpbPfMp7b7VXyRXpaohSxktWnMrZEsdB68G24Ajq1FJuDPggp8b43pAC1wgC10Be6oFWwhO8SQIXQMEg+JgIbkGy/FzFI/XX7AWq7nce4OcOivWIu+/AT4uPVx2fOEt1lcD+MEmBuZsiqi0JMzgW6eMVGCRIj3zZLGyYeq+04ZL7fYH0AvqUARFJQr6FAcEfiudUwBEdd8Z4CHG5OnswIqxUGhl4d6b/nhPwx5BzoU8AWmjFRdK4Zll04EwNioW5OswyK8ProFdteQqVsGtWsOKO41XxwcamXNA3ASfpBJVJrSwSOgSFcV4AyrK5+9+XWWRy51pm+mqGBCt/KEeVFuTSsGY6Y3J6aWfGK1Cj+0EiTqi0cqfl1ltVXvBXbKScfny6XPwcUCpTped6dkYcwB9ceuXPYW/XiQcLB0Icf4bK2wtD11S0YillD67HjSKnhKALdkIOYtaWVaple4SbbTwk125xRSl/xwDFDHZHmal+oJM2Ctw6mFIK/1RYwJ7ESK/+R2Idold9MuowtTqWnyvfPZDXQUf1xstHl0Ov13S55ovME9/GUR+8gRSnOnfKjUdBUfSrGyhBXqExSHLGXcMeWL6EM8C6gI1bzI3vAFS1yogOpLt8xCdrNY6gpNY/ZevWZNEHrfTuOUPyfk/pWZOluUSN778D4cnik68GXlJpQy96HQwFWfCjnB6gVx/v5t9cgjlNJ6R0YuH5GY9t1RW38sEMAM5SR1z9py1IBaN05MyTaF5JJHe9hbV43p4t6Exdj00lH++52rg7qBB4s1JJAMHKfnJMMKxJAGe8p2XpnAypYaARZvD1Wm2BPzISpOMwIxmRdWF1FtuM10w8dU/6YcGdBKtGVhRpA2iCw7u5S/D0hFiobpcWpW49VoAR8MhsCF87r/SiNZCR2x0DQLHXWIDP/wdCx25AxzR5zXk54241yThYi3EomOm7fXDztdX2dLWv0eBNkYWHGEEHa3sceirs7xYLU09FsZQEU/50+ljLasewwlSZuhVFe414rZKW8L0Mv7LfhOdvzK+ly33tGAFDEF5QaXaYZ+zMCkRlYnw9FF4OwnFwB8o9UgANfrfmtU1owDyJOcWmDe4Z2YYVraDzF0U9u3cSLUys4d1hvkzrNDYG19YSbf2xORQjWZ501ITwLvIfYf3J3R7+HUv7Ehz7bgKzvBwF8/R/Q+nTnMMBz+4ueF089H9skVqHi3y9BfCjMZUKaDohF0OPH5+zmEyuMwJRnHoBdnS56TicHS69ydUiE+eXg++g+LrsjCFHyl93/kwu05hwgQ7+MY/x/BBaJryBKWTPFxzGyRYe7sUOXpYyOlmQqA5+/aInPbYxmaaTp6FxMrGMz95+HAxq5KrsJX0oooE4+DPzXA+9z/Uo37oihnYdPZVLrxsgZhVGWcCPyAlPN88BVEq/eI6+jGBVzggNS7hWjBZuFrN2YFHhs6J/8JIy7VGR/DxuuubSAdv8ceJoptDu7s+07RhQNCGGjsZMwXxQBODBhBBDBRBfaQ/j1AuBnGP7nZENGXgYHDJWKnVWyBPN4oaDmNvFThbh7wWbntVis2FCNpqFEHQ8cZ//1errb5NY2s0J6MNgvKd5hQgX71UOgeii0hUZwUiHh3dwZVhsjzRVWO9P8cpNp9ZBmE/bRb9oDBjsuHYAqRsL26MAXvEG6Ws9TrUCNVe/ZrD1ppYs3YU2/yvX0QFeWUK4k+QIxh4DxPiEiQUqDoTW4th0FsYofBCdHrMjSGfVXmLiCXjm/6G33nEWf1cfe/u3hi75a0imJSEsBGWgD2gH06H5D6NIjalucIF+FKvggpPyzX27QYgBo2KDLRWoOdWJjtDJXwH7WMylnXquRl2fcsC3e5FIcVxZpphjP5scZPBrvRTfrg689BGXZOoCHx6QNzSe81je2ZrMaAkg8GHwrn5cxzMxDXXmxS6Aa0/Ij02oeIPzhEzvIA/5jpGfmZ3BTEPl9NaJwetf+OINEsgf7D1rZWn+rzU9jE8PD/0bi00sZjtSv3W9itUgptHGSx59QafPAOYCGfuYg4difn7BRUlRawEIWhj7avIoGmMmge4uFTFjxFJMHyq8vyIEbFnj+BKhFRG8dHeSgLG+KfdCoiN81H3z53mzujhZGivaBJ0/lJWaM6IrEU1nDEvbZfO9gv7pJtbSnd4dY1/rZrwEMKSEQAc2LXRrYBjd9cDF1F6n82dYxH14Fcg9Fpt451xXT8GzZoZva9E0p6CLjEFi+YrGgs+LwryXomf+nrH8NTs3Fv2U2EXylbsxRKqMyIQI15g8h9e7Tg6BGOOfu6EbsNLawGv+61/VbmTVOvuy2c8sSwBRpx9FzM1VkunSNIpoms1DuxqS2TBiI6ge1dE9sYwgaUfP0u8A77oWBtvCR4chpsqyulfdzsIN/N2Gk9pQGaV13G0ctBJubE8/aa0QuUWGys4=,iv:dGSmyDNBdVyF54bYS/Zxm2NNXZyGtLjkyYlrI9/nKvc=,tag:ip2fy76NjObWbW20HyuZUA==,type:str]", "data": "ENC[AES256_GCM,data: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,iv:q3j8EAokyyxiszf+wyRqxEr2igaD1bX7YnFx/NbsGg8=,tag:HKKYWRJEUwW2/TxL+5dSng==,type:str]",
"sops": { "sops": {
"age": [ "age": [
{ {
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck", "recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNQS96MUFubVdOR2VCc2xO\ncTc5QnNHNTFpdURnSnF3dVhBQXQ3bnBuRW1RCngzSVlhSW9rNUxoSWdKcEtKVXc3\nQitLZ2NDUXBSUmxtVWpYRUlvOHVXcW8KLS0tIGZaWlRVak9NYmt2elpwYStYenRE\nanlkT3BET1FjQ2lFZkp3SXFMSkJSaVkKKkr+MNNqs6Ve3K5OrZfBEGlnc7OAthqf\nOZrP9NYOTMgkvhFsZTVpUS0zskry0iwmTNt+KeluYf0Tko8K53Kx2A==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaeXRjU214aWk5ajl1aW9E\naGJlb1ViaVRmMTBHdkFDQUNDZS94WFZiNUNvCllmWTJBck9hR3U3V09VWDZwQ2xI\nd3ZEQnBIUG5ZSTVIdS8rQ2FMYVhyNk0KLS0tIEE1UG8rSzFyU01sVXhGVHpoaE9i\nSis4Qi9tMGFqbTNMTDZUVk1ZdXkrM28Km4VkfaOsZ69ckjvrg+os43H/O1IoWHzC\nt4LqZRz1Tk7/d1aLWavSPPjVYrCOMZeNBqGbQpGfjjuXrafClRNQdQ==\n-----END AGE ENCRYPTED FILE-----\n"
}, },
{ {
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXd2dVYmJIbUVVeXk5Nk1E\nekFiUldVVUhRTmE4dHRiTHNDdEMyS1pRV1RrCkNScGdXVSs4UU5id29DV0pZWDQr\nenV1QmpnOFk5aFpTTUxmb0hDVHZDdFkKLS0tIHpmalJtRC94bEhaUStmeUlHT21w\nd3o3UzJHZklxK0RCYUUxc2c3aG1XclkKEPq1ZgyGiAK/Hy4zT7wfdDfPEE3vMHpR\nzwQV5y3M3DmlnKQEvJu0DpQ334CyAcubZC7cswQdUrM8TPqJhb/TuA==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3R1RHTGViTnRLVVkyM3J0\nbm96cGVPTlo4NXBNL0g1eEVSNG9DUkgwVFRBCmRKVTlMRmV3Tmg2RTZIclBlWlcr\ndzI5MUxhcllzbE1IMDNxa08zVkpITmsKLS0tIG01Y2dyQkY3UmRudFk2d0p6bThn\nemlaWnZoS3p4VHhMTFFwTm9VN0ttYzQKVbLFgtK6NIRIiryWHeeOPD45iwUds4QD\n7b8xYYoxlo+DETggxK6Vz3IdT/BSK5bFtgAxl864b5gW+Aw4c6AO5w==\n-----END AGE ENCRYPTED FILE-----\n"
} }
], ],
"lastmodified": "2025-10-21T13:17:17Z", "lastmodified": "2025-09-18T14:33:37Z",
"mac": "ENC[AES256_GCM,data:wdAFURkJZvclbz3UFPSPV9fma7zrZVEhMhsRqylGQMLepX/WohEAr8nJgeHl05be1Q8M8biPXCCoL0vfwg4BRZOkhD8PusJh8iBI3+STNQe/S1qoIK1ByfBFhJD+tIsVsgduLp6G32e6SRNvkuX3UpJqyViuRUavfQd3b8LRU4I=,iv:S3sMNTz5Kg4TxHj1tnk/ayiFuO74dR4aPnnomtkGByo=,tag:uive2bYe42s6VtPd03jTMw==,type:str]", "mac": "ENC[AES256_GCM,data:XKCnd0QrAlOCECSeSvbLYHMLbmUh4fMRnLaTb5ARoP4Zc9joWGsCaRZxokc2/sG4BXA/6pkbQXHyIOudKbcBpVjjvs9E+6Mnzt53nfRoH/iOkYPbN2EO49okVZJXW0M1rlBxrxvGuiDlz2p2p6L7neKLy4EB482pYea5+dUr2Yw=,iv:oj/MkZCfkvCmAb79uzEvKwEAm1bKtWhS4rPRAWSgRgw=,tag:h5TPPILXkhJplnDT2Gqtfw==,type:str]",
"version": "3.11.0" "unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
} }
} }

View File

@@ -29,7 +29,7 @@
}; };
perInstance = perInstance =
{ settings, roles, ... }: { settings, ... }:
{ {
nixosModule = nixosModule =
{ {
@@ -38,19 +38,8 @@
pkgs, pkgs,
... ...
}: }:
let
uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list);
# Collect searchDomains from all servers in this instance
allServerSearchDomains = lib.flatten (
lib.mapAttrsToList (_name: machineConfig: machineConfig.settings.certificate.searchDomains or [ ]) (
roles.server.machines or { }
)
);
# Merge client's searchDomains with all servers' searchDomains
searchDomains = uniqueStrings (settings.certificate.searchDomains ++ allServerSearchDomains);
in
{ {
clan.core.vars.generators.openssh-ca = lib.mkIf (searchDomains != [ ]) { clan.core.vars.generators.openssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) {
share = true; share = true;
files.id_ed25519.deploy = false; files.id_ed25519.deploy = false;
files."id_ed25519.pub" = { files."id_ed25519.pub" = {
@@ -65,9 +54,9 @@
''; '';
}; };
programs.ssh.knownHosts.ssh-ca = lib.mkIf (searchDomains != [ ]) { programs.ssh.knownHosts.ssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) {
certAuthority = true; certAuthority = true;
extraHostNames = builtins.map (domain: "*.${domain}") searchDomains; extraHostNames = builtins.map (domain: "*.${domain}") settings.certificate.searchDomains;
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value; publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
}; };
}; };

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

@@ -22,7 +22,6 @@ in
../../clanServices/syncthing ../../clanServices/syncthing
# Required modules # Required modules
../../nixosModules/clanCore ../../nixosModules/clanCore
../../nixosModules/machineModules
# Dependencies like clan-cli # Dependencies like clan-cli
../../pkgs/clan-cli ../../pkgs/clan-cli
]; ];

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

@@ -54,10 +54,7 @@
- For other controllers: The controller's /56 subnet - For other controllers: The controller's /56 subnet
*/ */
{ { ... }:
clanLib,
...
}:
let let
# Shared module for extraHosts configuration # Shared module for extraHosts configuration
extraHostsModule = extraHostsModule =
@@ -77,12 +74,10 @@ let
controllerHosts = lib.mapAttrsToList ( controllerHosts = lib.mapAttrsToList (
name: _value: name: _value:
let let
prefix = clanLib.vars.getPublicValue { prefix = builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = name; + "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value"
generator = "wireguard-network-${instanceName}"; );
file = "prefix";
};
# Controller IP is always ::1 in their subnet # Controller IP is always ::1 in their subnet
ip = prefix + "::1"; ip = prefix + "::1";
in in
@@ -93,24 +88,20 @@ let
peerHosts = lib.mapAttrsToList ( peerHosts = lib.mapAttrsToList (
peerName: peerValue: peerName: peerValue:
let let
peerSuffix = clanLib.vars.getPublicValue { peerSuffix = builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = peerName; + "/vars/per-machine/${peerName}/wireguard-network-${instanceName}/suffix/value"
generator = "wireguard-network-${instanceName}"; );
file = "suffix";
};
# Determine designated controller # Determine designated controller
designatedController = designatedController =
if (builtins.length (builtins.attrNames roles.controller.machines) == 1) then if (builtins.length (builtins.attrNames roles.controller.machines) == 1) then
(builtins.head (builtins.attrNames roles.controller.machines)) (builtins.head (builtins.attrNames roles.controller.machines))
else else
peerValue.settings.controller; peerValue.settings.controller;
controllerPrefix = clanLib.vars.getPublicValue { controllerPrefix = builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = designatedController; + "/vars/per-machine/${designatedController}/wireguard-network-${instanceName}/prefix/value"
generator = "wireguard-network-${instanceName}"; );
file = "prefix";
};
peerIP = controllerPrefix + ":" + peerSuffix; peerIP = controllerPrefix + ":" + peerSuffix;
in in
"${peerIP} ${peerName}.${domain}" "${peerIP} ${peerName}.${domain}"
@@ -229,12 +220,10 @@ in
lib.mapAttrsToList ( lib.mapAttrsToList (
ctrlName: _: ctrlName: _:
let let
controllerPrefix = clanLib.vars.getPublicValue { controllerPrefix = builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = ctrlName; + "/vars/per-machine/${ctrlName}/wireguard-network-${instanceName}/prefix/value"
generator = "wireguard-network-${instanceName}"; );
file = "prefix";
};
peerIP = controllerPrefix + ":" + peerSuffix; peerIP = controllerPrefix + ":" + peerSuffix;
in in
"${peerIP}/56" "${peerIP}/56"
@@ -245,22 +234,20 @@ in
# Connect to all controllers # Connect to all controllers
peers = lib.mapAttrsToList (name: value: { peers = lib.mapAttrsToList (name: value: {
publicKey = clanLib.vars.getPublicValue { publicKey = (
flake = config.clan.core.settings.directory; builtins.readFile (
machine = name; config.clan.core.settings.directory
generator = "wireguard-keys-${instanceName}"; + "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
file = "publickey"; )
}; );
# Allow each controller's /56 subnet # Allow each controller's /56 subnet
allowedIPs = [ allowedIPs = [
"${ "${
clanLib.vars.getPublicValue { builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = name; + "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value"
generator = "wireguard-network-${instanceName}"; )
file = "prefix";
}
}::/56" }::/56"
]; ];
@@ -362,29 +349,25 @@ in
if allPeers ? ${name} then if allPeers ? ${name} then
# For peers: they now have our entire /56 subnet # For peers: they now have our entire /56 subnet
{ {
publicKey = clanLib.vars.getPublicValue { publicKey = (
flake = config.clan.core.settings.directory; builtins.readFile (
machine = name; config.clan.core.settings.directory
generator = "wireguard-keys-${instanceName}"; + "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
file = "publickey"; )
}; );
# Allow the peer's /96 range in ALL controller subnets # Allow the peer's /96 range in ALL controller subnets
allowedIPs = lib.mapAttrsToList ( allowedIPs = lib.mapAttrsToList (
ctrlName: _: ctrlName: _:
let let
controllerPrefix = clanLib.vars.getPublicValue { controllerPrefix = builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = ctrlName; + "/vars/per-machine/${ctrlName}/wireguard-network-${instanceName}/prefix/value"
generator = "wireguard-network-${instanceName}"; );
file = "prefix"; peerSuffix = builtins.readFile (
}; config.clan.core.settings.directory
peerSuffix = clanLib.vars.getPublicValue { + "/vars/per-machine/${name}/wireguard-network-${instanceName}/suffix/value"
flake = config.clan.core.settings.directory; );
machine = name;
generator = "wireguard-network-${instanceName}";
file = "suffix";
};
in in
"${controllerPrefix}:${peerSuffix}/96" "${controllerPrefix}:${peerSuffix}/96"
) roles.controller.machines; ) roles.controller.machines;
@@ -394,21 +377,19 @@ in
else else
# For other controllers: use their /56 subnet # For other controllers: use their /56 subnet
{ {
publicKey = clanLib.vars.getPublicValue { publicKey = (
flake = config.clan.core.settings.directory; builtins.readFile (
machine = name; config.clan.core.settings.directory
generator = "wireguard-keys-${instanceName}"; + "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
file = "publickey"; )
}; );
allowedIPs = [ allowedIPs = [
"${ "${
clanLib.vars.getPublicValue { builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = name; + "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value"
generator = "wireguard-network-${instanceName}"; )
file = "prefix";
}
}::/56" }::/56"
]; ];

View File

@@ -1,23 +1,7 @@
🚧🚧🚧 Experimental 🚧🚧🚧
Use at your own risk.
We are still refining its interfaces, instability and breakages are expected.
---
This module sets up [yggdrasil](https://yggdrasil-network.github.io/) across your clan. This module sets up [yggdrasil](https://yggdrasil-network.github.io/) across your clan.
Yggdrasil is designed to be a future-proof and decentralised alternative to the Yggdrasil is designed to be a future-proof and decentralised alternative to
structured routing protocols commonly used today on the internet. Inside your the structured routing protocols commonly used today on the internet. Inside your clan, it will allow you to reach all of your machines.
clan, it will allow you to reach all of your machines.
If you have other services in your inventory which export peers (e.g. the
`internet` or the services) as [service
exports](https://docs.clan.lol/reference/options/clan_service/#exports), they
will be added as yggdrasil peers automatically. This allows using the stable
yggdrasil IPv6 address to refer to other hosts and letting yggdrasil decide on
the best routing based on available connections.
## Example Usage ## Example Usage

View File

@@ -29,13 +29,12 @@
]; ];
}; };
options.extraPeers = lib.mkOption { options.peers = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = [ ]; default = [ ];
description = '' description = ''
Additional static peers to configure for this host. If you use a Static peers to configure for this host.
VPN clan service, it will automatically be added as peers to other hosts. If not set, local peers will be auto-discovered
Local peers are also auto-discovered and don't need to be added.
''; '';
example = [ example = [
"tcp://192.168.1.1:6443" "tcp://192.168.1.1:6443"
@@ -46,74 +45,16 @@
}; };
}; };
perInstance = perInstance =
{ { settings, ... }:
settings,
roles,
exports,
...
}:
{ {
nixosModule = nixosModule =
{ {
config, config,
pkgs, pkgs,
lib,
clan-core,
... ...
}: }:
let
mkPeers = ip: [
# "tcp://${ip}:6443"
"quic://${ip}:6443"
"ws://${ip}:6443"
"tls://${ip}:6443"
];
select' = clan-core.inputs.nix-select.lib.select;
# TODO make it nicer @lassulus, @picnoir wants microlens
# Get a list of all exported IPs from all VPN modules
# exportedPeerIPs = builtins.foldl' (
# acc: e:
# if e == { } then
# acc
# else
# acc ++ (lib.flatten (builtins.filter (s: s != "") (lib.attrValues (select' "peers.*.plain" e))))
# ) [ ] (lib.attrValues (select' "*.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
exportedPeers = lib.flatten (map mkPeers exportedPeerIPs);
in
{ {
# Set <yggdrasil ip> <hostname>.<tld> for all hosts.
# Networking modules will then add themselves as peers, so we can
# always use this to resolve a host via the best possible route,
# doing fail-over if needed.
networking.extraHosts = lib.strings.concatStringsSep "\n" (
lib.filter (n: n != "") (
map (
name:
let
ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/yggdrasil/address/value";
in
if builtins.pathExists ipPath then
"${builtins.readFile ipPath} ${name}.${config.clan.core.settings.tld}"
else
""
) (lib.attrNames roles.default.machines)
)
);
clan.core.vars.generators.yggdrasil = { clan.core.vars.generators.yggdrasil = {
files.privateKey = { }; files.privateKey = { };
@@ -158,7 +99,7 @@
settings = { settings = {
PrivateKeyPath = "/key"; PrivateKeyPath = "/key";
IfName = "ygg"; IfName = "ygg";
Peers = lib.lists.unique (exportedPeers ++ settings.extraPeers); Peers = settings.peers;
MulticastInterfaces = [ MulticastInterfaces = [
# Ethernet is preferred over WIFI # Ethernet is preferred over WIFI
{ {

View File

@@ -17,20 +17,6 @@
roles.default.machines.peer1 = { }; roles.default.machines.peer1 = { };
roles.default.machines.peer2 = { }; roles.default.machines.peer2 = { };
}; };
# Peers are set form exports of the internet service
instances."internet" = {
module.name = "internet";
roles.default.machines.peer1.settings.host = "peer1-internet";
roles.default.machines.peer2.settings.host = "peer2-internet";
};
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,8 +1,4 @@
{ { ... }:
clanLib,
directory,
...
}:
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/zerotier"; manifest.name = "clan-core/zerotier";
@@ -17,23 +13,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,
@@ -45,7 +39,6 @@
imports = [ imports = [
(import ./shared.nix { (import ./shared.nix {
inherit inherit
clanLib
instanceName instanceName
roles roles
config config
@@ -97,7 +90,6 @@
imports = [ imports = [
(import ./shared.nix { (import ./shared.nix {
inherit inherit
clanLib
instanceName instanceName
roles roles
config config
@@ -150,7 +142,6 @@
imports = [ imports = [
(import ./shared.nix { (import ./shared.nix {
inherit inherit
clanLib
instanceName instanceName
roles roles
config config
@@ -169,16 +160,15 @@
); );
networkIps = builtins.foldl' ( networkIps = builtins.foldl' (
ips: name: ips: name:
let if
ztIp = clanLib.vars.getPublicValue { builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value"
flake = config.clan.core.settings.directory; then
machine = name; ips
generator = "zerotier"; ++ [
file = "zerotier-ip"; (builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value")
default = null; ]
}; else
in ips
if ztIp != null then ips ++ [ ztIp ] else ips
) [ ] machines; ) [ ] machines;
allHostIPs = settings.allowedIps ++ networkIps; allHostIPs = settings.allowedIps ++ networkIps;
in in

View File

@@ -21,7 +21,6 @@ in
../../clanServices/zerotier ../../clanServices/zerotier
# Required modules # Required modules
../../nixosModules/clanCore ../../nixosModules/clanCore
../../nixosModules/machineModules
# Dependencies like clan-cli # Dependencies like clan-cli
../../pkgs/clan-cli ../../pkgs/clan-cli
]; ];

View File

@@ -1,5 +1,4 @@
{ {
clanLib,
lib, lib,
config, config,
pkgs, pkgs,
@@ -9,26 +8,20 @@
}: }:
let let
controllerMachine = builtins.head (lib.attrNames roles.controller.machines or { }); controllerMachine = builtins.head (lib.attrNames roles.controller.machines or { });
networkId = clanLib.vars.getPublicValue { networkIdPath = "${config.clan.core.settings.directory}/vars/per-machine/${controllerMachine}/zerotier/zerotier-network-id/value";
flake = config.clan.core.settings.directory; networkId = if builtins.pathExists networkIdPath then builtins.readFile networkIdPath else null;
machine = controllerMachine;
generator = "zerotier";
file = "zerotier-network-id";
default = null;
};
moons = lib.attrNames (roles.moon.machines or { }); moons = lib.attrNames (roles.moon.machines or { });
moonIps = builtins.foldl' ( moonIps = builtins.foldl' (
ips: name: ips: name:
let if
moonIp = clanLib.vars.getPublicValue { builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value"
flake = config.clan.core.settings.directory; then
machine = name; ips
generator = "zerotier"; ++ [
file = "zerotier-ip"; (builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value")
default = null; ]
}; else
in ips
if moonIp != null then ips ++ [ moonIp ] else ips
) [ ] moons; ) [ ] moons;
in in
{ {

24
devFlake/flake.lock generated
View File

@@ -3,10 +3,10 @@
"clan-core-for-checks": { "clan-core-for-checks": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1761204206, "lastModified": 1760361585,
"narHash": "sha256-A4KDudGblln1yh8c95OVow2NRlHtbGZXr/pgNenyrNc=", "narHash": "sha256-v4PnSmt1hXW4dSgVWxcd1ZeEBlhO7NksNRC5cX7L5iw=",
"ref": "main", "ref": "main",
"rev": "aabbe0dfac47b7cfbe2210bcb27fb7ecce93350f", "rev": "7e7e58eb64ef61beb0a938a6622ec0122382131b",
"shallow": true, "shallow": true,
"type": "git", "type": "git",
"url": "https://git.clan.lol/clan/clan-core" "url": "https://git.clan.lol/clan/clan-core"
@@ -105,11 +105,11 @@
}, },
"nixpkgs-dev": { "nixpkgs-dev": {
"locked": { "locked": {
"lastModified": 1761748483, "lastModified": 1760965023,
"narHash": "sha256-v7fttCB5lJ22Ok7+N7ZbLhDeM89QIz9YWtQP4XN7xgA=", "narHash": "sha256-cpcgkeLApMGFCdp4jFqeIxTwlcGaSI+Zwmv8z2E85pY=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "061c55856b29b8b9360e14231a0986c7f85f1130", "rev": "40ef6b9aa73f70b265c29df083fafae66b9df351",
"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": {
@@ -208,11 +208,11 @@
"nixpkgs": [] "nixpkgs": []
}, },
"locked": { "locked": {
"lastModified": 1761311587, "lastModified": 1760945191,
"narHash": "sha256-Msq86cR5SjozQGCnC6H8C+0cD4rnx91BPltZ9KK613Y=", "narHash": "sha256-ZRVs8UqikBa4Ki3X4KCnMBtBW0ux1DaT35tgsnB1jM4=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "2eddae033e4e74bf581c2d1dfa101f9033dbd2dc", "rev": "f56b1934f5f8fcab8deb5d38d42fd692632b47c2",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -88,7 +88,6 @@ For the provide flake example, your flake should now look like this:
self = self; # this needs to point at the repository root self = self; # this needs to point at the repository root
specialArgs = {}; specialArgs = {};
meta.name = throw "Change me to something unique"; meta.name = throw "Change me to something unique";
meta.tld = throw "Change me to something unique";
machines = { machines = {
berlin = { berlin = {

View File

@@ -137,13 +137,12 @@ Description: None
This confirms your setup is working correctly. This confirms your setup is working correctly.
You can now change the default name and tld by editing the `meta.name` and `meta.tld` fields in your `clan.nix` file. You can now change the default name by editing the `meta.name` field in your `clan.nix` file.
```{.nix title="clan.nix" hl_lines="3 4"} ```{.nix title="clan.nix" hl_lines="3"}
{ {
# Ensure this is unique among all clans you want to use. # Ensure this is unique among all clans you want to use.
meta.name = "__CHANGE_ME__"; meta.name = "__CHANGE_ME__";
meta.tld = "changeme";
# ... # ...
# elided # elided

View File

@@ -10,11 +10,10 @@ and how to define a remote builder for your machine closures.
Set the machines `targetHost` to the reachable IP address of the new machine. Set the machines `targetHost` to the reachable IP address of the new machine.
This eliminates the need to specify `--target-host` in CLI commands. This eliminates the need to specify `--target-host` in CLI commands.
```{.nix title="clan.nix" hl_lines="10"} ```{.nix title="clan.nix" hl_lines="9"}
{ {
# Ensure this is unique among all clans you want to use. # Ensure this is unique among all clans you want to use.
meta.name = "my-clan"; meta.name = "my-clan";
meta.tld = "ccc";
inventory.machines = { inventory.machines = {
# Define machines here. # Define machines here.

View File

@@ -60,7 +60,6 @@ Configure Clan-wide settings and define machines. Here's an example `flake.nix`:
# Define your Clan # Define your Clan
clan = { clan = {
meta.name = ""; # Required and must be unique meta.name = ""; # Required and must be unique
meta.tld = ""; # Required and must be unique
machines = { machines = {
jon = { jon = {

View File

@@ -43,7 +43,6 @@ For the purpose of this guide we have two machines:
inherit self; inherit self;
meta.name = "myclan"; meta.name = "myclan";
meta.tld = "ccc";
inventory.machines = { inventory.machines = {
controller = {}; controller = {};

View File

@@ -63,7 +63,6 @@ To use `age` plugins with Clan, you need to configure them in your `flake.nix` f
inherit self; inherit self;
meta.name = "myclan"; meta.name = "myclan";
meta.tld = "ccc";
# Add YubiKey and FIDO2 HMAC plugins # Add YubiKey and FIDO2 HMAC plugins
# Note: Plugins must be available in nixpkgs. # Note: Plugins must be available in nixpkgs.

24
flake.lock generated
View File

@@ -71,11 +71,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1761339987, "lastModified": 1760721282,
"narHash": "sha256-IUaawVwItZKi64IA6kF6wQCLCzpXbk2R46dHn8sHkig=", "narHash": "sha256-aAHphQbU9t/b2RRy2Eb8oMv+I08isXv2KUGFAFn7nCo=",
"owner": "nix-darwin", "owner": "nix-darwin",
"repo": "nix-darwin", "repo": "nix-darwin",
"rev": "7cd9aac79ee2924a85c211d21fafd394b06a38de", "rev": "c3211fcd0c56c11ff110d346d4487b18f7365168",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -99,11 +99,11 @@
}, },
"nixos-facter-modules": { "nixos-facter-modules": {
"locked": { "locked": {
"lastModified": 1761137276, "lastModified": 1756491981,
"narHash": "sha256-4lDjGnWRBLwqKQ4UWSUq6Mvxu9r8DSqCCydodW/Jsi8=", "narHash": "sha256-lXyDAWPw/UngVtQfgQ8/nrubs2r+waGEYIba5UX62+k=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixos-facter-modules", "repo": "nixos-facter-modules",
"rev": "70bcd64225d167c7af9b475c4df7b5abba5c7de8", "rev": "c1b29520945d3e148cd96618c8a0d1f850965d8c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -146,11 +146,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760998189, "lastModified": 1760845571,
"narHash": "sha256-ee2e1/AeGL5X8oy/HXsZQvZnae6XfEVdstGopKucYLY=", "narHash": "sha256-PwGzU3EOU65Ef1VvuNnVLie+l+P0g/fzf/PGUG82KbM=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "5a7d18b5c55642df5c432aadb757140edfeb70b3", "rev": "9c9a9798be331ed3f4b2902933d7677d0659ee61",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -181,11 +181,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1761311587, "lastModified": 1760945191,
"narHash": "sha256-Msq86cR5SjozQGCnC6H8C+0cD4rnx91BPltZ9KK613Y=", "narHash": "sha256-ZRVs8UqikBa4Ki3X4KCnMBtBW0ux1DaT35tgsnB1jM4=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "2eddae033e4e74bf581c2d1dfa101f9033dbd2dc", "rev": "f56b1934f5f8fcab8deb5d38d42fd692632b47c2",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -68,7 +68,6 @@
( (
{ ... }: { ... }:
{ {
debug = true;
clan = { clan = {
meta.name = "clan-core"; meta.name = "clan-core";
inventory = { inventory = {
@@ -99,7 +98,6 @@
./lib/filter-clan-core/flake-module.nix ./lib/filter-clan-core/flake-module.nix
./lib/flake-module.nix ./lib/flake-module.nix
./lib/flake-parts/clan-nixos-test.nix ./lib/flake-parts/clan-nixos-test.nix
./modules/flake-module.nix
./nixosModules/clanCore/vars/flake-module.nix ./nixosModules/clanCore/vars/flake-module.nix
./nixosModules/flake-module.nix ./nixosModules/flake-module.nix
./pkgs/clan-cli/clan_cli/tests/flake-module.nix ./pkgs/clan-cli/clan_cli/tests/flake-module.nix

View File

@@ -39,10 +39,44 @@ in
}; };
modules = [ modules = [
clan-core.modules.clan.default clan-core.modules.clan.default
{
checks.minNixpkgsVersion = {
assertion = lib.versionAtLeast nixpkgs.lib.version "25.11";
message = ''
Nixpkgs version: ${nixpkgs.lib.version} is incompatible with clan-core. (>= 25.11 is recommended)
---
Your version of 'nixpkgs' seems too old for clan-core.
Please read: https://docs.clan.lol/guides/nixpkgs-flake-input
You can ignore this check by setting:
clan.checks.minNixpkgsVersion.ignore = true;
---
'';
};
# TODO: Pinpox
checks.conflictingPorts =
let
conflicts = [ ];
in
{
assertion = builtins.length conflicts == 0;
message = ''
The following endpoints have conflicting port assignments:
---
'';
};
}
]; ];
}; };
# Important: !This logic needs to be kept in sync with lib.clan function! apply =
apply = config: clan-core.lib.checkConfig config.checks config; config:
lib.deepSeq (lib.mapAttrs (
id: check:
if check.ignore || check.assertion then
null
else
throw "clan.checks.${id} failed with message\n${check.message}"
) config.checks) config;
}; };
# Mapped flake toplevel outputs # Mapped flake toplevel outputs

View File

@@ -1,19 +0,0 @@
{ lib, ... }:
/**
Function to assert clan configuration checks.
Arguments:
- 'checks' attribute of clan configuration
- Any: the returned configuration (can be anything, is just passed through)
*/
checks:
lib.deepSeq (
lib.mapAttrs (
id: check:
if check.ignore || check.assertion then
null
else
throw "clan.checks.${id} failed with message\n${check.message}"
) checks
)

View File

@@ -33,23 +33,20 @@
let let
nixpkgs = self.inputs.nixpkgs or clan-core.inputs.nixpkgs; nixpkgs = self.inputs.nixpkgs or clan-core.inputs.nixpkgs;
nix-darwin = self.inputs.nix-darwin or clan-core.inputs.nix-darwin; nix-darwin = self.inputs.nix-darwin or clan-core.inputs.nix-darwin;
configuration = (
lib.evalModules {
class = "clan";
specialArgs = {
inherit
self
;
inherit
nixpkgs
nix-darwin
;
};
modules = [
clan-core.modules.clan.default
m
];
}
);
in in
clan-core.clanLib.checkConfig configuration.config.checks configuration lib.evalModules {
class = "clan";
specialArgs = {
inherit
self
;
inherit
nixpkgs
nix-darwin
;
};
modules = [
clan-core.modules.clan.default
m
];
}

View File

@@ -137,12 +137,6 @@ in
default = { }; default = { };
type = types.submoduleWith { type = types.submoduleWith {
specialArgs = { specialArgs = {
self = throw ''
'self' is banned in the use of clan.services
Use 'exports' instead: https://docs.clan.lol/reference/options/clan_service/#exports
---
If you really need to used 'self' here, that makes the module less portable
'';
inherit (config.clanSettings) inherit (config.clanSettings)
clan-core clan-core
nixpkgs nixpkgs

View File

@@ -16,12 +16,10 @@ lib.fix (
*/ */
callLib = file: args: import file ({ inherit lib clanLib; } // args); callLib = file: args: import file ({ inherit lib clanLib; } // args);
checkConfig = clanLib.callLib ./clan/checkConfig.nix { }; evalService = clanLib.callLib ./modules/inventory/distributed-service/evalService.nix { };
evalService = clanLib.callLib ./evalService.nix { };
# ------------------------------------ # ------------------------------------
# ClanLib functions # ClanLib functions
inventory = clanLib.callLib ./inventory { }; inventory = clanLib.callLib ./modules/inventory { };
test = clanLib.callLib ./test { }; test = clanLib.callLib ./test { };
flake-inputs = clanLib.callLib ./flake-inputs.nix { }; flake-inputs = clanLib.callLib ./flake-inputs.nix { };
# Custom types # Custom types
@@ -32,17 +30,12 @@ lib.fix (
jsonschema = import ./jsonschema { inherit lib; }; jsonschema = import ./jsonschema { inherit lib; };
docs = import ./docs.nix { inherit lib; }; docs = import ./docs.nix { inherit lib; };
vars = import ./vars.nix { inherit lib; };
# flakes # flakes
flakes = clanLib.callLib ./flakes.nix { }; flakes = clanLib.callLib ./flakes.nix { };
# 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

@@ -10,11 +10,12 @@ in
rec { rec {
# TODO: automatically generate this from the directory conventions # TODO: automatically generate this from the directory conventions
imports = [ imports = [
./modules/flake-module.nix
./clanTest/flake-module.nix ./clanTest/flake-module.nix
./introspection/flake-module.nix ./introspection/flake-module.nix
./modules/inventory/flake-module.nix
./jsonschema/flake-module.nix ./jsonschema/flake-module.nix
./types/flake-module.nix ./types/flake-module.nix
./inventory/flake-module.nix
]; ];
flake.clanLib = flake.clanLib =
let let
@@ -77,6 +78,9 @@ rec {
../lib ../lib
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../.) (lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../.)
../flakeModules ../flakeModules
# ../../nixosModules/clanCore
# ../../machines
# ../../inventory.json
]; ];
}; };
in in
@@ -97,41 +101,6 @@ rec {
touch $out touch $out
''; '';
}; };
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests-build-clan
legacyPackages.evalTests-build-clan = import ./tests.nix {
inherit lib;
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 = {
eval-lib-build-clan = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
export HOME="$(realpath .)"
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${
self.filter {
include = [
"flakeModules"
"inventory.json"
"lib"
"machines"
"nixosModules"
"modules"
];
}
}#legacyPackages.${system}.evalTests-build-clan
touch $out
'';
};
}; };
} }

View File

@@ -0,0 +1,12 @@
{ clan-core }:
{
_class = "clan";
_module.args = {
inherit clan-core;
inherit (clan-core) clanLib;
};
imports = [
./module.nix
./interface.nix
];
}

View File

@@ -0,0 +1,4 @@
{ self, lib, ... }:
{
flake.modules.clan.default = lib.modules.importApply ./default.nix { clan-core = self; };
}

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,108 @@ in
visible = false; visible = false;
type = types.deferredModule; type = types.deferredModule;
default = { default = {
options.networking = { options.endpoints = lib.mkOption {
type = types.attrsWith {
priority = lib.mkOption { placeholder = "endpointName";
type = lib.types.int; elemType = (
default = 1000; types.submodule {
description = '' options = {
priority with which this network should be tried. port = lib.mkOption {
higher priority means it gets used earlier in the chain type = types.int;
''; description = "The port the service is running on";
};
protocol = lib.mkOption {
type = types.enum [
"tcp"
"udp"
];
default = "tcp";
description = "The protocol used to access the service";
};
};
}
);
}; };
module = lib.mkOption { default = { };
# type = lib.types.enum [ };
# "clan_lib.network.direct" options.networking = lib.mkOption {
# "clan_lib.network.tor" default = null;
# ]; type = lib.types.nullOr (
type = lib.types.str; lib.types.submodule {
default = "clan_lib.network.direct"; options = {
description = '' priority = lib.mkOption {
the technology this network uses to connect to the target type = lib.types.int;
This is used for userspace networking with socks proxies. default = 1000;
''; description = ''
}; priority with which this network should be tried.
# should we call this machines? hosts? higher priority means it gets used earlier in the chain
'';
hosts = lib.mkOption { };
type = lib.types.listOf lib.types.str; module = lib.mkOption {
default = [ ]; # type = lib.types.enum [
}; # "clan_lib.network.direct"
# "clan_lib.network.tor"
# peers = lib.mkOption { # ];
# type = lib.types.str;
# # <name> default = "clan_lib.network.direct";
# type = lib.types.attrsOf ( description = ''
# lib.types.submodule ( the technology this network uses to connect to the target
# { name, ... }: This is used for userspace networking with socks proxies.
# { '';
# options = { };
# name = lib.mkOption { # should we call this machines? hosts?
# type = lib.types.str; peers = lib.mkOption {
# default = name; # <name>
# }; type = lib.types.attrsOf (
# SSHOptions = lib.mkOption { lib.types.submodule (
# type = lib.types.listOf lib.types.str; { name, ... }:
# default = [ ]; {
# }; options = {
# name = lib.mkOption {
# host = lib.mkOption { type = lib.types.str;
# description = ''''; default = name;
# type = lib.types.attrTag { };
# plain = lib.mkOption { SSHOptions = lib.mkOption {
# type = lib.types.str; type = lib.types.listOf lib.types.str;
# description = '' default = [ ];
# a plain value, which can be read directly from the config };
# ''; host = lib.mkOption {
# }; description = '''';
# var = lib.mkOption { type = lib.types.attrTag {
# type = lib.types.submodule { plain = lib.mkOption {
# options = { type = lib.types.str;
# machine = lib.mkOption { description = ''
# type = lib.types.str; a plain value, which can be read directly from the config
# example = "jon"; '';
# }; };
# generator = lib.mkOption { var = lib.mkOption {
# type = lib.types.str; type = lib.types.submodule {
# example = "tor-ssh"; options = {
# }; machine = lib.mkOption {
# file = lib.mkOption { type = lib.types.str;
# type = lib.types.str; example = "jon";
# example = "hostname"; };
# }; generator = lib.mkOption {
# }; type = lib.types.str;
# }; example = "tor-ssh";
# }; };
# }; file = lib.mkOption {
# }; type = lib.types.str;
# }; example = "hostname";
# } };
# ) };
# ); };
# }; };
};
};
};
}
)
);
};
};
}
);
}; };
}; };
description = '' description = ''
@@ -288,7 +312,7 @@ in
Global information about the clan. Global information about the clan.
''; '';
type = types.deferredModuleWith { type = types.deferredModuleWith {
staticModules = [ ../inventoryClass/meta.nix ]; staticModules = [ ../inventoryClass/meta-interface.nix ];
}; };
default = { }; default = { };
}; };

View File

@@ -100,7 +100,7 @@ let
_: machine: _: machine:
machine.extendModules { machine.extendModules {
modules = [ modules = [
(lib.modules.importApply ../../nixosModules/machineModules/overridePkgs.nix { (lib.modules.importApply ../machineModules/overridePkgs.nix {
pkgs = pkgsFor.${system}; pkgs = pkgsFor.${system};
}) })
]; ];
@@ -167,9 +167,6 @@ in
{ ... }@args: { ... }@args:
let let
_class = _class =
# _class was added in https://github.com/NixOS/nixpkgs/pull/395141
# Clan relies on it to determine which modules to load
# people need to use at least that version of nixpkgs
args._class or (throw '' args._class or (throw ''
Your version of nixpkgs is incompatible with the latest clan. Your version of nixpkgs is incompatible with the latest clan.
Please update nixpkgs input to the latest nixos-unstable or nixpkgs-unstable. Please update nixpkgs input to the latest nixos-unstable or nixpkgs-unstable.
@@ -179,7 +176,7 @@ in
in in
{ {
imports = [ imports = [
(lib.modules.importApply ../../nixosModules/machineModules/forName.nix { (lib.modules.importApply ../machineModules/forName.nix {
inherit (config.inventory) meta; inherit (config.inventory) meta;
inherit inherit
name name
@@ -219,44 +216,43 @@ in
inherit nixosConfigurations; inherit nixosConfigurations;
inherit darwinConfigurations; inherit darwinConfigurations;
exports = config.clanInternals.inventoryClass.distributedServices.servicesEval.config.exports;
clanInternals = { clanInternals = {
inventoryClass = inventoryClass =
let let
flakeInputs = config.self.inputs; flakeInputs = config.self.inputs;
# Compute the relative directory path
selfStr = toString config.self;
dirStr = toString directory;
relativeDirectory =
if selfStr == dirStr then
""
else if lib.hasPrefix selfStr dirStr then
lib.removePrefix (selfStr + "/") dirStr
else
# This shouldn't happen in normal usage, but can occur when
# the flake is copied (e.g., in tests). Fall back to empty string.
"";
in in
{ {
_module.args = { _module.args = {
inherit clanLib; inherit clanLib;
}; };
imports = [ imports = [
../inventoryClass/default.nix ../inventoryClass/builder/default.nix
(lib.modules.importApply ../inventoryClass/service-list-from-inputs.nix {
inherit flakeInputs clanLib;
})
{ {
inherit inherit inventory directory;
inventory
directory
flakeInputs
relativeDirectory
;
exportsModule = config.exportsModule;
} }
( (
{ ... }: let
clanConfig = config;
in
{ config, ... }:
{ {
staticModules = clan-core.clan.modules; staticModules = clan-core.clan.modules;
distributedServices = clanLib.inventory.mapInstances {
inherit (clanConfig) inventory exportsModule;
inherit flakeInputs directory;
clanCoreModules = clan-core.clan.modules;
prefix = [ "distributedServices" ];
};
machines = config.distributedServices.allMachines;
} }
) )
../inventoryClass/inventory-introspection.nix
]; ];
}; };

View File

@@ -1,28 +1,3 @@
/**
The templates submodule
'clan.templates'
Different kinds supported:
- clan templates: 'clan.templates.clan'
- disko templates: 'clan.templates.disko'
- machine templates: 'clan.templates.machine'
A template has the form:
```nix
{
description: string; # short summary what the template contains
path: path; # path to the template
}
```
The clan API copies the template from the given 'path'
into a target folder. For example,
`./machines/<machine-name>` for 'machine' templates.
*/
{ {
lib, lib,
... ...

View File

@@ -2,7 +2,7 @@
lib ? import <nixpkgs/lib>, lib ? import <nixpkgs/lib>,
}: }:
let let
clanLibOrig = (import ./. { inherit lib; }).__unfix__; clanLibOrig = (import ./.. { inherit lib; }).__unfix__;
clanLibWithFs = clanLibWithFs =
{ virtual_fs }: { virtual_fs }:
lib.fix ( lib.fix (
@@ -11,19 +11,19 @@ let
let let
clan-core = { clan-core = {
clanLib = final; clanLib = final;
modules.clan.default = lib.modules.importApply ../modules/clan { inherit clan-core; }; modules.clan.default = lib.modules.importApply ./clan { inherit clan-core; };
# Note: Can add other things to "clan-core" # Note: Can add other things to "clan-core"
# ... Not needed for this test # ... Not needed for this test
}; };
in in
{ {
clan = import ./clan { clan = import ../clan {
inherit lib clan-core; inherit lib clan-core;
}; };
# Override clanLib.fs for unit-testing against a virtual filesystem # Override clanLib.fs for unit-testing against a virtual filesystem
fs = import ./clanTest/virtual-fs.nix { inherit lib; } { fs = import ../clanTest/virtual-fs.nix { inherit lib; } {
inherit rootPath virtual_fs; inherit rootPath virtual_fs;
# Example of a passthru # Example of a passthru
# passthru = [ # passthru = [
@@ -53,12 +53,7 @@ in
}; };
}; };
}).clan }).clan
{ { config.directory = rootPath; };
directory = rootPath;
self = {
inputs.nixpkgs.lib.version = "25.11";
};
};
in in
{ {
inherit vclan; inherit vclan;
@@ -99,12 +94,7 @@ in
}; };
}; };
}).clan }).clan
{ { config.directory = rootPath; };
directory = rootPath;
self = {
inputs.nixpkgs.lib.version = "25.11";
};
};
in in
{ {
inherit vclan; inherit vclan;

View File

@@ -1,24 +1,19 @@
{ {
pkgs, pkgs,
lib, lib,
clanModule,
clanLib,
clan-core, clan-core,
}: }:
let let
eval = lib.evalModules { eval = lib.evalModules {
modules = [ modules = [
clanModule clan-core.modules.clan.default
]; ];
specialArgs = {
self = clan-core;
};
}; };
evalDocs = pkgs.nixosOptionsDoc { evalDocs = pkgs.nixosOptionsDoc {
options = eval.options; options = eval.options;
warningsAreErrors = false; warningsAreErrors = false;
transformOptions = clanLib.docs.stripStorePathsFromDeclarations; transformOptions = clan-core.clanLib.docs.stripStorePathsFromDeclarations;
}; };
in in
{ {

View File

@@ -0,0 +1,58 @@
{
self,
inputs,
...
}:
let
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
in
{
imports = [
./clan/flake-module.nix
];
perSystem =
{
pkgs,
lib,
system,
...
}:
let
jsonDocs = import ./eval-docs.nix {
inherit pkgs lib;
clan-core = self;
};
in
{
legacyPackages.clan-options = jsonDocs.optionsJSON;
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests-build-clan
legacyPackages.evalTests-build-clan = import ./tests.nix {
inherit lib;
clan-core = self;
};
checks = {
eval-lib-build-clan = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
export HOME="$(realpath .)"
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${
self.filter {
include = [
"flakeModules"
"inventory.json"
"lib"
"machines"
"nixosModules"
];
}
}#legacyPackages.${system}.evalTests-build-clan
touch $out
'';
};
};
}

View File

@@ -2,11 +2,15 @@
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 = [
../../modules/inventoryClass/inventory.nix ../inventoryClass/inventory.nix
]; ];
_module.args = { inherit clanLib; }; _module.args = { inherit clanLib; };
}; };

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,11 +28,9 @@ 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 = [
( (
@@ -54,13 +51,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

@@ -17,9 +17,9 @@ lib.evalModules {
specialArgs._ctx = prefix; specialArgs._ctx = prefix;
modules = [ modules = [
# Base module # Base module
./inventory/distributed-service/service-module.nix ./service-module.nix
# Feature modules # Feature modules
(lib.modules.importApply ./inventory/distributed-service/api-feature.nix { (lib.modules.importApply ./api-feature.nix {
inherit clanLib prefix; inherit clanLib prefix;
}) })
] ]

View File

@@ -13,18 +13,16 @@ in
let let
# Common filtered source for inventory tests # Common filtered source for inventory tests
inventoryTestsSrc = lib.fileset.toSource { inventoryTestsSrc = lib.fileset.toSource {
root = ../../..; root = ../../../..;
fileset = lib.fileset.unions [ fileset = lib.fileset.unions [
../../../flake.nix ../../../../flake.nix
../../../flake.lock ../../../../flake.lock
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../..) (lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../../..)
../../../flakeModules ../../../../flakeModules
../../../lib ../../../../lib
../../../nixosModules/clanCore ../../../../nixosModules/clanCore
../../../nixosModules/machineModules ../../../../machines
../../../machines ../../../../inventory.json
../../../inventory.json
../../../modules
]; ];
}; };
in in

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

@@ -81,7 +81,6 @@ let
applySettings = applySettings =
instanceName: instance: instanceName: instance:
lib.mapAttrs (roleName: role: { lib.mapAttrs (roleName: role: {
settings = config.instances.${instanceName}.roles.${roleName}.finalSettings.config;
machines = lib.mapAttrs (machineName: _v: { machines = lib.mapAttrs (machineName: _v: {
settings = settings =
config.instances.${instanceName}.roles.${roleName}.machines.${machineName}.finalSettings.config; config.instances.${instanceName}.roles.${roleName}.machines.${machineName}.finalSettings.config;
@@ -159,29 +158,6 @@ in
( (
{ name, ... }@role: { name, ... }@role:
{ {
options.finalSettings = mkOption {
default = evalMachineSettings instance.name role.name null role.config.settings { };
type = types.raw;
description = ''
Final evaluated settings of the curent-machine
This contains the merged and evaluated settings of the role interface,
the role settings and the machine settings.
Type: 'configuration' as returned by 'lib.evalModules'
'';
apply = lib.warn ''
=== WANRING ===
'roles.<roleName>.settings' do not contain machine specific settings.
Prefer `machines.<machineName>.settings` instead. (i.e `perInstance: roles.<roleName>.machines.<machineName>.settings`)
If you have a use-case that requires access to the original role settings without machine overrides.
Contact us via matrix (https://matrix.to/#/#clan:clan.lol) or file an issue: https://git.clan.lol
This feature will be removed in the next release
'';
};
# instances.{instanceName}.roles.{roleName}.machines # instances.{instanceName}.roles.{roleName}.machines
options.machines = mkOption { options.machines = mkOption {
description = '' description = ''
@@ -240,7 +216,7 @@ in
options.extraModules = lib.mkOption { options.extraModules = lib.mkOption {
default = [ ]; default = [ ];
type = types.listOf types.deferredModule; type = types.listOf (types.either types.deferredModule types.str);
}; };
} }
) )
@@ -504,12 +480,9 @@ in
staticModules = [ staticModules = [
({ ({
options.exports = mkOption { options.exports = mkOption {
type = types.lazyAttrsOf types.deferredModule; type = types.deferredModule;
default = { }; default = { };
description = '' description = ''
!!! Danger "Experimental Feature"
This feature is experimental and will change in the future.
export modules defined in 'perInstance' export modules defined in 'perInstance'
mapped to their instance name mapped to their instance name
@@ -634,21 +607,10 @@ 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"
This feature is experimental and will change in the future.
export modules defined in 'perMachine' export modules defined in 'perMachine'
mapped to their machine name mapped to their machine name
@@ -750,9 +712,6 @@ in
exports = mkOption { exports = mkOption {
description = '' description = ''
!!! Danger "Experimental Feature"
This feature is experimental and will change in the future.
This services exports. This services exports.
Gets merged with all other services exports. Gets merged with all other services exports.
@@ -775,38 +734,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
@@ -850,11 +850,7 @@ in
instanceRes.nixosModule instanceRes.nixosModule
] ]
++ (map ( ++ (map (
s: s: if builtins.typeOf s == "string" then "${directory}/${s}" else s
if builtins.typeOf s == "string" then
lib.warn "String types for 'extraModules' will be deprecated - ${s}" "${directory}/${s}"
else
lib.setDefaultModuleLocation "via inventory.instances.${instanceName}.roles.${roleName}" s
) instanceCfg.roles.${roleName}.extraModules); ) instanceCfg.roles.${roleName}.extraModules);
}; };
} }
@@ -991,39 +987,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";
@@ -142,7 +137,6 @@ in
settings = { }; settings = { };
}; };
}; };
settings = { };
}; };
peer = { peer = {
machines = { machines = {
@@ -152,9 +146,6 @@ in
}; };
}; };
}; };
settings = {
timeout = "foo-peer";
};
}; };
}; };
settings = { settings = {
@@ -165,9 +156,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,25 +100,20 @@ 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 = rec {
hasMachineSettings = true; hasMachineSettings = true;
hasRoleSettings = true; hasRoleSettings = false;
specificMachineSettings = { specificMachineSettings = {
timeout = "foo-peer-jon"; timeout = "foo-peer-jon";
}; };
specificRoleSettings = { specificRoleSettings = {
machines = { machines = {
jon = { jon = {
settings = { settings = specificMachineSettings;
timeout = "foo-peer-jon";
};
}; };
}; };
settings = {
timeout = "foo-peer";
};
}; };
}; };
}; };

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

@@ -0,0 +1,5 @@
{
imports = [
./interface.nix
];
}

View File

@@ -0,0 +1,28 @@
{ lib, ... }:
let
inherit (lib) types mkOption;
submodule = m: types.submoduleWith { modules = [ m ]; };
in
{
options = {
directory = mkOption {
type = types.path;
};
distributedServices = mkOption {
type = types.raw;
};
inventory = mkOption {
type = types.raw;
};
machines = mkOption {
type = types.attrsOf (submodule ({
options = {
machineImports = mkOption {
type = types.listOf types.raw;
};
};
}));
};
};
}

View File

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

View File

@@ -115,7 +115,7 @@ in
meta = lib.mkOption { meta = lib.mkOption {
type = lib.types.submoduleWith { type = lib.types.submoduleWith {
modules = [ modules = [
./meta.nix ./meta-interface.nix
]; ];
}; };
}; };
@@ -167,7 +167,7 @@ in
''; '';
type = types.submoduleWith { type = types.submoduleWith {
specialArgs = { specialArgs = {
inherit (config) machines clanLib; inherit (config) machines;
}; };
modules = [ modules = [
{ {
@@ -359,7 +359,7 @@ in
inherit clanLib; inherit clanLib;
}; };
} }
(import ./role.nix { }) (import ./roles-interface.nix { })
]; ];
} }
); );

View File

@@ -31,20 +31,6 @@ let
Under construction, will be used for the UI Under construction, will be used for the UI
''; '';
}; };
tld = lib.mkOption {
type = types.strMatching "[a-z]+";
default = "clan";
example = "ccc";
description = ''
Top level domain (TLD) of the clan. It should be set to a valid, but
not already existing TLD.
It will be used to provide clan-internal services and resolve each host of the
clan with:
<hostname>.<tld>
'';
};
}; };
in in
{ {

View File

@@ -44,6 +44,12 @@ in
description = '' description = ''
List of additionally imported `.nix` expressions. List of additionally imported `.nix` expressions.
Supported types:
- **Strings**: Interpreted relative to the 'directory' passed to `lib.clan`.
- **Paths**: should be relative to the current file.
- **Any**: Nix expression must be serializable to JSON.
!!! Note !!! Note
**The import only happens if the machine is part of the service or role.** **The import only happens if the machine is part of the service or role.**
@@ -67,8 +73,15 @@ in
} }
``` ```
''; '';
apply = value: if lib.isString value then value else builtins.seq (builtins.toJSON value) value;
default = [ ]; default = [ ];
type = types.listOf types.raw; type = types.listOf (
types.oneOf [
types.str
types.path
(types.attrsOf types.anything)
]
);
}; };
}; };
} }

View File

@@ -0,0 +1,70 @@
{
flakeInputs,
clanLib,
}:
{ lib, config, ... }:
let
inspectModule =
inputName: moduleName: module:
let
eval = clanLib.evalService {
modules = [ module ];
prefix = [
inputName
"clan"
"modules"
moduleName
];
};
in
{
manifest = eval.config.manifest;
roles = lib.mapAttrs (_n: v: { inherit (v) description; }) eval.config.roles;
};
in
{
options.staticModules = lib.mkOption {
readOnly = true;
type = lib.types.raw;
apply = moduleSet: lib.mapAttrs (inspectModule "<clan-core>") moduleSet;
};
options.modulesPerSource = lib.mkOption {
# { sourceName :: { moduleName :: {} }}
readOnly = true;
type = lib.types.raw;
default =
let
inputsWithModules = lib.filterAttrs (_inputName: v: v ? clan.modules) flakeInputs;
in
lib.mapAttrs (
inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules
) inputsWithModules;
};
options.moduleSchemas = lib.mkOption {
# { sourceName :: { moduleName :: { roleName :: Schema }}}
readOnly = true;
type = lib.types.raw;
default = lib.mapAttrs (
_inputName: moduleSet:
lib.mapAttrs (
_moduleName: module:
(clanLib.evalService {
modules = [ module ];
prefix = [ ];
}).config.result.api.schema
) moduleSet
) config.modulesPerSource;
};
options.templatesPerSource = lib.mkOption {
# { sourceName :: { moduleName :: {} }}
readOnly = true;
type = lib.types.raw;
default =
let
inputsWithTemplates = lib.filterAttrs (_inputName: v: v ? clan.templates) flakeInputs;
in
lib.mapAttrs (_inputName: v: lib.mapAttrs (_n: t: t) v.clan.templates) inputsWithTemplates;
};
}

View File

@@ -3,7 +3,6 @@
directory, directory,
meta, meta,
}: }:
# The following is a nixos/darwin module
{ {
_class, _class,
lib, lib,
@@ -21,7 +20,7 @@
); );
clan.core.settings = { clan.core.settings = {
inherit (meta) name icon tld; inherit (meta) name icon;
inherit directory; inherit directory;
machine = { machine = {
inherit name; inherit name;

View File

@@ -81,7 +81,6 @@ in
description = null; description = null;
icon = null; icon = null;
name = "test"; name = "test";
tld = "clan";
}; };
}; };
@@ -106,7 +105,7 @@ in
self = { self = {
inputs = { }; inputs = { };
}; };
directory = ../.; directory = ../../.;
meta.name = "test-clan-core"; meta.name = "test-clan-core";
}; };
in in
@@ -124,7 +123,7 @@ in
self = { self = {
inputs = { }; inputs = { };
}; };
directory = ../.; directory = ../../.;
meta.name = "test-clan-core"; meta.name = "test-clan-core";
}; };
in in
@@ -212,87 +211,6 @@ in
}; };
}; };
test_clan_check_simple_fail =
let
eval = clan {
checks.constFail = {
assertion = false;
message = "This is a constant failure";
};
};
in
{
result = eval;
expr = eval.config;
expectedError.type = "ThrownError";
expectedError.msg = "This is a constant failure";
};
test_clan_check_simple_pass =
let
eval = clan {
checks.constFail = {
assertion = true;
message = "This is a constant success";
};
};
in
{
result = eval;
expr = lib.seq eval.config 42;
expected = 42;
};
test_get_var_machine =
let
varsLib = import ./vars.nix { };
in
{
expr = varsLib.getPublicValue {
backend = "in_repo";
default = "test";
shared = false;
generator = "test-generator";
machine = "test-machine";
file = "test-file";
flake = ./vars-test-flake;
};
expected = "foo-machine";
};
test_get_var_shared =
let
varsLib = import ./vars.nix { };
in
{
expr = varsLib.getPublicValue {
backend = "in_repo";
default = "test";
shared = true;
generator = "test-generator";
machine = "test-machine";
file = "test-file";
flake = ./vars-test-flake;
};
expected = "foo-shared";
};
test_get_var_default =
let
varsLib = import ./vars.nix { };
in
{
expr = varsLib.getPublicValue {
backend = "in_repo";
default = "test-default";
shared = true;
generator = "test-generator-wrong";
machine = "test-machine";
file = "test-file";
flake = ./vars-test-flake;
};
expected = "test-default";
};
test_clan_all_machines_laziness = test_clan_all_machines_laziness =
let let
eval = clan { eval = clan {

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

@@ -62,9 +62,6 @@
# Core libraries # Core libraries
(root + "/lib") (root + "/lib")
# modules directory
(root + "/modules")
# User-provided fileset # User-provided fileset
fileset fileset
]; ];

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

@@ -1,25 +0,0 @@
_: {
getPublicValue =
{
backend ? "in_repo",
default ? throw "getPublicValue: Public value ${machine}/${generator}/${file} not found!",
shared ? false,
generator,
machine,
file,
flake,
}:
if backend == "in_repo" then
let
path =
if shared then
"${flake}/vars/shared/${generator}/${file}/value"
else
"${flake}/vars/per-machine/${machine}/${generator}/${file}/value";
in
if builtins.pathExists path then builtins.readFile path else default
else
throw "backend ${backend} does not implement getPublicValue";
}

View File

@@ -1,3 +0,0 @@
{
}

Some files were not shown because too many files have changed in this diff Show More