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
238 changed files with 3687 additions and 9641 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

@@ -58,53 +58,51 @@
pkgs.buildPackages.xorg.lndir
pkgs.glibcLocales
pkgs.kbd.out
self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".pkgs.perlPackages.ConfigIniFiles
self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".pkgs.perlPackages.FileSlurp
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.ConfigIniFiles
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.FileSlurp
pkgs.bubblewrap
self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.toplevel
self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.diskoScript
self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.diskoScript.drvPath
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.toplevel
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.diskoScript
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.diskoScript.drvPath
]
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
in
{
# Skip flash test on aarch64-linux for now as it's too slow
checks =
lib.optionalAttrs (pkgs.stdenv.isLinux && pkgs.stdenv.hostPlatform.system != "aarch64-linux")
{
nixos-test-flash = self.clanLib.test.baseTest {
name = "flash";
nodes.target = {
virtualisation.emptyDiskImages = [ 4096 ];
virtualisation.memorySize = 4096;
checks = lib.optionalAttrs (pkgs.stdenv.isLinux && pkgs.hostPlatform.system != "aarch64-linux") {
nixos-test-flash = self.clanLib.test.baseTest {
name = "flash";
nodes.target = {
virtualisation.emptyDiskImages = [ 4096 ];
virtualisation.memorySize = 4096;
virtualisation.useNixStoreImage = true;
virtualisation.writableStore = true;
virtualisation.useNixStoreImage = true;
virtualisation.writableStore = true;
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
environment.etc."install-closure".source = "${closureInfo}/store-paths";
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
environment.etc."install-closure".source = "${closureInfo}/store-paths";
nix.settings = {
substituters = lib.mkForce [ ];
hashed-mirrors = null;
connect-timeout = lib.mkForce 3;
flake-registry = "";
experimental-features = [
"nix-command"
"flakes"
];
};
};
testScript = ''
start_all()
machine.succeed("echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRWUusawhlIorx7VFeQJHmMkhl9X3QpnvOdhnV/bQNG root@target' > ./test_id_ed25519.pub")
# Some distros like to automount disks with spaces
machine.succeed('mkdir -p "/mnt/with spaces" && mkfs.ext4 /dev/vdc && mount /dev/vdc "/mnt/with spaces"')
machine.succeed("clan flash write --ssh-pubkey ./test_id_ed25519.pub --keymap de --language de_DE.UTF-8 --debug --flake ${self.checks.x86_64-linux.clan-core-for-checks} --yes --disk main /dev/vdc test-flash-machine-${pkgs.stdenv.hostPlatform.system}")
'';
} { inherit pkgs self; };
nix.settings = {
substituters = lib.mkForce [ ];
hashed-mirrors = null;
connect-timeout = lib.mkForce 3;
flake-registry = "";
experimental-features = [
"nix-command"
"flakes"
];
};
};
testScript = ''
start_all()
machine.succeed("echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRWUusawhlIorx7VFeQJHmMkhl9X3QpnvOdhnV/bQNG root@target' > ./test_id_ed25519.pub")
# Some distros like to automount disks with spaces
machine.succeed('mkdir -p "/mnt/with spaces" && mkfs.ext4 /dev/vdc && mount /dev/vdc "/mnt/with spaces"')
machine.succeed("clan flash write --ssh-pubkey ./test_id_ed25519.pub --keymap de --language de_DE.UTF-8 --debug --flake ${self.checks.x86_64-linux.clan-core-for-checks} --yes --disk main /dev/vdc test-flash-machine-${pkgs.hostPlatform.system}")
'';
} { inherit pkgs self; };
};
};
}

View File

@@ -160,9 +160,9 @@
closureInfo = pkgs.closureInfo {
rootPaths = [
privateInputs.clan-core-for-checks
self.nixosConfigurations."test-install-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.toplevel
self.nixosConfigurations."test-install-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.initialRamdisk
self.nixosConfigurations."test-install-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.diskoScript
self.nixosConfigurations."test-install-machine-${pkgs.hostPlatform.system}".config.system.build.toplevel
self.nixosConfigurations."test-install-machine-${pkgs.hostPlatform.system}".config.system.build.initialRamdisk
self.nixosConfigurations."test-install-machine-${pkgs.hostPlatform.system}".config.system.build.diskoScript
pkgs.stdenv.drvPath
pkgs.bash.drvPath
pkgs.buildPackages.xorg.lndir
@@ -215,7 +215,7 @@
# Prepare test flake and Nix store
flake_dir = prepare_test_flake(
temp_dir,
"${self.checks.${pkgs.stdenv.hostPlatform.system}.clan-core-for-checks}",
"${self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks}",
"${closureInfo}"
)
@@ -296,7 +296,7 @@
# Prepare test flake and Nix store
flake_dir = prepare_test_flake(
temp_dir,
"${self.checks.${pkgs.stdenv.hostPlatform.system}.clan-core-for-checks}",
"${self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks}",
"${closureInfo}"
)

View File

@@ -1,82 +0,0 @@
{ self, pkgs, ... }:
let
cli = self.packages.${pkgs.stdenv.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.stdenv.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

@@ -2,7 +2,7 @@
let
cli = self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli-full;
cli = self.packages.${pkgs.hostPlatform.system}.clan-cli-full;
in
{
name = "systemd-abstraction";
@@ -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

@@ -115,9 +115,9 @@
let
closureInfo = pkgs.closureInfo {
rootPaths = [
self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli
self.checks.${pkgs.stdenv.hostPlatform.system}.clan-core-for-checks
self.clanInternals.machines.${pkgs.stdenv.hostPlatform.system}.test-update-machine.config.system.build.toplevel
self.packages.${pkgs.hostPlatform.system}.clan-cli
self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-update-machine.config.system.build.toplevel
pkgs.stdenv.drvPath
pkgs.bash.drvPath
pkgs.buildPackages.xorg.lndir
@@ -132,7 +132,7 @@
imports = [ self.nixosModules.test-update-machine ];
};
extraPythonPackages = _p: [
self.legacyPackages.${pkgs.stdenv.hostPlatform.system}.nixosTestLib
self.legacyPackages.${pkgs.hostPlatform.system}.nixosTestLib
];
testScript = ''
@@ -154,7 +154,7 @@
# Prepare test flake and Nix store
flake_dir = prepare_test_flake(
temp_dir,
"${self.checks.${pkgs.stdenv.hostPlatform.system}.clan-core-for-checks}",
"${self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks}",
"${closureInfo}"
)
(flake_dir / ".clan-flake").write_text("") # Ensure .clan-flake exists
@@ -226,7 +226,7 @@
"--to",
"ssh://root@192.168.1.1",
"--no-check-sigs",
f"${self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli}",
f"${self.packages.${pkgs.hostPlatform.system}.clan-cli}",
"--extra-experimental-features", "nix-command flakes",
],
check=True,
@@ -242,7 +242,7 @@
"-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no",
f"root@192.168.1.1",
"${self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli}/bin/clan",
"${self.packages.${pkgs.hostPlatform.system}.clan-cli}/bin/clan",
"machines",
"update",
"--debug",
@@ -270,7 +270,7 @@
# Run clan update command
subprocess.run([
"${self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli-full}/bin/clan",
"${self.packages.${pkgs.hostPlatform.system}.clan-cli-full}/bin/clan",
"machines",
"update",
"--debug",
@@ -297,7 +297,7 @@
# Run clan update command with --build-host
subprocess.run([
"${self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli-full}/bin/clan",
"${self.packages.${pkgs.hostPlatform.system}.clan-cli-full}/bin/clan",
"machines",
"update",
"--debug",

View File

@@ -1,6 +1,3 @@
!!! Danger "Experimental"
This service is experimental and will change in the future.
This service sets up a certificate authority (CA) that can issue certificates to
other machines in your clan. For this the `ca` role is used.
It additionally provides a `default` role, that can be applied to all machines

View File

@@ -1,6 +1,3 @@
!!! Danger "Experimental"
This service is experimental and will change in the future.
This module enables hosting clan-internal services easily, which can be resolved
inside your VPN. This allows defining a custom top-level domain (e.g. `.clan`)
and exposing endpoints from a machine to others, which will be

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

@@ -1,83 +1 @@
!!! Danger "Experimental"
This service is for demonstration purpose only and may change in the future.
The Hello-World Clan Service is a minimal example showing how to build and register your own service.
It serves as a reference implementation and is used in clan-core CI tests to ensure compatibility.
## What it demonstrates
- How to define a basic Clan-compatible service.
- How to structure your service for discovery and configuration.
- How Clan services interact with nixos.
## Testing
This service demonstrates two levels of testing to ensure quality and stability across releases:
1. **Unit & Integration Testing** — via [`nix-unit`](https://github.com/nix-community/nix-unit)
2. **End-to-End Testing** — via **NixOS VM tests**, which we extended to support **container virtualization** for better performance.
We highly advocate following the [Practical Testing Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html):
* Write **unit tests** for core logic and invariants.
* Add **one or two end-to-end (E2E)** tests to confirm your service starts and behaves correctly in a real NixOS environment.
NixOS is **untyped** and frequently changes; tests are the safest way to ensure long-term stability of services.
```
/ \
/ \
/ E2E \
/-------\
/ \
/Integration\
/-------------\
/ \
/ Unit Tests \
-------------------
```
### nix-unit
We highly advocate the usage of
[nix-unit](https://github.com/nix-community/nix-unit)
Example in: tests/eval-tests.nix
If you use flake-parts you can use the [native integration](https://flake.parts/options/nix-unit.html)
If nix-unit succeeds you'r nixos evaluation should be mostly correct.
!!! Tip
- Ensure most used 'settings' and variants are tested.
- Think about some important edge-cases your system should handle.
### NixOS VM / Container Test
!!! Warning "Early Vars & clanTest"
The testing system around vars is experimental
`clanTest` is still experimental and enables container virtualization by default.
This is still early and might have some limitations.
Some minimal boilerplate is needed to use `clanTest`
```nix
nixosLib = import (inputs.nixpkgs + "/nixos/lib") { }
nixosLib.runTest (
{ ... }:
{
imports = [
self.modules.nixosTest.clanTest
# Example in tests/vm/default.nix
testModule
];
hostPkgs = pkgs;
# Uncomment if you don't want or cannot use containers
# test.useContainers = false;
}
)
```
This a test README just to appease the eval warnings if we don't have one

View File

@@ -8,7 +8,7 @@
{
_class = "clan.service";
manifest.name = "clan-core/hello-word";
manifest.description = "Minimal example clan service that greets the world";
manifest.description = "This is a test";
manifest.readme = builtins.readFile ./README.md;
# This service provides two roles: "morning" and "evening". Roles can be
@@ -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

@@ -26,7 +26,7 @@ in
# The hello-world service being tested
../../clanServices/hello-world
# Required modules
../../nixosModules
../../nixosModules/clanCore
];
testName = "hello-world";
tests = ./tests/eval-tests.nix;

View File

@@ -4,7 +4,7 @@
...
}:
let
testClan = clanLib.clan {
testFlake = clanLib.clan {
self = { };
# Point to the folder of the module
# TODO: make this optional
@@ -33,20 +33,10 @@ let
};
in
{
/**
We highly advocate the usage of:
https://github.com/nix-community/nix-unit
If you use flake-parts you can use the native integration: https://flake.parts/options/nix-unit.html
*/
test_simple = {
# Allows inspection via the nix-repl
# Ignored by nix-unit; it only looks at 'expr' and 'expected'
inherit testClan;
config = testFlake.config;
# Assert that jon has the
# configured greeting in 'environment.etc.hello.text'
expr = testClan.config.nixosConfigurations.jon.config.environment.etc."hello".text;
expected = "Good evening World!";
expr = { };
expected = { };
};
}

View File

@@ -1,5 +1,8 @@
!!! Danger "Experimental"
This service is experimental and will change in the future.
🚧🚧🚧 Experimental 🚧🚧🚧
Use at your own risk.
We are still refining its interfaces, instability and breakages are expected.
---

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

@@ -1,6 +1,3 @@
!!! Danger "Experimental"
This service is experimental and will change in the future.
## Usage
```

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

@@ -1,5 +1,8 @@
!!! Danger "Experimental"
This service is experimental and will change in the future.
🚧🚧🚧 Experimental 🚧🚧🚧
Use at your own risk.
We are still refining its interfaces, instability and breakages are expected.
---

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,20 +1,7 @@
!!! Danger "Experimental"
This service is experimental and will change in the future.
This module sets up [yggdrasil](https://yggdrasil-network.github.io/) across your clan.
---
This module sets up [yggdrasil](https://yggdrasil-network.github.io/) across your clan.
Yggdrasil is designed to be a future-proof and decentralised alternative to the
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": 1762113984,
"narHash": "sha256-Gwah5F3ONMhvTYbsnJM4bAv0qcaI3wjz1Nq0rBGWVgo=",
"lastModified": 1760361585,
"narHash": "sha256-v4PnSmt1hXW4dSgVWxcd1ZeEBlhO7NksNRC5cX7L5iw=",
"ref": "main",
"rev": "0f847b4799deee4a2c878ba69bda9c446fe16177",
"rev": "7e7e58eb64ef61beb0a938a6622ec0122382131b",
"shallow": true,
"type": "git",
"url": "https://git.clan.lol/clan/clan-core"
@@ -105,11 +105,11 @@
},
"nixpkgs-dev": {
"locked": {
"lastModified": 1762080734,
"narHash": "sha256-fFunzA7ITlPHRr7dECaFGTBucNiWYEVDNPBw/9gFmII=",
"lastModified": 1760965023,
"narHash": "sha256-cpcgkeLApMGFCdp4jFqeIxTwlcGaSI+Zwmv8z2E85pY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "bc7f6fa86de9b208edf4ea7bbf40bcd8cc7d70a5",
"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

@@ -51,7 +51,7 @@ Make sure you have the following:
**Note:** This creates a new directory in your current location
```shellSession
nix run "https://git.clan.lol/clan/clan-core/archive/main.tar.gz#clan-cli" --refresh -- flakes create
nix run https://git.clan.lol/clan/clan-core/archive/main.tar.gz#clan-cli --refresh -- flakes create
```
3. Enter a **name** in the prompt:
@@ -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.

36
flake.lock generated
View File

@@ -31,11 +31,11 @@
]
},
"locked": {
"lastModified": 1761899396,
"narHash": "sha256-XOpKBp6HLzzMCbzW50TEuXN35zN5WGQREC7n34DcNMM=",
"lastModified": 1760701190,
"narHash": "sha256-y7UhnWlER8r776JsySqsbTUh2Txf7K30smfHlqdaIQw=",
"owner": "nix-community",
"repo": "disko",
"rev": "6f4cf5abbe318e4cd1e879506f6eeafd83f7b998",
"rev": "3a9450b26e69dcb6f8de6e2b07b3fc1c288d85f5",
"type": "github"
},
"original": {
@@ -51,11 +51,11 @@
]
},
"locked": {
"lastModified": 1762040540,
"narHash": "sha256-z5PlZ47j50VNF3R+IMS9LmzI5fYRGY/Z5O5tol1c9I4=",
"lastModified": 1760948891,
"narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "0010412d62a25d959151790968765a70c436598b",
"rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04",
"type": "github"
},
"original": {
@@ -71,11 +71,11 @@
]
},
"locked": {
"lastModified": 1762039661,
"narHash": "sha256-oM5BwAGE78IBLZn+AqxwH/saqwq3e926rNq5HmOulkc=",
"lastModified": 1760721282,
"narHash": "sha256-aAHphQbU9t/b2RRy2Eb8oMv+I08isXv2KUGFAFn7nCo=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "c3c8c9f2a5ed43175ac4dc030308756620e6e4e4",
"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,8 +30,6 @@ 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 { };

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,36 +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;
};
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

@@ -120,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 (
@@ -288,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

@@ -28,9 +28,9 @@ in
elemType = submoduleWith {
class = "clan.service";
specialArgs = {
exports = config.exports;
directory = directory;
clanLib = specialArgs.clanLib;
exports = config.exports;
};
modules = [
(

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);
};
}
)
@@ -507,9 +483,6 @@ in
type = types.deferredModule;
default = { };
description = ''
!!! Danger "Experimental Feature"
This feature is experimental and will change in the future.
export modules defined in 'perInstance'
mapped to their instance name
@@ -638,9 +611,6 @@ in
type = types.deferredModule;
default = { };
description = ''
!!! Danger "Experimental Feature"
This feature is experimental and will change in the future.
export modules defined in 'perMachine'
mapped to their machine name
@@ -742,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.
@@ -883,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);
};
}

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

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

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;
})
# 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

@@ -18,7 +18,7 @@ let
inputs.data-mesher.nixosModules.data-mesher
];
config = {
clan.core.clanPkgs = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system};
clan.core.clanPkgs = lib.mkDefault self.packages.${pkgs.hostPlatform.system};
};
};
in

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