Compare commits
416 Commits
clan-25.05
...
macos
| Author | SHA1 | Date | |
|---|---|---|---|
| 55e343c43e | |||
| 8bef2e6b2e | |||
|
|
8eaca289ad | ||
|
|
6f2d482187 | ||
|
|
4c30418f12 | ||
|
|
3c66094d89 | ||
|
|
a8f180f8da | ||
|
|
e22218d589 | ||
|
|
228c60bcf7 | ||
|
|
ed2b2d9df9 | ||
|
|
7e2a127d11 | ||
|
|
8c8bacb1ab | ||
|
|
8ba71144b6 | ||
|
|
7f2d15c8a1 | ||
|
|
486463c793 | ||
|
|
071603d688 | ||
|
|
c612561ec3 | ||
|
|
a88cd2be40 | ||
|
|
7140b417d3 | ||
|
|
c7a42cca7f | ||
|
|
29ca23c629 | ||
|
|
cd7210de1b | ||
|
|
c2ebafcf92 | ||
|
|
2a9e4e7860 | ||
|
|
43a7652624 | ||
|
|
65fd25bc2e | ||
|
|
f89ea15749 | ||
|
|
19d4833be8 | ||
|
|
82f12eaf6f | ||
|
|
0b5a8e98de | ||
|
|
c5bddada05 | ||
|
|
62b64c3b3e | ||
|
|
19a1ad6081 | ||
|
|
a2df5db3d6 | ||
|
|
ac46f890ea | ||
|
|
83f78d9f59 | ||
|
|
19abf8d288 | ||
|
|
e5105e31c4 | ||
|
|
0f847b4799 | ||
|
|
40a8a823b8 | ||
|
|
e3adb3fc71 | ||
|
|
a569a1d147 | ||
|
|
64718b77ca | ||
|
|
7b34c39736 | ||
|
|
4d6ab60793 | ||
|
|
35bffee544 | ||
|
|
16917fd79b | ||
|
|
895c116c01 | ||
|
|
e67151f7b9 | ||
|
|
8d26ec1760 | ||
|
|
7a9062b629 | ||
|
|
de07454a0a | ||
|
|
6fe60f61cf | ||
|
|
3fa74847e4 | ||
|
|
fc37140b52 | ||
|
|
83406c61f3 | ||
|
|
6d736e7e80 | ||
|
|
7b6cec4100 | ||
|
|
e21a6516b5 | ||
|
|
6ffe8ea5f6 | ||
|
|
0a2fefd141 | ||
|
|
0c885d05b6 | ||
|
|
58d85b117a | ||
|
|
ad58d7b6e9 | ||
|
|
7a63cb9642 | ||
|
|
196b98da36 | ||
|
|
42acbe95b8 | ||
|
|
b6b065e365 | ||
|
|
4b1955b189 | ||
|
|
ef7ef8b843 | ||
|
|
38c1367322 | ||
|
|
8e72c086fd | ||
|
|
c454b1339d | ||
|
|
d1b2d43e5b | ||
|
|
da98ca0f1c | ||
|
|
1953540d08 | ||
|
|
be31b9ce21 | ||
|
|
169b4016e6 | ||
|
|
2e55028a1b | ||
|
|
1d228231f2 | ||
|
|
affb926450 | ||
|
|
c7f65e929f | ||
|
|
ba4ff493e8 | ||
|
|
eb08803e2a | ||
|
|
bbc9486f0e | ||
|
|
999d709350 | ||
|
|
0b1a330cc2 | ||
|
|
995b7cf50d | ||
|
|
bc290fe59f | ||
|
|
5477b13233 | ||
|
|
d6170e5efb | ||
|
|
18fe117363 | ||
|
|
33a868acc2 | ||
|
|
11372d35e1 | ||
|
|
b7508b2b43 | ||
|
|
183817b769 | ||
|
|
591e53e9be | ||
|
|
a6a6415e31 | ||
|
|
0060ead876 | ||
|
|
224e41d3ad | ||
|
|
b3323007b2 | ||
|
|
3e950bc66f | ||
|
|
9503b46b21 | ||
|
|
a2cec323a2 | ||
|
|
4239f4d27f | ||
|
|
8ac8264997 | ||
|
|
544a53ae9c | ||
|
|
89e18482ed | ||
|
|
a8217b5a32 | ||
|
|
bdd5de5628 | ||
|
|
61d8bfd0d1 | ||
|
|
b8d79c7fc2 | ||
|
|
fb25ab028b | ||
|
|
5b136ecaf0 | ||
|
|
d4733dbb0a | ||
|
|
bfb30251e6 | ||
|
|
33115f76b7 | ||
|
|
9e9208e699 | ||
|
|
6b3fd57174 | ||
|
|
3be5237cf6 | ||
|
|
368f80eaae | ||
|
|
4d7079534c | ||
|
|
7d4cf1c551 | ||
|
|
ea088b95e9 | ||
|
|
a7a37f5320 | ||
|
|
8bda4880a7 | ||
|
|
6eb83618c0 | ||
|
|
1fe3833779 | ||
|
|
e63f5c966e | ||
|
|
69241183ac | ||
|
|
0a7c65cd27 | ||
|
|
bd13eb3e23 | ||
|
|
1e5191a16c | ||
|
|
1e4bf0dd4e | ||
|
|
4d66dc59aa | ||
|
|
9a442c15e9 | ||
|
|
157af90a56 | ||
|
|
24b94965d8 | ||
|
|
46bcad9267 | ||
|
|
1aba0577dc | ||
|
|
383088af2d | ||
|
|
c3456c1f0c | ||
|
|
183de9209f | ||
|
|
1df5c5ff60 | ||
|
|
e55a3b4fc6 | ||
|
|
6ee4657da3 | ||
|
|
7294d8bcbe | ||
|
|
3fec5aa5b3 | ||
|
|
63e55b8631 | ||
|
|
dd771f8dd9 | ||
|
|
d31a4cc7d8 | ||
|
|
73d4cf51af | ||
|
|
d015218226 | ||
|
|
f50475fcfd | ||
|
|
ae5efd9e2f | ||
|
|
c2c2874e82 | ||
|
|
1f8c2a3722 | ||
|
|
50aa7eb0cf | ||
|
|
920b39a124 | ||
|
|
c159ef79d2 | ||
|
|
7a95b169c1 | ||
|
|
0c3fd40120 | ||
|
|
7268697dc5 | ||
|
|
ec395bada4 | ||
|
|
757552671c | ||
|
|
04b52d5bee | ||
|
|
ffeb8b892a | ||
|
|
26806b5750 | ||
|
|
6e70054566 | ||
|
|
93268e8592 | ||
|
|
a420c6ca25 | ||
|
|
69fd13a76f | ||
|
|
ab3f262c22 | ||
|
|
aabbe0dfac | ||
|
|
35cb99a3a2 | ||
|
|
90e6d77e26 | ||
|
|
5fb4751bd8 | ||
|
|
03640e44a4 | ||
|
|
51fd60917e | ||
|
|
2d7e659953 | ||
|
|
c638df8ed9 | ||
|
|
ec269a48f3 | ||
|
|
fc4c9287cb | ||
|
|
fee62373a9 | ||
|
|
f075b339b5 | ||
|
|
872a622f71 | ||
|
|
4a41c4cefb | ||
|
|
a5cd36e845 | ||
|
|
0dd6c08e33 | ||
|
|
dc0b7fc3bf | ||
|
|
0d4bbbd17e | ||
|
|
90797ffa7d | ||
|
|
7f2bd809d6 | ||
|
|
da7ff9a40a | ||
|
|
410d0d0532 | ||
|
|
a25d983c87 | ||
|
|
3953fa4047 | ||
|
|
ea93cb9987 | ||
|
|
c13278f3c2 | ||
|
|
66fdf937e3 | ||
|
|
843f55f844 | ||
|
|
5a5633d779 | ||
|
|
8310433342 | ||
|
|
51141772b3 | ||
|
|
58b88e874f | ||
|
|
5fb616efb4 | ||
|
|
6c6afd6f4b | ||
|
|
4010953041 | ||
|
|
6b74c66292 | ||
|
|
fd35adbc3e | ||
|
|
f86b0ec3da | ||
|
|
5f6e0540cd | ||
|
|
c7ec9a9715 | ||
|
|
841e9135fe | ||
|
|
9299cd9666 | ||
|
|
9851993b82 | ||
|
|
55d1807f07 | ||
|
|
ee0abdc7f4 | ||
|
|
6c9ab63842 | ||
|
|
d70db5af79 | ||
|
|
eae858dec6 | ||
|
|
2ec035a1cb | ||
|
|
bd6c227bbe | ||
|
|
02f3474a58 | ||
|
|
c838e08d77 | ||
|
|
346e3d816a | ||
|
|
a15959fad2 | ||
|
|
0e0c2ead1f | ||
|
|
c42381d810 | ||
|
|
587ce7258a | ||
|
|
d0bb804843 | ||
|
|
410eecb988 | ||
|
|
98136142b4 | ||
|
|
37da9fb3e4 | ||
|
|
4566ad9789 | ||
|
|
6faacc7dde | ||
|
|
0d088cac7e | ||
|
|
3ffad3f97f | ||
|
|
3d0c281fd6 | ||
|
|
5b399fd447 | ||
|
|
bfedc93f2c | ||
|
|
c63e706fe9 | ||
|
|
dfc241c62d | ||
|
|
6fcdc05911 | ||
|
|
137f22b39e | ||
|
|
9857a395f1 | ||
|
|
4302b06715 | ||
|
|
064bc43f27 | ||
|
|
7ab1b6823f | ||
|
|
f96a487bc3 | ||
|
|
e6a1953785 | ||
|
|
4bedb380b8 | ||
|
|
3b070ae1f3 | ||
|
|
bdb0a97285 | ||
|
|
a0cce07259 | ||
|
|
1edf576702 | ||
|
|
7824ee99cb | ||
|
|
13c20242ad | ||
|
|
ba81b1ae12 | ||
|
|
7eb54b6564 | ||
|
|
5c007edd9f | ||
|
|
03ba598842 | ||
|
|
d699f0b66a | ||
|
|
65e599b92a | ||
|
|
788f8beea4 | ||
|
|
da8768c4c0 | ||
|
|
eb11054f65 | ||
|
|
5922b4617c | ||
|
|
5286994288 | ||
|
|
8ad025b462 | ||
|
|
4442cb2fe0 | ||
|
|
460c6d4fc9 | ||
|
|
893fa47a50 | ||
|
|
62d3a18783 | ||
|
|
ac1cd5114a | ||
|
|
e0b5855013 | ||
|
|
4fd057413f | ||
|
|
96fcc41b19 | ||
|
|
811b994d57 | ||
|
|
87aa62e128 | ||
|
|
612275477a | ||
|
|
046cfcb4d7 | ||
|
|
965d41b37d | ||
|
|
c36935d81a | ||
|
|
ae4197277a | ||
|
|
0cc7bd7066 | ||
|
|
31f77f7a56 | ||
|
|
d3d56e83e7 | ||
|
|
c725fc9fa3 | ||
|
|
7d6d5967dc | ||
|
|
afcc5d9e26 | ||
|
|
0e9931d749 | ||
|
|
b4d1f07ed9 | ||
|
|
0d182f4431 | ||
|
|
20a6a5684d | ||
|
|
e5967bb0de | ||
|
|
dcd7b6a8a2 | ||
|
|
a48451cc8f | ||
|
|
7580475cb6 | ||
|
|
ebe7a8ed0b | ||
|
|
c7cee905d1 | ||
|
|
5b7925c079 | ||
|
|
5e56f746ad | ||
|
|
4f5abe32f9 | ||
|
|
64a0dcb37f | ||
|
|
422f3da9df | ||
|
|
f7e20f70a8 | ||
|
|
cbabcd91ed | ||
|
|
5221d34448 | ||
|
|
5ab3c86b68 | ||
|
|
0324f68709 | ||
|
|
7cd14a5959 | ||
|
|
a6584023ae | ||
|
|
2f381bf677 | ||
|
|
a1c29f8aed | ||
|
|
42eb8e7a05 | ||
|
|
890cd47b2a | ||
|
|
cf4622052d | ||
|
|
88bf893228 | ||
|
|
0b1f711f2c | ||
|
|
39d9f44286 | ||
|
|
f91aca959f | ||
|
|
8af166b899 | ||
|
|
61a185f947 | ||
|
|
6ad2b0b124 | ||
|
|
b70c20b260 | ||
|
|
9821e39b06 | ||
|
|
2ac65b9c83 | ||
|
|
18dc042a0b | ||
|
|
7a4a940e83 | ||
|
|
df73169392 | ||
|
|
50e4b7a2f4 | ||
|
|
e836ff86b4 | ||
|
|
714bc58573 | ||
|
|
17c35c4259 | ||
|
|
727474055e | ||
|
|
892cb1baae | ||
|
|
186656999f | ||
|
|
00b7347d00 | ||
|
|
5eb6b703f0 | ||
|
|
3d436b3c6b | ||
|
|
d78dca47e2 | ||
|
|
8254d197f0 | ||
|
|
a4839f9cf2 | ||
|
|
bb4b43f5be | ||
|
|
4c7699b205 | ||
|
|
b7013dc795 | ||
|
|
617e4b0ce1 | ||
|
|
600d37682c | ||
|
|
ac4800a7df | ||
|
|
0af64dad01 | ||
|
|
a32a5151dc | ||
|
|
16d245b179 | ||
|
|
24ecdb227e | ||
|
|
867fa5140b | ||
|
|
891aac8381 | ||
|
|
2b616575e1 | ||
|
|
3f07f6ac79 | ||
|
|
c6b0b114c5 | ||
|
|
8803343ae1 | ||
|
|
4cfe866079 | ||
|
|
8609538756 | ||
|
|
5ea0e7776e | ||
|
|
a296b8a1fe | ||
|
|
ebbbdcaa59 | ||
|
|
ccf64d5951 | ||
|
|
16e20e159f | ||
|
|
43a5a5db5a | ||
|
|
c1686691fa | ||
|
|
115d0a05b7 | ||
|
|
4cfef1e21c | ||
|
|
42e3fea9e5 | ||
|
|
6b7530f27d | ||
|
|
7e00a08111 | ||
|
|
0c245f8eda | ||
|
|
9469968851 | ||
|
|
c2a71fb423 | ||
|
|
13d3bc9391 | ||
|
|
3161ab3903 | ||
|
|
6df67aee00 | ||
|
|
ed9f9c0d9d | ||
|
|
969c17e410 | ||
|
|
3c7c52e35a | ||
|
|
b0e327e0d1 | ||
|
|
550b374d20 | ||
|
|
23008d1f73 | ||
|
|
733d80d0b2 | ||
|
|
9dceddc6c2 | ||
|
|
f1747079c8 | ||
|
|
ffdcd9b41e | ||
|
|
2ef56aff70 | ||
|
|
2ca4abbfef | ||
|
|
1344466097 | ||
|
|
b1b68c514d | ||
|
|
08072b3850 | ||
|
|
67637007a2 | ||
|
|
558dd55058 | ||
|
|
63ccbd7ca7 | ||
|
|
d9f6b7e3fb | ||
|
|
254f9b9c5f | ||
|
|
44ff545436 | ||
|
|
40de60946a | ||
|
|
f25d17d9c2 | ||
|
|
70233b5e53 | ||
|
|
044d5f1c7d | ||
|
|
389c586a26 | ||
|
|
e01a79696b | ||
|
|
aade61b019 | ||
|
|
6956858d61 | ||
|
|
0343e4b91a | ||
|
|
137d505c3b | ||
|
|
d8c9508507 | ||
|
|
63fdc13928 | ||
|
|
9b0557803e | ||
|
|
c13879ce69 | ||
|
|
f57bc30c5a |
@@ -17,4 +17,4 @@ jobs:
|
||||
|
||||
- name: Build clan-app for x86_64-darwin
|
||||
run: |
|
||||
nix build .#packages.x86_64-darwin.clan-app --system x86_64-darwin --log-format bar-with-logs
|
||||
nix build .#packages.x86_64-darwin.clan-app --log-format bar-with-logs
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
clanServices/.* @pinpox @kenji
|
||||
|
||||
lib/test/container-test-driver/.* @DavHau @mic92
|
||||
lib/modules/inventory/.* @hsjobeki
|
||||
lib/modules/inventoryClass/.* @hsjobeki
|
||||
lib/inventory/.* @hsjobeki
|
||||
lib/inventoryClass/.* @hsjobeki
|
||||
|
||||
modules/.* @hsjobeki
|
||||
|
||||
pkgs/clan-app/ui/.* @hsjobeki @brianmcgee
|
||||
pkgs/clan-app/clan_app/.* @qubasa @hsjobeki
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2023-2024 Clan contributors
|
||||
Copyright 2023-2025 Clan contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
105
PLAN.md
Normal file
105
PLAN.md
Normal file
@@ -0,0 +1,105 @@
|
||||
Title: Add nix-darwin Support to Clan Services (clan.service)
|
||||
|
||||
Summary
|
||||
- Extend clan services so authors can ship a darwinModule alongside nixosModule.
|
||||
- Wire service results into darwin machines the same way we already do for NixOS.
|
||||
- Keep full backward compatibility: existing services that only export nixosModule continue to work unchanged.
|
||||
|
||||
Goals
|
||||
- Service authors can return perInstance/perMachine darwinModule similarly to nixosModule.
|
||||
- Darwin machines import the correct aggregated service module outputs.
|
||||
- Documentation describes the new result attribute and authoring pattern.
|
||||
|
||||
Non-Goals (initial phase)
|
||||
- No rework of service settings schema or UI beyond documenting darwinModule.
|
||||
- No OS-specific extraModules handling (we will keep extraModules affecting only nixos aggregation initially to avoid breaking existing users).
|
||||
- No sweeping updates of all services; we’ll add a concrete example (users) and leave others to be migrated incrementally.
|
||||
|
||||
Design Overview
|
||||
- Service result attributes gain darwinModule in both roles.<name>.perInstance and perMachine results.
|
||||
- The service aggregator composes both nixosModule and darwinModule per machine.
|
||||
- The machine wiring picks the correct module based on the machine’s class (nixos vs darwin).
|
||||
|
||||
Changes By File (with anchors)
|
||||
- lib/inventory/distributed-service/service-module.nix
|
||||
- Add darwinModule to per-instance return type next to nixosModule.
|
||||
- Where: lib/inventory/distributed-service/service-module.nix:536 (options.nixosModule = mkOption { … })
|
||||
- Action: Add sibling options.darwinModule = mkOption { type = types.deferredModule; default = { }; description = "A single nix-darwin module for the instance."; }.
|
||||
- Add darwinModule to per-machine return type next to nixosModule.
|
||||
- Where: lib/inventory/distributed-service/service-module.nix:666 (options.nixosModule = mkOption { … })
|
||||
- Action: Add sibling options.darwinModule = mkOption { type = types.deferredModule; default = { }; description = "A single nix-darwin module for the machine."; }.
|
||||
- Compose darwinModule per (role, instance, machine) similarly to nixosModule.
|
||||
- Where: lib/inventory/distributed-service/service-module.nix:878–893 (wrapper that builds nixosModule = { imports = [ instanceRes.nixosModule ] ++ extraModules … })
|
||||
- Action: Build darwinModule = { imports = [ instanceRes.darwinModule ]; }.
|
||||
Note: Do NOT include roles.*.extraModules here for darwin initially to avoid importing nixos-specific modules into darwin eval.
|
||||
- Aggregate darwinModules in final result.
|
||||
- Where: lib/inventory/distributed-service/service-module.nix:958–993 (instanceResults builder and final nixosModule = { imports = [ machineResult.nixosModule ] ++ instanceResults.nixosModules; })
|
||||
- Actions:
|
||||
- Track instanceResults.darwinModules in parallel to instanceResults.nixosModules.
|
||||
- Add final darwinModule = { imports = [ machineResult.darwinModule ] ++ instanceResults.darwinModules; }.
|
||||
|
||||
- modules/clan/distributed-services.nix
|
||||
- Feed the right service module to each machine based on machineClass.
|
||||
- Where: modules/clan/distributed-services.nix:147–152
|
||||
- Current: machineImports = fold over services, collecting serviceModule.result.final.${machineName}.nixosModule
|
||||
- Change: If inventory.machines.${machineName}.machineClass == "darwin" then collect .darwinModule else .nixosModule.
|
||||
|
||||
- modules/clan/module.nix
|
||||
- Ensure machineImports are included for both nixos and darwin machines.
|
||||
- Where: modules/clan/module.nix:195 (currently ++ lib.optionals (_class == "nixos") (v.machineImports or [ ]))
|
||||
- Change: Include machineImports for darwin as well (or remove the conditional and always append v.machineImports).
|
||||
|
||||
- docs/site/decisions/01-Clan-Modules.md
|
||||
- Document darwinModule as a result attribute.
|
||||
- Where: docs/site/decisions/01-Clan-Modules.md:129–146 (Result attributes and perMachine text mentioning only nixosModule)
|
||||
- Change: Add “darwinModule” to the Result attributes list and examples, mirroring nixosModule.
|
||||
|
||||
- Example service update: clanServices/users/default.nix
|
||||
- Add perInstance.darwinModule and perMachine.darwinModule mirroring nixos behavior where feasible.
|
||||
- Where: clanServices/users/default.nix:28–90 (roles.default.perInstance.nixosModule), 148–153 (perMachine.nixosModule)
|
||||
- Change: Provide minimal darwinModule that sets users.users.<name> (and any safe, cross-platform bits). If some nixos-only settings (e.g., systemd hooks) exist, keep them nixos-only.
|
||||
|
||||
Implementation Steps
|
||||
1) Service API extensions
|
||||
- Add options.darwinModule to roles.*.perInstance and perMachine (see anchors above).
|
||||
- Keep defaults to {} so services can omit it safely.
|
||||
|
||||
2) Aggregation logic
|
||||
- result.allRoles: emit darwinModule wrapper from instanceRes.darwinModule.
|
||||
- result.final:
|
||||
- Collect instanceResults.darwinModules alongside instanceResults.nixosModules.
|
||||
- Produce final darwinModule with [ machineResult.darwinModule ] ++ instanceResults.darwinModules.
|
||||
- Leave exports logic unchanged.
|
||||
|
||||
3) Machine wiring
|
||||
- modules/clan/distributed-services.nix: choose .darwinModule vs .nixosModule based on inventory.machines.<name>.machineClass.
|
||||
- modules/clan/module.nix: include v.machineImports for both OS classes.
|
||||
|
||||
4) Example migration (users)
|
||||
- Add darwinModule in clanServices/users/default.nix.
|
||||
- Validate that users service evaluates for a darwin machine and does not reference nixos-specific options.
|
||||
|
||||
5) Documentation
|
||||
- Update ADR docs to mention darwinModule in Result attributes and examples.
|
||||
- Add a short “Authoring for Darwin” snippet showing perInstance/perMachine returning both modules.
|
||||
|
||||
6) Tests and verification
|
||||
- Unit-level: extend lib/inventory/distributed-service/tests to assert presence of result.final.<machine>.darwinModule when perInstance/perMachine return it.
|
||||
- Integration-level: evaluate a sample darwin machine (e.g., inventory.json has test-darwin-machine) and assert clan.darwinModules.<machine> includes the aggregated module.
|
||||
- Sanity: ensure existing nixos-only services still evaluate unchanged.
|
||||
|
||||
Backward Compatibility
|
||||
- Existing services that only return nixosModule continue to work.
|
||||
- Darwin machines won’t import service modules until services provide darwinModule, avoiding accidental breakage.
|
||||
- extraModules remain applied only to nixos aggregation initially to prevent nixos-only modules from breaking darwin evaluation. We can add OS-specific extraModules in a follow-up (e.g., roles.*.extraModulesDarwin).
|
||||
|
||||
Acceptance Criteria
|
||||
- Services can return darwinModule in perInstance/perMachine without errors.
|
||||
- Darwin machines import aggregated darwinModule outputs from all participating services.
|
||||
- nixos behavior remains unchanged for existing services.
|
||||
- Documentation updated to reflect the new attribute and example.
|
||||
|
||||
Rollout Notes
|
||||
- Start by updating clanServices/users as a working example.
|
||||
- Encourage service authors to add darwinModule incrementally; no global migration is required.
|
||||
|
||||
@@ -86,11 +86,13 @@ in
|
||||
|
||||
# Container Tests
|
||||
nixos-test-container = self.clanLib.test.containerTest ./container nixosTestArgs;
|
||||
nixos-systemd-abstraction = self.clanLib.test.containerTest ./systemd-abstraction nixosTestArgs;
|
||||
nixos-llm-test = self.clanLib.test.containerTest ./llm nixosTestArgs;
|
||||
nixos-test-user-firewall-iptables = self.clanLib.test.containerTest ./user-firewall/iptables.nix nixosTestArgs;
|
||||
nixos-test-user-firewall-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs;
|
||||
nixos-test-extra-python-packages = self.clanLib.test.containerTest ./test-extra-python-packages nixosTestArgs;
|
||||
|
||||
service-dummy-test = import ./service-dummy-test nixosTestArgs;
|
||||
wireguard = import ./wireguard nixosTestArgs;
|
||||
service-dummy-test-from-flake = import ./service-dummy-test-from-flake nixosTestArgs;
|
||||
};
|
||||
|
||||
|
||||
@@ -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; };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
82
checks/llm/default.nix
Normal file
82
checks/llm/default.nix
Normal file
@@ -0,0 +1,82 @@
|
||||
{ self, pkgs, ... }:
|
||||
|
||||
let
|
||||
|
||||
cli = self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli-full;
|
||||
|
||||
ollama-model = pkgs.callPackage ./qwen3-4b-instruct.nix { };
|
||||
in
|
||||
{
|
||||
name = "llm";
|
||||
|
||||
nodes = {
|
||||
peer1 =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
|
||||
users.users.text-user = {
|
||||
isNormalUser = true;
|
||||
linger = true;
|
||||
uid = 1000;
|
||||
extraGroups = [ "systemd-journal" ];
|
||||
};
|
||||
|
||||
# Set environment variables for user systemd
|
||||
environment.extraInit = ''
|
||||
if [ "$(id -u)" = "1000" ]; then
|
||||
export XDG_RUNTIME_DIR="/run/user/1000"
|
||||
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
|
||||
|
||||
ollama_dir="$HOME/.ollama"
|
||||
mkdir -p "$ollama_dir"
|
||||
ln -sf ${ollama-model}/models "$ollama_dir"/models
|
||||
fi
|
||||
'';
|
||||
|
||||
# Enable PAM for user systemd sessions
|
||||
security.pam.services.systemd-user = {
|
||||
startSession = true;
|
||||
# Workaround for containers - use pam_permit to avoid helper binary issues
|
||||
text = pkgs.lib.mkForce ''
|
||||
account required pam_permit.so
|
||||
session required pam_permit.so
|
||||
session required pam_env.so conffile=/etc/pam/environment readenv=0
|
||||
session required ${pkgs.systemd}/lib/security/pam_systemd.so
|
||||
'';
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
cli
|
||||
pkgs.ollama
|
||||
(cli.pythonRuntime.withPackages (
|
||||
ps: with ps; [
|
||||
pytest
|
||||
pytest-xdist
|
||||
(cli.pythonRuntime.pkgs.toPythonModule cli)
|
||||
self.legacyPackages.${pkgs.stdenv.hostPlatform.system}.nixosTestLib
|
||||
]
|
||||
))
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ ... }:
|
||||
''
|
||||
start_all()
|
||||
|
||||
peer1.wait_for_unit("multi-user.target")
|
||||
peer1.wait_for_unit("user@1000.service")
|
||||
|
||||
# Fix user journal permissions so text-user can read their own logs
|
||||
peer1.succeed("chown text-user:systemd-journal /var/log/journal/*/user-1000.journal*")
|
||||
peer1.succeed("chmod 640 /var/log/journal/*/user-1000.journal*")
|
||||
# the -o adopts="" is needed to overwrite any args coming from pyproject.toml
|
||||
# -p no:cacheprovider disables pytest's cacheprovider which tries to write to the nix store in this case
|
||||
cmd = "su - text-user -c 'pytest -s -n0 -m service_runner -p no:cacheprovider -o addopts="" ${cli.passthru.sourceWithTests}/clan_lib/llm'"
|
||||
print("Running tests with command: " + cmd)
|
||||
|
||||
# Run tests as text-user (environment variables are set automatically)
|
||||
peer1.succeed(cmd)
|
||||
'';
|
||||
}
|
||||
70
checks/llm/qwen3-4b-instruct.nix
Normal file
70
checks/llm/qwen3-4b-instruct.nix
Normal file
@@ -0,0 +1,70 @@
|
||||
{ pkgs }:
|
||||
|
||||
let
|
||||
# Got them from https://github.com/Gholamrezadar/ollama-direct-downloader
|
||||
|
||||
# Download manifest
|
||||
manifest = pkgs.fetchurl {
|
||||
url = "https://registry.ollama.ai/v2/library/qwen3/manifests/4b-instruct";
|
||||
# You'll need to calculate this hash - run the derivation once and it will tell you the correct hash
|
||||
hash = "sha256-Dtze80WT6sGqK+nH0GxDLc+BlFrcpeyi8nZiwY8Wi6A=";
|
||||
};
|
||||
|
||||
# Download blobs
|
||||
blob1 = pkgs.fetchurl {
|
||||
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:b72accf9724e93698c57cbd3b1af2d3341b3d05ec2089d86d273d97964853cd2";
|
||||
hash = "sha256-tyrM+XJOk2mMV8vTsa8tM0Gz0F7CCJ2G0nPZeWSFPNI=";
|
||||
};
|
||||
|
||||
blob2 = pkgs.fetchurl {
|
||||
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:85e4a5b7b8ef0e48af0e8658f5aaab9c2324c76c1641493f4d1e25fce54b18b9";
|
||||
hash = "sha256-heSlt7jvDkivDoZY9aqrnCMkx2wWQUk/TR4l/OVLGLk=";
|
||||
};
|
||||
|
||||
blob3 = pkgs.fetchurl {
|
||||
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:eade0a07cac7712787bbce23d12f9306adb4781d873d1df6e16f7840fa37afec";
|
||||
hash = "sha256-6t4KB8rHcSeHu84j0S+TBq20eB2HPR324W94QPo3r+w=";
|
||||
};
|
||||
|
||||
blob4 = pkgs.fetchurl {
|
||||
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:d18a5cc71b84bc4af394a31116bd3932b42241de70c77d2b76d69a314ec8aa12";
|
||||
hash = "sha256-0YpcxxuEvErzlKMRFr05MrQiQd5wx30rdtaaMU7IqhI=";
|
||||
};
|
||||
|
||||
blob5 = pkgs.fetchurl {
|
||||
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:0914c7781e001948488d937994217538375b4fd8c1466c5e7a625221abd3ea7a";
|
||||
hash = "sha256-CRTHeB4AGUhIjZN5lCF1ODdbT9jBRmxeemJSIavT6no=";
|
||||
};
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "ollama-qwen3-4b-instruct";
|
||||
version = "1.0";
|
||||
|
||||
dontUnpack = true;
|
||||
|
||||
buildPhase = ''
|
||||
mkdir -p $out/models/manifests/registry.ollama.ai/library/qwen3
|
||||
mkdir -p $out/models/blobs
|
||||
|
||||
# Copy manifest
|
||||
cp ${manifest} $out/models/manifests/registry.ollama.ai/library/qwen3/4b-instruct
|
||||
|
||||
# Copy blobs with correct names
|
||||
cp ${blob1} $out/models/blobs/sha256-b72accf9724e93698c57cbd3b1af2d3341b3d05ec2089d86d273d97964853cd2
|
||||
cp ${blob2} $out/models/blobs/sha256-85e4a5b7b8ef0e48af0e8658f5aaab9c2324c76c1641493f4d1e25fce54b18b9
|
||||
cp ${blob3} $out/models/blobs/sha256-eade0a07cac7712787bbce23d12f9306adb4781d873d1df6e16f7840fa37afec
|
||||
cp ${blob4} $out/models/blobs/sha256-d18a5cc71b84bc4af394a31116bd3932b42241de70c77d2b76d69a314ec8aa12
|
||||
cp ${blob5} $out/models/blobs/sha256-0914c7781e001948488d937994217538375b4fd8c1466c5e7a625221abd3ea7a
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
# buildPhase already created everything in $out
|
||||
:
|
||||
'';
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Qwen3 4B Instruct model for Ollama";
|
||||
license = "apache-2.0";
|
||||
platforms = platforms.all;
|
||||
};
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
modules.new-service = {
|
||||
_class = "clan.service";
|
||||
manifest.name = "new-service";
|
||||
manifest.readme = "Just a sample readme to not trigger the warning.";
|
||||
roles.peer = {
|
||||
description = "A peer that uses the new-service to generate some files.";
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ nixosLib.runTest (
|
||||
modules.new-service = {
|
||||
_class = "clan.service";
|
||||
manifest.name = "new-service";
|
||||
manifest.readme = "Just a sample readme to not trigger the warning.";
|
||||
roles.peer = {
|
||||
description = "A peer that uses the new-service to generate some files.";
|
||||
};
|
||||
|
||||
67
checks/systemd-abstraction/default.nix
Normal file
67
checks/systemd-abstraction/default.nix
Normal file
@@ -0,0 +1,67 @@
|
||||
{ self, pkgs, ... }:
|
||||
|
||||
let
|
||||
|
||||
cli = self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli-full;
|
||||
in
|
||||
{
|
||||
name = "systemd-abstraction";
|
||||
|
||||
nodes = {
|
||||
peer1 = {
|
||||
|
||||
users.users.text-user = {
|
||||
isNormalUser = true;
|
||||
linger = true;
|
||||
uid = 1000;
|
||||
extraGroups = [ "systemd-journal" ];
|
||||
};
|
||||
|
||||
# Set environment variables for user systemd
|
||||
environment.extraInit = ''
|
||||
if [ "$(id -u)" = "1000" ]; then
|
||||
export XDG_RUNTIME_DIR="/run/user/1000"
|
||||
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
|
||||
fi
|
||||
'';
|
||||
|
||||
# Enable PAM for user systemd sessions
|
||||
security.pam.services.systemd-user = {
|
||||
startSession = true;
|
||||
# Workaround for containers - use pam_permit to avoid helper binary issues
|
||||
text = pkgs.lib.mkForce ''
|
||||
account required pam_permit.so
|
||||
session required pam_permit.so
|
||||
session required pam_env.so conffile=/etc/pam/environment readenv=0
|
||||
session required ${pkgs.systemd}/lib/security/pam_systemd.so
|
||||
'';
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
cli
|
||||
(cli.pythonRuntime.withPackages (
|
||||
ps: with ps; [
|
||||
pytest
|
||||
pytest-xdist
|
||||
]
|
||||
))
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ ... }:
|
||||
''
|
||||
start_all()
|
||||
|
||||
peer1.wait_for_unit("multi-user.target")
|
||||
peer1.wait_for_unit("user@1000.service")
|
||||
|
||||
# Fix user journal permissions so text-user can read their own logs
|
||||
peer1.succeed("chown text-user:systemd-journal /var/log/journal/*/user-1000.journal*")
|
||||
peer1.succeed("chmod 640 /var/log/journal/*/user-1000.journal*")
|
||||
|
||||
# Run tests as text-user (environment variables are set automatically)
|
||||
peer1.succeed("su - text-user -c 'pytest -p no:cacheprovider -o addopts="" -s -n0 ${cli.passthru.sourceWithTests}/clan_lib/service_runner'")
|
||||
'';
|
||||
}
|
||||
26
checks/test-extra-python-packages/default.nix
Normal file
26
checks/test-extra-python-packages/default.nix
Normal file
@@ -0,0 +1,26 @@
|
||||
(
|
||||
{ ... }:
|
||||
{
|
||||
name = "test-extra-python-packages";
|
||||
|
||||
extraPythonPackages = ps: [ ps.numpy ];
|
||||
|
||||
nodes.machine =
|
||||
{ ... }:
|
||||
{
|
||||
networking.hostName = "machine";
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import numpy as np
|
||||
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# Test availability of numpy
|
||||
arr = np.array([1, 2, 3])
|
||||
print(f"Numpy array: {arr}")
|
||||
assert len(arr) == 3
|
||||
'';
|
||||
}
|
||||
)
|
||||
@@ -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",
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
{
|
||||
pkgs,
|
||||
nixosLib,
|
||||
clan-core,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
nixosLib.runTest (
|
||||
{ ... }:
|
||||
|
||||
let
|
||||
machines = [
|
||||
"controller1"
|
||||
"controller2"
|
||||
"peer1"
|
||||
"peer2"
|
||||
"peer3"
|
||||
];
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
clan-core.modules.nixosTest.clanTest
|
||||
];
|
||||
|
||||
hostPkgs = pkgs;
|
||||
|
||||
name = "wireguard";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
modules."@clan/wireguard" = import ../../clanServices/wireguard/default.nix;
|
||||
inventory = {
|
||||
|
||||
machines = lib.genAttrs machines (_: { });
|
||||
|
||||
instances = {
|
||||
|
||||
/*
|
||||
wg-test-one
|
||||
┌───────────────────────────────┐
|
||||
│ ◄───────────── │
|
||||
│ controller2 controller1
|
||||
│ ▲ ─────────────► ▲ ▲
|
||||
│ │ │ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │ │ │
|
||||
│ │ │ │ └───────────────┐ │ │ │ │
|
||||
│ │ │ └──────────────┐ │ │ │ │ │
|
||||
│ ▼ │ ▼ ▼ ▼
|
||||
└─► peer2 │ peer1 peer3
|
||||
│ ▲
|
||||
└──────────┘
|
||||
*/
|
||||
|
||||
wg-test-one = {
|
||||
|
||||
module.name = "@clan/wireguard";
|
||||
module.input = "self";
|
||||
|
||||
roles.controller.machines."controller1".settings = {
|
||||
endpoint = "192.168.1.1";
|
||||
};
|
||||
|
||||
roles.controller.machines."controller2".settings = {
|
||||
endpoint = "192.168.1.2";
|
||||
};
|
||||
|
||||
roles.peer.machines = {
|
||||
peer1.settings.controller = "controller1";
|
||||
peer2.settings.controller = "controller2";
|
||||
peer3.settings.controller = "controller1";
|
||||
};
|
||||
};
|
||||
|
||||
# TODO: Will this actually work with conflicting ports? Can we re-use interfaces?
|
||||
#wg-test-two = {
|
||||
# module.name = "@clan/wireguard";
|
||||
|
||||
# roles.controller.machines."controller1".settings = {
|
||||
# endpoint = "192.168.1.1";
|
||||
# port = 51922;
|
||||
# };
|
||||
|
||||
# roles.peer.machines = {
|
||||
# peer1 = { };
|
||||
# };
|
||||
#};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
# Show all addresses
|
||||
machines = [peer1, peer2, peer3, controller1, controller2]
|
||||
for m in machines:
|
||||
m.systemctl("start network-online.target")
|
||||
|
||||
for m in machines:
|
||||
m.wait_for_unit("network-online.target")
|
||||
m.wait_for_unit("systemd-networkd.service")
|
||||
|
||||
print("\n\n" + "="*60)
|
||||
print("STARTING PING TESTS")
|
||||
print("="*60)
|
||||
|
||||
for m1 in machines:
|
||||
for m2 in machines:
|
||||
if m1 != m2:
|
||||
print(f"\n--- Pinging from {m1.name} to {m2.name}.wg-test-one ---")
|
||||
m1.wait_until_succeeds(f"ping -c1 {m2.name}.wg-test-one >&2")
|
||||
'';
|
||||
}
|
||||
)
|
||||
25
clanServices/admin/README.md
Normal file
25
clanServices/admin/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
The admin service aggregates components that allow an administrator to log in to and manage the machine.
|
||||
|
||||
The following configuration:
|
||||
|
||||
1. Enables OpenSSH with root login and adds an SSH public key named`myusersKey` to the machine's authorized_keys via the `allowedKeys` setting.
|
||||
|
||||
2. Automatically generates a password for the root user.
|
||||
|
||||
```nix
|
||||
instances = {
|
||||
admin = {
|
||||
roles.default.tags = {
|
||||
all = { };
|
||||
};
|
||||
roles.default.settings = {
|
||||
allowedKeys = {
|
||||
myusersKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEFDNnynMbFWatSFdANzbJ8iiEKL7+9ZpDaMLrWRQjyH lhebendanz@wintux";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
manifest.name = "clan-core/admin";
|
||||
manifest.description = "Adds a root user with ssh access";
|
||||
manifest.categories = [ "Utility" ];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.default = {
|
||||
description = "Placeholder role to apply the admin service";
|
||||
|
||||
@@ -2,7 +2,7 @@ let
|
||||
public-key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII6zj7ubTg6z/aDwRNwvM/WlQdUocMprQ8E92NWxl6t+ test@test";
|
||||
in
|
||||
{
|
||||
name = "service-admin";
|
||||
name = "admin";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -3,7 +3,7 @@
|
||||
...
|
||||
}:
|
||||
{
|
||||
name = "service-borgbackup";
|
||||
name = "borgbackup";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,13 +50,13 @@
|
||||
dns =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [ pkgs.nettools ];
|
||||
environment.systemPackages = [ pkgs.net-tools ];
|
||||
};
|
||||
|
||||
client =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [ pkgs.nettools ];
|
||||
environment.systemPackages = [ pkgs.net-tools ];
|
||||
};
|
||||
|
||||
server01 = {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{ ... }:
|
||||
{
|
||||
clanLib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
sharedInterface =
|
||||
{ lib, ... }:
|
||||
@@ -51,15 +54,15 @@ let
|
||||
builtins.foldl' (
|
||||
urls: name:
|
||||
let
|
||||
ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value";
|
||||
ip = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
machine = name;
|
||||
generator = "zerotier";
|
||||
file = "zerotier-ip";
|
||||
default = null;
|
||||
};
|
||||
in
|
||||
if builtins.pathExists ipPath then
|
||||
let
|
||||
ip = builtins.readFile ipPath;
|
||||
in
|
||||
urls ++ [ "[${ip}]:${builtins.toString settings.network.port}" ]
|
||||
else
|
||||
urls
|
||||
if ip != null then urls ++ [ "[${ip}]:${builtins.toString settings.network.port}" ] else urls
|
||||
) [ ] (builtins.attrNames ((roles.admin.machines or { }) // (roles.signer.machines or { })))
|
||||
);
|
||||
|
||||
@@ -156,9 +159,14 @@ in
|
||||
readHostKey =
|
||||
machine:
|
||||
let
|
||||
path = "${config.clan.core.settings.directory}/vars/per-machine/${machine}/data-mesher-host-key/public_key/value";
|
||||
publicKey = clanLib.vars.getPublicValue {
|
||||
flake = config.clan.core.settings.directory;
|
||||
inherit machine;
|
||||
generator = "data-mesher-host-key";
|
||||
file = "public_key";
|
||||
};
|
||||
in
|
||||
builtins.elemAt (lib.splitString "\n" (builtins.readFile path)) 1;
|
||||
builtins.elemAt (lib.splitString "\n" publicKey) 1;
|
||||
in
|
||||
{
|
||||
enable = true;
|
||||
|
||||
@@ -9,7 +9,7 @@ in
|
||||
perSystem =
|
||||
{ ... }:
|
||||
{
|
||||
clan.nixosTests.service-data-mesher = {
|
||||
clan.nixosTests.data-mesher = {
|
||||
imports = [ ./tests/vm/default.nix ];
|
||||
clan.modules."@clan/data-mesher" = module;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
...
|
||||
}:
|
||||
{
|
||||
name = "service-data-mesher";
|
||||
name = "data-mesher";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -3,7 +3,7 @@
|
||||
...
|
||||
}:
|
||||
{
|
||||
name = "service-dyndns";
|
||||
name = "dyndns";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
6
clanServices/dyndns/tests/vm/sops/machines/server/key.json
Executable file
6
clanServices/dyndns/tests/vm/sops/machines/server/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age164wrhlnake7f7duhzs936lq6w49dtg53hcdyxqwxj0agad6tqg2s2u4yta",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:seLxbv590dO0KvMJmtN7WVvUcH27VYwAc3rmyD7q6ZmwCgswOKx55LFnh0stRDKSZa8K7Dq1x7D9adhZtPAMWX8tbJswBeNMPt8=,iv:G52eugxfTi0tTzH4EN4CWmpyv6feSL34++UVSjb0aAo=,tag:6r10/a7kD2hBAmae0nz2OQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHVC8wZUZJYUl5MXVNa2k5\ndGV1MnFWbUNLNVdxeEtCVUc3MTd0ck9aeFFBCnFhZW40amVYc3FlN1FPRTFSWTJR\nQzhNOERKbnRnSlJVeElNSEM5ZUJsZGsKLS0tIG1uNnlNN3MweHlYczNRTW9xSytu\neThzUmxKZTJBT2lCcTdiNUI4N3paTVEKgS9j2/GVt1KBoggUj9d6UK/mIlK4niLQ\nzVq2BHt3irxQpkpGUogXH2b86zSAOEJFzsL1Rk8HM1mogTG8jqf0qA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-10-19T12:49:11Z",
|
||||
"mac": "ENC[AES256_GCM,data:T/2xw2mvUi8YALyxz78qG/g/xguoUTeHNzcZfXwwSyCXMg9ircsGGLO9SOVWy/QNkibnw3Yp80tXNJyr4oJH28PhFH7RrRp8jzNdopF49ZNJb2IqJ3C7xNYRZMHfjOCd/raka+ehZq8YGilEpXUWLRk1ere9lbBMh1ycL7jJS3c=,iv:FZbY/jTNPM+p4qD41FD0K7B9zoppGuvnUY5hL/EkmYM=,tag:IF5QTyUkHXWthlAGBn9R8w==,type:str]",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../sops/machines/server
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:Zu+n+DDYP7rQRTS17PJ6Apo=,iv:5WOs81Pj+S85kdC1AlOXSyPMGDfwM5UD8x7nyRZtRYQ=,tag:2JYkGnLugAni49Upv43o2g==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age164wrhlnake7f7duhzs936lq6w49dtg53hcdyxqwxj0agad6tqg2s2u4yta",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlR3RGQ2ZLTkR3ZWxNVCsv\naXJHRjBiVUVYZVRIY2swY2xubGhmb3pLRkNvCldhQUV2WDlqYjZ4ZUFWYXkvUEEw\nZi9XRWw0Mi9mRENDcnI0aENDR2Z4MHcKLS0tIGFQU3Q4WEErbnBjOHpNR1BSR2cr\nRFg0anE1cHExT0sySmxuUks1R05nczAKZO3R6+f9co2+YGO8HPufoq1fLqqrdTWD\n4zqemMmG2BjMRDumxtcKp8CLaZWlJoP4e/+tonfdoe42qmNF5NJcFw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzZWo4WGh1cWxKeDhDdlBm\nTVFjVFBIUU9xaGRkanNHaUVUUHN1czNRSUhNCkp5MmwzSGdycmsrZGhaRUhEbXBF\nNUhtdEF6bHZQOGJYUVhFVHlYc3FPODAKLS0tIDBRQ2VGT2IvU1F4MEVabzhYSFJq\nOWZmbGpkQmNSMnNKa0s4K2JXdGgwRlkKUQRREpG5H1mNHSc/cZrdMiSz0veJFR4N\n+W49XL/wQUZwajykwYj++G+dWDO7DQ+fpbB9w4mzbsAmCsXirseTLA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-10-19T12:49:11Z",
|
||||
"mac": "ENC[AES256_GCM,data:0msda7WbQQxXQ+juT7yErgT7NADgnzqEZLTQw+4JPuAE4xcqRIYwrrAALaA0GCCM2aIWlICzJigLCuzQUfSUbIzeP79tEHiKez+NOt/xgSM9ljz7GlsmLd0vzkxdt3WSxP+sHxy0S866N2sLMUkLqPGdqeTjB+Jji5ghGhzk9ys=,iv:8UU7iA4SdR6ZlVolm708l2Iea0sQYRT+5wPBBP5tpS0=,tag:VQXslAlqLqs1QEkwW6x6qg==,type:str]",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../sops/users/admin
|
||||
12
clanServices/garage/README.md
Normal file
12
clanServices/garage/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
[Garage](https://garagehq.deuxfleurs.fr/) is an open-source, S3-compatible distributed object storage service for self-hosting.
|
||||
|
||||
This module provisions a single-instance S3 bucket. To customize its behavior, set `services.garage.settings` in your Nix configuration.
|
||||
|
||||
Example configuration:
|
||||
```
|
||||
instances = {
|
||||
garage = {
|
||||
roles.default.machines."server" = {};
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -4,6 +4,7 @@
|
||||
manifest.name = "clan-core/garage";
|
||||
manifest.description = "S3-compatible object store for small self-hosted geo-distributed deployments";
|
||||
manifest.categories = [ "System" ];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.default = {
|
||||
description = "Placeholder role to apply the garage service";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
...
|
||||
}:
|
||||
{
|
||||
name = "service-garage";
|
||||
name = "garage";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
83
clanServices/hello-world/README.md
Normal file
83
clanServices/hello-world/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
!!! 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;
|
||||
}
|
||||
)
|
||||
```
|
||||
@@ -8,7 +8,8 @@
|
||||
{
|
||||
_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
|
||||
# defined in this file directly (e.g. the "morning" role) or split up into a
|
||||
@@ -34,10 +35,13 @@
|
||||
settings,
|
||||
|
||||
# The name of this instance of the service
|
||||
instanceName,
|
||||
|
||||
# The current machine
|
||||
machine,
|
||||
|
||||
# All roles of this service, with their assigned machines
|
||||
roles,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
name = "service-hello-service";
|
||||
name = "hello-service";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
24
clanServices/internet/README.md
Normal file
24
clanServices/internet/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
!!! Danger "Experimental"
|
||||
This service is experimental and will change in the future.
|
||||
|
||||
---
|
||||
|
||||
This module is part of Clan's [networking interface](https://docs.clan.lol/guides/networking/networking/).
|
||||
|
||||
Clan's networking module automatically manages connections across available network transports and falls back intelligently. When you run `clan ssh` or `clan machines update`, Clan attempts each configured network in priority order until a connection succeeds.
|
||||
|
||||
The example below shows how to configure a domain so server1 is reachable over the clearnet. By default, the `internet` module has the highest priority among networks.
|
||||
|
||||
```nix
|
||||
inventory.instances = {
|
||||
# Direct SSH with fallback support
|
||||
internet = {
|
||||
roles.default.machines.server1 = {
|
||||
settings.host = "server1.example.com";
|
||||
};
|
||||
roles.default.machines.server2 = {
|
||||
settings.host = "192.168.1.100";
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -7,6 +7,7 @@
|
||||
"System"
|
||||
"Network"
|
||||
];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
roles.default = {
|
||||
description = "Placeholder role to apply the internet service";
|
||||
interface =
|
||||
@@ -15,6 +16,7 @@
|
||||
options = {
|
||||
host = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
ip address or hostname (domain) of the machine
|
||||
'';
|
||||
|
||||
28
clanServices/kde/README.md
Normal file
28
clanServices/kde/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
This module sets up the [KDE Plasma](https://kde.org) Desktop environment.
|
||||
|
||||
!!! Note "Customisation"
|
||||
This service intentionally does not provide any settings or customisation
|
||||
options, as desktop preferences are highly subjective. Clan currently
|
||||
supports only this default desktop configuration. Any additional
|
||||
customisation can be done via the `extraModules` option. Furthermore, if you
|
||||
want to use a different desktop environment or compositor (e.g. Gnome or
|
||||
sway), we encourage you to to build your own
|
||||
[Clan Service](https://docs.clan.lol/guides/services/community/) or have a
|
||||
look at the [Community Services](https://docs.clan.lol/services/community/).
|
||||
|
||||
## Example Usage
|
||||
|
||||
```nix
|
||||
inventory = {
|
||||
instances = {
|
||||
kde = {
|
||||
|
||||
# Deploy on all machines
|
||||
roles.default.tags.all = { };
|
||||
|
||||
# Or individual hosts
|
||||
roles.default.machines.laptop = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
19
clanServices/kde/default.nix
Normal file
19
clanServices/kde/default.nix
Normal file
@@ -0,0 +1,19 @@
|
||||
{ ... }:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/kde";
|
||||
manifest.description = "Sets up a graphical desktop environment";
|
||||
manifest.categories = [ "Desktop" ];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.default = {
|
||||
description = "KDE/Plasma (wayland): Full-featured desktop environment with modern Qt-based interface";
|
||||
perInstance.nixosModule = {
|
||||
services = {
|
||||
displayManager.sddm.enable = true;
|
||||
displayManager.sddm.wayland.enable = true;
|
||||
desktopManager.plasma6.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
24
clanServices/kde/flake-module.nix
Normal file
24
clanServices/kde/flake-module.nix
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
self,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix {
|
||||
inherit (self) packages;
|
||||
};
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
kde = module;
|
||||
};
|
||||
perSystem =
|
||||
{ ... }:
|
||||
{
|
||||
clan.nixosTests.kde = {
|
||||
imports = [ ./tests/vm/default.nix ];
|
||||
|
||||
clan.modules.kde = module;
|
||||
};
|
||||
};
|
||||
}
|
||||
30
clanServices/kde/tests/vm/default.nix
Normal file
30
clanServices/kde/tests/vm/default.nix
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
name = "kde";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
|
||||
machines.client = { };
|
||||
|
||||
instances = {
|
||||
kde = {
|
||||
module.name = "kde";
|
||||
module.input = "self";
|
||||
roles.default.machines."client" = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
client.systemctl("start network-online.target")
|
||||
client.wait_for_unit("network-online.target")
|
||||
|
||||
client.wait_for_unit("graphical.target")
|
||||
client.wait_for_unit("display-manager.service")
|
||||
client.succeed("systemctl status display-manager.service")
|
||||
'';
|
||||
}
|
||||
4
clanServices/kde/tests/vm/sops/users/admin/key.json
Normal file
4
clanServices/kde/tests/vm/sops/users/admin/key.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"type": "age"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -1,6 +1,6 @@
|
||||
{ ... }:
|
||||
{
|
||||
name = "service-localbackup";
|
||||
name = "localbackup";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"type": "age"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
23
clanServices/matrix-synapse/README.md
Normal file
23
clanServices/matrix-synapse/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
This NixOS module installs and configures Synapse — a federated Matrix homeserver with end-to-end encryption — and optionally provides the Element web client.
|
||||
|
||||
The example below demonstrates a minimal setup that includes:
|
||||
|
||||
- Element web client.
|
||||
- Synapse backed by PostgreSQL and nginx.
|
||||
- An admin user and an additional regular user.
|
||||
|
||||
Example configuration:
|
||||
|
||||
```nix
|
||||
instances = {
|
||||
matrix-synapse = {
|
||||
roles.default.machines."jon".settings = {
|
||||
acmeEmail = "admins@clan.lol";
|
||||
server_tld = "clan.test";
|
||||
app_domain = "matrix.clan.test";
|
||||
users.admin.admin = true;
|
||||
users.someuser = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -4,6 +4,7 @@
|
||||
manifest.name = "clan-core/matrix-synapese";
|
||||
manifest.description = "A federated messaging server with end-to-end encryption.";
|
||||
manifest.categories = [ "Social" ];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.default = {
|
||||
description = "Placeholder role to apply the matrix-synapse service";
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
!!! Danger "Experimental"
|
||||
This service is experimental and will change in the future.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
|
||||
@@ -44,8 +44,10 @@
|
||||
pkgs.openssl
|
||||
];
|
||||
|
||||
# TODO: Implement automated certificate rotation instead of using a 100-year expiration
|
||||
script = ''
|
||||
openssl req -x509 -nodes -newkey rsa:4096 \
|
||||
-days 36500 \
|
||||
-keyout "$out"/key \
|
||||
-out "$out"/crt \
|
||||
-subj "/C=US/ST=CA/L=San Francisco/O=Example Corp/OU=IT/CN=example.com"
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
k1PNWTkgLsXO8DNkCudxcvPElXfmaw6zwaLrZys=
|
||||
VQQDDAtleGFtcGxlLmNvbTAgFw0yNTEwMjExMzE3MTZaGA8yMTI1MDkyNzEzMTcx
|
||||
NlowbDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh
|
||||
bmNpc2NvMRUwEwYDVQQKDAxFeGFtcGxlIENvcnAxCzAJBgNVBAsMAklUMRQwEgYD
|
||||
VQQDDAtleGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
|
||||
AMbUCTs38JdEFlz+fiEwsEb9OV+6u4P5pkKkRFIJ04sTW9/NIeUJx5xOcAPn6B8K
|
||||
mi+d6vHln2WDCNJHqthGHQDS250x8Qs+JrmtIvDPko+oDOlbWMPiT4Lv6p134+lV
|
||||
obkiEMKSKz1gHuhlnHXFjkU+xTjxvEtGuq1+JPem4oJ9HUhSk1F6cftigzrYqUuk
|
||||
JRROiUrbKiFp/TLedmAqQg/7wOrJKSKX91pQwNZhjB2/1REt0HP92W8uZIrzvLqq
|
||||
JkrGfK9Y6e87DwXoTT0lvMAT7jbMsMWdGoCw/BQV8CwciUUG4ggI/jb+2TTktB3f
|
||||
kMN/qRTKZ3zv/rn68RJfecAXYCQ2VfvO/Mr9nml2/cM7nrUBcs12YAHcm3766VWJ
|
||||
pq6qBLcz/pHzMdt+/23nbO7bH2PL6r69VCSYvsDDnqpVL+LnYhgYUE0lPjuWuGmp
|
||||
oKjggS6p4p1PXEQMOcj9UWdOyjefSzJsOp+25Of9SQzxHkBsVw0iArRFUYP6G15k
|
||||
kNjYpuinFTw1XVDCFGPRIAhySnERlkv6WNyQQC87QTVJITKkz3R5cv4gwFG0kjAi
|
||||
Va4nIJs2CctcizuEaPlwnEFrZ99gcB7RYPSUQVGAbfkqt2bhy/xGr+Jlp4kqPfS5
|
||||
iPomwfcDwEnDbmcM8S2adPWtZ+oHskxZQmJ6+jhGgM73AgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBRHz2QAo1z8r9BewZro+HYv18AxTzAfBgNVHSMEGDAWgBRHz2QAo1z8r9Be
|
||||
wZro+HYv18AxTzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCz
|
||||
BTuZI7VymDWerWLfHMWyogoJWOkFB2yEpQe7J+LjS8yZmJg4CYpA4JJ+uM2sBm2Q
|
||||
yL6M57ZmSY6EFoYeYw3gRfwGC32qJHirhsWvrjUpRC5+4YT9P6fNmgm5aD27JZao
|
||||
bjyNA9Vy9SCL4JMeWET2w9VGNDaYQCs0x57HZioxYRMSD5vMVbirvCtqX7H3F/X+
|
||||
r/VHEqEae7tVtuAB2D2GdcFzslCRb9uomuVfLJNqR6Nz1Tw+2adyySijRMCDdpRl
|
||||
Pg9MBv4sevL6F4C1vUqUG1LXzcfHLFtrV1oUIEpJ0frxAgpdhSbnHiQa64cKX3N0
|
||||
CsS6VALipGFmxj01+jD0Vhhf4rjjTT5C3Ag4WTqI98Fu4RMW35eBstnt6UUWyJQO
|
||||
Q1skk+hg0ynfb3lO8OIZ4sDkmxDqAOQXeMMo1tU2YMgNA5Lv1FyO9Silc0VlkOiO
|
||||
ft1RC8UbECqYyTvz7SNrv8aQP6EUoNSpxQHyBHOQy65dyOLOdP4S+PccUwsdxv/N
|
||||
O5eN9ndMWqNvnyPKyQ3M+MLVvkCR1vDb6ABgPhH17BLkj8fWQgy5lhjJy5a8VHlO
|
||||
1VDzV1Xeezy/MYCpS+TamaWTXscbhLMzWWiiAiDT8dltKw4G6U+g7DiF80kM59L5
|
||||
D1hOs4gOQ853+83L/Ej4ESTj0B04NLVMlzMGtl3qcA==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data: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,iv:q3j8EAokyyxiszf+wyRqxEr2igaD1bX7YnFx/NbsGg8=,tag:HKKYWRJEUwW2/TxL+5dSng==,type:str]",
|
||||
"data": "ENC[AES256_GCM,data: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,iv:dGSmyDNBdVyF54bYS/Zxm2NNXZyGtLjkyYlrI9/nKvc=,tag:ip2fy76NjObWbW20HyuZUA==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaeXRjU214aWk5ajl1aW9E\naGJlb1ViaVRmMTBHdkFDQUNDZS94WFZiNUNvCllmWTJBck9hR3U3V09VWDZwQ2xI\nd3ZEQnBIUG5ZSTVIdS8rQ2FMYVhyNk0KLS0tIEE1UG8rSzFyU01sVXhGVHpoaE9i\nSis4Qi9tMGFqbTNMTDZUVk1ZdXkrM28Km4VkfaOsZ69ckjvrg+os43H/O1IoWHzC\nt4LqZRz1Tk7/d1aLWavSPPjVYrCOMZeNBqGbQpGfjjuXrafClRNQdQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNQS96MUFubVdOR2VCc2xO\ncTc5QnNHNTFpdURnSnF3dVhBQXQ3bnBuRW1RCngzSVlhSW9rNUxoSWdKcEtKVXc3\nQitLZ2NDUXBSUmxtVWpYRUlvOHVXcW8KLS0tIGZaWlRVak9NYmt2elpwYStYenRE\nanlkT3BET1FjQ2lFZkp3SXFMSkJSaVkKKkr+MNNqs6Ve3K5OrZfBEGlnc7OAthqf\nOZrP9NYOTMgkvhFsZTVpUS0zskry0iwmTNt+KeluYf0Tko8K53Kx2A==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3R1RHTGViTnRLVVkyM3J0\nbm96cGVPTlo4NXBNL0g1eEVSNG9DUkgwVFRBCmRKVTlMRmV3Tmg2RTZIclBlWlcr\ndzI5MUxhcllzbE1IMDNxa08zVkpITmsKLS0tIG01Y2dyQkY3UmRudFk2d0p6bThn\nemlaWnZoS3p4VHhMTFFwTm9VN0ttYzQKVbLFgtK6NIRIiryWHeeOPD45iwUds4QD\n7b8xYYoxlo+DETggxK6Vz3IdT/BSK5bFtgAxl864b5gW+Aw4c6AO5w==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXd2dVYmJIbUVVeXk5Nk1E\nekFiUldVVUhRTmE4dHRiTHNDdEMyS1pRV1RrCkNScGdXVSs4UU5id29DV0pZWDQr\nenV1QmpnOFk5aFpTTUxmb0hDVHZDdFkKLS0tIHpmalJtRC94bEhaUStmeUlHT21w\nd3o3UzJHZklxK0RCYUUxc2c3aG1XclkKEPq1ZgyGiAK/Hy4zT7wfdDfPEE3vMHpR\nzwQV5y3M3DmlnKQEvJu0DpQ334CyAcubZC7cswQdUrM8TPqJhb/TuA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-09-18T14:33:37Z",
|
||||
"mac": "ENC[AES256_GCM,data:XKCnd0QrAlOCECSeSvbLYHMLbmUh4fMRnLaTb5ARoP4Zc9joWGsCaRZxokc2/sG4BXA/6pkbQXHyIOudKbcBpVjjvs9E+6Mnzt53nfRoH/iOkYPbN2EO49okVZJXW0M1rlBxrxvGuiDlz2p2p6L7neKLy4EB482pYea5+dUr2Yw=,iv:oj/MkZCfkvCmAb79uzEvKwEAm1bKtWhS4rPRAWSgRgw=,tag:h5TPPILXkhJplnDT2Gqtfw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
"lastmodified": "2025-10-21T13:17:17Z",
|
||||
"mac": "ENC[AES256_GCM,data:wdAFURkJZvclbz3UFPSPV9fma7zrZVEhMhsRqylGQMLepX/WohEAr8nJgeHl05be1Q8M8biPXCCoL0vfwg4BRZOkhD8PusJh8iBI3+STNQe/S1qoIK1ByfBFhJD+tIsVsgduLp6G32e6SRNvkuX3UpJqyViuRUavfQd3b8LRU4I=,iv:S3sMNTz5Kg4TxHj1tnk/ayiFuO74dR4aPnnomtkGByo=,tag:uive2bYe42s6VtPd03jTMw==,type:str]",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
20
clanServices/mycelium/README.md
Normal file
20
clanServices/mycelium/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
[Mycelium](https://github.com/threefoldtech/mycelium) is an end-to-end encrypted IPv6 overlay network that spans the globe.
|
||||
|
||||
## Features
|
||||
- Locality-aware routing: finds the shortest path between nodes.
|
||||
- All traffic is end-to-end encrypted.
|
||||
- Can route traffic via friend nodes and is location-aware.
|
||||
- Automatic rerouting if a physical link goes down.
|
||||
- IPv6 addresses are derived from private keys.
|
||||
- A simple, reliable message bus is implemented on top of Mycelium.
|
||||
- Supports multiple transports (QUIC, TCP, …). Hole punching for QUIC is in progress to enable true P2P connectivity behind NATs.
|
||||
- Designed for planetary-scale scalability; previous overlay networks reached practical limits, and Mycelium focuses on scaling.
|
||||
- Can run without a TUN device and be used solely as a reliable message bus.
|
||||
|
||||
Example configuration below connects all your machines to the Mycelium network:
|
||||
```nix
|
||||
mycelium = {
|
||||
roles.peer.tags.all = {};
|
||||
};
|
||||
```
|
||||
@@ -7,6 +7,7 @@
|
||||
"System"
|
||||
"Network"
|
||||
];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.peer = {
|
||||
description = "A peer in the mycelium network";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
...
|
||||
}:
|
||||
{
|
||||
name = "service-mycelium";
|
||||
name = "mycelium";
|
||||
|
||||
clan = {
|
||||
test.useContainers = false;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
11
clanServices/packages/README.md
Normal file
11
clanServices/packages/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
This service is meant to be consumed by the UI / API, and exposes a JSON serializable interface to add packages to a machine over the inventory.
|
||||
|
||||
The example below demonstrates installing the "cbonsai" application to a machine named "server.
|
||||
|
||||
```
|
||||
instances.packages = {
|
||||
roles.default.machines."server".settings = {
|
||||
packages = [ "cbonsai" ];
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -6,6 +6,7 @@
|
||||
manifest.categories = [
|
||||
"System"
|
||||
];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.default = {
|
||||
description = "Placeholder role to apply the packages service";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
name = "service-packages";
|
||||
name = "packages";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -1,39 +1,92 @@
|
||||
The `sshd` Clan service manages SSH to make it easy to securely access your
|
||||
machines over the internet. The service uses `vars` to store the SSH host keys
|
||||
for each machine to ensure they remain stable across deployments.
|
||||
|
||||
`sshd` also generates SSH certificates for both servers and clients allowing for
|
||||
certificate-based authentication for SSH.
|
||||
## What it does
|
||||
- Generates and persists SSH host keys via `vars`.
|
||||
- Optionally issues CA-signed host certificates for servers.
|
||||
- Installs the `server` CA public key into `clients` `known_hosts` for TOFU-less verification.
|
||||
|
||||
The service also disables password-based authentication over SSH, to access your
|
||||
machines you'll need to use public key authentication or certificate-based
|
||||
authentication.
|
||||
|
||||
## Usage
|
||||
## When to use it
|
||||
- Zero-TOFU SSH for dynamic fleets: admins/CI can connect to frequently rebuilt hosts (e.g., server-1.example.com) without prompts or per-host `known_hosts` churn.
|
||||
|
||||
### Roles
|
||||
- Server: runs sshd, presents a CA-signed host certificate for `<machine>.<domain>`.
|
||||
- Client: trusts the CA for the given domains to verify servers' certificates.
|
||||
Tip: assign both roles to a machine if it should both present a cert and verify others.
|
||||
|
||||
Quick start (with host certificates)
|
||||
Useful if you never want to get a prompt about trusting the ssh fingerprint.
|
||||
```nix
|
||||
{
|
||||
inventory.instances = {
|
||||
sshd-with-certs = {
|
||||
module = { name = "sshd"; input = "clan-core"; };
|
||||
# Servers present certificates for <machine>.example.com
|
||||
roles.server.tags.all = { };
|
||||
roles.server.settings = {
|
||||
certificate.searchDomains = [ "example.com" ];
|
||||
# Optional: also add RSA host keys
|
||||
# hostKeys.rsa.enable = true;
|
||||
};
|
||||
# Clients trust the CA for *.example.com
|
||||
roles.client.tags.all = { };
|
||||
roles.client.settings = {
|
||||
certificate.searchDomains = [ "example.com" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Basic: only add persistent host keys (ed25519), no certificates
|
||||
Useful if you want to get an ssh "trust this server" prompt once and then never again.
|
||||
```nix
|
||||
{
|
||||
inventory.instances = {
|
||||
# By default this service only generates ed25519 host keys
|
||||
sshd-basic = {
|
||||
module = {
|
||||
name = "sshd";
|
||||
input = "clan-core";
|
||||
};
|
||||
roles.server.tags.all = { };
|
||||
roles.client.tags.all = { };
|
||||
};
|
||||
# Also generate RSA host keys for all servers
|
||||
sshd-with-rsa = {
|
||||
module = {
|
||||
name = "sshd";
|
||||
input = "clan-core";
|
||||
};
|
||||
roles.server.tags.all = { };
|
||||
roles.server.settings = {
|
||||
hostKeys.rsa.enable = true;
|
||||
};
|
||||
roles.client.tags.all = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Example: selective trust per environment
|
||||
Admins should trust only production; CI should trust prod and staging. Servers are reachable under both domains.
|
||||
```nix
|
||||
{
|
||||
inventory.instances = {
|
||||
sshd-env-scoped = {
|
||||
module = { name = "sshd"; input = "clan-core"; };
|
||||
|
||||
# Servers present certs for both prod and staging FQDNs
|
||||
roles.server.tags.all = { };
|
||||
roles.server.settings = {
|
||||
certificate.searchDomains = [ "prod.example.com" "staging.example.com" ];
|
||||
};
|
||||
|
||||
# Admin laptop: trust prod only
|
||||
roles.client.machines."admin-laptop".settings = {
|
||||
certificate.searchDomains = [ "prod.example.com" ];
|
||||
};
|
||||
|
||||
# CI runner: trust prod and staging
|
||||
roles.client.machines."ci-runner-1".settings = {
|
||||
certificate.searchDomains = [ "prod.example.com" "staging.example.com" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
### Explanation
|
||||
- Admin -> server1.prod.example.com: zero-TOFU (verified via cert).
|
||||
- Admin -> server1.staging.example.com: falls back to TOFU (or is blocked by policy).
|
||||
- CI -> either prod or staging: zero-TOFU for both.
|
||||
Note: server and client searchDomains don't have to be identical; they only need to overlap for the hostnames you actually use.
|
||||
|
||||
### Notes
|
||||
- Connect using a name that matches a cert principal (e.g., `server1.example.com`); wildcards are not allowed inside the certificate.
|
||||
- CA private key stays in `vars` (not deployed); only the CA public key is distributed.
|
||||
- Logins still require your user SSH keys on the server (passwords are disabled).
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/sshd";
|
||||
manifest.description = "Enables secure remote access to the machine over SSH";
|
||||
manifest.description = "Enables secure remote access to the machine over SSH with automatic host key management and optional CA-signed host certificates.";
|
||||
manifest.categories = [
|
||||
"System"
|
||||
"Network"
|
||||
@@ -29,7 +29,7 @@
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{ settings, ... }:
|
||||
{ settings, roles, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
@@ -38,8 +38,19 @@
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list);
|
||||
# Collect searchDomains from all servers in this instance
|
||||
allServerSearchDomains = lib.flatten (
|
||||
lib.mapAttrsToList (_name: machineConfig: machineConfig.settings.certificate.searchDomains or [ ]) (
|
||||
roles.server.machines or { }
|
||||
)
|
||||
);
|
||||
# Merge client's searchDomains with all servers' searchDomains
|
||||
searchDomains = uniqueStrings (settings.certificate.searchDomains ++ allServerSearchDomains);
|
||||
in
|
||||
{
|
||||
clan.core.vars.generators.openssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) {
|
||||
clan.core.vars.generators.openssh-ca = lib.mkIf (searchDomains != [ ]) {
|
||||
share = true;
|
||||
files.id_ed25519.deploy = false;
|
||||
files."id_ed25519.pub" = {
|
||||
@@ -54,9 +65,9 @@
|
||||
'';
|
||||
};
|
||||
|
||||
programs.ssh.knownHosts.ssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) {
|
||||
programs.ssh.knownHosts.ssh-ca = lib.mkIf (searchDomains != [ ]) {
|
||||
certAuthority = true;
|
||||
extraHostNames = builtins.map (domain: "*.${domain}") settings.certificate.searchDomains;
|
||||
extraHostNames = builtins.map (domain: "*.${domain}") searchDomains;
|
||||
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
|
||||
};
|
||||
};
|
||||
@@ -180,7 +191,9 @@
|
||||
settings.PasswordAuthentication = false;
|
||||
|
||||
settings.HostCertificate = lib.mkIf (
|
||||
# this check needs to go first, as otherwise generators.openssh-cert does not exist
|
||||
settings.certificate.searchDomains != [ ]
|
||||
&& config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".exists
|
||||
) config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path;
|
||||
|
||||
hostKeys = [
|
||||
|
||||
@@ -13,6 +13,11 @@ in
|
||||
clan.nixosTests.sshd = {
|
||||
imports = [ ./tests/vm/default.nix ];
|
||||
|
||||
clan.modules."@clan/sshd" = module;
|
||||
};
|
||||
clan.nixosTests.sshd-no-search-domains = {
|
||||
imports = [ ./tests/vm/no-search-domains.nix ];
|
||||
|
||||
clan.modules."@clan/sshd" = module;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
...
|
||||
}:
|
||||
{
|
||||
name = "service-sshd";
|
||||
name = "sshd";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
45
clanServices/sshd/tests/vm/no-search-domains.nix
Normal file
45
clanServices/sshd/tests/vm/no-search-domains.nix
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
This is a regression test for the following error:
|
||||
error: attribute 'openssh-cert' missing
|
||||
at /nix/store/y1k4bqwjql6bhlry456cs4marpamiqlr-source/clanServices/sshd/default.nix:184:17:
|
||||
183| # this check needs to go first, as otherwise generators.openssh-cert does not exist
|
||||
184| config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".exists
|
||||
| ^
|
||||
185| && settings.certificate.searchDomains != [ ]
|
||||
*/
|
||||
{
|
||||
...
|
||||
}:
|
||||
{
|
||||
name = "sshd";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
machines.server = { };
|
||||
machines.client = { };
|
||||
|
||||
instances = {
|
||||
sshd-test = {
|
||||
module.name = "@clan/sshd";
|
||||
module.input = "self";
|
||||
roles.server.machines."server".settings = {
|
||||
hostKeys.rsa.enable = true;
|
||||
};
|
||||
roles.client.machines."client".settings = {
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nodes = {
|
||||
server = { };
|
||||
client = { };
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
# don't do anything, just evaluate the machines
|
||||
exit(0)
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../sops/machines/client
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../sops/machines/server
|
||||
@@ -2,9 +2,17 @@
|
||||
"data": "ENC[AES256_GCM,data:Qje3bXRHcAiFCslFfAeUTcOn2woc06e1sLAoH16x1sZ7N0i07rHqwsBjn7nKvMee6tktIjLMGTPOQL9TLgYI+wDgU5MHqlZlVBnYLk+VXYEKhymDlS3RAg5pbrmJzkucl/Vw5VBlDK+n8qnrgFG2pgpiC6Lzb5f4I7pxUl4zcz9jCf0Stj10lVQgkbvnr8UMUvcb1vUF/EAhF6WhhhgrhpbYrK+PkNW6EXmxIXdUdXvSqVWvJby2NgBaGOM1lwnWv4LOOiJ1BNXnOnLE0a8+8SjqahzqroeFvUCtoFxJ45/LqFTtgS9cQ44FaqrMRSWlOgsNR2BkeT449fKUCwYfEW1g/R47I9hAm/bcKrOBpHdAji/hpcDy73Qb9ZKrY6/0kRXl/ECI3YVX8nrHIjEJpoZ7a/dxJu7z8OOdb7gP2mVPTiWcbpD/KwpefDWLKnYhBrXUNaqSajf95Ahln2Vn6sALbbjcUzs/x5OqchYe1/kYy7MaGbEFNPeINnGid73c7xfUoyI90Ho4azLW8Q/n,iv:9heXX8g1P8/4gGT3+RYYmz6rJ5EnIDr5w1OAbGybL+I=,tag:p6mHZ8+EaJ+Nyn59n2TQ4g==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age124l8cfswl97ck0e0qw8l47usf375srn69e4mhxr3gr40erxw7pesftxshx",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5b0Y3aldQWXJyZzU3aGZU\naWpOK1VXdUVlSUVvZzJqSDVCekQ0Z2JxblRvCmFWd0c1SC9jckMweTIrNmg0VnN3\ncTNwMkVTL0ZyT3RGeUs2NnlNb21GWTQKLS0tIEpGM2FacUNoakJ0dlJwMWZMNU0x\nVWhmV1pDOTRFNExpVHBTRUx2L2ZaMVUK+ka9oqcvoLjwwUqIol0rU1VsJHhs4S1P\nWpKKPetUPEF4xxWj0OdeMc40XCTjl6CBdbtcrslH3tuZHjeSWQ6QCg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkNlpCNGR0SHVtVzJ2K0hM\ncng1ZWlTSGQzWndXVWhlbEZMN25TUklXV1I4Cm9PZkJ3Zi81YXoxdzVPTFI3dE1a\nUnJnUmJPbi9CYmdFL2ltaTRSQ1MreFUKLS0tIDE4Y05IamJjL0huY2l2YU4zbDJs\ndTl1b0wzaTM4MndlcFZYVThqbWtVeFUKuSZLJpUrccuusJPU2xWHw19wTN8mKZW3\n1GJJjlb79rZp/RbSMxFxkyVHgu+F9kbpRgViICJSWkeR495786oArA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLM3lNL1UyUjhRdFBvenlv\naXBTYXJva0hCTS8vK1hCYmdQdlpBNGFNTkNrCmVTbFROTWVWRzZyc2E0SjVDT1hs\nMm9qT0plcHNJaWczcWJTd0llYldGMVkKLS0tIGh6M04yQ3RiMGVYbTNYMEdCVTNK\nUDhaS3FSVWl4ZGlPYTRodW80VElsZ1UKxi48UZpxuu7gkRtQrCi4//suOpuFY6sl\n8b1xcN/tMn2MWW9hvx4K4qJLXTWOm+9GiZqJ51JBb0hihRh7fC3SfA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1f39qxz84yv272wk636el0kdyagzudcs99ucpkjarsj2rey6yvccse9lwet",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGK2Z5OGFsVjBJcGFITENH\nL205TDBaNkRvQ3ZBbGVDU0wrWnl5QkJaSFZNCjhNSXpMTTd1ejlqbVNBUDZ2TlFn\neUhNdEh4M1RhQnFabHpaMVd5eG11THcKLS0tIE9PS3JlVFVPNG5sM3hpWG95V1ho\nSXBySU1SUExkVHNHZElmQWExTVN3cHMKvzdlCWP8/9xviu/9AMxw/4ZyXo4O3AE9\n84IQBDO4GYrqnXvOroxjsNCDrCBDH0WPuYAphctJvyI5SSAtL4uHhQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-06-09T19:51:28Z",
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
## Usage
|
||||
This service configures Syncthing to continuously synchronize a folder peer-to-peer across your machines.
|
||||
|
||||
Example configuration:
|
||||
|
||||
```nix
|
||||
{
|
||||
instances.syncthing = {
|
||||
roles.peer.tags.all = { };
|
||||
roles.peer.settings.folders = {
|
||||
documents = {
|
||||
path = "~/syncthing/documents";
|
||||
};
|
||||
};
|
||||
instances.syncthing = {
|
||||
roles.peer.tags.all = { };
|
||||
roles.peer.settings.folders = {
|
||||
documents = {
|
||||
path = "/home/youruser/syncthing/documents";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Now the folder `~/syncthing/documents` will be shared and kept in sync with all your machines.
|
||||
Notes:
|
||||
- Each key under `folders` is a folder ID (an arbitrary identifier for Syncthing).
|
||||
- Prefer absolute paths (example shown). `~` may work in some environments but can be ambiguous in service contexts.
|
||||
|
||||
|
||||
## Documentation
|
||||
Extensive documentation is available on the [Syncthing](https://docs.syncthing.net/) website.
|
||||
## Documentation
|
||||
See the official Syncthing docs: https://docs.syncthing.net/
|
||||
|
||||
@@ -22,6 +22,7 @@ in
|
||||
../../clanServices/syncthing
|
||||
# Required modules
|
||||
../../nixosModules/clanCore
|
||||
../../nixosModules/machineModules
|
||||
# Dependencies like clan-cli
|
||||
../../pkgs/clan-cli
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
name = "service-syncthing-service";
|
||||
name = "syncthing-service";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
pkgs.syncthing
|
||||
];
|
||||
script = ''
|
||||
syncthing generate --config "$out"
|
||||
export TMPDIR=/tmp
|
||||
TEMPORARY=$(mktemp -d)
|
||||
syncthing generate --config "$out" --data "$TEMPORARY"
|
||||
mv "$out"/key.pem "$out"/key
|
||||
mv "$out"/cert.pem "$out"/cert
|
||||
cat "$out"/config.xml | grep -oP '(?<=<device id=")[^"]+' | uniq > "$out"/id
|
||||
|
||||
19
clanServices/tor/README.md
Normal file
19
clanServices/tor/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
!!! Danger "Experimental"
|
||||
This service is experimental and will change in the future.
|
||||
|
||||
---
|
||||
|
||||
This module is part of Clan's [networking interface](https://docs.clan.lol/guides/networking/networking/).
|
||||
|
||||
Clan's networking module automatically manages connections across available network transports and falls back intelligently. When you run `clan ssh` or `clan machines update`, Clan attempts each configured network in priority order until a connection succeeds.
|
||||
|
||||
The example below configures all your nixos machines to be reachable over the Tor network. By default, the `tor` module has the lowest priority among networks, as it's the slowest.
|
||||
|
||||
```nix
|
||||
inventory.instances = {
|
||||
# Fallback: Secure connections via Tor
|
||||
tor = {
|
||||
roles.server.tags.nixos = { };
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -7,6 +7,7 @@
|
||||
"System"
|
||||
"Network"
|
||||
];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.client = {
|
||||
description = ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
name = "service-trusted-nix-caches";
|
||||
name = "trusted-nix-caches";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -120,6 +120,63 @@
|
||||
|
||||
share = settings.share;
|
||||
|
||||
script =
|
||||
(
|
||||
if settings.prompt then
|
||||
''
|
||||
prompt_value=$(cat "$prompts"/user-password)
|
||||
if [[ -n "''${prompt_value-}" ]]; then
|
||||
echo "$prompt_value" | tr -d "\n" > "$out"/user-password
|
||||
else
|
||||
xkcdpass --numwords 4 --delimiter - --count 1 | tr -d "\n" > "$out"/user-password
|
||||
fi
|
||||
''
|
||||
else
|
||||
''
|
||||
xkcdpass --numwords 4 --delimiter - --count 1 | tr -d "\n" > "$out"/user-password
|
||||
''
|
||||
)
|
||||
+ ''
|
||||
mkpasswd -s -m sha-512 < "$out"/user-password | tr -d "\n" > "$out"/user-password-hash
|
||||
'';
|
||||
};
|
||||
};
|
||||
darwinModule =
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
# For darwin, we currently only generate and manage the password secret.
|
||||
# Hooking into actual macOS account management may be added later.
|
||||
clan.core.vars.generators."user-password-${settings.user}" = {
|
||||
files.user-password-hash.neededFor = "users";
|
||||
files.user-password.deploy = false;
|
||||
|
||||
prompts.user-password = lib.mkIf settings.prompt {
|
||||
display = {
|
||||
group = settings.user;
|
||||
label = "password";
|
||||
required = false;
|
||||
helperText = ''
|
||||
Your password will be encrypted and stored securely using the secret store you've configured.
|
||||
'';
|
||||
};
|
||||
type = "hidden";
|
||||
persist = true;
|
||||
description = "Leave empty to generate automatically";
|
||||
};
|
||||
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.xkcdpass
|
||||
pkgs.mkpasswd
|
||||
];
|
||||
|
||||
share = settings.share;
|
||||
|
||||
script =
|
||||
(
|
||||
if settings.prompt then
|
||||
@@ -149,5 +206,7 @@
|
||||
# Immutable users to ensure that this module has exclusive control over the users.
|
||||
users.mutableUsers = false;
|
||||
};
|
||||
# No-op for darwin by default; can be extended later if needed.
|
||||
darwinModule = { };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
name = "service-users";
|
||||
name = "users";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
name = "service-wifi";
|
||||
name = "wifi";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
6
clanServices/wifi/tests/vm/sops/machines/second/key.json
Executable file
6
clanServices/wifi/tests/vm/sops/machines/second/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age1afr59kwlpuapv2g9m0sa4k9yc22ulj7zcway538z7nnsgvf2re5qd7k03a",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:xAQ6TcwttQNb1BXlq2j+xZX+vGbqV5XKZyZMBAnQ00hWLu6Ba6pN2qR4HItKQ1KWza8WGhzgGdcwZv6Qobgmp9wAcwWlAubS7FU=,iv:KalRsDqWxqscJOeZcnQfFkP7QTBVZpP8XAdPHvikfaM=,tag:EAndFTqRYRcCgD5/ixniBA==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjZnpWd0grazhjak0ra3NK\nSEpVTWVEaXEzc3ZIeloySXBqVk01SVNsVmtzCk5weWJCSGNlb2FRM0RWVEYwTHlC\nTVFBd1cvYlZrNlQ4TFRydFNzWXNicE0KLS0tIFZ5WllRNEI0Q3hQaVU2WjhGNDFr\nLy9pSXU5Y2FINy9LbXg4dzdHREQveE0KyxWDDyRpo0eTIXj0lHQtOunLtAP/Q+70\n+GvfjW7WXHXvXyg6CXzpCy6F6UWie4LHO9VrJM2mTjoh+q4l5DT6CA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-10-19T12:53:35Z",
|
||||
"mac": "ENC[AES256_GCM,data:P6Wcm7daPMe6laceFIiIzhcGa9k37Fo5ZnBFhdVmkATuR2oqMZp9Ke5r73SbC5B95QoPnWVNNnrxkn/oiVQmSiiDaf718isLAfU+7zGkV0BZCtfCrqe82JzH2iQ0tKSxsVJqklCijAfUBGpt/EYN6c4QnM2IFulbiWs2kqWMi4I=,iv:mRuTg2RblZCNX3yQNFKCtuwjwIptZ5VGpSyLV6Ah5NI=,tag:Xl8/jrtIhxfAi+FrnwKh7Q==,type:str]",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user