Compare commits
4 Commits
push-unvrq
...
wireguard
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0b16e30b6 | ||
|
|
9e36b00b48 | ||
|
|
c48be6b34f | ||
|
|
6a3f5e077b |
@@ -1,10 +1,8 @@
|
||||
clanServices/.* @pinpox @kenji
|
||||
|
||||
lib/test/container-test-driver/.* @DavHau @mic92
|
||||
lib/inventory/.* @hsjobeki
|
||||
lib/inventoryClass/.* @hsjobeki
|
||||
|
||||
modules/.* @hsjobeki
|
||||
lib/modules/inventory/.* @hsjobeki
|
||||
lib/modules/inventoryClass/.* @hsjobeki
|
||||
|
||||
pkgs/clan-app/ui/.* @hsjobeki @brianmcgee
|
||||
pkgs/clan-app/clan_app/.* @qubasa @hsjobeki
|
||||
|
||||
@@ -87,7 +87,6 @@ in
|
||||
# Container Tests
|
||||
nixos-test-container = self.clanLib.test.containerTest ./container nixosTestArgs;
|
||||
nixos-systemd-abstraction = self.clanLib.test.containerTest ./systemd-abstraction nixosTestArgs;
|
||||
nixos-llm-test = self.clanLib.test.containerTest ./llm nixosTestArgs;
|
||||
nixos-test-user-firewall-iptables = self.clanLib.test.containerTest ./user-firewall/iptables.nix nixosTestArgs;
|
||||
nixos-test-user-firewall-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs;
|
||||
nixos-test-extra-python-packages = self.clanLib.test.containerTest ./test-extra-python-packages nixosTestArgs;
|
||||
|
||||
@@ -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)
|
||||
'';
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -62,6 +62,6 @@ in
|
||||
peer1.succeed("chmod 640 /var/log/journal/*/user-1000.journal*")
|
||||
|
||||
# Run tests as text-user (environment variables are set automatically)
|
||||
peer1.succeed("su - text-user -c 'pytest -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'")
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
clanLib,
|
||||
...
|
||||
}:
|
||||
{ ... }:
|
||||
let
|
||||
sharedInterface =
|
||||
{ lib, ... }:
|
||||
@@ -54,15 +51,15 @@ let
|
||||
builtins.foldl' (
|
||||
urls: name:
|
||||
let
|
||||
ip = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "zerotier";
|
||||
file = "zerotier-ip";
|
||||
default = null;
|
||||
};
|
||||
ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value";
|
||||
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 { })))
|
||||
);
|
||||
|
||||
@@ -159,14 +156,9 @@ in
|
||||
readHostKey =
|
||||
machine:
|
||||
let
|
||||
publicKey = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
inherit machine;
|
||||
generator = "data-mesher-host-key";
|
||||
file = "public_key";
|
||||
};
|
||||
path = "${config.clan.core.settings.directory}/vars/per-machine/${machine}/data-mesher-host-key/public_key/value";
|
||||
in
|
||||
builtins.elemAt (lib.splitString "\n" publicKey) 1;
|
||||
builtins.elemAt (lib.splitString "\n" (builtins.readFile path)) 1;
|
||||
in
|
||||
{
|
||||
enable = true;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
options = {
|
||||
host = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
ip address or hostname (domain) of the machine
|
||||
'';
|
||||
@@ -32,15 +31,17 @@
|
||||
};
|
||||
perInstance =
|
||||
{
|
||||
instanceName,
|
||||
settings,
|
||||
machine,
|
||||
roles,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
exports."internet/${instanceName}/default/${machine.name}".networking = {
|
||||
hosts = [ settings.host ];
|
||||
exports.networking = {
|
||||
# TODO add user space network support to clan-cli
|
||||
peers = lib.mapAttrs (_name: machine: {
|
||||
host.plain = machine.settings.host;
|
||||
SSHOptions = map (_x: "-J x") machine.settings.jumphosts;
|
||||
}) roles.default.machines;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -44,10 +44,8 @@
|
||||
pkgs.openssl
|
||||
];
|
||||
|
||||
# TODO: Implement automated certificate rotation instead of using a 100-year expiration
|
||||
script = ''
|
||||
openssl req -x509 -nodes -newkey rsa:4096 \
|
||||
-days 36500 \
|
||||
-keyout "$out"/key \
|
||||
-out "$out"/crt \
|
||||
-subj "/C=US/ST=CA/L=San Francisco/O=Example Corp/OU=IT/CN=example.com"
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
k1PNWTkgLsXO8DNkCudxcvPElXfmaw6zwaLrZys=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -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": {
|
||||
"age": [
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"mac": "ENC[AES256_GCM,data:wdAFURkJZvclbz3UFPSPV9fma7zrZVEhMhsRqylGQMLepX/WohEAr8nJgeHl05be1Q8M8biPXCCoL0vfwg4BRZOkhD8PusJh8iBI3+STNQe/S1qoIK1ByfBFhJD+tIsVsgduLp6G32e6SRNvkuX3UpJqyViuRUavfQd3b8LRU4I=,iv:S3sMNTz5Kg4TxHj1tnk/ayiFuO74dR4aPnnomtkGByo=,tag:uive2bYe42s6VtPd03jTMw==,type:str]",
|
||||
"version": "3.11.0"
|
||||
"lastmodified": "2025-09-18T14:33:37Z",
|
||||
"mac": "ENC[AES256_GCM,data:XKCnd0QrAlOCECSeSvbLYHMLbmUh4fMRnLaTb5ARoP4Zc9joWGsCaRZxokc2/sG4BXA/6pkbQXHyIOudKbcBpVjjvs9E+6Mnzt53nfRoH/iOkYPbN2EO49okVZJXW0M1rlBxrxvGuiDlz2p2p6L7neKLy4EB482pYea5+dUr2Yw=,iv:oj/MkZCfkvCmAb79uzEvKwEAm1bKtWhS4rPRAWSgRgw=,tag:h5TPPILXkhJplnDT2Gqtfw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{ settings, roles, ... }:
|
||||
{ settings, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
@@ -38,19 +38,8 @@
|
||||
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;
|
||||
files.id_ed25519.deploy = false;
|
||||
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;
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
This a test README just to appease the eval warnings if we don't have one
|
||||
@@ -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
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
'';
|
||||
}
|
||||
@@ -22,7 +22,6 @@ in
|
||||
../../clanServices/syncthing
|
||||
# Required modules
|
||||
../../nixosModules/clanCore
|
||||
../../nixosModules/machineModules
|
||||
# Dependencies like clan-cli
|
||||
../../pkgs/clan-cli
|
||||
];
|
||||
|
||||
@@ -41,14 +41,14 @@ let
|
||||
# In this case it is 'self-zerotier-redux'
|
||||
# This is usually only used internally, but we can use it to test the evaluation of service module in isolation
|
||||
# evaluatedService =
|
||||
# testFlake.clanInternals.inventoryClass.distributedServices.servicesEval.config.mappedServices.self-zerotier-redux.config;
|
||||
# testFlake.clanInternals.inventoryClass.distributedServices.importedModulesEvaluated.self-zerotier-redux.config;
|
||||
in
|
||||
{
|
||||
test_simple = {
|
||||
inherit testFlake;
|
||||
|
||||
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;
|
||||
|
||||
# expr = {
|
||||
|
||||
@@ -133,6 +133,75 @@ graph TB
|
||||
|
||||
### Advanced Options
|
||||
|
||||
#### External Peers
|
||||
|
||||
External peers are devices outside of your clan (like phones, laptops, etc.) that can connect to the mesh network through controllers. Each external peer gets its own keypair and can be configured with specific options.
|
||||
|
||||
##### IPv6-only external peers
|
||||
|
||||
```nix
|
||||
{
|
||||
instances = {
|
||||
wireguard = {
|
||||
module.name = "wireguard";
|
||||
module.input = "clan-core";
|
||||
roles.controller.machines.server1.settings = {
|
||||
endpoint = "vpn.example.com";
|
||||
# Define external peers with configuration options
|
||||
externalPeers = {
|
||||
dave = {
|
||||
# No internet access - can only reach clan mesh
|
||||
allowInternetAccess = false;
|
||||
};
|
||||
moms-phone = {
|
||||
# Internet access enabled - IPv6 traffic routed through VPN
|
||||
allowInternetAccess = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
roles.peer.machines.laptop1 = {};
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### IPv4 support for external peers
|
||||
|
||||
If you need IPv4 internet access for external peers, you can enable IPv4 on the controller and assign IPv4 addresses to external peers:
|
||||
|
||||
```nix
|
||||
{
|
||||
instances = {
|
||||
wireguard = {
|
||||
module.name = "wireguard";
|
||||
module.input = "clan-core";
|
||||
roles.controller.machines.server1.settings = {
|
||||
endpoint = "vpn.example.com";
|
||||
# Enable IPv4 with controller's address
|
||||
ipv4.enable = true;
|
||||
ipv4.address = "10.42.1.1/24";
|
||||
externalPeers = {
|
||||
dave = {
|
||||
# No internet access - can only reach clan mesh
|
||||
allowInternetAccess = false;
|
||||
ipv4.address = "10.42.1.50/32";
|
||||
};
|
||||
moms-phone = {
|
||||
# Internet access enabled - IPv4 and IPv6 traffic routed through VPN
|
||||
allowInternetAccess = true;
|
||||
ipv4.address = "10.42.1.51/32";
|
||||
};
|
||||
};
|
||||
};
|
||||
roles.peer.machines.laptop1 = {};
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** IPv4 addresses for external peers are only used for internet access through the controller, not for mesh communication (which uses IPv6).
|
||||
|
||||
External peers can connect to multiple controllers by adding the same peer name to multiple controllers' `externalPeers` configuration.
|
||||
|
||||
### Automatic Hostname Resolution
|
||||
|
||||
|
||||
@@ -54,10 +54,7 @@
|
||||
- For other controllers: The controller's /56 subnet
|
||||
*/
|
||||
|
||||
{
|
||||
clanLib,
|
||||
...
|
||||
}:
|
||||
{ ... }:
|
||||
let
|
||||
# Shared module for extraHosts configuration
|
||||
extraHostsModule =
|
||||
@@ -77,12 +74,10 @@ let
|
||||
controllerHosts = lib.mapAttrsToList (
|
||||
name: _value:
|
||||
let
|
||||
prefix = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "prefix";
|
||||
};
|
||||
prefix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value"
|
||||
);
|
||||
# Controller IP is always ::1 in their subnet
|
||||
ip = prefix + "::1";
|
||||
in
|
||||
@@ -93,30 +88,48 @@ let
|
||||
peerHosts = lib.mapAttrsToList (
|
||||
peerName: peerValue:
|
||||
let
|
||||
peerSuffix = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = peerName;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "suffix";
|
||||
};
|
||||
peerSuffix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${peerName}/wireguard-network-${instanceName}/suffix/value"
|
||||
);
|
||||
# Determine designated controller
|
||||
designatedController =
|
||||
if (builtins.length (builtins.attrNames roles.controller.machines) == 1) then
|
||||
(builtins.head (builtins.attrNames roles.controller.machines))
|
||||
else
|
||||
peerValue.settings.controller;
|
||||
controllerPrefix = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = designatedController;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "prefix";
|
||||
};
|
||||
controllerPrefix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${designatedController}/wireguard-network-${instanceName}/prefix/value"
|
||||
);
|
||||
peerIP = controllerPrefix + ":" + peerSuffix;
|
||||
in
|
||||
"${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
|
||||
builtins.concatStringsSep "\n" (controllerHosts ++ peerHosts);
|
||||
builtins.concatStringsSep "\n" (controllerHosts ++ peerHosts ++ externalPeerHosts);
|
||||
};
|
||||
|
||||
# Shared interface options
|
||||
@@ -229,12 +242,10 @@ in
|
||||
lib.mapAttrsToList (
|
||||
ctrlName: _:
|
||||
let
|
||||
controllerPrefix = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = ctrlName;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "prefix";
|
||||
};
|
||||
controllerPrefix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${ctrlName}/wireguard-network-${instanceName}/prefix/value"
|
||||
);
|
||||
peerIP = controllerPrefix + ":" + peerSuffix;
|
||||
in
|
||||
"${peerIP}/56"
|
||||
@@ -245,22 +256,20 @@ in
|
||||
|
||||
# Connect to all controllers
|
||||
peers = lib.mapAttrsToList (name: value: {
|
||||
publicKey = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-keys-${instanceName}";
|
||||
file = "publickey";
|
||||
};
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
|
||||
)
|
||||
);
|
||||
|
||||
# Allow each controller's /56 subnet
|
||||
allowedIPs = [
|
||||
"${
|
||||
clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "prefix";
|
||||
}
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value"
|
||||
)
|
||||
}::/56"
|
||||
];
|
||||
|
||||
@@ -281,12 +290,89 @@ in
|
||||
{
|
||||
imports = [ sharedInterface ];
|
||||
|
||||
options.endpoint = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "vpn.clan.lol";
|
||||
description = ''
|
||||
Endpoint where the controller can be reached
|
||||
'';
|
||||
options = {
|
||||
endpoint = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "vpn.clan.lol";
|
||||
description = ''
|
||||
Endpoint where the controller can be reached
|
||||
'';
|
||||
};
|
||||
ipv4 = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable IPv4 support for external peers on this controller.
|
||||
When enabled, the controller will have an IPv4 address and can route IPv4 traffic.
|
||||
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
address = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "10.42.1.1/24";
|
||||
description = ''
|
||||
IPv4 address for this controller in CIDR notation.
|
||||
External peers with IPv4 addresses must be within the same subnet.
|
||||
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
};
|
||||
externalPeers = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
allowInternetAccess = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to allow this external peer to access the internet through the controller.
|
||||
When enabled, the controller will route internet traffic for this peer.
|
||||
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
ipv4.address = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "10.42.1.50/32";
|
||||
description = ''
|
||||
IPv4 address for this external peer in CIDR notation.
|
||||
The peer must be within the controller's IPv4 subnet.
|
||||
Only used when the controller has IPv4 enabled.
|
||||
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
example = {
|
||||
dave = {
|
||||
allowInternetAccess = false;
|
||||
};
|
||||
"moms-phone" = {
|
||||
allowInternetAccess = true;
|
||||
ipv4.address = "10.42.1.51/32";
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
External peers that are not part of the clan.
|
||||
|
||||
For every entry here, a key pair for an external device will be generated.
|
||||
This key pair can then be displayed via `clan vars get` and inserted into an external device, like a phone or laptop.
|
||||
|
||||
Each external peer can connect to the mesh through one or more controllers.
|
||||
To connect to multiple controllers, add the same peer name to multiple controllers' `externalPeers`, or simply set set `roles.controller.settings.externalPeers`.
|
||||
|
||||
The external peer names must not collide with machine names in the clan.
|
||||
The machines which are part of the clan will be able to resolve the external peers via their host names, but not vice versa.
|
||||
External peers can still reach machines from within the clan via their IPv6 addresses.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
perInstance =
|
||||
@@ -309,7 +395,37 @@ in
|
||||
}:
|
||||
let
|
||||
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
|
||||
{
|
||||
imports = [
|
||||
@@ -323,99 +439,172 @@ in
|
||||
;
|
||||
})
|
||||
];
|
||||
# Network allocation generator for this controller
|
||||
clan.core.vars.generators."wireguard-network-${instanceName}" = {
|
||||
files.prefix.secret = false;
|
||||
# Network prefix allocation generator for this controller
|
||||
clan.core.vars.generators = {
|
||||
"wireguard-network-${instanceName}" = {
|
||||
files.prefix.secret = false;
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
python3
|
||||
];
|
||||
runtimeInputs = with pkgs; [
|
||||
python3
|
||||
];
|
||||
|
||||
# Invalidate on network or hostname changes
|
||||
validation.hostname = machine.name;
|
||||
# Invalidate on network or hostname changes
|
||||
validation.hostname = machine.name;
|
||||
|
||||
script = ''
|
||||
${pkgs.python3}/bin/python3 ${./ipv6_allocator.py} "$out" "${instanceName}" controller "${machine.name}"
|
||||
'';
|
||||
script = ''
|
||||
${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.extraCommands =
|
||||
let
|
||||
peersWithInternetAccess = lib.filterAttrs (
|
||||
_: peerConfig: peerConfig.allowInternetAccess
|
||||
) settings.externalPeers;
|
||||
|
||||
peerInfo = lib.mapAttrs (
|
||||
peer: peerConfig:
|
||||
let
|
||||
ipv6Address = "${thisControllerPrefix}:${externalPeerSuffix peer}";
|
||||
ipv4Address =
|
||||
if settings.ipv4.enable && peerConfig.ipv4.address != null then
|
||||
lib.head (lib.splitString "/" peerConfig.ipv4.address)
|
||||
else
|
||||
null;
|
||||
in
|
||||
{
|
||||
inherit ipv6Address ipv4Address;
|
||||
}
|
||||
) peersWithInternetAccess;
|
||||
|
||||
in
|
||||
lib.concatStringsSep "\n" (
|
||||
(lib.mapAttrsToList (_peer: info: ''
|
||||
ip6tables -t nat -A POSTROUTING -s ${info.ipv6Address}/128 ! -o '${instanceName}' -j MASQUERADE
|
||||
'') peerInfo)
|
||||
++ (lib.mapAttrsToList (
|
||||
_peer: info:
|
||||
lib.optionalString (info.ipv4Address != null) ''
|
||||
iptables -t nat -A POSTROUTING -s ${info.ipv4Address} ! -o '${instanceName}' -j MASQUERADE
|
||||
''
|
||||
) peerInfo)
|
||||
);
|
||||
|
||||
# Single wireguard interface
|
||||
networking.wireguard.interfaces."${instanceName}" = {
|
||||
listenPort = settings.port;
|
||||
|
||||
ips = [
|
||||
# Controller uses ::1 in its /56 subnet but with /40 prefix for proper routing
|
||||
"${config.clan.core.vars.generators."wireguard-network-${instanceName}".files.prefix.value}::1/40"
|
||||
];
|
||||
"${thisControllerPrefix}::1/40"
|
||||
]
|
||||
++ lib.optional settings.ipv4.enable settings.ipv4.address;
|
||||
|
||||
privateKeyFile =
|
||||
config.clan.core.vars.generators."wireguard-keys-${instanceName}".files."privatekey".path;
|
||||
|
||||
# Connect to all peers and other controllers
|
||||
peers = lib.mapAttrsToList (
|
||||
name: value:
|
||||
if allPeers ? ${name} then
|
||||
# For peers: they now have our entire /56 subnet
|
||||
{
|
||||
publicKey = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-keys-${instanceName}";
|
||||
file = "publickey";
|
||||
};
|
||||
peers =
|
||||
# Peers configuration
|
||||
(lib.mapAttrsToList (name: _value: {
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
|
||||
)
|
||||
);
|
||||
|
||||
# Allow the peer's /96 range in ALL controller subnets
|
||||
allowedIPs = lib.mapAttrsToList (
|
||||
ctrlName: _:
|
||||
let
|
||||
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;
|
||||
# Allow the peer's /96 range in ALL controller subnets
|
||||
allowedIPs = lib.mapAttrsToList (
|
||||
ctrlName: _: "${controllerPrefix ctrlName}:${peerSuffix name}/96"
|
||||
) roles.controller.machines;
|
||||
|
||||
persistentKeepalive = 25;
|
||||
}
|
||||
else
|
||||
# For other controllers: use their /56 subnet
|
||||
{
|
||||
publicKey = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-keys-${instanceName}";
|
||||
file = "publickey";
|
||||
};
|
||||
persistentKeepalive = 25;
|
||||
}) allPeers)
|
||||
++
|
||||
# External peers configuration - includes all external peers from all controllers
|
||||
(map (
|
||||
peer:
|
||||
let
|
||||
# IPv6 allowed IPs for mesh communication
|
||||
ipv6AllowedIPs = lib.mapAttrsToList (
|
||||
ctrlName: _: "${controllerPrefix ctrlName}:${externalPeerSuffix peer}/96"
|
||||
) roles.controller.machines;
|
||||
|
||||
allowedIPs = [
|
||||
"${
|
||||
clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "wireguard-network-${instanceName}";
|
||||
file = "prefix";
|
||||
}
|
||||
}::/56"
|
||||
];
|
||||
# IPv4 allowed IP (only if this controller manages this peer and has IPv4 enabled)
|
||||
ipv4AllowedIPs = lib.optional (
|
||||
settings.ipv4.enable
|
||||
&& settings.externalPeers ? ${peer}
|
||||
&& settings.externalPeers.${peer}.ipv4.address != null
|
||||
) settings.externalPeers.${peer}.ipv4.address;
|
||||
in
|
||||
{
|
||||
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}";
|
||||
persistentKeepalive = 25;
|
||||
}
|
||||
) (allPeers // allOtherControllers);
|
||||
}) allOtherControllers);
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -435,7 +624,7 @@ in
|
||||
let
|
||||
isController =
|
||||
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
|
||||
lib.optional (isController && isPeer) {
|
||||
inherit instanceName;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
|
||||
@@ -10,7 +11,28 @@ let
|
||||
"peer1"
|
||||
"peer2"
|
||||
"peer3"
|
||||
# external machine for external peer testing
|
||||
"external1"
|
||||
];
|
||||
|
||||
controllerPrefix =
|
||||
controllerName:
|
||||
builtins.readFile (
|
||||
config.clan.directory
|
||||
+ "/vars/per-machine/${controllerName}/wireguard-network-wg-test-one/prefix/value"
|
||||
);
|
||||
peerSuffix =
|
||||
peerName:
|
||||
builtins.readFile (
|
||||
config.clan.directory + "/vars/per-machine/${peerName}/wireguard-network-wg-test-one/suffix/value"
|
||||
);
|
||||
# external peer suffixes are stored via shared vars
|
||||
externalPeerSuffix =
|
||||
externalName:
|
||||
builtins.readFile (
|
||||
config.clan.directory
|
||||
+ "/vars/shared/wireguard-network-wg-test-one-external-peer-${externalName}/suffix/value"
|
||||
);
|
||||
in
|
||||
{
|
||||
name = "wireguard";
|
||||
@@ -47,10 +69,19 @@ in
|
||||
|
||||
roles.controller.machines."controller1".settings = {
|
||||
endpoint = "192.168.1.1";
|
||||
# Enable IPv4 for external peers
|
||||
ipv4.enable = true;
|
||||
ipv4.address = "10.42.1.1/24";
|
||||
# add an external peer to controller1 with IPv4
|
||||
externalPeers.external1 = {
|
||||
ipv4.address = "10.42.1.50/32";
|
||||
};
|
||||
};
|
||||
|
||||
roles.controller.machines."controller2".settings = {
|
||||
endpoint = "192.168.1.2";
|
||||
# add the same external peer to controller2 to test multi-controller connection
|
||||
externalPeers.external1 = { };
|
||||
};
|
||||
|
||||
roles.peer.machines = {
|
||||
@@ -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 = ''
|
||||
start_all()
|
||||
|
||||
# Show all addresses
|
||||
machines = [peer1, peer2, peer3, controller1, controller2]
|
||||
# Start network on all machines including external1
|
||||
machines = [peer1, peer2, peer3, controller1, controller2, external1]
|
||||
for m in machines:
|
||||
m.systemctl("start network-online.target")
|
||||
|
||||
@@ -93,10 +190,39 @@ in
|
||||
print("STARTING PING TESTS")
|
||||
print("="*60)
|
||||
|
||||
for m1 in machines:
|
||||
for m2 in machines:
|
||||
# Test mesh connectivity between regular clan machines
|
||||
clan_machines = [peer1, peer2, peer3, controller1, controller2]
|
||||
for m1 in clan_machines:
|
||||
for m2 in clan_machines:
|
||||
if m1 != m2:
|
||||
print(f"\n--- Pinging from {m1.name} to {m2.name}.wg-test-one ---")
|
||||
m1.wait_until_succeeds(f"ping -c1 {m2.name}.wg-test-one >&2")
|
||||
|
||||
# Test that external peer can reach both controllers (multi-controller connection)
|
||||
print("\n--- Testing external1 -> controller1 (direct connection) ---")
|
||||
external1.wait_until_succeeds("ping -c1 controller1.wg-test-one >&2")
|
||||
|
||||
print("\n--- Testing external1 -> controller2 (direct connection) ---")
|
||||
external1.wait_until_succeeds("ping -c1 controller2.wg-test-one >&2")
|
||||
|
||||
# Test IPv4 connectivity
|
||||
print("\n--- Testing external1 -> controller1 (IPv4) ---")
|
||||
external1.wait_until_succeeds("ping -c1 10.42.1.1 >&2")
|
||||
|
||||
# Test that all clan machines can reach the external peer
|
||||
for m in clan_machines:
|
||||
print(f"\n--- Pinging from {m.name} to external1.wg-test-one ---")
|
||||
m.wait_until_succeeds("ping -c1 external1.wg-test-one >&2")
|
||||
|
||||
# Test that external peer can reach a regular peer via controller1
|
||||
print("\n--- Testing external1 -> peer1 (via controller1) ---")
|
||||
external1.wait_until_succeeds("ping -c1 ${controllerPrefix "controller1"}:${peerSuffix "peer1"} >&2")
|
||||
|
||||
# Test controller failover
|
||||
print("\n--- Shutting down controller1 ---")
|
||||
controller1.shutdown()
|
||||
print("\n--- Testing external1 -> peer1 (via controller2 after controller1 shutdown) ---")
|
||||
external1.wait_until_succeeds("ping -c1 ${controllerPrefix "controller2"}:${peerSuffix "peer1"} >&2")
|
||||
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
966fa6c74dbea65154816755934180edaf20fc031b15622c3e4433516c08fbbc
|
||||
@@ -0,0 +1 @@
|
||||
966fa6c74dbea65154816755934180edaf20fc031b15622c3e4433516c08fbbc
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
zrzikusuYoFNu7nF0HX6IdCYbCoZxOUutkVHArvSySc=
|
||||
@@ -0,0 +1 @@
|
||||
3f4b:fde7:06da:8b6a
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -0,0 +1 @@
|
||||
966fa6c74dbea65154816755934180edaf20fc031b15622c3e4433516c08fbbc
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
bb+pM+fpJzqc1A+dvTdsE4JliVvdMMXoQUPaElkrs0w=
|
||||
@@ -0,0 +1 @@
|
||||
3f4b:fde7:06da:8b6a
|
||||
@@ -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.
|
||||
|
||||
Yggdrasil is designed to be a future-proof and decentralised alternative to the
|
||||
structured routing protocols commonly used today on the internet. Inside your
|
||||
clan, it will allow you to reach all of your machines.
|
||||
|
||||
If you have other services in your inventory which export peers (e.g. the
|
||||
`internet` or the services) as [service
|
||||
exports](https://docs.clan.lol/reference/options/clan_service/#exports), they
|
||||
will be added as yggdrasil peers automatically. This allows using the stable
|
||||
yggdrasil IPv6 address to refer to other hosts and letting yggdrasil decide on
|
||||
the best routing based on available connections.
|
||||
Yggdrasil is designed to be a future-proof and decentralised alternative to
|
||||
the structured routing protocols commonly used today on the internet. Inside your clan, it will allow you to reach all of your machines.
|
||||
|
||||
## Example Usage
|
||||
|
||||
|
||||
@@ -29,13 +29,12 @@
|
||||
];
|
||||
};
|
||||
|
||||
options.extraPeers = lib.mkOption {
|
||||
options.peers = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Additional static peers to configure for this host. If you use a
|
||||
VPN clan service, it will automatically be added as peers to other hosts.
|
||||
Local peers are also auto-discovered and don't need to be added.
|
||||
Static peers to configure for this host.
|
||||
If not set, local peers will be auto-discovered
|
||||
'';
|
||||
example = [
|
||||
"tcp://192.168.1.1:6443"
|
||||
@@ -46,74 +45,16 @@
|
||||
};
|
||||
};
|
||||
perInstance =
|
||||
{
|
||||
settings,
|
||||
roles,
|
||||
exports,
|
||||
...
|
||||
}:
|
||||
{ settings, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
clan-core,
|
||||
...
|
||||
}:
|
||||
let
|
||||
|
||||
mkPeers = ip: [
|
||||
# "tcp://${ip}:6443"
|
||||
"quic://${ip}:6443"
|
||||
"ws://${ip}:6443"
|
||||
"tls://${ip}:6443"
|
||||
];
|
||||
|
||||
select' = clan-core.inputs.nix-select.lib.select;
|
||||
|
||||
# TODO make it nicer @lassulus, @picnoir wants microlens
|
||||
# Get a list of all exported IPs from all VPN modules
|
||||
# exportedPeerIPs = builtins.foldl' (
|
||||
# acc: e:
|
||||
# if e == { } then
|
||||
# acc
|
||||
# else
|
||||
# acc ++ (lib.flatten (builtins.filter (s: s != "") (lib.attrValues (select' "peers.*.plain" e))))
|
||||
# ) [ ] (lib.attrValues (select' "*.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 = {
|
||||
|
||||
files.privateKey = { };
|
||||
@@ -158,7 +99,7 @@
|
||||
settings = {
|
||||
PrivateKeyPath = "/key";
|
||||
IfName = "ygg";
|
||||
Peers = lib.lists.unique (exportedPeers ++ settings.extraPeers);
|
||||
Peers = settings.peers;
|
||||
MulticastInterfaces = [
|
||||
# Ethernet is preferred over WIFI
|
||||
{
|
||||
|
||||
@@ -17,20 +17,6 @@
|
||||
roles.default.machines.peer1 = { };
|
||||
roles.default.machines.peer2 = { };
|
||||
};
|
||||
|
||||
# Peers are set form exports of the internet service
|
||||
instances."internet" = {
|
||||
module.name = "internet";
|
||||
roles.default.machines.peer1.settings.host = "peer1-internet";
|
||||
roles.default.machines.peer2.settings.host = "peer2-internet";
|
||||
};
|
||||
|
||||
instances."zerotier" = {
|
||||
module.name = "zerotier";
|
||||
roles.controller.machines.peer1 = { };
|
||||
roles.peer.machines.peer2 = { };
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../../../../sops/machines/peer1
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
fd06:8020:2351:b57:2899:9306:8020:2351
|
||||
@@ -1 +0,0 @@
|
||||
06802023510b5728
|
||||
@@ -1 +0,0 @@
|
||||
../../../../../../sops/machines/peer2
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -1 +0,0 @@
|
||||
fd06:8020:2351:b57:2899:9340:7f3b:e1b3
|
||||
@@ -1,8 +1,4 @@
|
||||
{
|
||||
clanLib,
|
||||
directory,
|
||||
...
|
||||
}:
|
||||
{ ... }:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/zerotier";
|
||||
@@ -17,23 +13,21 @@
|
||||
instanceName,
|
||||
roles,
|
||||
lib,
|
||||
machine,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
exports."internet/${instanceName}/peer/${machine.name}".networking = {
|
||||
hosts = lib.flatten [
|
||||
(clanLib.vars.getPublicValue {
|
||||
flake = directory;
|
||||
machine = machine.name;
|
||||
exports.networking = {
|
||||
priority = lib.mkDefault 900;
|
||||
# TODO add user space network support to clan-cli
|
||||
module = "clan_lib.network.zerotier";
|
||||
peers = lib.mapAttrs (name: _machine: {
|
||||
host.var = {
|
||||
machine = name;
|
||||
generator = "zerotier";
|
||||
file = "zerotier-ip";
|
||||
# default = throw "kaputt";
|
||||
})
|
||||
];
|
||||
};
|
||||
}) roles.peer.machines;
|
||||
};
|
||||
|
||||
nixosModule =
|
||||
{
|
||||
config,
|
||||
@@ -45,7 +39,6 @@
|
||||
imports = [
|
||||
(import ./shared.nix {
|
||||
inherit
|
||||
clanLib
|
||||
instanceName
|
||||
roles
|
||||
config
|
||||
@@ -97,7 +90,6 @@
|
||||
imports = [
|
||||
(import ./shared.nix {
|
||||
inherit
|
||||
clanLib
|
||||
instanceName
|
||||
roles
|
||||
config
|
||||
@@ -150,7 +142,6 @@
|
||||
imports = [
|
||||
(import ./shared.nix {
|
||||
inherit
|
||||
clanLib
|
||||
instanceName
|
||||
roles
|
||||
config
|
||||
@@ -169,16 +160,15 @@
|
||||
);
|
||||
networkIps = builtins.foldl' (
|
||||
ips: name:
|
||||
let
|
||||
ztIp = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "zerotier";
|
||||
file = "zerotier-ip";
|
||||
default = null;
|
||||
};
|
||||
in
|
||||
if ztIp != null then ips ++ [ ztIp ] else ips
|
||||
if
|
||||
builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value"
|
||||
then
|
||||
ips
|
||||
++ [
|
||||
(builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value")
|
||||
]
|
||||
else
|
||||
ips
|
||||
) [ ] machines;
|
||||
allHostIPs = settings.allowedIps ++ networkIps;
|
||||
in
|
||||
|
||||
@@ -21,7 +21,6 @@ in
|
||||
../../clanServices/zerotier
|
||||
# Required modules
|
||||
../../nixosModules/clanCore
|
||||
../../nixosModules/machineModules
|
||||
# Dependencies like clan-cli
|
||||
../../pkgs/clan-cli
|
||||
];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
clanLib,
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
@@ -9,26 +8,20 @@
|
||||
}:
|
||||
let
|
||||
controllerMachine = builtins.head (lib.attrNames roles.controller.machines or { });
|
||||
networkId = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = controllerMachine;
|
||||
generator = "zerotier";
|
||||
file = "zerotier-network-id";
|
||||
default = null;
|
||||
};
|
||||
networkIdPath = "${config.clan.core.settings.directory}/vars/per-machine/${controllerMachine}/zerotier/zerotier-network-id/value";
|
||||
networkId = if builtins.pathExists networkIdPath then builtins.readFile networkIdPath else null;
|
||||
moons = lib.attrNames (roles.moon.machines or { });
|
||||
moonIps = builtins.foldl' (
|
||||
ips: name:
|
||||
let
|
||||
moonIp = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "zerotier";
|
||||
file = "zerotier-ip";
|
||||
default = null;
|
||||
};
|
||||
in
|
||||
if moonIp != null then ips ++ [ moonIp ] else ips
|
||||
if
|
||||
builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value"
|
||||
then
|
||||
ips
|
||||
++ [
|
||||
(builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value")
|
||||
]
|
||||
else
|
||||
ips
|
||||
) [ ] moons;
|
||||
in
|
||||
{
|
||||
|
||||
24
devFlake/flake.lock
generated
24
devFlake/flake.lock
generated
@@ -3,10 +3,10 @@
|
||||
"clan-core-for-checks": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1761204206,
|
||||
"narHash": "sha256-A4KDudGblln1yh8c95OVow2NRlHtbGZXr/pgNenyrNc=",
|
||||
"lastModified": 1760361585,
|
||||
"narHash": "sha256-v4PnSmt1hXW4dSgVWxcd1ZeEBlhO7NksNRC5cX7L5iw=",
|
||||
"ref": "main",
|
||||
"rev": "aabbe0dfac47b7cfbe2210bcb27fb7ecce93350f",
|
||||
"rev": "7e7e58eb64ef61beb0a938a6622ec0122382131b",
|
||||
"shallow": true,
|
||||
"type": "git",
|
||||
"url": "https://git.clan.lol/clan/clan-core"
|
||||
@@ -105,11 +105,11 @@
|
||||
},
|
||||
"nixpkgs-dev": {
|
||||
"locked": {
|
||||
"lastModified": 1761748483,
|
||||
"narHash": "sha256-v7fttCB5lJ22Ok7+N7ZbLhDeM89QIz9YWtQP4XN7xgA=",
|
||||
"lastModified": 1760965023,
|
||||
"narHash": "sha256-cpcgkeLApMGFCdp4jFqeIxTwlcGaSI+Zwmv8z2E85pY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "061c55856b29b8b9360e14231a0986c7f85f1130",
|
||||
"rev": "40ef6b9aa73f70b265c29df083fafae66b9df351",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -128,11 +128,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1761730856,
|
||||
"narHash": "sha256-t1i5p/vSWwueZSC0Z2BImxx3BjoUDNKyC2mk24krcMY=",
|
||||
"lastModified": 1760652422,
|
||||
"narHash": "sha256-C88Pgz38QIl9JxQceexqL2G7sw9vodHWx1Uaq+NRJrw=",
|
||||
"owner": "NuschtOS",
|
||||
"repo": "search",
|
||||
"rev": "e29de6db0cb3182e9aee75a3b1fd1919d995d85b",
|
||||
"rev": "3ebeebe8b6a49dfb11f771f761e0310f7c48d726",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -208,11 +208,11 @@
|
||||
"nixpkgs": []
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1761311587,
|
||||
"narHash": "sha256-Msq86cR5SjozQGCnC6H8C+0cD4rnx91BPltZ9KK613Y=",
|
||||
"lastModified": 1760945191,
|
||||
"narHash": "sha256-ZRVs8UqikBa4Ki3X4KCnMBtBW0ux1DaT35tgsnB1jM4=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "2eddae033e4e74bf581c2d1dfa101f9033dbd2dc",
|
||||
"rev": "f56b1934f5f8fcab8deb5d38d42fd692632b47c2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -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
|
||||
specialArgs = {};
|
||||
meta.name = throw "Change me to something unique";
|
||||
meta.tld = throw "Change me to something unique";
|
||||
|
||||
machines = {
|
||||
berlin = {
|
||||
|
||||
@@ -137,13 +137,12 @@ Description: None
|
||||
|
||||
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.
|
||||
meta.name = "__CHANGE_ME__";
|
||||
meta.tld = "changeme";
|
||||
|
||||
# ...
|
||||
# elided
|
||||
|
||||
@@ -10,11 +10,10 @@ and how to define a remote builder for your machine closures.
|
||||
Set the machine’s `targetHost` to the reachable IP address of the new machine.
|
||||
This eliminates the need to specify `--target-host` in CLI commands.
|
||||
|
||||
```{.nix title="clan.nix" hl_lines="10"}
|
||||
```{.nix title="clan.nix" hl_lines="9"}
|
||||
{
|
||||
# Ensure this is unique among all clans you want to use.
|
||||
meta.name = "my-clan";
|
||||
meta.tld = "ccc";
|
||||
|
||||
inventory.machines = {
|
||||
# Define machines here.
|
||||
|
||||
@@ -60,7 +60,6 @@ Configure Clan-wide settings and define machines. Here's an example `flake.nix`:
|
||||
# Define your Clan
|
||||
clan = {
|
||||
meta.name = ""; # Required and must be unique
|
||||
meta.tld = ""; # Required and must be unique
|
||||
|
||||
machines = {
|
||||
jon = {
|
||||
|
||||
@@ -43,7 +43,6 @@ For the purpose of this guide we have two machines:
|
||||
inherit self;
|
||||
|
||||
meta.name = "myclan";
|
||||
meta.tld = "ccc";
|
||||
|
||||
inventory.machines = {
|
||||
controller = {};
|
||||
|
||||
@@ -63,7 +63,6 @@ To use `age` plugins with Clan, you need to configure them in your `flake.nix` f
|
||||
inherit self;
|
||||
|
||||
meta.name = "myclan";
|
||||
meta.tld = "ccc";
|
||||
|
||||
# Add YubiKey and FIDO2 HMAC plugins
|
||||
# Note: Plugins must be available in nixpkgs.
|
||||
|
||||
24
flake.lock
generated
24
flake.lock
generated
@@ -71,11 +71,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1761339987,
|
||||
"narHash": "sha256-IUaawVwItZKi64IA6kF6wQCLCzpXbk2R46dHn8sHkig=",
|
||||
"lastModified": 1760721282,
|
||||
"narHash": "sha256-aAHphQbU9t/b2RRy2Eb8oMv+I08isXv2KUGFAFn7nCo=",
|
||||
"owner": "nix-darwin",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "7cd9aac79ee2924a85c211d21fafd394b06a38de",
|
||||
"rev": "c3211fcd0c56c11ff110d346d4487b18f7365168",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -99,11 +99,11 @@
|
||||
},
|
||||
"nixos-facter-modules": {
|
||||
"locked": {
|
||||
"lastModified": 1761137276,
|
||||
"narHash": "sha256-4lDjGnWRBLwqKQ4UWSUq6Mvxu9r8DSqCCydodW/Jsi8=",
|
||||
"lastModified": 1756491981,
|
||||
"narHash": "sha256-lXyDAWPw/UngVtQfgQ8/nrubs2r+waGEYIba5UX62+k=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixos-facter-modules",
|
||||
"rev": "70bcd64225d167c7af9b475c4df7b5abba5c7de8",
|
||||
"rev": "c1b29520945d3e148cd96618c8a0d1f850965d8c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -146,11 +146,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1760998189,
|
||||
"narHash": "sha256-ee2e1/AeGL5X8oy/HXsZQvZnae6XfEVdstGopKucYLY=",
|
||||
"lastModified": 1760845571,
|
||||
"narHash": "sha256-PwGzU3EOU65Ef1VvuNnVLie+l+P0g/fzf/PGUG82KbM=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "5a7d18b5c55642df5c432aadb757140edfeb70b3",
|
||||
"rev": "9c9a9798be331ed3f4b2902933d7677d0659ee61",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -181,11 +181,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1761311587,
|
||||
"narHash": "sha256-Msq86cR5SjozQGCnC6H8C+0cD4rnx91BPltZ9KK613Y=",
|
||||
"lastModified": 1760945191,
|
||||
"narHash": "sha256-ZRVs8UqikBa4Ki3X4KCnMBtBW0ux1DaT35tgsnB1jM4=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "2eddae033e4e74bf581c2d1dfa101f9033dbd2dc",
|
||||
"rev": "f56b1934f5f8fcab8deb5d38d42fd692632b47c2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
(
|
||||
{ ... }:
|
||||
{
|
||||
debug = true;
|
||||
clan = {
|
||||
meta.name = "clan-core";
|
||||
inventory = {
|
||||
@@ -99,7 +98,6 @@
|
||||
./lib/filter-clan-core/flake-module.nix
|
||||
./lib/flake-module.nix
|
||||
./lib/flake-parts/clan-nixos-test.nix
|
||||
./modules/flake-module.nix
|
||||
./nixosModules/clanCore/vars/flake-module.nix
|
||||
./nixosModules/flake-module.nix
|
||||
./pkgs/clan-cli/clan_cli/tests/flake-module.nix
|
||||
|
||||
@@ -39,10 +39,32 @@ in
|
||||
};
|
||||
modules = [
|
||||
clan-core.modules.clan.default
|
||||
{
|
||||
checks.minNixpkgsVersion = {
|
||||
assertion = lib.versionAtLeast nixpkgs.lib.version "25.11";
|
||||
message = ''
|
||||
Nixpkgs version: ${nixpkgs.lib.version} is incompatible with clan-core. (>= 25.11 is recommended)
|
||||
---
|
||||
Your version of 'nixpkgs' seems too old for clan-core.
|
||||
Please read: https://docs.clan.lol/guides/nixpkgs-flake-input
|
||||
|
||||
You can ignore this check by setting:
|
||||
clan.checks.minNixpkgsVersion.ignore = true;
|
||||
---
|
||||
'';
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
# Important: !This logic needs to be kept in sync with lib.clan function!
|
||||
apply = config: clan-core.lib.checkConfig config.checks config;
|
||||
apply =
|
||||
config:
|
||||
lib.deepSeq (lib.mapAttrs (
|
||||
id: check:
|
||||
if check.ignore || check.assertion then
|
||||
null
|
||||
else
|
||||
throw "clan.checks.${id} failed with message\n${check.message}"
|
||||
) config.checks) config;
|
||||
};
|
||||
|
||||
# Mapped flake toplevel outputs
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -33,23 +33,20 @@
|
||||
let
|
||||
nixpkgs = self.inputs.nixpkgs or clan-core.inputs.nixpkgs;
|
||||
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
|
||||
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
|
||||
];
|
||||
}
|
||||
|
||||
@@ -137,12 +137,6 @@ in
|
||||
default = { };
|
||||
type = types.submoduleWith {
|
||||
specialArgs = {
|
||||
self = throw ''
|
||||
'self' is banned in the use of clan.services
|
||||
Use 'exports' instead: https://docs.clan.lol/reference/options/clan_service/#exports
|
||||
---
|
||||
If you really need to used 'self' here, that makes the module less portable
|
||||
'';
|
||||
inherit (config.clanSettings)
|
||||
clan-core
|
||||
nixpkgs
|
||||
|
||||
@@ -16,12 +16,10 @@ lib.fix (
|
||||
*/
|
||||
callLib = file: args: import file ({ inherit lib clanLib; } // args);
|
||||
|
||||
checkConfig = clanLib.callLib ./clan/checkConfig.nix { };
|
||||
|
||||
evalService = clanLib.callLib ./evalService.nix { };
|
||||
evalService = clanLib.callLib ./modules/inventory/distributed-service/evalService.nix { };
|
||||
# ------------------------------------
|
||||
# ClanLib functions
|
||||
inventory = clanLib.callLib ./inventory { };
|
||||
inventory = clanLib.callLib ./modules/inventory { };
|
||||
test = clanLib.callLib ./test { };
|
||||
flake-inputs = clanLib.callLib ./flake-inputs.nix { };
|
||||
# Custom types
|
||||
@@ -32,17 +30,12 @@ lib.fix (
|
||||
jsonschema = import ./jsonschema { inherit lib; };
|
||||
docs = import ./docs.nix { inherit lib; };
|
||||
|
||||
vars = import ./vars.nix { inherit lib; };
|
||||
|
||||
# flakes
|
||||
flakes = clanLib.callLib ./flakes.nix { };
|
||||
|
||||
# TODO: Flatten our lib functions like this:
|
||||
resolveModule = clanLib.callLib ./resolve-module { };
|
||||
|
||||
# Functions to help define exports
|
||||
exports = clanLib.callLib ./exports.nix { };
|
||||
|
||||
fs = {
|
||||
inherit (builtins) pathExists readDir;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -10,11 +10,12 @@ in
|
||||
rec {
|
||||
# TODO: automatically generate this from the directory conventions
|
||||
imports = [
|
||||
./modules/flake-module.nix
|
||||
./clanTest/flake-module.nix
|
||||
./introspection/flake-module.nix
|
||||
./modules/inventory/flake-module.nix
|
||||
./jsonschema/flake-module.nix
|
||||
./types/flake-module.nix
|
||||
./inventory/flake-module.nix
|
||||
];
|
||||
flake.clanLib =
|
||||
let
|
||||
@@ -77,6 +78,9 @@ rec {
|
||||
../lib
|
||||
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../.)
|
||||
../flakeModules
|
||||
# ../../nixosModules/clanCore
|
||||
# ../../machines
|
||||
# ../../inventory.json
|
||||
];
|
||||
};
|
||||
in
|
||||
@@ -97,41 +101,6 @@ rec {
|
||||
touch $out
|
||||
'';
|
||||
};
|
||||
|
||||
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests-build-clan
|
||||
legacyPackages.evalTests-build-clan = import ./tests.nix {
|
||||
inherit lib;
|
||||
clan-core = self;
|
||||
};
|
||||
# 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
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
12
lib/modules/clan/default.nix
Normal file
12
lib/modules/clan/default.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
{ clan-core }:
|
||||
{
|
||||
_class = "clan";
|
||||
_module.args = {
|
||||
inherit clan-core;
|
||||
inherit (clan-core) clanLib;
|
||||
};
|
||||
imports = [
|
||||
./module.nix
|
||||
./interface.nix
|
||||
];
|
||||
}
|
||||
4
lib/modules/clan/flake-module.nix
Normal file
4
lib/modules/clan/flake-module.nix
Normal file
@@ -0,0 +1,4 @@
|
||||
{ self, lib, ... }:
|
||||
{
|
||||
flake.modules.clan.default = lib.modules.importApply ./default.nix { clan-core = self; };
|
||||
}
|
||||
@@ -110,7 +110,9 @@ in
|
||||
|
||||
# TODO: make this writable by moving the options from inventoryClass into clan.
|
||||
exports = lib.mkOption {
|
||||
type = types.lazyAttrsOf (types.submoduleWith { modules = [ config.exportsModule ]; });
|
||||
readOnly = true;
|
||||
visible = false;
|
||||
internal = true;
|
||||
};
|
||||
|
||||
exportsModule = lib.mkOption {
|
||||
@@ -118,86 +120,84 @@ in
|
||||
visible = false;
|
||||
type = types.deferredModule;
|
||||
default = {
|
||||
options.networking = {
|
||||
|
||||
priority = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 1000;
|
||||
description = ''
|
||||
priority with which this network should be tried.
|
||||
higher priority means it gets used earlier in the chain
|
||||
'';
|
||||
};
|
||||
module = lib.mkOption {
|
||||
# type = lib.types.enum [
|
||||
# "clan_lib.network.direct"
|
||||
# "clan_lib.network.tor"
|
||||
# ];
|
||||
type = lib.types.str;
|
||||
default = "clan_lib.network.direct";
|
||||
description = ''
|
||||
the technology this network uses to connect to the target
|
||||
This is used for userspace networking with socks proxies.
|
||||
'';
|
||||
};
|
||||
# should we call this machines? hosts?
|
||||
|
||||
hosts = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
# peers = lib.mkOption {
|
||||
#
|
||||
# # <name>
|
||||
# type = lib.types.attrsOf (
|
||||
# lib.types.submodule (
|
||||
# { name, ... }:
|
||||
# {
|
||||
# options = {
|
||||
# name = lib.mkOption {
|
||||
# type = lib.types.str;
|
||||
# default = name;
|
||||
# };
|
||||
# SSHOptions = lib.mkOption {
|
||||
# type = lib.types.listOf lib.types.str;
|
||||
# default = [ ];
|
||||
# };
|
||||
#
|
||||
# host = lib.mkOption {
|
||||
# description = '''';
|
||||
# type = lib.types.attrTag {
|
||||
# plain = lib.mkOption {
|
||||
# type = lib.types.str;
|
||||
# description = ''
|
||||
# a plain value, which can be read directly from the config
|
||||
# '';
|
||||
# };
|
||||
# var = lib.mkOption {
|
||||
# type = lib.types.submodule {
|
||||
# options = {
|
||||
# machine = lib.mkOption {
|
||||
# type = lib.types.str;
|
||||
# example = "jon";
|
||||
# };
|
||||
# generator = lib.mkOption {
|
||||
# type = lib.types.str;
|
||||
# example = "tor-ssh";
|
||||
# };
|
||||
# file = lib.mkOption {
|
||||
# type = lib.types.str;
|
||||
# example = "hostname";
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
# }
|
||||
# )
|
||||
# );
|
||||
# };
|
||||
options.networking = lib.mkOption {
|
||||
default = null;
|
||||
type = lib.types.nullOr (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
priority = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 1000;
|
||||
description = ''
|
||||
priority with which this network should be tried.
|
||||
higher priority means it gets used earlier in the chain
|
||||
'';
|
||||
};
|
||||
module = lib.mkOption {
|
||||
# type = lib.types.enum [
|
||||
# "clan_lib.network.direct"
|
||||
# "clan_lib.network.tor"
|
||||
# ];
|
||||
type = lib.types.str;
|
||||
default = "clan_lib.network.direct";
|
||||
description = ''
|
||||
the technology this network uses to connect to the target
|
||||
This is used for userspace networking with socks proxies.
|
||||
'';
|
||||
};
|
||||
# should we call this machines? hosts?
|
||||
peers = lib.mkOption {
|
||||
# <name>
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
};
|
||||
SSHOptions = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
};
|
||||
host = lib.mkOption {
|
||||
description = '''';
|
||||
type = lib.types.attrTag {
|
||||
plain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
a plain value, which can be read directly from the config
|
||||
'';
|
||||
};
|
||||
var = lib.mkOption {
|
||||
type = lib.types.submodule {
|
||||
options = {
|
||||
machine = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "jon";
|
||||
};
|
||||
generator = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "tor-ssh";
|
||||
};
|
||||
file = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "hostname";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
@@ -288,7 +288,7 @@ in
|
||||
Global information about the clan.
|
||||
'';
|
||||
type = types.deferredModuleWith {
|
||||
staticModules = [ ../inventoryClass/meta.nix ];
|
||||
staticModules = [ ../inventoryClass/meta-interface.nix ];
|
||||
};
|
||||
default = { };
|
||||
};
|
||||
@@ -100,7 +100,7 @@ let
|
||||
_: machine:
|
||||
machine.extendModules {
|
||||
modules = [
|
||||
(lib.modules.importApply ../../nixosModules/machineModules/overridePkgs.nix {
|
||||
(lib.modules.importApply ../machineModules/overridePkgs.nix {
|
||||
pkgs = pkgsFor.${system};
|
||||
})
|
||||
];
|
||||
@@ -167,9 +167,6 @@ in
|
||||
{ ... }@args:
|
||||
let
|
||||
_class =
|
||||
# _class was added in https://github.com/NixOS/nixpkgs/pull/395141
|
||||
# Clan relies on it to determine which modules to load
|
||||
# people need to use at least that version of nixpkgs
|
||||
args._class or (throw ''
|
||||
Your version of nixpkgs is incompatible with the latest clan.
|
||||
Please update nixpkgs input to the latest nixos-unstable or nixpkgs-unstable.
|
||||
@@ -179,7 +176,7 @@ in
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
(lib.modules.importApply ../../nixosModules/machineModules/forName.nix {
|
||||
(lib.modules.importApply ../machineModules/forName.nix {
|
||||
inherit (config.inventory) meta;
|
||||
inherit
|
||||
name
|
||||
@@ -219,44 +216,43 @@ in
|
||||
inherit nixosConfigurations;
|
||||
inherit darwinConfigurations;
|
||||
|
||||
exports = config.clanInternals.inventoryClass.distributedServices.servicesEval.config.exports;
|
||||
|
||||
clanInternals = {
|
||||
inventoryClass =
|
||||
let
|
||||
flakeInputs = config.self.inputs;
|
||||
# Compute the relative directory path
|
||||
selfStr = toString config.self;
|
||||
dirStr = toString directory;
|
||||
relativeDirectory =
|
||||
if selfStr == dirStr then
|
||||
""
|
||||
else if lib.hasPrefix selfStr dirStr then
|
||||
lib.removePrefix (selfStr + "/") dirStr
|
||||
else
|
||||
# This shouldn't happen in normal usage, but can occur when
|
||||
# the flake is copied (e.g., in tests). Fall back to empty string.
|
||||
"";
|
||||
in
|
||||
{
|
||||
_module.args = {
|
||||
inherit clanLib;
|
||||
};
|
||||
imports = [
|
||||
../inventoryClass/default.nix
|
||||
../inventoryClass/builder/default.nix
|
||||
(lib.modules.importApply ../inventoryClass/service-list-from-inputs.nix {
|
||||
inherit flakeInputs clanLib;
|
||||
})
|
||||
{
|
||||
inherit
|
||||
inventory
|
||||
directory
|
||||
flakeInputs
|
||||
relativeDirectory
|
||||
;
|
||||
exportsModule = config.exportsModule;
|
||||
inherit inventory directory;
|
||||
}
|
||||
(
|
||||
{ ... }:
|
||||
let
|
||||
clanConfig = config;
|
||||
in
|
||||
{ config, ... }:
|
||||
{
|
||||
staticModules = clan-core.clan.modules;
|
||||
|
||||
distributedServices = clanLib.inventory.mapInstances {
|
||||
inherit (clanConfig) inventory exportsModule;
|
||||
inherit flakeInputs directory;
|
||||
clanCoreModules = clan-core.clan.modules;
|
||||
prefix = [ "distributedServices" ];
|
||||
};
|
||||
machines = config.distributedServices.allMachines;
|
||||
}
|
||||
)
|
||||
../inventoryClass/inventory-introspection.nix
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,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,
|
||||
...
|
||||
@@ -2,7 +2,7 @@
|
||||
lib ? import <nixpkgs/lib>,
|
||||
}:
|
||||
let
|
||||
clanLibOrig = (import ./. { inherit lib; }).__unfix__;
|
||||
clanLibOrig = (import ./.. { inherit lib; }).__unfix__;
|
||||
clanLibWithFs =
|
||||
{ virtual_fs }:
|
||||
lib.fix (
|
||||
@@ -11,19 +11,19 @@ let
|
||||
let
|
||||
clan-core = {
|
||||
clanLib = final;
|
||||
modules.clan.default = lib.modules.importApply ../modules/clan { inherit clan-core; };
|
||||
modules.clan.default = lib.modules.importApply ./clan { inherit clan-core; };
|
||||
|
||||
# Note: Can add other things to "clan-core"
|
||||
# ... Not needed for this test
|
||||
};
|
||||
in
|
||||
{
|
||||
clan = import ./clan {
|
||||
clan = import ../clan {
|
||||
inherit lib clan-core;
|
||||
};
|
||||
|
||||
# Override clanLib.fs for unit-testing against a virtual filesystem
|
||||
fs = import ./clanTest/virtual-fs.nix { inherit lib; } {
|
||||
fs = import ../clanTest/virtual-fs.nix { inherit lib; } {
|
||||
inherit rootPath virtual_fs;
|
||||
# Example of a passthru
|
||||
# passthru = [
|
||||
@@ -53,12 +53,7 @@ in
|
||||
};
|
||||
};
|
||||
}).clan
|
||||
{
|
||||
directory = rootPath;
|
||||
self = {
|
||||
inputs.nixpkgs.lib.version = "25.11";
|
||||
};
|
||||
};
|
||||
{ config.directory = rootPath; };
|
||||
in
|
||||
{
|
||||
inherit vclan;
|
||||
@@ -99,12 +94,7 @@ in
|
||||
};
|
||||
};
|
||||
}).clan
|
||||
{
|
||||
directory = rootPath;
|
||||
self = {
|
||||
inputs.nixpkgs.lib.version = "25.11";
|
||||
};
|
||||
};
|
||||
{ config.directory = rootPath; };
|
||||
in
|
||||
{
|
||||
inherit vclan;
|
||||
@@ -1,24 +1,19 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
clanModule,
|
||||
clanLib,
|
||||
clan-core,
|
||||
}:
|
||||
let
|
||||
eval = lib.evalModules {
|
||||
modules = [
|
||||
clanModule
|
||||
clan-core.modules.clan.default
|
||||
];
|
||||
specialArgs = {
|
||||
self = clan-core;
|
||||
};
|
||||
};
|
||||
|
||||
evalDocs = pkgs.nixosOptionsDoc {
|
||||
options = eval.options;
|
||||
warningsAreErrors = false;
|
||||
transformOptions = clanLib.docs.stripStorePathsFromDeclarations;
|
||||
transformOptions = clan-core.clanLib.docs.stripStorePathsFromDeclarations;
|
||||
};
|
||||
in
|
||||
{
|
||||
58
lib/modules/flake-module.nix
Normal file
58
lib/modules/flake-module.nix
Normal 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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
lib,
|
||||
clanLib,
|
||||
}:
|
||||
let
|
||||
services = clanLib.callLib ./distributed-service/inventory-adapter.nix { };
|
||||
in
|
||||
{
|
||||
inherit (services) mapInstances;
|
||||
inventoryModule = {
|
||||
_file = "clanLib.inventory.module";
|
||||
imports = [
|
||||
../../modules/inventoryClass/inventory.nix
|
||||
../inventoryClass/inventory.nix
|
||||
];
|
||||
_module.args = { inherit clanLib; };
|
||||
};
|
||||
@@ -2,7 +2,6 @@
|
||||
{
|
||||
# TODO: consume directly from clan.config
|
||||
directory,
|
||||
exports,
|
||||
}:
|
||||
{
|
||||
lib,
|
||||
@@ -18,10 +17,10 @@ in
|
||||
{
|
||||
# TODO: merge these options into clan options
|
||||
options = {
|
||||
# exportsModule = mkOption {
|
||||
# type = types.deferredModule;
|
||||
# readOnly = true;
|
||||
# };
|
||||
exportsModule = mkOption {
|
||||
type = types.deferredModule;
|
||||
readOnly = true;
|
||||
};
|
||||
mappedServices = mkOption {
|
||||
visible = false;
|
||||
type = attrsWith {
|
||||
@@ -29,11 +28,9 @@ in
|
||||
elemType = submoduleWith {
|
||||
class = "clan.service";
|
||||
specialArgs = {
|
||||
exports = config.exports;
|
||||
directory = directory;
|
||||
clanLib = specialArgs.clanLib;
|
||||
inherit
|
||||
exports
|
||||
directory
|
||||
;
|
||||
};
|
||||
modules = [
|
||||
(
|
||||
@@ -54,13 +51,34 @@ in
|
||||
default = { };
|
||||
};
|
||||
exports = mkOption {
|
||||
type = types.lazyAttrsOf types.deferredModule;
|
||||
|
||||
# collect exports from all services
|
||||
# zipAttrs is needed until we use the record type.
|
||||
default = lib.zipAttrsWith (_name: values: { imports = values; }) (
|
||||
lib.mapAttrsToList (_name: service: service.exports) config.mappedServices
|
||||
);
|
||||
type = submoduleWith {
|
||||
modules = [
|
||||
{
|
||||
options = {
|
||||
instances = lib.mkOption {
|
||||
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 = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -17,9 +17,9 @@ lib.evalModules {
|
||||
specialArgs._ctx = prefix;
|
||||
modules = [
|
||||
# Base module
|
||||
./inventory/distributed-service/service-module.nix
|
||||
./service-module.nix
|
||||
# Feature modules
|
||||
(lib.modules.importApply ./inventory/distributed-service/api-feature.nix {
|
||||
(lib.modules.importApply ./api-feature.nix {
|
||||
inherit clanLib prefix;
|
||||
})
|
||||
]
|
||||
@@ -13,18 +13,16 @@ in
|
||||
let
|
||||
# Common filtered source for inventory tests
|
||||
inventoryTestsSrc = lib.fileset.toSource {
|
||||
root = ../../..;
|
||||
root = ../../../..;
|
||||
fileset = lib.fileset.unions [
|
||||
../../../flake.nix
|
||||
../../../flake.lock
|
||||
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../..)
|
||||
../../../flakeModules
|
||||
../../../lib
|
||||
../../../nixosModules/clanCore
|
||||
../../../nixosModules/machineModules
|
||||
../../../machines
|
||||
../../../inventory.json
|
||||
../../../modules
|
||||
../../../../flake.nix
|
||||
../../../../flake.lock
|
||||
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../../..)
|
||||
../../../../flakeModules
|
||||
../../../../lib
|
||||
../../../../nixosModules/clanCore
|
||||
../../../../machines
|
||||
../../../../inventory.json
|
||||
];
|
||||
};
|
||||
in
|
||||
171
lib/modules/inventory/distributed-service/inventory-adapter.nix
Normal file
171
lib/modules/inventory/distributed-service/inventory-adapter.nix
Normal 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
|
||||
;
|
||||
};
|
||||
}
|
||||
@@ -81,7 +81,6 @@ let
|
||||
applySettings =
|
||||
instanceName: instance:
|
||||
lib.mapAttrs (roleName: role: {
|
||||
settings = config.instances.${instanceName}.roles.${roleName}.finalSettings.config;
|
||||
machines = lib.mapAttrs (machineName: _v: {
|
||||
settings =
|
||||
config.instances.${instanceName}.roles.${roleName}.machines.${machineName}.finalSettings.config;
|
||||
@@ -159,29 +158,6 @@ in
|
||||
(
|
||||
{ name, ... }@role:
|
||||
{
|
||||
options.finalSettings = mkOption {
|
||||
default = evalMachineSettings instance.name role.name null role.config.settings { };
|
||||
type = types.raw;
|
||||
description = ''
|
||||
Final evaluated settings of the curent-machine
|
||||
|
||||
This contains the merged and evaluated settings of the role interface,
|
||||
the role settings and the machine settings.
|
||||
|
||||
Type: 'configuration' as returned by 'lib.evalModules'
|
||||
'';
|
||||
apply = lib.warn ''
|
||||
=== WANRING ===
|
||||
'roles.<roleName>.settings' do not contain machine specific settings.
|
||||
|
||||
Prefer `machines.<machineName>.settings` instead. (i.e `perInstance: roles.<roleName>.machines.<machineName>.settings`)
|
||||
|
||||
If you have a use-case that requires access to the original role settings without machine overrides.
|
||||
Contact us via matrix (https://matrix.to/#/#clan:clan.lol) or file an issue: https://git.clan.lol
|
||||
|
||||
This feature will be removed in the next release
|
||||
'';
|
||||
};
|
||||
# instances.{instanceName}.roles.{roleName}.machines
|
||||
options.machines = mkOption {
|
||||
description = ''
|
||||
@@ -240,7 +216,7 @@ in
|
||||
|
||||
options.extraModules = lib.mkOption {
|
||||
default = [ ];
|
||||
type = types.listOf types.deferredModule;
|
||||
type = types.listOf (types.either types.deferredModule types.str);
|
||||
};
|
||||
}
|
||||
)
|
||||
@@ -504,12 +480,9 @@ in
|
||||
staticModules = [
|
||||
({
|
||||
options.exports = mkOption {
|
||||
type = types.lazyAttrsOf types.deferredModule;
|
||||
type = types.deferredModule;
|
||||
default = { };
|
||||
description = ''
|
||||
!!! Danger "Experimental Feature"
|
||||
This feature is experimental and will change in the future.
|
||||
|
||||
export modules defined in 'perInstance'
|
||||
mapped to their instance name
|
||||
|
||||
@@ -634,21 +607,10 @@ in
|
||||
type = types.deferredModuleWith {
|
||||
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 {
|
||||
type = types.lazyAttrsOf types.deferredModule;
|
||||
type = types.deferredModule;
|
||||
default = { };
|
||||
description = ''
|
||||
!!! Danger "Experimental Feature"
|
||||
This feature is experimental and will change in the future.
|
||||
|
||||
export modules defined in 'perMachine'
|
||||
mapped to their machine name
|
||||
|
||||
@@ -750,9 +712,6 @@ in
|
||||
|
||||
exports = mkOption {
|
||||
description = ''
|
||||
!!! Danger "Experimental Feature"
|
||||
This feature is experimental and will change in the future.
|
||||
|
||||
This services exports.
|
||||
Gets merged with all other services exports.
|
||||
|
||||
@@ -775,38 +734,79 @@ in
|
||||
```
|
||||
'';
|
||||
default = { };
|
||||
type = types.lazyAttrsOf (
|
||||
types.deferredModuleWith {
|
||||
# staticModules = [];
|
||||
# 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
|
||||
# )
|
||||
# )
|
||||
# ++
|
||||
}
|
||||
);
|
||||
# # 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;
|
||||
# }
|
||||
# ];
|
||||
type = types.submoduleWith {
|
||||
# Static modules
|
||||
modules = [
|
||||
{
|
||||
options.instances = mkOption {
|
||||
type = types.attrsOf types.deferredModule;
|
||||
description = ''
|
||||
export modules defined in 'perInstance'
|
||||
mapped to their instance name
|
||||
|
||||
Example
|
||||
|
||||
with instances:
|
||||
|
||||
```nix
|
||||
instances.A = { ... };
|
||||
instances.B= { ... };
|
||||
|
||||
roles.peer.perInstance = { instanceName, machine, ... }:
|
||||
{
|
||||
exports.foo = 1;
|
||||
}
|
||||
|
||||
This yields all other services can access these exports
|
||||
=>
|
||||
exports.instances.A.foo = 1;
|
||||
exports.instances.B.foo = 1;
|
||||
```
|
||||
'';
|
||||
};
|
||||
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
|
||||
@@ -850,11 +850,7 @@ in
|
||||
instanceRes.nixosModule
|
||||
]
|
||||
++ (map (
|
||||
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
|
||||
s: if builtins.typeOf s == "string" then "${directory}/${s}" else s
|
||||
) instanceCfg.roles.${roleName}.extraModules);
|
||||
};
|
||||
}
|
||||
@@ -991,39 +987,5 @@ in
|
||||
}
|
||||
) 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
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
];
|
||||
|
||||
}
|
||||
@@ -4,53 +4,63 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
evalModules
|
||||
;
|
||||
|
||||
flakeInputsFixture = {
|
||||
upstream.clan.modules = {
|
||||
uzzi = {
|
||||
_class = "clan.service";
|
||||
manifest = {
|
||||
name = "uzzi-from-upstream";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
evalInventory =
|
||||
m:
|
||||
(evalModules {
|
||||
# Static modules
|
||||
modules = [
|
||||
clanLib.inventory.inventoryModule
|
||||
{
|
||||
_file = "test file";
|
||||
tags.all = [ ];
|
||||
tags.nixos = [ ];
|
||||
tags.darwin = [ ];
|
||||
}
|
||||
{
|
||||
modules.test = { };
|
||||
}
|
||||
m
|
||||
];
|
||||
}).config;
|
||||
|
||||
createTestClan =
|
||||
testClan:
|
||||
callInventoryAdapter =
|
||||
inventoryModule:
|
||||
let
|
||||
res = clanLib.clan ({
|
||||
# Static / mocked
|
||||
specialArgs = {
|
||||
clan-core = {
|
||||
clan.modules = { };
|
||||
inventory = evalInventory inventoryModule;
|
||||
flakeInputsFixture = {
|
||||
self.clan.modules = inventoryModule.modules or { };
|
||||
# Example upstream module
|
||||
upstream.clan.modules = {
|
||||
uzzi = {
|
||||
_class = "clan.service";
|
||||
manifest = {
|
||||
name = "uzzi-from-upstream";
|
||||
};
|
||||
};
|
||||
};
|
||||
self.inputs = flakeInputsFixture // {
|
||||
self.clan = res.config;
|
||||
};
|
||||
directory = ./.;
|
||||
exportsModule = { };
|
||||
|
||||
imports = [
|
||||
testClan
|
||||
];
|
||||
});
|
||||
};
|
||||
in
|
||||
res;
|
||||
|
||||
clanLib.inventory.mapInstances {
|
||||
directory = ./.;
|
||||
clanCoreModules = { };
|
||||
flakeInputs = flakeInputsFixture;
|
||||
inherit inventory;
|
||||
exportsModule = { };
|
||||
};
|
||||
in
|
||||
{
|
||||
extraModules = import ./extraModules.nix { inherit clanLib; };
|
||||
exports = import ./exports.nix { inherit lib clanLib; };
|
||||
settings = import ./settings.nix { inherit lib createTestClan; };
|
||||
specialArgs = import ./specialArgs.nix { inherit lib createTestClan; };
|
||||
resolve_module_spec = import ./import_module_spec.nix {
|
||||
inherit lib createTestClan;
|
||||
};
|
||||
settings = import ./settings.nix { inherit lib callInventoryAdapter; };
|
||||
specialArgs = import ./specialArgs.nix { inherit lib callInventoryAdapter; };
|
||||
resolve_module_spec = import ./import_module_spec.nix { inherit lib callInventoryAdapter; };
|
||||
test_simple =
|
||||
let
|
||||
res = createTestClan {
|
||||
res = callInventoryAdapter {
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
# It isn't exactly doing anything but it's a valid module that produces an output
|
||||
@@ -61,7 +71,7 @@ in
|
||||
};
|
||||
};
|
||||
# User config
|
||||
inventory.instances."instance_foo" = {
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "simple-module";
|
||||
};
|
||||
@@ -71,7 +81,7 @@ in
|
||||
{
|
||||
# Test that the module is mapped into the output
|
||||
# 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;
|
||||
inherit res;
|
||||
};
|
||||
@@ -82,7 +92,7 @@ in
|
||||
# All instances should be included within one evaluation to make all of them available
|
||||
test_module_grouping =
|
||||
let
|
||||
res = createTestClan {
|
||||
res = callInventoryAdapter {
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
# It isn't exactly doing anything but it's a valid module that produces an output
|
||||
@@ -102,19 +112,18 @@ in
|
||||
|
||||
perMachine = { }: { };
|
||||
};
|
||||
|
||||
# User config
|
||||
inventory.instances."instance_foo" = {
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
};
|
||||
};
|
||||
inventory.instances."instance_bar" = {
|
||||
instances."instance_bar" = {
|
||||
module = {
|
||||
name = "B";
|
||||
};
|
||||
};
|
||||
inventory.instances."instance_baz" = {
|
||||
instances."instance_baz" = {
|
||||
module = {
|
||||
name = "A";
|
||||
};
|
||||
@@ -124,16 +133,16 @@ in
|
||||
{
|
||||
# Test that the module is mapped into the output
|
||||
# We might change the attribute name in the future
|
||||
expr = lib.attrNames res.config._services.mappedServices;
|
||||
expected = [
|
||||
"<clan-core>-A"
|
||||
"<clan-core>-B"
|
||||
];
|
||||
expr = lib.mapAttrs (_n: v: builtins.length v) res.grouped;
|
||||
expected = {
|
||||
"<clan-core>-A" = 2;
|
||||
"<clan-core>-B" = 1;
|
||||
};
|
||||
};
|
||||
|
||||
test_creates_all_instances =
|
||||
let
|
||||
res = createTestClan {
|
||||
res = callInventoryAdapter {
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
# It isn't exactly doing anything but it's a valid module that produces an output
|
||||
@@ -145,24 +154,22 @@ in
|
||||
|
||||
perMachine = { }: { };
|
||||
};
|
||||
inventory = {
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
instances."instance_bar" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
};
|
||||
instances."instance_bar" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
instances."instance_zaza" = {
|
||||
module = {
|
||||
name = "B";
|
||||
input = null;
|
||||
};
|
||||
};
|
||||
instances."instance_zaza" = {
|
||||
module = {
|
||||
name = "B";
|
||||
input = null;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -170,7 +177,7 @@ in
|
||||
{
|
||||
# Test that the module is mapped into the output
|
||||
# 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 = [
|
||||
"instance_bar"
|
||||
"instance_foo"
|
||||
@@ -180,7 +187,7 @@ in
|
||||
# Membership via roles
|
||||
test_add_machines_directly =
|
||||
let
|
||||
res = createTestClan {
|
||||
res = callInventoryAdapter {
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
# It isn't exactly doing anything but it's a valid module that produces an output
|
||||
@@ -195,40 +202,38 @@ in
|
||||
|
||||
# perMachine = {}: {};
|
||||
};
|
||||
inventory = {
|
||||
machines = {
|
||||
jon = { };
|
||||
sara = { };
|
||||
hxi = { };
|
||||
machines = {
|
||||
jon = { };
|
||||
sara = { };
|
||||
hxi = { };
|
||||
};
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
roles.peer.machines.jon = { };
|
||||
roles.peer.machines.jon = { };
|
||||
};
|
||||
instances."instance_bar" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
instances."instance_bar" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
roles.peer.machines.sara = { };
|
||||
};
|
||||
instances."instance_zaza" = {
|
||||
module = {
|
||||
name = "B";
|
||||
input = null;
|
||||
};
|
||||
roles.peer.tags.all = { };
|
||||
roles.peer.machines.sara = { };
|
||||
};
|
||||
instances."instance_zaza" = {
|
||||
module = {
|
||||
name = "B";
|
||||
input = null;
|
||||
};
|
||||
roles.peer.tags.all = { };
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
# Test that the module is mapped into the output
|
||||
# We might change the attribute name in the future
|
||||
expr = lib.attrNames res.config._services.mappedServices.self-A.result.allMachines;
|
||||
expr = lib.attrNames res.importedModulesEvaluated.self-A.result.allMachines;
|
||||
expected = [
|
||||
"jon"
|
||||
"sara"
|
||||
@@ -238,7 +243,7 @@ in
|
||||
# Membership via tags
|
||||
test_add_machines_via_tags =
|
||||
let
|
||||
res = createTestClan {
|
||||
res = callInventoryAdapter {
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
# It isn't exactly doing anything but it's a valid module that produces an output
|
||||
@@ -252,37 +257,35 @@ in
|
||||
|
||||
# perMachine = {}: {};
|
||||
};
|
||||
inventory = {
|
||||
machines = {
|
||||
jon = {
|
||||
tags = [ "foo" ];
|
||||
};
|
||||
sara = {
|
||||
tags = [ "foo" ];
|
||||
};
|
||||
hxi = { };
|
||||
machines = {
|
||||
jon = {
|
||||
tags = [ "foo" ];
|
||||
};
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
roles.peer.tags.foo = { };
|
||||
sara = {
|
||||
tags = [ "foo" ];
|
||||
};
|
||||
instances."instance_zaza" = {
|
||||
module = {
|
||||
name = "B";
|
||||
input = null;
|
||||
};
|
||||
roles.peer.tags.all = { };
|
||||
hxi = { };
|
||||
};
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
roles.peer.tags.foo = { };
|
||||
};
|
||||
instances."instance_zaza" = {
|
||||
module = {
|
||||
name = "B";
|
||||
input = null;
|
||||
};
|
||||
roles.peer.tags.all = { };
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
# Test that the module is mapped into the output
|
||||
# We might change the attribute name in the future
|
||||
expr = lib.attrNames res.config._services.mappedServices.self-A.result.allMachines;
|
||||
expr = lib.attrNames res.importedModulesEvaluated.self-A.result.allMachines;
|
||||
expected = [
|
||||
"jon"
|
||||
"sara"
|
||||
@@ -290,9 +293,6 @@ in
|
||||
};
|
||||
|
||||
machine_imports = import ./machine_imports.nix { inherit lib clanLib; };
|
||||
per_machine_args = import ./per_machine_args.nix { inherit lib createTestClan; };
|
||||
per_instance_args = import ./per_instance_args.nix {
|
||||
inherit lib;
|
||||
callInventoryAdapter = createTestClan;
|
||||
};
|
||||
per_machine_args = import ./per_machine_args.nix { inherit lib callInventoryAdapter; };
|
||||
per_instance_args = import ./per_instance_args.nix { inherit lib callInventoryAdapter; };
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{ createTestClan, ... }:
|
||||
{ callInventoryAdapter, ... }:
|
||||
let
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
@@ -23,13 +23,10 @@ let
|
||||
|
||||
resolve =
|
||||
spec:
|
||||
createTestClan {
|
||||
inherit modules;
|
||||
inventory = {
|
||||
inherit machines;
|
||||
instances."instance_foo" = {
|
||||
module = spec;
|
||||
};
|
||||
callInventoryAdapter {
|
||||
inherit modules machines;
|
||||
instances."instance_foo" = {
|
||||
module = spec;
|
||||
};
|
||||
};
|
||||
in
|
||||
@@ -39,16 +36,25 @@ in
|
||||
(resolve {
|
||||
name = "A";
|
||||
input = "self";
|
||||
}).config._services.mappedServices.self-A.manifest.name;
|
||||
expected = "network";
|
||||
}).importedModuleWithInstances.instance_foo.resolvedModule;
|
||||
expected = {
|
||||
_class = "clan.service";
|
||||
manifest = {
|
||||
name = "network";
|
||||
};
|
||||
};
|
||||
};
|
||||
test_import_remote_module_by_name = {
|
||||
expr =
|
||||
(resolve {
|
||||
name = "uzzi";
|
||||
input = "upstream";
|
||||
}).config._services.mappedServices.upstream-uzzi.manifest.name;
|
||||
expected = "uzzi-from-upstream";
|
||||
|
||||
}).importedModuleWithInstances.instance_foo.resolvedModule;
|
||||
expected = {
|
||||
_class = "clan.service";
|
||||
manifest = {
|
||||
name = "uzzi-from-upstream";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -58,43 +58,39 @@ let
|
||||
sara = { };
|
||||
};
|
||||
res = callInventoryAdapter {
|
||||
inherit modules;
|
||||
|
||||
inventory = {
|
||||
inherit machines;
|
||||
instances."instance_foo" = {
|
||||
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 = { };
|
||||
inherit modules machines;
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
instances."instance_bar" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
roles.peer.machines.jon = {
|
||||
settings.timeout = "bar-peer-jon";
|
||||
};
|
||||
roles.peer.machines.jon = {
|
||||
settings.timeout = lib.mkForce "foo-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 = { };
|
||||
roles.peer = {
|
||||
settings.timeout = "foo-peer";
|
||||
};
|
||||
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
|
||||
test_per_instance_arguments = {
|
||||
inherit res;
|
||||
expr = {
|
||||
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.
|
||||
# Below we access:
|
||||
@@ -120,11 +115,11 @@ in
|
||||
# roles = peer
|
||||
# machines = jon
|
||||
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 =
|
||||
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 =
|
||||
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 = {
|
||||
instanceName = "instance_foo";
|
||||
@@ -142,7 +137,6 @@ in
|
||||
settings = { };
|
||||
};
|
||||
};
|
||||
settings = { };
|
||||
};
|
||||
peer = {
|
||||
machines = {
|
||||
@@ -152,9 +146,6 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
settings = {
|
||||
timeout = "foo-peer";
|
||||
};
|
||||
};
|
||||
};
|
||||
settings = {
|
||||
@@ -165,9 +156,9 @@ in
|
||||
|
||||
# TODO: Cannot be tested like this anymore
|
||||
test_per_instance_settings_vendoring = {
|
||||
x = res.config._services.mappedServices.self-A;
|
||||
x = res.importedModulesEvaluated.self-A;
|
||||
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 = {
|
||||
timeout = "config.thing";
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
{ lib, createTestClan }:
|
||||
{ lib, callInventoryAdapter }:
|
||||
let
|
||||
# Authored module
|
||||
# A minimal module looks like this
|
||||
@@ -39,40 +39,36 @@ let
|
||||
jon = { };
|
||||
sara = { };
|
||||
};
|
||||
res = createTestClan {
|
||||
inherit modules;
|
||||
inventory = {
|
||||
|
||||
inherit machines;
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
roles.peer.machines.jon = {
|
||||
settings.timeout = lib.mkForce "foo-peer-jon";
|
||||
};
|
||||
roles.peer = {
|
||||
settings.timeout = "foo-peer";
|
||||
};
|
||||
res = callInventoryAdapter {
|
||||
inherit modules machines;
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
instances."instance_bar" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
roles.peer.machines.jon = {
|
||||
settings.timeout = "bar-peer-jon";
|
||||
};
|
||||
roles.peer.machines.jon = {
|
||||
settings.timeout = lib.mkForce "foo-peer-jon";
|
||||
};
|
||||
instances."instance_zaza" = {
|
||||
module = {
|
||||
name = "B";
|
||||
input = null;
|
||||
};
|
||||
roles.peer.tags.all = { };
|
||||
roles.peer = {
|
||||
settings.timeout = "foo-peer";
|
||||
};
|
||||
};
|
||||
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
|
||||
|
||||
@@ -83,7 +79,7 @@ in
|
||||
inherit res;
|
||||
expr = {
|
||||
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 are specific.
|
||||
@@ -92,10 +88,10 @@ in
|
||||
# roles = peer
|
||||
# machines = jon
|
||||
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 =
|
||||
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 are specific.
|
||||
@@ -104,25 +100,20 @@ in
|
||||
# roles = peer
|
||||
# machines = *
|
||||
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;
|
||||
hasRoleSettings = true;
|
||||
hasRoleSettings = false;
|
||||
specificMachineSettings = {
|
||||
timeout = "foo-peer-jon";
|
||||
};
|
||||
specificRoleSettings = {
|
||||
machines = {
|
||||
jon = {
|
||||
settings = {
|
||||
timeout = "foo-peer-jon";
|
||||
};
|
||||
settings = specificMachineSettings;
|
||||
};
|
||||
};
|
||||
settings = {
|
||||
timeout = "foo-peer";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{ createTestClan, lib, ... }:
|
||||
{ callInventoryAdapter, lib, ... }:
|
||||
let
|
||||
res = createTestClan {
|
||||
res = callInventoryAdapter {
|
||||
modules."A" = {
|
||||
_class = "clan.service";
|
||||
manifest = {
|
||||
@@ -21,31 +21,28 @@ let
|
||||
};
|
||||
};
|
||||
};
|
||||
inventory = {
|
||||
|
||||
machines = {
|
||||
jon = { };
|
||||
sara = { };
|
||||
machines = {
|
||||
jon = { };
|
||||
sara = { };
|
||||
};
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
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 = { };
|
||||
# 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 = { };
|
||||
};
|
||||
};
|
||||
|
||||
config = res.config._services.mappedServices.self-A;
|
||||
config = res.servicesEval.config.mappedServices.self-A;
|
||||
|
||||
#
|
||||
applySettings =
|
||||
@@ -1,6 +1,6 @@
|
||||
{ createTestClan, lib, ... }:
|
||||
{ callInventoryAdapter, lib, ... }:
|
||||
let
|
||||
res = createTestClan {
|
||||
res = callInventoryAdapter {
|
||||
modules."A" = m: {
|
||||
_class = "clan.service";
|
||||
config = {
|
||||
@@ -14,21 +14,19 @@ let
|
||||
default = m;
|
||||
};
|
||||
};
|
||||
inventory = {
|
||||
machines = {
|
||||
jon = { };
|
||||
};
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
roles.peer.machines.jon = { };
|
||||
machines = {
|
||||
jon = { };
|
||||
};
|
||||
instances."instance_foo" = {
|
||||
module = {
|
||||
name = "A";
|
||||
input = "self";
|
||||
};
|
||||
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
|
||||
{
|
||||
test_simple = {
|
||||
5
lib/modules/inventoryClass/builder/default.nix
Normal file
5
lib/modules/inventoryClass/builder/default.nix
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
imports = [
|
||||
./interface.nix
|
||||
];
|
||||
}
|
||||
28
lib/modules/inventoryClass/builder/interface.nix
Normal file
28
lib/modules/inventoryClass/builder/interface.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
}));
|
||||
};
|
||||
};
|
||||
}
|
||||
20
lib/modules/inventoryClass/inventory-introspection.nix
Normal file
20
lib/modules/inventoryClass/inventory-introspection.nix
Normal 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"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -115,7 +115,7 @@ in
|
||||
meta = lib.mkOption {
|
||||
type = lib.types.submoduleWith {
|
||||
modules = [
|
||||
./meta.nix
|
||||
./meta-interface.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
@@ -167,7 +167,7 @@ in
|
||||
'';
|
||||
type = types.submoduleWith {
|
||||
specialArgs = {
|
||||
inherit (config) machines clanLib;
|
||||
inherit (config) machines;
|
||||
};
|
||||
modules = [
|
||||
{
|
||||
@@ -359,7 +359,7 @@ in
|
||||
inherit clanLib;
|
||||
};
|
||||
}
|
||||
(import ./role.nix { })
|
||||
(import ./roles-interface.nix { })
|
||||
];
|
||||
}
|
||||
);
|
||||
@@ -31,20 +31,6 @@ let
|
||||
Under construction, will be used for the UI
|
||||
'';
|
||||
};
|
||||
tld = lib.mkOption {
|
||||
type = types.strMatching "[a-z]+";
|
||||
default = "clan";
|
||||
example = "ccc";
|
||||
description = ''
|
||||
Top level domain (TLD) of the clan. It should be set to a valid, but
|
||||
not already existing TLD.
|
||||
|
||||
It will be used to provide clan-internal services and resolve each host of the
|
||||
clan with:
|
||||
|
||||
<hostname>.<tld>
|
||||
'';
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
@@ -44,6 +44,12 @@ in
|
||||
description = ''
|
||||
List of additionally imported `.nix` expressions.
|
||||
|
||||
Supported types:
|
||||
|
||||
- **Strings**: Interpreted relative to the 'directory' passed to `lib.clan`.
|
||||
- **Paths**: should be relative to the current file.
|
||||
- **Any**: Nix expression must be serializable to JSON.
|
||||
|
||||
!!! Note
|
||||
**The import only happens if the machine is part of the service or role.**
|
||||
|
||||
@@ -67,8 +73,15 @@ in
|
||||
}
|
||||
```
|
||||
'';
|
||||
apply = value: if lib.isString value then value else builtins.seq (builtins.toJSON value) value;
|
||||
default = [ ];
|
||||
type = types.listOf types.raw;
|
||||
type = types.listOf (
|
||||
types.oneOf [
|
||||
types.str
|
||||
types.path
|
||||
(types.attrsOf types.anything)
|
||||
]
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
70
lib/modules/inventoryClass/service-list-from-inputs.nix
Normal file
70
lib/modules/inventoryClass/service-list-from-inputs.nix
Normal 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;
|
||||
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
directory,
|
||||
meta,
|
||||
}:
|
||||
# The following is a nixos/darwin module
|
||||
{
|
||||
_class,
|
||||
lib,
|
||||
@@ -21,7 +20,7 @@
|
||||
);
|
||||
|
||||
clan.core.settings = {
|
||||
inherit (meta) name icon tld;
|
||||
inherit (meta) name icon;
|
||||
inherit directory;
|
||||
machine = {
|
||||
inherit name;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user