Compare commits

..

1 Commits

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

View File

@@ -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

View File

@@ -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;

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,4 @@
{
clanLib,
...
}:
{ ... }:
let
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;

View File

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

View File

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

View File

@@ -16,7 +16,6 @@
options = {
host = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
ip address or hostname (domain) of the machine
'';

View File

@@ -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"

View File

@@ -1,33 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFuzCCA6OgAwIBAgIUNV3+MOkEcQinHmoFprxZfyR6TF4wDQYJKoZIhvcNAQEL
MIIFuTCCA6GgAwIBAgIUMXnA00bMrYvYSq0PjU5/HhXTpmcwDQYJKoZIhvcNAQEL
BQAwbDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh
bmNpc2NvMRUwEwYDVQQKDAxFeGFtcGxlIENvcnAxCzAJBgNVBAsMAklUMRQwEgYD
VQQDDAtleGFtcGxlLmNvbTAgFw0yNTEwMjExMzE3MTZaGA8yMTI1MDkyNzEzMTcx
NlowbDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh
bmNpc2NvMRUwEwYDVQQKDAxFeGFtcGxlIENvcnAxCzAJBgNVBAsMAklUMRQwEgYD
VQQDDAtleGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
AMbUCTs38JdEFlz+fiEwsEb9OV+6u4P5pkKkRFIJ04sTW9/NIeUJx5xOcAPn6B8K
mi+d6vHln2WDCNJHqthGHQDS250x8Qs+JrmtIvDPko+oDOlbWMPiT4Lv6p134+lV
obkiEMKSKz1gHuhlnHXFjkU+xTjxvEtGuq1+JPem4oJ9HUhSk1F6cftigzrYqUuk
JRROiUrbKiFp/TLedmAqQg/7wOrJKSKX91pQwNZhjB2/1REt0HP92W8uZIrzvLqq
JkrGfK9Y6e87DwXoTT0lvMAT7jbMsMWdGoCw/BQV8CwciUUG4ggI/jb+2TTktB3f
kMN/qRTKZ3zv/rn68RJfecAXYCQ2VfvO/Mr9nml2/cM7nrUBcs12YAHcm3766VWJ
pq6qBLcz/pHzMdt+/23nbO7bH2PL6r69VCSYvsDDnqpVL+LnYhgYUE0lPjuWuGmp
oKjggS6p4p1PXEQMOcj9UWdOyjefSzJsOp+25Of9SQzxHkBsVw0iArRFUYP6G15k
kNjYpuinFTw1XVDCFGPRIAhySnERlkv6WNyQQC87QTVJITKkz3R5cv4gwFG0kjAi
Va4nIJs2CctcizuEaPlwnEFrZ99gcB7RYPSUQVGAbfkqt2bhy/xGr+Jlp4kqPfS5
iPomwfcDwEnDbmcM8S2adPWtZ+oHskxZQmJ6+jhGgM73AgMBAAGjUzBRMB0GA1Ud
DgQWBBRHz2QAo1z8r9BewZro+HYv18AxTzAfBgNVHSMEGDAWgBRHz2QAo1z8r9Be
wZro+HYv18AxTzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCz
BTuZI7VymDWerWLfHMWyogoJWOkFB2yEpQe7J+LjS8yZmJg4CYpA4JJ+uM2sBm2Q
yL6M57ZmSY6EFoYeYw3gRfwGC32qJHirhsWvrjUpRC5+4YT9P6fNmgm5aD27JZao
bjyNA9Vy9SCL4JMeWET2w9VGNDaYQCs0x57HZioxYRMSD5vMVbirvCtqX7H3F/X+
r/VHEqEae7tVtuAB2D2GdcFzslCRb9uomuVfLJNqR6Nz1Tw+2adyySijRMCDdpRl
Pg9MBv4sevL6F4C1vUqUG1LXzcfHLFtrV1oUIEpJ0frxAgpdhSbnHiQa64cKX3N0
CsS6VALipGFmxj01+jD0Vhhf4rjjTT5C3Ag4WTqI98Fu4RMW35eBstnt6UUWyJQO
Q1skk+hg0ynfb3lO8OIZ4sDkmxDqAOQXeMMo1tU2YMgNA5Lv1FyO9Silc0VlkOiO
ft1RC8UbECqYyTvz7SNrv8aQP6EUoNSpxQHyBHOQy65dyOLOdP4S+PccUwsdxv/N
O5eN9ndMWqNvnyPKyQ3M+MLVvkCR1vDb6ABgPhH17BLkj8fWQgy5lhjJy5a8VHlO
1VDzV1Xeezy/MYCpS+TamaWTXscbhLMzWWiiAiDT8dltKw4G6U+g7DiF80kM59L5
D1hOs4gOQ853+83L/Ej4ESTj0B04NLVMlzMGtl3qcA==
VQQDDAtleGFtcGxlLmNvbTAeFw0yNTA5MTgxNDMzMzZaFw0yNTEwMTgxNDMzMzZa
MGwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5j
aXNjbzEVMBMGA1UECgwMRXhhbXBsZSBDb3JwMQswCQYDVQQLDAJJVDEUMBIGA1UE
AwwLZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC7
sdy27E/XMAyKrgeFcXY70R/vX0gx6EcZlWGp2vZSUVAfW1ni/Vq/LVC02sxGEGwv
10+42yP2yghi89doKo8oCoLsbVu+Pi+TmRsgAijy4jN8pHqbn9/Vk8M8utLa1u4z
VonSIx9pzCYd2+IIdwVuWoyPAAnK/JIKS3n0A8KWkZ/1lq6YDl2whj8iY4YF2Ekg
M0SWhquLZiaApAs7STTYvcP7iLfL4U6cH65dRAbwWMpMErPuLf/CedkXiSUp8Zqx
YIXXE5lf7wqt7tM6k6BHic9FEzAo1HnBWBXV5eB5fs1lX9M1VPmx43XINCfzKwxE
xODtIBrmvj+qOp6/ihBsu3LlOoOikxmL+T9Wgvf7fOuFC4BgmX85mGUV+EMZCDoJ
44jlwFF8wgrfG/ZawkP+opNsQLsdOm9DbAdWpx5+JYdgWBahjxuH4z2eIiBmMKgj
puqDgXdZzcERiYtOEEn0p0tvIkVLO3Tm2GjtHbmg1yF2nwsZjupGfcOGTVX4Zi5x
ZCs7vYgBtZy96kNAuyZcFl8eBUr/oVg//i3Zc9Vnw/UJryB7I6dvj228hlrSz0Ve
pGoeZXbcCzRv8NX2V0V1VTtrblSA3w5WRxVzK7UAVetPZ4dlJX+eyx3x2wiC3TiW
ZYH8haFubQqr1h9oXFHgDE5xYZKr51T3SRGfpn6KvQIDAQABo1MwUTAdBgNVHQ4E
FgQUJHOErJYWaGdla1XhxWha4XBKFYgwHwYDVR0jBBgwFoAUJHOErJYWaGdla1Xh
xWha4XBKFYgwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXqcg
DW6qzFccR+JTqNR5HBOneB07LxaUqfBTAzU5GTRljY3mVpnTa6vVvXlStChqdmwU
JJdRhWzTpzE4K92l4UKiYKy486PT1ff34aPLPX5BB9OzL4dgvC3gO0MYDJ84AFZl
6BN/MRTinioG+s14SsxmgcUTl+HXsxt75r3WKjXvqECqhONLPXEXDJ6TVmfb2yd5
X9cE6HLS2IXqfvs0EdXmQhSQVS7AlUQWZPDeoBTDUA1tT6ZKCcG0BuHEFnHxg4Yg
W9xp/wMJCEly+9eNJYZYzyK1AHRGnTMRCSifTJEybwI4A35v68FyRLfAC0lM2qVL
yQIGjj55+r4yGCK7bySSKjs59LLLxi6Px3S61OxAYq9KMT65nBLK9JAPFyTnikw9
q/xW208lL+kcRtG+ARo5ycx5QUjWdsHn7TCnqxnDhHznwSV4KGbJFaGQZTtgfcz0
g5a1GwxqHjEZ9IWiN38f2l4kpLLybKhwVQMYeG000s7rDa5hgjbh13qtQN6vUvI6
VozzZPnFcR1Rsa8RR9njDugxbVwlJQfGkoMiMZwNGgXnZRC2XaI6SCyPwqTPBuVP
ZR1eWv4qwsIGKJzJYcdChb5dimlTuVSfZmONpnrOP/4mhQLyaWr3XLqxxP3mIXsz
k1PNWTkgLsXO8DNkCudxcvPElXfmaw6zwaLrZys=
-----END CERTIFICATE-----

View File

@@ -1,18 +1,19 @@
{
"data": "ENC[AES256_GCM,data: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,iv:dGSmyDNBdVyF54bYS/Zxm2NNXZyGtLjkyYlrI9/nKvc=,tag:ip2fy76NjObWbW20HyuZUA==,type:str]",
"data": "ENC[AES256_GCM,data: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,iv:q3j8EAokyyxiszf+wyRqxEr2igaD1bX7YnFx/NbsGg8=,tag:HKKYWRJEUwW2/TxL+5dSng==,type:str]",
"sops": {
"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"
}
}

View File

@@ -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;
};
};

View File

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

View File

@@ -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 = {

View File

@@ -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,24 +88,20 @@ 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}"
@@ -229,12 +220,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 +234,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"
];
@@ -362,29 +349,25 @@ in
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";
};
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";
};
controllerPrefix = builtins.readFile (
config.clan.core.settings.directory
+ "/vars/per-machine/${ctrlName}/wireguard-network-${instanceName}/prefix/value"
);
peerSuffix = builtins.readFile (
config.clan.core.settings.directory
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/suffix/value"
);
in
"${controllerPrefix}:${peerSuffix}/96"
) roles.controller.machines;
@@ -394,21 +377,19 @@ in
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";
};
publicKey = (
builtins.readFile (
config.clan.core.settings.directory
+ "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
)
);
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"
];

View File

@@ -1,23 +1,7 @@
🚧🚧🚧 Experimental 🚧🚧🚧
Use at your own risk.
We are still refining its interfaces, instability and breakages are expected.
---
This module sets up [yggdrasil](https://yggdrasil-network.github.io/) across your clan.
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

View File

@@ -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,67 +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' "instances.*.networking.?peers.*.host.?plain" exports));
# Construct a list of peers in yggdrasil format
exportedPeers = lib.flatten (map mkPeers exportedPeerIPs);
in
{
# Set <yggdrasil ip> <hostname>.<tld> for all hosts.
# Networking modules will then add themselves as peers, so we can
# always use this to resolve a host via the best possible route,
# doing fail-over if needed.
networking.extraHosts = lib.strings.concatStringsSep "\n" (
lib.filter (n: n != "") (
map (
name:
let
ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/yggdrasil/address/value";
in
if builtins.pathExists ipPath then
"${builtins.readFile ipPath} ${name}.${config.clan.core.settings.tld}"
else
""
) (lib.attrNames roles.default.machines)
)
);
clan.core.vars.generators.yggdrasil = {
files.privateKey = { };
@@ -151,7 +99,7 @@
settings = {
PrivateKeyPath = "/key";
IfName = "ygg";
Peers = lib.lists.unique (exportedPeers ++ settings.extraPeers);
Peers = settings.peers;
MulticastInterfaces = [
# Ethernet is preferred over WIFI
{

View File

@@ -17,13 +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";
roles.default.machines.peer2.settings.host = "peer2";
};
};
};

View File

@@ -1,7 +1,4 @@
{
clanLib,
...
}:
{ ... }:
{
_class = "clan.service";
manifest.name = "clan-core/zerotier";
@@ -42,7 +39,6 @@
imports = [
(import ./shared.nix {
inherit
clanLib
instanceName
roles
config
@@ -94,7 +90,6 @@
imports = [
(import ./shared.nix {
inherit
clanLib
instanceName
roles
config
@@ -147,7 +142,6 @@
imports = [
(import ./shared.nix {
inherit
clanLib
instanceName
roles
config
@@ -166,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

View File

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

View File

@@ -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
View File

@@ -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": {

View File

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

View File

@@ -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

View File

@@ -10,11 +10,10 @@ and how to define a remote builder for your machine closures.
Set the machines `targetHost` to the reachable IP address of the new machine.
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.

View File

@@ -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 = {

View File

@@ -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 = {};

View File

@@ -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
View File

@@ -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": {

View File

@@ -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

View File

@@ -39,10 +39,44 @@ 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;
---
'';
};
# TODO: Pinpox
checks.conflictingPorts =
let
conflicts = [ ];
in
{
assertion = builtins.length conflicts == 0;
message = ''
The following endpoints have conflicting port assignments:
---
'';
};
}
];
};
# Important: !This logic needs to be kept in sync with lib.clan function!
apply = 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

View File

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

View File

@@ -33,23 +33,20 @@
let
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
];
}

View File

@@ -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

View File

@@ -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;
};

View File

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

View File

@@ -10,11 +10,12 @@ in
rec {
# 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
'';
};
};
}

View File

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

View File

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

View File

@@ -110,7 +110,9 @@ in
# TODO: make this writable by moving the options from inventoryClass into clan.
exports = lib.mkOption {
type = types.lazyAttrsOf (types.submoduleWith { modules = [ config.exportsModule ]; });
readOnly = true;
visible = false;
internal = true;
};
exportsModule = lib.mkOption {
@@ -118,6 +120,30 @@ in
visible = false;
type = types.deferredModule;
default = {
options.endpoints = lib.mkOption {
type = types.attrsWith {
placeholder = "endpointName";
elemType = (
types.submodule {
options = {
port = lib.mkOption {
type = types.int;
description = "The port the service is running on";
};
protocol = lib.mkOption {
type = types.enum [
"tcp"
"udp"
];
default = "tcp";
description = "The protocol used to access the service";
};
};
}
);
};
default = { };
};
options.networking = lib.mkOption {
default = null;
type = lib.types.nullOr (
@@ -286,7 +312,7 @@ in
Global information about the clan.
'';
type = types.deferredModuleWith {
staticModules = [ ../inventoryClass/meta.nix ];
staticModules = [ ../inventoryClass/meta-interface.nix ];
};
default = { };
};

View File

@@ -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
];
};

View File

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

View File

@@ -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;

View File

@@ -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
{

View File

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

View File

@@ -2,11 +2,15 @@
lib,
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; };
};

View File

@@ -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 = { };
};
};
}

View File

@@ -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;
})
]

View File

@@ -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

View File

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

View File

@@ -81,7 +81,6 @@ let
applySettings =
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
)
)
);
}
];
}

View File

@@ -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; };
}

View File

@@ -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";
};
};
};
}

View File

@@ -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";
};

View File

@@ -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";
};
};
};
};

View File

@@ -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 =

View File

@@ -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 = {

View File

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

View File

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

View File

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

View File

@@ -115,7 +115,7 @@ in
meta = lib.mkOption {
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 { })
];
}
);

View File

@@ -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
{

View File

@@ -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)
]
);
};
};
}

View File

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

View File

@@ -3,7 +3,6 @@
directory,
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
{ lib, nixpkgs, ... }:
{
checks.minNixpkgsVersion = {
assertion = lib.versionAtLeast nixpkgs.lib.version "25.11";
message = ''
Nixpkgs version: ${nixpkgs.lib.version} is incompatible with clan-core. (>= 25.11 is recommended)
---
Your version of 'nixpkgs' seems too old for clan-core.
Please read: https://docs.clan.lol/guides/nixpkgs-flake-input
You can ignore this check by setting:
clan.checks.minNixpkgsVersion.ignore = true;
---
'';
};
}

View File

@@ -1,25 +0,0 @@
/**
Root 'clan' Module
Defines lib.clan and flake-parts.clan options
and all common logic for the 'clan' module.
- has Class _class = "clan"
- _module.args.clan-core: reference to clan-core flake
- _module.args.clanLib: reference to lib.clan function
*/
{ clan-core }:
{
_class = "clan";
_module.args = {
inherit clan-core;
inherit (clan-core) clanLib;
};
imports = [
./top-level-interface.nix
./module.nix
./distributed-services.nix
./checks.nix
];
}

View File

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

View File

@@ -1,27 +0,0 @@
{ self, lib, ... }:
let
clanModule = lib.modules.importApply ./default.nix { clan-core = self; };
in
{
flake.modules.clan.default = clanModule;
perSystem =
{
pkgs,
lib,
...
}:
let
jsonDocs = import ./eval-docs.nix {
clan-core = self;
inherit
pkgs
lib
clanModule
;
clanLib = self.clanLib;
};
in
{
legacyPackages.clan-options = jsonDocs.optionsJSON;
};
}

View File

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

View File

@@ -1,156 +0,0 @@
{
lib,
clanLib,
config,
...
}:
let
inherit (lib) types mkOption;
submodule = m: types.submoduleWith { modules = [ m ]; };
inspectModule =
inputName: moduleName: module:
let
eval = clanLib.evalService {
modules = [ module ];
prefix = [
inputName
"clan"
"modules"
moduleName
];
};
in
{
manifest = eval.config.manifest;
roles = lib.mapAttrs (_n: v: { inherit (v) description; }) eval.config.roles;
};
exposedInventory = lib.intersectAttrs {
meta = null;
machines = null;
instances = null;
tags = null;
} config.inventory;
filterAttrsRecursive' =
path: pred: set:
lib.listToAttrs (
lib.concatMap (
name:
let
v = set.${name};
loc = path ++ [ name ];
in
if pred loc v then
[
(lib.nameValuePair name (if lib.isAttrs v then filterAttrsRecursive' loc pred v else v))
]
else
[ ]
) (lib.attrNames set)
);
filteredInventory = filterAttrsRecursive' [ ] (
# Remove extraModules from serialization,
# identified by: prefix + pathLength + name
# inventory.instances.*.roles.*.extraModules
path: _value: !(lib.length path == 5 && ((lib.last path)) == "extraModules")
) exposedInventory;
in
{
options = {
flakeInputs = mkOption {
type = types.raw;
};
exportsModule = mkOption {
type = types.raw;
};
inventory = mkOption {
type = types.raw;
};
inventorySerialization = mkOption {
type = types.raw;
readOnly = true;
default = filteredInventory;
};
directory = mkOption {
type = types.path;
};
relativeDirectory = mkOption {
type = types.str;
internal = true;
description = ''
The relative directory path from the flake root to the clan directory.
Empty string if directory equals the flake root.
'';
};
machines = mkOption {
type = types.attrsOf (submodule ({
options = {
machineImports = mkOption {
type = types.listOf types.raw;
};
};
}));
};
introspection = lib.mkOption {
readOnly = true;
# TODO: use options.inventory instead of the evaluate config attribute
default =
builtins.removeAttrs (clanLib.introspection.getPrios { options = config.inventory.options; })
# tags are freeformType which is not supported yet.
# services is removed and throws an error if accessed.
[
"tags"
"services"
];
};
staticModules = lib.mkOption {
readOnly = true;
type = lib.types.raw;
apply = moduleSet: lib.mapAttrs (inspectModule "<clan-core>") moduleSet;
};
modulesPerSource = lib.mkOption {
# { sourceName :: { moduleName :: {} }}
readOnly = true;
type = lib.types.raw;
default =
let
inputsWithModules = lib.filterAttrs (_inputName: v: v ? clan.modules) config.flakeInputs;
in
lib.mapAttrs (
inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules
) inputsWithModules;
};
moduleSchemas = lib.mkOption {
# { sourceName :: { moduleName :: { roleName :: Schema }}}
readOnly = true;
type = lib.types.raw;
default = lib.mapAttrs (
_inputName: moduleSet:
lib.mapAttrs (
_moduleName: module:
(clanLib.evalService {
modules = [ module ];
prefix = [ ];
}).config.result.api.schema
) moduleSet
) config.modulesPerSource;
};
templatesPerSource = lib.mkOption {
# { sourceName :: { moduleName :: {} }}
readOnly = true;
type = lib.types.raw;
default =
let
inputsWithTemplates = lib.filterAttrs (_inputName: v: v ? clan.templates) config.flakeInputs;
in
lib.mapAttrs (_inputName: v: lib.mapAttrs (_n: t: t) v.clan.templates) inputsWithTemplates;
};
};
}

View File

@@ -106,14 +106,6 @@ in
# Set by the flake, so it's read-only in the machine
readOnly = true;
};
tld = lib.mkOption {
type = types.strMatching "[a-z]+";
description = ''
the TLD for the clan
'';
# Set by the flake, so it's read-only in the machine
readOnly = true;
};
machine = mkOption {
description = ''
Settings of the machine.

View File

@@ -1,11 +1,9 @@
{ lib, ... }:
{
perSystem =
{
self',
pkgs,
config,
system,
...
}:
{
@@ -22,6 +20,7 @@
clan-ts-api = config.packages.clan-ts-api;
fonts = config.packages.fonts;
};
};
# //
# todo add darwin support
@@ -42,11 +41,6 @@
inherit (config.packages) clan-ts-api;
};
checks =
config.packages.clan-app.tests
# Sandboxed Darwin nix build can't spawn a headless brwoser
// lib.optionalAttrs (!lib.hasSuffix "darwin" system) {
inherit (config.packages.clan-app-ui.tests) clan-app-ui-storybook;
};
checks = config.packages.clan-app.tests;
};
}

View File

@@ -12,13 +12,11 @@
fetchzip,
process-compose,
json2ts,
playwright,
playwright-driver,
luakit,
jq,
self',
}:
let
RED = "\\033[1;31m";
GREEN = "\\033[1;32m";
NC = "\\033[0m";
@@ -110,39 +108,32 @@ mkShell {
export PC_CONFIG_FILES="$CLAN_CORE_PATH/pkgs/clan-app/process-compose.yaml"
echo -e "${GREEN}To launch a qemu VM for testing, run:\n start-vm <number of VMs>${NC}"
''
+
# todo darwin support needs some work
(lib.optionalString stdenv.hostPlatform.isLinux ''
# configure playwright for storybook snapshot testing
# we only want webkit as that matches what the app is rendered with
# configure playwright for storybook snapshot testing
# we only want webkit as that matches what the app is rendered with
playwright_ver=$(${jq}/bin/jq --raw-output .devDependencies.playwright ${./ui/package.json})
if [[ $playwright_ver != '${playwright.version}' ]]; then
echo >&2 -en '${RED}'
echo >&2 "Error: playwright npm package version ($playwright_ver) is different from that from the nixpkgs (${playwright.version})"
echo >&2 "Run this command to update the npm package version"
echo >&2
echo >&2 " npm i -D --save-exact playwright@${playwright.version}"
echo >&2
echo >&2 -en '${NC}'
else
export PLAYWRIGHT_BROWSERS_PATH=${
playwright.browsers.override {
playwright-driver.browsers.override {
withFfmpeg = false;
withFirefox = true;
withWebkit = false;
withFirefox = false;
withWebkit = true;
withChromium = false;
withChromiumHeadlessShell = false;
}
}
# This is needed to disable revisionOverrides in browsers.json which
# the playwright nix package does not support
# https://github.com/NixOS/nixpkgs/blob/f9c3b27aa3f9caac6717973abcc549dbde16bdd4/pkgs/development/web/playwright/driver.nix#L261
export PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=nixos
# stop playwright from trying to validate it has downloaded the necessary browsers
# we are providing them manually via nix
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=1
fi
'';
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true
# playwright browser drivers are versioned e.g. webkit-2191
# this helps us avoid having to update the playwright js dependency everytime we update nixpkgs and vice versa
# see vitest.config.js for corresponding launch configuration
export PLAYWRIGHT_WEBKIT_EXECUTABLE=$(find -L "$PLAYWRIGHT_BROWSERS_PATH" -type f -name "pw_run.sh")
'');
}

View File

@@ -4,11 +4,8 @@
importNpmLock,
clan-ts-api,
fonts,
ps,
jq,
playwright,
}:
buildNpmPackage (finalAttrs: {
buildNpmPackage (_finalAttrs: {
pname = "clan-app-ui";
version = "0.0.1";
nodejs = nodejs_22;
@@ -35,53 +32,36 @@ buildNpmPackage (finalAttrs: {
# todo figure out why this fails only inside of Nix
# Something about passing orientation in any of the Form stories is causing the browser to crash
# `npm run test-storybook-static` works fine in the devshell
passthru = {
tests = {
"${finalAttrs.pname}-storybook" = buildNpmPackage {
pname = "${finalAttrs.pname}-storybook";
inherit (finalAttrs)
version
nodejs
src
npmDeps
npmConfigHook
;
nativeBuildInputs = finalAttrs.nativeBuildInputs ++ [
ps
jq
];
npmBuildScript = "test-storybook";
env = {
PLAYWRIGHT_BROWSERS_PATH = "${playwright.browsers.override {
withFfmpeg = false;
withFirefox = true;
withWebkit = false;
withChromium = false;
withChromiumHeadlessShell = false;
}}";
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = true;
# This is needed to disable revisionOverrides in browsers.json which
# the playwright nix package does not support:
# https://github.com/NixOS/nixpkgs/blob/f9c3b27aa3f9caac6717973abcc549dbde16bdd4/pkgs/development/web/playwright/driver.nix#L261
PLAYWRIGHT_HOST_PLATFORM_OVERRIDE = "nixos";
DEBUG = "vitest:*";
};
preBuild = finalAttrs.preBuild + ''
playwright_ver=$(jq --raw-output .devDependencies.playwright ${./ui/package.json})
if [[ $playwright_ver != '${playwright.version}' ]]; then
echo >&2 "playwright npm package version ($playwright_ver) is different from that from the nixpkgs (${playwright.version})"
echo >&2 "Run this command to update the npm package version"
echo >&2
echo >&2 " npm i -D --save-exact playwright@${playwright.version}"
echo >&2
exit 1
fi
'';
};
};
};
#
# passthru = rec {
# storybook = buildNpmPackage {
# pname = "${finalAttrs.pname}-storybook";
# inherit (finalAttrs)
# version
# nodejs
# src
# npmDeps
# npmConfigHook
# preBuild
# ;
#
# nativeBuildInputs = finalAttrs.nativeBuildInputs ++ [
# ps
# ];
#
# npmBuildScript = "test-storybook-static";
#
# env = finalAttrs.env // {
# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = 1;
# PLAYWRIGHT_BROWSERS_PATH = "${playwright-driver.browsers.override {
# withChromiumHeadlessShell = true;
# }}";
# PLAYWRIGHT_HOST_PLATFORM_OVERRIDE = "ubuntu-24.04";
# };
#
# postBuild = ''
# mv storybook-static $out
# '';
# };
# };
})

View File

@@ -2,4 +2,5 @@ app/api
app/.fonts
.vite
*.css.d.ts
storybook-static
*.css.d.ts

View File

@@ -1,20 +1,31 @@
import type { StorybookConfig } from "storybook-solidjs-vite";
import { mergeConfig } from "vite";
import type { StorybookConfig } from "@kachurun/storybook-solid-vite";
export default {
framework: "storybook-solidjs-vite",
const config: StorybookConfig = {
framework: "@kachurun/storybook-solid-vite",
stories: ["../src/**/*.mdx", "../src/**/*.stories.tsx"],
addons: [
"@storybook/addon-links",
"@storybook/addon-docs",
"@storybook/addon-a11y",
{
name: "@storybook/addon-vitest",
options: {
cli: false,
},
},
],
async viteFinal(config) {
return mergeConfig(config, {
define: { "process.env": {} },
});
},
core: {
disableTelemetry: true,
},
} satisfies StorybookConfig;
typescript: {
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
// 👇 Default prop filter, which excludes props from node_modules
propFilter: (prop: any) =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
},
},
};
export default config;

View File

@@ -1,4 +1,4 @@
import type { Preview } from "storybook-solidjs-vite";
import type { Preview } from "@kachurun/storybook-solid-vite";
import "../src/index.css";
import "./preview.css";

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