Compare commits
169 Commits
wireguard
...
push-lxuyo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6531ca69a | ||
|
|
b6b065e365 | ||
|
|
4b1955b189 | ||
|
|
ef7ef8b843 | ||
|
|
38c1367322 | ||
|
|
8e72c086fd | ||
|
|
c454b1339d | ||
|
|
d1b2d43e5b | ||
|
|
da98ca0f1c | ||
|
|
1953540d08 | ||
|
|
be31b9ce21 | ||
|
|
169b4016e6 | ||
|
|
2e55028a1b | ||
|
|
1d228231f2 | ||
|
|
affb926450 | ||
|
|
c7f65e929f | ||
|
|
ba4ff493e8 | ||
|
|
eb08803e2a | ||
|
|
bbc9486f0e | ||
|
|
999d709350 | ||
|
|
0b1a330cc2 | ||
|
|
995b7cf50d | ||
|
|
5477b13233 | ||
|
|
d6170e5efb | ||
|
|
18fe117363 | ||
|
|
33a868acc2 | ||
|
|
11372d35e1 | ||
|
|
b7508b2b43 | ||
|
|
183817b769 | ||
|
|
591e53e9be | ||
|
|
a6a6415e31 | ||
|
|
0060ead876 | ||
|
|
224e41d3ad | ||
|
|
b3323007b2 | ||
|
|
3e950bc66f | ||
|
|
9503b46b21 | ||
|
|
a2cec323a2 | ||
|
|
4239f4d27f | ||
|
|
8ac8264997 | ||
|
|
544a53ae9c | ||
|
|
89e18482ed | ||
|
|
a8217b5a32 | ||
|
|
bdd5de5628 | ||
|
|
61d8bfd0d1 | ||
|
|
b8d79c7fc2 | ||
|
|
fb25ab028b | ||
|
|
5b136ecaf0 | ||
|
|
d4733dbb0a | ||
|
|
bfb30251e6 | ||
|
|
33115f76b7 | ||
|
|
9e9208e699 | ||
|
|
6b3fd57174 | ||
|
|
3be5237cf6 | ||
|
|
368f80eaae | ||
|
|
4d7079534c | ||
|
|
7d4cf1c551 | ||
|
|
ea088b95e9 | ||
|
|
a7a37f5320 | ||
|
|
8bda4880a7 | ||
|
|
6eb83618c0 | ||
|
|
1fe3833779 | ||
|
|
e63f5c966e | ||
|
|
69241183ac | ||
|
|
0a7c65cd27 | ||
|
|
bd13eb3e23 | ||
|
|
1e5191a16c | ||
|
|
1e4bf0dd4e | ||
|
|
4d66dc59aa | ||
|
|
9a442c15e9 | ||
|
|
157af90a56 | ||
|
|
24b94965d8 | ||
|
|
46bcad9267 | ||
|
|
1aba0577dc | ||
|
|
383088af2d | ||
|
|
c3456c1f0c | ||
|
|
183de9209f | ||
|
|
1df5c5ff60 | ||
|
|
e55a3b4fc6 | ||
|
|
6ee4657da3 | ||
|
|
7294d8bcbe | ||
|
|
3fec5aa5b3 | ||
|
|
63e55b8631 | ||
|
|
dd771f8dd9 | ||
|
|
d31a4cc7d8 | ||
|
|
73d4cf51af | ||
|
|
d015218226 | ||
|
|
f50475fcfd | ||
|
|
ae5efd9e2f | ||
|
|
c2c2874e82 | ||
|
|
1f8c2a3722 | ||
|
|
50aa7eb0cf | ||
|
|
920b39a124 | ||
|
|
c159ef79d2 | ||
|
|
7a95b169c1 | ||
|
|
0c3fd40120 | ||
|
|
7268697dc5 | ||
|
|
ec395bada4 | ||
|
|
757552671c | ||
|
|
04b52d5bee | ||
|
|
ffeb8b892a | ||
|
|
26806b5750 | ||
|
|
6e70054566 | ||
|
|
93268e8592 | ||
|
|
a420c6ca25 | ||
|
|
69fd13a76f | ||
|
|
ab3f262c22 | ||
|
|
aabbe0dfac | ||
|
|
35cb99a3a2 | ||
|
|
90e6d77e26 | ||
|
|
5fb4751bd8 | ||
|
|
03640e44a4 | ||
|
|
51fd60917e | ||
|
|
2d7e659953 | ||
|
|
c638df8ed9 | ||
|
|
ec269a48f3 | ||
|
|
fc4c9287cb | ||
|
|
fee62373a9 | ||
|
|
f075b339b5 | ||
|
|
872a622f71 | ||
|
|
4a41c4cefb | ||
|
|
a5cd36e845 | ||
|
|
0dd6c08e33 | ||
|
|
dc0b7fc3bf | ||
|
|
0d4bbbd17e | ||
|
|
90797ffa7d | ||
|
|
7f2bd809d6 | ||
|
|
da7ff9a40a | ||
|
|
410d0d0532 | ||
|
|
a25d983c87 | ||
|
|
3953fa4047 | ||
|
|
ea93cb9987 | ||
|
|
c13278f3c2 | ||
|
|
66fdf937e3 | ||
|
|
843f55f844 | ||
|
|
5a5633d779 | ||
|
|
8310433342 | ||
|
|
51141772b3 | ||
|
|
58b88e874f | ||
|
|
5fb616efb4 | ||
|
|
6c6afd6f4b | ||
|
|
4010953041 | ||
|
|
6b74c66292 | ||
|
|
fd35adbc3e | ||
|
|
f86b0ec3da | ||
|
|
5f6e0540cd | ||
|
|
c7ec9a9715 | ||
|
|
841e9135fe | ||
|
|
9299cd9666 | ||
|
|
9851993b82 | ||
|
|
55d1807f07 | ||
|
|
ee0abdc7f4 | ||
|
|
6c9ab63842 | ||
|
|
d70db5af79 | ||
|
|
eae858dec6 | ||
|
|
2ec035a1cb | ||
|
|
bd6c227bbe | ||
|
|
02f3474a58 | ||
|
|
c838e08d77 | ||
|
|
346e3d816a | ||
|
|
a15959fad2 | ||
|
|
0e0c2ead1f | ||
|
|
c42381d810 | ||
|
|
587ce7258a | ||
|
|
d0bb804843 | ||
|
|
410eecb988 | ||
|
|
98136142b4 | ||
|
|
37da9fb3e4 | ||
|
|
4566ad9789 | ||
|
|
6faacc7dde |
@@ -1,8 +1,10 @@
|
||||
clanServices/.* @pinpox @kenji
|
||||
|
||||
lib/test/container-test-driver/.* @DavHau @mic92
|
||||
lib/modules/inventory/.* @hsjobeki
|
||||
lib/modules/inventoryClass/.* @hsjobeki
|
||||
lib/inventory/.* @hsjobeki
|
||||
lib/inventoryClass/.* @hsjobeki
|
||||
|
||||
modules/.* @hsjobeki
|
||||
|
||||
pkgs/clan-app/ui/.* @hsjobeki @brianmcgee
|
||||
pkgs/clan-app/clan_app/.* @qubasa @hsjobeki
|
||||
|
||||
@@ -87,6 +87,7 @@ in
|
||||
# Container Tests
|
||||
nixos-test-container = self.clanLib.test.containerTest ./container 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-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs;
|
||||
nixos-test-extra-python-packages = self.clanLib.test.containerTest ./test-extra-python-packages nixosTestArgs;
|
||||
|
||||
82
checks/llm/default.nix
Normal file
82
checks/llm/default.nix
Normal file
@@ -0,0 +1,82 @@
|
||||
{ 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)
|
||||
'';
|
||||
}
|
||||
70
checks/llm/qwen3-4b-instruct.nix
Normal file
70
checks/llm/qwen3-4b-instruct.nix
Normal file
@@ -0,0 +1,70 @@
|
||||
{ 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;
|
||||
};
|
||||
}
|
||||
@@ -62,6 +62,6 @@ in
|
||||
peer1.succeed("chmod 640 /var/log/journal/*/user-1000.journal*")
|
||||
|
||||
# Run tests as text-user (environment variables are set automatically)
|
||||
peer1.succeed("su - text-user -c 'pytest -s -n0 ${cli}/${cli.pythonRuntime.sitePackages}/clan_lib/service_runner'")
|
||||
peer1.succeed("su - text-user -c 'pytest -p no:cacheprovider -o addopts="" -s -n0 ${cli.passthru.sourceWithTests}/clan_lib/service_runner'")
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{ ... }:
|
||||
{
|
||||
clanLib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
sharedInterface =
|
||||
{ lib, ... }:
|
||||
@@ -51,15 +54,15 @@ let
|
||||
builtins.foldl' (
|
||||
urls: name:
|
||||
let
|
||||
ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value";
|
||||
ip = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "zerotier";
|
||||
file = "zerotier-ip";
|
||||
default = null;
|
||||
};
|
||||
in
|
||||
if builtins.pathExists ipPath then
|
||||
let
|
||||
ip = builtins.readFile ipPath;
|
||||
in
|
||||
urls ++ [ "[${ip}]:${builtins.toString settings.network.port}" ]
|
||||
else
|
||||
urls
|
||||
if ip != null then urls ++ [ "[${ip}]:${builtins.toString settings.network.port}" ] else urls
|
||||
) [ ] (builtins.attrNames ((roles.admin.machines or { }) // (roles.signer.machines or { })))
|
||||
);
|
||||
|
||||
@@ -156,9 +159,14 @@ in
|
||||
readHostKey =
|
||||
machine:
|
||||
let
|
||||
path = "${config.clan.core.settings.directory}/vars/per-machine/${machine}/data-mesher-host-key/public_key/value";
|
||||
publicKey = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
inherit machine;
|
||||
generator = "data-mesher-host-key";
|
||||
file = "public_key";
|
||||
};
|
||||
in
|
||||
builtins.elemAt (lib.splitString "\n" (builtins.readFile path)) 1;
|
||||
builtins.elemAt (lib.splitString "\n" publicKey) 1;
|
||||
in
|
||||
{
|
||||
enable = true;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
options = {
|
||||
host = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
ip address or hostname (domain) of the machine
|
||||
'';
|
||||
|
||||
@@ -44,8 +44,10 @@
|
||||
pkgs.openssl
|
||||
];
|
||||
|
||||
# TODO: Implement automated certificate rotation instead of using a 100-year expiration
|
||||
script = ''
|
||||
openssl req -x509 -nodes -newkey rsa:4096 \
|
||||
-days 36500 \
|
||||
-keyout "$out"/key \
|
||||
-out "$out"/crt \
|
||||
-subj "/C=US/ST=CA/L=San Francisco/O=Example Corp/OU=IT/CN=example.com"
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
k1PNWTkgLsXO8DNkCudxcvPElXfmaw6zwaLrZys=
|
||||
VQQDDAtleGFtcGxlLmNvbTAgFw0yNTEwMjExMzE3MTZaGA8yMTI1MDkyNzEzMTcx
|
||||
NlowbDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh
|
||||
bmNpc2NvMRUwEwYDVQQKDAxFeGFtcGxlIENvcnAxCzAJBgNVBAsMAklUMRQwEgYD
|
||||
VQQDDAtleGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
|
||||
AMbUCTs38JdEFlz+fiEwsEb9OV+6u4P5pkKkRFIJ04sTW9/NIeUJx5xOcAPn6B8K
|
||||
mi+d6vHln2WDCNJHqthGHQDS250x8Qs+JrmtIvDPko+oDOlbWMPiT4Lv6p134+lV
|
||||
obkiEMKSKz1gHuhlnHXFjkU+xTjxvEtGuq1+JPem4oJ9HUhSk1F6cftigzrYqUuk
|
||||
JRROiUrbKiFp/TLedmAqQg/7wOrJKSKX91pQwNZhjB2/1REt0HP92W8uZIrzvLqq
|
||||
JkrGfK9Y6e87DwXoTT0lvMAT7jbMsMWdGoCw/BQV8CwciUUG4ggI/jb+2TTktB3f
|
||||
kMN/qRTKZ3zv/rn68RJfecAXYCQ2VfvO/Mr9nml2/cM7nrUBcs12YAHcm3766VWJ
|
||||
pq6qBLcz/pHzMdt+/23nbO7bH2PL6r69VCSYvsDDnqpVL+LnYhgYUE0lPjuWuGmp
|
||||
oKjggS6p4p1PXEQMOcj9UWdOyjefSzJsOp+25Of9SQzxHkBsVw0iArRFUYP6G15k
|
||||
kNjYpuinFTw1XVDCFGPRIAhySnERlkv6WNyQQC87QTVJITKkz3R5cv4gwFG0kjAi
|
||||
Va4nIJs2CctcizuEaPlwnEFrZ99gcB7RYPSUQVGAbfkqt2bhy/xGr+Jlp4kqPfS5
|
||||
iPomwfcDwEnDbmcM8S2adPWtZ+oHskxZQmJ6+jhGgM73AgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBRHz2QAo1z8r9BewZro+HYv18AxTzAfBgNVHSMEGDAWgBRHz2QAo1z8r9Be
|
||||
wZro+HYv18AxTzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCz
|
||||
BTuZI7VymDWerWLfHMWyogoJWOkFB2yEpQe7J+LjS8yZmJg4CYpA4JJ+uM2sBm2Q
|
||||
yL6M57ZmSY6EFoYeYw3gRfwGC32qJHirhsWvrjUpRC5+4YT9P6fNmgm5aD27JZao
|
||||
bjyNA9Vy9SCL4JMeWET2w9VGNDaYQCs0x57HZioxYRMSD5vMVbirvCtqX7H3F/X+
|
||||
r/VHEqEae7tVtuAB2D2GdcFzslCRb9uomuVfLJNqR6Nz1Tw+2adyySijRMCDdpRl
|
||||
Pg9MBv4sevL6F4C1vUqUG1LXzcfHLFtrV1oUIEpJ0frxAgpdhSbnHiQa64cKX3N0
|
||||
CsS6VALipGFmxj01+jD0Vhhf4rjjTT5C3Ag4WTqI98Fu4RMW35eBstnt6UUWyJQO
|
||||
Q1skk+hg0ynfb3lO8OIZ4sDkmxDqAOQXeMMo1tU2YMgNA5Lv1FyO9Silc0VlkOiO
|
||||
ft1RC8UbECqYyTvz7SNrv8aQP6EUoNSpxQHyBHOQy65dyOLOdP4S+PccUwsdxv/N
|
||||
O5eN9ndMWqNvnyPKyQ3M+MLVvkCR1vDb6ABgPhH17BLkj8fWQgy5lhjJy5a8VHlO
|
||||
1VDzV1Xeezy/MYCpS+TamaWTXscbhLMzWWiiAiDT8dltKw4G6U+g7DiF80kM59L5
|
||||
D1hOs4gOQ853+83L/Ej4ESTj0B04NLVMlzMGtl3qcA==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data: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,iv:q3j8EAokyyxiszf+wyRqxEr2igaD1bX7YnFx/NbsGg8=,tag:HKKYWRJEUwW2/TxL+5dSng==,type:str]",
|
||||
"data": "ENC[AES256_GCM,data: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,iv:dGSmyDNBdVyF54bYS/Zxm2NNXZyGtLjkyYlrI9/nKvc=,tag:ip2fy76NjObWbW20HyuZUA==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaeXRjU214aWk5ajl1aW9E\naGJlb1ViaVRmMTBHdkFDQUNDZS94WFZiNUNvCllmWTJBck9hR3U3V09VWDZwQ2xI\nd3ZEQnBIUG5ZSTVIdS8rQ2FMYVhyNk0KLS0tIEE1UG8rSzFyU01sVXhGVHpoaE9i\nSis4Qi9tMGFqbTNMTDZUVk1ZdXkrM28Km4VkfaOsZ69ckjvrg+os43H/O1IoWHzC\nt4LqZRz1Tk7/d1aLWavSPPjVYrCOMZeNBqGbQpGfjjuXrafClRNQdQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNQS96MUFubVdOR2VCc2xO\ncTc5QnNHNTFpdURnSnF3dVhBQXQ3bnBuRW1RCngzSVlhSW9rNUxoSWdKcEtKVXc3\nQitLZ2NDUXBSUmxtVWpYRUlvOHVXcW8KLS0tIGZaWlRVak9NYmt2elpwYStYenRE\nanlkT3BET1FjQ2lFZkp3SXFMSkJSaVkKKkr+MNNqs6Ve3K5OrZfBEGlnc7OAthqf\nOZrP9NYOTMgkvhFsZTVpUS0zskry0iwmTNt+KeluYf0Tko8K53Kx2A==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3R1RHTGViTnRLVVkyM3J0\nbm96cGVPTlo4NXBNL0g1eEVSNG9DUkgwVFRBCmRKVTlMRmV3Tmg2RTZIclBlWlcr\ndzI5MUxhcllzbE1IMDNxa08zVkpITmsKLS0tIG01Y2dyQkY3UmRudFk2d0p6bThn\nemlaWnZoS3p4VHhMTFFwTm9VN0ttYzQKVbLFgtK6NIRIiryWHeeOPD45iwUds4QD\n7b8xYYoxlo+DETggxK6Vz3IdT/BSK5bFtgAxl864b5gW+Aw4c6AO5w==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXd2dVYmJIbUVVeXk5Nk1E\nekFiUldVVUhRTmE4dHRiTHNDdEMyS1pRV1RrCkNScGdXVSs4UU5id29DV0pZWDQr\nenV1QmpnOFk5aFpTTUxmb0hDVHZDdFkKLS0tIHpmalJtRC94bEhaUStmeUlHT21w\nd3o3UzJHZklxK0RCYUUxc2c3aG1XclkKEPq1ZgyGiAK/Hy4zT7wfdDfPEE3vMHpR\nzwQV5y3M3DmlnKQEvJu0DpQ334CyAcubZC7cswQdUrM8TPqJhb/TuA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-09-18T14:33:37Z",
|
||||
"mac": "ENC[AES256_GCM,data:XKCnd0QrAlOCECSeSvbLYHMLbmUh4fMRnLaTb5ARoP4Zc9joWGsCaRZxokc2/sG4BXA/6pkbQXHyIOudKbcBpVjjvs9E+6Mnzt53nfRoH/iOkYPbN2EO49okVZJXW0M1rlBxrxvGuiDlz2p2p6L7neKLy4EB482pYea5+dUr2Yw=,iv:oj/MkZCfkvCmAb79uzEvKwEAm1bKtWhS4rPRAWSgRgw=,tag:h5TPPILXkhJplnDT2Gqtfw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
"lastmodified": "2025-10-21T13:17:17Z",
|
||||
"mac": "ENC[AES256_GCM,data:wdAFURkJZvclbz3UFPSPV9fma7zrZVEhMhsRqylGQMLepX/WohEAr8nJgeHl05be1Q8M8biPXCCoL0vfwg4BRZOkhD8PusJh8iBI3+STNQe/S1qoIK1ByfBFhJD+tIsVsgduLp6G32e6SRNvkuX3UpJqyViuRUavfQd3b8LRU4I=,iv:S3sMNTz5Kg4TxHj1tnk/ayiFuO74dR4aPnnomtkGByo=,tag:uive2bYe42s6VtPd03jTMw==,type:str]",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{ ... }:
|
||||
{
|
||||
lib,
|
||||
clanLib,
|
||||
directory,
|
||||
...
|
||||
}:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/mycelium";
|
||||
@@ -30,8 +35,24 @@
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{ settings, ... }:
|
||||
{
|
||||
settings,
|
||||
roles,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
exports.networking = {
|
||||
peers = lib.mapAttrs (name: _machine: {
|
||||
host.plain = clanLib.vars.getPublicValue {
|
||||
machine = name;
|
||||
generator = "mycelium";
|
||||
file = "ip";
|
||||
flake = directory;
|
||||
};
|
||||
}) roles.peer.machines;
|
||||
};
|
||||
|
||||
nixosModule =
|
||||
{
|
||||
config,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{ settings, ... }:
|
||||
{ settings, roles, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
@@ -38,8 +38,19 @@
|
||||
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 (settings.certificate.searchDomains != [ ]) {
|
||||
clan.core.vars.generators.openssh-ca = lib.mkIf (searchDomains != [ ]) {
|
||||
share = true;
|
||||
files.id_ed25519.deploy = false;
|
||||
files."id_ed25519.pub" = {
|
||||
@@ -54,9 +65,9 @@
|
||||
'';
|
||||
};
|
||||
|
||||
programs.ssh.knownHosts.ssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) {
|
||||
programs.ssh.knownHosts.ssh-ca = lib.mkIf (searchDomains != [ ]) {
|
||||
certAuthority = true;
|
||||
extraHostNames = builtins.map (domain: "*.${domain}") settings.certificate.searchDomains;
|
||||
extraHostNames = builtins.map (domain: "*.${domain}") searchDomains;
|
||||
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ in
|
||||
../../clanServices/syncthing
|
||||
# Required modules
|
||||
../../nixosModules/clanCore
|
||||
../../nixosModules/machineModules
|
||||
# Dependencies like clan-cli
|
||||
../../pkgs/clan-cli
|
||||
];
|
||||
|
||||
@@ -41,14 +41,14 @@ let
|
||||
# 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
|
||||
# evaluatedService =
|
||||
# testFlake.clanInternals.inventoryClass.distributedServices.importedModulesEvaluated.self-zerotier-redux.config;
|
||||
# testFlake.clanInternals.inventoryClass.distributedServices.servicesEval.config.mappedServices.self-zerotier-redux.config;
|
||||
in
|
||||
{
|
||||
test_simple = {
|
||||
inherit testFlake;
|
||||
|
||||
expr =
|
||||
testFlake.config.clan.clanInternals.inventoryClass.distributedServices.importedModulesEvaluated.self-wifi.config;
|
||||
testFlake.config.clan.clanInternals.inventoryClass.distributedServices.servicesEval.config.mappedServices.self-wifi.config;
|
||||
expected = 1;
|
||||
|
||||
# expr = {
|
||||
|
||||
@@ -133,75 +133,6 @@ graph TB
|
||||
|
||||
### Advanced Options
|
||||
|
||||
#### External Peers
|
||||
|
||||
External peers are devices outside of your clan (like phones, laptops, etc.) that can connect to the mesh network through controllers. Each external peer gets its own keypair and can be configured with specific options.
|
||||
|
||||
##### IPv6-only external peers
|
||||
|
||||
```nix
|
||||
{
|
||||
instances = {
|
||||
wireguard = {
|
||||
module.name = "wireguard";
|
||||
module.input = "clan-core";
|
||||
roles.controller.machines.server1.settings = {
|
||||
endpoint = "vpn.example.com";
|
||||
# Define external peers with configuration options
|
||||
externalPeers = {
|
||||
dave = {
|
||||
# No internet access - can only reach clan mesh
|
||||
allowInternetAccess = false;
|
||||
};
|
||||
moms-phone = {
|
||||
# Internet access enabled - IPv6 traffic routed through VPN
|
||||
allowInternetAccess = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
roles.peer.machines.laptop1 = {};
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### IPv4 support for external peers
|
||||
|
||||
If you need IPv4 internet access for external peers, you can enable IPv4 on the controller and assign IPv4 addresses to external peers:
|
||||
|
||||
```nix
|
||||
{
|
||||
instances = {
|
||||
wireguard = {
|
||||
module.name = "wireguard";
|
||||
module.input = "clan-core";
|
||||
roles.controller.machines.server1.settings = {
|
||||
endpoint = "vpn.example.com";
|
||||
# Enable IPv4 with controller's address
|
||||
ipv4.enable = true;
|
||||
ipv4.address = "10.42.1.1/24";
|
||||
externalPeers = {
|
||||
dave = {
|
||||
# No internet access - can only reach clan mesh
|
||||
allowInternetAccess = false;
|
||||
ipv4.address = "10.42.1.50/32";
|
||||
};
|
||||
moms-phone = {
|
||||
# Internet access enabled - IPv4 and IPv6 traffic routed through VPN
|
||||
allowInternetAccess = true;
|
||||
ipv4.address = "10.42.1.51/32";
|
||||
};
|
||||
};
|
||||
};
|
||||
roles.peer.machines.laptop1 = {};
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** IPv4 addresses for external peers are only used for internet access through the controller, not for mesh communication (which uses IPv6).
|
||||
|
||||
External peers can connect to multiple controllers by adding the same peer name to multiple controllers' `externalPeers` configuration.
|
||||
|
||||
### Automatic Hostname Resolution
|
||||
|
||||
|
||||
@@ -54,7 +54,10 @@
|
||||
- For other controllers: The controller's /56 subnet
|
||||
*/
|
||||
|
||||
{ ... }:
|
||||
{
|
||||
clanLib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
# Shared module for extraHosts configuration
|
||||
extraHostsModule =
|
||||
@@ -74,10 +77,12 @@ let
|
||||
controllerHosts = lib.mapAttrsToList (
|
||||
name: _value:
|
||||
let
|
||||
prefix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value"
|
||||
);
|
||||
prefix = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "prefix";
|
||||
};
|
||||
# Controller IP is always ::1 in their subnet
|
||||
ip = prefix + "::1";
|
||||
in
|
||||
@@ -88,48 +93,30 @@ let
|
||||
peerHosts = lib.mapAttrsToList (
|
||||
peerName: peerValue:
|
||||
let
|
||||
peerSuffix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${peerName}/wireguard-network-${instanceName}/suffix/value"
|
||||
);
|
||||
peerSuffix = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = peerName;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "suffix";
|
||||
};
|
||||
# Determine designated controller
|
||||
designatedController =
|
||||
if (builtins.length (builtins.attrNames roles.controller.machines) == 1) then
|
||||
(builtins.head (builtins.attrNames roles.controller.machines))
|
||||
else
|
||||
peerValue.settings.controller;
|
||||
controllerPrefix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${designatedController}/wireguard-network-${instanceName}/prefix/value"
|
||||
);
|
||||
controllerPrefix = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = designatedController;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "prefix";
|
||||
};
|
||||
peerIP = controllerPrefix + ":" + peerSuffix;
|
||||
in
|
||||
"${peerIP} ${peerName}.${domain}"
|
||||
) roles.peer.machines or { };
|
||||
|
||||
# External peers
|
||||
externalPeerHosts = lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
ctrlName: _ctrlValue:
|
||||
lib.mapAttrsToList (
|
||||
peer: _peerSettings:
|
||||
let
|
||||
peerSuffix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/shared/wireguard-network-${instanceName}-external-peer-${peer}/suffix/value"
|
||||
);
|
||||
controllerPrefix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${ctrlName}/wireguard-network-${instanceName}/prefix/value"
|
||||
);
|
||||
peerIP = controllerPrefix + ":" + peerSuffix;
|
||||
) roles.peer.machines;
|
||||
in
|
||||
"${peerIP} ${peer}.${domain}"
|
||||
) (roles.controller.machines.${ctrlName}.settings.externalPeers)
|
||||
) roles.controller.machines
|
||||
);
|
||||
in
|
||||
builtins.concatStringsSep "\n" (controllerHosts ++ peerHosts ++ externalPeerHosts);
|
||||
builtins.concatStringsSep "\n" (controllerHosts ++ peerHosts);
|
||||
};
|
||||
|
||||
# Shared interface options
|
||||
@@ -242,10 +229,12 @@ in
|
||||
lib.mapAttrsToList (
|
||||
ctrlName: _:
|
||||
let
|
||||
controllerPrefix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${ctrlName}/wireguard-network-${instanceName}/prefix/value"
|
||||
);
|
||||
controllerPrefix = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = ctrlName;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "prefix";
|
||||
};
|
||||
peerIP = controllerPrefix + ":" + peerSuffix;
|
||||
in
|
||||
"${peerIP}/56"
|
||||
@@ -256,20 +245,22 @@ in
|
||||
|
||||
# Connect to all controllers
|
||||
peers = lib.mapAttrsToList (name: value: {
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
|
||||
)
|
||||
);
|
||||
publicKey = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-keys-${instanceName}";
|
||||
file = "publickey";
|
||||
};
|
||||
|
||||
# Allow each controller's /56 subnet
|
||||
allowedIPs = [
|
||||
"${
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value"
|
||||
)
|
||||
clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "prefix";
|
||||
}
|
||||
}::/56"
|
||||
];
|
||||
|
||||
@@ -290,90 +281,13 @@ in
|
||||
{
|
||||
imports = [ sharedInterface ];
|
||||
|
||||
options = {
|
||||
endpoint = lib.mkOption {
|
||||
options.endpoint = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "vpn.clan.lol";
|
||||
description = ''
|
||||
Endpoint where the controller can be reached
|
||||
'';
|
||||
};
|
||||
ipv4 = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable IPv4 support for external peers on this controller.
|
||||
When enabled, the controller will have an IPv4 address and can route IPv4 traffic.
|
||||
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
address = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "10.42.1.1/24";
|
||||
description = ''
|
||||
IPv4 address for this controller in CIDR notation.
|
||||
External peers with IPv4 addresses must be within the same subnet.
|
||||
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
};
|
||||
externalPeers = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
allowInternetAccess = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to allow this external peer to access the internet through the controller.
|
||||
When enabled, the controller will route internet traffic for this peer.
|
||||
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
ipv4.address = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "10.42.1.50/32";
|
||||
description = ''
|
||||
IPv4 address for this external peer in CIDR notation.
|
||||
The peer must be within the controller's IPv4 subnet.
|
||||
Only used when the controller has IPv4 enabled.
|
||||
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
example = {
|
||||
dave = {
|
||||
allowInternetAccess = false;
|
||||
};
|
||||
"moms-phone" = {
|
||||
allowInternetAccess = true;
|
||||
ipv4.address = "10.42.1.51/32";
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
External peers that are not part of the clan.
|
||||
|
||||
For every entry here, a key pair for an external device will be generated.
|
||||
This key pair can then be displayed via `clan vars get` and inserted into an external device, like a phone or laptop.
|
||||
|
||||
Each external peer can connect to the mesh through one or more controllers.
|
||||
To connect to multiple controllers, add the same peer name to multiple controllers' `externalPeers`, or simply set set `roles.controller.settings.externalPeers`.
|
||||
|
||||
The external peer names must not collide with machine names in the clan.
|
||||
The machines which are part of the clan will be able to resolve the external peers via their host names, but not vice versa.
|
||||
External peers can still reach machines from within the clan via their IPv6 addresses.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
perInstance =
|
||||
{
|
||||
@@ -395,37 +309,7 @@ in
|
||||
}:
|
||||
let
|
||||
allOtherControllers = lib.filterAttrs (name: _v: name != machine.name) roles.controller.machines;
|
||||
allPeers = roles.peer.machines or { };
|
||||
# Collect all external peers from all controllers
|
||||
allExternalPeers = lib.unique (
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (_: ctrl: lib.attrNames ctrl.settings.externalPeers) roles.controller.machines
|
||||
)
|
||||
);
|
||||
|
||||
controllerPrefix =
|
||||
controllerName:
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${controllerName}/wireguard-network-${instanceName}/prefix/value"
|
||||
);
|
||||
|
||||
peerSuffix =
|
||||
peerName:
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${peerName}/wireguard-network-${instanceName}/suffix/value"
|
||||
);
|
||||
|
||||
externalPeerSuffix =
|
||||
externalName:
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/shared/wireguard-network-${instanceName}-external-peer-${externalName}/suffix/value"
|
||||
);
|
||||
|
||||
thisControllerPrefix =
|
||||
config.clan.core.vars.generators."wireguard-network-${instanceName}".files.prefix.value;
|
||||
allPeers = roles.peer.machines;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
@@ -439,9 +323,8 @@ in
|
||||
;
|
||||
})
|
||||
];
|
||||
# Network prefix allocation generator for this controller
|
||||
clan.core.vars.generators = {
|
||||
"wireguard-network-${instanceName}" = {
|
||||
# Network allocation generator for this controller
|
||||
clan.core.vars.generators."wireguard-network-${instanceName}" = {
|
||||
files.prefix.secret = false;
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
@@ -455,156 +338,84 @@ in
|
||||
${pkgs.python3}/bin/python3 ${./ipv6_allocator.py} "$out" "${instanceName}" controller "${machine.name}"
|
||||
'';
|
||||
};
|
||||
}
|
||||
# For external peers, generate: suffix, public key, private key
|
||||
// lib.genAttrs' (lib.attrNames settings.externalPeers) (peer: {
|
||||
name = "wireguard-network-${instanceName}-external-peer-${peer}";
|
||||
value = {
|
||||
files.suffix.secret = false;
|
||||
files.publickey.secret = false;
|
||||
files.privatekey.secret = true;
|
||||
files.privatekey.deploy = false;
|
||||
|
||||
# The external peers keys are not deployed and are globally unique.
|
||||
# Even if an external peer is connected to more than one controller,
|
||||
# its private keys will remain the same.
|
||||
share = true;
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
python3
|
||||
wireguard-tools
|
||||
];
|
||||
|
||||
# Invalidate on hostname changes
|
||||
validation.hostname = peer;
|
||||
|
||||
script = ''
|
||||
${pkgs.python3}/bin/python3 ${./ipv6_allocator.py} "$out" "${instanceName}" peer "${peer}"
|
||||
wg genkey > $out/privatekey
|
||||
wg pubkey < $out/privatekey > $out/publickey
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
||||
# Enable ip forwarding, so wireguard peers can reach eachother
|
||||
boot.kernel.sysctl = {
|
||||
"net.ipv6.conf.all.forwarding" = 1;
|
||||
}
|
||||
// lib.optionalAttrs settings.ipv4.enable {
|
||||
"net.ipv4.conf.all.forwarding" = 1;
|
||||
};
|
||||
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
|
||||
|
||||
networking.firewall.allowedUDPPorts = [ settings.port ];
|
||||
|
||||
networking.firewall.extraCommands =
|
||||
let
|
||||
peersWithInternetAccess = lib.filterAttrs (
|
||||
_: peerConfig: peerConfig.allowInternetAccess
|
||||
) settings.externalPeers;
|
||||
|
||||
peerInfo = lib.mapAttrs (
|
||||
peer: peerConfig:
|
||||
let
|
||||
ipv6Address = "${thisControllerPrefix}:${externalPeerSuffix peer}";
|
||||
ipv4Address =
|
||||
if settings.ipv4.enable && peerConfig.ipv4.address != null then
|
||||
lib.head (lib.splitString "/" peerConfig.ipv4.address)
|
||||
else
|
||||
null;
|
||||
in
|
||||
{
|
||||
inherit ipv6Address ipv4Address;
|
||||
}
|
||||
) peersWithInternetAccess;
|
||||
|
||||
in
|
||||
lib.concatStringsSep "\n" (
|
||||
(lib.mapAttrsToList (_peer: info: ''
|
||||
ip6tables -t nat -A POSTROUTING -s ${info.ipv6Address}/128 ! -o '${instanceName}' -j MASQUERADE
|
||||
'') peerInfo)
|
||||
++ (lib.mapAttrsToList (
|
||||
_peer: info:
|
||||
lib.optionalString (info.ipv4Address != null) ''
|
||||
iptables -t nat -A POSTROUTING -s ${info.ipv4Address} ! -o '${instanceName}' -j MASQUERADE
|
||||
''
|
||||
) peerInfo)
|
||||
);
|
||||
|
||||
# Single wireguard interface
|
||||
networking.wireguard.interfaces."${instanceName}" = {
|
||||
listenPort = settings.port;
|
||||
|
||||
ips = [
|
||||
"${thisControllerPrefix}::1/40"
|
||||
]
|
||||
++ lib.optional settings.ipv4.enable settings.ipv4.address;
|
||||
# Controller uses ::1 in its /56 subnet but with /40 prefix for proper routing
|
||||
"${config.clan.core.vars.generators."wireguard-network-${instanceName}".files.prefix.value}::1/40"
|
||||
];
|
||||
|
||||
privateKeyFile =
|
||||
config.clan.core.vars.generators."wireguard-keys-${instanceName}".files."privatekey".path;
|
||||
|
||||
# Connect to all peers and other controllers
|
||||
peers =
|
||||
# Peers configuration
|
||||
(lib.mapAttrsToList (name: _value: {
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
|
||||
)
|
||||
);
|
||||
peers = lib.mapAttrsToList (
|
||||
name: value:
|
||||
if allPeers ? ${name} then
|
||||
# For peers: they now have our entire /56 subnet
|
||||
{
|
||||
publicKey = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-keys-${instanceName}";
|
||||
file = "publickey";
|
||||
};
|
||||
|
||||
# Allow the peer's /96 range in ALL controller subnets
|
||||
allowedIPs = lib.mapAttrsToList (
|
||||
ctrlName: _: "${controllerPrefix ctrlName}:${peerSuffix name}/96"
|
||||
) roles.controller.machines;
|
||||
|
||||
persistentKeepalive = 25;
|
||||
}) allPeers)
|
||||
++
|
||||
# External peers configuration - includes all external peers from all controllers
|
||||
(map (
|
||||
peer:
|
||||
ctrlName: _:
|
||||
let
|
||||
# IPv6 allowed IPs for mesh communication
|
||||
ipv6AllowedIPs = lib.mapAttrsToList (
|
||||
ctrlName: _: "${controllerPrefix ctrlName}:${externalPeerSuffix peer}/96"
|
||||
) roles.controller.machines;
|
||||
|
||||
# IPv4 allowed IP (only if this controller manages this peer and has IPv4 enabled)
|
||||
ipv4AllowedIPs = lib.optional (
|
||||
settings.ipv4.enable
|
||||
&& settings.externalPeers ? ${peer}
|
||||
&& settings.externalPeers.${peer}.ipv4.address != null
|
||||
) settings.externalPeers.${peer}.ipv4.address;
|
||||
controllerPrefix = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = ctrlName;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "prefix";
|
||||
};
|
||||
peerSuffix = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "suffix";
|
||||
};
|
||||
in
|
||||
{
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/shared/wireguard-network-${instanceName}-external-peer-${peer}/publickey/value"
|
||||
)
|
||||
);
|
||||
|
||||
allowedIPs = ipv6AllowedIPs ++ ipv4AllowedIPs;
|
||||
"${controllerPrefix}:${peerSuffix}/96"
|
||||
) roles.controller.machines;
|
||||
|
||||
persistentKeepalive = 25;
|
||||
}
|
||||
) allExternalPeers)
|
||||
++
|
||||
# Other controllers configuration
|
||||
(lib.mapAttrsToList (name: value: {
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
|
||||
)
|
||||
);
|
||||
else
|
||||
# For other controllers: use their /56 subnet
|
||||
{
|
||||
publicKey = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-keys-${instanceName}";
|
||||
file = "publickey";
|
||||
};
|
||||
|
||||
allowedIPs = [ "${controllerPrefix name}::/56" ];
|
||||
allowedIPs = [
|
||||
"${
|
||||
clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "prefix";
|
||||
}
|
||||
}::/56"
|
||||
];
|
||||
|
||||
endpoint = "${value.settings.endpoint}:${toString value.settings.port}";
|
||||
persistentKeepalive = 25;
|
||||
}) allOtherControllers);
|
||||
}
|
||||
) (allPeers // allOtherControllers);
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -624,7 +435,7 @@ in
|
||||
let
|
||||
isController =
|
||||
instanceInfo.roles ? controller && instanceInfo.roles.controller.machines ? ${machine.name};
|
||||
isPeer = instanceInfo.roles ? peer && instanceInfo.roles.peer.machines or { } ? ${machine.name};
|
||||
isPeer = instanceInfo.roles ? peer && instanceInfo.roles.peer.machines ? ${machine.name};
|
||||
in
|
||||
lib.optional (isController && isPeer) {
|
||||
inherit instanceName;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
|
||||
@@ -11,28 +10,7 @@ let
|
||||
"peer1"
|
||||
"peer2"
|
||||
"peer3"
|
||||
# external machine for external peer testing
|
||||
"external1"
|
||||
];
|
||||
|
||||
controllerPrefix =
|
||||
controllerName:
|
||||
builtins.readFile (
|
||||
config.clan.directory
|
||||
+ "/vars/per-machine/${controllerName}/wireguard-network-wg-test-one/prefix/value"
|
||||
);
|
||||
peerSuffix =
|
||||
peerName:
|
||||
builtins.readFile (
|
||||
config.clan.directory + "/vars/per-machine/${peerName}/wireguard-network-wg-test-one/suffix/value"
|
||||
);
|
||||
# external peer suffixes are stored via shared vars
|
||||
externalPeerSuffix =
|
||||
externalName:
|
||||
builtins.readFile (
|
||||
config.clan.directory
|
||||
+ "/vars/shared/wireguard-network-wg-test-one-external-peer-${externalName}/suffix/value"
|
||||
);
|
||||
in
|
||||
{
|
||||
name = "wireguard";
|
||||
@@ -69,19 +47,10 @@ in
|
||||
|
||||
roles.controller.machines."controller1".settings = {
|
||||
endpoint = "192.168.1.1";
|
||||
# Enable IPv4 for external peers
|
||||
ipv4.enable = true;
|
||||
ipv4.address = "10.42.1.1/24";
|
||||
# add an external peer to controller1 with IPv4
|
||||
externalPeers.external1 = {
|
||||
ipv4.address = "10.42.1.50/32";
|
||||
};
|
||||
};
|
||||
|
||||
roles.controller.machines."controller2".settings = {
|
||||
endpoint = "192.168.1.2";
|
||||
# add the same external peer to controller2 to test multi-controller connection
|
||||
externalPeers.external1 = { };
|
||||
};
|
||||
|
||||
roles.peer.machines = {
|
||||
@@ -108,77 +77,11 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
nodes.external1 =
|
||||
let
|
||||
controller1Prefix = controllerPrefix "controller1";
|
||||
controller2Prefix = controllerPrefix "controller2";
|
||||
external1Suffix = externalPeerSuffix "external1";
|
||||
in
|
||||
{
|
||||
networking.extraHosts = ''
|
||||
${controller1Prefix}::1 controller1.wg-test-one
|
||||
${controller2Prefix}::1 controller2.wg-test-one
|
||||
'';
|
||||
networking.wireguard.interfaces."wg0" = {
|
||||
|
||||
# Multiple IPs, one in each controller's subnet (IPv6) plus IPv4
|
||||
ips = [
|
||||
"${controller1Prefix + ":" + external1Suffix}/56"
|
||||
"${controller2Prefix + ":" + external1Suffix}/56"
|
||||
"10.42.1.50/32" # IPv4 address for controller1
|
||||
];
|
||||
|
||||
privateKeyFile =
|
||||
builtins.toFile "wg-priv-key"
|
||||
# This needs to be updated whenever update-vars was executed
|
||||
# Get the value from the generated vars via this command:
|
||||
# echo "AGE-SECRET-KEY-1PL0M9CWRCG3PZ9DXRTTLMCVD57U6JDFE8K7DNVQ35F4JENZ6G3MQ0RQLRV" | SOPS_AGE_KEY_FILE=/dev/stdin nix run nixpkgs#sops decrypt clanServices/wireguard/tests/vm/vars/shared/wireguard-network-wg-test-one-external-peer-external1/privatekey/secret
|
||||
"wO8dl3JWgV5J+0D/2UDcLsxTD25IWTvd5ed6vv2Nikk=";
|
||||
|
||||
# Connect to both controllers
|
||||
peers = [
|
||||
# Controller 1
|
||||
{
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.directory + "/vars/per-machine/controller1/wireguard-keys-wg-test-one/publickey/value"
|
||||
)
|
||||
);
|
||||
|
||||
# Allow controller1's /56 subnet (IPv6) and IPv4 subnet
|
||||
allowedIPs = [
|
||||
"${controller1Prefix}::/56"
|
||||
"10.42.1.0/24" # IPv4 subnet for internet access
|
||||
];
|
||||
|
||||
endpoint = "controller1:51820";
|
||||
|
||||
persistentKeepalive = 25;
|
||||
}
|
||||
# Controller 2
|
||||
{
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.directory + "/vars/per-machine/controller2/wireguard-keys-wg-test-one/publickey/value"
|
||||
)
|
||||
);
|
||||
|
||||
# Allow controller2's /56 subnet
|
||||
allowedIPs = [ "${controller2Prefix}::/56" ];
|
||||
|
||||
endpoint = "controller2:51820";
|
||||
|
||||
persistentKeepalive = 25;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
# Start network on all machines including external1
|
||||
machines = [peer1, peer2, peer3, controller1, controller2, external1]
|
||||
# Show all addresses
|
||||
machines = [peer1, peer2, peer3, controller1, controller2]
|
||||
for m in machines:
|
||||
m.systemctl("start network-online.target")
|
||||
|
||||
@@ -190,39 +93,10 @@ in
|
||||
print("STARTING PING TESTS")
|
||||
print("="*60)
|
||||
|
||||
# Test mesh connectivity between regular clan machines
|
||||
clan_machines = [peer1, peer2, peer3, controller1, controller2]
|
||||
for m1 in clan_machines:
|
||||
for m2 in clan_machines:
|
||||
for m1 in machines:
|
||||
for m2 in machines:
|
||||
if m1 != m2:
|
||||
print(f"\n--- Pinging from {m1.name} to {m2.name}.wg-test-one ---")
|
||||
m1.wait_until_succeeds(f"ping -c1 {m2.name}.wg-test-one >&2")
|
||||
|
||||
# Test that external peer can reach both controllers (multi-controller connection)
|
||||
print("\n--- Testing external1 -> controller1 (direct connection) ---")
|
||||
external1.wait_until_succeeds("ping -c1 controller1.wg-test-one >&2")
|
||||
|
||||
print("\n--- Testing external1 -> controller2 (direct connection) ---")
|
||||
external1.wait_until_succeeds("ping -c1 controller2.wg-test-one >&2")
|
||||
|
||||
# Test IPv4 connectivity
|
||||
print("\n--- Testing external1 -> controller1 (IPv4) ---")
|
||||
external1.wait_until_succeeds("ping -c1 10.42.1.1 >&2")
|
||||
|
||||
# Test that all clan machines can reach the external peer
|
||||
for m in clan_machines:
|
||||
print(f"\n--- Pinging from {m.name} to external1.wg-test-one ---")
|
||||
m.wait_until_succeeds("ping -c1 external1.wg-test-one >&2")
|
||||
|
||||
# Test that external peer can reach a regular peer via controller1
|
||||
print("\n--- Testing external1 -> peer1 (via controller1) ---")
|
||||
external1.wait_until_succeeds("ping -c1 ${controllerPrefix "controller1"}:${peerSuffix "peer1"} >&2")
|
||||
|
||||
# Test controller failover
|
||||
print("\n--- Shutting down controller1 ---")
|
||||
controller1.shutdown()
|
||||
print("\n--- Testing external1 -> peer1 (via controller2 after controller1 shutdown) ---")
|
||||
external1.wait_until_succeeds("ping -c1 ${controllerPrefix "controller2"}:${peerSuffix "peer1"} >&2")
|
||||
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
966fa6c74dbea65154816755934180edaf20fc031b15622c3e4433516c08fbbc
|
||||
@@ -1 +0,0 @@
|
||||
966fa6c74dbea65154816755934180edaf20fc031b15622c3e4433516c08fbbc
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:dhEJ2dOGbK+oNj7YDzFD9455g5Cv7Ic8tZzPRI31ugzjHXOORbDq3+MyY/l/,iv:DP0S/taFONhNkVvvZQoTe2mnwRfJB7QgaILyZt9nT9E=,tag:TGFwbn6PTz4iGU7fnkSaWA==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMQVRQNDNzUklFQnVIODZo\nbXNITlNhL1I2c1lyRjlUUGN2RTJLcFRJUjAwCmthT3JDRHdEa1RHM3pHT1pTQWd2\nMlkvMmVQRXk5VmZyTkU0RkpYVzlkNzgKLS0tIGVFY0ppc1dqQis5ay9HK2dydlht\neEI1eDlBekNtMTVoSk1hTFJUNUJiU2sK30cizM0xUDUDeTAQhtliL1KMNvnFcvxP\n8CFqOT36MBmIiC42lV0JTh/lsQbdirBNQLP9QUph8hGqbpehkleLlg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-10-15T04:08:18Z",
|
||||
"mac": "ENC[AES256_GCM,data:1BPlDmBFyNubxq2DRqsYUuh+WsbihR5D6OIA6Zdx1XPpOXxzxXXWHddtWxcaT14HLL3VKfINAI5MSIx7aVMuTgEJzaCkRSd11T/qwrV1mAiGW1bo3vXBVtbii271hYHCRaLuWnm4kIDyIV6onQARprTZgZ0Rlh0x5qwGnkk4uPI=,iv:Vda4Vxmm5j2nAIw2g1ydy9+bJHkHy7v7fKvS+4K1zds=,tag:qUpSRCa66D/LXDawXl9eOQ==,type:str]",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -1 +0,0 @@
|
||||
zrzikusuYoFNu7nF0HX6IdCYbCoZxOUutkVHArvSySc=
|
||||
@@ -1 +0,0 @@
|
||||
3f4b:fde7:06da:8b6a
|
||||
@@ -1 +0,0 @@
|
||||
25.11
|
||||
@@ -1 +0,0 @@
|
||||
966fa6c74dbea65154816755934180edaf20fc031b15622c3e4433516c08fbbc
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:U5fkHGEM+/Xw9g1LGZckGWV225pnMZM28+oT4DscrMMt3XMFM+CRojd2JhC5,iv:ydAKRdJEKRI2wbcIsM/5YQQVkFVc3/JtCk3dwBJwJVw=,tag:ZiRsOzvFncUDZqok8fgh8A==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5OFFWRmp4R01WN0hZU2pu\nY0tCOWdrazZtbnVWczV2WnlPMUMvWUFmV1FBCjk1bnNtcmc5L0dRc1pTNUUwbzh2\nNUhhLzFycUFZd24yVEZVc3c4S2R0czAKLS0tIG8vY0lCRlhyYyszeEE0TzV4SWJj\nOFI0NFlvR25vNDB4KzJwdXQrTVJoS2MKHTgW/GlER4nP160Rcw9jZEraemouXrMz\nS9mLE/X3GBoAlgXmEwZC+7ZEZyNi/cd4FXX6edelD06S8uDjz0DKVg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-10-16T07:19:29Z",
|
||||
"mac": "ENC[AES256_GCM,data:DsQrWj8TwtgnAjDTFFu5ALjr3z3fx0k3grnPxk0zxo4RxvtNI6cv5de91YJ8GYvbElT+ylJul91XUSQ93PtqskrUDYvlKzU60zUDjR/G27g+BIHuKBYIDYNLFKjkoI2/KffbVexRFAqqvkjwSYbA2aJOLfyvUoZZhCuMmOW6jW4=,iv:gLe/wXzqix63d7Fv8vLDydU30ElkyVz6N2TlCueFInw=,tag:7QPD4kfyE4hUeGRC0cgsYg==,type:str]",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../../../../../sops/users/admin
|
||||
@@ -1 +0,0 @@
|
||||
bb+pM+fpJzqc1A+dvTdsE4JliVvdMMXoQUPaElkrs0w=
|
||||
@@ -1 +0,0 @@
|
||||
3f4b:fde7:06da:8b6a
|
||||
@@ -1,7 +1,23 @@
|
||||
🚧🚧🚧 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.
|
||||
|
||||
Yggdrasil is designed to be a future-proof and decentralised alternative to
|
||||
the structured routing protocols commonly used today on the internet. Inside your clan, it will allow you to reach all of your machines.
|
||||
Yggdrasil is designed to be a future-proof and decentralised alternative to the
|
||||
structured routing protocols commonly used today on the internet. Inside your
|
||||
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
|
||||
|
||||
|
||||
@@ -29,12 +29,13 @@
|
||||
];
|
||||
};
|
||||
|
||||
options.peers = lib.mkOption {
|
||||
options.extraPeers = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Static peers to configure for this host.
|
||||
If not set, local peers will be auto-discovered
|
||||
Additional static peers to configure for this host. If you use a
|
||||
VPN clan service, it will automatically be added as peers to other hosts.
|
||||
Local peers are also auto-discovered and don't need to be added.
|
||||
'';
|
||||
example = [
|
||||
"tcp://192.168.1.1:6443"
|
||||
@@ -45,16 +46,67 @@
|
||||
};
|
||||
};
|
||||
perInstance =
|
||||
{ settings, ... }:
|
||||
{
|
||||
settings,
|
||||
roles,
|
||||
exports,
|
||||
...
|
||||
}:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
config,
|
||||
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' "instances.*.networking.?peers.*.host.?plain" 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 = {
|
||||
|
||||
files.privateKey = { };
|
||||
@@ -99,7 +151,7 @@
|
||||
settings = {
|
||||
PrivateKeyPath = "/key";
|
||||
IfName = "ygg";
|
||||
Peers = settings.peers;
|
||||
Peers = lib.lists.unique (exportedPeers ++ settings.extraPeers);
|
||||
MulticastInterfaces = [
|
||||
# Ethernet is preferred over WIFI
|
||||
{
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
roles.default.machines.peer1 = { };
|
||||
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";
|
||||
roles.default.machines.peer2.settings.host = "peer2";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{ ... }:
|
||||
{
|
||||
clanLib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/zerotier";
|
||||
@@ -39,6 +42,7 @@
|
||||
imports = [
|
||||
(import ./shared.nix {
|
||||
inherit
|
||||
clanLib
|
||||
instanceName
|
||||
roles
|
||||
config
|
||||
@@ -90,6 +94,7 @@
|
||||
imports = [
|
||||
(import ./shared.nix {
|
||||
inherit
|
||||
clanLib
|
||||
instanceName
|
||||
roles
|
||||
config
|
||||
@@ -142,6 +147,7 @@
|
||||
imports = [
|
||||
(import ./shared.nix {
|
||||
inherit
|
||||
clanLib
|
||||
instanceName
|
||||
roles
|
||||
config
|
||||
@@ -160,15 +166,16 @@
|
||||
);
|
||||
networkIps = builtins.foldl' (
|
||||
ips: name:
|
||||
if
|
||||
builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value"
|
||||
then
|
||||
ips
|
||||
++ [
|
||||
(builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value")
|
||||
]
|
||||
else
|
||||
ips
|
||||
let
|
||||
ztIp = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "zerotier";
|
||||
file = "zerotier-ip";
|
||||
default = null;
|
||||
};
|
||||
in
|
||||
if ztIp != null then ips ++ [ ztIp ] else ips
|
||||
) [ ] machines;
|
||||
allHostIPs = settings.allowedIps ++ networkIps;
|
||||
in
|
||||
|
||||
@@ -21,6 +21,7 @@ in
|
||||
../../clanServices/zerotier
|
||||
# Required modules
|
||||
../../nixosModules/clanCore
|
||||
../../nixosModules/machineModules
|
||||
# Dependencies like clan-cli
|
||||
../../pkgs/clan-cli
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
clanLib,
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
@@ -8,20 +9,26 @@
|
||||
}:
|
||||
let
|
||||
controllerMachine = builtins.head (lib.attrNames roles.controller.machines or { });
|
||||
networkIdPath = "${config.clan.core.settings.directory}/vars/per-machine/${controllerMachine}/zerotier/zerotier-network-id/value";
|
||||
networkId = if builtins.pathExists networkIdPath then builtins.readFile networkIdPath else null;
|
||||
networkId = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = controllerMachine;
|
||||
generator = "zerotier";
|
||||
file = "zerotier-network-id";
|
||||
default = null;
|
||||
};
|
||||
moons = lib.attrNames (roles.moon.machines or { });
|
||||
moonIps = builtins.foldl' (
|
||||
ips: name:
|
||||
if
|
||||
builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value"
|
||||
then
|
||||
ips
|
||||
++ [
|
||||
(builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value")
|
||||
]
|
||||
else
|
||||
ips
|
||||
let
|
||||
moonIp = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "zerotier";
|
||||
file = "zerotier-ip";
|
||||
default = null;
|
||||
};
|
||||
in
|
||||
if moonIp != null then ips ++ [ moonIp ] else ips
|
||||
) [ ] moons;
|
||||
in
|
||||
{
|
||||
|
||||
24
devFlake/flake.lock
generated
24
devFlake/flake.lock
generated
@@ -3,10 +3,10 @@
|
||||
"clan-core-for-checks": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1760361585,
|
||||
"narHash": "sha256-v4PnSmt1hXW4dSgVWxcd1ZeEBlhO7NksNRC5cX7L5iw=",
|
||||
"lastModified": 1761204206,
|
||||
"narHash": "sha256-A4KDudGblln1yh8c95OVow2NRlHtbGZXr/pgNenyrNc=",
|
||||
"ref": "main",
|
||||
"rev": "7e7e58eb64ef61beb0a938a6622ec0122382131b",
|
||||
"rev": "aabbe0dfac47b7cfbe2210bcb27fb7ecce93350f",
|
||||
"shallow": true,
|
||||
"type": "git",
|
||||
"url": "https://git.clan.lol/clan/clan-core"
|
||||
@@ -105,11 +105,11 @@
|
||||
},
|
||||
"nixpkgs-dev": {
|
||||
"locked": {
|
||||
"lastModified": 1760965023,
|
||||
"narHash": "sha256-cpcgkeLApMGFCdp4jFqeIxTwlcGaSI+Zwmv8z2E85pY=",
|
||||
"lastModified": 1761853358,
|
||||
"narHash": "sha256-1tBdsBzYJOzVzNOmCFzFMWHw7UUbhkhiYCFGr+OjPTs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "40ef6b9aa73f70b265c29df083fafae66b9df351",
|
||||
"rev": "262333bca9b49964f8e3cad3af655466597c01d4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -128,11 +128,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1760652422,
|
||||
"narHash": "sha256-C88Pgz38QIl9JxQceexqL2G7sw9vodHWx1Uaq+NRJrw=",
|
||||
"lastModified": 1761730856,
|
||||
"narHash": "sha256-t1i5p/vSWwueZSC0Z2BImxx3BjoUDNKyC2mk24krcMY=",
|
||||
"owner": "NuschtOS",
|
||||
"repo": "search",
|
||||
"rev": "3ebeebe8b6a49dfb11f771f761e0310f7c48d726",
|
||||
"rev": "e29de6db0cb3182e9aee75a3b1fd1919d995d85b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -208,11 +208,11 @@
|
||||
"nixpkgs": []
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1760945191,
|
||||
"narHash": "sha256-ZRVs8UqikBa4Ki3X4KCnMBtBW0ux1DaT35tgsnB1jM4=",
|
||||
"lastModified": 1761311587,
|
||||
"narHash": "sha256-Msq86cR5SjozQGCnC6H8C+0cD4rnx91BPltZ9KK613Y=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "f56b1934f5f8fcab8deb5d38d42fd692632b47c2",
|
||||
"rev": "2eddae033e4e74bf581c2d1dfa101f9033dbd2dc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -88,6 +88,7 @@ For the provide flake example, your flake should now look like this:
|
||||
self = self; # this needs to point at the repository root
|
||||
specialArgs = {};
|
||||
meta.name = throw "Change me to something unique";
|
||||
meta.tld = throw "Change me to something unique";
|
||||
|
||||
machines = {
|
||||
berlin = {
|
||||
|
||||
@@ -137,12 +137,13 @@ Description: None
|
||||
|
||||
This confirms your setup is working correctly.
|
||||
|
||||
You can now change the default name by editing the `meta.name` field in your `clan.nix` file.
|
||||
You can now change the default name and tld by editing the `meta.name` and `meta.tld` fields in your `clan.nix` file.
|
||||
|
||||
```{.nix title="clan.nix" hl_lines="3"}
|
||||
```{.nix title="clan.nix" hl_lines="3 4"}
|
||||
{
|
||||
# Ensure this is unique among all clans you want to use.
|
||||
meta.name = "__CHANGE_ME__";
|
||||
meta.tld = "changeme";
|
||||
|
||||
# ...
|
||||
# elided
|
||||
|
||||
@@ -10,10 +10,11 @@ and how to define a remote builder for your machine closures.
|
||||
Set the machine’s `targetHost` to the reachable IP address of the new machine.
|
||||
This eliminates the need to specify `--target-host` in CLI commands.
|
||||
|
||||
```{.nix title="clan.nix" hl_lines="9"}
|
||||
```{.nix title="clan.nix" hl_lines="10"}
|
||||
{
|
||||
# Ensure this is unique among all clans you want to use.
|
||||
meta.name = "my-clan";
|
||||
meta.tld = "ccc";
|
||||
|
||||
inventory.machines = {
|
||||
# Define machines here.
|
||||
|
||||
@@ -60,6 +60,7 @@ Configure Clan-wide settings and define machines. Here's an example `flake.nix`:
|
||||
# Define your Clan
|
||||
clan = {
|
||||
meta.name = ""; # Required and must be unique
|
||||
meta.tld = ""; # Required and must be unique
|
||||
|
||||
machines = {
|
||||
jon = {
|
||||
|
||||
@@ -43,6 +43,7 @@ For the purpose of this guide we have two machines:
|
||||
inherit self;
|
||||
|
||||
meta.name = "myclan";
|
||||
meta.tld = "ccc";
|
||||
|
||||
inventory.machines = {
|
||||
controller = {};
|
||||
|
||||
@@ -63,6 +63,7 @@ To use `age` plugins with Clan, you need to configure them in your `flake.nix` f
|
||||
inherit self;
|
||||
|
||||
meta.name = "myclan";
|
||||
meta.tld = "ccc";
|
||||
|
||||
# Add YubiKey and FIDO2 HMAC plugins
|
||||
# Note: Plugins must be available in nixpkgs.
|
||||
|
||||
24
flake.lock
generated
24
flake.lock
generated
@@ -71,11 +71,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1760721282,
|
||||
"narHash": "sha256-aAHphQbU9t/b2RRy2Eb8oMv+I08isXv2KUGFAFn7nCo=",
|
||||
"lastModified": 1761339987,
|
||||
"narHash": "sha256-IUaawVwItZKi64IA6kF6wQCLCzpXbk2R46dHn8sHkig=",
|
||||
"owner": "nix-darwin",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "c3211fcd0c56c11ff110d346d4487b18f7365168",
|
||||
"rev": "7cd9aac79ee2924a85c211d21fafd394b06a38de",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -99,11 +99,11 @@
|
||||
},
|
||||
"nixos-facter-modules": {
|
||||
"locked": {
|
||||
"lastModified": 1756491981,
|
||||
"narHash": "sha256-lXyDAWPw/UngVtQfgQ8/nrubs2r+waGEYIba5UX62+k=",
|
||||
"lastModified": 1761137276,
|
||||
"narHash": "sha256-4lDjGnWRBLwqKQ4UWSUq6Mvxu9r8DSqCCydodW/Jsi8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixos-facter-modules",
|
||||
"rev": "c1b29520945d3e148cd96618c8a0d1f850965d8c",
|
||||
"rev": "70bcd64225d167c7af9b475c4df7b5abba5c7de8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -146,11 +146,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1760845571,
|
||||
"narHash": "sha256-PwGzU3EOU65Ef1VvuNnVLie+l+P0g/fzf/PGUG82KbM=",
|
||||
"lastModified": 1760998189,
|
||||
"narHash": "sha256-ee2e1/AeGL5X8oy/HXsZQvZnae6XfEVdstGopKucYLY=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "9c9a9798be331ed3f4b2902933d7677d0659ee61",
|
||||
"rev": "5a7d18b5c55642df5c432aadb757140edfeb70b3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -181,11 +181,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1760945191,
|
||||
"narHash": "sha256-ZRVs8UqikBa4Ki3X4KCnMBtBW0ux1DaT35tgsnB1jM4=",
|
||||
"lastModified": 1761311587,
|
||||
"narHash": "sha256-Msq86cR5SjozQGCnC6H8C+0cD4rnx91BPltZ9KK613Y=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "f56b1934f5f8fcab8deb5d38d42fd692632b47c2",
|
||||
"rev": "2eddae033e4e74bf581c2d1dfa101f9033dbd2dc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
(
|
||||
{ ... }:
|
||||
{
|
||||
debug = true;
|
||||
clan = {
|
||||
meta.name = "clan-core";
|
||||
inventory = {
|
||||
@@ -98,6 +99,7 @@
|
||||
./lib/filter-clan-core/flake-module.nix
|
||||
./lib/flake-module.nix
|
||||
./lib/flake-parts/clan-nixos-test.nix
|
||||
./modules/flake-module.nix
|
||||
./nixosModules/clanCore/vars/flake-module.nix
|
||||
./nixosModules/flake-module.nix
|
||||
./pkgs/clan-cli/clan_cli/tests/flake-module.nix
|
||||
|
||||
@@ -39,32 +39,10 @@ in
|
||||
};
|
||||
modules = [
|
||||
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;
|
||||
---
|
||||
'';
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
apply =
|
||||
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;
|
||||
# Important: !This logic needs to be kept in sync with lib.clan function!
|
||||
apply = config: clan-core.lib.checkConfig config.checks config;
|
||||
};
|
||||
|
||||
# Mapped flake toplevel outputs
|
||||
|
||||
19
lib/clan/checkConfig.nix
Normal file
19
lib/clan/checkConfig.nix
Normal file
@@ -0,0 +1,19 @@
|
||||
{ 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
|
||||
)
|
||||
@@ -33,7 +33,7 @@
|
||||
let
|
||||
nixpkgs = self.inputs.nixpkgs or clan-core.inputs.nixpkgs;
|
||||
nix-darwin = self.inputs.nix-darwin or clan-core.inputs.nix-darwin;
|
||||
in
|
||||
configuration = (
|
||||
lib.evalModules {
|
||||
class = "clan";
|
||||
specialArgs = {
|
||||
@@ -50,3 +50,6 @@ lib.evalModules {
|
||||
m
|
||||
];
|
||||
}
|
||||
);
|
||||
in
|
||||
clan-core.clanLib.checkConfig configuration.config.checks configuration
|
||||
|
||||
@@ -137,6 +137,12 @@ in
|
||||
default = { };
|
||||
type = types.submoduleWith {
|
||||
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)
|
||||
clan-core
|
||||
nixpkgs
|
||||
|
||||
@@ -16,10 +16,12 @@ lib.fix (
|
||||
*/
|
||||
callLib = file: args: import file ({ inherit lib clanLib; } // args);
|
||||
|
||||
evalService = clanLib.callLib ./modules/inventory/distributed-service/evalService.nix { };
|
||||
checkConfig = clanLib.callLib ./clan/checkConfig.nix { };
|
||||
|
||||
evalService = clanLib.callLib ./evalService.nix { };
|
||||
# ------------------------------------
|
||||
# ClanLib functions
|
||||
inventory = clanLib.callLib ./modules/inventory { };
|
||||
inventory = clanLib.callLib ./inventory { };
|
||||
test = clanLib.callLib ./test { };
|
||||
flake-inputs = clanLib.callLib ./flake-inputs.nix { };
|
||||
# Custom types
|
||||
@@ -30,6 +32,8 @@ lib.fix (
|
||||
jsonschema = import ./jsonschema { inherit lib; };
|
||||
docs = import ./docs.nix { inherit lib; };
|
||||
|
||||
vars = import ./vars.nix { inherit lib; };
|
||||
|
||||
# flakes
|
||||
flakes = clanLib.callLib ./flakes.nix { };
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
lib ? import <nixpkgs/lib>,
|
||||
}:
|
||||
let
|
||||
clanLibOrig = (import ./.. { inherit lib; }).__unfix__;
|
||||
clanLibOrig = (import ./. { inherit lib; }).__unfix__;
|
||||
clanLibWithFs =
|
||||
{ virtual_fs }:
|
||||
lib.fix (
|
||||
@@ -11,19 +11,19 @@ let
|
||||
let
|
||||
clan-core = {
|
||||
clanLib = final;
|
||||
modules.clan.default = lib.modules.importApply ./clan { inherit clan-core; };
|
||||
modules.clan.default = lib.modules.importApply ../modules/clan { inherit clan-core; };
|
||||
|
||||
# Note: Can add other things to "clan-core"
|
||||
# ... Not needed for this test
|
||||
};
|
||||
in
|
||||
{
|
||||
clan = import ../clan {
|
||||
clan = import ./clan {
|
||||
inherit lib clan-core;
|
||||
};
|
||||
|
||||
# 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;
|
||||
# Example of a passthru
|
||||
# passthru = [
|
||||
@@ -53,7 +53,12 @@ in
|
||||
};
|
||||
};
|
||||
}).clan
|
||||
{ config.directory = rootPath; };
|
||||
{
|
||||
directory = rootPath;
|
||||
self = {
|
||||
inputs.nixpkgs.lib.version = "25.11";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit vclan;
|
||||
@@ -94,7 +99,12 @@ in
|
||||
};
|
||||
};
|
||||
}).clan
|
||||
{ config.directory = rootPath; };
|
||||
{
|
||||
directory = rootPath;
|
||||
self = {
|
||||
inputs.nixpkgs.lib.version = "25.11";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit vclan;
|
||||
@@ -17,9 +17,9 @@ lib.evalModules {
|
||||
specialArgs._ctx = prefix;
|
||||
modules = [
|
||||
# Base module
|
||||
./service-module.nix
|
||||
./inventory/distributed-service/service-module.nix
|
||||
# Feature modules
|
||||
(lib.modules.importApply ./api-feature.nix {
|
||||
(lib.modules.importApply ./inventory/distributed-service/api-feature.nix {
|
||||
inherit clanLib prefix;
|
||||
})
|
||||
]
|
||||
@@ -10,12 +10,11 @@ in
|
||||
rec {
|
||||
# TODO: automatically generate this from the directory conventions
|
||||
imports = [
|
||||
./modules/flake-module.nix
|
||||
./clanTest/flake-module.nix
|
||||
./introspection/flake-module.nix
|
||||
./modules/inventory/flake-module.nix
|
||||
./jsonschema/flake-module.nix
|
||||
./types/flake-module.nix
|
||||
./inventory/flake-module.nix
|
||||
];
|
||||
flake.clanLib =
|
||||
let
|
||||
@@ -78,9 +77,6 @@ rec {
|
||||
../lib
|
||||
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../.)
|
||||
../flakeModules
|
||||
# ../../nixosModules/clanCore
|
||||
# ../../machines
|
||||
# ../../inventory.json
|
||||
];
|
||||
};
|
||||
in
|
||||
@@ -101,6 +97,36 @@ rec {
|
||||
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;
|
||||
};
|
||||
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
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
lib,
|
||||
clanLib,
|
||||
}:
|
||||
let
|
||||
services = clanLib.callLib ./distributed-service/inventory-adapter.nix { };
|
||||
in
|
||||
{
|
||||
inherit (services) mapInstances;
|
||||
inventoryModule = {
|
||||
_file = "clanLib.inventory.module";
|
||||
imports = [
|
||||
../inventoryClass/inventory.nix
|
||||
../../modules/inventoryClass/inventory.nix
|
||||
];
|
||||
_module.args = { inherit clanLib; };
|
||||
};
|
||||
@@ -28,9 +28,9 @@ in
|
||||
elemType = submoduleWith {
|
||||
class = "clan.service";
|
||||
specialArgs = {
|
||||
exports = config.exports;
|
||||
directory = directory;
|
||||
clanLib = specialArgs.clanLib;
|
||||
exports = config.exports;
|
||||
};
|
||||
modules = [
|
||||
(
|
||||
@@ -13,16 +13,18 @@ in
|
||||
let
|
||||
# Common filtered source for inventory tests
|
||||
inventoryTestsSrc = lib.fileset.toSource {
|
||||
root = ../../../..;
|
||||
root = ../../..;
|
||||
fileset = lib.fileset.unions [
|
||||
../../../../flake.nix
|
||||
../../../../flake.lock
|
||||
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../../..)
|
||||
../../../../flakeModules
|
||||
../../../../lib
|
||||
../../../../nixosModules/clanCore
|
||||
../../../../machines
|
||||
../../../../inventory.json
|
||||
../../../flake.nix
|
||||
../../../flake.lock
|
||||
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../..)
|
||||
../../../flakeModules
|
||||
../../../lib
|
||||
../../../nixosModules/clanCore
|
||||
../../../nixosModules/machineModules
|
||||
../../../machines
|
||||
../../../inventory.json
|
||||
../../../modules
|
||||
];
|
||||
};
|
||||
in
|
||||
@@ -81,6 +81,7 @@ let
|
||||
applySettings =
|
||||
instanceName: instance:
|
||||
lib.mapAttrs (roleName: role: {
|
||||
settings = config.instances.${instanceName}.roles.${roleName}.finalSettings.config;
|
||||
machines = lib.mapAttrs (machineName: _v: {
|
||||
settings =
|
||||
config.instances.${instanceName}.roles.${roleName}.machines.${machineName}.finalSettings.config;
|
||||
@@ -158,6 +159,29 @@ in
|
||||
(
|
||||
{ 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
|
||||
options.machines = mkOption {
|
||||
description = ''
|
||||
@@ -216,7 +240,7 @@ in
|
||||
|
||||
options.extraModules = lib.mkOption {
|
||||
default = [ ];
|
||||
type = types.listOf (types.either types.deferredModule types.str);
|
||||
type = types.listOf types.deferredModule;
|
||||
};
|
||||
}
|
||||
)
|
||||
@@ -483,6 +507,9 @@ in
|
||||
type = types.deferredModule;
|
||||
default = { };
|
||||
description = ''
|
||||
!!! Danger "Experimental Feature"
|
||||
This feature is experimental and will change in the future.
|
||||
|
||||
export modules defined in 'perInstance'
|
||||
mapped to their instance name
|
||||
|
||||
@@ -611,6 +638,9 @@ in
|
||||
type = types.deferredModule;
|
||||
default = { };
|
||||
description = ''
|
||||
!!! Danger "Experimental Feature"
|
||||
This feature is experimental and will change in the future.
|
||||
|
||||
export modules defined in 'perMachine'
|
||||
mapped to their machine name
|
||||
|
||||
@@ -712,6 +742,9 @@ in
|
||||
|
||||
exports = mkOption {
|
||||
description = ''
|
||||
!!! Danger "Experimental Feature"
|
||||
This feature is experimental and will change in the future.
|
||||
|
||||
This services exports.
|
||||
Gets merged with all other services exports.
|
||||
|
||||
@@ -850,7 +883,11 @@ in
|
||||
instanceRes.nixosModule
|
||||
]
|
||||
++ (map (
|
||||
s: if builtins.typeOf s == "string" then "${directory}/${s}" else s
|
||||
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);
|
||||
};
|
||||
}
|
||||
@@ -4,36 +4,8 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
evalModules
|
||||
;
|
||||
|
||||
evalInventory =
|
||||
m:
|
||||
(evalModules {
|
||||
# Static modules
|
||||
modules = [
|
||||
clanLib.inventory.inventoryModule
|
||||
{
|
||||
_file = "test file";
|
||||
tags.all = [ ];
|
||||
tags.nixos = [ ];
|
||||
tags.darwin = [ ];
|
||||
}
|
||||
{
|
||||
modules.test = { };
|
||||
}
|
||||
m
|
||||
];
|
||||
}).config;
|
||||
|
||||
callInventoryAdapter =
|
||||
inventoryModule:
|
||||
let
|
||||
inventory = evalInventory inventoryModule;
|
||||
flakeInputsFixture = {
|
||||
self.clan.modules = inventoryModule.modules or { };
|
||||
# Example upstream module
|
||||
upstream.clan.modules = {
|
||||
uzzi = {
|
||||
_class = "clan.service";
|
||||
@@ -43,24 +15,42 @@ let
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
clanLib.inventory.mapInstances {
|
||||
directory = ./.;
|
||||
clanCoreModules = { };
|
||||
flakeInputs = flakeInputsFixture;
|
||||
inherit inventory;
|
||||
exportsModule = { };
|
||||
|
||||
createTestClan =
|
||||
testClan:
|
||||
let
|
||||
res = clanLib.clan ({
|
||||
# Static / mocked
|
||||
specialArgs = {
|
||||
clan-core = {
|
||||
clan.modules = { };
|
||||
};
|
||||
};
|
||||
self.inputs = flakeInputsFixture // {
|
||||
self.clan = res.config;
|
||||
};
|
||||
directory = ./.;
|
||||
exportsModule = { };
|
||||
|
||||
imports = [
|
||||
testClan
|
||||
];
|
||||
});
|
||||
in
|
||||
res;
|
||||
|
||||
in
|
||||
{
|
||||
extraModules = import ./extraModules.nix { inherit clanLib; };
|
||||
exports = import ./exports.nix { inherit lib clanLib; };
|
||||
settings = import ./settings.nix { inherit lib callInventoryAdapter; };
|
||||
specialArgs = import ./specialArgs.nix { inherit lib callInventoryAdapter; };
|
||||
resolve_module_spec = import ./import_module_spec.nix { inherit lib callInventoryAdapter; };
|
||||
settings = import ./settings.nix { inherit lib createTestClan; };
|
||||
specialArgs = import ./specialArgs.nix { inherit lib createTestClan; };
|
||||
resolve_module_spec = import ./import_module_spec.nix {
|
||||
inherit lib createTestClan;
|
||||
};
|
||||
test_simple =
|
||||
let
|
||||
res = callInventoryAdapter {
|
||||
res = createTestClan {
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
# It isn't exactly doing anything but it's a valid module that produces an output
|
||||
@@ -71,7 +61,7 @@ in
|
||||
};
|
||||
};
|
||||
# User config
|
||||
instances."instance_foo" = {
|
||||
inventory.instances."instance_foo" = {
|
||||
module = {
|
||||
name = "simple-module";
|
||||
};
|
||||
@@ -81,7 +71,7 @@ in
|
||||
{
|
||||
# Test that the module is mapped into the output
|
||||
# We might change the attribute name in the future
|
||||
expr = res.importedModulesEvaluated ? "<clan-core>-simple-module";
|
||||
expr = res.config._services.mappedServices ? "<clan-core>-simple-module";
|
||||
expected = true;
|
||||
inherit res;
|
||||
};
|
||||
@@ -92,7 +82,7 @@ in
|
||||
# All instances should be included within one evaluation to make all of them available
|
||||
test_module_grouping =
|
||||
let
|
||||
res = callInventoryAdapter {
|
||||
res = createTestClan {
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
# It isn't exactly doing anything but it's a valid module that produces an output
|
||||
@@ -112,18 +102,19 @@ in
|
||||
|
||||
perMachine = { }: { };
|
||||
};
|
||||
|
||||
# User config
|
||||
instances."instance_foo" = {
|
||||
inventory.instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
};
|
||||
};
|
||||
instances."instance_bar" = {
|
||||
inventory.instances."instance_bar" = {
|
||||
module = {
|
||||
name = "B";
|
||||
};
|
||||
};
|
||||
instances."instance_baz" = {
|
||||
inventory.instances."instance_baz" = {
|
||||
module = {
|
||||
name = "A";
|
||||
};
|
||||
@@ -133,16 +124,16 @@ in
|
||||
{
|
||||
# Test that the module is mapped into the output
|
||||
# We might change the attribute name in the future
|
||||
expr = lib.mapAttrs (_n: v: builtins.length v) res.grouped;
|
||||
expected = {
|
||||
"<clan-core>-A" = 2;
|
||||
"<clan-core>-B" = 1;
|
||||
};
|
||||
expr = lib.attrNames res.config._services.mappedServices;
|
||||
expected = [
|
||||
"<clan-core>-A"
|
||||
"<clan-core>-B"
|
||||
];
|
||||
};
|
||||
|
||||
test_creates_all_instances =
|
||||
let
|
||||
res = callInventoryAdapter {
|
||||
res = createTestClan {
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
# It isn't exactly doing anything but it's a valid module that produces an output
|
||||
@@ -154,6 +145,7 @@ in
|
||||
|
||||
perMachine = { }: { };
|
||||
};
|
||||
inventory = {
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
@@ -173,11 +165,12 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
# Test that the module is mapped into the output
|
||||
# We might change the attribute name in the future
|
||||
expr = lib.attrNames res.importedModulesEvaluated.self-A.instances;
|
||||
expr = lib.attrNames res.config._services.mappedServices.self-A.instances;
|
||||
expected = [
|
||||
"instance_bar"
|
||||
"instance_foo"
|
||||
@@ -187,7 +180,7 @@ in
|
||||
# Membership via roles
|
||||
test_add_machines_directly =
|
||||
let
|
||||
res = callInventoryAdapter {
|
||||
res = createTestClan {
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
# It isn't exactly doing anything but it's a valid module that produces an output
|
||||
@@ -202,6 +195,7 @@ in
|
||||
|
||||
# perMachine = {}: {};
|
||||
};
|
||||
inventory = {
|
||||
machines = {
|
||||
jon = { };
|
||||
sara = { };
|
||||
@@ -229,11 +223,12 @@ in
|
||||
roles.peer.tags.all = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
# Test that the module is mapped into the output
|
||||
# We might change the attribute name in the future
|
||||
expr = lib.attrNames res.importedModulesEvaluated.self-A.result.allMachines;
|
||||
expr = lib.attrNames res.config._services.mappedServices.self-A.result.allMachines;
|
||||
expected = [
|
||||
"jon"
|
||||
"sara"
|
||||
@@ -243,7 +238,7 @@ in
|
||||
# Membership via tags
|
||||
test_add_machines_via_tags =
|
||||
let
|
||||
res = callInventoryAdapter {
|
||||
res = createTestClan {
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
# It isn't exactly doing anything but it's a valid module that produces an output
|
||||
@@ -257,6 +252,7 @@ in
|
||||
|
||||
# perMachine = {}: {};
|
||||
};
|
||||
inventory = {
|
||||
machines = {
|
||||
jon = {
|
||||
tags = [ "foo" ];
|
||||
@@ -281,11 +277,12 @@ in
|
||||
roles.peer.tags.all = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
# Test that the module is mapped into the output
|
||||
# We might change the attribute name in the future
|
||||
expr = lib.attrNames res.importedModulesEvaluated.self-A.result.allMachines;
|
||||
expr = lib.attrNames res.config._services.mappedServices.self-A.result.allMachines;
|
||||
expected = [
|
||||
"jon"
|
||||
"sara"
|
||||
@@ -293,6 +290,9 @@ in
|
||||
};
|
||||
|
||||
machine_imports = import ./machine_imports.nix { inherit lib clanLib; };
|
||||
per_machine_args = import ./per_machine_args.nix { inherit lib callInventoryAdapter; };
|
||||
per_instance_args = import ./per_instance_args.nix { inherit lib callInventoryAdapter; };
|
||||
per_machine_args = import ./per_machine_args.nix { inherit lib createTestClan; };
|
||||
per_instance_args = import ./per_instance_args.nix {
|
||||
inherit lib;
|
||||
callInventoryAdapter = createTestClan;
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{ callInventoryAdapter, ... }:
|
||||
{ createTestClan, ... }:
|
||||
let
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
@@ -23,12 +23,15 @@ let
|
||||
|
||||
resolve =
|
||||
spec:
|
||||
callInventoryAdapter {
|
||||
inherit modules machines;
|
||||
createTestClan {
|
||||
inherit modules;
|
||||
inventory = {
|
||||
inherit machines;
|
||||
instances."instance_foo" = {
|
||||
module = spec;
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
test_import_local_module_by_name = {
|
||||
@@ -36,25 +39,16 @@ in
|
||||
(resolve {
|
||||
name = "A";
|
||||
input = "self";
|
||||
}).importedModuleWithInstances.instance_foo.resolvedModule;
|
||||
expected = {
|
||||
_class = "clan.service";
|
||||
manifest = {
|
||||
name = "network";
|
||||
};
|
||||
};
|
||||
}).config._services.mappedServices.self-A.manifest.name;
|
||||
expected = "network";
|
||||
};
|
||||
test_import_remote_module_by_name = {
|
||||
expr =
|
||||
(resolve {
|
||||
name = "uzzi";
|
||||
input = "upstream";
|
||||
}).importedModuleWithInstances.instance_foo.resolvedModule;
|
||||
expected = {
|
||||
_class = "clan.service";
|
||||
manifest = {
|
||||
name = "uzzi-from-upstream";
|
||||
};
|
||||
};
|
||||
}).config._services.mappedServices.upstream-uzzi.manifest.name;
|
||||
expected = "uzzi-from-upstream";
|
||||
|
||||
};
|
||||
}
|
||||
@@ -58,7 +58,10 @@ let
|
||||
sara = { };
|
||||
};
|
||||
res = callInventoryAdapter {
|
||||
inherit modules machines;
|
||||
inherit modules;
|
||||
|
||||
inventory = {
|
||||
inherit machines;
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
@@ -93,6 +96,7 @@ let
|
||||
roles.peer.tags.all = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
1 { imports = [ { instanceName = "instance_foo"; machine = { name = "jon"; roles = [ "controller" "pe 1 null
|
||||
@@ -105,9 +109,10 @@ in
|
||||
{
|
||||
# settings should evaluate
|
||||
test_per_instance_arguments = {
|
||||
inherit res;
|
||||
expr = {
|
||||
instanceName =
|
||||
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName;
|
||||
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName;
|
||||
|
||||
# settings are specific.
|
||||
# Below we access:
|
||||
@@ -115,11 +120,11 @@ in
|
||||
# roles = peer
|
||||
# machines = jon
|
||||
settings =
|
||||
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings;
|
||||
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings;
|
||||
machine =
|
||||
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.machine;
|
||||
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.machine;
|
||||
roles =
|
||||
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.roles;
|
||||
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.roles;
|
||||
};
|
||||
expected = {
|
||||
instanceName = "instance_foo";
|
||||
@@ -137,6 +142,7 @@ in
|
||||
settings = { };
|
||||
};
|
||||
};
|
||||
settings = { };
|
||||
};
|
||||
peer = {
|
||||
machines = {
|
||||
@@ -146,6 +152,9 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
settings = {
|
||||
timeout = "foo-peer";
|
||||
};
|
||||
};
|
||||
};
|
||||
settings = {
|
||||
@@ -156,9 +165,9 @@ in
|
||||
|
||||
# TODO: Cannot be tested like this anymore
|
||||
test_per_instance_settings_vendoring = {
|
||||
x = res.importedModulesEvaluated.self-A;
|
||||
x = res.config._services.mappedServices.self-A;
|
||||
expr =
|
||||
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.vendoredSettings;
|
||||
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.vendoredSettings;
|
||||
expected = {
|
||||
timeout = "config.thing";
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
{ lib, callInventoryAdapter }:
|
||||
{ lib, createTestClan }:
|
||||
let
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
@@ -39,8 +39,11 @@ let
|
||||
jon = { };
|
||||
sara = { };
|
||||
};
|
||||
res = callInventoryAdapter {
|
||||
inherit modules machines;
|
||||
res = createTestClan {
|
||||
inherit modules;
|
||||
inventory = {
|
||||
|
||||
inherit machines;
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
@@ -70,6 +73,7 @@ let
|
||||
roles.peer.tags.all = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
@@ -79,7 +83,7 @@ in
|
||||
inherit res;
|
||||
expr = {
|
||||
hasMachineSettings =
|
||||
res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon
|
||||
res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon
|
||||
? settings;
|
||||
|
||||
# settings are specific.
|
||||
@@ -88,10 +92,10 @@ in
|
||||
# roles = peer
|
||||
# machines = jon
|
||||
specificMachineSettings =
|
||||
res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon.settings;
|
||||
res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon.settings;
|
||||
|
||||
hasRoleSettings =
|
||||
res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer
|
||||
res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer
|
||||
? settings;
|
||||
|
||||
# settings are specific.
|
||||
@@ -100,21 +104,26 @@ in
|
||||
# roles = peer
|
||||
# machines = *
|
||||
specificRoleSettings =
|
||||
res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer;
|
||||
res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer;
|
||||
};
|
||||
expected = rec {
|
||||
expected = {
|
||||
hasMachineSettings = true;
|
||||
hasRoleSettings = false;
|
||||
hasRoleSettings = true;
|
||||
specificMachineSettings = {
|
||||
timeout = "foo-peer-jon";
|
||||
};
|
||||
specificRoleSettings = {
|
||||
machines = {
|
||||
jon = {
|
||||
settings = specificMachineSettings;
|
||||
settings = {
|
||||
timeout = "foo-peer-jon";
|
||||
};
|
||||
};
|
||||
};
|
||||
settings = {
|
||||
timeout = "foo-peer";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{ callInventoryAdapter, lib, ... }:
|
||||
{ createTestClan, lib, ... }:
|
||||
let
|
||||
res = callInventoryAdapter {
|
||||
res = createTestClan {
|
||||
modules."A" = {
|
||||
_class = "clan.service";
|
||||
manifest = {
|
||||
@@ -21,6 +21,8 @@ let
|
||||
};
|
||||
};
|
||||
};
|
||||
inventory = {
|
||||
|
||||
machines = {
|
||||
jon = { };
|
||||
sara = { };
|
||||
@@ -41,8 +43,9 @@ let
|
||||
roles.peer.machines.sara = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = res.servicesEval.config.mappedServices.self-A;
|
||||
config = res.config._services.mappedServices.self-A;
|
||||
|
||||
#
|
||||
applySettings =
|
||||
@@ -1,6 +1,6 @@
|
||||
{ callInventoryAdapter, lib, ... }:
|
||||
{ createTestClan, lib, ... }:
|
||||
let
|
||||
res = callInventoryAdapter {
|
||||
res = createTestClan {
|
||||
modules."A" = m: {
|
||||
_class = "clan.service";
|
||||
config = {
|
||||
@@ -14,6 +14,7 @@ let
|
||||
default = m;
|
||||
};
|
||||
};
|
||||
inventory = {
|
||||
machines = {
|
||||
jon = { };
|
||||
};
|
||||
@@ -25,8 +26,9 @@ let
|
||||
roles.peer.machines.jon = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
specialArgs = lib.attrNames res.servicesEval.config.mappedServices.self-A.test.specialArgs;
|
||||
specialArgs = lib.attrNames res.config._services.mappedServices.self-A.test.specialArgs;
|
||||
in
|
||||
{
|
||||
test_simple = {
|
||||
@@ -1,12 +0,0 @@
|
||||
{ clan-core }:
|
||||
{
|
||||
_class = "clan";
|
||||
_module.args = {
|
||||
inherit clan-core;
|
||||
inherit (clan-core) clanLib;
|
||||
};
|
||||
imports = [
|
||||
./module.nix
|
||||
./interface.nix
|
||||
];
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{ self, lib, ... }:
|
||||
{
|
||||
flake.modules.clan.default = lib.modules.importApply ./default.nix { clan-core = self; };
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
# 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
|
||||
;
|
||||
};
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
imports = [
|
||||
./interface.nix
|
||||
];
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{ 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;
|
||||
};
|
||||
};
|
||||
}));
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
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"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
{
|
||||
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;
|
||||
|
||||
};
|
||||
}
|
||||
@@ -62,6 +62,9 @@
|
||||
# Core libraries
|
||||
(root + "/lib")
|
||||
|
||||
# modules directory
|
||||
(root + "/modules")
|
||||
|
||||
# User-provided fileset
|
||||
fileset
|
||||
];
|
||||
|
||||
@@ -81,6 +81,7 @@ in
|
||||
description = null;
|
||||
icon = null;
|
||||
name = "test";
|
||||
tld = "clan";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -105,7 +106,7 @@ in
|
||||
self = {
|
||||
inputs = { };
|
||||
};
|
||||
directory = ../../.;
|
||||
directory = ../.;
|
||||
meta.name = "test-clan-core";
|
||||
};
|
||||
in
|
||||
@@ -123,7 +124,7 @@ in
|
||||
self = {
|
||||
inputs = { };
|
||||
};
|
||||
directory = ../../.;
|
||||
directory = ../.;
|
||||
meta.name = "test-clan-core";
|
||||
};
|
||||
in
|
||||
@@ -211,6 +212,87 @@ 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 =
|
||||
let
|
||||
eval = clan {
|
||||
@@ -0,0 +1 @@
|
||||
foo-machine
|
||||
@@ -0,0 +1 @@
|
||||
foo-shared
|
||||
25
lib/vars.nix
Normal file
25
lib/vars.nix
Normal file
@@ -0,0 +1,25 @@
|
||||
_: {
|
||||
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";
|
||||
}
|
||||
3
lib/vars_test.nix
Normal file
3
lib/vars_test.nix
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
16
modules/clan/checks.nix
Normal file
16
modules/clan/checks.nix
Normal file
@@ -0,0 +1,16 @@
|
||||
{ lib, nixpkgs, ... }:
|
||||
{
|
||||
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;
|
||||
---
|
||||
'';
|
||||
};
|
||||
}
|
||||
25
modules/clan/default.nix
Normal file
25
modules/clan/default.nix
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
Root 'clan' Module
|
||||
|
||||
Defines lib.clan and flake-parts.clan options
|
||||
and all common logic for the 'clan' module.
|
||||
|
||||
- has Class _class = "clan"
|
||||
|
||||
- _module.args.clan-core: reference to clan-core flake
|
||||
- _module.args.clanLib: reference to lib.clan function
|
||||
*/
|
||||
{ clan-core }:
|
||||
{
|
||||
_class = "clan";
|
||||
_module.args = {
|
||||
inherit clan-core;
|
||||
inherit (clan-core) clanLib;
|
||||
};
|
||||
imports = [
|
||||
./top-level-interface.nix
|
||||
./module.nix
|
||||
./distributed-services.nix
|
||||
./checks.nix
|
||||
];
|
||||
}
|
||||
163
modules/clan/distributed-services.nix
Normal file
163
modules/clan/distributed-services.nix
Normal file
@@ -0,0 +1,163 @@
|
||||
{
|
||||
lib,
|
||||
clanLib,
|
||||
config,
|
||||
clan-core,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib) mkOption types;
|
||||
# Keep a reference to top-level
|
||||
clanConfig = config;
|
||||
|
||||
inventory = clanConfig.inventory;
|
||||
flakeInputs = clanConfig.self.inputs;
|
||||
clanCoreModules = clan-core.clan.modules;
|
||||
|
||||
grouped = lib.foldlAttrs (
|
||||
acc: instanceName: instance:
|
||||
let
|
||||
inputName = if instance.module.input == null then "<clan-core>" else instance.module.input;
|
||||
id = inputName + "-" + instance.module.name;
|
||||
in
|
||||
acc
|
||||
// {
|
||||
${id} = acc.${id} or [ ] ++ [
|
||||
{
|
||||
inherit instanceName instance;
|
||||
}
|
||||
];
|
||||
}
|
||||
) { } importedModuleWithInstances;
|
||||
|
||||
importedModuleWithInstances = lib.mapAttrs (
|
||||
instanceName: instance:
|
||||
let
|
||||
resolvedModule = clanLib.resolveModule {
|
||||
moduleSpec = instance.module;
|
||||
inherit flakeInputs clanCoreModules;
|
||||
};
|
||||
|
||||
# Every instance includes machines via roles
|
||||
# :: { client :: ... }
|
||||
instanceRoles = lib.mapAttrs (
|
||||
roleName: role:
|
||||
let
|
||||
resolvedMachines = clanLib.inventory.resolveTags {
|
||||
members = {
|
||||
# Explicit members
|
||||
machines = lib.attrNames role.machines;
|
||||
# Resolved Members
|
||||
tags = lib.attrNames role.tags;
|
||||
};
|
||||
inherit (inventory) machines;
|
||||
inherit instanceName roleName;
|
||||
};
|
||||
in
|
||||
# instances.<instanceName>.roles.<roleName> =
|
||||
# Remove "tags", they are resolved into "machines"
|
||||
(removeAttrs role [ "tags" ])
|
||||
// {
|
||||
machines = lib.genAttrs resolvedMachines.machines (
|
||||
machineName:
|
||||
let
|
||||
machineSettings = instance.roles.${roleName}.machines.${machineName}.settings or { };
|
||||
in
|
||||
# TODO: tag settings
|
||||
# Wait for this feature until option introspection for 'settings' is done.
|
||||
# This might get too complex to handle otherwise.
|
||||
# settingsViaTags = lib.filterAttrs (
|
||||
# tagName: _: machineHasTag machineName tagName
|
||||
# ) instance.roles.${roleName}.tags;
|
||||
{
|
||||
# TODO: Do we want to wrap settings with
|
||||
# setDefaultModuleLocation "inventory.instances.${instanceName}.roles.${roleName}.tags.${tagName}";
|
||||
settings = {
|
||||
imports = [
|
||||
machineSettings
|
||||
]; # ++ lib.attrValues (lib.mapAttrs (_tagName: v: v.settings) settingsViaTags);
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
) instance.roles;
|
||||
in
|
||||
{
|
||||
inherit (instance) module;
|
||||
inherit resolvedModule instanceRoles;
|
||||
}
|
||||
) inventory.instances or { };
|
||||
in
|
||||
{
|
||||
_class = "clan";
|
||||
options._services = mkOption {
|
||||
visible = false;
|
||||
description = ''
|
||||
All service instances
|
||||
|
||||
!!! Danger "Internal API"
|
||||
Do not rely on this API yet.
|
||||
|
||||
- Will be renamed to just 'services' in the future.
|
||||
Once the name can be claimed again.
|
||||
- Structure will change.
|
||||
|
||||
API will be declared as public after beeing simplified.
|
||||
'';
|
||||
type = types.submoduleWith {
|
||||
# TODO: Remove specialArgs
|
||||
specialArgs = {
|
||||
inherit clanLib;
|
||||
};
|
||||
modules = [
|
||||
(import ../../lib/inventory/distributed-service/all-services-wrapper.nix {
|
||||
inherit (clanConfig) directory;
|
||||
})
|
||||
# Dependencies
|
||||
{
|
||||
exportsModule = clanConfig.exportsModule;
|
||||
}
|
||||
{
|
||||
# TODO: Rename to "allServices"
|
||||
# All services
|
||||
mappedServices = lib.mapAttrs (_module_ident: instances: {
|
||||
imports = [
|
||||
# Import the resolved module.
|
||||
# i.e. clan.modules.admin
|
||||
{
|
||||
options.module = lib.mkOption {
|
||||
type = lib.types.raw;
|
||||
default = (builtins.head instances).instance.module;
|
||||
};
|
||||
}
|
||||
(builtins.head instances).instance.resolvedModule
|
||||
] # Include all the instances that correlate to the resolved module
|
||||
++ (builtins.map (v: {
|
||||
instances.${v.instanceName}.roles = v.instance.instanceRoles;
|
||||
}) instances);
|
||||
}) grouped;
|
||||
}
|
||||
];
|
||||
};
|
||||
default = { };
|
||||
};
|
||||
options._allMachines = mkOption {
|
||||
internal = true;
|
||||
type = types.raw;
|
||||
default = lib.mapAttrs (machineName: _: {
|
||||
# This is the list of nixosModules for each machine
|
||||
machineImports = lib.foldlAttrs (
|
||||
acc: _module_ident: serviceModule:
|
||||
acc ++ [ serviceModule.result.final.${machineName}.nixosModule or { } ]
|
||||
) [ ] config._services.mappedServices;
|
||||
}) inventory.machines or { };
|
||||
};
|
||||
|
||||
config = {
|
||||
clanInternals.inventoryClass.machines = config._allMachines;
|
||||
# clanInternals.inventoryClass.distributedServices = config._services;
|
||||
|
||||
# Exports from distributed services
|
||||
exports = config._services.exports;
|
||||
};
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
clanModule,
|
||||
clanLib,
|
||||
clan-core,
|
||||
}:
|
||||
let
|
||||
eval = lib.evalModules {
|
||||
modules = [
|
||||
clan-core.modules.clan.default
|
||||
clanModule
|
||||
];
|
||||
specialArgs = {
|
||||
self = clan-core;
|
||||
};
|
||||
};
|
||||
|
||||
evalDocs = pkgs.nixosOptionsDoc {
|
||||
options = eval.options;
|
||||
warningsAreErrors = false;
|
||||
transformOptions = clan-core.clanLib.docs.stripStorePathsFromDeclarations;
|
||||
transformOptions = clanLib.docs.stripStorePathsFromDeclarations;
|
||||
};
|
||||
in
|
||||
{
|
||||
27
modules/clan/flake-module.nix
Normal file
27
modules/clan/flake-module.nix
Normal file
@@ -0,0 +1,27 @@
|
||||
{ self, lib, ... }:
|
||||
let
|
||||
clanModule = lib.modules.importApply ./default.nix { clan-core = self; };
|
||||
in
|
||||
{
|
||||
flake.modules.clan.default = clanModule;
|
||||
perSystem =
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
jsonDocs = import ./eval-docs.nix {
|
||||
clan-core = self;
|
||||
inherit
|
||||
pkgs
|
||||
lib
|
||||
clanModule
|
||||
;
|
||||
clanLib = self.clanLib;
|
||||
};
|
||||
in
|
||||
{
|
||||
legacyPackages.clan-options = jsonDocs.optionsJSON;
|
||||
};
|
||||
}
|
||||
@@ -100,7 +100,7 @@ let
|
||||
_: machine:
|
||||
machine.extendModules {
|
||||
modules = [
|
||||
(lib.modules.importApply ../machineModules/overridePkgs.nix {
|
||||
(lib.modules.importApply ../../nixosModules/machineModules/overridePkgs.nix {
|
||||
pkgs = pkgsFor.${system};
|
||||
})
|
||||
];
|
||||
@@ -167,6 +167,9 @@ in
|
||||
{ ... }@args:
|
||||
let
|
||||
_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 ''
|
||||
Your version of nixpkgs is incompatible with the latest clan.
|
||||
Please update nixpkgs input to the latest nixos-unstable or nixpkgs-unstable.
|
||||
@@ -176,7 +179,7 @@ in
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
(lib.modules.importApply ../machineModules/forName.nix {
|
||||
(lib.modules.importApply ../../nixosModules/machineModules/forName.nix {
|
||||
inherit (config.inventory) meta;
|
||||
inherit
|
||||
name
|
||||
@@ -216,43 +219,44 @@ in
|
||||
inherit nixosConfigurations;
|
||||
inherit darwinConfigurations;
|
||||
|
||||
exports = config.clanInternals.inventoryClass.distributedServices.servicesEval.config.exports;
|
||||
|
||||
clanInternals = {
|
||||
inventoryClass =
|
||||
let
|
||||
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
|
||||
{
|
||||
_module.args = {
|
||||
inherit clanLib;
|
||||
};
|
||||
imports = [
|
||||
../inventoryClass/builder/default.nix
|
||||
(lib.modules.importApply ../inventoryClass/service-list-from-inputs.nix {
|
||||
inherit flakeInputs clanLib;
|
||||
})
|
||||
../inventoryClass/default.nix
|
||||
{
|
||||
inherit inventory directory;
|
||||
inherit
|
||||
inventory
|
||||
directory
|
||||
flakeInputs
|
||||
relativeDirectory
|
||||
;
|
||||
exportsModule = config.exportsModule;
|
||||
}
|
||||
(
|
||||
let
|
||||
clanConfig = config;
|
||||
in
|
||||
{ config, ... }:
|
||||
{ ... }:
|
||||
{
|
||||
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
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
/**
|
||||
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,
|
||||
...
|
||||
@@ -288,7 +288,7 @@ in
|
||||
Global information about the clan.
|
||||
'';
|
||||
type = types.deferredModuleWith {
|
||||
staticModules = [ ../inventoryClass/meta-interface.nix ];
|
||||
staticModules = [ ../inventoryClass/meta.nix ];
|
||||
};
|
||||
default = { };
|
||||
};
|
||||
5
modules/flake-module.nix
Normal file
5
modules/flake-module.nix
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
imports = [
|
||||
./clan/flake-module.nix
|
||||
];
|
||||
}
|
||||
156
modules/inventoryClass/default.nix
Normal file
156
modules/inventoryClass/default.nix
Normal file
@@ -0,0 +1,156 @@
|
||||
{
|
||||
lib,
|
||||
clanLib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib) types mkOption;
|
||||
submodule = m: types.submoduleWith { modules = [ m ]; };
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
exposedInventory = lib.intersectAttrs {
|
||||
meta = null;
|
||||
machines = null;
|
||||
instances = null;
|
||||
tags = null;
|
||||
} config.inventory;
|
||||
|
||||
filterAttrsRecursive' =
|
||||
path: pred: set:
|
||||
lib.listToAttrs (
|
||||
lib.concatMap (
|
||||
name:
|
||||
let
|
||||
v = set.${name};
|
||||
loc = path ++ [ name ];
|
||||
in
|
||||
if pred loc v then
|
||||
[
|
||||
(lib.nameValuePair name (if lib.isAttrs v then filterAttrsRecursive' loc pred v else v))
|
||||
]
|
||||
else
|
||||
[ ]
|
||||
) (lib.attrNames set)
|
||||
);
|
||||
|
||||
filteredInventory = filterAttrsRecursive' [ ] (
|
||||
# Remove extraModules from serialization,
|
||||
# identified by: prefix + pathLength + name
|
||||
# inventory.instances.*.roles.*.extraModules
|
||||
path: _value: !(lib.length path == 5 && ((lib.last path)) == "extraModules")
|
||||
) exposedInventory;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
flakeInputs = mkOption {
|
||||
type = types.raw;
|
||||
};
|
||||
exportsModule = mkOption {
|
||||
type = types.raw;
|
||||
};
|
||||
|
||||
inventory = mkOption {
|
||||
type = types.raw;
|
||||
};
|
||||
inventorySerialization = mkOption {
|
||||
type = types.raw;
|
||||
readOnly = true;
|
||||
default = filteredInventory;
|
||||
};
|
||||
directory = mkOption {
|
||||
type = types.path;
|
||||
};
|
||||
relativeDirectory = mkOption {
|
||||
type = types.str;
|
||||
internal = true;
|
||||
description = ''
|
||||
The relative directory path from the flake root to the clan directory.
|
||||
Empty string if directory equals the flake root.
|
||||
'';
|
||||
};
|
||||
machines = mkOption {
|
||||
type = types.attrsOf (submodule ({
|
||||
options = {
|
||||
machineImports = mkOption {
|
||||
type = types.listOf types.raw;
|
||||
};
|
||||
};
|
||||
}));
|
||||
};
|
||||
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"
|
||||
];
|
||||
};
|
||||
|
||||
staticModules = lib.mkOption {
|
||||
readOnly = true;
|
||||
type = lib.types.raw;
|
||||
|
||||
apply = moduleSet: lib.mapAttrs (inspectModule "<clan-core>") moduleSet;
|
||||
};
|
||||
modulesPerSource = lib.mkOption {
|
||||
# { sourceName :: { moduleName :: {} }}
|
||||
readOnly = true;
|
||||
type = lib.types.raw;
|
||||
default =
|
||||
let
|
||||
inputsWithModules = lib.filterAttrs (_inputName: v: v ? clan.modules) config.flakeInputs;
|
||||
in
|
||||
lib.mapAttrs (
|
||||
inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules
|
||||
) inputsWithModules;
|
||||
};
|
||||
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;
|
||||
};
|
||||
templatesPerSource = lib.mkOption {
|
||||
# { sourceName :: { moduleName :: {} }}
|
||||
readOnly = true;
|
||||
type = lib.types.raw;
|
||||
default =
|
||||
let
|
||||
inputsWithTemplates = lib.filterAttrs (_inputName: v: v ? clan.templates) config.flakeInputs;
|
||||
in
|
||||
lib.mapAttrs (_inputName: v: lib.mapAttrs (_n: t: t) v.clan.templates) inputsWithTemplates;
|
||||
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -115,7 +115,7 @@ in
|
||||
meta = lib.mkOption {
|
||||
type = lib.types.submoduleWith {
|
||||
modules = [
|
||||
./meta-interface.nix
|
||||
./meta.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
@@ -167,7 +167,7 @@ in
|
||||
'';
|
||||
type = types.submoduleWith {
|
||||
specialArgs = {
|
||||
inherit (config) machines;
|
||||
inherit (config) machines clanLib;
|
||||
};
|
||||
modules = [
|
||||
{
|
||||
@@ -359,7 +359,7 @@ in
|
||||
inherit clanLib;
|
||||
};
|
||||
}
|
||||
(import ./roles-interface.nix { })
|
||||
(import ./role.nix { })
|
||||
];
|
||||
}
|
||||
);
|
||||
@@ -31,6 +31,20 @@ let
|
||||
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
|
||||
{
|
||||
@@ -44,12 +44,6 @@ in
|
||||
description = ''
|
||||
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
|
||||
**The import only happens if the machine is part of the service or role.**
|
||||
|
||||
@@ -73,15 +67,8 @@ in
|
||||
}
|
||||
```
|
||||
'';
|
||||
apply = value: if lib.isString value then value else builtins.seq (builtins.toJSON value) value;
|
||||
default = [ ];
|
||||
type = types.listOf (
|
||||
types.oneOf [
|
||||
types.str
|
||||
types.path
|
||||
(types.attrsOf types.anything)
|
||||
]
|
||||
);
|
||||
type = types.listOf types.raw;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -106,6 +106,14 @@ in
|
||||
# Set by the flake, so it's read-only in the machine
|
||||
readOnly = true;
|
||||
};
|
||||
tld = lib.mkOption {
|
||||
type = types.strMatching "[a-z]+";
|
||||
description = ''
|
||||
the TLD for the clan
|
||||
'';
|
||||
# Set by the flake, so it's read-only in the machine
|
||||
readOnly = true;
|
||||
};
|
||||
machine = mkOption {
|
||||
description = ''
|
||||
Settings of the machine.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
directory,
|
||||
meta,
|
||||
}:
|
||||
# The following is a nixos/darwin module
|
||||
{
|
||||
_class,
|
||||
lib,
|
||||
@@ -20,7 +21,7 @@
|
||||
);
|
||||
|
||||
clan.core.settings = {
|
||||
inherit (meta) name icon;
|
||||
inherit (meta) name icon tld;
|
||||
inherit directory;
|
||||
machine = {
|
||||
inherit name;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user