Compare commits

..

4 Commits

Author SHA1 Message Date
DavHau
a0b16e30b6 wireguard: optional internet access for external peers
Add an option to allow external peers to route all their internet traffic through a controller.

Also add options to enable ipv4 and assign v4 addresses manually as this is needed for proper internet access for some regions
2025-10-21 11:49:37 +07:00
DavHau
9e36b00b48 wireguard: make external peers connect to all controllers 2025-10-21 11:49:37 +07:00
DavHau
c48be6b34f wireguard/test: update vars 2025-10-21 11:49:37 +07:00
DavHau
6a3f5e077b wireguard: add support for external peers
This adds support for external peers via the instance option `roles.controller.<name>.settings.externalPeers = ["external1"]`.

External peers are peers which are not associated wth a machine inside the clan, for example a mobile phone or a device that cannot be managed via clan for some reason.
2025-10-21 11:49:37 +07:00
253 changed files with 4321 additions and 10533 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,6 @@
options = { options = {
host = lib.mkOption { host = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "";
description = '' description = ''
ip address or hostname (domain) of the machine ip address or hostname (domain) of the machine
''; '';
@@ -32,15 +31,17 @@
}; };
perInstance = perInstance =
{ {
instanceName, roles,
settings, lib,
machine,
... ...
}: }:
{ {
exports.networking = {
exports."internet/${instanceName}/default/${machine.name}".networking = { # TODO add user space network support to clan-cli
hosts = [ settings.host ]; peers = lib.mapAttrs (_name: machine: {
host.plain = machine.settings.host;
SSHOptions = map (_x: "-J x") machine.settings.jumphosts;
}) roles.default.machines;
}; };
}; };
}; };

View File

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

View File

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

View File

@@ -1,18 +1,19 @@
{ {
"data": "ENC[AES256_GCM,data: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,iv:dGSmyDNBdVyF54bYS/Zxm2NNXZyGtLjkyYlrI9/nKvc=,tag:ip2fy76NjObWbW20HyuZUA==,type:str]", "data": "ENC[AES256_GCM,data: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,iv:q3j8EAokyyxiszf+wyRqxEr2igaD1bX7YnFx/NbsGg8=,tag:HKKYWRJEUwW2/TxL+5dSng==,type:str]",
"sops": { "sops": {
"age": [ "age": [
{ {
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck", "recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNQS96MUFubVdOR2VCc2xO\ncTc5QnNHNTFpdURnSnF3dVhBQXQ3bnBuRW1RCngzSVlhSW9rNUxoSWdKcEtKVXc3\nQitLZ2NDUXBSUmxtVWpYRUlvOHVXcW8KLS0tIGZaWlRVak9NYmt2elpwYStYenRE\nanlkT3BET1FjQ2lFZkp3SXFMSkJSaVkKKkr+MNNqs6Ve3K5OrZfBEGlnc7OAthqf\nOZrP9NYOTMgkvhFsZTVpUS0zskry0iwmTNt+KeluYf0Tko8K53Kx2A==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaeXRjU214aWk5ajl1aW9E\naGJlb1ViaVRmMTBHdkFDQUNDZS94WFZiNUNvCllmWTJBck9hR3U3V09VWDZwQ2xI\nd3ZEQnBIUG5ZSTVIdS8rQ2FMYVhyNk0KLS0tIEE1UG8rSzFyU01sVXhGVHpoaE9i\nSis4Qi9tMGFqbTNMTDZUVk1ZdXkrM28Km4VkfaOsZ69ckjvrg+os43H/O1IoWHzC\nt4LqZRz1Tk7/d1aLWavSPPjVYrCOMZeNBqGbQpGfjjuXrafClRNQdQ==\n-----END AGE ENCRYPTED FILE-----\n"
}, },
{ {
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXd2dVYmJIbUVVeXk5Nk1E\nekFiUldVVUhRTmE4dHRiTHNDdEMyS1pRV1RrCkNScGdXVSs4UU5id29DV0pZWDQr\nenV1QmpnOFk5aFpTTUxmb0hDVHZDdFkKLS0tIHpmalJtRC94bEhaUStmeUlHT21w\nd3o3UzJHZklxK0RCYUUxc2c3aG1XclkKEPq1ZgyGiAK/Hy4zT7wfdDfPEE3vMHpR\nzwQV5y3M3DmlnKQEvJu0DpQ334CyAcubZC7cswQdUrM8TPqJhb/TuA==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3R1RHTGViTnRLVVkyM3J0\nbm96cGVPTlo4NXBNL0g1eEVSNG9DUkgwVFRBCmRKVTlMRmV3Tmg2RTZIclBlWlcr\ndzI5MUxhcllzbE1IMDNxa08zVkpITmsKLS0tIG01Y2dyQkY3UmRudFk2d0p6bThn\nemlaWnZoS3p4VHhMTFFwTm9VN0ttYzQKVbLFgtK6NIRIiryWHeeOPD45iwUds4QD\n7b8xYYoxlo+DETggxK6Vz3IdT/BSK5bFtgAxl864b5gW+Aw4c6AO5w==\n-----END AGE ENCRYPTED FILE-----\n"
} }
], ],
"lastmodified": "2025-10-21T13:17:17Z", "lastmodified": "2025-09-18T14:33:37Z",
"mac": "ENC[AES256_GCM,data:wdAFURkJZvclbz3UFPSPV9fma7zrZVEhMhsRqylGQMLepX/WohEAr8nJgeHl05be1Q8M8biPXCCoL0vfwg4BRZOkhD8PusJh8iBI3+STNQe/S1qoIK1ByfBFhJD+tIsVsgduLp6G32e6SRNvkuX3UpJqyViuRUavfQd3b8LRU4I=,iv:S3sMNTz5Kg4TxHj1tnk/ayiFuO74dR4aPnnomtkGByo=,tag:uive2bYe42s6VtPd03jTMw==,type:str]", "mac": "ENC[AES256_GCM,data:XKCnd0QrAlOCECSeSvbLYHMLbmUh4fMRnLaTb5ARoP4Zc9joWGsCaRZxokc2/sG4BXA/6pkbQXHyIOudKbcBpVjjvs9E+6Mnzt53nfRoH/iOkYPbN2EO49okVZJXW0M1rlBxrxvGuiDlz2p2p6L7neKLy4EB482pYea5+dUr2Yw=,iv:oj/MkZCfkvCmAb79uzEvKwEAm1bKtWhS4rPRAWSgRgw=,tag:h5TPPILXkhJplnDT2Gqtfw==,type:str]",
"version": "3.11.0" "unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -133,6 +133,75 @@ graph TB
### Advanced Options ### 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 ### Automatic Hostname Resolution

View File

@@ -54,10 +54,7 @@
- For other controllers: The controller's /56 subnet - For other controllers: The controller's /56 subnet
*/ */
{ { ... }:
clanLib,
...
}:
let let
# Shared module for extraHosts configuration # Shared module for extraHosts configuration
extraHostsModule = extraHostsModule =
@@ -77,12 +74,10 @@ let
controllerHosts = lib.mapAttrsToList ( controllerHosts = lib.mapAttrsToList (
name: _value: name: _value:
let let
prefix = clanLib.vars.getPublicValue { prefix = builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = name; + "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value"
generator = "wireguard-network-${instanceName}"; );
file = "prefix";
};
# Controller IP is always ::1 in their subnet # Controller IP is always ::1 in their subnet
ip = prefix + "::1"; ip = prefix + "::1";
in in
@@ -93,30 +88,48 @@ let
peerHosts = lib.mapAttrsToList ( peerHosts = lib.mapAttrsToList (
peerName: peerValue: peerName: peerValue:
let let
peerSuffix = clanLib.vars.getPublicValue { peerSuffix = builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = peerName; + "/vars/per-machine/${peerName}/wireguard-network-${instanceName}/suffix/value"
generator = "wireguard-network-${instanceName}"; );
file = "suffix";
};
# Determine designated controller # Determine designated controller
designatedController = designatedController =
if (builtins.length (builtins.attrNames roles.controller.machines) == 1) then if (builtins.length (builtins.attrNames roles.controller.machines) == 1) then
(builtins.head (builtins.attrNames roles.controller.machines)) (builtins.head (builtins.attrNames roles.controller.machines))
else else
peerValue.settings.controller; peerValue.settings.controller;
controllerPrefix = clanLib.vars.getPublicValue { controllerPrefix = builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = designatedController; + "/vars/per-machine/${designatedController}/wireguard-network-${instanceName}/prefix/value"
generator = "wireguard-network-${instanceName}"; );
file = "prefix";
};
peerIP = controllerPrefix + ":" + peerSuffix; peerIP = controllerPrefix + ":" + peerSuffix;
in in
"${peerIP} ${peerName}.${domain}" "${peerIP} ${peerName}.${domain}"
) roles.peer.machines; ) 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;
in
"${peerIP} ${peer}.${domain}"
) (roles.controller.machines.${ctrlName}.settings.externalPeers)
) roles.controller.machines
);
in in
builtins.concatStringsSep "\n" (controllerHosts ++ peerHosts); builtins.concatStringsSep "\n" (controllerHosts ++ peerHosts ++ externalPeerHosts);
}; };
# Shared interface options # Shared interface options
@@ -229,12 +242,10 @@ in
lib.mapAttrsToList ( lib.mapAttrsToList (
ctrlName: _: ctrlName: _:
let let
controllerPrefix = clanLib.vars.getPublicValue { controllerPrefix = builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = ctrlName; + "/vars/per-machine/${ctrlName}/wireguard-network-${instanceName}/prefix/value"
generator = "wireguard-network-${instanceName}"; );
file = "prefix";
};
peerIP = controllerPrefix + ":" + peerSuffix; peerIP = controllerPrefix + ":" + peerSuffix;
in in
"${peerIP}/56" "${peerIP}/56"
@@ -245,22 +256,20 @@ in
# Connect to all controllers # Connect to all controllers
peers = lib.mapAttrsToList (name: value: { peers = lib.mapAttrsToList (name: value: {
publicKey = clanLib.vars.getPublicValue { publicKey = (
flake = config.clan.core.settings.directory; builtins.readFile (
machine = name; config.clan.core.settings.directory
generator = "wireguard-keys-${instanceName}"; + "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
file = "publickey"; )
}; );
# Allow each controller's /56 subnet # Allow each controller's /56 subnet
allowedIPs = [ allowedIPs = [
"${ "${
clanLib.vars.getPublicValue { builtins.readFile (
flake = config.clan.core.settings.directory; config.clan.core.settings.directory
machine = name; + "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value"
generator = "wireguard-network-${instanceName}"; )
file = "prefix";
}
}::/56" }::/56"
]; ];
@@ -281,12 +290,89 @@ in
{ {
imports = [ sharedInterface ]; imports = [ sharedInterface ];
options.endpoint = lib.mkOption { options = {
type = lib.types.str; endpoint = lib.mkOption {
example = "vpn.clan.lol"; type = lib.types.str;
description = '' example = "vpn.clan.lol";
Endpoint where the controller can be reached 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 = perInstance =
@@ -309,7 +395,37 @@ in
}: }:
let let
allOtherControllers = lib.filterAttrs (name: _v: name != machine.name) roles.controller.machines; allOtherControllers = lib.filterAttrs (name: _v: name != machine.name) roles.controller.machines;
allPeers = roles.peer.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;
in in
{ {
imports = [ imports = [
@@ -323,99 +439,172 @@ in
; ;
}) })
]; ];
# Network allocation generator for this controller # Network prefix allocation generator for this controller
clan.core.vars.generators."wireguard-network-${instanceName}" = { clan.core.vars.generators = {
files.prefix.secret = false; "wireguard-network-${instanceName}" = {
files.prefix.secret = false;
runtimeInputs = with pkgs; [ runtimeInputs = with pkgs; [
python3 python3
]; ];
# Invalidate on network or hostname changes # Invalidate on network or hostname changes
validation.hostname = machine.name; validation.hostname = machine.name;
script = '' script = ''
${pkgs.python3}/bin/python3 ${./ipv6_allocator.py} "$out" "${instanceName}" controller "${machine.name}" ${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 each other
boot.kernel.sysctl = {
"net.ipv6.conf.all.forwarding" = 1;
}
// lib.optionalAttrs settings.ipv4.enable {
"net.ipv4.conf.all.forwarding" = 1;
}; };
# Enable ip forwarding, so wireguard peers can reach eachother
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
networking.firewall.allowedUDPPorts = [ settings.port ]; 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 # Single wireguard interface
networking.wireguard.interfaces."${instanceName}" = { networking.wireguard.interfaces."${instanceName}" = {
listenPort = settings.port; listenPort = settings.port;
ips = [ ips = [
# Controller uses ::1 in its /56 subnet but with /40 prefix for proper routing "${thisControllerPrefix}::1/40"
"${config.clan.core.vars.generators."wireguard-network-${instanceName}".files.prefix.value}::1/40" ]
]; ++ lib.optional settings.ipv4.enable settings.ipv4.address;
privateKeyFile = privateKeyFile =
config.clan.core.vars.generators."wireguard-keys-${instanceName}".files."privatekey".path; config.clan.core.vars.generators."wireguard-keys-${instanceName}".files."privatekey".path;
# Connect to all peers and other controllers # Connect to all peers and other controllers
peers = lib.mapAttrsToList ( peers =
name: value: # Peers configuration
if allPeers ? ${name} then (lib.mapAttrsToList (name: _value: {
# For peers: they now have our entire /56 subnet publicKey = (
{ builtins.readFile (
publicKey = clanLib.vars.getPublicValue { config.clan.core.settings.directory
flake = config.clan.core.settings.directory; + "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
machine = name; )
generator = "wireguard-keys-${instanceName}"; );
file = "publickey";
};
# Allow the peer's /96 range in ALL controller subnets # Allow the peer's /96 range in ALL controller subnets
allowedIPs = lib.mapAttrsToList ( allowedIPs = lib.mapAttrsToList (
ctrlName: _: ctrlName: _: "${controllerPrefix ctrlName}:${peerSuffix name}/96"
let ) roles.controller.machines;
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
"${controllerPrefix}:${peerSuffix}/96"
) roles.controller.machines;
persistentKeepalive = 25; persistentKeepalive = 25;
} }) allPeers)
else ++
# For other controllers: use their /56 subnet # External peers configuration - includes all external peers from all controllers
{ (map (
publicKey = clanLib.vars.getPublicValue { peer:
flake = config.clan.core.settings.directory; let
machine = name; # IPv6 allowed IPs for mesh communication
generator = "wireguard-keys-${instanceName}"; ipv6AllowedIPs = lib.mapAttrsToList (
file = "publickey"; ctrlName: _: "${controllerPrefix ctrlName}:${externalPeerSuffix peer}/96"
}; ) roles.controller.machines;
allowedIPs = [ # IPv4 allowed IP (only if this controller manages this peer and has IPv4 enabled)
"${ ipv4AllowedIPs = lib.optional (
clanLib.vars.getPublicValue { settings.ipv4.enable
flake = config.clan.core.settings.directory; && settings.externalPeers ? ${peer}
machine = name; && settings.externalPeers.${peer}.ipv4.address != null
generator = "wireguard-network-${instanceName}"; ) settings.externalPeers.${peer}.ipv4.address;
file = "prefix"; in
} {
}::/56" publicKey = (
]; builtins.readFile (
config.clan.core.settings.directory
+ "/vars/shared/wireguard-network-${instanceName}-external-peer-${peer}/publickey/value"
)
);
allowedIPs = ipv6AllowedIPs ++ ipv4AllowedIPs;
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"
)
);
allowedIPs = [ "${controllerPrefix name}::/56" ];
endpoint = "${value.settings.endpoint}:${toString value.settings.port}"; endpoint = "${value.settings.endpoint}:${toString value.settings.port}";
persistentKeepalive = 25; persistentKeepalive = 25;
} }) allOtherControllers);
) (allPeers // allOtherControllers);
}; };
}; };
}; };
@@ -435,7 +624,7 @@ in
let let
isController = isController =
instanceInfo.roles ? controller && instanceInfo.roles.controller.machines ? ${machine.name}; instanceInfo.roles ? controller && instanceInfo.roles.controller.machines ? ${machine.name};
isPeer = instanceInfo.roles ? peer && instanceInfo.roles.peer.machines ? ${machine.name}; isPeer = instanceInfo.roles ? peer && instanceInfo.roles.peer.machines or { } ? ${machine.name};
in in
lib.optional (isController && isPeer) { lib.optional (isController && isPeer) {
inherit instanceName; inherit instanceName;

View File

@@ -1,5 +1,6 @@
{ {
lib, lib,
config,
... ...
}: }:
@@ -10,7 +11,28 @@ let
"peer1" "peer1"
"peer2" "peer2"
"peer3" "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 in
{ {
name = "wireguard"; name = "wireguard";
@@ -47,10 +69,19 @@ in
roles.controller.machines."controller1".settings = { roles.controller.machines."controller1".settings = {
endpoint = "192.168.1.1"; 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 = { roles.controller.machines."controller2".settings = {
endpoint = "192.168.1.2"; endpoint = "192.168.1.2";
# add the same external peer to controller2 to test multi-controller connection
externalPeers.external1 = { };
}; };
roles.peer.machines = { roles.peer.machines = {
@@ -77,11 +108,77 @@ 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 = '' testScript = ''
start_all() start_all()
# Show all addresses # Start network on all machines including external1
machines = [peer1, peer2, peer3, controller1, controller2] machines = [peer1, peer2, peer3, controller1, controller2, external1]
for m in machines: for m in machines:
m.systemctl("start network-online.target") m.systemctl("start network-online.target")
@@ -93,10 +190,39 @@ in
print("STARTING PING TESTS") print("STARTING PING TESTS")
print("="*60) print("="*60)
for m1 in machines: # Test mesh connectivity between regular clan machines
for m2 in machines: clan_machines = [peer1, peer2, peer3, controller1, controller2]
for m1 in clan_machines:
for m2 in clan_machines:
if m1 != m2: if m1 != m2:
print(f"\n--- Pinging from {m1.name} to {m2.name}.wg-test-one ---") 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") 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")
''; '';
} }

View File

@@ -0,0 +1 @@
966fa6c74dbea65154816755934180edaf20fc031b15622c3e4433516c08fbbc

View File

@@ -0,0 +1 @@
966fa6c74dbea65154816755934180edaf20fc031b15622c3e4433516c08fbbc

View File

@@ -0,0 +1,14 @@
{
"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"
}
}

View File

@@ -0,0 +1 @@
966fa6c74dbea65154816755934180edaf20fc031b15622c3e4433516c08fbbc

View File

@@ -0,0 +1,14 @@
{
"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"
}
}

View File

@@ -0,0 +1 @@
bb+pM+fpJzqc1A+dvTdsE4JliVvdMMXoQUPaElkrs0w=

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,4 @@
{ { ... }:
clanLib,
directory,
...
}:
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/zerotier"; manifest.name = "clan-core/zerotier";
@@ -17,23 +13,21 @@
instanceName, instanceName,
roles, roles,
lib, lib,
machine,
... ...
}: }:
{ {
exports.networking = {
exports."internet/${instanceName}/peer/${machine.name}".networking = { priority = lib.mkDefault 900;
hosts = lib.flatten [ # TODO add user space network support to clan-cli
(clanLib.vars.getPublicValue { module = "clan_lib.network.zerotier";
flake = directory; peers = lib.mapAttrs (name: _machine: {
machine = machine.name; host.var = {
machine = name;
generator = "zerotier"; generator = "zerotier";
file = "zerotier-ip"; file = "zerotier-ip";
# default = throw "kaputt"; };
}) }) roles.peer.machines;
];
}; };
nixosModule = nixosModule =
{ {
config, config,
@@ -45,7 +39,6 @@
imports = [ imports = [
(import ./shared.nix { (import ./shared.nix {
inherit inherit
clanLib
instanceName instanceName
roles roles
config config
@@ -97,7 +90,6 @@
imports = [ imports = [
(import ./shared.nix { (import ./shared.nix {
inherit inherit
clanLib
instanceName instanceName
roles roles
config config
@@ -150,7 +142,6 @@
imports = [ imports = [
(import ./shared.nix { (import ./shared.nix {
inherit inherit
clanLib
instanceName instanceName
roles roles
config config
@@ -169,16 +160,15 @@
); );
networkIps = builtins.foldl' ( networkIps = builtins.foldl' (
ips: name: ips: name:
let if
ztIp = clanLib.vars.getPublicValue { builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value"
flake = config.clan.core.settings.directory; then
machine = name; ips
generator = "zerotier"; ++ [
file = "zerotier-ip"; (builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value")
default = null; ]
}; else
in ips
if ztIp != null then ips ++ [ ztIp ] else ips
) [ ] machines; ) [ ] machines;
allHostIPs = settings.allowedIps ++ networkIps; allHostIPs = settings.allowedIps ++ networkIps;
in in

View File

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

View File

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

24
devFlake/flake.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

24
flake.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,12 +16,10 @@ lib.fix (
*/ */
callLib = file: args: import file ({ inherit lib clanLib; } // args); callLib = file: args: import file ({ inherit lib clanLib; } // args);
checkConfig = clanLib.callLib ./clan/checkConfig.nix { }; evalService = clanLib.callLib ./modules/inventory/distributed-service/evalService.nix { };
evalService = clanLib.callLib ./evalService.nix { };
# ------------------------------------ # ------------------------------------
# ClanLib functions # ClanLib functions
inventory = clanLib.callLib ./inventory { }; inventory = clanLib.callLib ./modules/inventory { };
test = clanLib.callLib ./test { }; test = clanLib.callLib ./test { };
flake-inputs = clanLib.callLib ./flake-inputs.nix { }; flake-inputs = clanLib.callLib ./flake-inputs.nix { };
# Custom types # Custom types
@@ -32,17 +30,12 @@ lib.fix (
jsonschema = import ./jsonschema { inherit lib; }; jsonschema = import ./jsonschema { inherit lib; };
docs = import ./docs.nix { inherit lib; }; docs = import ./docs.nix { inherit lib; };
vars = import ./vars.nix { inherit lib; };
# flakes # flakes
flakes = clanLib.callLib ./flakes.nix { }; flakes = clanLib.callLib ./flakes.nix { };
# TODO: Flatten our lib functions like this: # TODO: Flatten our lib functions like this:
resolveModule = clanLib.callLib ./resolve-module { }; resolveModule = clanLib.callLib ./resolve-module { };
# Functions to help define exports
exports = clanLib.callLib ./exports.nix { };
fs = { fs = {
inherit (builtins) pathExists readDir; inherit (builtins) pathExists readDir;
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,7 +81,6 @@ let
applySettings = applySettings =
instanceName: instance: instanceName: instance:
lib.mapAttrs (roleName: role: { lib.mapAttrs (roleName: role: {
settings = config.instances.${instanceName}.roles.${roleName}.finalSettings.config;
machines = lib.mapAttrs (machineName: _v: { machines = lib.mapAttrs (machineName: _v: {
settings = settings =
config.instances.${instanceName}.roles.${roleName}.machines.${machineName}.finalSettings.config; config.instances.${instanceName}.roles.${roleName}.machines.${machineName}.finalSettings.config;
@@ -159,29 +158,6 @@ in
( (
{ name, ... }@role: { name, ... }@role:
{ {
options.finalSettings = mkOption {
default = evalMachineSettings instance.name role.name null role.config.settings { };
type = types.raw;
description = ''
Final evaluated settings of the curent-machine
This contains the merged and evaluated settings of the role interface,
the role settings and the machine settings.
Type: 'configuration' as returned by 'lib.evalModules'
'';
apply = lib.warn ''
=== WANRING ===
'roles.<roleName>.settings' do not contain machine specific settings.
Prefer `machines.<machineName>.settings` instead. (i.e `perInstance: roles.<roleName>.machines.<machineName>.settings`)
If you have a use-case that requires access to the original role settings without machine overrides.
Contact us via matrix (https://matrix.to/#/#clan:clan.lol) or file an issue: https://git.clan.lol
This feature will be removed in the next release
'';
};
# instances.{instanceName}.roles.{roleName}.machines # instances.{instanceName}.roles.{roleName}.machines
options.machines = mkOption { options.machines = mkOption {
description = '' description = ''
@@ -240,7 +216,7 @@ in
options.extraModules = lib.mkOption { options.extraModules = lib.mkOption {
default = [ ]; default = [ ];
type = types.listOf types.deferredModule; type = types.listOf (types.either types.deferredModule types.str);
}; };
} }
) )
@@ -504,12 +480,9 @@ in
staticModules = [ staticModules = [
({ ({
options.exports = mkOption { options.exports = mkOption {
type = types.lazyAttrsOf types.deferredModule; type = types.deferredModule;
default = { }; default = { };
description = '' description = ''
!!! Danger "Experimental Feature"
This feature is experimental and will change in the future.
export modules defined in 'perInstance' export modules defined in 'perInstance'
mapped to their instance name mapped to their instance name
@@ -634,21 +607,10 @@ in
type = types.deferredModuleWith { type = types.deferredModuleWith {
staticModules = [ staticModules = [
({ ({
# exports."///".generator.name = { _file ... import = []; _type = }
# exports."///".networking = { _file ... import = []; }
# generators."///".name = { name, ...}: { _file ... import = [];}
# networks."///" = { _file ... import = []; }
# { _file ... import = []; }
# { _file ... import = []; }
options.exports = mkOption { options.exports = mkOption {
type = types.lazyAttrsOf types.deferredModule; type = types.deferredModule;
default = { }; default = { };
description = '' description = ''
!!! Danger "Experimental Feature"
This feature is experimental and will change in the future.
export modules defined in 'perMachine' export modules defined in 'perMachine'
mapped to their machine name mapped to their machine name
@@ -750,9 +712,6 @@ in
exports = mkOption { exports = mkOption {
description = '' description = ''
!!! Danger "Experimental Feature"
This feature is experimental and will change in the future.
This services exports. This services exports.
Gets merged with all other services exports. Gets merged with all other services exports.
@@ -775,38 +734,79 @@ in
``` ```
''; '';
default = { }; default = { };
type = types.lazyAttrsOf ( type = types.submoduleWith {
types.deferredModuleWith { # Static modules
# staticModules = []; modules = [
# lib.concatLists ( {
# lib.concatLists ( options.instances = mkOption {
# lib.mapAttrsToList ( type = types.attrsOf types.deferredModule;
# _roleName: role: description = ''
# lib.mapAttrsToList ( export modules defined in 'perInstance'
# _instanceName: instance: lib.mapAttrsToList (_machineName: v: v.exports) instance.allMachines mapped to their instance name
# ) role.allInstances
# ) config.result.allRoles Example
# )
# ) with instances:
# ++
} ```nix
); instances.A = { ... };
# # Lazy default via imports instances.B= { ... };
# # should probably be moved to deferredModuleWith { staticModules = [ ]; }
# imports = roles.peer.perInstance = { instanceName, machine, ... }:
# if config._docs_rendering then {
# [ ] exports.foo = 1;
# else }
# lib.mapAttrsToList (_roleName: role: {
# instances = lib.mapAttrs (_instanceName: instance: { This yields all other services can access these exports
# imports = lib.mapAttrsToList (_machineName: v: v.exports) instance.allMachines; =>
# }) role.allInstances; exports.instances.A.foo = 1;
# }) config.result.allRoles exports.instances.B.foo = 1;
# ++ lib.mapAttrsToList (machineName: machine: { ```
# machines.${machineName} = machine.exports; '';
# }) config.result.allMachines; };
# } options.machines = mkOption {
# ]; type = types.attrsOf types.deferredModule;
description = ''
export modules defined in 'perMachine'
mapped to their machine name
Example
with machines:
```nix
instances.A = { roles.peer.machines.jon = ... };
instances.B = { roles.peer.machines.jon = ... };
perMachine = { machine, ... }:
{
exports.foo = 1;
}
This yields all other services can access these exports
=>
exports.machines.jon.foo = 1;
exports.machines.sara.foo = 1;
```
'';
};
# Lazy default via imports
# should probably be moved to deferredModuleWith { staticModules = [ ]; }
imports =
if config._docs_rendering then
[ ]
else
lib.mapAttrsToList (_roleName: role: {
instances = lib.mapAttrs (_instanceName: instance: {
imports = lib.mapAttrsToList (_machineName: v: v.exports) instance.allMachines;
}) role.allInstances;
}) config.result.allRoles
++ lib.mapAttrsToList (machineName: machine: {
machines.${machineName} = machine.exports;
}) config.result.allMachines;
}
];
};
}; };
# --- # ---
# Place the result in _module.result to mark them as "internal" and discourage usage/overrides # Place the result in _module.result to mark them as "internal" and discourage usage/overrides
@@ -850,11 +850,7 @@ in
instanceRes.nixosModule instanceRes.nixosModule
] ]
++ (map ( ++ (map (
s: s: if builtins.typeOf s == "string" then "${directory}/${s}" else s
if builtins.typeOf s == "string" then
lib.warn "String types for 'extraModules' will be deprecated - ${s}" "${directory}/${s}"
else
lib.setDefaultModuleLocation "via inventory.instances.${instanceName}.roles.${roleName}" s
) instanceCfg.roles.${roleName}.extraModules); ) instanceCfg.roles.${roleName}.extraModules);
}; };
} }
@@ -991,39 +987,5 @@ in
} }
) config.result.allMachines; ) config.result.allMachines;
}; };
debug = mkOption {
default = lib.zipAttrsWith (_name: values: { imports = values; }) (
lib.mapAttrsToList (_machineName: machine: machine.exports) config.result.allMachines
);
};
}; };
imports = [
{
# collect exports from all machines
# zipAttrs is needed until we use the record type.
exports = lib.zipAttrsWith (_name: values: { imports = values; }) (
lib.mapAttrsToList (_machineName: machine: machine.exports) config.result.allMachines
);
}
{
# collect exports from all instances, roles and machines
# zipAttrs is needed until we use the record type.
exports = lib.zipAttrsWith (_name: values: { imports = values; }) (
lib.concatLists (
lib.concatLists (
lib.mapAttrsToList (
_roleName: role:
lib.mapAttrsToList (
_instanceName: instance: lib.mapAttrsToList (_machineName: v: v.exports) instance.allMachines
) role.allInstances
) config.result.allRoles
)
)
);
}
];
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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