Compare commits

..

70 Commits

Author SHA1 Message Date
Qubasa
864b131010 clan-app: Move middleware to it's own folder 2025-09-16 16:06:16 +02:00
Qubasa
ee0f111fc9 clan-app: change ApiBridge ABC class to Protocol 2025-09-16 11:48:59 +02:00
Mic92
1b193123b2 Merge pull request 'docs: Add missing space' (#5160) from hgl/clan-core:doc into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5160
Reviewed-by: Kenji Berthold <aks.kenji@protonmail.com>
2025-09-16 08:34:17 +00:00
Glen Huang
81126da053 docs: Add missing space 2025-09-16 15:07:32 +08:00
clan-bot
67795730a2 Merge pull request 'Update nixpkgs-dev in devFlake' (#5158) from update-devFlake-nixpkgs-dev into main 2025-09-16 00:07:39 +00:00
clan-bot
e6797c6f20 Update nixpkgs-dev in devFlake 2025-09-16 00:01:36 +00:00
clan-bot
93280a9f98 Merge pull request 'Update data-mesher' (#5150) from update-data-mesher into main 2025-09-15 05:09:00 +00:00
clan-bot
d89ddfabec Update data-mesher 2025-09-15 05:00:39 +00:00
clan-bot
e2946615f0 Merge pull request 'Update nuschtos in devFlake' (#5149) from update-devFlake-nuschtos into main 2025-09-15 00:17:45 +00:00
clan-bot
bce9f9a747 Update nuschtos in devFlake 2025-09-15 00:01:49 +00:00
clan-bot
b494bdee21 Merge pull request 'Update nixpkgs-dev in devFlake' (#5148) from update-devFlake-nixpkgs-dev into main 2025-09-14 10:09:05 +00:00
clan-bot
13632ff659 Update nixpkgs-dev in devFlake 2025-09-14 10:01:35 +00:00
clan-bot
90ad8054d0 Merge pull request 'Update nixpkgs-dev in devFlake' (#5147) from update-devFlake-nixpkgs-dev into main 2025-09-13 15:10:47 +00:00
clan-bot
716d4a17f5 Update nixpkgs-dev in devFlake 2025-09-13 15:01:35 +00:00
clan-bot
dcd1273f3f Merge pull request 'Update nixpkgs-dev in devFlake' (#5145) from update-devFlake-nixpkgs-dev into main 2025-09-12 15:10:09 +00:00
clan-bot
899c9eed0e Update nixpkgs-dev in devFlake 2025-09-12 15:01:35 +00:00
Luis Hebendanz
af85041e5e Merge pull request 'docs: Move age plugins to vars/sops backend group. Improve age plugin documentation' (#5144) from Qubasa/clan-core:improve_vars_docs2 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5144
2025-09-12 12:20:28 +00:00
Qubasa
6a96ce8679 docs: Move age plugins to vars/sops backend group. Improve age plugin documentation 2025-09-12 14:13:49 +02:00
Luis Hebendanz
60195f9614 Merge pull request 'docs: fix multiple format errors, improve readability of vars' (#5142) from Qubasa/clan-core:improve_vars_docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5142
2025-09-12 10:46:15 +00:00
Qubasa
447b0bf8ac docs: fix uml errors 2025-09-12 12:42:41 +02:00
clan-bot
fd162f6fc8 Merge pull request 'Update nuschtos in devFlake' (#5143) from update-devFlake-nuschtos into main 2025-09-12 00:10:15 +00:00
clan-bot
e4bf6523ad Update nuschtos in devFlake 2025-09-12 00:01:43 +00:00
Qubasa
5312799784 docs: fix multiple format errors, improve readability of vars 2025-09-11 19:45:16 +02:00
Luis Hebendanz
7d265a6156 Merge pull request 'Fix link in README and typo in zerotier service' (#5137) from ErinvanderVeen/clan-core:main into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5137
2025-09-11 13:02:07 +00:00
Luis Hebendanz
f8428947ca Merge pull request 'fix: (re)add missing tofu --host-key-check option' (#5140) from friedow/clan-core:fix/missing-host-key-check into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5140
2025-09-11 13:00:26 +00:00
Christian Friedow
196d7c95c0 fix: add missing --host-key-check option 2025-09-11 14:30:48 +02:00
clan-bot
6be40f6f79 Merge pull request 'Update nixpkgs-dev in devFlake' (#5139) from update-devFlake-nixpkgs-dev into main 2025-09-11 10:09:35 +00:00
clan-bot
3aefabd818 Update nixpkgs-dev in devFlake 2025-09-11 10:01:38 +00:00
clan-bot
230e7e6769 Merge pull request 'Update nixpkgs-dev in devFlake' (#5138) from update-devFlake-nixpkgs-dev into main 2025-09-11 05:10:22 +00:00
clan-bot
46bae67645 Update nixpkgs-dev in devFlake 2025-09-11 05:01:36 +00:00
Erin van der Veen
890e8c7003 chore(zerotier): fix stableEndpoint example 2025-09-10 20:48:50 +02:00
Erin van der Veen
0d3a62321a chore(readme): fix contributing link 2025-09-10 20:47:22 +02:00
clan-bot
ef82e07293 Merge pull request 'Update nixpkgs-dev in devFlake' (#5136) from update-devFlake-nixpkgs-dev into main 2025-09-10 15:13:31 +00:00
clan-bot
7c8c3811f4 Merge pull request 'Update disko' (#5134) from update-disko into main 2025-09-10 15:06:42 +00:00
clan-bot
9b2c97a855 Update nixpkgs-dev in devFlake 2025-09-10 15:01:50 +00:00
clan-bot
785f789628 Update disko 2025-09-10 15:00:51 +00:00
clan-bot
a034fefb51 Merge pull request 'Update sops-nix' (#5130) from update-sops-nix into main 2025-09-10 14:04:53 +00:00
clan-bot
bcd846fe5e Update sops-nix 2025-09-10 10:01:13 +00:00
clan-bot
a6214f431d Merge pull request 'Update nixpkgs-dev in devFlake' (#5131) from update-devFlake-nixpkgs-dev into main 2025-09-10 00:12:29 +00:00
clan-bot
b8890f6732 Update nixpkgs-dev in devFlake 2025-09-10 00:01:36 +00:00
Luis Hebendanz
370b4f535d Merge pull request 'vars: docs' (#4119) from vars-docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4119
2025-09-09 20:59:52 +00:00
Qubasa
ef66c9b5be docs: vars ai fixups 2025-09-09 22:53:15 +02:00
Jörg Thalheim
79d44f7c30 vars: docs
re-add vars-backend.md

re-add vars-backend.md
2025-09-09 22:12:07 +02:00
clan-bot
e72e100965 Merge pull request 'Update nixpkgs-dev in devFlake' (#5129) from update-devFlake-nixpkgs-dev into main 2025-09-09 20:10:13 +00:00
clan-bot
180e2a601c Merge pull request 'Update nix-darwin' (#5128) from update-nix-darwin into main 2025-09-09 20:06:57 +00:00
clan-bot
90d265089b Update nixpkgs-dev in devFlake 2025-09-09 20:01:39 +00:00
clan-bot
a0fa52fded Update nix-darwin 2025-09-09 20:00:41 +00:00
Luis Hebendanz
af4e9e784b Merge pull request 'docs: Add secure boot info to disk encryption guide' (#5127) from docs_fix into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5127
2025-09-09 17:46:09 +00:00
Qubasa
cb162a53b8 docs: Add secure boot info to disk encryption guide
fix wrong link
2025-09-09 19:41:59 +02:00
Luis Hebendanz
16e506ea1a Merge pull request 'doc: use clan-core as inputs name' (#5126) from Mayeu-doc/clan-core-input2 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5126
2025-09-09 17:29:06 +00:00
Mayeu
11ec94c17f doc: use clan-core as inputs name 2025-09-09 19:18:35 +02:00
clan-bot
8468b1ebaf Merge pull request 'Update nixpkgs-dev in devFlake' (#5123) from update-devFlake-nixpkgs-dev into main 2025-09-09 15:08:55 +00:00
clan-bot
ec83130fa4 Update nixpkgs-dev in devFlake 2025-09-09 15:01:38 +00:00
Luis Hebendanz
c1e41f8fd9 Merge pull request 'docs: update concepts/inventory to match new option structure' (#5121) from friedow/clan-core:docs/concept-inventory into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5121
2025-09-09 14:44:30 +00:00
Christian Friedow
3630e778ad docs: update concepts/inventory to match new option structure 2025-09-09 15:35:23 +02:00
Luis Hebendanz
916186c465 Merge pull request 'webview: update to support displaying app icon on macOS' (#5120) from Qubasa/clan-core:demo_fix into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5120
2025-09-09 10:12:56 +00:00
clan-bot
25e733b8d7 Merge pull request 'Update nixpkgs-dev in devFlake' (#5112) from update-devFlake-nixpkgs-dev into main 2025-09-09 10:11:14 +00:00
Luis Hebendanz
2599998b17 Merge pull request 'add apply "machine" as an alias to clan machines create' (#5005) from apply into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5005
2025-09-09 10:08:58 +00:00
clan-bot
56649b7fe2 Merge pull request 'Update data-mesher' (#5111) from update-data-mesher into main 2025-09-09 10:07:57 +00:00
Luis Hebendanz
fc85622e01 Merge pull request 'ui/imports: fix asset imports' (#5119) from fix-imports into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5119
2025-09-09 10:02:29 +00:00
clan-bot
c499c563bb Update nixpkgs-dev in devFlake 2025-09-09 10:02:19 +00:00
clan-bot
b255ba0367 Update data-mesher 2025-09-09 10:01:18 +00:00
Luis Hebendanz
493adebd7c Merge pull request 'docs: Fix minor typo' (#5110) from vorburger/clan-core:docs-typo into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5110
2025-09-09 09:59:21 +00:00
Qubasa
cac2866356 webview: update to support displaying app icon on macOS 2025-09-09 11:54:18 +02:00
Qubasa
981f6052ad zerotierone: Add restartUnit to vars generators 2025-09-09 09:49:38 +00:00
Michael Vorburger
6e888c38fa docs: Fix minor typo 2025-09-08 01:03:55 +02:00
clan-bot
e953f807de Merge pull request 'Update disko' (#5108) from update-disko into main 2025-09-07 15:07:42 +00:00
clan-bot
c2534e9a42 Update disko 2025-09-07 15:00:37 +00:00
Johannes Kirschbauer
42bbd7c5fd ui/imports: fix asset imports 2025-09-04 19:35:06 +02:00
Jörg Thalheim
758eacd27e add apply "machine" as an alias to clan machines create
I was a bit confused that I was able to list templates but not
apply them. Turns out that "apply" only supported disk templates
2025-08-27 13:39:39 +00:00
60 changed files with 1296 additions and 423 deletions

View File

@@ -30,7 +30,7 @@ In the Clan ecosystem, security is paramount. Learn how to handle secrets effect
The Clan project thrives on community contributions. We welcome everyone to contribute and collaborate:
- **Contribution Guidelines**: Make a meaningful impact by following the steps in [contributing](https://docs.clan.lol/contributing/contributing/)<!-- [contributing.md](docs/CONTRIBUTING.md) -->.
- **Contribution Guidelines**: Make a meaningful impact by following the steps in [contributing](https://docs.clan.lol/guides/contributing/CONTRIBUTING/)<!-- [contributing.md](docs/CONTRIBUTING.md) -->.
## Join the revolution

View File

@@ -5,7 +5,7 @@ inventory.instances = {
borgbackup = {
module = {
name = "borgbackup";
input = "clan";
input = "clan-core";
};
roles.client.machines."jon".settings = {
destinations."storagebox" = {

View File

@@ -8,7 +8,7 @@ The service consists of two roles:
- A `server` role: This is the DNS-server that will be queried when trying to
resolve clan-internal services. It defines the top-level domain.
- A `default` role: This does two things. First, it sets up the nameservers so
thatclan-internal queries are resolved via the `server` machine, while
that clan-internal queries are resolved via the `server` machine, while
external queries are resolved as normal via DHCP. Second, it allows exposing
services (see example below).

View File

@@ -7,7 +7,7 @@ inventory.instances = {
clan-cache = {
module = {
name = "trusted-nix-caches";
input = "clan";
input = "clan-core";
};
roles.default.machines.draper = { };
};

View File

@@ -8,7 +8,7 @@
user-alice = {
module = {
name = "users";
input = "clan";
input = "clan-core";
};
roles.default.tags.all = { };
roles.default.settings = {
@@ -35,7 +35,7 @@
user-bob = {
module = {
name = "users";
input = "clan";
input = "clan-core";
};
roles.default.machines.bobs-laptop = { };
roles.default.settings.user = "bob";

View File

@@ -1,108 +0,0 @@
# Example clan service. See https://docs.clan.lol/guides/services/community/
# for more details
# The test for this module in ./tests/vm/default.nix shows an example of how
# the service is used.
{ packages }:
{ ... }:
{
_class = "clan.service";
manifest.name = "clan-core/yggdrasil";
manifest.description = "Yggdrasil VPN";
roles.default = {
# interface =
# { lib, ... }:
# {
# # Here we define the settings for this role. They will be accessible
# # via `roles.morning.settings` in the role
#
# options.greeting = lib.mkOption {
# type = lib.types.str;
# default = "Good morning";
# description = "The greeting to use";
# };
# };
# Maps over all instances and produces one result per instance.
perInstance =
{
# Role settings for this machine/instance
settings,
# The name of this instance of the service
instanceName,
# The current machine
machine,
# All roles of this service, with their assigned machines
roles,
...
}:
{
# Analog to 'perSystem' of flake-parts.
# For every instance of this service we will add a nixosModule to a morning-machine
nixosModule =
{ config, pkgs, ... }:
{
clan.core.vars.generators.yggdrasil = {
files.privateKey = { };
runtimeInputs = with pkgs; [
yggdrasil
jq
];
script = ''
yggdrasil -genconf -json | jq 'to_entries|map(select(.key|endswith("Key")))|from_entries' > $out/privateKey
'';
};
services.yggdrasil = {
persistentKeys = true;
enable = true;
};
systemd.services.yggdrasil.serviceConfig.BindReadOnlyPaths = [
"${config.clan.core.vars.generators.yggdrasil.files.privateKey.path}:/var/lib/yggdrasil/keys.json"
];
# Interaction examples what you could do here:
# - Get some settings of this machine
# settings.ipRanges
#
# - Get all evening names:
# allEveningNames = lib.attrNames roles.evening.machines
#
# - Get all roles of the machine:
# machine.roles
#
# - Get the settings that where applied to a specific evening machine:
# roles.evening.machines.peer1.settings
# environment.etc.hello.text = "${settings.greeting} World!";
};
};
};
# This part gets applied to all machines, regardless of their role.
# perMachine =
# { machine, ... }:
# {
# nixosModule =
# { pkgs, ... }:
# {
# environment.systemPackages = [
# (pkgs.writeShellScriptBin "greet-world" ''
# #!${pkgs.bash}/bin/bash
# set -euo pipefail
#
# cat /etc/hello
# echo " I'm ${machine.name}"
# '')
# ];
# };
# };
}

View File

@@ -1,25 +0,0 @@
{
self,
inputs,
lib,
...
}:
let
module = lib.modules.importApply ./default.nix {
inherit (self) packages;
};
in
{
clan.modules = {
yggdrasil = module;
};
perSystem =
{ ... }:
{
clan.nixosTests.yggdrasil = {
imports = [ ./tests/vm/default.nix ];
clan.modules.yggdrasil = module;
};
};
}

View File

@@ -1,41 +0,0 @@
{
name = "yggdrasil";
clan = {
directory = ./.;
inventory = {
machines.peer1 = { };
# machines.peer2 = { };
instances."yggdrasil" = {
module.name = "yggdrasil";
module.input = "self";
# Assign the roles to the two machines
roles.default.machines.peer1 = { };
# roles.evening.machines.peer2 = {
# # Set roles settings for the peers, where we want to differ from
# # the role defaults
# settings = {
# greeting = "Good night";
# };
# };
};
};
};
testScript =
{ ... }:
''
start_all()
# value = peer1.succeed("greet-world")
# assert value.strip() == "Good morning World! I'm peer1", value
#
# value = peer2.succeed("greet-world")
# assert value.strip() == "Good night World! I'm peer2", value
'';
}

View File

@@ -1,4 +0,0 @@
{
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"type": "age"
}

View File

@@ -5,7 +5,7 @@ inventory.instances = {
zerotier = {
module = {
name = "zerotier";
input = "clan";
input = "clan-core";
};
roles.peer.tags.all = { };
roles.controller.machines.jon = { };
@@ -18,7 +18,6 @@ All machines will be peers and connected to the zerotier network.
Jon is the controller machine, which will will accept other machines into the network.
Sara is a moon and sets the `stableEndpoint` setting with a publicly reachable IP, the moon is optional.
## Overview
This guide explains how to set up and manage a [ZeroTier VPN](https://zerotier.com) for a clan network. Each VPN requires a single controller and can support multiple peers and optional moons for better connectivity.

View File

@@ -45,7 +45,7 @@
It will be reachable under the given stable endpoints.
'';
example = ''
[ 1.2.3.4" "10.0.0.3/9993" "2001:abcd:abcd::3/9993" ]
[ "1.2.3.4" "10.0.0.3/9993" "2001:abcd:abcd::3/9993" ]
'';
};

12
devFlake/flake.lock generated
View File

@@ -84,11 +84,11 @@
},
"nixpkgs-dev": {
"locked": {
"lastModified": 1757195359,
"narHash": "sha256-Uf/d5NGvq+Q6ct+n5xRr76N1ZGV0vkfsJ6iVTciPkY0=",
"lastModified": 1757924820,
"narHash": "sha256-to/hwbY9/jsRaejPa5oJmPUFZsJfFCB3WReKhD0+/+E=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f4cefbe0160ba99567be386a043824549ccd5cb7",
"rev": "aa54acd34af0e86f49d55ea52823031e2da399df",
"type": "github"
},
"original": {
@@ -107,11 +107,11 @@
]
},
"locked": {
"lastModified": 1756738487,
"narHash": "sha256-8QX7Ab5CcICp7zktL47VQVS+QeaU4YDNAjzty7l7TQE=",
"lastModified": 1757885130,
"narHash": "sha256-56CMb5W/pgjKLh0bx2ekhn5rde/YmgR63HAqrY9/BCw=",
"owner": "NuschtOS",
"repo": "search",
"rev": "5feeaeefb571e6ca2700888b944f436f7c05149b",
"rev": "fae3c59a646e00c4b1d359c50b27458a0713d2fd",
"type": "github"
},
"original": {

View File

@@ -61,9 +61,16 @@ nav:
- Continuous Integration: guides/getting-started/flake-check.md
- Convert Existing NixOS Config: guides/getting-started/convert-flake.md
- ClanServices: guides/clanServices.md
- Vars:
- Overview: guides/vars/vars-overview.md
- Getting Started: guides/vars/vars-backend.md
- Concepts: guides/vars/vars-concepts.md
- Sops Backend:
- Yubikeys & Age Plugins: guides/vars/sops/age-plugins.md
- Advanced Examples: guides/vars/vars-advanced-examples.md
- Troubleshooting: guides/vars/vars-troubleshooting.md
- Backup & Restore: guides/backups.md
- Disk Encryption: guides/disk-encryption.md
- Age Plugins: guides/age-plugins.md
- Secrets management: guides/secrets.md
- Networking: guides/networking.md
- Zerotier VPN: guides/mesh-vpn.md
@@ -83,7 +90,6 @@ nav:
- Disk id: guides/migrations/disk-id.md
- Concepts:
- Inventory: concepts/inventory.md
- Generators: concepts/generators.md
- Autoincludes: concepts/autoincludes.md
- Templates: concepts/templates.md
- Reference:
@@ -218,4 +224,4 @@ plugins:
- redoc-tag
- redirects:
redirect_maps:
guides/getting-started/secrets.md: concepts/generators.md
guides/getting-started/secrets.md: guides/vars/vars-overview.md

View File

@@ -205,25 +205,31 @@
# };
packages = {
docs-options = privateInputs.nuschtos.packages.${pkgs.stdenv.hostPlatform.system}.mkMultiSearch {
inherit baseHref;
title = "Clan Options";
# scopes = mapAttrsToList mkScope serviceModules;
scopes = [
{
docs-options =
if privateInputs ? nuschtos then
privateInputs.nuschtos.packages.${pkgs.stdenv.hostPlatform.system}.mkMultiSearch {
inherit baseHref;
name = "Flake Options (clan.nix file)";
modules = docModules;
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
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";
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";
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
}
];
};
else
pkgs.stdenv.mkDerivation {
name = "empty";
buildCommand = "echo 'This is an empty derivation' > $out";
};
};
};
}

View File

@@ -21,7 +21,7 @@ The following tutorial will walk through setting up a Backup service where the t
## Services
The inventory defines `services`. Membership of `machines` is defined via `roles` exclusively.
The inventory defines `instances` of clan services. Membership of `machines` is defined via `roles` exclusively.
See each [modules documentation](../reference/clanServices/index.md) for its available roles.
@@ -31,9 +31,8 @@ A service can be added to one or multiple machines via `Roles`. Clan's `Role` in
Each service can still be customized and configured according to the modules options.
- Per instance configuration via `services.<serviceName>.<instanceName>.config`
- Per role configuration via `services.<serviceName>.<instanceName>.roles.<roleName>.config`
- Per machine configuration via `services.<serviceName>.<instanceName>.machines.<machineName>.config`
- Per role configuration via `inventory.instances.<instanceName>.roles.<roleName>.settings`
- Per machine configuration via `inventory.instances.<instanceName>.roles.<roleName>.machines.<machineName>.settings`
### Setting up the Backup Service
@@ -44,16 +43,17 @@ Each service can still be customized and configured according to the modules opt
See also: [Multiple Service Instances](#multiple-service-instances)
```{.nix hl_lines="6-7"}
clan-core.lib.clan {
inventory = {
services = {
borgbackup.instance_1 = {
# Machines can be added here.
roles.client.machines = [ "jon" ];
roles.server.machines = [ "backup_server" ];
};
```{.nix hl_lines="9-10"}
{
inventory.instances.instance_1 = {
module = {
name = "borgbackup";
input = "clan-core";
};
# Machines can be added here.
roles.client.machines."jon" {};
roles.server.machines."backup_server" = {};
};
}
```
@@ -66,8 +66,8 @@ It is possible to add services to multiple machines via tags as shown
!!! Example "Tags Example"
```{.nix hl_lines="5 8 14"}
clan-core.lib.clan {
```{.nix hl_lines="5 8 18"}
{
inventory = {
machines = {
"jon" = {
@@ -76,13 +76,16 @@ It is possible to add services to multiple machines via tags as shown
"sara" = {
tags = [ "backup" ];
};
# ...
};
services = {
borgbackup.instance_1 = {
roles.client.tags = [ "backup" ];
roles.server.machines = [ "backup_server" ];
instances.instance_1 = {
module = {
name = "borgbackup";
input = "clan-core";
};
roles.client.tags = [ "backup" ];
roles.server.machines."backup_server" = {};
};
};
}
@@ -98,22 +101,34 @@ It is possible to add services to multiple machines via tags as shown
In this example `backup_server` has role `client` and `server` in different instances.
```{.nix hl_lines="11 14"}
clan-core.lib.clan {
```{.nix hl_lines="17 26"}
{
inventory = {
machines = {
"jon" = {};
"backup_server" = {};
"backup_backup_server" = {}
"backup_backup_server" = {};
};
services = {
borgbackup.instance_1 = {
roles.client.machines = [ "jon" ];
roles.server.machines = [ "backup_server" ];
instances = {
instance_1 = {
module = {
name = "borgbackup";
input = "clan-core";
};
roles.client.machines."jon" = {};
roles.server.machines."backup_server" = {};
};
borgbackup.instance_2 = {
roles.client.machines = [ "backup_server" ];
roles.server.machines = [ "backup_backup_server" ];
instance_2 = {
module = {
name = "borgbackup";
input = "clan-core";
};
roles.client.machines."backup_server" = {};
roles.server.machines."backup_backup_server" = {};
};
};
};

View File

@@ -1,59 +0,0 @@
## Using Age Plugins
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
You must **precede your secret key with a comment that contains its corresponding recipient**.
This is usually output as part of the generation process
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
Here is an example:
```title="~/.config/sops/age/keys.txt"
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
```
!!! note
The comment that precedes the plugin secret key need only contain the recipient.
Any other text is ignored.
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
just `# age1zdy....`
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
are loaded when using Clan:
```nix title="flake.nix"
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs =
{ self, clan-core, ... }:
let
# Sometimes this attribute set is defined in clan.nix
clan = clan-core.lib.clan {
inherit self;
meta.name = "myclan";
# Add Yubikey and FIDO2 HMAC plugins
# Note: the plugins listed here must be available in nixpkgs.
secrets.age.plugins = [
"age-plugin-yubikey"
"age-plugin-fido2-hmac"
];
machines = {
# elided for brevity
};
};
in
{
inherit (clan) nixosConfigurations nixosModules clanInternals;
# elided for brevity
};
}
```

View File

@@ -1,4 +1,3 @@
This guide explains how to set up and manage
[BorgBackup](https://borgbackup.readthedocs.io/) for secure, efficient backups
in a clan network. BorgBackup provides:
@@ -18,7 +17,7 @@ inventory.instances = {
borgbackup = {
module = {
name = "borgbackup";
input = "clan";
input = "clan-core";
};
roles.client.machines."jon".settings = {
destinations."storagebox" = {
@@ -177,7 +176,7 @@ storagebox::username@username.your-storagebox.de:/./borgbackup::jon-storagebox-2
### Restoring backups
For restoring a backup you have two options.
For restoring a backup you have two options.
#### Full restoration
@@ -194,6 +193,3 @@ To restore only a specific service (e.g., `linkding`):
```bash
clan backups restore --service linkding jon borgbackup storagebox::u444061@u444061.your-storagebox.de:/./borgbackup::jon-storagebox-2025-07-24T06:02:35
```

View File

@@ -4,6 +4,8 @@ This guide provides an example setup for a single-disk ZFS system with native en
!!! Warning
This configuration only applies to `systemd-boot` enabled systems and **requires** UEFI booting.
!!! Info "Secure Boot"
This guide is compatible with systems that have [secure boot disabled](../guides/secure-boot.md). If you encounter boot issues, check if secure boot needs to be disabled in your UEFI settings.
Replace the highlighted lines with your own disk-id.
You can find our your disk-id by executing:

View File

@@ -7,7 +7,7 @@ This guide explains how to manage macOS machines using Clan.
Currently, Clan supports the following features for macOS:
- `clan machines update` for existing [nix-darwin](https://github.com/nix-darwin/nix-darwin) installations
- Support for [vars](../concepts/generators.md)
- Support for [vars](../guides/vars/vars-overview.md)
## Add Your Machine to Your Clan Flake

View File

@@ -3,7 +3,7 @@
For a high level overview about `vars` see our [blog post](https://clan.lol/blog/vars/).
This guide will help you migrate your modules that still use our [`facts`](../../guides/secrets.md) backend
to the [`vars`](../../concepts/generators.md) backend.
to the [`vars`](../../guides/vars/vars-overview.md) backend.
The `vars` [module](../../reference/clan.core/vars.md) and the clan [command](../../reference/cli/vars.md) work in tandem, they should ideally be kept in sync.

View File

@@ -1,5 +1,5 @@
This article provides an overview over the underlying secrets system which is used by [Vars](../concepts/generators.md).
Under most circumstances you should use [Vars](../concepts/generators.md) directly instead.
This article provides an overview over the underlying secrets system which is used by [Vars](../guides/vars/vars-overview.md).
Under most circumstances you should use [Vars](../guides/vars/vars-overview.md) directly instead.
Consider using `clan secrets` only for managing admin users and groups, as well as a debugging tool.
@@ -292,15 +292,14 @@ The following diagrams illustrates how a user can provide a secret (i.e. a Passw
```plantuml
@startuml
!include C4_Container.puml
Person(user, "User", "Someone who manages secrets")
ContainerDb(secret, "Secret")
Container(machine, "Machine", "A Machine. i.e. Needs the Secret for a given Service." )
actor "User" as user
database "Secret" as secret
rectangle "Machine" as machine
Rel_R(user, secret, "Encrypt", "", "Pubkeys: User, Machine")
Rel_L(secret, user, "Decrypt", "", "user privkey")
Rel_R(secret, machine, "Decrypt", "", "machine privkey" )
user -right-> secret : Encrypt\n(Pubkeys: User, Machine)
secret -left-> user : Decrypt\n(user privkey)
secret -right-> machine : Decrypt\n(machine privkey)
@enduml
```
@@ -316,19 +315,18 @@ Common use cases:
```plantuml
@startuml
!include C4_Container.puml
System_Boundary(c1, "Group") {
Person(user1, "User A", "has access")
Person(user2, "User B", "has access")
rectangle "Group" {
actor "User A" as user1
actor "User B" as user2
}
ContainerDb(secret, "Secret")
Container(machine, "Machine", "A Machine. i.e. Needs the Secret for a given Service." )
Rel_R(c1, secret, "Encrypt", "", "Pubkeys: User A, User B, Machine")
Rel_R(secret, machine, "Decrypt", "", "machine privkey" )
database "Secret" as secret
rectangle "Machine" as machine
user1 -right-> secret : Encrypt
user2 -right-> secret : (Pubkeys: User A, User B, Machine)
secret -right-> machine : Decrypt\n(machine privkey)
@enduml
```
@@ -347,19 +345,17 @@ Common use cases:
```plantuml
@startuml
!include C4_Container.puml
!include C4_Deployment.puml
Person(user, "User", "Someone who manages secrets")
ContainerDb(secret, "Secret")
System_Boundary(c1, "Group") {
Container(machine1, "Machine A", "Both machines need the same secret" )
Container(machine2, "Machine B", "Both machines need the same secret" )
actor "User" as user
database "Secret" as secret
rectangle "Group" {
rectangle "Machine A" as machine1
rectangle "Machine B" as machine2
}
Rel_R(user, secret, "Encrypt", "", "Pubkeys: machine A, machine B, User")
Rel(secret, c1, "Decrypt", "", "Both machine A or B can decrypt using their private key" )
user -right-> secret : Encrypt\n(Pubkeys: machine A, machine B, User)
secret -down-> machine1 : Decrypt
secret -down-> machine2 : (Both machines can decrypt\nusing their private key)
@enduml
```

View File

@@ -0,0 +1,85 @@
# Using Age Plugins with Clan Vars
This guide explains how to set up YubiKey and other plugins for `clan vars` secrets.
By default the `clan vars` subcommand uses the `age` encryption tool, which supports various plugins.
---
## Supported Age Plugins
Below is a [list of popular `age` plugins](https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins) you can use with Clan. (Last updated: **September 12, 2025**)
- ⭐️ [**age-plugin-yubikey**](https://github.com/str4d/age-plugin-yubikey): YubiKey (and other PIV tokens) plugin.
- [**age-plugin-se**](https://github.com/remko/age-plugin-se): Apple Secure Enclave plugin.
- 🧪 [**age-plugin-tpm**](https://github.com/Foxboron/age-plugin-tpm): TPM 2.0 plugin.
- 🧪 [**age-plugin-tkey**](https://github.com/quite/age-plugin-tkey): Tillitis TKey plugin.
[**age-plugin-trezor**](https://github.com/romanz/trezor-agent/blob/master/doc/README-age.md): Hardware wallet plugin (TREZOR, Ledger, etc.).
- 🧪 [**age-plugin-sntrup761x25519**](https://github.com/keisentraut/age-plugin-sntrup761x25519): Post-quantum hybrid plugin (NTRU Prime + X25519).
- 🧪 [**age-plugin-fido**](https://github.com/riastradh/age-plugin-fido): Prototype symmetric encryption plugin for FIDO2 keys.
- 🧪 [**age-plugin-fido2-hmac**](https://github.com/olastor/age-plugin-fido2-hmac): FIDO2 plugin with PIN support.
- 🧪 [**age-plugin-sss**](https://github.com/olastor/age-plugin-sss): Shamir's Secret Sharing (SSS) plugin.
- 🧪 [**age-plugin-amnesia**](https://github.com/cedws/amnesia/blob/master/README.md#age-plugin-experimental): Adds Q&A-based identity wrapping.
> **Note:** Plugins marked with 🧪 are experimental. Plugins marked with ⭐️ are official.
---
## Using Plugin-Generated Keys
If you want to use `fido2 tokens` to encrypt your secret instead of the normal age secret key then you need to prefix your age secret key with the corresponding plugin name. In our case we want to use the `age-plugin-fido2-hmac` plugin so we replace `AGE-SECRET-KEY` with `AGE-PLUGIN-FIDO2-HMAC`.
??? tip
- On Linux the age secret key is located at `~/.config/sops/age/keys.txt`
- On macOS it is located at `/Users/admin/Library/Application Support/sops/age/keys.txt`
**Before**:
```hl_lines="2"
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
AGE-SECRET-KEY-1QQPQZRFR7ZZ2WCV...
```
**After**:
```hl_lines="2"
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
```
## Configuring Plugins in `flake.nix`
To use `age` plugins with Clan, you need to configure them in your `flake.nix` file. Heres an example:
```nix title="flake.nix"
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs = { self, clan-core, ... }:
let
# Define Clan configuration
clan = clan-core.lib.clan {
inherit self;
meta.name = "myclan";
# Add YubiKey and FIDO2 HMAC plugins
# Note: Plugins must be available in nixpkgs.
secrets.age.plugins = [
"age-plugin-yubikey"
"age-plugin-fido2-hmac"
];
machines = {
# Machine configurations (elided for brevity)
};
};
in
{
inherit (clan) nixosConfigurations nixosModules clanInternals;
# Additional configurations (elided for brevity)
};
}
```

View File

@@ -0,0 +1,290 @@
# Advanced Vars Examples
This guide demonstrates complex, real-world patterns for the vars system.
## Certificate Authority with Intermediate Certificates
This example shows how to create a complete certificate authority with root and intermediate certificates using dependencies.
```nix
{
# Generate root CA (not deployed to machines)
clan.core.vars.generators.root-ca = {
files."ca.key" = {
secret = true;
deploy = false; # Keep root key offline
};
files."ca.crt".secret = false;
runtimeInputs = [ pkgs.step-cli ];
script = ''
step certificate create "My Root CA" \
$out/ca.crt $out/ca.key \
--profile root-ca \
--no-password \
--not-after 87600h
'';
};
# Generate intermediate key
clan.core.vars.generators.intermediate-key = {
files."intermediate.key" = {
secret = true;
deploy = true;
};
runtimeInputs = [ pkgs.step-cli ];
script = ''
step crypto keypair \
$out/intermediate.pub \
$out/intermediate.key \
--no-password
'';
};
# Generate intermediate certificate signed by root
clan.core.vars.generators.intermediate-cert = {
files."intermediate.crt".secret = false;
dependencies = [
"root-ca"
"intermediate-key"
];
runtimeInputs = [ pkgs.step-cli ];
script = ''
step certificate create "My Intermediate CA" \
$out/intermediate.crt \
$in/intermediate-key/intermediate.key \
--ca $in/root-ca/ca.crt \
--ca-key $in/root-ca/ca.key \
--profile intermediate-ca \
--not-after 8760h \
--no-password
'';
};
# Use the certificates in services
services.nginx.virtualHosts."example.com" = {
sslCertificate = config.clan.core.vars.generators.intermediate-cert.files."intermediate.crt".value;
sslCertificateKey = config.clan.core.vars.generators.intermediate-key.files."intermediate.key".path;
};
}
```
## Multi-Service Secret Sharing
Generate secrets that multiple services can use:
```nix
{
# Generate database credentials
clan.core.vars.generators.database = {
share = true; # Share across machines
files."password" = { };
files."connection-string" = { };
prompts.dbname = {
description = "Database name";
type = "line";
};
script = ''
# Generate password
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32 > $out/password
# Create connection string
echo "postgresql://app:$(cat $out/password)@localhost/$prompts/dbname" \
> $out/connection-string
'';
};
# PostgreSQL uses the password
services.postgresql = {
enable = true;
initialScript = pkgs.writeText "init.sql" ''
CREATE USER app WITH PASSWORD '${
builtins.readFile config.clan.core.vars.generators.database.files."password".path
}';
'';
};
# Application uses the connection string
systemd.services.myapp = {
serviceConfig.EnvironmentFile =
config.clan.core.vars.generators.database.files."connection-string".path;
};
}
```
## SSH Host Keys with Certificates
Generate SSH host keys and sign them with a CA:
```nix
{
# SSH Certificate Authority (shared)
clan.core.vars.generators.ssh-ca = {
share = true;
files."ca" = { secret = true; deploy = false; };
files."ca.pub" = { secret = false; };
runtimeInputs = [ pkgs.openssh ];
script = ''
ssh-keygen -t ed25519 -N "" -f $out/ca
mv $out/ca.pub $out/ca.pub
'';
};
# Host-specific SSH keys
clan.core.vars.generators.ssh-host = {
files."ssh_host_ed25519_key" = {
secret = true;
owner = "root";
group = "root";
mode = "0600";
};
files."ssh_host_ed25519_key.pub" = { secret = false; };
files."ssh_host_ed25519_key-cert.pub" = { secret = false; };
dependencies = [ "ssh-ca" ];
runtimeInputs = [ pkgs.openssh ];
script = ''
# Generate host key
ssh-keygen -t ed25519 -N "" -f $out/ssh_host_ed25519_key
# Sign with CA
ssh-keygen -s $in/ssh-ca/ca \
-I "host:${config.networking.hostName}" \
-h \
-V -5m:+365d \
$out/ssh_host_ed25519_key.pub
'';
};
# Configure SSH to use the generated keys
services.openssh = {
hostKeys = [{
path = config.clan.core.vars.generators.ssh-host.files."ssh_host_ed25519_key".path;
type = "ed25519";
}];
};
}
```
## WireGuard Mesh Network
Create a WireGuard configuration with pre-shared keys:
```nix
{
# Generate WireGuard keys for this host
clan.core.vars.generators.wireguard = {
files."privatekey" = {
secret = true;
owner = "systemd-network";
mode = "0400";
};
files."publickey" = { secret = false; };
files."preshared" = { secret = true; };
runtimeInputs = [ pkgs.wireguard-tools ];
script = ''
# Generate key pair
wg genkey > $out/privatekey
wg pubkey < $out/privatekey > $out/publickey
# Generate pre-shared key
wg genpsk > $out/preshared
'';
};
# Configure WireGuard
networking.wireguard.interfaces.wg0 = {
privateKeyFile = config.clan.core.vars.generators.wireguard.files."privatekey".path;
peers = [{
publicKey = "peer-public-key-here";
presharedKeyFile = config.clan.core.vars.generators.wireguard.files."preshared".path;
allowedIPs = [ "10.0.0.2/32" ];
}];
};
}
```
## Conditional Generation Based on Machine Role
Generate different secrets based on machine configuration:
```nix
{
clan.core.vars.generators = lib.mkMerge [
# All machines get basic auth
{
basic-auth = {
files."htpasswd" = { };
prompts.username = {
description = "Username for basic auth";
type = "line";
};
prompts.password = {
description = "Password for basic auth";
type = "hidden";
};
runtimeInputs = [ pkgs.apacheHttpd ];
script = ''
htpasswd -nbB "$prompts/username" "$prompts/password" > $out/htpasswd
'';
};
}
# Only servers get API tokens
(lib.mkIf config.services.myapi.enable {
api-tokens = {
files."admin-token" = { };
files."readonly-token" = { };
runtimeInputs = [ pkgs.openssl ];
script = ''
openssl rand -hex 32 > $out/admin-token
openssl rand -hex 16 > $out/readonly-token
'';
};
})
];
}
```
## Backup Encryption Keys
Generate and manage backup encryption keys:
```nix
{
clan.core.vars.generators.backup = {
share = true; # Same key for all backup sources
files."encryption.key" = {
secret = true;
deploy = true;
};
files."encryption.pub" = { secret = false; };
runtimeInputs = [ pkgs.age ];
script = ''
# Generate age key pair
age-keygen -o $out/encryption.key 2>/dev/null
# Extract public key
grep "public key:" $out/encryption.key | cut -d: -f2 | tr -d ' ' \
> $out/encryption.pub
'';
};
# Use in backup service
services.borgbackup.jobs.system = {
encryption = {
mode = "repokey-blake2";
passCommand = "cat ${config.clan.core.vars.generators.backup.files."encryption.key".path}";
};
};
}
```
## Tips and Best Practices
1. **Use dependencies** to build complex multi-stage generations
2. **Share generators** when the same secret is needed across machines
3. **Set appropriate permissions** for service-specific secrets
4. **Use prompts** for user-specific values that shouldn't be generated
5. **Combine secret and non-secret files** in the same generator when they're related
6. **Use conditional generation** with `lib.mkIf` for role-specific secrets

View File

@@ -1,26 +1,21 @@
# Generators
The `clan vars` subcommand is a powerful tool for managing machine-specific variables in a declarative and reproducible way. This guide will walk you through its usage, from setting up a generator to sharing and updating variables across machines.
Defining a linux user's password via the nixos configuration previously required running `mkpasswd ...` and then copying the hash back into the nix configuration.
For a detailed API reference, see the [vars module documentation](../../reference/clan.core/vars.md).
In this example, we will guide you through automating that interaction using clan `vars`.
In this guide, you will learn how to:
For a more general explanation of what clan vars are and how it works, see the intro of the [Reference Documentation for vars](../reference/clan.core/vars.md)
1. Declare a `generator` in the machine's NixOS configuration.
2. Inspect the status of variables using the Clan CLI.
3. Generate variables interactively.
4. Observe the changes made to your repository.
5. Update the machine configuration.
6. Share the root password between multiple machines.
7. Change the root password when needed.
This guide assumes
- Clan is set up already (see [Getting Started](../guides/getting-started/index.md))
- a machine has been added to the clan (see [Adding Machines](../guides/getting-started/add-machines.md))
By the end of this guide, you will have a clear understanding of how to use `clan vars` to manage sensitive data, such as passwords, in a secure and efficient manner.
This section will walk you through the following steps:
1. declare a `generator` in the machine's nixos configuration
2. inspect the status via the Clan CLI
3. generate the vars
4. observe the changes
5. update the machine
6. share the root password between machines
7. change the password
## Declare a generator
## Declare the generator
In this example, a `vars` `generator` is used to:
@@ -114,7 +109,7 @@ If we just imported the `root-password.nix` from above into more machines, clan
If the root password instead should only be entered once and shared across all machines, the generator defined above needs to be declared as `shared`, by adding `share = true` to it:
```nix
{config, pkgs, ...}: {
clan.vars.generators.root-password = {
clan.core.vars.generators.root-password = {
share = true;
# ...
}
@@ -141,8 +136,3 @@ Updated var root-password/password-hash
new: $6$OyoQtDVzeemgh8EQ$zRK...
```
## Further Reading
- [Reference Documentation for `clan.core.vars` NixOS options](../reference/clan.core/vars.md)
- [Reference Documentation for the `clan vars` CLI command](../reference/cli/vars.md)

View File

@@ -0,0 +1,123 @@
# Understanding Clan Vars - Concepts & Architecture
This guide explains the architecture and design principles behind the vars system.
## Architecture Overview
The vars system provides a declarative, reproducible way to manage generated files (especially secrets) in NixOS configurations.
## Data Flow
```mermaid
graph LR
A[Generator Script] --> B[Output Files]
C[User Prompts] --> A
D[Dependencies] --> A
B --> E[Secret Storage<br/>sops/password-store]
B --> F[Nix Store<br/>public files]
E --> G[Machine Deployment]
F --> G
```
## Key Design Principles
### 1. Declarative Generation
Unlike imperative secret management, vars are declared in your NixOS configuration and generated deterministically. This ensures reproducibility across deployments.
### 2. Separation of Concerns
- **Generation logic**: Defined in generator scripts
- **Storage**: Handled by pluggable backends (sops, password-store, etc.)
- **Deployment**: Managed by NixOS activation scripts
- **Access control**: Enforced through file permissions and ownership
### 3. Composability Through Dependencies
Generators can depend on outputs from other generators, enabling complex workflows:
```nix
# Dependencies create a directed acyclic graph (DAG)
A B C
D
```
This allows building sophisticated systems like certificate authorities where intermediate certificates depend on root certificates.
### 4. Type Safety
The vars system distinguishes between:
- **Secret files**: Only accessible via `.path`, deployed to `/run/secrets/`
- **Public files**: Accessible via `.value`, stored in nix store
This prevents accidental exposure of secrets in the nix store.
## Storage Backend Architecture
The vars system uses pluggable storage backends:
- **sops** (default): Integrates with clan's existing sops encryption
- **password-store**: For users already using pass
Each backend handles encryption/decryption transparently, allowing the same generator definitions to work across different security models.
## Timing and Lifecycle
### Generation Phases
1. **Pre-deployment**: `clan vars generate` creates vars before deployment
2. **During deployment**: Missing vars are generated automatically
3. **Regeneration**: Explicit regeneration with `--regenerate` flag
### The `neededFor` Option
Control when vars are available during system activation:
```nix
files."early-secret" = {
secret = true;
neededFor = [ "users" "groups" ]; # Available early in activation
};
```
## Advanced Patterns
### Multi-Machine Coordination
The `share` option enables cross-machine secret sharing:
```mermaid
graph LR
A[Shared Generator] --> B[Machine 1]
A --> C[Machine 2]
A --> D[Machine 3]
```
This is useful for:
- Shared certificate authorities
- Mesh VPN pre-shared keys
- Cluster join tokens
### Generator Composition
Complex systems can be built by composing simple generators:
```
root-ca → intermediate-ca → service-cert
ocsp-responder
```
Each generator focuses on one task, making the system modular and testable.
## Key Advantages
Compared to manual secret management, vars provides:
- **Declarative configuration**: Define once, generate consistently
- **Dependency management**: Build complex systems with generator dependencies
- **Type safety**: Separate handling of secret and public files
- **User prompts**: Gather input when needed
- **Easy regeneration**: Update secrets with a single command

View File

@@ -0,0 +1,145 @@
# Vars System Overview
The vars system is clan's declarative solution for managing generated files, secrets, and dynamic configuration in your NixOS deployments. It eliminates the manual steps of generating credentials, certificates, and other dynamic values by automating these processes within your infrastructure-as-code workflow.
## What Problems Does Vars Solve?
### Before Vars: Manual Secret Management
Traditional NixOS deployments require manual steps for secrets and generated files:
```bash
# Generate password hash manually
mkpasswd -m sha-512 > /tmp/root-password-hash
# Copy hash into configuration
users.users.root.hashedPasswordFile = "/tmp/root-password-hash";
```
This approach has several problems:
- **Not reproducible**: Manual steps vary between team members
- **Hard to maintain**: Updating secrets requires remembering manual commands
- **Deployment friction**: Secrets must be managed outside of your configuration
- **Team collaboration issues**: Sharing credentials securely is complex
### After Vars: Declarative Generation
With vars, the same process becomes declarative and automated:
```nix
clan.core.vars.generators.root-password = {
prompts.password.description = "Root password";
prompts.password.type = "hidden";
files.hash.secret = false;
script = "mkpasswd -m sha-512 < $prompts/password > $out/hash";
runtimeInputs = [ pkgs.mkpasswd ];
};
users.users.root.hashedPasswordFile =
config.clan.core.vars.generators.root-password.files.hash.path;
```
## Core Benefits
- **🔄 Reproducible**: Same inputs always produce the same outputs
- **📝 Declarative**: Defined alongside your NixOS configuration
- **🔐 Secure**: Automatic secret storage and encrypted deployment
- **👥 Collaborative**: Built-in sharing for team environments
- **🚀 Automated**: No manual intervention required for deployments
- **🔗 Integrated**: Works seamlessly with clan's deployment workflow
## How It Works
```mermaid
graph TB
A[Generator Declaration] --> B[clan vars generate]
B --> C{Prompts User}
C --> D[Execute Script]
D --> E[Output Files]
E --> F{Secret?}
F -->|Yes| G[Encrypted Storage]
F -->|No| H[Git Repository]
G --> I[Deploy to Machine]
H --> I
I --> J[Available in NixOS]
```
1. **Declare generators** in your NixOS configuration
2. **Generate values** using `clan vars generate` (or automatically during deployment)
3. **Store securely** in encrypted backends or version control
4. **Deploy seamlessly** to your machines where they're accessible as file paths
## Common Use Cases
| Use Case | What Gets Generated | Benefits |
|----------|-------------------|----------|
| **User passwords** | Password hashes | No plaintext in config |
| **SSH keys** | Host/user keypairs | Automated key rotation |
| **TLS certificates** | Certificates + private keys | Automated PKI |
| **Database credentials** | Passwords + connection strings | Secure service communication |
| **API tokens** | Random tokens | Service authentication |
| **Configuration files** | Complex configs with secrets | Dynamic config generation |
## Architecture Overview
The vars system has three main components:
### 1. **Generators**
Define how to create files from inputs:
- **Prompts**: Values requested from users
- **Scripts**: Generation logic
- **Dependencies**: Other generators this depends on
- **Outputs**: Files that get created
### 2. **Storage Backends**
Handle secret storage and deployment:
- **sops**: Encrypted files in git (recommended)
- **password-store**: GPG/age-based secret storage
## Quick Start Example
Here's a complete example showing password generation and usage:
```nix
# generator.nix
{ config, pkgs, ... }: {
clan.core.vars.generators.user-password = {
prompts.password = {
description = "User password";
type = "hidden";
};
files.hash = { secret = false; };
script = ''
mkpasswd -m sha-512 < $prompts/password > $out/hash
'';
runtimeInputs = [ pkgs.mkpasswd ];
};
users.users.myuser = {
hashedPasswordFile =
config.clan.core.vars.generators.user-password.files.hash.path;
};
}
```
```bash
# Generate the password
clan vars generate my-machine
# Deploy to machine
clan machines update my-machine
```
## Migration from Facts
If you're currently using the legacy facts system, see our [Migration Guide](../migrations/migration-facts-vars.md) for step-by-step instructions on upgrading to vars.

View File

@@ -0,0 +1,272 @@
# Troubleshooting Vars
Quick reference for diagnosing and fixing vars issues.
## Common Issues
### Generator Script Fails
**Symptom**: Error during `clan vars generate` or deployment
**Possible causes and solutions**:
1. **Missing runtime inputs**
```nix
# Wrong - missing required tool
runtimeInputs = [ ];
script = ''
openssl rand -hex 32 > $out/secret # openssl not found!
'';
# Correct
runtimeInputs = [ pkgs.openssl ];
```
2. **Wrong output path**
```nix
# Wrong - must use $out
script = ''
echo "secret" > ./myfile
'';
# Correct
script = ''
echo "secret" > $out/myfile
'';
```
3. **Missing declared files**
```nix
files."config" = { };
files."key" = { };
script = ''
# Wrong - only generates one file
echo "data" > $out/config
'';
# Correct - must generate all declared files
script = ''
echo "data" > $out/config
echo "key" > $out/key
'';
```
### Cannot Access Generated Files
**Symptom**: "attribute 'value' missing" or file not found
**Solutions**:
1. **Secret files don't have `.value`**
```nix
# Wrong - secret files can't use .value
files."secret" = { secret = true; };
# ...
environment.etc."app.conf".text =
config.clan.core.vars.generators.app.files."secret".value;
# Correct - use .path for secrets
environment.etc."app.conf".source =
config.clan.core.vars.generators.app.files."secret".path;
```
2. **Public files should use `.value`**
```nix
# Better for non-secrets
files."cert.pem" = { secret = false; };
# ...
sslCertificate =
config.clan.core.vars.generators.ca.files."cert.pem".value;
```
### Dependencies Not Available
**Symptom**: "No such file or directory" when accessing `$in/...`
**Solution**: Declare dependencies correctly
```nix
clan.core.vars.generators.child = {
# Wrong - missing dependency
script = ''
cat $in/parent/file > $out/newfile
'';
# Correct
dependencies = [ "parent" ];
script = ''
cat $in/parent/file > $out/newfile
'';
};
```
### Permission Denied
**Symptom**: Service cannot read generated secret file
**Solution**: Set correct ownership and permissions
```nix
files."service.key" = {
secret = true;
owner = "myservice"; # Match service user
group = "myservice";
mode = "0400"; # Read-only for owner
};
```
### Vars Not Regenerating
**Symptom**: Changes to generator script don't trigger regeneration
**Solution**: Use `--regenerate` flag
```bash
clan vars generate my-machine --generator my-generator --regenerate
```
### Prompts Not Working
**Symptom**: Script fails with "No such file or directory" for prompts
**Solution**: Access prompts correctly
```nix
# Wrong
script = ''
echo $password > $out/file
'';
# Correct
prompts.password.type = "hidden";
script = ''
cat $prompts/password > $out/file
'';
```
## Debugging Techniques
### 1. Check Generator Status
See what vars are set:
```bash
clan vars list my-machine
```
### 2. Inspect Generated Files
For shared vars:
```bash
ls -la vars/shared/my-generator/
```
For per-machine vars:
```bash
ls -la vars/per-machine/my-machine/my-generator/
```
### 3. Test Generators Locally
Create a test script to debug:
```nix
# test-generator.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation {
name = "test-generator";
buildInputs = [ pkgs.openssl ]; # Your runtime inputs
buildCommand = ''
# Your generator script here
mkdir -p $out
openssl rand -hex 32 > $out/secret
ls -la $out/
'';
}
```
Run with:
```bash
nix-build test-generator.nix
```
### 4. Enable Debug Logging
Set debug mode:
```bash
clan --debug vars generate my-machine
```
### 5. Check File Permissions
Verify generated secret permissions:
```bash
# On the target machine
ls -la /run/secrets/
```
## Recovery Procedures
### Regenerate All Vars
If vars are corrupted or need refresh:
```bash
# Regenerate all for a machine
clan vars generate my-machine --regenerate
# Regenerate specific generator
clan vars generate my-machine --generator my-generator --regenerate
```
### Manual Secret Injection
For recovery or testing:
```bash
# Set a var manually (bypass generator)
echo "temporary-secret" | clan vars set my-machine my-generator/my-file
```
### Restore from Backup
Vars are stored in the repository:
```bash
# Restore previous version
git checkout HEAD~1 -- vars/
# Check and regenerate if needed
clan vars list my-machine
```
## Storage Backend Issues
### SOPS Decryption Fails
**Symptom**: "Failed to decrypt" or permission errors
**Solution**: Ensure your user/machine has the correct age keys configured. Clan manages encryption keys automatically based on the configured users and machines in your flake.
Check that:
1. Your machine is properly configured in the flake
2. Your user has access to the machine's secrets
3. The age key is available in the expected location
### Password Store Issues
**Symptom**: "pass: store not initialized"
**Solution**: Initialize password store:
```bash
export PASSWORD_STORE_DIR=/path/to/clan/vars
pass init your-gpg-key
```
## Getting Help
If these solutions don't resolve your issue:
1. Check the [clan-core issue tracker](https://git.clan.lol/clan/clan-core/issues)
2. Ask in the Clan community channels
3. Provide:
- The generator configuration
- The exact error message
- Output of `clan --debug vars generate`

View File

@@ -55,7 +55,7 @@ Explore the underlying principles of Clan
<div class="grid cards" markdown>
- [Generators](./concepts/generators.md)
- [Vars](./guides/vars/vars-overview.md)
---

26
flake.lock generated
View File

@@ -13,11 +13,11 @@
]
},
"locked": {
"lastModified": 1756695982,
"narHash": "sha256-dyLhOSDzxZtRgi5aj/OuaZJUsuvo+8sZ9CU/qieZ15c=",
"rev": "cc8f26e7e6c2dc985526ba59b286ae5a83168cdb",
"lastModified": 1757905600,
"narHash": "sha256-Yd7buL9N7N7IaDVViItqP9HsECfnlDFykxvvNgMYcKk=",
"rev": "c10c4002bdc5aef040fcbb814d5f482e82dc8345",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/cc8f26e7e6c2dc985526ba59b286ae5a83168cdb.tar.gz"
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/c10c4002bdc5aef040fcbb814d5f482e82dc8345.tar.gz"
},
"original": {
"type": "tarball",
@@ -31,11 +31,11 @@
]
},
"locked": {
"lastModified": 1756733629,
"narHash": "sha256-dwWGlDhcO5SMIvMSTB4mjQ5Pvo2vtxvpIknhVnSz2I8=",
"lastModified": 1757508292,
"narHash": "sha256-7lVWL5bC6xBIMWWDal41LlGAG+9u2zUorqo3QCUL4p4=",
"owner": "nix-community",
"repo": "disko",
"rev": "a5c4f2ab72e3d1ab43e3e65aa421c6f2bd2e12a1",
"rev": "146f45bee02b8bd88812cfce6ffc0f933788875a",
"type": "github"
},
"original": {
@@ -71,11 +71,11 @@
]
},
"locked": {
"lastModified": 1757130842,
"narHash": "sha256-4i7KKuXesSZGUv0cLPLfxbmF1S72Gf/3aSypgvVkwuA=",
"lastModified": 1757430124,
"narHash": "sha256-MhDltfXesGH8VkGv3hmJ1QEKl1ChTIj9wmGAFfWj/Wk=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "15f067638e2887c58c4b6ba1bdb65a0b61dc58c5",
"rev": "830b3f0b50045cf0bcfd4dab65fad05bf882e196",
"type": "github"
},
"original": {
@@ -146,11 +146,11 @@
]
},
"locked": {
"lastModified": 1754988908,
"narHash": "sha256-t+voe2961vCgrzPFtZxha0/kmFSHFobzF00sT8p9h0U=",
"lastModified": 1757449901,
"narHash": "sha256-qwN8nYdSRnmmyyi+uR6m4gXnVktmy5smG1MOrSFD8PI=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "3223c7a92724b5d804e9988c6b447a0d09017d48",
"rev": "3b4a369df9dd6ee171a7ea4448b50e2528faf850",
"type": "github"
},
"original": {

View File

@@ -51,7 +51,7 @@
;
privateInputs =
if builtins.pathExists (./. + ".skip-private-inputs") then
if builtins.pathExists (./. + "/.skip-private-inputs") then
{ }
else
(import ./devFlake/flake-compat.nix {

View File

@@ -189,8 +189,12 @@ in
clan.core.vars.generators.zerotier = {
migrateFact = "zerotier";
files.zerotier-ip.secret = false;
files.zerotier-ip.restartUnits = [ "zerotierone.service" ];
files.zerotier-network-id.secret = false;
files.zerotier-identity-secret = { };
files.zerotier-network-id.restartUnits = [ "zerotierone.service" ];
files.zerotier-identity-secret = {
restartUnits = [ "zerotierone.service" ];
};
runtimeInputs = [
config.services.zerotierone.package
pkgs.python3
@@ -211,7 +215,10 @@ in
clan.core.vars.generators.zerotier = {
migrateFact = "zerotier";
files.zerotier-ip.secret = false;
files.zerotier-identity-secret = { };
files.zerotier-ip.restartUnits = [ "zerotierone.service" ];
files.zerotier-identity-secret = {
restartUnits = [ "zerotierone.service" ];
};
runtimeInputs = [
config.services.zerotierone.package
pkgs.python3

View File

@@ -1,16 +1,15 @@
import logging
import threading
from abc import ABC, abstractmethod
from contextlib import ExitStack
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Protocol
from clan_lib.api import ApiError, ApiResponse, ErrorDataClass
from clan_lib.api.tasks import WebThread
from clan_lib.async_run import set_current_thread_opkey, set_should_cancel
if TYPE_CHECKING:
from .middleware import Middleware
from clan_app.middleware.base import Middleware
log = logging.getLogger(__name__)
@@ -30,20 +29,17 @@ class BackendResponse:
_op_key: str
@dataclass
class ApiBridge(ABC):
class ApiBridge(Protocol):
"""Generic interface for API bridges that can handle method calls from different sources."""
middleware_chain: tuple["Middleware", ...]
threads: dict[str, WebThread] = field(default_factory=dict)
threads: dict[str, WebThread]
@abstractmethod
def send_api_response(self, response: BackendResponse) -> None:
"""Send response back to the client."""
def send_api_response(self, response: BackendResponse) -> None: ...
def process_request(self, request: BackendRequest) -> None:
"""Process an API request through the middleware chain."""
from .middleware import MiddlewareContext # noqa: PLC0415
from clan_app.middleware.base import MiddlewareContext # noqa: PLC0415
with ExitStack() as stack:
context = MiddlewareContext(

View File

@@ -0,0 +1,20 @@
"""Compatibility wrapper for relocated middleware components.
This module preserves the legacy import path ``clan_app.api.middleware`` while
the actual middleware implementations now live in ``clan_app.middleware``.
"""
from __future__ import annotations
from warnings import warn
import clan_app.middleware as _middleware
from clan_app.middleware import * # noqa: F403
warn(
"clan_app.api.middleware is deprecated; use clan_app.middleware instead",
DeprecationWarning,
stacklevel=2,
)
__all__ = _middleware.__all__

View File

@@ -12,13 +12,13 @@ from clan_lib.log_manager import LogGroupConfig, LogManager
from clan_lib.log_manager import api as log_manager_api
from clan_app.api.file_gtk import get_clan_folder, get_system_file
from clan_app.api.middleware import (
from clan_app.deps.http.http_server import HttpApiServer
from clan_app.deps.webview.webview import Size, SizeHint, Webview
from clan_app.middleware import (
ArgumentParsingMiddleware,
LoggingMiddleware,
MethodExecutionMiddleware,
)
from clan_app.deps.http.http_server import HttpApiServer
from clan_app.deps.webview.webview import Size, SizeHint, Webview
log = logging.getLogger(__name__)

View File

@@ -21,7 +21,7 @@ from clan_lib.async_run import (
from clan_app.api.api_bridge import ApiBridge, BackendRequest, BackendResponse
if TYPE_CHECKING:
from clan_app.api.middleware import Middleware
from clan_app.middleware.base import Middleware
log = logging.getLogger(__name__)

View File

@@ -8,7 +8,7 @@ from clan_lib.api import MethodRegistry
from clan_lib.api.tasks import WebThread
if TYPE_CHECKING:
from clan_app.api.middleware import Middleware
from clan_app.middleware.base import Middleware
from .http_bridge import HttpBridge

View File

@@ -10,11 +10,11 @@ import pytest
from clan_lib.api import MethodRegistry, tasks
from clan_lib.async_run import is_async_cancelled
from clan_app.api.middleware import (
from clan_app.deps.http.http_server import HttpApiServer
from clan_app.middleware import (
ArgumentParsingMiddleware,
MethodExecutionMiddleware,
)
from clan_app.deps.http.http_server import HttpApiServer
log = logging.getLogger(__name__)

View File

@@ -19,7 +19,7 @@ from ._webview_ffi import (
from .webview_bridge import WebviewBridge
if TYPE_CHECKING:
from clan_app.api.middleware import Middleware
from clan_app.middleware.base import Middleware
log = logging.getLogger(__name__)

View File

@@ -1,6 +1,6 @@
import json
import logging
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from clan_lib.api import dataclass_to_dict
@@ -9,6 +9,8 @@ from clan_lib.api.tasks import WebThread
from clan_app.api.api_bridge import ApiBridge, BackendRequest, BackendResponse
if TYPE_CHECKING:
from clan_app.middleware.base import Middleware
from .webview import Webview
log = logging.getLogger(__name__)
@@ -19,7 +21,8 @@ class WebviewBridge(ApiBridge):
"""Webview-specific implementation of the API bridge."""
webview: "Webview"
threads: dict[str, WebThread] # Inherited from ApiBridge
middleware_chain: tuple["Middleware", ...]
threads: dict[str, WebThread] = field(default_factory=dict)
def send_api_response(self, response: BackendResponse) -> None:
"""Send response back to the webview client."""

View File

@@ -1,4 +1,4 @@
"""Middleware components for the webview API bridge."""
"""Middleware components shared by API bridge implementations."""
from .argument_parsing import ArgumentParsingMiddleware
from .base import Middleware, MiddlewareContext

View File

@@ -38,7 +38,7 @@ fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
assets.forEach((asset) => {
// console.log(asset);
if (asset.src === "index.html") {
asset.css.forEach((cssEntry) => {
asset.css?.forEach((cssEntry) => {
// css to be processed
const css = fs.readFileSync(`dist/${cssEntry}`, "utf8");

View File

@@ -19,6 +19,7 @@ import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
import { useApiClient } from "@/src/hooks/ApiClient";
import { useClanURI } from "@/src/hooks/clan";
import { AlertProps } from "@/src/components/Alert/Alert";
import usbLogo from "@/logos/usb-stick-min.png?url";
// TODO: Deduplicate
interface UpdateStepperProps {
@@ -135,11 +136,7 @@ const UpdateProgress = () => {
return (
<div class="relative flex size-full flex-col items-center justify-end bg-inv-4">
<img
src="/logos/usb-stick-min.png"
alt="usb logo"
class="absolute top-2 z-0"
/>
<img src={usbLogo} alt="usb logo" class="absolute top-2 z-0" />
<div class="z-10 mb-6 flex w-full max-w-md flex-col items-center gap-2 fg-inv-1">
<Typography
hierarchy="title"

View File

@@ -20,6 +20,7 @@ import { useSystemStorageOptions } from "@/src/hooks/queries";
import { useApiClient } from "@/src/hooks/ApiClient";
import { onMount } from "solid-js";
import cx from "classnames";
import usbLogo from "@/logos/usb-stick-min.png?url";
const Prose = () => (
<StepLayout
@@ -335,11 +336,7 @@ const FlashProgress = () => {
"relative flex size-full flex-col items-center justify-end bg-inv-4",
)}
>
<img
src="/logos/usb-stick-min.png"
alt="usb logo"
class="absolute top-4 z-0"
/>
<img src={usbLogo} alt="usb logo" class="absolute top-4 z-0" />
<div class="z-10 mb-6 flex w-full max-w-md flex-col items-center gap-2 fg-inv-1">
<Typography
hierarchy="title"

View File

@@ -36,6 +36,7 @@ import { useApiClient } from "@/src/hooks/ApiClient";
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
import { Loader } from "@/src/components/Loader/Loader";
import { Button as KButton } from "@kobalte/core/button";
import usbLogo from "@/logos/usb-stick-min.png?url";
export const InstallHeader = (props: { machineName: string }) => {
return (
@@ -829,11 +830,7 @@ const InstallProgress = () => {
return (
<div class="relative flex size-full flex-col items-center justify-end bg-inv-4">
<img
src="/logos/usb-stick-min.png"
alt="usb logo"
class="absolute top-2 z-0"
/>
<img src={usbLogo} alt="usb logo" class="absolute top-2 z-0" />
<div class="z-10 mb-6 flex w-full max-w-md flex-col items-center gap-2 fg-inv-1">
<Typography
hierarchy="title"

View File

@@ -30,6 +30,7 @@ export default defineConfig({
"@": path.resolve(__dirname, "./"), // Adjust the path as needed
},
},
base: "./",
optimizeDeps: {
include: ["debug", "extend"],
},
@@ -48,7 +49,18 @@ export default defineConfig({
},
build: {
target: "safari11",
modulePreload: false,
// assetsDi
manifest: true,
// Inline everything: TODO
// Detect file:///assets requests and point to the correct directory in webview
rollupOptions: {
output: {
format: "iife",
// entryFileName: ""
// inlineDynamicImports: true,
},
},
// assetsInlineLimit: 0,
},
});

View File

@@ -24,8 +24,8 @@ clangStdenv.mkDerivation {
domain = "git.clan.lol";
owner = "clan";
repo = "webview";
rev = "c27041cb50f79c197080a3f4fa2bad4557ef3234";
hash = "sha256-xNkX7O+GFMbv3YnXPrtO6vw+BUqCbVeFd8FjgPKfEG0=";
rev = "d83c3ebffb76f25b3a9e37d59237c5d8e94060a2";
hash = "sha256-YAfH1KCw4r2WPvBQho2ypAVH+/c/a05SsEDUYKGadFI=";
};
outputs = [

View File

@@ -198,7 +198,7 @@ This subcommand provides an interface to templates provided by clan.
Examples:
$ clan templates list
List all the machines managed by Clan.
List all available templates
Usage differs based on the template type
@@ -227,6 +227,16 @@ Disk templates
Real world example
$ clan templates apply disk single-disk jon --set mainDisk "/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368"
---
Machine templates
$ clan templates apply machine [TEMPLATE] [MACHINE_NAME]
Will create a new machine [MACHINE_NAME] from the specified [TEMPLATE]
Real world example
$ clan templates apply machine flash-installer my-installer
"""
),
formatter_class=argparse.RawTextHelpFormatter,

View File

@@ -303,6 +303,27 @@ def complete_templates_clan(
return []
def complete_templates_machine(
_prefix: str,
parsed_args: argparse.Namespace,
**_kwargs: Any,
) -> Iterable[str]:
"""Provides completion functionality for machine templates"""
flake = (
clan_dir_result
if (clan_dir_result := clan_dir(getattr(parsed_args, "flake", None)))
is not None
else "."
)
list_all_templates = list_templates(Flake(flake))
machine_template_list = list_all_templates.builtins.get("machine")
if machine_template_list:
machine_templates = list(machine_template_list)
return dict.fromkeys(machine_templates, "machine")
return []
def complete_vars_for_machine(
_prefix: str,
parsed_args: argparse.Namespace,

View File

@@ -1,6 +1,7 @@
import argparse
from .apply_disk import register_apply_disk_template_parser
from .apply_machine import register_apply_machine_template_parser
def register_apply_parser(parser: argparse.ArgumentParser) -> None:
@@ -11,5 +12,7 @@ def register_apply_parser(parser: argparse.ArgumentParser) -> None:
required=True,
)
disk_parser = subparser.add_parser("disk", help="Apply a disk template")
machine_parser = subparser.add_parser("machine", help="Apply a machine template")
register_apply_disk_template_parser(disk_parser)
register_apply_machine_template_parser(machine_parser)

View File

@@ -0,0 +1,42 @@
import argparse
import logging
from clan_lib.nix_models.clan import InventoryMachine
from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy
from clan_cli.machines.create import CreateOptions, create_machine
log = logging.getLogger(__name__)
def apply_command(args: argparse.Namespace) -> None:
"""Apply a machine template - actually an alias for machines create --template."""
# Create machine using the create_machine API directly
machine = InventoryMachine(
name=args.machine,
tags=[],
deploy=MachineDeploy(targetHost=None),
)
opts = CreateOptions(
clan_dir=args.flake,
machine=machine,
template=args.template,
)
create_machine(opts)
def register_apply_machine_template_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"template",
type=str,
help="The name of the machine template to apply",
)
parser.add_argument(
"machine",
type=str,
help="The name of the machine to create from the template",
)
parser.set_defaults(func=apply_command)

View File

@@ -0,0 +1,79 @@
import json
import pytest
from clan_lib.errors import ClanError
from clan_lib.flake import Flake
from clan_lib.machines.machines import Machine
from clan_lib.templates.disk import set_machine_disk_schema
from clan_cli.tests.fixtures_flakes import FlakeForTest
from clan_cli.tests.helpers import cli
@pytest.mark.with_core
def test_templates_apply_machine_and_disk(
test_flake_with_core: FlakeForTest,
) -> None:
"""Test both machine template creation and disk template application."""
flake_path = str(test_flake_with_core.path)
cli.run(
[
"templates",
"apply",
"machine",
"new-machine",
"test-apply-machine",
"--flake",
flake_path,
]
)
# Verify machine was created
machine_dir = test_flake_with_core.path / "machines" / "test-apply-machine"
assert machine_dir.exists(), "Machine directory should be created"
assert (machine_dir / "configuration.nix").exists(), (
"Configuration file should exist"
)
facter_content = {
"disks": [
{
"name": "test-disk",
"path": "/dev/sda",
"size": 107374182400,
"type": "disk",
}
]
}
facter_path = machine_dir / "facter.json"
facter_path.write_text(json.dumps(facter_content, indent=2))
machine = Machine(name="test-apply-machine", flake=Flake(flake_path))
set_machine_disk_schema(
machine,
"single-disk",
{"mainDisk": "/dev/sda"},
force=False,
check_hw=False, # Skip hardware validation for test
)
# Verify disk template was applied by checking that disko.nix exists or was updated
disko_file = machine_dir / "disko.nix"
assert disko_file.exists(), "Disko configuration should be created"
# Verify error handling - try to create duplicate machine
# Since apply machine now uses machines create, it raises ClanError for duplicates
with pytest.raises(ClanError, match="already exists"):
cli.run(
[
"templates",
"apply",
"machine",
"new-machine",
"test-apply-machine", # Same name as existing
"--flake",
flake_path,
]
)

View File

@@ -8,6 +8,7 @@ HostKeyCheck = Literal[
"strict", # Strictly check ssh host keys, prompt for unknown ones
"ask", # Ask for confirmation on first use
"accept-new", # Trust on ssh keys on first use
"tofu", # Trust on ssh keys on first use
"none", # Do not check ssh host keys
]

View File

@@ -27,7 +27,7 @@
# The lines below will define a zerotier network and add all machines as 'peer' to it.
# !!! Manual steps required:
# - Define a controller machine for the zerotier network.
# - Deploy the controller machine first to initilize the network.
# - Deploy the controller machine first to initialize the network.
zerotier = {
# Replace with the name (string) of your machine that you will use as zerotier-controller
# See: https://docs.zerotier.com/controller/

View File

@@ -1,7 +1,7 @@
{ inputs, ... }:
{
imports = [
inputs.clan.flakeModules.default
inputs.clan-core.flakeModules.default
];
clan = {
meta.name = "__CHANGE_ME__";

View File

@@ -9,7 +9,7 @@ _: {
devShells = {
default = pkgs.mkShellNoCC {
packages = [
inputs'.clan.packages.default
inputs'.clan-core.packages.default
];
};
};

View File

@@ -27,7 +27,7 @@
# The lines below will define a zerotier network and add all machines as 'peer' to it.
# !!! Manual steps required:
# - Define a controller machine for the zerotier network.
# - Deploy the controller machine first to initilize the network.
# - Deploy the controller machine first to initialize the network.
zerotier = {
# Replace with the name (string) of your machine that you will use as zerotier-controller
# See: https://docs.zerotier.com/controller/