Merge branch 'main' into ke-docs-testing-container

This commit is contained in:
Kenji Berthold
2025-11-03 13:06:14 +00:00
58 changed files with 771 additions and 730 deletions

View File

@@ -58,51 +58,53 @@
pkgs.buildPackages.xorg.lndir
pkgs.glibcLocales
pkgs.kbd.out
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.ConfigIniFiles
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.FileSlurp
self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".pkgs.perlPackages.ConfigIniFiles
self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".pkgs.perlPackages.FileSlurp
pkgs.bubblewrap
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
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
]
++ 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.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.stdenv.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"
];
};
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; };
};
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.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
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
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.hostPlatform.system}.clan-core-for-checks}",
"${self.checks.${pkgs.stdenv.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.hostPlatform.system}.clan-core-for-checks}",
"${self.checks.${pkgs.stdenv.hostPlatform.system}.clan-core-for-checks}",
"${closureInfo}"
)

View File

@@ -2,7 +2,7 @@
let
cli = self.packages.${pkgs.hostPlatform.system}.clan-cli-full;
cli = self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli-full;
ollama-model = pkgs.callPackage ./qwen3-4b-instruct.nix { };
in
@@ -53,7 +53,7 @@ in
pytest
pytest-xdist
(cli.pythonRuntime.pkgs.toPythonModule cli)
self.legacyPackages.${pkgs.hostPlatform.system}.nixosTestLib
self.legacyPackages.${pkgs.stdenv.hostPlatform.system}.nixosTestLib
]
))
];

View File

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

View File

@@ -115,9 +115,9 @@
let
closureInfo = pkgs.closureInfo {
rootPaths = [
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
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
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.hostPlatform.system}.nixosTestLib
self.legacyPackages.${pkgs.stdenv.hostPlatform.system}.nixosTestLib
];
testScript = ''
@@ -154,7 +154,7 @@
# Prepare test flake and Nix store
flake_dir = prepare_test_flake(
temp_dir,
"${self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks}",
"${self.checks.${pkgs.stdenv.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.hostPlatform.system}.clan-cli}",
f"${self.packages.${pkgs.stdenv.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.hostPlatform.system}.clan-cli}/bin/clan",
"${self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli}/bin/clan",
"machines",
"update",
"--debug",
@@ -270,7 +270,7 @@
# Run clan update command
subprocess.run([
"${self.packages.${pkgs.hostPlatform.system}.clan-cli-full}/bin/clan",
"${self.packages.${pkgs.stdenv.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.hostPlatform.system}.clan-cli-full}/bin/clan",
"${self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli-full}/bin/clan",
"machines",
"update",
"--debug",

View File

@@ -1,3 +1,6 @@
!!! 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,3 +1,6 @@
!!! 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 +1,83 @@
This a test README just to appease the eval warnings if we don't have one
!!! 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;
}
)
```

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
...
}:
let
testFlake = clanLib.clan {
testClan = clanLib.clan {
self = { };
# Point to the folder of the module
# TODO: make this optional
@@ -33,10 +33,20 @@ let
};
in
{
test_simple = {
config = testFlake.config;
/**
We highly advocate the usage of:
https://github.com/nix-community/nix-unit
expr = { };
expected = { };
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;
# 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!";
};
}

View File

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

View File

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

View File

@@ -39,6 +39,7 @@
...
}:
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 [ ]) (
@@ -46,7 +47,7 @@
)
);
# Merge client's searchDomains with all servers' searchDomains
searchDomains = lib.uniqueStrings (settings.certificate.searchDomains ++ allServerSearchDomains);
searchDomains = uniqueStrings (settings.certificate.searchDomains ++ allServerSearchDomains);
in
{
clan.core.vars.generators.openssh-ca = lib.mkIf (searchDomains != [ ]) {

View File

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

View File

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

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.importedModulesEvaluated.self-zerotier-redux.config;
# testFlake.clanInternals.inventoryClass.distributedServices.servicesEval.config.mappedServices.self-zerotier-redux.config;
in
{
test_simple = {
inherit testFlake;
expr =
testFlake.config.clan.clanInternals.inventoryClass.distributedServices.importedModulesEvaluated.self-wifi.config;
testFlake.config.clan.clanInternals.inventoryClass.distributedServices.servicesEval.config.mappedServices.self-wifi.config;
expected = 1;
# expr = {

View File

@@ -1,12 +1,9 @@
🚧🚧🚧 Experimental 🚧🚧🚧
Use at your own risk.
We are still refining its interfaces, instability and breakages are expected.
!!! 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

View File

@@ -140,6 +140,9 @@
pkgs,
...
}:
let
uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list);
in
{
imports = [
(import ./shared.nix {
@@ -156,7 +159,7 @@
config = {
systemd.services.zerotier-inventory-autoaccept =
let
machines = lib.uniqueStrings (
machines = uniqueStrings (
(lib.optionals (roles ? moon) (lib.attrNames roles.moon.machines))
++ (lib.optionals (roles ? controller) (lib.attrNames roles.controller.machines))
++ (lib.optionals (roles ? peer) (lib.attrNames roles.peer.machines))

View File

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

12
devFlake/flake.lock generated
View File

@@ -105,11 +105,11 @@
},
"nixpkgs-dev": {
"locked": {
"lastModified": 1761544814,
"narHash": "sha256-t5f0A+2MtSWTfA6hzMNiotpIMGLlSQF2JnK9m6nkzIY=",
"lastModified": 1762125068,
"narHash": "sha256-G2flpMLVSk/EJ/HJ3YABZA0FgbWJcp1VKj0/yX3Fz48=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e5aa45ed6c45058ec109658b2b7352a9a062cdf3",
"rev": "cd46903a30b60161802ffe01728949e3385b983e",
"type": "github"
},
"original": {
@@ -128,11 +128,11 @@
]
},
"locked": {
"lastModified": 1760652422,
"narHash": "sha256-C88Pgz38QIl9JxQceexqL2G7sw9vodHWx1Uaq+NRJrw=",
"lastModified": 1761730856,
"narHash": "sha256-t1i5p/vSWwueZSC0Z2BImxx3BjoUDNKyC2mk24krcMY=",
"owner": "NuschtOS",
"repo": "search",
"rev": "3ebeebe8b6a49dfb11f771f761e0310f7c48d726",
"rev": "e29de6db0cb3182e9aee75a3b1fd1919d995d85b",
"type": "github"
},
"original": {

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:

18
flake.lock generated
View File

@@ -31,11 +31,11 @@
]
},
"locked": {
"lastModified": 1760701190,
"narHash": "sha256-y7UhnWlER8r776JsySqsbTUh2Txf7K30smfHlqdaIQw=",
"lastModified": 1761899396,
"narHash": "sha256-XOpKBp6HLzzMCbzW50TEuXN35zN5WGQREC7n34DcNMM=",
"owner": "nix-community",
"repo": "disko",
"rev": "3a9450b26e69dcb6f8de6e2b07b3fc1c288d85f5",
"rev": "6f4cf5abbe318e4cd1e879506f6eeafd83f7b998",
"type": "github"
},
"original": {
@@ -51,11 +51,11 @@
]
},
"locked": {
"lastModified": 1760948891,
"narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=",
"lastModified": 1762040540,
"narHash": "sha256-z5PlZ47j50VNF3R+IMS9LmzI5fYRGY/Z5O5tol1c9I4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04",
"rev": "0010412d62a25d959151790968765a70c436598b",
"type": "github"
},
"original": {
@@ -71,11 +71,11 @@
]
},
"locked": {
"lastModified": 1761339987,
"narHash": "sha256-IUaawVwItZKi64IA6kF6wQCLCzpXbk2R46dHn8sHkig=",
"lastModified": 1762039661,
"narHash": "sha256-oM5BwAGE78IBLZn+AqxwH/saqwq3e926rNq5HmOulkc=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "7cd9aac79ee2924a85c211d21fafd394b06a38de",
"rev": "c3c8c9f2a5ed43175ac4dc030308756620e6e4e4",
"type": "github"
},
"original": {

View File

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

19
lib/clan/checkConfig.nix Normal file
View File

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

View File

@@ -33,20 +33,23 @@
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
lib.evalModules {
class = "clan";
specialArgs = {
inherit
self
;
inherit
nixpkgs
nix-darwin
;
};
modules = [
clan-core.modules.clan.default
m
];
}
clan-core.clanLib.checkConfig configuration.config.checks configuration

View File

@@ -16,6 +16,8 @@ lib.fix (
*/
callLib = file: args: import file ({ inherit lib clanLib; } // args);
checkConfig = clanLib.callLib ./clan/checkConfig.nix { };
evalService = clanLib.callLib ./evalService.nix { };
# ------------------------------------
# ClanLib functions

View File

@@ -53,7 +53,12 @@ in
};
};
}).clan
{ config.directory = rootPath; };
{
directory = rootPath;
self = {
inputs.nixpkgs.lib.version = "25.11";
};
};
in
{
inherit vclan;
@@ -94,7 +99,12 @@ in
};
};
}).clan
{ config.directory = rootPath; };
{
directory = rootPath;
self = {
inputs.nixpkgs.lib.version = "25.11";
};
};
in
{
inherit vclan;

View File

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

View File

@@ -28,19 +28,15 @@ in
elemType = submoduleWith {
class = "clan.service";
specialArgs = {
exports = config.exports;
directory = directory;
clanLib = specialArgs.clanLib;
exports = config.exports;
};
modules = [
(
{ name, ... }:
{
_module.args._ctx = [ name ];
_module.args.clanLib = specialArgs.clanLib;
_module.args.exports = config.exports;
_module.args.directory = directory;
}
)
./service-module.nix

View File

@@ -21,6 +21,7 @@ in
../../../flakeModules
../../../lib
../../../nixosModules/clanCore
../../../nixosModules/machineModules
../../../machines
../../../inventory.json
../../../modules

View File

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

View File

@@ -7,10 +7,14 @@
...
}:
let
inherit (lib) mkOption types uniqueStrings;
inherit (lib) mkOption types;
inherit (types) attrsWith submoduleWith;
errorContext = "Error context: ${lib.concatStringsSep "." _ctx}";
# TODO:
# Remove once this gets merged upstream; performs in O(n*log(n) instead of O(n^2))
# https://github.com/NixOS/nixpkgs/pull/355616/files
uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list);
/**
Merges the role- and machine-settings using the role interface

View File

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

View File

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

View File

@@ -58,39 +58,43 @@ let
sara = { };
};
res = callInventoryAdapter {
inherit modules machines;
instances."instance_foo" = {
module = {
name = "A";
input = "self";
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 = { };
};
roles.peer.machines.jon = {
settings.timeout = lib.mkForce "foo-peer-jon";
instances."instance_bar" = {
module = {
name = "A";
input = "self";
};
roles.peer.machines.jon = {
settings.timeout = "bar-peer-jon";
};
};
roles.peer = {
settings.timeout = "foo-peer";
# 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.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 = { };
};
};
@@ -105,9 +109,10 @@ in
{
# settings should evaluate
test_per_instance_arguments = {
inherit res;
expr = {
instanceName =
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName;
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName;
# settings are specific.
# Below we access:
@@ -115,11 +120,11 @@ in
# roles = peer
# machines = jon
settings =
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings;
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings;
machine =
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.machine;
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.machine;
roles =
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.roles;
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.roles;
};
expected = {
instanceName = "instance_foo";
@@ -160,9 +165,9 @@ in
# TODO: Cannot be tested like this anymore
test_per_instance_settings_vendoring = {
x = res.importedModulesEvaluated.self-A;
x = res.config._services.mappedServices.self-A;
expr =
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.vendoredSettings;
res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.vendoredSettings;
expected = {
timeout = "config.thing";
};

View File

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

View File

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

View File

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

View File

@@ -212,6 +212,36 @@ 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 { };

16
modules/clan/checks.nix Normal file
View File

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

View File

@@ -1,3 +1,14 @@
/**
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";
@@ -6,7 +17,9 @@
inherit (clan-core) clanLib;
};
imports = [
./top-level-interface.nix
./module.nix
./interface.nix
./distributed-services.nix
./checks.nix
];
}

View File

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

View File

@@ -3,12 +3,16 @@
lib,
clanModule,
clanLib,
clan-core,
}:
let
eval = lib.evalModules {
modules = [
clanModule
];
specialArgs = {
self = clan-core;
};
};
evalDocs = pkgs.nixosOptionsDoc {

View File

@@ -12,6 +12,7 @@ in
}:
let
jsonDocs = import ./eval-docs.nix {
clan-core = self;
inherit
pkgs
lib

View File

@@ -100,7 +100,7 @@ let
_: machine:
machine.extendModules {
modules = [
(lib.modules.importApply ../machineModules/overridePkgs.nix {
(lib.modules.importApply ../../nixosModules/machineModules/overridePkgs.nix {
pkgs = pkgsFor.${system};
})
];
@@ -167,6 +167,9 @@ in
{ ... }@args:
let
_class =
# _class was added in https://github.com/NixOS/nixpkgs/pull/395141
# Clan relies on it to determine which modules to load
# people need to use at least that version of nixpkgs
args._class or (throw ''
Your version of nixpkgs is incompatible with the latest clan.
Please update nixpkgs input to the latest nixos-unstable or nixpkgs-unstable.
@@ -176,7 +179,7 @@ in
in
{
imports = [
(lib.modules.importApply ../machineModules/forName.nix {
(lib.modules.importApply ../../nixosModules/machineModules/forName.nix {
inherit (config.inventory) meta;
inherit
name
@@ -216,8 +219,6 @@ in
inherit nixosConfigurations;
inherit darwinConfigurations;
exports = config.clanInternals.inventoryClass.distributedServices.servicesEval.config.exports;
clanInternals = {
inventoryClass =
let
@@ -251,21 +252,9 @@ in
exportsModule = config.exportsModule;
}
(
{ config, ... }:
{ ... }:
{
staticModules = clan-core.clan.modules;
distributedServices = clanLib.inventory.mapInstances {
inherit (config)
inventory
directory
flakeInputs
exportsModule
;
clanCoreModules = clan-core.clan.modules;
prefix = [ "distributedServices" ];
};
machines = config.distributedServices.allMachines;
}
)
];

View File

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

View File

@@ -67,9 +67,6 @@ in
type = types.raw;
};
distributedServices = mkOption {
type = types.raw;
};
inventory = mkOption {
type = types.raw;
};

View File

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

View File

@@ -30,5 +30,5 @@ let
in
{
# Note this might jump back and worth as kernel get added or removed.
boot.kernelPackages = lib.mkIf (lib.meta.availableOn pkgs.hostPlatform pkgs.zfs) latestKernelPackage;
boot.kernelPackages = lib.mkIf (lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.zfs) latestKernelPackage;
}

View File

@@ -3,6 +3,7 @@
directory,
meta,
}:
# The following is a nixos/darwin module
{
_class,
lib,

View File

@@ -245,7 +245,7 @@ class SecretStore(StoreBase):
output_dir / "activation" / generator.name / file.name
)
out_file.parent.mkdir(parents=True, exist_ok=True)
out_file.write_bytes(self.get(generator, file.name))
out_file.write_bytes(file.value)
if "partitioning" in phases:
for generator in vars_generators:
for file in generator.files:
@@ -254,7 +254,7 @@ class SecretStore(StoreBase):
output_dir / "partitioning" / generator.name / file.name
)
out_file.parent.mkdir(parents=True, exist_ok=True)
out_file.write_bytes(self.get(generator, file.name))
out_file.write_bytes(file.value)
hash_data = self.generate_hash(machine)
if hash_data:

View File

@@ -246,7 +246,7 @@ class SecretStore(StoreBase):
)
# chmod after in case it doesn't have u+w
target_path.touch(mode=0o600)
target_path.write_bytes(self.get(generator, file.name))
target_path.write_bytes(file.value)
target_path.chmod(file.mode)
if "partitioning" in phases:
@@ -260,7 +260,7 @@ class SecretStore(StoreBase):
)
# chmod after in case it doesn't have u+w
target_path.touch(mode=0o600)
target_path.write_bytes(self.get(generator, file.name))
target_path.write_bytes(file.value)
target_path.chmod(file.mode)
@override

View File

@@ -211,7 +211,7 @@ class ClanSelectError(ClanError):
def __str__(self) -> str:
if self.description:
return f"{self.msg} Reason: {self.description}"
return f"{self.msg} Reason: {self.description}. Use flag '--debug' to see full nix trace."
return self.msg
def __repr__(self) -> str:

View File

@@ -64,6 +64,9 @@
'';
in
{
legacyPackages = {
inherit jsonDocs clanModulesViaService;
};
packages = {
inherit module-docs;
};

View File

@@ -11,151 +11,10 @@
...
}:
let
inherit (lib)
mapAttrsToList
mapAttrs
mkOption
types
splitString
stringLength
substring
;
inherit (self) clanLib;
serviceModules = self.clan.modules;
baseHref = "/option-search/";
getRoles =
module:
(clanLib.evalService {
modules = [ module ];
prefix = [ ];
}).config.roles;
getManifest =
module:
(clanLib.evalService {
modules = [ module ];
prefix = [ ];
}).config.manifest;
settingsModules = module: mapAttrs (_roleName: roleConfig: roleConfig.interface) (getRoles module);
# Map each letter to its capitalized version
capitalizeChar =
char:
{
a = "A";
b = "B";
c = "C";
d = "D";
e = "E";
f = "F";
g = "G";
h = "H";
i = "I";
j = "J";
k = "K";
l = "L";
m = "M";
n = "N";
o = "O";
p = "P";
q = "Q";
r = "R";
s = "S";
t = "T";
u = "U";
v = "V";
w = "W";
x = "X";
y = "Y";
z = "Z";
}
.${char};
title =
name:
let
# split by -
parts = splitString "-" name;
# capitalize first letter of each part
capitalize = part: (capitalizeChar (substring 0 1 part)) + substring 1 (stringLength part) part;
capitalizedParts = map capitalize parts;
in
builtins.concatStringsSep " " capitalizedParts;
fakeInstanceOptions =
name: module:
let
manifest = getManifest module;
description = ''
# ${title name} (Clan Service)
**${manifest.description}**
${lib.optionalString (manifest ? readme) manifest.readme}
${
if manifest.categories != [ ] then
"Categories: " + builtins.concatStringsSep ", " manifest.categories
else
"No categories defined"
}
'';
in
{
options = {
instances.${name} = lib.mkOption {
inherit description;
type = types.submodule {
options.roles = mapAttrs (
roleName: roleSettingsModule:
mkOption {
type = types.submodule {
_file = "docs flake-module";
imports = [
{ _module.args = { inherit clanLib; }; }
(import ../../modules/inventoryClass/role.nix {
nestedSettingsOption = mkOption {
type = types.raw;
description = ''
See [instances.${name}.roles.${roleName}.settings](${baseHref}?option_scope=0&option=inventory.instances.${name}.roles.${roleName}.settings)
'';
};
settingsOption = mkOption {
type = types.submoduleWith {
modules = [ roleSettingsModule ];
};
};
})
];
};
}
) (settingsModules module);
};
};
};
};
docModules = [
{
inherit self;
}
self.modules.clan.default
{
options.inventory = lib.mkOption {
type = types.submoduleWith {
modules = [
{ noInstanceOptions = true; }
]
++ mapAttrsToList fakeInstanceOptions serviceModules;
};
};
}
];
baseModule =
# Module
@@ -208,12 +67,6 @@
title = "Clan Options";
# scopes = mapAttrsToList mkScope serviceModules;
scopes = [
{
inherit baseHref;
name = "Flake Options (clan.nix file)";
modules = docModules;
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
}
{
name = "Machine Options (clan.core NixOS options)";
optionsJSON = "${coreOptions}/share/doc/nixos/options.json";