Compare commits
473 Commits
fix-typogr
...
deps-poc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e7d4e122e | ||
|
|
90ad8054d0 | ||
|
|
716d4a17f5 | ||
|
|
dcd1273f3f | ||
|
|
899c9eed0e | ||
|
|
af85041e5e | ||
|
|
6a96ce8679 | ||
|
|
60195f9614 | ||
|
|
447b0bf8ac | ||
|
|
fd162f6fc8 | ||
|
|
e4bf6523ad | ||
|
|
5312799784 | ||
|
|
7d265a6156 | ||
|
|
f8428947ca | ||
|
|
196d7c95c0 | ||
|
|
6be40f6f79 | ||
|
|
3aefabd818 | ||
|
|
230e7e6769 | ||
|
|
46bae67645 | ||
|
|
890e8c7003 | ||
|
|
0d3a62321a | ||
|
|
ef82e07293 | ||
|
|
7c8c3811f4 | ||
|
|
9b2c97a855 | ||
|
|
785f789628 | ||
|
|
a034fefb51 | ||
|
|
bcd846fe5e | ||
|
|
a6214f431d | ||
|
|
b8890f6732 | ||
|
|
370b4f535d | ||
|
|
ef66c9b5be | ||
|
|
79d44f7c30 | ||
|
|
e72e100965 | ||
|
|
180e2a601c | ||
|
|
90d265089b | ||
|
|
a0fa52fded | ||
|
|
af4e9e784b | ||
|
|
cb162a53b8 | ||
|
|
16e506ea1a | ||
|
|
11ec94c17f | ||
|
|
8468b1ebaf | ||
|
|
ec83130fa4 | ||
|
|
c1e41f8fd9 | ||
|
|
3630e778ad | ||
|
|
916186c465 | ||
|
|
25e733b8d7 | ||
|
|
2599998b17 | ||
|
|
56649b7fe2 | ||
|
|
fc85622e01 | ||
|
|
c499c563bb | ||
|
|
b255ba0367 | ||
|
|
493adebd7c | ||
|
|
cac2866356 | ||
|
|
981f6052ad | ||
|
|
6e888c38fa | ||
|
|
e953f807de | ||
|
|
c2534e9a42 | ||
|
|
b0feef1a40 | ||
|
|
d4c26087df | ||
|
|
1a9bbab667 | ||
|
|
b23171f291 | ||
|
|
087423597b | ||
|
|
602dc192f3 | ||
|
|
dba166cc8a | ||
|
|
21b872a1c9 | ||
|
|
be48ffe724 | ||
|
|
7673b72991 | ||
|
|
42bbd7c5fd | ||
|
|
823114435a | ||
|
|
e7efbb701b | ||
|
|
30d9c86015 | ||
|
|
313b77be79 | ||
|
|
6229e62281 | ||
|
|
49ff4da6be | ||
|
|
6d6521803d | ||
|
|
afd7bfc8c0 | ||
|
|
88fa3dff83 | ||
|
|
629ef65ce5 | ||
|
|
92151331f3 | ||
|
|
67dcd45dd5 | ||
|
|
95a4a69ffb | ||
|
|
88343ce523 | ||
|
|
fd9dd6f872 | ||
|
|
aaaa310c7f | ||
|
|
ffbf22eb60 | ||
|
|
8d3e0d2209 | ||
|
|
c05a890d50 | ||
|
|
03458ffbd8 | ||
|
|
ea098048c8 | ||
|
|
838ed6ead7 | ||
|
|
7e7278b99b | ||
|
|
f4d7728f3f | ||
|
|
c9b71496eb | ||
|
|
cd1f9c5a8b | ||
|
|
56379510d0 | ||
|
|
389299ac7d | ||
|
|
9cf04bcb5f | ||
|
|
c370598564 | ||
|
|
04001ff178 | ||
|
|
194c3080ea | ||
|
|
60d1e524ac | ||
|
|
672af1c63d | ||
|
|
6cb728a4ca | ||
|
|
a074650947 | ||
|
|
f169a40c69 | ||
|
|
480d5ee18c | ||
|
|
ba47d797e4 | ||
|
|
3e5f84dcb4 | ||
|
|
e398d98b42 | ||
|
|
09e5f78aae | ||
|
|
ae1680a720 | ||
|
|
9abf557353 | ||
|
|
dc0ec3443e | ||
|
|
d6c6918f85 | ||
|
|
24756442c8 | ||
|
|
c61a0f0712 | ||
|
|
f05bfcb13d | ||
|
|
6d8ea1f2c5 | ||
|
|
f1de0e28ff | ||
|
|
53ce3cf53d | ||
|
|
0ac6d7be87 | ||
|
|
e55401ecd9 | ||
|
|
37a49a14f4 | ||
|
|
7f68b10611 | ||
|
|
a2867ba29d | ||
|
|
0817cf868b | ||
|
|
018ffdaeeb | ||
|
|
eebb9b6a12 | ||
|
|
36f73d40b3 | ||
|
|
db84369000 | ||
|
|
359b2d4e7a | ||
|
|
2af9bd5003 | ||
|
|
a8cbfcbd18 | ||
|
|
dc17d62131 | ||
|
|
f97e22e125 | ||
|
|
1d9ad2ae54 | ||
|
|
c266261d3b | ||
|
|
93c31d4c26 | ||
|
|
c9275db377 | ||
|
|
cf83833d8b | ||
|
|
494f79edb4 | ||
|
|
de3102614a | ||
|
|
a6f0924c05 | ||
|
|
99dc4f6787 | ||
|
|
5f2ad6432e | ||
|
|
f8c34caaab | ||
|
|
8c2399446b | ||
|
|
95c781bf4d | ||
|
|
fe58de0997 | ||
|
|
7582458bae | ||
|
|
3a7d7afaab | ||
|
|
321eeacff0 | ||
|
|
8ae43ff9a0 | ||
|
|
e6efd5e731 | ||
|
|
7c1c8a5486 | ||
|
|
7932562fa6 | ||
|
|
ac22843abc | ||
|
|
eb83386098 | ||
|
|
7877075847 | ||
|
|
7206dd8219 | ||
|
|
f21e1e7641 | ||
|
|
c2a3f5e498 | ||
|
|
63c0db482f | ||
|
|
d2456be3dd | ||
|
|
c3c08482ac | ||
|
|
62126f0c32 | ||
|
|
28139560c2 | ||
|
|
45c916fb6d | ||
|
|
727d4e70ae | ||
|
|
261c5d2be8 | ||
|
|
87ea942399 | ||
|
|
39a032a285 | ||
|
|
a06940e981 | ||
|
|
4aebfadc8a | ||
|
|
f45f26994e | ||
|
|
c777a1a2b9 | ||
|
|
36fe7822f7 | ||
|
|
0ccf3310f9 | ||
|
|
a8d6552caa | ||
|
|
a131448dcf | ||
|
|
14a52dbc2e | ||
|
|
565391bd8c | ||
|
|
9bffa2a774 | ||
|
|
e42a07423e | ||
|
|
c5178ac16a | ||
|
|
33791e06cd | ||
|
|
c7e3bf624e | ||
|
|
ba027c2239 | ||
|
|
25fdabee29 | ||
|
|
de69c63ee3 | ||
|
|
b9573636d8 | ||
|
|
3862ad2a06 | ||
|
|
c447aec9d3 | ||
|
|
5137d19b0f | ||
|
|
453f2649d3 | ||
|
|
58cfcf3d25 | ||
|
|
c260a97cc1 | ||
|
|
3eb64870b0 | ||
|
|
7412b958c6 | ||
|
|
a0c27194a6 | ||
|
|
3437af29cb | ||
|
|
0b1c12d2e5 | ||
|
|
8620761bbd | ||
|
|
d793b6ca07 | ||
|
|
17e9231657 | ||
|
|
acc2674d79 | ||
|
|
c34a21a3bb | ||
|
|
275bff23da | ||
|
|
1a766a3447 | ||
|
|
c22844c83b | ||
|
|
5472ca0e21 | ||
|
|
ad890b0b6b | ||
|
|
a364b5ebf3 | ||
|
|
d0134d131e | ||
|
|
ccf0dace11 | ||
|
|
9977a903ce | ||
|
|
dc9bf5068e | ||
|
|
6b4f79c9fa | ||
|
|
b2985b59e9 | ||
|
|
d4ac3b83ee | ||
|
|
00bf55be5a | ||
|
|
851d6aaa89 | ||
|
|
f007279bee | ||
|
|
5a3381d9ff | ||
|
|
83e51db2e7 | ||
|
|
4e4af8a52f | ||
|
|
54a8ec717e | ||
|
|
d3e5e6edf1 | ||
|
|
a4277ad312 | ||
|
|
8877f2d451 | ||
|
|
9275b66bd9 | ||
|
|
6a964f37d5 | ||
|
|
73f2a4f56f | ||
|
|
85fb0187ee | ||
|
|
db9812a08b | ||
|
|
ca69530591 | ||
|
|
fc5b0e4113 | ||
|
|
278af5f0f4 | ||
|
|
e7baf25ff7 | ||
|
|
fada75144c | ||
|
|
803ef5476f | ||
|
|
016bd263d0 | ||
|
|
f9143f8a5d | ||
|
|
92eb27fcb1 | ||
|
|
0cc9b91ae8 | ||
|
|
2ed3608e34 | ||
|
|
a92a1a7dd1 | ||
|
|
9a903be6d4 | ||
|
|
adea270b27 | ||
|
|
765eb142a5 | ||
|
|
faa1405d6b | ||
|
|
0c93aab818 | ||
|
|
56923ae2c3 | ||
|
|
e2f64e1d40 | ||
|
|
c574b84278 | ||
|
|
640f15d55e | ||
|
|
789d326273 | ||
|
|
1763d85d91 | ||
|
|
082fa05083 | ||
|
|
9ed7190606 | ||
|
|
6c22539dd4 | ||
|
|
e6819ede61 | ||
|
|
186a760529 | ||
|
|
a84aee7b0c | ||
|
|
cab2fa44ba | ||
|
|
758eacd27e | ||
|
|
5962149e55 | ||
|
|
00f9d08a4b | ||
|
|
3d0c843308 | ||
|
|
847138472b | ||
|
|
c7786a59fd | ||
|
|
3b2d357f10 | ||
|
|
a83dbf604c | ||
|
|
f77456a123 | ||
|
|
6e4c3a638d | ||
|
|
3d2127ce1e | ||
|
|
a4a5916fa2 | ||
|
|
f6727055cd | ||
|
|
0517d87caa | ||
|
|
89e587592c | ||
|
|
439495d738 | ||
|
|
0b2fd681be | ||
|
|
41de615331 | ||
|
|
b7639b1d81 | ||
|
|
602879c9e4 | ||
|
|
53e16242b9 | ||
|
|
24c5146763 | ||
|
|
dca7aa0487 | ||
|
|
647bc4e4df | ||
|
|
1c80223fe3 | ||
|
|
7ac9b00398 | ||
|
|
d37c9e3b04 | ||
|
|
0fe9d0e157 | ||
|
|
5479c767c1 | ||
|
|
edc389ba4b | ||
|
|
4cb17d42e1 | ||
|
|
f26499edb8 | ||
|
|
2857cb7ed8 | ||
|
|
3168fecd52 | ||
|
|
24c20ff243 | ||
|
|
8ba8fda54b | ||
|
|
0992a47b00 | ||
|
|
d5b09f18ed | ||
|
|
fb2fe36c87 | ||
|
|
3db51887b1 | ||
|
|
24f3bcca57 | ||
|
|
85006c8103 | ||
|
|
db5571d623 | ||
|
|
d4bdaec586 | ||
|
|
cb9c8e5b5a | ||
|
|
0a1802c341 | ||
|
|
dfae1a4429 | ||
|
|
c1dc73a21b | ||
|
|
8145740cc1 | ||
|
|
b2a54f5b0d | ||
|
|
9c9adc6e16 | ||
|
|
f7cde8eb0f | ||
|
|
501d020562 | ||
|
|
a9bafd71e1 | ||
|
|
166e4b8081 | ||
|
|
c3eb40f17a | ||
|
|
7330285150 | ||
|
|
8cf8573c61 | ||
|
|
5bfa0d7a9d | ||
|
|
8ea2dd9b72 | ||
|
|
6efcade56a | ||
|
|
6d2372be56 | ||
|
|
626af4691b | ||
|
|
63697ac4b1 | ||
|
|
0ebb1f0c66 | ||
|
|
1dda60847e | ||
|
|
a7bce4cb19 | ||
|
|
a5474bc25f | ||
|
|
f634b8f1fb | ||
|
|
0ad40a0233 | ||
|
|
78abc36cd3 | ||
|
|
f5158b068f | ||
|
|
e6066a6cb1 | ||
|
|
fc8b66effa | ||
|
|
16b92963fd | ||
|
|
2ff3d871ac | ||
|
|
108936ef07 | ||
|
|
c45d4cfec9 | ||
|
|
64217e1281 | ||
|
|
d1421bb534 | ||
|
|
ac20514a8e | ||
|
|
79c4e73a15 | ||
|
|
61a647b436 | ||
|
|
c9a709783a | ||
|
|
c55b369899 | ||
|
|
084b8bacd3 | ||
|
|
47ad7d8a95 | ||
|
|
3798808013 | ||
|
|
43a39267f3 | ||
|
|
db94ea2d2e | ||
|
|
f0533f9bba | ||
|
|
360048fd04 | ||
|
|
8f8426de52 | ||
|
|
4bce390e64 | ||
|
|
2b7837e2b6 | ||
|
|
cbf9678534 | ||
|
|
b38b10c9a6 | ||
|
|
31cbb7dc00 | ||
|
|
0fa4377793 | ||
|
|
7b0d10e8c2 | ||
|
|
bb41adab4b | ||
|
|
648aa7dc59 | ||
|
|
3073969c92 | ||
|
|
2f1dc3a33d | ||
|
|
b707dcea2d | ||
|
|
4f0c8025b2 | ||
|
|
b91bee537a | ||
|
|
7207a3e8cd | ||
|
|
ac675a5af0 | ||
|
|
64caebde62 | ||
|
|
4934884e0c | ||
|
|
22cd9baee2 | ||
|
|
84232b5355 | ||
|
|
5bc7c255c1 | ||
|
|
d11d83f699 | ||
|
|
2ef1b2a8fa | ||
|
|
f7414d7e6e | ||
|
|
ab384150b2 | ||
|
|
0b6939ffee | ||
|
|
bc6a1a9d17 | ||
|
|
7055461cf0 | ||
|
|
a9564df6a9 | ||
|
|
e2dfc74d02 | ||
|
|
326cb60aea | ||
|
|
68b264970a | ||
|
|
1fa4ef82e9 | ||
|
|
bd93651f12 | ||
|
|
85ad51ce4c | ||
|
|
59e50c6150 | ||
|
|
f347568de3 | ||
|
|
bdad7d81b2 | ||
|
|
b8203cdf73 | ||
|
|
431e45cc3a | ||
|
|
f185d28f68 | ||
|
|
d8e6fcf773 | ||
|
|
23b7d24399 | ||
|
|
a1ed512da4 | ||
|
|
40ac96cd10 | ||
|
|
c4da43da0f | ||
|
|
8822f6dadc | ||
|
|
b5a7a91612 | ||
|
|
453b1a91a8 | ||
|
|
70274d69e9 | ||
|
|
c57d8b30d3 | ||
|
|
7407fef21b | ||
|
|
23c152541a | ||
|
|
6765e27031 | ||
|
|
cbb789bc69 | ||
|
|
7f68a21257 | ||
|
|
fc66dc78c3 | ||
|
|
1d0e0f243e | ||
|
|
8134ffd787 | ||
|
|
7f1590c729 | ||
|
|
c65bb0b1ce | ||
|
|
d8bc5269ee | ||
|
|
917407c475 | ||
|
|
d9e6e0c540 | ||
|
|
ef5ab0c2f4 | ||
|
|
34816013ad | ||
|
|
05665b1c7e | ||
|
|
2bebcab736 | ||
|
|
306f83e357 | ||
|
|
04457b1272 | ||
|
|
4986fe30c3 | ||
|
|
de33a07875 | ||
|
|
5233eb7fdb | ||
|
|
94a158b77a | ||
|
|
98af47d0b5 | ||
|
|
4470bb886e | ||
|
|
f4feac0d6b | ||
|
|
7547761812 | ||
|
|
23d11651fc | ||
|
|
03a4ac5bde | ||
|
|
ab50b433ee | ||
|
|
123e8398d8 | ||
|
|
6a2dfb8176 | ||
|
|
332d10e306 | ||
|
|
f3f6692e4d | ||
|
|
954301465f | ||
|
|
2199f4efd5 | ||
|
|
e208c02be7 | ||
|
|
7747e3cc0d | ||
|
|
1c24b4c6cb | ||
|
|
4b1ab4cdde | ||
|
|
4852e79c3c | ||
|
|
0a70ed6268 | ||
|
|
136acc7901 | ||
|
|
70d1dd0deb | ||
|
|
df32da304f | ||
|
|
76eb3c13e9 | ||
|
|
6e88046fd4 | ||
|
|
b3cafa4a8c | ||
|
|
d1cf87d2ce | ||
|
|
dc5485d9f1 | ||
|
|
1b12882e29 | ||
|
|
5be9b8383b | ||
|
|
c308fd63a7 | ||
|
|
fcdfd80b34 | ||
|
|
c5d975542d | ||
|
|
526eccdf16 | ||
|
|
f7dd34be21 | ||
|
|
51c679d3a9 | ||
|
|
470c3d330f | ||
|
|
df596ed59f | ||
|
|
f2c1202b03 | ||
|
|
4414403dec | ||
|
|
2d78730037 | ||
|
|
ec70de406b |
2
.github/workflows/repo-sync.yml
vendored
2
.github/workflows/repo-sync.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
if: github.repository_owner == 'clan-lol'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/create-github-app-token@v2
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{ fetchgit }:
|
||||
fetchgit {
|
||||
url = "https://git.clan.lol/clan/clan-core.git";
|
||||
rev = "5d884cecc2585a29b6a3596681839d081b4de192";
|
||||
sha256 = "09is1afmncamavb2q88qac37vmsijxzsy1iz1vr6gsyjq2rixaxc";
|
||||
}
|
||||
@@ -50,12 +50,14 @@
|
||||
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
|
||||
(import ../installation/facter-report.nix pkgs.hostPlatform.system)
|
||||
]
|
||||
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||
in
|
||||
{
|
||||
checks = pkgs.lib.mkIf pkgs.stdenv.isLinux {
|
||||
# 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 = {
|
||||
|
||||
10
checks/installation/facter-report.nix
Normal file
10
checks/installation/facter-report.nix
Normal file
@@ -0,0 +1,10 @@
|
||||
system:
|
||||
builtins.fetchurl {
|
||||
url = "https://git.clan.lol/clan/test-fixtures/raw/commit/4a2bc56d886578124b05060d3fb7eddc38c019f8/nixos-vm-facter-json/${system}.json";
|
||||
sha256 =
|
||||
{
|
||||
aarch64-linux = "sha256:1rlfymk03rmfkm2qgrc8l5kj5i20srx79n1y1h4nzlpwaz0j7hh2";
|
||||
x86_64-linux = "sha256:16myh0ll2gdwsiwkjw5ba4dl23ppwbsanxx214863j7nvzx42pws";
|
||||
}
|
||||
.${system};
|
||||
}
|
||||
@@ -18,27 +18,23 @@
|
||||
fileSystems."/".device = lib.mkDefault "/dev/vda";
|
||||
boot.loader.grub.device = lib.mkDefault "/dev/vda";
|
||||
|
||||
imports = [ self.nixosModules.test-install-machine-without-system ];
|
||||
imports = [
|
||||
self.nixosModules.test-install-machine-without-system
|
||||
];
|
||||
};
|
||||
|
||||
clan.machines.test-install-machine-with-system =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
# https://git.clan.lol/clan/test-fixtures
|
||||
facter.reportPath = builtins.fetchurl {
|
||||
url = "https://git.clan.lol/clan/test-fixtures/raw/commit/4a2bc56d886578124b05060d3fb7eddc38c019f8/nixos-vm-facter-json/${pkgs.hostPlatform.system}.json";
|
||||
sha256 =
|
||||
{
|
||||
aarch64-linux = "sha256:1rlfymk03rmfkm2qgrc8l5kj5i20srx79n1y1h4nzlpwaz0j7hh2";
|
||||
x86_64-linux = "sha256:16myh0ll2gdwsiwkjw5ba4dl23ppwbsanxx214863j7nvzx42pws";
|
||||
}
|
||||
.${pkgs.hostPlatform.system};
|
||||
};
|
||||
facter.reportPath = import ./facter-report.nix pkgs.hostPlatform.system;
|
||||
|
||||
fileSystems."/".device = lib.mkDefault "/dev/vda";
|
||||
boot.loader.grub.device = lib.mkDefault "/dev/vda";
|
||||
|
||||
imports = [ self.nixosModules.test-install-machine-without-system ];
|
||||
};
|
||||
|
||||
flake.nixosModules = {
|
||||
test-install-machine-without-system =
|
||||
{ lib, modulesPath, ... }:
|
||||
@@ -159,6 +155,7 @@
|
||||
pkgs.stdenv.drvPath
|
||||
pkgs.bash.drvPath
|
||||
pkgs.buildPackages.xorg.lndir
|
||||
(import ./facter-report.nix pkgs.hostPlatform.system)
|
||||
]
|
||||
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
};
|
||||
@@ -302,7 +299,8 @@
|
||||
"test-install-machine-without-system",
|
||||
"-i", ssh_conn.ssh_key,
|
||||
"--option", "store", os.environ['CLAN_TEST_STORE'],
|
||||
f"nonrootuser@localhost:{ssh_conn.host_port}"
|
||||
"--target-host", f"nonrootuser@localhost:{ssh_conn.host_port}",
|
||||
"--yes"
|
||||
]
|
||||
|
||||
result = subprocess.run(clan_cmd, capture_output=True, cwd=flake_dir)
|
||||
@@ -326,7 +324,9 @@
|
||||
"test-install-machine-without-system",
|
||||
"-i", ssh_conn.ssh_key,
|
||||
"--option", "store", os.environ['CLAN_TEST_STORE'],
|
||||
f"nonrootuser@localhost:{ssh_conn.host_port}"
|
||||
"--target-host",
|
||||
f"nonrootuser@localhost:{ssh_conn.host_port}",
|
||||
"--yes"
|
||||
]
|
||||
|
||||
result = subprocess.run(clan_cmd, capture_output=True, cwd=flake_dir)
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
pkgs.stdenv.drvPath
|
||||
pkgs.stdenvNoCC
|
||||
self.nixosConfigurations.test-morph-machine.config.system.build.toplevel
|
||||
(import ../installation/facter-report.nix pkgs.hostPlatform.system)
|
||||
]
|
||||
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
pkgs.stdenv.drvPath
|
||||
pkgs.bash.drvPath
|
||||
pkgs.buildPackages.xorg.lndir
|
||||
(import ../installation/facter-report.nix pkgs.hostPlatform.system)
|
||||
]
|
||||
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ inventory.instances = {
|
||||
borgbackup = {
|
||||
module = {
|
||||
name = "borgbackup";
|
||||
input = "clan";
|
||||
input = "clan-core";
|
||||
};
|
||||
roles.client.machines."jon".settings = {
|
||||
destinations."storagebox" = {
|
||||
|
||||
32
clanServices/certificates/README.md
Normal file
32
clanServices/certificates/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
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
|
||||
in your clan and will make sure they trust your CA.
|
||||
|
||||
## Example Usage
|
||||
|
||||
The following configuration would add a CA for the top level domain `.foo`. If
|
||||
the machine `server` now hosts a webservice at `https://something.foo`, it will
|
||||
get a certificate from `ca` which is valid inside your clan. The machine
|
||||
`client` will trust this certificate if it makes a request to
|
||||
`https://something.foo`.
|
||||
|
||||
This clan service can be combined with the `coredns` service for easy to deploy,
|
||||
SSL secured clan-internal service hosting.
|
||||
|
||||
```nix
|
||||
inventory = {
|
||||
machines.ca = { };
|
||||
machines.client = { };
|
||||
machines.server = { };
|
||||
|
||||
instances."certificates" = {
|
||||
module.name = "certificates";
|
||||
module.input = "self";
|
||||
|
||||
roles.ca.machines.ca.settings.tlds = [ "foo" ];
|
||||
roles.default.machines.client = { };
|
||||
roles.default.machines.server = { };
|
||||
};
|
||||
};
|
||||
```
|
||||
245
clanServices/certificates/default.nix
Normal file
245
clanServices/certificates/default.nix
Normal file
@@ -0,0 +1,245 @@
|
||||
{ ... }:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "certificates";
|
||||
manifest.description = "Sets up a certificates internal to your Clan";
|
||||
manifest.categories = [ "Network" ];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.ca = {
|
||||
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
|
||||
options.acmeEmail = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "none@none.tld";
|
||||
description = ''
|
||||
Email address for account creation and correspondence from the CA.
|
||||
It is recommended to use the same email for all certs to avoid account
|
||||
creation limits.
|
||||
'';
|
||||
};
|
||||
|
||||
options.tlds = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "Top level domain for this CA. Certificates will be issued and trusted for *.<tld>";
|
||||
};
|
||||
|
||||
options.expire = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
description = "When the certificate should expire.";
|
||||
default = "8760h";
|
||||
example = "8760h";
|
||||
};
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{ settings, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
domains = map (tld: "ca.${tld}") settings.tlds;
|
||||
in
|
||||
{
|
||||
security.acme.defaults.email = settings.acmeEmail;
|
||||
security.acme = {
|
||||
certs = builtins.listToAttrs (
|
||||
map (domain: {
|
||||
name = domain;
|
||||
value = {
|
||||
server = "https://${domain}:1443/acme/acme/directory";
|
||||
};
|
||||
}) domains
|
||||
);
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80
|
||||
443
|
||||
];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
virtualHosts = builtins.listToAttrs (
|
||||
map (domain: {
|
||||
name = domain;
|
||||
value = {
|
||||
addSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "https://localhost:1443";
|
||||
locations."= /ca.crt".alias =
|
||||
config.clan.core.vars.generators.step-intermediate-cert.files."intermediate.crt".path;
|
||||
};
|
||||
}) domains
|
||||
);
|
||||
};
|
||||
|
||||
clan.core.vars.generators = {
|
||||
|
||||
# Intermediate key generator
|
||||
"step-intermediate-key" = {
|
||||
files."intermediate.key" = {
|
||||
secret = true;
|
||||
deploy = true;
|
||||
owner = "step-ca";
|
||||
group = "step-ca";
|
||||
};
|
||||
runtimeInputs = [ pkgs.step-cli ];
|
||||
script = ''
|
||||
step crypto keypair --kty EC --curve P-256 --no-password --insecure $out/intermediate.pub $out/intermediate.key
|
||||
'';
|
||||
};
|
||||
|
||||
# Intermediate certificate generator
|
||||
"step-intermediate-cert" = {
|
||||
files."intermediate.crt".secret = false;
|
||||
dependencies = [
|
||||
"step-ca"
|
||||
"step-intermediate-key"
|
||||
];
|
||||
runtimeInputs = [ pkgs.step-cli ];
|
||||
script = ''
|
||||
# Create intermediate certificate
|
||||
step certificate create \
|
||||
--ca $in/step-ca/ca.crt \
|
||||
--ca-key $in/step-ca/ca.key \
|
||||
--ca-password-file /dev/null \
|
||||
--key $in/step-intermediate-key/intermediate.key \
|
||||
--template ${pkgs.writeText "intermediate.tmpl" ''
|
||||
{
|
||||
"subject": {{ toJson .Subject }},
|
||||
"keyUsage": ["certSign", "crlSign"],
|
||||
"basicConstraints": {
|
||||
"isCA": true,
|
||||
"maxPathLen": 0
|
||||
},
|
||||
"nameConstraints": {
|
||||
"critical": true,
|
||||
"permittedDNSDomains": [${
|
||||
(lib.strings.concatStringsSep "," (map (tld: ''"${tld}"'') settings.tlds))
|
||||
}]
|
||||
}
|
||||
}
|
||||
''} ${lib.optionalString (settings.expire != null) "--not-after ${settings.expire}"} \
|
||||
--not-before=-12h \
|
||||
--no-password --insecure \
|
||||
"Clan Intermediate CA" \
|
||||
$out/intermediate.crt
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
services.step-ca = {
|
||||
enable = true;
|
||||
intermediatePasswordFile = "/dev/null";
|
||||
address = "0.0.0.0";
|
||||
port = 1443;
|
||||
settings = {
|
||||
root = config.clan.core.vars.generators.step-ca.files."ca.crt".path;
|
||||
crt = config.clan.core.vars.generators.step-intermediate-cert.files."intermediate.crt".path;
|
||||
key = config.clan.core.vars.generators.step-intermediate-key.files."intermediate.key".path;
|
||||
dnsNames = domains;
|
||||
logger.format = "text";
|
||||
db = {
|
||||
type = "badger";
|
||||
dataSource = "/var/lib/step-ca/db";
|
||||
};
|
||||
authority = {
|
||||
provisioners = [
|
||||
{
|
||||
type = "ACME";
|
||||
name = "acme";
|
||||
forceCN = true;
|
||||
}
|
||||
];
|
||||
claims = {
|
||||
maxTLSCertDuration = "2160h";
|
||||
defaultTLSCertDuration = "2160h";
|
||||
};
|
||||
backdate = "1m0s";
|
||||
};
|
||||
tls = {
|
||||
cipherSuites = [
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
|
||||
];
|
||||
minVersion = 1.2;
|
||||
maxVersion = 1.3;
|
||||
renegotiation = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Empty role, so we can add non-ca machins to the instance to trust the CA
|
||||
roles.default = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.acmeEmail = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "none@none.tld";
|
||||
description = ''
|
||||
Email address for account creation and correspondence from the CA.
|
||||
It is recommended to use the same email for all certs to avoid account
|
||||
creation limits.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{ settings, ... }:
|
||||
{
|
||||
nixosModule.security.acme.defaults.email = settings.acmeEmail;
|
||||
};
|
||||
};
|
||||
|
||||
# All machines (independent of role) will trust the CA
|
||||
perMachine.nixosModule =
|
||||
{ pkgs, config, ... }:
|
||||
{
|
||||
# Root CA generator
|
||||
clan.core.vars.generators = {
|
||||
"step-ca" = {
|
||||
share = true;
|
||||
files."ca.key" = {
|
||||
secret = true;
|
||||
deploy = false;
|
||||
};
|
||||
files."ca.crt".secret = false;
|
||||
runtimeInputs = [ pkgs.step-cli ];
|
||||
script = ''
|
||||
step certificate create --template ${pkgs.writeText "root.tmpl" ''
|
||||
{
|
||||
"subject": {{ toJson .Subject }},
|
||||
"issuer": {{ toJson .Subject }},
|
||||
"keyUsage": ["certSign", "crlSign"],
|
||||
"basicConstraints": {
|
||||
"isCA": true,
|
||||
"maxPathLen": 1
|
||||
}
|
||||
}
|
||||
''} "Clan Root CA" $out/ca.crt $out/ca.key \
|
||||
--kty EC --curve P-256 \
|
||||
--not-after=8760h \
|
||||
--not-before=-12h \
|
||||
--no-password --insecure
|
||||
'';
|
||||
};
|
||||
};
|
||||
security.pki.certificateFiles = [ config.clan.core.vars.generators."step-ca".files."ca.crt".path ];
|
||||
environment.systemPackages = [ pkgs.openssl ];
|
||||
security.acme.acceptTerms = true;
|
||||
};
|
||||
}
|
||||
21
clanServices/certificates/flake-module.nix
Normal file
21
clanServices/certificates/flake-module.nix
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
self,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix {
|
||||
inherit (self) packages;
|
||||
};
|
||||
in
|
||||
{
|
||||
clan.modules.certificates = module;
|
||||
perSystem =
|
||||
{ ... }:
|
||||
{
|
||||
clan.nixosTests.certificates = {
|
||||
imports = [ ./tests/vm/default.nix ];
|
||||
clan.modules.certificates = module;
|
||||
};
|
||||
};
|
||||
}
|
||||
84
clanServices/certificates/tests/vm/default.nix
Normal file
84
clanServices/certificates/tests/vm/default.nix
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
name = "certificates";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
|
||||
machines.ca = { }; # 192.168.1.1
|
||||
machines.client = { }; # 192.168.1.2
|
||||
machines.server = { }; # 192.168.1.3
|
||||
|
||||
instances."certificates" = {
|
||||
module.name = "certificates";
|
||||
module.input = "self";
|
||||
|
||||
roles.ca.machines.ca.settings.tlds = [ "foo" ];
|
||||
roles.default.machines.client = { };
|
||||
roles.default.machines.server = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nodes =
|
||||
let
|
||||
hostConfig = ''
|
||||
192.168.1.1 ca.foo
|
||||
192.168.1.3 test.foo
|
||||
'';
|
||||
in
|
||||
{
|
||||
|
||||
client.networking.extraHosts = hostConfig;
|
||||
ca.networking.extraHosts = hostConfig;
|
||||
|
||||
server = {
|
||||
|
||||
networking.extraHosts = hostConfig;
|
||||
|
||||
# TODO: Could this be set automatically?
|
||||
# I would like to get this information from the coredns module, but we
|
||||
# cannot model dependencies yet
|
||||
security.acme.certs."test.foo".server = "https://ca.foo/acme/acme/directory";
|
||||
|
||||
# Host a simple service on 'server', with SSL provided via our CA. 'client'
|
||||
# should be able to curl it via https and accept the certificates
|
||||
# presented
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80
|
||||
443
|
||||
];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."test.foo" = {
|
||||
enableACME = true;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
return = "200 'test server response'";
|
||||
extraConfig = "add_header Content-Type text/plain;";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
import time
|
||||
|
||||
time.sleep(3)
|
||||
ca.succeed("systemctl restart acme-order-renew-ca.foo.service ")
|
||||
|
||||
time.sleep(3)
|
||||
server.succeed("systemctl restart acme-test.foo.service")
|
||||
|
||||
# It takes a while for the correct certs to appear (before that self-signed
|
||||
# are presented by nginx) so we wait for a bit.
|
||||
client.wait_until_succeeds("curl -v https://test.foo")
|
||||
|
||||
# Show certificate information for debugging
|
||||
client.succeed("openssl s_client -connect test.foo:443 -servername test.foo </dev/null 2>/dev/null | openssl x509 -text -noout 1>&2")
|
||||
'';
|
||||
}
|
||||
6
clanServices/certificates/tests/vm/sops/machines/ca/key.json
Executable file
6
clanServices/certificates/tests/vm/sops/machines/ca/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age1yd2cden7jav8x4nzx2fwze2fsa5j0qm2m3t7zum765z3u4gj433q7dqj43",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
6
clanServices/certificates/tests/vm/sops/machines/client/key.json
Executable file
6
clanServices/certificates/tests/vm/sops/machines/client/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age1js225d8jc507sgcg0fdfv2x3xv3asm4ds5c6s4hp37nq8spxu95sc5x3ce",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
6
clanServices/certificates/tests/vm/sops/machines/server/key.json
Executable file
6
clanServices/certificates/tests/vm/sops/machines/server/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age1nwuh8lc604mnz5r8ku8zswyswnwv02excw237c0cmtlejp7xfp8sdrcwfa",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:6+XilULKRuWtAZ6B8Lj9UqCfi1T6dmqrDqBNXqS4SvBwM1bIWiL6juaT1Q7ByOexzID7tY740gmQBqTey54uLydh8mW0m4ZtUqw=,iv:9kscsrMPBGkutTnxrc5nrc7tQXpzLxw+929pUDKqTu0=,tag:753uIjm8ZRs0xsjiejEY8g==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1d3kycldZRXhmR0FqTXJp\nWWU0MDBYNmxxbFE5M2xKYm5KWnQ0MXBHNEM4CjN4RFFVcFlkd3pjTFVDQ3Vackdj\nVTVhMWoxdFpsWHp5S1p4L05kYk5LUkkKLS0tIENtZFZZTjY2amFVQmZLZFplQzBC\nZm1vWFI4MXR1ZHIxTTQ5VXdSYUhvOTQKte0bKjXQ0xA8FrpuChjDUvjVqp97D8kT\n3tVh6scdjxW48VSBZP1GRmqcMqCdj75GvJTbWeNEV4PDBW7GI0UW+Q==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-09-02T08:42:39Z",
|
||||
"mac": "ENC[AES256_GCM,data:AftMorrH7qX5ctVu5evYHn5h9pC4Mmm2VYaAV8Hy0PKTc777jNsL6DrxFVV3NVqtecpwrzZFWKgzukcdcRJe4veVeBrusmoZYtifH0AWZTEVpVlr2UXYYxCDmNZt1WHfVUo40bT//X6QM0ye6a/2Y1jYPbMbryQNcGmnpk9PDvU=,iv:5nk+d8hzA05LQp7ZHRbIgiENg2Ha6J6YzyducM6zcNU=,tag:dy1hqWVzMu/+fSK57h9ZCA==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../users/admin
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:jdTuGQUYvT1yXei1RHKsOCsABmMlkcLuziHDVhA7NequZeNu0fSbrJTXQDCHsDGhlYRcjU5EsEDT750xdleXuD3Gs9zWvPVobI4=,iv:YVow3K1j6fzRF9bRfIEpuOkO/nRpku/UQxWNGC+UJQQ=,tag:cNLM5R7uu6QpwPB9K6MYzg==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvOVF2WXRSL0NpQzFZR01I\nNU85TGcyQmVDazN1dmpuRFVTZEg5NDRKTGhrCk1IVjFSU1V6WHBVRnFWcHkyVERr\nTjFKbW1mQ2FWOWhjN2VPamMxVEQ5VkkKLS0tIENVUGlhanhuWGtDKzBzRmk2dE4v\nMXZBRXNMa3IrOTZTNHRUWVE3UXEwSWMK2cBLoL/H/Vxd/klVrqVLdX9Mww5j7gw/\nEWc5/hN+km6XoW+DiJxVG4qaJ7qqld6u5ZnKgJT+2h9CfjA04I2akg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-09-02T08:42:51Z",
|
||||
"mac": "ENC[AES256_GCM,data:zOBQVM2Ydu4v0+Fw3p3cEU+5+7eKaadV0tKro1JVOxclG1Vs6Myq57nw2eWf5JxIl0ulL+FavPKY26qOQ3aqcGOT3PMRlCda9z+0oSn9Im9bE/DzAGmoH/bp76kFkgTTOCZTMUoqJ+UJqv0qy1BH/92sSSKmYshEX6d1vr5ISrw=,iv:i9ZW4sLxOCan4UokHlySVr1CW39nCTusG4DmEPj/gIw=,tag:iZBDPHDkE3Vt5mFcFu1TPQ==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../users/admin
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:5CJuHcxJMXZJ8GqAeG3BrbWtT1kade4kxgJsn1cRpmr1UgN0ZVYnluPEiBscClNSOzcc6vcrBpfTI3dj1tASKTLP58M+GDBFQDo=,iv:gsK7XqBGkYCoqAvyFlIXuJ27PKSbTmy7f6cgTmT2gow=,tag:qG5KejkBvy9ytfhGXa/Mnw==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxbzVqYkplTzJKN1pwS3VM\naFFIK2VsR3lYUVExYW9ieERBL0tlcFZtVzJRCkpiLzdmWmFlOUZ5QUJ4WkhXZ2tQ\nZm92YXBCV0RpYnIydUdEVTRiamI4bjAKLS0tIG93a2htS1hFcjBOeVFnNCtQTHVr\na2FPYjVGbWtORjJVWXE5bndPU1RWcXMKikMEB7X+kb7OtiyqXn3HRpLYkCdoayDh\n7cjGnplk17q25/lRNHM4JVS5isFfuftCl01enESqkvgq+cwuFwa9DQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-09-02T08:42:59Z",
|
||||
"mac": "ENC[AES256_GCM,data:xybV2D0xukZnH2OwRpIugPnS7LN9AbgGKwFioPJc1FQWx9TxMUVDwgMN6V5WrhWkXgF2zP4krtDYpEz4Vq+LbOjcnTUteuCc+7pMHubuRuip7j+M32MH1kuf4bVZuXbCfvm7brGxe83FzjoioLqzA8g/X6Q1q7/ErkNeFjluC3Q=,iv:QEW3EUKSRZY3fbXlP7z+SffWkQeXwMAa5K8RQW7NvPE=,tag:DhFxY7xr7H1Wbd527swD0Q==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../users/admin
|
||||
@@ -0,0 +1,12 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBsDCCAVegAwIBAgIQbT1Ivm+uwyf0HNkJfan2BTAKBggqhkjOPQQDAjAXMRUw
|
||||
EwYDVQQDEwxDbGFuIFJvb3QgQ0EwHhcNMjUwOTAxMjA0MzAzWhcNMjYwOTAyMDg0
|
||||
MzAzWjAfMR0wGwYDVQQDExRDbGFuIEludGVybWVkaWF0ZSBDQTBZMBMGByqGSM49
|
||||
AgEGCCqGSM49AwEHA0IABDXCNrUIotju9P1U6JxLV43sOxLlRphQJS4dM+lvjTZc
|
||||
aQ+HwQg0AHVlQNRwS3JqKrJJtJVyKbZklh6eFaDPoj6jfTB7MA4GA1UdDwEB/wQE
|
||||
AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRKHaccHgP2ccSWVBWN
|
||||
zGoDdTg7aTAfBgNVHSMEGDAWgBSfsnz4phMJx9su/kgeF/FbZQCBgzAVBgNVHR4B
|
||||
Af8ECzAJoAcwBYIDZm9vMAoGCCqGSM49BAMCA0cAMEQCICiUDk1zGNzpS/iVKLfW
|
||||
zUGaCagpn2mCx4xAXQM9UranAiAn68nVYGWjkzhU31wyCAupxOjw7Bt96XXqIAz9
|
||||
hLLtMA==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/ca
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:Auonh9fa7jSkld1Zyxw74x5ydj6Xc+0SOgiqumVETNCfner9K96Rmv1PkREuHNGWPsnzyEM3pRT8ijvu3QoKvy9QPCCewyT07Wqe4G74+bk1iMeAHsV3To6kHs6M8OISvE+CmG0+hlLmdfRSabTzyWPLHbOjvFTEEuA5G7xiryacSYOE++eeEHdn+oUDh/IMTcfLjCGMjsXFikx1Hb+ofeRTlCg47+0w4MXVvQkOzQB5V2C694jZXvZ19jd/ioqr8YASz2xatGvqwW6cpZxqOWyZJ0UAj/6yFk6tZWifqVB3wgU=,iv:ITFCrDkeWl4GWCebVq15ei9QmkOLDwUIYojKZ2TU6JU=,tag:8k4iYbCIusUykY79H86WUQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsT25UbjJTQ2tzbnQyUm9p\neWx1UlZIeVpocnBqUCt0YnFlN2FOU25Lb0hNCmdXUUsyalRTbHRRQ0NLSGc1YllV\nUXRwaENhaXU1WmdnVDE0UWprUUUyeDAKLS0tIHV3dHU3aG5JclM0V3FadzN0SU14\ndFptbEJUNXQ4QVlqbkJ1TjAvdDQwSGsKcKPWUjhK7wzIpdIdksMShF2fpLdDTUBS\nZiU7P1T+3psxad9qhapvU0JrAY+9veFaYVEHha2aN/XKs8HqUcTp3A==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1yd2cden7jav8x4nzx2fwze2fsa5j0qm2m3t7zum765z3u4gj433q7dqj43",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjZFVteVZwVGVmRE9NT3hG\nNGMyS3FSaXluM1FpeUp6SDVMUEpwYzg5SmdvCkRPU0QyU1JicGNkdlMyQWVkT0k3\nL2YrbDhWeGk4WFhxcUFmTmhZQ0pEQncKLS0tIG85Ui9rKzBJQ2VkMFBUQTMvSTlu\nbm8rZ09Wa24rQkNvTTNtYTZBN3MrZlkK7cjNhlUKZdOrRq/nKUsbUQgNTzX8jO+0\nzADpz6WCMvsJ15xazc10BGh03OtdMWl5tcoWMaZ71HWtI9Gip5DH0w==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-09-02T08:42:42Z",
|
||||
"mac": "ENC[AES256_GCM,data:9xlO5Yis8DG/y8GjvP63NltD4xEL7zqdHL2cQE8gAoh/ZamAmK5ZL0ld80mB3eIYEPKZYvmUYI4Lkrge2ZdqyDoubrW+eJ3dxn9+StxA9FzXYwUE0t+bbsNJfOOp/kDojf060qLGsu0kAGKd2ca4WiDccR0Cieky335C7Zzhi/Q=,iv:bWQ4wr0CJHSN+6ipUbkYTDWZJyFQjDKszfpVX9EEUsY=,tag:kADIFgJBEGCvr5fPbbdEDA==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -0,0 +1 @@
|
||||
25.11
|
||||
@@ -0,0 +1,10 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBcTCCARigAwIBAgIRAIix99+AE7Y+uyiLGaRHEhUwCgYIKoZIzj0EAwIwFzEV
|
||||
MBMGA1UEAxMMQ2xhbiBSb290IENBMB4XDTI1MDkwMTIwNDI1N1oXDTI2MDkwMjA4
|
||||
NDI1N1owFzEVMBMGA1UEAxMMQ2xhbiBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZI
|
||||
zj0DAQcDQgAEk7nn9kzxI+xkRmNMlxD+7T78UqV3aqus0foJh6uu1CHC+XaebMcw
|
||||
JN95nAe3oYA3yZG6Mnq9nCxsYha4EhzGYqNFMEMwDgYDVR0PAQH/BAQDAgEGMBIG
|
||||
A1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFJ+yfPimEwnH2y7+SB4X8VtlAIGD
|
||||
MAoGCCqGSM49BAMCA0cAMEQCIBId/CcbT5MPFL90xa+XQz+gVTdRwsu6Bg7ehMso
|
||||
Bj0oAiBjSlttd5yeuZGXBm+O0Gl+WdKV60QlrWutNewXFS4UpQ==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:PnEXteU3I7U0OKgE+oR3xjHdLWYTpJjM/jlzxtGU0uP2pUBuQv3LxtEz+cP0ZsafHLNq2iNJ7xpUEE0g4d3M296S56oSocK3fREWBiJFiaC7SAEUiil1l3UCwHn7LzmdEmn8Kq7T+FK89wwqtVWIASLo2gZC/yHE5eEanEATTchGLSNiHJRzZ8n0Ekm8EFUA6czOqA5nPQHaSmeLzu1g80lSSi1ICly6dJksa6DVucwOyVFYFEeq8Dfyc1eyP8L1ee0D7QFYBMduYOXTKPtNnyDmdaQMj7cMMvE7fn04idIiAqw=,iv:nvLmAfFk2GXnnUy+Afr648R60Ou13eu9UKykkiA8Y+4=,tag:lTTAxfG0EDCU6u7xlW6xSQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEMjNWUm5NbktQeTRWRjJE\nWWFZc2Rsa3I5aitPSno1WnhORENNcng5OHprCjNUQVhBVHFBcWFjaW5UdmxKTnZw\nQlI4MDk5Wkp0RElCeWgzZ2dFQkF2dkkKLS0tIDVreTkydnJ0RDdHSHlQeVV6bGlP\nTmpJOVBSb2dkVS9TZG5SRmFjdnQ1b3cKQ5XvwH1jD4XPVs5RzOotBDq8kiE6S5k2\nDBv6ugjsM5qV7/oGP9H69aSB4jKPZjEn3yiNw++Oorc8uXd5kSGh7w==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-09-02T08:43:00Z",
|
||||
"mac": "ENC[AES256_GCM,data:3jFf66UyZUWEtPdPu809LCS3K/Hc6zbnluystl3eXS+KGI+dCoYmN9hQruRNBRxf6jli2RIlArmmEPBDQVt67gG/qugTdT12krWnYAZ78iocmOnkf44fWxn/pqVnn4JYpjEYRgy8ueGDnUkwvpGWVZpcXw5659YeDQuYOJ2mq0U=,iv:3k7fBPrABdLItQ2Z+Mx8Nx0eIEKo93zG/23K+Q5Hl3I=,tag:aehAObdx//DEjbKlOeM7iQ==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../sops/users/admin
|
||||
68
clanServices/coredns/README.md
Normal file
68
clanServices/coredns/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
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
|
||||
accessible under `http://<service>.clan` in your browser.
|
||||
|
||||
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
|
||||
external queries are resolved as normal via DHCP. Second, it allows exposing
|
||||
services (see example below).
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here the machine `dnsserver` is designated as internal DNS-server for the TLD
|
||||
`.foo`. `server01` will host an application that shall be reachable at
|
||||
`http://one.foo` and `server02` is going to be reachable at `http://two.foo`.
|
||||
`client` is any other machine that is part of the clan but does not host any
|
||||
services.
|
||||
|
||||
When `client` tries to resolve `http://one.foo`, the DNS query will be
|
||||
routed to `dnsserver`, which will answer with `192.168.1.3`. If it tries to
|
||||
resolve some external domain (e.g. `https://clan.lol`), the query will not be
|
||||
routed to `dnsserver` but resolved as before, via the nameservers advertised by
|
||||
DHCP.
|
||||
|
||||
```nix
|
||||
inventory = {
|
||||
|
||||
machines = {
|
||||
dnsserver = { }; # 192.168.1.2
|
||||
server01 = { }; # 192.168.1.3
|
||||
server02 = { }; # 192.168.1.4
|
||||
client = { }; # 192.168.1.5
|
||||
};
|
||||
|
||||
instances = {
|
||||
coredns = {
|
||||
|
||||
module.name = "@clan/coredns";
|
||||
module.input = "self";
|
||||
|
||||
# Add the default role to all machines, including `client`
|
||||
roles.default.tags.all = { };
|
||||
|
||||
# DNS server
|
||||
roles.server.machines."dnsserver".settings = {
|
||||
ip = "192.168.1.2";
|
||||
tld = "foo";
|
||||
};
|
||||
|
||||
# First service
|
||||
roles.default.machines."server01".settings = {
|
||||
ip = "192.168.1.3";
|
||||
services = [ "one" ];
|
||||
};
|
||||
|
||||
# Second service
|
||||
roles.default.machines."server02".settings = {
|
||||
ip = "192.168.1.4";
|
||||
services = [ "two" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
176
clanServices/coredns/default.nix
Normal file
176
clanServices/coredns/default.nix
Normal file
@@ -0,0 +1,176 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "coredns";
|
||||
manifest.description = "Clan-internal DNS and service exposure";
|
||||
manifest.categories = [ "Network" ];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.server = {
|
||||
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.tld = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "clan";
|
||||
description = ''
|
||||
Top-level domain for this instance. All services below this will be
|
||||
resolved internally.
|
||||
'';
|
||||
};
|
||||
|
||||
options.ip = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
# TODO: Set a default
|
||||
description = "IP for the DNS to listen on";
|
||||
};
|
||||
|
||||
options.dnsPort = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 1053;
|
||||
description = "Port of the clan-internal DNS server";
|
||||
};
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{
|
||||
roles,
|
||||
settings,
|
||||
...
|
||||
}:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ settings.dnsPort ];
|
||||
networking.firewall.allowedUDPPorts = [ settings.dnsPort ];
|
||||
|
||||
services.coredns =
|
||||
let
|
||||
|
||||
# Get all service entries for one host
|
||||
hostServiceEntries =
|
||||
host:
|
||||
lib.strings.concatStringsSep "\n" (
|
||||
map (
|
||||
service: "${service} IN A ${roles.default.machines.${host}.settings.ip} ; ${host}"
|
||||
) roles.default.machines.${host}.settings.services
|
||||
);
|
||||
|
||||
zonefile = pkgs.writeTextFile {
|
||||
name = "db.${settings.tld}";
|
||||
text = ''
|
||||
$TTL 3600
|
||||
@ IN SOA ns.${settings.tld}. admin.${settings.tld}. 1 7200 3600 1209600 3600
|
||||
IN NS ns.${settings.tld}.
|
||||
ns IN A ${settings.ip} ; DNS server
|
||||
|
||||
''
|
||||
+ (lib.strings.concatStringsSep "\n" (
|
||||
map (host: hostServiceEntries host) (lib.attrNames roles.default.machines)
|
||||
));
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
enable = true;
|
||||
config =
|
||||
|
||||
let
|
||||
dnsPort = builtins.toString settings.dnsPort;
|
||||
in
|
||||
|
||||
''
|
||||
.:${dnsPort} {
|
||||
forward . 1.1.1.1
|
||||
cache 30
|
||||
}
|
||||
|
||||
${settings.tld}:${dnsPort} {
|
||||
file ${zonefile}
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
roles.default = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.services = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Service endpoints this host exposes (without TLD). Each entry will
|
||||
be resolved to <entry>.<tld> using the configured top-level domain.
|
||||
'';
|
||||
};
|
||||
|
||||
options.ip = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
# TODO: Set a default
|
||||
description = "IP on which the services will listen";
|
||||
};
|
||||
|
||||
options.dnsPort = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 1053;
|
||||
description = "Port of the clan-internal DNS server";
|
||||
};
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{ roles, settings, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{ lib, ... }:
|
||||
{
|
||||
|
||||
networking.nameservers = map (m: "127.0.0.1:5353#${roles.server.machines.${m}.settings.tld}") (
|
||||
lib.attrNames roles.server.machines
|
||||
);
|
||||
|
||||
services.resolved.domains = map (m: "~${roles.server.machines.${m}.settings.tld}") (
|
||||
lib.attrNames roles.server.machines
|
||||
);
|
||||
|
||||
services.unbound = {
|
||||
enable = true;
|
||||
settings = {
|
||||
server = {
|
||||
port = 5353;
|
||||
verbosity = 2;
|
||||
interface = [ "127.0.0.1" ];
|
||||
access-control = [ "127.0.0.0/8 allow" ];
|
||||
do-not-query-localhost = "no";
|
||||
domain-insecure = map (m: "${roles.server.machines.${m}.settings.tld}.") (
|
||||
lib.attrNames roles.server.machines
|
||||
);
|
||||
};
|
||||
|
||||
# Default: forward everything else to DHCP-provided resolvers
|
||||
forward-zone = [
|
||||
{
|
||||
name = ".";
|
||||
forward-addr = "127.0.0.53@53"; # Forward to systemd-resolved
|
||||
}
|
||||
];
|
||||
stub-zone = map (m: {
|
||||
name = "${roles.server.machines.${m}.settings.tld}.";
|
||||
stub-addr = "${roles.server.machines.${m}.settings.ip}@${builtins.toString settings.dnsPort}";
|
||||
}) (lib.attrNames roles.server.machines);
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -3,14 +3,16 @@ let
|
||||
module = lib.modules.importApply ./default.nix { };
|
||||
in
|
||||
{
|
||||
clan.modules.state-version = module;
|
||||
clan.modules = {
|
||||
coredns = module;
|
||||
};
|
||||
perSystem =
|
||||
{ ... }:
|
||||
{
|
||||
clan.nixosTests.state-version = {
|
||||
clan.nixosTests.coredns = {
|
||||
imports = [ ./tests/vm/default.nix ];
|
||||
|
||||
clan.modules."@clan/state-version" = module;
|
||||
clan.modules."@clan/coredns" = module;
|
||||
};
|
||||
};
|
||||
}
|
||||
110
clanServices/coredns/tests/vm/default.nix
Normal file
110
clanServices/coredns/tests/vm/default.nix
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
...
|
||||
}:
|
||||
{
|
||||
name = "coredns";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
test.useContainers = true;
|
||||
inventory = {
|
||||
|
||||
machines = {
|
||||
dns = { }; # 192.168.1.2
|
||||
server01 = { }; # 192.168.1.3
|
||||
server02 = { }; # 192.168.1.4
|
||||
client = { }; # 192.168.1.1
|
||||
};
|
||||
|
||||
instances = {
|
||||
coredns = {
|
||||
|
||||
module.name = "@clan/coredns";
|
||||
module.input = "self";
|
||||
|
||||
roles.default.tags.all = { };
|
||||
|
||||
# First service
|
||||
roles.default.machines."server01".settings = {
|
||||
ip = "192.168.1.3";
|
||||
services = [ "one" ];
|
||||
};
|
||||
|
||||
# Second service
|
||||
roles.default.machines."server02".settings = {
|
||||
ip = "192.168.1.4";
|
||||
services = [ "two" ];
|
||||
};
|
||||
|
||||
# DNS server
|
||||
roles.server.machines."dns".settings = {
|
||||
ip = "192.168.1.2";
|
||||
tld = "foo";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nodes = {
|
||||
dns =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [ pkgs.net-tools ];
|
||||
};
|
||||
|
||||
client =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [ pkgs.net-tools ];
|
||||
};
|
||||
|
||||
server01 = {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."one.foo" = {
|
||||
locations."/" = {
|
||||
return = "200 'test server response one'";
|
||||
extraConfig = "add_header Content-Type text/plain;";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
server02 = {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."two.foo" = {
|
||||
locations."/" = {
|
||||
return = "200 'test server response two'";
|
||||
extraConfig = "add_header Content-Type text/plain;";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import json
|
||||
start_all()
|
||||
|
||||
machines = [server01, server02, dns, client]
|
||||
|
||||
for m in machines:
|
||||
m.systemctl("start network-online.target")
|
||||
|
||||
for m in machines:
|
||||
m.wait_for_unit("network-online.target")
|
||||
|
||||
# This should work, but is borken in tests i think? Instead we dig directly
|
||||
|
||||
# client.succeed("curl -k -v http://one.foo")
|
||||
# client.succeed("curl -k -v http://two.foo")
|
||||
|
||||
answer = client.succeed("dig @192.168.1.2 -p 1053 one.foo")
|
||||
assert "192.168.1.3" in answer, "IP not found"
|
||||
|
||||
answer = client.succeed("dig @192.168.1.2 -p 1053 two.foo")
|
||||
assert "192.168.1.4" in answer, "IP not found"
|
||||
|
||||
'';
|
||||
}
|
||||
4
clanServices/coredns/tests/vm/sops/users/admin/key.json
Normal file
4
clanServices/coredns/tests/vm/sops/users/admin/key.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"type": "age"
|
||||
}
|
||||
@@ -3,13 +3,20 @@
|
||||
|
||||
# The test for this module in ./tests/vm/default.nix shows an example of how
|
||||
# the service is used.
|
||||
|
||||
{ packages }:
|
||||
{ ... }:
|
||||
{ ... }@service:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/hello-word";
|
||||
manifest.name = "clan-core/hello-world";
|
||||
manifest.description = "This is a test";
|
||||
manifest.dependencies = {
|
||||
#
|
||||
home-manager = {
|
||||
recomendedUrl = "github:nix-community/home-manager/release-25.05";
|
||||
};
|
||||
};
|
||||
|
||||
# Declare dependencies that the user must provide via flake inputs
|
||||
# Or via 'clan.serviceOverrides.<manifest-name>.dependencies.resolved
|
||||
|
||||
# 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
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix {
|
||||
inherit (self) packages;
|
||||
};
|
||||
module = ./default.nix;
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
|
||||
@@ -56,6 +56,11 @@
|
||||
systemd.services.telegraf-json = {
|
||||
enable = true;
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "telegraf.service" ];
|
||||
wants = [ "telegraf.service" ];
|
||||
serviceConfig = {
|
||||
Restart = "on-failure";
|
||||
};
|
||||
script = "${pkgs.miniserve}/bin/miniserve -p 9990 ${jsonpath} --auth-file ${config.clan.core.vars.generators.telegraf.files.miniserve-auth.path}";
|
||||
};
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
This service generates the `system.stateVersion` of the nixos installation
|
||||
automatically.
|
||||
|
||||
Possible values:
|
||||
[system.stateVersion](https://search.nixos.org/options?channel=unstable&show=system.stateVersion&from=0&size=50&sort=relevance&type=packages&query=stateVersion)
|
||||
|
||||
## Usage
|
||||
|
||||
The following configuration will set `stateVersion` for all machines:
|
||||
|
||||
```
|
||||
inventory.instances = {
|
||||
state-version = {
|
||||
module = {
|
||||
name = "state-version";
|
||||
input = "clan";
|
||||
};
|
||||
roles.default.tags.all = { };
|
||||
};
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
If you are already setting `system.stateVersion`, either let the automatic
|
||||
generation happen, or trigger the generation manually for the machine. The
|
||||
service will take the specified version, if one is already supplied through the
|
||||
config.
|
||||
|
||||
To manually generate the version for a specified machine run:
|
||||
|
||||
```
|
||||
clan vars generate [MACHINE]
|
||||
```
|
||||
|
||||
If the setting was already set, you can then remove `system.stateVersion` from
|
||||
your machine configuration. For new machines, just import the service as shown
|
||||
above.
|
||||
@@ -1,50 +0,0 @@
|
||||
{ ... }:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/state-version";
|
||||
manifest.description = "Automatically generate the state version of the nixos installation.";
|
||||
manifest.categories = [ "System" ];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.default = {
|
||||
|
||||
perInstance =
|
||||
{ ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
var = config.clan.core.vars.generators.state-version.files.version or { };
|
||||
in
|
||||
{
|
||||
|
||||
warnings = [
|
||||
''
|
||||
The clan.state-version service is deprecated and will be
|
||||
removed on 2025-07-15 in favor of a nix option.
|
||||
|
||||
Please migrate your configuration to use `clan.core.settings.state-version.enable = true` instead.
|
||||
''
|
||||
];
|
||||
|
||||
system.stateVersion = lib.mkDefault (lib.removeSuffix "\n" var.value);
|
||||
|
||||
clan.core.vars.generators.state-version = {
|
||||
files.version = {
|
||||
secret = false;
|
||||
value = lib.mkDefault config.system.nixos.release;
|
||||
};
|
||||
runtimeInputs = [ ];
|
||||
script = ''
|
||||
echo -n ${config.system.stateVersion} > "$out"/version
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
name = "service-state-version";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
machines.server = { };
|
||||
instances.default = {
|
||||
module.name = "@clan/state-version";
|
||||
module.input = "self";
|
||||
roles.default.machines."server" = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nodes.server = { };
|
||||
|
||||
testScript = lib.mkDefault ''
|
||||
start_all()
|
||||
'';
|
||||
}
|
||||
@@ -7,7 +7,7 @@ inventory.instances = {
|
||||
clan-cache = {
|
||||
module = {
|
||||
name = "trusted-nix-caches";
|
||||
input = "clan";
|
||||
input = "clan-core";
|
||||
};
|
||||
roles.default.machines.draper = { };
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -12,6 +12,11 @@ import ipaddress
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Constants for argument count validation
|
||||
MIN_ARGS_BASE = 4
|
||||
MIN_ARGS_CONTROLLER = 5
|
||||
MIN_ARGS_PEER = 5
|
||||
|
||||
|
||||
def hash_string(s: str) -> str:
|
||||
"""Generate SHA256 hash of string."""
|
||||
@@ -39,8 +44,7 @@ def generate_ula_prefix(instance_name: str) -> ipaddress.IPv6Network:
|
||||
prefix = f"fd{prefix_bits:08x}"
|
||||
prefix_formatted = f"{prefix[:4]}:{prefix[4:8]}::/40"
|
||||
|
||||
network = ipaddress.IPv6Network(prefix_formatted)
|
||||
return network
|
||||
return ipaddress.IPv6Network(prefix_formatted)
|
||||
|
||||
|
||||
def generate_controller_subnet(
|
||||
@@ -60,9 +64,7 @@ def generate_controller_subnet(
|
||||
# The controller subnet is at base_prefix:controller_id::/56
|
||||
base_int = int(base_network.network_address)
|
||||
controller_subnet_int = base_int | (controller_id << (128 - 56))
|
||||
controller_subnet = ipaddress.IPv6Network((controller_subnet_int, 56))
|
||||
|
||||
return controller_subnet
|
||||
return ipaddress.IPv6Network((controller_subnet_int, 56))
|
||||
|
||||
|
||||
def generate_peer_suffix(peer_name: str) -> str:
|
||||
@@ -76,12 +78,11 @@ def generate_peer_suffix(peer_name: str) -> str:
|
||||
suffix_bits = h[:16]
|
||||
|
||||
# Format as IPv6 suffix without leading colon
|
||||
suffix = f"{suffix_bits[0:4]}:{suffix_bits[4:8]}:{suffix_bits[8:12]}:{suffix_bits[12:16]}"
|
||||
return suffix
|
||||
return f"{suffix_bits[0:4]}:{suffix_bits[4:8]}:{suffix_bits[8:12]}:{suffix_bits[12:16]}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 4:
|
||||
if len(sys.argv) < MIN_ARGS_BASE:
|
||||
print(
|
||||
"Usage: ipv6_allocator.py <output_dir> <instance_name> <controller|peer> <machine_name>",
|
||||
)
|
||||
@@ -95,7 +96,7 @@ def main() -> None:
|
||||
base_network = generate_ula_prefix(instance_name)
|
||||
|
||||
if node_type == "controller":
|
||||
if len(sys.argv) < 5:
|
||||
if len(sys.argv) < MIN_ARGS_CONTROLLER:
|
||||
print("Controller name required")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -111,7 +112,7 @@ def main() -> None:
|
||||
(output_dir / "prefix").write_text(prefix_str)
|
||||
|
||||
elif node_type == "peer":
|
||||
if len(sys.argv) < 5:
|
||||
if len(sys.argv) < MIN_ARGS_PEER:
|
||||
print("Peer name required")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" ]
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
24
devFlake/flake.lock
generated
24
devFlake/flake.lock
generated
@@ -3,10 +3,10 @@
|
||||
"clan-core-for-checks": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1756081310,
|
||||
"narHash": "sha256-wj1H5Pr6w4AsB+nG3K07SgSIDZ7jDCkGnh5XXWLdtk8=",
|
||||
"lastModified": 1756166884,
|
||||
"narHash": "sha256-skg4rwpbCjhpLlrv/Pndd43FoEgrJz98WARtGLhCSzo=",
|
||||
"ref": "main",
|
||||
"rev": "7b926d43dc361cd8d3ad3c14a2e7e75375b7d215",
|
||||
"rev": "f7414d7e6e58709af27b6fe16eb530278e81eaaf",
|
||||
"shallow": true,
|
||||
"type": "git",
|
||||
"url": "https://git.clan.lol/clan/clan-core"
|
||||
@@ -84,11 +84,11 @@
|
||||
},
|
||||
"nixpkgs-dev": {
|
||||
"locked": {
|
||||
"lastModified": 1756050191,
|
||||
"narHash": "sha256-lMtTT4rv5On7D0P4Z+k7UkvbAKKuVGRbJi/VJeRCQwI=",
|
||||
"lastModified": 1757752761,
|
||||
"narHash": "sha256-HBM2YTKSegLZjdamfqH9KADj2zQBQBNQHmwdrYkatpg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "759dcc6981cd4aa222d36069f78fe7064d563305",
|
||||
"rev": "4b46c744cbd5f9336027dff287e74ead84d80041",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -107,11 +107,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755555503,
|
||||
"narHash": "sha256-WiOO7GUOsJ4/DoMy2IC5InnqRDSo2U11la48vCCIjjY=",
|
||||
"lastModified": 1757624466,
|
||||
"narHash": "sha256-25ExS2AkQD05Jf0Y2Wnn5KHpucN2d3ObEQOVaDh7ubg=",
|
||||
"owner": "NuschtOS",
|
||||
"repo": "search",
|
||||
"rev": "6f3efef888b92e6520f10eae15b86ff537e1d2ea",
|
||||
"rev": "da8bcb74407e41d334fc79081fdd8948b795bd6f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -165,11 +165,11 @@
|
||||
"nixpkgs": []
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755934250,
|
||||
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
|
||||
"lastModified": 1756662192,
|
||||
"narHash": "sha256-F1oFfV51AE259I85av+MAia221XwMHCOtZCMcZLK2Jk=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
|
||||
"rev": "1aabc6c05ccbcbf4a635fb7a90400e44282f61c4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
self'.packages.tea-create-pr
|
||||
self'.packages.merge-after-ci
|
||||
self'.packages.pending-reviews
|
||||
self'.packages.agit
|
||||
# treefmt with config defined in ./flake-parts/formatting.nix
|
||||
config.treefmt.build.wrapper
|
||||
];
|
||||
@@ -46,7 +45,7 @@
|
||||
ln -sfT ${inputs.nix-select} "$PRJ_ROOT/pkgs/clan-cli/clan_lib/select"
|
||||
|
||||
# Generate classes.py from schemas
|
||||
${self'.packages.classgen}/bin/classgen ${self'.legacyPackages.schemas.clan-schema-abstract}/schema.json $PRJ_ROOT/pkgs/clan-cli/clan_lib/nix_models/clan.py
|
||||
${self'.packages.classgen}/bin/classgen ${self'.legacyPackages.schemas.clanSchemaJson}/schema.json $PRJ_ROOT/pkgs/clan-cli/clan_lib/nix_models/clan.py
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
suffix = config.clan.core.vars.generators.disk-id.files.diskId.value;
|
||||
mirrorBoot = idx: {
|
||||
# suffix is to prevent disk name collisions
|
||||
name = idx + suffix;
|
||||
name = idx;
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${idx}";
|
||||
content = {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
suffix = config.clan.core.vars.generators.disk-id.files.diskId.value;
|
||||
mirrorBoot = idx: {
|
||||
# suffix is to prevent disk name collisions
|
||||
name = idx + suffix;
|
||||
name = idx;
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${idx}";
|
||||
content = {
|
||||
|
||||
@@ -2,7 +2,7 @@ site_name: Clan Documentation
|
||||
site_url: https://docs.clan.lol
|
||||
repo_url: https://git.clan.lol/clan/clan-core/
|
||||
repo_name: "_>"
|
||||
edit_uri: _edit/main/docs/docs/
|
||||
edit_uri: _edit/main/docs/site/
|
||||
|
||||
validation:
|
||||
omitted_files: warn
|
||||
@@ -59,14 +59,22 @@ nav:
|
||||
- Configure Disk Config: guides/getting-started/choose-disk.md
|
||||
- Update Machine: guides/getting-started/update.md
|
||||
- Continuous Integration: guides/getting-started/flake-check.md
|
||||
- Using Services: guides/clanServices.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
|
||||
- Secure Boot: guides/secure-boot.md
|
||||
- How to disable Secure Boot: guides/secure-boot.md
|
||||
- Flake-parts: guides/flake-parts.md
|
||||
- macOS: guides/macos.md
|
||||
- Contributing:
|
||||
@@ -77,13 +85,11 @@ nav:
|
||||
- Writing a Service Module: guides/services/community.md
|
||||
- Writing a Disko Template: guides/disko-templates/community.md
|
||||
- Migrations:
|
||||
- Migrate existing Flakes: guides/migrations/migration-guide.md
|
||||
- Migrate from clan modules to services: guides/migrations/migrate-inventory-services.md
|
||||
- Facts Vars Migration: guides/migrations/migration-facts-vars.md
|
||||
- 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:
|
||||
@@ -94,6 +100,8 @@ nav:
|
||||
- reference/clanServices/index.md
|
||||
- reference/clanServices/admin.md
|
||||
- reference/clanServices/borgbackup.md
|
||||
- reference/clanServices/certificates.md
|
||||
- reference/clanServices/coredns.md
|
||||
- reference/clanServices/data-mesher.md
|
||||
- reference/clanServices/dyndns.md
|
||||
- reference/clanServices/emergency-access.md
|
||||
@@ -106,7 +114,6 @@ nav:
|
||||
- reference/clanServices/monitoring.md
|
||||
- reference/clanServices/packages.md
|
||||
- reference/clanServices/sshd.md
|
||||
- reference/clanServices/state-version.md
|
||||
- reference/clanServices/syncthing.md
|
||||
- reference/clanServices/trusted-nix-caches.md
|
||||
- reference/clanServices/users.md
|
||||
@@ -173,6 +180,7 @@ theme:
|
||||
- content.code.annotate
|
||||
- content.code.copy
|
||||
- content.tabs.link
|
||||
- content.action.edit
|
||||
icon:
|
||||
repo: fontawesome/brands/git
|
||||
custom_dir: overrides
|
||||
@@ -216,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
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Module for rendering NixOS options documentation from JSON format."""
|
||||
|
||||
# Options are available in the following format:
|
||||
# https://github.com/nixos/nixpkgs/blob/master/nixos/lib/make-options-doc/default.nix
|
||||
#
|
||||
@@ -46,7 +48,7 @@ CLAN_SERVICE_INTERFACE = os.environ.get("CLAN_SERVICE_INTERFACE")
|
||||
|
||||
CLAN_MODULES_VIA_SERVICE = os.environ.get("CLAN_MODULES_VIA_SERVICE")
|
||||
|
||||
OUT = os.environ.get("out")
|
||||
OUT = os.environ.get("out") # noqa: SIM112
|
||||
|
||||
|
||||
def sanitize(text: str) -> str:
|
||||
@@ -173,9 +175,11 @@ def print_options(
|
||||
res += head if len(options.items()) else no_options
|
||||
for option_name, info in options.items():
|
||||
if replace_prefix:
|
||||
option_name = option_name.replace(replace_prefix + ".", "")
|
||||
display_name = option_name.replace(replace_prefix + ".", "")
|
||||
else:
|
||||
display_name = option_name
|
||||
|
||||
res += render_option(option_name, info, 4)
|
||||
res += render_option(display_name, info, 4)
|
||||
return res
|
||||
|
||||
|
||||
@@ -547,8 +551,7 @@ def options_docs_from_tree(
|
||||
|
||||
return output
|
||||
|
||||
md = render_tree(root)
|
||||
return md
|
||||
return render_tree(root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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" = {};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -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" = {
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
# Using `clanServices`
|
||||
# Using the Inventory
|
||||
|
||||
Clan's `clanServices` system is a composable way to define and deploy services across machines.
|
||||
Clan's inventory system is a composable way to define and deploy services across
|
||||
machines.
|
||||
|
||||
This guide shows how to **instantiate** a `clanService`, explains how service definitions are structured in your inventory, and how to pick or create services from modules exposed by flakes.
|
||||
This guide shows how to **instantiate** a `clanService`, explains how service
|
||||
definitions are structured in your inventory, and how to pick or create services
|
||||
from modules exposed by flakes.
|
||||
|
||||
The term **Multi-host-modules** was introduced previously in the [nixus repository](https://github.com/infinisil/nixus) and represents a similar concept.
|
||||
The term **Multi-host-modules** was introduced previously in the [nixus
|
||||
repository](https://github.com/infinisil/nixus) and represents a similar
|
||||
concept.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## Overview
|
||||
|
||||
Services are used in `inventory.instances`, and then they attach to *roles* and *machines* — meaning you decide which machines run which part of the service.
|
||||
Services are used in `inventory.instances`, and assigned to *roles* and
|
||||
*machines* -- meaning you decide which machines run which part of the service.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -18,116 +24,135 @@ For example:
|
||||
inventory.instances = {
|
||||
borgbackup = {
|
||||
roles.client.machines."laptop" = {};
|
||||
roles.client.machines."server1" = {};
|
||||
roles.client.machines."workstation" = {};
|
||||
|
||||
roles.server.machines."backup-box" = {};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This says: “Run borgbackup as a *client* on my *laptop* and *server1*, and as a *server* on *backup-box*.”
|
||||
This says: "Run borgbackup as a *client* on my *laptop* and *workstation*, and
|
||||
as a *server* on *backup-box*". `client` and `server` are roles defined by the
|
||||
`borgbackup` service.
|
||||
|
||||
## Module source specification
|
||||
|
||||
Each instance includes a reference to a **module specification** — this is how Clan knows which service module to use and where it came from.
|
||||
Usually one would just use `imports` but we needd to make the `module source` configurable via Python API.
|
||||
By default it is not required to specify the `module`, in which case it defaults to the preprovided services of clan-core.
|
||||
Each instance includes a reference to a **module specification** -- this is how
|
||||
Clan knows which service module to use and where it came from.
|
||||
|
||||
---
|
||||
|
||||
## Override Example
|
||||
It is not required to specify the `module.input` parameter, in which case it
|
||||
defaults to the pre-provided services of clan-core. In a similar fashion, the
|
||||
`module.name` parameter can also be omitted, it will default to the name of the
|
||||
instance.
|
||||
|
||||
Example of instantiating a `borgbackup` service using `clan-core`:
|
||||
|
||||
```nix
|
||||
inventory.instances = {
|
||||
# Instance Name: Different name for this 'borgbackup' instance
|
||||
borgbackup = {
|
||||
# Since this is instances."borgbackup" the whole `module = { ... }` below is equivalent and optional.
|
||||
module = {
|
||||
name = "borgbackup"; # <-- Name of the module (optional)
|
||||
input = "clan-core"; # <-- The flake input where the service is defined (optional)
|
||||
};
|
||||
|
||||
borgbackup = { # <- Instance name
|
||||
|
||||
# This can be partially/fully specified,
|
||||
# - If the instance name is not the name of the module
|
||||
# - If the input is not clan-core
|
||||
# module = {
|
||||
# name = "borgbackup"; # Name of the module (optional)
|
||||
# input = "clan-core"; # The flake input where the service is defined (optional)
|
||||
# };
|
||||
|
||||
# Participation of the machines is defined via roles
|
||||
# Right side needs to be an attribute set. Its purpose will become clear later
|
||||
roles.client.machines."machine-a" = {};
|
||||
roles.server.machines."backup-host" = {};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
If you used `clan-core` as an input attribute for your flake:
|
||||
## Module Settings
|
||||
|
||||
Each role might expose configurable options. See clan's [clanServices
|
||||
reference](../reference/clanServices/index.md) for all available options.
|
||||
|
||||
Settings can be set in per-machine or per-role. The latter is applied to all
|
||||
machines that are assigned to that role.
|
||||
|
||||
|
||||
```nix
|
||||
# ↓ module.input = "clan-core"
|
||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
||||
```
|
||||
|
||||
## Simplified Example
|
||||
|
||||
If only one instance is needed for a service and the service is a clan core service, the `module` definition can be omitted.
|
||||
|
||||
```nix
|
||||
# Simplified way of specifying a single instance
|
||||
inventory.instances = {
|
||||
# instance name is `borgbackup` -> clan core module `borgbackup` will be loaded.
|
||||
borgbackup = {
|
||||
# Participation of the machines is defined via roles
|
||||
# Right side needs to be an attribute set. Its purpose will become clear later
|
||||
roles.client.machines."machine-a" = {};
|
||||
roles.server.machines."backup-host" = {};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Example
|
||||
|
||||
Each role might expose configurable options
|
||||
|
||||
See clan's [clanServices reference](../reference/clanServices/index.md) for available options
|
||||
|
||||
```nix
|
||||
inventory.instances = {
|
||||
borgbackup-example = {
|
||||
module = {
|
||||
name = "borgbackup";
|
||||
input = "clan-core";
|
||||
};
|
||||
# Settings for 'machine-a'
|
||||
roles.client.machines."machine-a" = {
|
||||
# 'client' -Settings of 'machine-a'
|
||||
settings = {
|
||||
backupFolders = [
|
||||
/home
|
||||
/var
|
||||
];
|
||||
};
|
||||
# ---------------------------
|
||||
};
|
||||
roles.server.machines."backup-host" = {};
|
||||
|
||||
# Settings for all machines of the role "server"
|
||||
roles.server.settings = {
|
||||
directory = "/var/lib/borgbackup";
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Tags
|
||||
|
||||
Multiple members can be defined using tags as follows
|
||||
Tags can be used to assign multiple machines to a role at once. It can be thought of as a grouping mechanism.
|
||||
|
||||
For example using the `all` tag for services that you want to be configured on all
|
||||
your machines is a common pattern.
|
||||
|
||||
The following example could be used to backup all your machines to a common
|
||||
backup server
|
||||
|
||||
```nix
|
||||
inventory.instances = {
|
||||
borgbackup-example = {
|
||||
module = {
|
||||
name = "borgbackup";
|
||||
input = "clan-core";
|
||||
};
|
||||
#
|
||||
# The 'all' -tag targets all machines
|
||||
roles.client.tags."all" = {};
|
||||
# ---------------------------
|
||||
borgbackup = {
|
||||
# "All" machines are assigned to the borgbackup 'client' role
|
||||
roles.client.tags = [ "all" ];
|
||||
|
||||
# But only one specific machine (backup-host) is assigned to the 'server' role
|
||||
roles.server.machines."backup-host" = {};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Sharing additional Nix configuration
|
||||
|
||||
Sometimes you need to add custom NixOS configuration alongside your clan
|
||||
services. The `extraModules` option allows you to include additional NixOS
|
||||
configuration that is applied for every machine assigned to that role.
|
||||
|
||||
There are multiple valid syntaxes for specifying modules:
|
||||
|
||||
```nix
|
||||
inventory.instances = {
|
||||
borgbackup = {
|
||||
roles.client = {
|
||||
# Direct module reference
|
||||
extraModules = [ ../nixosModules/borgbackup.nix ];
|
||||
|
||||
# Or using self (needs to be json serializable)
|
||||
# See next example, for a workaround.
|
||||
extraModules = [ self.nixosModules.borgbackup ];
|
||||
|
||||
# Or inline module definition, (needs to be json compatible)
|
||||
extraModules = [
|
||||
{
|
||||
# Your module configuration here
|
||||
# ...
|
||||
#
|
||||
# If the module needs to contain non-serializable expressions:
|
||||
imports = [ ./path/to/non-serializable.nix ];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Picking a clanService
|
||||
|
||||
You can use services exposed by Clan's core module library, `clan-core`.
|
||||
@@ -142,18 +167,19 @@ You can also author your own `clanService` modules.
|
||||
|
||||
You might expose your service module from your flake — this makes it easy for other people to also use your module in their clan.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 💡 Tips for Working with clanServices
|
||||
|
||||
* You can add multiple inputs to your flake (`clan-core`, `your-org-modules`, etc.) to mix and match services.
|
||||
* Each service instance is isolated by its key in `inventory.instances`, allowing you to deploy multiple versions or roles of the same service type.
|
||||
* Roles can target different machines or be scoped dynamically.
|
||||
- You can add multiple inputs to your flake (`clan-core`, `your-org-modules`, etc.) to mix and match services.
|
||||
- Each service instance is isolated by its key in `inventory.instances`, allowing to deploy multiple versions or roles of the same service type.
|
||||
- Roles can target different machines or be scoped dynamically.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## What's Next?
|
||||
|
||||
* [Author your own clanService →](../guides/services/community.md)
|
||||
* [Migrate from clanModules →](../guides/migrations/migrate-inventory-services.md)
|
||||
- [Author your own clanService →](../guides/services/community.md)
|
||||
- [Migrate from clanModules →](../guides/migrations/migrate-inventory-services.md)
|
||||
|
||||
<!-- TODO: * [Understand the architecture →](../explanation/clan-architecture.md) -->
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
Machines can be added using the following methods
|
||||
|
||||
- Editing nix expressions in flake.nix (i.e. via `clan-core.lib.clan`)
|
||||
- Editing machines/`machine_name`/configuration.nix (automatically included if it exists)
|
||||
- `clan machines create` (imperative)
|
||||
- Create a file `machines/{machine_name}/configuration.nix` (See: [File Autoincludes](../../concepts/autoincludes.md))
|
||||
- Imperative via cli command: `clan machines create`
|
||||
- Editing nix expressions in flake.nix See [`clan-core.lib.clan`](/options/?scope=Flake Options (clan.nix file))
|
||||
|
||||
See the complete [list](../../concepts/autoincludes.md) of auto-loaded files.
|
||||
|
||||
@@ -39,7 +39,6 @@ See the complete [list](../../concepts/autoincludes.md) of auto-loaded files.
|
||||
The imperative command might create a machine folder in `machines/jon`
|
||||
And might persist information in `inventory.json`
|
||||
|
||||
|
||||
### Configuring a machine
|
||||
|
||||
!!! Note
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
# Migrate existing NixOS configurations
|
||||
# Convert existing NixOS configurations
|
||||
|
||||
This guide will help you migrate your existing NixOS configurations into Clan.
|
||||
This guide will help you convert your existing NixOS configurations into a Clan.
|
||||
|
||||
!!! Warning
|
||||
Migrating instead of starting new can be trickier and might lead to bugs or
|
||||
unexpected issues. We recommend following the [Getting Started](../getting-started/index.md) guide first. Once you have a working setup, you can easily transfer your NixOS configurations over.
|
||||
unexpected issues. We recommend reading the [Getting Started](./index.md) guide first.
|
||||
|
||||
Once you have a working setup and understand the concepts transfering your NixOS configurations over is easy.
|
||||
|
||||
## Back up your existing configuration
|
||||
|
||||
## Back up your existing configuration!
|
||||
Before you start, it is strongly recommended to back up your existing
|
||||
configuration in any form you see fit. If you use version control to manage
|
||||
your configuration changes, it is also a good idea to follow the migration
|
||||
guide in a separte branch until everything works as expected.
|
||||
|
||||
|
||||
## Starting Point
|
||||
|
||||
We assume you are already using NixOS flakes to manage your configuration. If
|
||||
@@ -43,10 +45,9 @@ have have two hosts: **berlin** and **cologne**.
|
||||
}
|
||||
```
|
||||
|
||||
## Add clan-core Input
|
||||
## 1. Add `clan-core` to `inputs`
|
||||
|
||||
Add `clan-core` to your flake as input. It will provide everything we need to
|
||||
manage your configurations with clan.
|
||||
Add `clan-core` to your flake as input.
|
||||
|
||||
```nix
|
||||
inputs.clan-core = {
|
||||
@@ -56,7 +57,7 @@ inputs.clan-core = {
|
||||
}
|
||||
```
|
||||
|
||||
## Update Outputs
|
||||
## 2. Update Outputs
|
||||
|
||||
To be able to access our newly added dependency, it has to be added to the
|
||||
output parameters.
|
||||
@@ -103,26 +104,23 @@ For the provide flake example, your flake should now look like this:
|
||||
};
|
||||
in
|
||||
{
|
||||
nixosConfigurations = clan.nixosConfigurations;
|
||||
|
||||
inherit (clan) clanInternals;
|
||||
|
||||
clan = {
|
||||
inherit (clan) templates;
|
||||
};
|
||||
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
|
||||
clan = clan.config;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Et voilà! Your existing hosts are now part of a clan. Existing Nix tooling
|
||||
✅ Et voilà! Your existing hosts are now part of a clan.
|
||||
|
||||
Existing Nix tooling
|
||||
should still work as normal. To check that you didn't make any errors, run `nix
|
||||
flake show` and verify both hosts are still recognized as if nothing had
|
||||
changed. You should also see the new `clanInternals` output.
|
||||
changed. You should also see the new `clan` output.
|
||||
|
||||
```
|
||||
❯ nix flake show
|
||||
git+file:///my-nixos-config
|
||||
├───clanInternals: unknown
|
||||
├───clan: unknown
|
||||
└───nixosConfigurations
|
||||
├───berlin: NixOS configuration
|
||||
└───cologne: NixOS configuration
|
||||
@@ -131,7 +129,7 @@ git+file:///my-nixos-config
|
||||
Of course you can also rebuild your configuration using `nixos-rebuild` and
|
||||
veryify everything still works.
|
||||
|
||||
## Add Clan CLI devShell
|
||||
## 3. Add `clan-cli` to your `devShells`
|
||||
|
||||
At this point Clan is set up, but you can't use the CLI yet. To do so, it is
|
||||
recommended to expose it via a `devShell` in your flake. It is also possible to
|
||||
@@ -163,8 +161,8 @@ cologne
|
||||
|
||||
## Specify Targets
|
||||
|
||||
Clan needs to know where it can reach your hosts. For each of your hosts, set
|
||||
`clan.core.networking.targetHost` to its adress or hostname.
|
||||
Clan needs to know where it can reach your hosts. For testing purpose set
|
||||
`clan.core.networking.targetHost` to the machines adress or hostname.
|
||||
|
||||
```nix
|
||||
# machines/berlin/configuration.nix
|
||||
@@ -173,6 +171,8 @@ Clan needs to know where it can reach your hosts. For each of your hosts, set
|
||||
}
|
||||
```
|
||||
|
||||
See our guide on for properly [configuring machines networking](../networking.md)
|
||||
|
||||
## Next Steps
|
||||
|
||||
You are now fully set up. Use the CLI to manage your hosts or proceed to
|
||||
@@ -1,12 +1,15 @@
|
||||
|
||||
# Update Your Machines
|
||||
# Update Machines
|
||||
|
||||
Clan CLI enables you to remotely update your machines over SSH. This requires setting up a target address for each target machine.
|
||||
The Clan command line interface enables you to update machines remotely over SSH.
|
||||
In this guide we will teach you how to set a `targetHost` in Nix,
|
||||
and how to define a remote builder for your machine closures.
|
||||
|
||||
### Setting `targetHost`
|
||||
|
||||
In your Nix files, set the `targetHost` to the reachable IP address of your new machine. This eliminates the need to specify `--target-host` with every command.
|
||||
## Setting `targetHost`
|
||||
|
||||
Set the machine’s `targetHost` to the reachable IP address of the new machine.
|
||||
This eliminates the need to specify `--target-host` in CLI commands.
|
||||
|
||||
```{.nix title="clan.nix" hl_lines="9"}
|
||||
{
|
||||
@@ -23,15 +26,42 @@ inventory.machines = {
|
||||
# [...]
|
||||
}
|
||||
```
|
||||
|
||||
The use of `root@` in the target address implies SSH access as the `root` user.
|
||||
Ensure that the root login is secured and only used when necessary.
|
||||
|
||||
## Multiple Target Hosts
|
||||
|
||||
### Setting a Build Host
|
||||
You can now experiment with a new interface that allows you to define multiple `targetHost` addresses for different VPNs. Learn more and try it out in our [networking guide](../networking.md).
|
||||
|
||||
If the machine does not have enough resources to run the NixOS evaluation or build itself,
|
||||
it is also possible to specify a build host instead.
|
||||
During an update, the cli will ssh into the build host and run `nixos-rebuild` from there.
|
||||
## Updating Machine Configurations
|
||||
|
||||
Execute the following command to update the specified machine:
|
||||
|
||||
```bash
|
||||
clan machines update jon
|
||||
```
|
||||
|
||||
All machines can be updated simultaneously by omitting the machine name:
|
||||
|
||||
```bash
|
||||
clan machines update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
The following options are only needed for special cases, such as limited resources, mixed environments, or private flakes.
|
||||
|
||||
### Setting `buildHost`
|
||||
|
||||
If the machine does not have enough resources to run the NixOS **evaluation** or **build** itself,
|
||||
it is also possible to specify a `buildHost` instead.
|
||||
During an update, clan will ssh into the `buildHost` and run `nixos-rebuild` from there.
|
||||
|
||||
!!! Note
|
||||
The `buildHost` option should be set directly within your machine’s Nix configuration, **not** under `inventory.machines`.
|
||||
|
||||
|
||||
```{.nix hl_lines="5" .no-copy}
|
||||
@@ -45,7 +75,11 @@ buildClan {
|
||||
};
|
||||
```
|
||||
|
||||
You can also override the build host via the command line:
|
||||
### Overriding configuration with CLI flags
|
||||
|
||||
`buildHost` / `targetHost`, and other network settings can be temporarily overridden for a single command:
|
||||
|
||||
For the full list of flags refer to the [Clan CLI](../../reference/cli/index.md)
|
||||
|
||||
```bash
|
||||
# Build on a remote host
|
||||
@@ -56,23 +90,9 @@ clan machines update jon --build-host local
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Make sure that the CPU architecture is the same for the buildHost as for the targetHost.
|
||||
Example:
|
||||
If you want to deploy to a macOS machine, your architecture is an ARM64-Darwin, that means you need a second macOS machine to build it.
|
||||
Make sure the CPU architecture of the `buildHost` matches that of the `targetHost`
|
||||
|
||||
### Updating Machine Configurations
|
||||
|
||||
Execute the following command to update the specified machine:
|
||||
|
||||
```bash
|
||||
clan machines update jon
|
||||
```
|
||||
|
||||
You can also update all configured machines simultaneously by omitting the machine name:
|
||||
|
||||
```bash
|
||||
clan machines update
|
||||
```
|
||||
For example, if deploying to a macOS machine with an ARM64-Darwin architecture, you need a second macOS machine with the same architecture to build it.
|
||||
|
||||
|
||||
### Excluding a machine from `clan machine update`
|
||||
@@ -96,14 +116,15 @@ This is useful for machines that are not always online or are not part of the re
|
||||
### Uploading Flake Inputs
|
||||
|
||||
When updating remote machines, flake inputs are usually fetched by the build host.
|
||||
However, if your flake inputs require authentication (e.g., private repositories),
|
||||
you can use the `--upload-inputs` flag to upload all inputs from your local machine:
|
||||
However, if flake inputs require authentication (e.g., private repositories),
|
||||
|
||||
Use the `--upload-inputs` flag to upload all inputs from your local machine:
|
||||
|
||||
```bash
|
||||
clan machines update jon --upload-inputs
|
||||
```
|
||||
|
||||
This is particularly useful when:
|
||||
- Your flake references private Git repositories
|
||||
- Authentication credentials are only available on your local machine
|
||||
- The flake references private Git repositories
|
||||
- Authentication credentials are only available on local machine
|
||||
- The build host doesn't have access to certain network resources
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ The following table shows the migration status of each deprecated clanModule:
|
||||
| `data-mesher` | ✅ [Migrated](../../reference/clanServices/data-mesher.md) | |
|
||||
| `deltachat` | ❌ Removed | |
|
||||
| `disk-id` | ❌ Removed | |
|
||||
| `dyndns` | [Being Migrated](https://git.clan.lol/clan/clan-core/pulls/4390) | |
|
||||
| `dyndns` | ✅ [Migrated](../../reference/clanServices/dyndns.md) | |
|
||||
| `ergochat` | ❌ Removed | |
|
||||
| `garage` | ✅ [Migrated](../../reference/clanServices/garage.md) | |
|
||||
| `golem-provider` | ❌ Removed | |
|
||||
@@ -263,18 +263,18 @@ The following table shows the migration status of each deprecated clanModule:
|
||||
| `iwd` | ❌ Removed | Use [wifi service](../../reference/clanServices/wifi.md) instead |
|
||||
| `localbackup` | ✅ [Migrated](../../reference/clanServices/localbackup.md) | |
|
||||
| `localsend` | ❌ Removed | |
|
||||
| `machine-id` | ❌ Removed | Now an [option](../../reference/clan.core/settings.md) |
|
||||
| `machine-id` | ✅ [Migrated](../../reference/clan.core/settings.md) | Now an [option](../../reference/clan.core/settings.md) |
|
||||
| `matrix-synapse` | ✅ [Migrated](../../reference/clanServices/matrix-synapse.md) | |
|
||||
| `moonlight` | ❌ Removed | |
|
||||
| `mumble` | ❌ Removed | |
|
||||
| `mycelium` | ✅ [Migrated](../../reference/clanServices/mycelium.md) | |
|
||||
| `nginx` | ❌ Removed | |
|
||||
| `packages` | ✅ [Migrated](../../reference/clanServices/packages.md) | |
|
||||
| `postgresql` | ❌ Removed | Now an [option](../../reference/clan.core/settings.md) |
|
||||
| `postgresql` | ✅ [Migrated](../../reference/clan.core/settings.md) | Now an [option](../../reference/clan.core/settings.md) |
|
||||
| `root-password` | ✅ [Migrated](../../reference/clanServices/users.md) | See [migration guide](../../reference/clanServices/users.md#migration-from-root-password-module) |
|
||||
| `single-disk` | ❌ Removed | |
|
||||
| `sshd` | ✅ [Migrated](../../reference/clanServices/sshd.md) | |
|
||||
| `state-version` | ✅ [Migrated](../../reference/clanServices/state-version.md) | |
|
||||
| `state-version` | ✅ [Migrated](../../reference/clan.core/settings.md) | Now an [option](../../reference/clan.core/settings.md) |
|
||||
| `static-hosts` | ❌ Removed | |
|
||||
| `sunshine` | ❌ Removed | |
|
||||
| `syncthing-static-peers` | ❌ Removed | |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ For machines with public IPs or DNS names, use the `internet` service to configu
|
||||
# Direct SSH with fallback support
|
||||
internet = {
|
||||
roles.default.machines.server1 = {
|
||||
settings.address = "server1.example.com";
|
||||
settings.host = "server1.example.com";
|
||||
};
|
||||
roles.default.machines.server2 = {
|
||||
settings.address = "192.168.1.100";
|
||||
settings.host = "192.168.1.100";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ For machines with public IPs or DNS names, use the `internet` service to configu
|
||||
# Priority 1: Try direct connection first
|
||||
internet = {
|
||||
roles.default.machines.publicserver = {
|
||||
settings.address = "public.example.com";
|
||||
settings.host = "public.example.com";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -255,11 +255,50 @@ outputs = inputs: flake-parts.lib.mkFlake { inherit inputs; } ({self, lib, ...}:
|
||||
})
|
||||
```
|
||||
|
||||
The benefit of this approach is that downstream users can override the value of `myClan` by using `mkForce` or other priority modifiers.
|
||||
The benefit of this approach is that downstream users can override the value of
|
||||
`myClan` by using `mkForce` or other priority modifiers.
|
||||
|
||||
## Example: A machine-type service
|
||||
|
||||
Users often have different types of machines. These could be any classification
|
||||
you like, for example "servers" and "desktops". Having such distictions, allows
|
||||
reusing parts of your configuration that should be appplied to a class of
|
||||
machines. Since this is such a common pattern, here is how to write such a
|
||||
service.
|
||||
|
||||
For this example the we have to roles: `server` and `desktop`. Additionally, we
|
||||
can use the `perMachine` section to add configuration to all machines regardless
|
||||
of their type.
|
||||
|
||||
```nix title="machine-type.nix"
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "machine-type";
|
||||
|
||||
roles.server.perInstance.nixosModule = ./server.nix;
|
||||
roles.desktop.perInstance.nixosModule = ./desktop.nix;
|
||||
|
||||
perMachine.nixosModule = {
|
||||
# Configuration for all machines (any type)
|
||||
};
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
In the inventory we the assign machines to a type, e.g. by using tags
|
||||
|
||||
```nix title="flake.nix"
|
||||
instnaces.machine-type = {
|
||||
module.input = "self";
|
||||
module.name = "@pinpox/machine-type";
|
||||
roles.desktop.tags.desktop = { };
|
||||
roles.server.tags.server = { };
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Further
|
||||
## Further Reading
|
||||
|
||||
- [Reference Documentation for Service Authors](../../reference/clanServices/clan-service-author-interface.md)
|
||||
- [Migration Guide from ClanModules to ClanServices](../../guides/migrations/migrate-inventory-services.md)
|
||||
|
||||
85
docs/site/guides/vars/sops/age-plugins.md
Normal file
85
docs/site/guides/vars/sops/age-plugins.md
Normal 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. Here’s 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)
|
||||
};
|
||||
}
|
||||
```
|
||||
290
docs/site/guides/vars/vars-advanced-examples.md
Normal file
290
docs/site/guides/vars/vars-advanced-examples.md
Normal 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
|
||||
@@ -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)
|
||||
123
docs/site/guides/vars/vars-concepts.md
Normal file
123
docs/site/guides/vars/vars-concepts.md
Normal 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
|
||||
|
||||
145
docs/site/guides/vars/vars-overview.md
Normal file
145
docs/site/guides/vars/vars-overview.md
Normal 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.
|
||||
|
||||
272
docs/site/guides/vars/vars-troubleshooting.md
Normal file
272
docs/site/guides/vars/vars-troubleshooting.md
Normal 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`
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
44
flake.lock
generated
44
flake.lock
generated
@@ -13,11 +13,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756091210,
|
||||
"narHash": "sha256-oEUEAZnLbNHi8ti4jY8x10yWcIkYoFc5XD+2hjmOS04=",
|
||||
"rev": "eb831bca21476fa8f6df26cb39e076842634700d",
|
||||
"lastModified": 1757300813,
|
||||
"narHash": "sha256-JYQl+8nJYImg/inqotu9nEPcTXrRJixFN6sOfn6Tics=",
|
||||
"rev": "b5f2157bcd26c73551374cd6e5b027b0119b2f3d",
|
||||
"type": "tarball",
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/eb831bca21476fa8f6df26cb39e076842634700d.tar.gz"
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/b5f2157bcd26c73551374cd6e5b027b0119b2f3d.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -31,11 +31,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755519972,
|
||||
"narHash": "sha256-bU4nqi3IpsUZJeyS8Jk85ytlX61i4b0KCxXX9YcOgVc=",
|
||||
"lastModified": 1757508292,
|
||||
"narHash": "sha256-7lVWL5bC6xBIMWWDal41LlGAG+9u2zUorqo3QCUL4p4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "4073ff2f481f9ef3501678ff479ed81402caae6d",
|
||||
"rev": "146f45bee02b8bd88812cfce6ffc0f933788875a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -51,11 +51,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1754487366,
|
||||
"narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=",
|
||||
"lastModified": 1756770412,
|
||||
"narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18",
|
||||
"rev": "4524271976b625a4a605beefd893f270620fd751",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -71,11 +71,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755825449,
|
||||
"narHash": "sha256-XkiN4NM9Xdy59h69Pc+Vg4PxkSm9EWl6u7k6D5FZ5cM=",
|
||||
"lastModified": 1757430124,
|
||||
"narHash": "sha256-MhDltfXesGH8VkGv3hmJ1QEKl1ChTIj9wmGAFfWj/Wk=",
|
||||
"owner": "nix-darwin",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "8df64f819698c1fee0c2969696f54a843b2231e8",
|
||||
"rev": "830b3f0b50045cf0bcfd4dab65fad05bf882e196",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -99,11 +99,11 @@
|
||||
},
|
||||
"nixos-facter-modules": {
|
||||
"locked": {
|
||||
"lastModified": 1755504238,
|
||||
"narHash": "sha256-mw7q5DPdmz/1au8mY0u1DztRgVyJToGJfJszxjKSNes=",
|
||||
"lastModified": 1756491981,
|
||||
"narHash": "sha256-lXyDAWPw/UngVtQfgQ8/nrubs2r+waGEYIba5UX62+k=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixos-facter-modules",
|
||||
"rev": "354ed498c9628f32383c3bf5b6668a17cdd72a28",
|
||||
"rev": "c1b29520945d3e148cd96618c8a0d1f850965d8c",
|
||||
"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": {
|
||||
@@ -181,11 +181,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755934250,
|
||||
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
|
||||
"lastModified": 1756662192,
|
||||
"narHash": "sha256-F1oFfV51AE259I85av+MAia221XwMHCOtZCMcZLK2Jk=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
|
||||
"rev": "1aabc6c05ccbcbf4a635fb7a90400e44282f61c4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
34
flake.nix
34
flake.nix
@@ -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 {
|
||||
@@ -66,7 +66,39 @@
|
||||
};
|
||||
clan = {
|
||||
meta.name = "clan-core";
|
||||
|
||||
modules = {
|
||||
myModule = { ... }: {
|
||||
#
|
||||
_module.args = { inherit inputs; };
|
||||
};
|
||||
|
||||
# # depends on home-manager 25.05
|
||||
# myEnzime = { ... }: {
|
||||
# imports = [ inputs.enzime.yours ];
|
||||
# dependencies.home-manager = lib.mkForce "my-home-manager";
|
||||
# };
|
||||
# # depends on home-manager 24.05
|
||||
# myLassulus = { ... }: {
|
||||
# imports = [ inputs.lassulus.his ];
|
||||
# dependencies.home-manager = lib.mkForce "my-home-manager";
|
||||
# };
|
||||
};
|
||||
|
||||
|
||||
serviceOverrides = {
|
||||
"clan-core/hello-world" = {
|
||||
dependencies = {
|
||||
flake-parts = "flake-parts";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
inventory = {
|
||||
instances.hello-world = {
|
||||
roles.morning.tags = [ "all" ];
|
||||
};
|
||||
|
||||
machines = {
|
||||
"test-darwin-machine" = {
|
||||
machineClass = "darwin";
|
||||
|
||||
@@ -87,6 +87,8 @@ in
|
||||
relativeDir = removePrefix "${self}/" (toString config.clan.directory);
|
||||
|
||||
update-vars = hostPkgs.writeShellScriptBin "update-vars" ''
|
||||
set -x
|
||||
export PRJ_ROOT=$(git rev-parse --show-toplevel)
|
||||
${update-vars-script} $PRJ_ROOT/${relativeDir} ${testName}
|
||||
'';
|
||||
|
||||
|
||||
@@ -228,6 +228,38 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
serviceOverrides = lib.mkOption {
|
||||
type = types.attrsOf (types.submoduleWith {
|
||||
modules = [
|
||||
{
|
||||
options.dependencies = lib.mkOption {
|
||||
type = types.attrsOf types.raw;
|
||||
description = "Override a dependencies of this service";
|
||||
};
|
||||
}
|
||||
];
|
||||
});
|
||||
default = { };
|
||||
description = ''
|
||||
Override/inject dependencies to a service.
|
||||
|
||||
Example:
|
||||
|
||||
```nix
|
||||
{
|
||||
servicesOverrides = {
|
||||
# Override need to be done by manifest name to avoid ambiguity
|
||||
"clan-core/hello-world" = {
|
||||
dependencies = {
|
||||
home-manager = inputs.home-manager-v2;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
'';
|
||||
};
|
||||
|
||||
inventory = lib.mkOption {
|
||||
type = types.submoduleWith {
|
||||
modules = [
|
||||
|
||||
@@ -245,8 +245,10 @@ in
|
||||
in
|
||||
{ config, ... }:
|
||||
{
|
||||
staticModules = clan-core.clan.modules;
|
||||
|
||||
distributedServices = clanLib.inventory.mapInstances {
|
||||
inherit (clanConfig) inventory exportsModule;
|
||||
inherit (clanConfig) inventory exportsModule serviceOverrides;
|
||||
inherit flakeInputs directory;
|
||||
clanCoreModules = clan-core.clan.modules;
|
||||
prefix = [ "distributedServices" ];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
# TODO: consume directly from clan.config
|
||||
directory,
|
||||
serviceOverrides,
|
||||
}:
|
||||
{
|
||||
lib,
|
||||
@@ -31,10 +32,12 @@ in
|
||||
(
|
||||
{ name, ... }:
|
||||
{
|
||||
_module.args._ctx = [ name ];
|
||||
_module.args.exports = config.exports;
|
||||
_module.args.directory = directory;
|
||||
|
||||
_module.args = {
|
||||
_ctx = [ name ];
|
||||
exports = config.exports;
|
||||
directory = directory;
|
||||
inherit (specialArgs) clanLib _unsafe;
|
||||
};
|
||||
}
|
||||
)
|
||||
./service-module.nix
|
||||
@@ -43,6 +46,9 @@ in
|
||||
inherit (specialArgs) clanLib;
|
||||
prefix = _ctx;
|
||||
})
|
||||
(service: {
|
||||
dependencies = lib.mapAttrs (n: v: { resolved = v; }) serviceOverrides.${service.config.manifest.name}.dependencies or { };
|
||||
})
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,8 +26,9 @@ in
|
||||
inventory,
|
||||
directory,
|
||||
clanCoreModules,
|
||||
prefix ? [ ],
|
||||
exportsModule,
|
||||
prefix ? [ ],
|
||||
serviceOverrides ? { },
|
||||
}:
|
||||
let
|
||||
# machineHasTag = machineName: tagName: lib.elem tagName inventory.machines.${machineName}.tags;
|
||||
@@ -127,9 +128,10 @@ in
|
||||
specialArgs = {
|
||||
inherit clanLib;
|
||||
_ctx = prefix;
|
||||
_unsafe.flakeInputs = flakeInputs;
|
||||
};
|
||||
modules = [
|
||||
(import ./all-services-wrapper.nix { inherit directory; })
|
||||
(import ./all-services-wrapper.nix { inherit directory serviceOverrides; })
|
||||
]
|
||||
++ modules;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
lib,
|
||||
config,
|
||||
_ctx,
|
||||
_unsafe,
|
||||
directory,
|
||||
exports,
|
||||
...
|
||||
@@ -106,6 +107,10 @@ let
|
||||
in
|
||||
{
|
||||
options = {
|
||||
|
||||
debug = mkOption {
|
||||
default = _unsafe.flakeInputs;
|
||||
};
|
||||
# Option to disable some behavior during docs rendering
|
||||
_docs_rendering = mkOption {
|
||||
default = false;
|
||||
@@ -113,6 +118,54 @@ in
|
||||
type = types.bool;
|
||||
};
|
||||
|
||||
dependencies = mkOption {
|
||||
type = types.attrsWith {
|
||||
placeholder = "dependencyName";
|
||||
elemType = types.submoduleWith {
|
||||
modules = [
|
||||
({name,...}@dep: {
|
||||
options.name = mkOption {
|
||||
default = name;
|
||||
type = types.str;
|
||||
description = "The name of the dependency, usually the input name.";
|
||||
};
|
||||
options.resolved = mkOption {
|
||||
type = types.raw;
|
||||
default = _unsafe.flakeInputs.${dep.config.name} or (throw ''
|
||||
The dependency '${dep.config.name}' could not be found in the flake inputs.
|
||||
|
||||
This module requires '${dep.config.name}' to be present
|
||||
Fixes:
|
||||
- Add '${dep.config.name}' to the flake inputs
|
||||
- Inject a custom dependency via 'clan.serviceOverrides.<manifest-name>.dependencies.${dep.config.name} = ...'
|
||||
'');
|
||||
description = ''
|
||||
The resolved value of the dependency.
|
||||
'';
|
||||
};
|
||||
})
|
||||
];
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
Dependencies of this service.
|
||||
|
||||
Can be declared via `clan.lib.mkDependency`.
|
||||
|
||||
```nix
|
||||
{
|
||||
home-manager = clan.lib.mkDependency {
|
||||
name = "home-manager";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This will map `inputs.home-manager` to `dependencies.home-manager`.
|
||||
The dependency can then be safely accessed via `config.dependencies.home-manager` from the toplevel arguments of this module.
|
||||
'';
|
||||
default = { };
|
||||
};
|
||||
|
||||
instances = mkOption {
|
||||
visible = false;
|
||||
defaultText = "Throws: 'The service must define its instances' when not defined";
|
||||
@@ -639,7 +692,7 @@ in
|
||||
|
||||
Exports are used to share and expose information between instances.
|
||||
|
||||
Define exports in the [`perInstance`](#perInstance) or [`perMachine`](#perMachine) scope.
|
||||
Define exports in the [`perInstance`](#roles.perInstance) or [`perMachine`](#perMachine) scope.
|
||||
|
||||
Accessing the exports:
|
||||
|
||||
|
||||
@@ -21,14 +21,14 @@ let
|
||||
"secrets"
|
||||
"templates"
|
||||
];
|
||||
clanSchema = jsonLib.parseOptions (lib.filterAttrs (n: _v: lib.elem n include) clanOpts) { };
|
||||
clanSchemaNix = jsonLib.parseOptions (lib.filterAttrs (n: _v: lib.elem n include) clanOpts) { };
|
||||
|
||||
clan-schema-abstract = pkgs.stdenv.mkDerivation {
|
||||
clanSchemaJson = pkgs.stdenv.mkDerivation {
|
||||
name = "clan-schema-files";
|
||||
buildInputs = [ pkgs.cue ];
|
||||
src = ./.;
|
||||
buildPhase = ''
|
||||
export SCHEMA=${builtins.toFile "clan-schema.json" (builtins.toJSON clanSchema)}
|
||||
export SCHEMA=${builtins.toFile "clan-schema.json" (builtins.toJSON clanSchemaNix)}
|
||||
cp $SCHEMA schema.json
|
||||
# Also generate a CUE schema version that is derived from the JSON schema
|
||||
cue import -f -p compose -l '#Root:' schema.json
|
||||
@@ -41,7 +41,7 @@ in
|
||||
{
|
||||
inherit
|
||||
flakeOptions
|
||||
clanSchema
|
||||
clan-schema-abstract
|
||||
clanSchemaNix
|
||||
clanSchemaJson
|
||||
;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ in
|
||||
default = { };
|
||||
};
|
||||
tags = lib.mkOption {
|
||||
type = types.attrsOf (types.submodule { });
|
||||
type = types.coercedTo (types.listOf types.str) (t: lib.genAttrs t (_: { })) (
|
||||
types.attrsOf (types.submodule { })
|
||||
);
|
||||
default = { };
|
||||
};
|
||||
settings =
|
||||
|
||||
@@ -23,6 +23,12 @@ let
|
||||
};
|
||||
in
|
||||
{
|
||||
options.staticModules = lib.mkOption {
|
||||
readOnly = true;
|
||||
type = lib.types.raw;
|
||||
|
||||
apply = moduleSet: lib.mapAttrs (inspectModule "<clan-core>") moduleSet;
|
||||
};
|
||||
options.modulesPerSource = lib.mkOption {
|
||||
# { sourceName :: { moduleName :: {} }}
|
||||
readOnly = true;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Test driver for container-based NixOS testing."""
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import os
|
||||
@@ -11,7 +13,7 @@ import uuid
|
||||
from collections.abc import Callable
|
||||
from contextlib import _GeneratorContextManager
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from functools import cache, cached_property
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
||||
from typing import Any
|
||||
@@ -20,16 +22,10 @@ from colorama import Fore, Style
|
||||
|
||||
from .logger import AbstractLogger, CompositeLogger, TerminalLogger
|
||||
|
||||
# Global flag to track if test environment has been initialized
|
||||
_test_env_initialized = False
|
||||
|
||||
|
||||
@cache
|
||||
def init_test_environment() -> None:
|
||||
"""Set up the test environment (network bridge, /etc/passwd) once."""
|
||||
global _test_env_initialized
|
||||
if _test_env_initialized:
|
||||
return
|
||||
|
||||
# Set up network bridge
|
||||
subprocess.run(
|
||||
["ip", "link", "add", "br0", "type", "bridge"],
|
||||
@@ -48,7 +44,7 @@ def init_test_environment() -> None:
|
||||
passwd_content = """root:x:0:0:Root:/root:/bin/sh
|
||||
nixbld:x:1000:100:Nix build user:/tmp:/bin/sh
|
||||
nobody:x:65534:65534:Nobody:/:/bin/sh
|
||||
"""
|
||||
""" # noqa: S105 - This is not a password, it's a Unix passwd file format for testing
|
||||
|
||||
with NamedTemporaryFile(mode="w", delete=False, prefix="test-passwd-") as f:
|
||||
f.write(passwd_content)
|
||||
@@ -88,8 +84,6 @@ nogroup:x:65534:
|
||||
errno = ctypes.get_errno()
|
||||
raise OSError(errno, os.strerror(errno), "Failed to mount group")
|
||||
|
||||
_test_env_initialized = True
|
||||
|
||||
|
||||
# Load the C library
|
||||
libc = ctypes.CDLL("libc.so.6", use_errno=True)
|
||||
@@ -148,7 +142,7 @@ class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def prepare_machine_root(machinename: str, root: Path) -> None:
|
||||
def prepare_machine_root(root: Path) -> None:
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
root.joinpath("etc").mkdir(parents=True, exist_ok=True)
|
||||
root.joinpath(".env").write_text(
|
||||
@@ -195,7 +189,7 @@ class Machine:
|
||||
return self.get_systemd_process()
|
||||
|
||||
def start(self) -> None:
|
||||
prepare_machine_root(self.name, self.rootdir)
|
||||
prepare_machine_root(self.rootdir)
|
||||
init_test_environment()
|
||||
cmd = [
|
||||
"systemd-nspawn",
|
||||
@@ -218,8 +212,12 @@ class Machine:
|
||||
self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True, env=env)
|
||||
|
||||
def get_systemd_process(self) -> int:
|
||||
assert self.process is not None, "Machine not started"
|
||||
assert self.process.stdout is not None, "Machine has no stdout"
|
||||
if self.process is None:
|
||||
msg = "Machine not started"
|
||||
raise RuntimeError(msg)
|
||||
if self.process.stdout is None:
|
||||
msg = "Machine has no stdout"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
for line in self.process.stdout:
|
||||
print(line, end="")
|
||||
@@ -236,9 +234,9 @@ class Machine:
|
||||
.read_text()
|
||||
.split()
|
||||
)
|
||||
assert len(childs) == 1, (
|
||||
f"Expected exactly one child process for systemd-nspawn, got {childs}"
|
||||
)
|
||||
if len(childs) != 1:
|
||||
msg = f"Expected exactly one child process for systemd-nspawn, got {childs}"
|
||||
raise RuntimeError(msg)
|
||||
try:
|
||||
return int(childs[0])
|
||||
except ValueError as e:
|
||||
@@ -258,7 +256,9 @@ class Machine:
|
||||
|
||||
def tuple_from_line(line: str) -> tuple[str, str]:
|
||||
match = line_pattern.match(line)
|
||||
assert match is not None
|
||||
if match is None:
|
||||
msg = f"Failed to parse line: {line}"
|
||||
raise RuntimeError(msg)
|
||||
return match[1], match[2]
|
||||
|
||||
return dict(
|
||||
@@ -268,8 +268,14 @@ class Machine:
|
||||
)
|
||||
|
||||
def nsenter_command(self, command: str) -> list[str]:
|
||||
nsenter = shutil.which("nsenter")
|
||||
|
||||
if not nsenter:
|
||||
msg = "nsenter command not found"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
return [
|
||||
"nsenter",
|
||||
nsenter,
|
||||
"--target",
|
||||
str(self.container_pid),
|
||||
"--mount",
|
||||
@@ -286,8 +292,8 @@ class Machine:
|
||||
def execute(
|
||||
self,
|
||||
command: str,
|
||||
check_return: bool = True,
|
||||
check_output: bool = True,
|
||||
check_return: bool = True, # noqa: ARG002
|
||||
check_output: bool = True, # noqa: ARG002
|
||||
timeout: int | None = 900,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""Execute a shell command, returning a list `(status, stdout)`.
|
||||
@@ -324,14 +330,14 @@ class Machine:
|
||||
# Always run command with shell opts
|
||||
command = f"set -eo pipefail; source /etc/profile; set -xu; {command}"
|
||||
|
||||
proc = subprocess.run(
|
||||
return subprocess.run(
|
||||
self.nsenter_command(command),
|
||||
env={},
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
return proc
|
||||
|
||||
def nested(
|
||||
self,
|
||||
@@ -575,7 +581,9 @@ class Driver:
|
||||
# We lauch a sleep here, so we can pgrep the process cmdline for
|
||||
# the uuid
|
||||
sleep = shutil.which("sleep")
|
||||
assert sleep is not None, "sleep command not found"
|
||||
if sleep is None:
|
||||
msg = "sleep command not found"
|
||||
raise RuntimeError(msg)
|
||||
machine.execute(
|
||||
f"systemd-run /bin/sh -c '{sleep} 999999999 && echo {nspawn_uuid}'",
|
||||
)
|
||||
@@ -629,7 +637,7 @@ class Driver:
|
||||
|
||||
def test_script(self) -> None:
|
||||
"""Run the test script"""
|
||||
exec(self.testscript, self.test_symbols(), None)
|
||||
exec(self.testscript, self.test_symbols(), None) # noqa: S102
|
||||
|
||||
def run_tests(self) -> None:
|
||||
"""Run the test script (for non-interactive test runs)"""
|
||||
|
||||
@@ -41,15 +41,15 @@ class AbstractLogger(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def info(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def info(self, *args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def warning(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def warning(self, *args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def error(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def error(self, *args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@@ -63,6 +63,8 @@ class AbstractLogger(ABC):
|
||||
|
||||
class JunitXMLLogger(AbstractLogger):
|
||||
class TestCaseState:
|
||||
"""State tracking for individual test cases in JUnit XML reports."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.stdout = ""
|
||||
self.stderr = ""
|
||||
@@ -78,6 +80,7 @@ class JunitXMLLogger(AbstractLogger):
|
||||
atexit.register(self.close)
|
||||
|
||||
def log(self, message: str, attributes: dict[str, str] | None = None) -> None:
|
||||
del attributes # Unused but kept for API compatibility
|
||||
self.tests[self.currentSubtest].stdout += message + os.linesep
|
||||
|
||||
@contextmanager
|
||||
@@ -86,6 +89,7 @@ class JunitXMLLogger(AbstractLogger):
|
||||
name: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
del attributes # Unused but kept for API compatibility
|
||||
old_test = self.currentSubtest
|
||||
self.tests.setdefault(name, self.TestCaseState())
|
||||
self.currentSubtest = name
|
||||
@@ -100,16 +104,20 @@ class JunitXMLLogger(AbstractLogger):
|
||||
message: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
del attributes # Unused but kept for API compatibility
|
||||
self.log(message)
|
||||
yield
|
||||
|
||||
def info(self, *args: Any, **kwargs: Any) -> None:
|
||||
del kwargs # Unused but kept for API compatibility
|
||||
self.tests[self.currentSubtest].stdout += args[0] + os.linesep
|
||||
|
||||
def warning(self, *args: Any, **kwargs: Any) -> None:
|
||||
del kwargs # Unused but kept for API compatibility
|
||||
self.tests[self.currentSubtest].stdout += args[0] + os.linesep
|
||||
|
||||
def error(self, *args: Any, **kwargs: Any) -> None:
|
||||
del kwargs # Unused but kept for API compatibility
|
||||
self.tests[self.currentSubtest].stderr += args[0] + os.linesep
|
||||
self.tests[self.currentSubtest].failure = True
|
||||
|
||||
@@ -172,15 +180,15 @@ class CompositeLogger(AbstractLogger):
|
||||
stack.enter_context(logger.nested(message, attributes))
|
||||
yield
|
||||
|
||||
def info(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def info(self, *args: Any, **kwargs: Any) -> None: # type: ignore[no-untyped-def]
|
||||
for logger in self.logger_list:
|
||||
logger.info(*args, **kwargs)
|
||||
|
||||
def warning(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def warning(self, *args: Any, **kwargs: Any) -> None: # type: ignore[no-untyped-def]
|
||||
for logger in self.logger_list:
|
||||
logger.warning(*args, **kwargs)
|
||||
|
||||
def error(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def error(self, *args: Any, **kwargs: Any) -> None: # type: ignore[no-untyped-def]
|
||||
for logger in self.logger_list:
|
||||
logger.error(*args, **kwargs)
|
||||
sys.exit(1)
|
||||
@@ -237,13 +245,13 @@ class TerminalLogger(AbstractLogger):
|
||||
toc = time.time()
|
||||
self.log(f"(finished: {message}, in {toc - tic:.2f} seconds)")
|
||||
|
||||
def info(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def info(self, *args: Any, **kwargs: Any) -> None: # type: ignore[no-untyped-def]
|
||||
self.log(*args, **kwargs)
|
||||
|
||||
def warning(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def warning(self, *args: Any, **kwargs: Any) -> None: # type: ignore[no-untyped-def]
|
||||
self.log(*args, **kwargs)
|
||||
|
||||
def error(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def error(self, *args: Any, **kwargs: Any) -> None: # type: ignore[no-untyped-def]
|
||||
self.log(*args, **kwargs)
|
||||
|
||||
def print_serial_logs(self, enable: bool) -> None:
|
||||
@@ -289,13 +297,13 @@ class XMLLogger(AbstractLogger):
|
||||
self.xml.characters(message)
|
||||
self.xml.endElement("line")
|
||||
|
||||
def info(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def info(self, *args: Any, **kwargs: Any) -> None: # type: ignore[no-untyped-def]
|
||||
self.log(*args, **kwargs)
|
||||
|
||||
def warning(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def warning(self, *args: Any, **kwargs: Any) -> None: # type: ignore[no-untyped-def]
|
||||
self.log(*args, **kwargs)
|
||||
|
||||
def error(self, *args: Any, **kwargs: Any) -> None: # type: ignore
|
||||
def error(self, *args: Any, **kwargs: Any) -> None: # type: ignore[no-untyped-def]
|
||||
self.log(*args, **kwargs)
|
||||
|
||||
def log(self, message: str, attributes: dict[str, str] | None = None) -> None:
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
{
|
||||
imports = lib.optional (_class == "nixos") (
|
||||
lib.mkIf config.clan.core.enableRecommendedDefaults {
|
||||
|
||||
# Enable automatic state-version generation.
|
||||
clan.core.settings.state-version.enable = lib.mkDefault true;
|
||||
|
||||
# Use systemd during boot as well except:
|
||||
# - systems with raids as this currently require manual configuration: https://github.com/NixOS/nixpkgs/issues/210210
|
||||
# - for containers we currently rely on the `stage-2` init script that sets up our /etc
|
||||
@@ -37,6 +41,7 @@
|
||||
};
|
||||
|
||||
config = lib.mkIf config.clan.core.enableRecommendedDefaults {
|
||||
|
||||
# This disables the HTML manual and `nixos-help` command but leaves
|
||||
# `man configuration.nix`
|
||||
documentation.doc.enable = lib.mkDefault false;
|
||||
|
||||
@@ -9,28 +9,11 @@
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
# Workaround until we can use nodes.server = { };
|
||||
modules."@clan/importer" = ../../../../clanServices/importer;
|
||||
|
||||
inventory = {
|
||||
machines.server = { };
|
||||
instances.importer = {
|
||||
module.name = "@clan/importer";
|
||||
module.input = "self";
|
||||
roles.default.tags.all = { };
|
||||
roles.default.extraModules = [
|
||||
{
|
||||
clan.core.settings.state-version.enable = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
machines.server = {
|
||||
clan.core.settings.state-version.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
# TODO: Broken. Use instead of importer after fixing.
|
||||
# nodes.server = { };
|
||||
|
||||
# This is not an actual vm test, this is a workaround to
|
||||
# generate the needed vars for the eval test.
|
||||
testScript = "";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,6 +16,10 @@ from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any
|
||||
|
||||
# Constants
|
||||
NODE_ID_LENGTH = 10
|
||||
NETWORK_ID_LENGTH = 16
|
||||
|
||||
|
||||
class ClanError(Exception):
|
||||
pass
|
||||
@@ -55,9 +59,9 @@ class Identity:
|
||||
|
||||
def node_id(self) -> str:
|
||||
nid = self.public.split(":")[0]
|
||||
assert len(nid) == 10, (
|
||||
f"node_id must be 10 characters long, got {len(nid)}: {nid}"
|
||||
)
|
||||
if len(nid) != NODE_ID_LENGTH:
|
||||
msg = f"node_id must be {NODE_ID_LENGTH} characters long, got {len(nid)}: {nid}"
|
||||
raise ClanError(msg)
|
||||
return nid
|
||||
|
||||
|
||||
@@ -84,9 +88,10 @@ class ZerotierController:
|
||||
headers["Content-Type"] = "application/json"
|
||||
headers["X-ZT1-AUTH"] = self.authtoken
|
||||
url = f"http://127.0.0.1:{self.port}{path}"
|
||||
req = urllib.request.Request(url, headers=headers, method=method, data=body)
|
||||
resp = urllib.request.urlopen(req)
|
||||
return json.load(resp)
|
||||
# Safe: only connecting to localhost zerotier API
|
||||
req = urllib.request.Request(url, headers=headers, method=method, data=body) # noqa: S310
|
||||
with urllib.request.urlopen(req, timeout=5) as resp: # noqa: S310
|
||||
return json.load(resp)
|
||||
|
||||
def status(self) -> dict[str, Any]:
|
||||
return self._http_request("/status")
|
||||
@@ -172,9 +177,9 @@ def create_identity() -> Identity:
|
||||
|
||||
|
||||
def compute_zerotier_ip(network_id: str, identity: Identity) -> ipaddress.IPv6Address:
|
||||
assert len(network_id) == 16, (
|
||||
f"network_id must be 16 characters long, got '{network_id}'"
|
||||
)
|
||||
if len(network_id) != NETWORK_ID_LENGTH:
|
||||
msg = f"network_id must be {NETWORK_ID_LENGTH} characters long, got '{network_id}'"
|
||||
raise ClanError(msg)
|
||||
nwid = int(network_id, 16)
|
||||
node_id = int(identity.node_id(), 16)
|
||||
addr_parts = bytearray(
|
||||
|
||||
5
nixosModules/clanCore/zerotier/genmoon.py
Normal file → Executable file
5
nixosModules/clanCore/zerotier/genmoon.py
Normal file → Executable file
@@ -6,9 +6,12 @@ import sys
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
# Constants
|
||||
REQUIRED_ARGS = 4
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 4:
|
||||
if len(sys.argv) != REQUIRED_ARGS:
|
||||
print("Usage: genmoon.py <moon.json> <endpoint.json> <moons.d>")
|
||||
sys.exit(1)
|
||||
moon_json_path = sys.argv[1]
|
||||
|
||||
@@ -12,8 +12,14 @@ let
|
||||
(builtins.match "linux_[0-9]+_[0-9]+" name) != null
|
||||
&& (builtins.tryEval kernelPackages).success
|
||||
&& (
|
||||
(!isUnstable && !kernelPackages.zfs.meta.broken)
|
||||
|| (isUnstable && !kernelPackages.zfs_unstable.meta.broken)
|
||||
let
|
||||
zfsPackage =
|
||||
if isUnstable then
|
||||
kernelPackages.zfs_unstable
|
||||
else
|
||||
kernelPackages.${pkgs.zfs.kernelModuleAttribute};
|
||||
in
|
||||
!(zfsPackage.meta.broken or false)
|
||||
)
|
||||
) pkgs.linuxKernel.packages;
|
||||
latestKernelPackage = lib.last (
|
||||
@@ -24,5 +30,5 @@ let
|
||||
in
|
||||
{
|
||||
# Note this might jump back and worth as kernel get added or removed.
|
||||
boot.kernelPackages = latestKernelPackage;
|
||||
boot.kernelPackages = lib.mkIf (lib.meta.availableOn pkgs.hostPlatform pkgs.zfs) latestKernelPackage;
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# agit
|
||||
|
||||
A helper script for the AGit workflow with a gitea instance.
|
||||
|
||||
<!-- `$ agit --help` -->
|
||||
|
||||
```
|
||||
usage: agit [-h] {create,c,list,l} ...
|
||||
|
||||
AGit utility for creating and pulling PRs
|
||||
|
||||
positional arguments:
|
||||
{create,c,list,l} Commands
|
||||
create (c) Create an AGit PR
|
||||
list (l) List open AGit pull requests
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
|
||||
The defaults that are assumed are:
|
||||
TARGET_REMOTE_REPOSITORY = origin
|
||||
DEFAULT_TARGET_BRANCH = main
|
||||
|
||||
Examples:
|
||||
$ agit create
|
||||
Opens editor to compose PR title and description (first line is title, rest is body)
|
||||
|
||||
$ agit create --auto
|
||||
Creates PR using latest commit message automatically
|
||||
|
||||
$ agit create --topic "my-feature"
|
||||
Set a custom topic.
|
||||
|
||||
$ agit create --force
|
||||
Force push to a certain topic
|
||||
|
||||
$ agit list
|
||||
Lists all open pull requests for the current repository
|
||||
|
||||
```
|
||||
|
||||
References:
|
||||
- https://docs.gitea.com/usage/agit
|
||||
- https://git-repo.info/en/2020/03/agit-flow-and-git-repo/
|
||||
|
||||
## How to fetch AGit PR's
|
||||
|
||||
For a hypothetical PR with the number #4077:
|
||||
|
||||
```
|
||||
git fetch origin pull/4077/head:your-favorite-name
|
||||
```
|
||||
|
||||
Replace `your-favorite-name` with your preferred branch name.
|
||||
|
||||
You can push back to the PR with with:
|
||||
```
|
||||
agit create --topic="The topic of the open PR"
|
||||
```
|
||||
@@ -1,581 +0,0 @@
|
||||
import argparse
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
# push origin HEAD:refs/for/main
|
||||
# HEAD: The target branch
|
||||
# origin: The target repository (not a fork!)
|
||||
# HEAD: The local branch containing the changes you are proposing
|
||||
TARGET_REMOTE_REPOSITORY = "origin"
|
||||
DEFAULT_TARGET_BRANCH = "main"
|
||||
|
||||
|
||||
def get_gitea_api_url(remote: str = "origin") -> str:
|
||||
"""Parse the gitea api url, this parser is fairly naive, but should work for most setups"""
|
||||
exit_code, remote_url, error = run_git_command(["git", "remote", "get-url", remote])
|
||||
|
||||
if exit_code != 0:
|
||||
print(f"Error getting remote URL for '{remote}': {error}")
|
||||
sys.exit(1)
|
||||
|
||||
# Parse different remote URL formats
|
||||
# SSH formats: git@git.clan.lol:clan/clan-core.git or gitea@git.clan.lol:clan/clan-core.git
|
||||
# HTTPS format: https://git.clan.lol/clan/clan-core.git
|
||||
|
||||
if (
|
||||
"@" in remote_url
|
||||
and ":" in remote_url
|
||||
and not remote_url.startswith("https://")
|
||||
):
|
||||
# SSH format: [user]@git.clan.lol:clan/clan-core.git
|
||||
host_and_path = remote_url.split("@")[1] # git.clan.lol:clan/clan-core.git
|
||||
host = host_and_path.split(":")[0] # git.clan.lol
|
||||
repo_path = host_and_path.split(":")[1] # clan/clan-core.git
|
||||
repo_path = repo_path.removesuffix(".git") # clan/clan-core
|
||||
elif remote_url.startswith("https://"):
|
||||
# HTTPS format: https://git.clan.lol/clan/clan-core.git
|
||||
url_parts = remote_url.replace("https://", "").split("/")
|
||||
host = url_parts[0] # git.clan.lol
|
||||
repo_path = "/".join(url_parts[1:]) # clan/clan-core.git
|
||||
if repo_path.endswith(".git"):
|
||||
repo_path = repo_path.removesuffix(".git") # clan/clan-core
|
||||
else:
|
||||
print(f"Unsupported remote URL format: {remote_url}")
|
||||
sys.exit(1)
|
||||
|
||||
api_url = f"https://{host}/api/v1/repos/{repo_path}/pulls"
|
||||
return api_url
|
||||
|
||||
|
||||
def fetch_open_prs(remote: str = "origin") -> list[dict]:
|
||||
"""Fetch open pull requests from the Gitea API."""
|
||||
api_url = get_gitea_api_url(remote)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(f"{api_url}?state=open") as response:
|
||||
data = json.loads(response.read().decode())
|
||||
return data
|
||||
except urllib.error.URLError as e:
|
||||
print(f"Error fetching PRs from {api_url}: {e}")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing JSON response: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_repo_info_from_api_url(api_url: str) -> tuple[str, str]:
|
||||
"""Extract repository owner and name from API URL."""
|
||||
# api_url format: https://git.clan.lol/api/v1/repos/clan/clan-core/pulls
|
||||
parts = api_url.split("/")
|
||||
if len(parts) >= 6 and "repos" in parts:
|
||||
repo_index = parts.index("repos")
|
||||
if repo_index + 2 < len(parts):
|
||||
owner = parts[repo_index + 1]
|
||||
repo_name = parts[repo_index + 2]
|
||||
return owner, repo_name
|
||||
msg = f"Invalid API URL format: {api_url}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def fetch_pr_statuses(
|
||||
repo_owner: str,
|
||||
repo_name: str,
|
||||
commit_sha: str,
|
||||
host: str,
|
||||
) -> list[dict]:
|
||||
"""Fetch CI statuses for a specific commit SHA."""
|
||||
status_url = (
|
||||
f"https://{host}/api/v1/repos/{repo_owner}/{repo_name}/statuses/{commit_sha}"
|
||||
)
|
||||
|
||||
try:
|
||||
request = urllib.request.Request(status_url)
|
||||
with urllib.request.urlopen(request, timeout=3) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
return data
|
||||
except (urllib.error.URLError, json.JSONDecodeError, TimeoutError):
|
||||
# Fail silently for individual status requests to keep listing fast
|
||||
return []
|
||||
|
||||
|
||||
def get_latest_status_by_context(statuses: list[dict]) -> dict[str, str]:
|
||||
"""Group statuses by context and return the latest status for each context."""
|
||||
context_statuses = {}
|
||||
|
||||
for status in statuses:
|
||||
context = status.get("context", "unknown")
|
||||
created_at = status.get("created_at", "")
|
||||
status_state = status.get("status", "unknown")
|
||||
|
||||
if (
|
||||
context not in context_statuses
|
||||
or created_at > context_statuses[context]["created_at"]
|
||||
):
|
||||
context_statuses[context] = {
|
||||
"status": status_state,
|
||||
"created_at": created_at,
|
||||
}
|
||||
|
||||
return {context: info["status"] for context, info in context_statuses.items()}
|
||||
|
||||
|
||||
def status_to_emoji(status: str) -> str:
|
||||
"""Convert status string to emoji."""
|
||||
status_map = {"success": "✅", "failure": "❌", "pending": "🟡", "error": "❓"}
|
||||
return status_map.get(status.lower(), "❓")
|
||||
|
||||
|
||||
def create_osc8_link(url: str, text: str) -> str:
|
||||
return f"\033]8;;{url}\033\\{text}\033]8;;\033\\"
|
||||
|
||||
|
||||
def format_pr_with_status(pr: dict, remote: str = "origin") -> str:
|
||||
"""Format PR title with status emojis and OSC8 link."""
|
||||
title = pr["title"]
|
||||
pr_url = pr.get("html_url", "")
|
||||
|
||||
commit_sha = pr.get("head", {}).get("sha")
|
||||
if not commit_sha:
|
||||
if pr_url:
|
||||
return create_osc8_link(pr_url, title)
|
||||
return title
|
||||
|
||||
try:
|
||||
api_url = get_gitea_api_url(remote)
|
||||
repo_owner, repo_name = get_repo_info_from_api_url(api_url)
|
||||
|
||||
host = api_url.split("/")[2]
|
||||
|
||||
statuses = fetch_pr_statuses(repo_owner, repo_name, commit_sha, host)
|
||||
if not statuses:
|
||||
if pr_url:
|
||||
return create_osc8_link(pr_url, title)
|
||||
return title
|
||||
|
||||
latest_statuses = get_latest_status_by_context(statuses)
|
||||
|
||||
emojis = [status_to_emoji(status) for status in latest_statuses.values()]
|
||||
formatted_title = f"{title} {' '.join(emojis)}" if emojis else title
|
||||
|
||||
return create_osc8_link(pr_url, formatted_title) if pr_url else formatted_title
|
||||
|
||||
except (ValueError, IndexError):
|
||||
# If there's any error in processing, just return the title with link if available
|
||||
if pr_url:
|
||||
return create_osc8_link(pr_url, title)
|
||||
|
||||
return title
|
||||
|
||||
|
||||
def run_git_command(command: list) -> tuple[int, str, str]:
|
||||
"""Run a git command and return exit code, stdout, and stderr."""
|
||||
try:
|
||||
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
||||
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
||||
except Exception as e:
|
||||
return 1, "", str(e)
|
||||
|
||||
|
||||
def get_current_branch_name() -> str:
|
||||
exit_code, branch_name, error = run_git_command(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
)
|
||||
|
||||
if exit_code != 0:
|
||||
print(f"Error getting branch name: {error}")
|
||||
sys.exit(1)
|
||||
|
||||
return branch_name.strip()
|
||||
|
||||
|
||||
def get_latest_commit_info() -> tuple[str, str]:
|
||||
"""Get the title and body of the latest commit."""
|
||||
exit_code, commit_msg, error = run_git_command(
|
||||
["git", "log", "-1", "--pretty=format:%B"],
|
||||
)
|
||||
|
||||
if exit_code != 0:
|
||||
print(f"Error getting commit info: {error}")
|
||||
sys.exit(1)
|
||||
|
||||
lines = commit_msg.strip().split("\n")
|
||||
title = lines[0].strip() if lines else ""
|
||||
|
||||
body_lines = []
|
||||
for line in lines[1:]:
|
||||
if body_lines or line.strip():
|
||||
body_lines.append(line)
|
||||
|
||||
body = "\n".join(body_lines).strip()
|
||||
|
||||
return title, body
|
||||
|
||||
|
||||
def get_commits_since_main() -> list[tuple[str, str]]:
|
||||
"""Get all commits since main as (title, body) tuples."""
|
||||
exit_code, commit_log, error = run_git_command(
|
||||
[
|
||||
"git",
|
||||
"log",
|
||||
"main..HEAD",
|
||||
"--no-merges",
|
||||
"--pretty=format:%s|%b|---END---",
|
||||
],
|
||||
)
|
||||
|
||||
if exit_code != 0:
|
||||
print(f"Error getting commits since main: {error}")
|
||||
return []
|
||||
|
||||
if not commit_log:
|
||||
return []
|
||||
|
||||
commits = []
|
||||
commit_messages = commit_log.split("---END---")
|
||||
|
||||
for commit_msg in commit_messages:
|
||||
commit_msg = commit_msg.strip()
|
||||
if not commit_msg:
|
||||
continue
|
||||
|
||||
parts = commit_msg.split("|")
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
|
||||
title = parts[0].strip()
|
||||
body = parts[1].strip() if len(parts) > 1 else ""
|
||||
|
||||
if not title:
|
||||
continue
|
||||
|
||||
commits.append((title, body))
|
||||
|
||||
return commits
|
||||
|
||||
|
||||
def open_editor_for_pr() -> tuple[str, str]:
|
||||
"""Open editor to get PR title and description. First line is title, rest is description."""
|
||||
commits_since_main = get_commits_since_main()
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w+",
|
||||
suffix="COMMIT_EDITMSG",
|
||||
delete=False,
|
||||
) as temp_file:
|
||||
temp_file.flush()
|
||||
temp_file_path = temp_file.name
|
||||
|
||||
for title, body in commits_since_main:
|
||||
temp_file.write(f"{title}\n")
|
||||
if body:
|
||||
temp_file.write(f"{body}\n")
|
||||
temp_file.write("\n")
|
||||
|
||||
temp_file.write("\n")
|
||||
temp_file.write("# Please enter the PR title on the first line.\n")
|
||||
temp_file.write("# Lines starting with '#' will be ignored.\n")
|
||||
temp_file.write("# The first line will be used as the PR title.\n")
|
||||
temp_file.write("# Everything else will be used as the PR description.\n")
|
||||
temp_file.write(
|
||||
"# To abort creation of the PR, close editor with an error code.\n",
|
||||
)
|
||||
temp_file.write("# In vim for example you can use :cq!\n")
|
||||
temp_file.write("#\n")
|
||||
temp_file.write("# All commits since main:\n")
|
||||
temp_file.write("#\n")
|
||||
for i, (title, body) in enumerate(commits_since_main, 1):
|
||||
temp_file.write(f"# Commit {i}:\n")
|
||||
temp_file.write(f"# {title}\n")
|
||||
if body:
|
||||
for line in body.split("\n"):
|
||||
temp_file.write(f"# {line}\n")
|
||||
temp_file.write("#\n")
|
||||
|
||||
try:
|
||||
editor = os.environ.get("EDITOR", "vim")
|
||||
|
||||
exit_code = subprocess.call([editor, temp_file_path])
|
||||
|
||||
if exit_code != 0:
|
||||
print(f"Editor exited with code {exit_code}.")
|
||||
print("AGit PR creation has been aborted.")
|
||||
sys.exit(1)
|
||||
|
||||
with Path(temp_file_path).open() as f:
|
||||
content = f.read()
|
||||
|
||||
lines = []
|
||||
for line in content.split("\n"):
|
||||
if not line.lstrip().startswith("#"):
|
||||
lines.append(line)
|
||||
|
||||
cleaned_content = "\n".join(lines).strip()
|
||||
|
||||
if not cleaned_content:
|
||||
print("No content provided, aborting.")
|
||||
sys.exit(0)
|
||||
|
||||
content_lines = cleaned_content.split("\n")
|
||||
title = content_lines[0].strip()
|
||||
|
||||
if not title:
|
||||
print("No title provided, aborting.")
|
||||
sys.exit(0)
|
||||
|
||||
description_lines = []
|
||||
for line in content_lines[1:]:
|
||||
if description_lines or line.strip():
|
||||
description_lines.append(line)
|
||||
|
||||
description = "\n".join(description_lines).strip()
|
||||
|
||||
return title, description
|
||||
|
||||
finally:
|
||||
with contextlib.suppress(OSError):
|
||||
Path(temp_file_path).unlink()
|
||||
|
||||
|
||||
def create_agit_push(
|
||||
remote: str = "origin",
|
||||
branch: str = "main",
|
||||
topic: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
force_push: bool = False,
|
||||
local_branch: str = "HEAD",
|
||||
) -> None:
|
||||
if topic is None:
|
||||
if title is not None:
|
||||
topic = title
|
||||
else:
|
||||
topic = get_current_branch_name()
|
||||
|
||||
refspec = f"{local_branch}:refs/for/{branch}"
|
||||
push_cmd = ["git", "push", remote, refspec]
|
||||
|
||||
push_cmd.extend(["-o", f"topic={topic}"])
|
||||
|
||||
if title:
|
||||
push_cmd.extend(["-o", f"title={title}"])
|
||||
|
||||
if description:
|
||||
escaped_desc = description.rstrip("\n").replace('"', '\\"')
|
||||
push_cmd.extend(["-o", f"description={escaped_desc}"])
|
||||
|
||||
if force_push:
|
||||
push_cmd.extend(["-o", "force-push"])
|
||||
|
||||
if description:
|
||||
print(
|
||||
f" Description: {description[:50]}..."
|
||||
if len(description) > 50
|
||||
else f" Description: {description}",
|
||||
)
|
||||
print()
|
||||
|
||||
exit_code, stdout, stderr = run_git_command(push_cmd)
|
||||
|
||||
if stdout:
|
||||
print(stdout)
|
||||
if stderr:
|
||||
print(stderr, file=sys.stderr)
|
||||
|
||||
if exit_code != 0:
|
||||
print("\nPush failed!")
|
||||
sys.exit(exit_code)
|
||||
else:
|
||||
print("\nPush successful!")
|
||||
|
||||
|
||||
def cmd_create(args: argparse.Namespace) -> None:
|
||||
"""Handle the create subcommand."""
|
||||
title = args.title
|
||||
description = args.description
|
||||
|
||||
if not args.auto and (title is None or description is None):
|
||||
editor_title, editor_description = open_editor_for_pr()
|
||||
if title is None:
|
||||
title = editor_title
|
||||
if description is None:
|
||||
description = editor_description
|
||||
|
||||
create_agit_push(
|
||||
remote=args.remote,
|
||||
branch=args.branch,
|
||||
topic=args.topic,
|
||||
title=title,
|
||||
description=description,
|
||||
force_push=args.force,
|
||||
local_branch=args.local_branch,
|
||||
)
|
||||
|
||||
|
||||
def cmd_list(args: argparse.Namespace) -> None:
|
||||
"""Handle the list subcommand."""
|
||||
prs = fetch_open_prs(args.remote)
|
||||
|
||||
if not prs:
|
||||
print("No open AGit pull requests found.")
|
||||
return
|
||||
|
||||
# This is the only way I found to query the actual AGit PRs
|
||||
# Gitea doesn't seem to have an actual api endpoint for them
|
||||
filtered_prs = [pr for pr in prs if pr.get("head", {}).get("label", "") == ""]
|
||||
|
||||
if not filtered_prs:
|
||||
print("No open AGit pull requests found.")
|
||||
return
|
||||
|
||||
for pr in filtered_prs:
|
||||
formatted_pr = format_pr_with_status(pr, args.remote)
|
||||
print(formatted_pr)
|
||||
|
||||
|
||||
def create_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="agit",
|
||||
description="AGit utility for creating and pulling PRs",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=f"""
|
||||
The defaults that are assumed are:
|
||||
TARGET_REMOTE_REPOSITORY = {TARGET_REMOTE_REPOSITORY}
|
||||
DEFAULT_TARGET_BRANCH = {DEFAULT_TARGET_BRANCH}
|
||||
|
||||
Examples:
|
||||
$ agit create
|
||||
Opens editor to compose PR title and description (first line is title, rest is body)
|
||||
|
||||
$ agit create --auto
|
||||
Creates PR using latest commit message automatically
|
||||
|
||||
$ agit create --topic "my-feature"
|
||||
Set a custom topic.
|
||||
|
||||
$ agit create --force
|
||||
Force push to a certain topic
|
||||
|
||||
$ agit list
|
||||
Lists all open pull requests for the current repository
|
||||
""",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="subcommand", help="Commands")
|
||||
|
||||
create_parser = subparsers.add_parser(
|
||||
"create",
|
||||
aliases=["c"],
|
||||
help="Create an AGit PR",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
$ agit create
|
||||
Opens editor to compose PR title and description (first line is title, rest is body).
|
||||
|
||||
$ agit create --auto
|
||||
Creates PR using latest commit message automatically (old behavior).
|
||||
|
||||
$ agit create --topic "my-feature"
|
||||
Set a custom topic.
|
||||
|
||||
$ agit create --force
|
||||
Force push to a certain topic
|
||||
""",
|
||||
)
|
||||
|
||||
list_parser = subparsers.add_parser(
|
||||
"list",
|
||||
aliases=["l"],
|
||||
help="List open AGit pull requests",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=f"""
|
||||
Examples:
|
||||
$ agit list
|
||||
Lists all open AGit PRs for the current repository.
|
||||
|
||||
$ agit list --remote upstream
|
||||
Lists PRs using the 'upstream' remote instead of '{TARGET_REMOTE_REPOSITORY}'.
|
||||
""",
|
||||
)
|
||||
|
||||
list_parser.add_argument(
|
||||
"-r",
|
||||
"--remote",
|
||||
default=TARGET_REMOTE_REPOSITORY,
|
||||
help=f"Git remote to use for fetching PRs (default: {TARGET_REMOTE_REPOSITORY})",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"-r",
|
||||
"--remote",
|
||||
default=TARGET_REMOTE_REPOSITORY,
|
||||
help=f"Git remote to push to (default: {TARGET_REMOTE_REPOSITORY})",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"-b",
|
||||
"--branch",
|
||||
default=DEFAULT_TARGET_BRANCH,
|
||||
help=f"Target branch for the PR (default: {DEFAULT_TARGET_BRANCH})",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"-l",
|
||||
"--local-branch",
|
||||
default="HEAD",
|
||||
help="Local branch to push (default: HEAD)",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"-t",
|
||||
"--topic",
|
||||
help="Set PR topic (default: current branch name)",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"--title",
|
||||
help="Set the PR title (default: last commit title)",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"--description",
|
||||
help="Override the PR description (default: commit body)",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Force push the changes",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"-a",
|
||||
"--auto",
|
||||
action="store_true",
|
||||
help="Skip editor and use commit message automatically",
|
||||
)
|
||||
|
||||
create_parser.set_defaults(func=cmd_create)
|
||||
list_parser.set_defaults(func=cmd_list)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = create_parser()
|
||||
args = parser.parse_args()
|
||||
if args.subcommand is None:
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
bash,
|
||||
callPackage,
|
||||
git,
|
||||
lib,
|
||||
openssh,
|
||||
...
|
||||
}:
|
||||
let
|
||||
writers = callPackage ../builders/script-writers.nix { };
|
||||
in
|
||||
writers.writePython3Bin "agit" {
|
||||
flakeIgnore = [
|
||||
"E501"
|
||||
"W503" # treefmt reapplies the conditions to trigger this check
|
||||
];
|
||||
makeWrapperArgs = [
|
||||
"--prefix"
|
||||
"PATH"
|
||||
":"
|
||||
(lib.makeBinPath [
|
||||
bash
|
||||
git
|
||||
openssh
|
||||
])
|
||||
];
|
||||
} ./agit.py
|
||||
@@ -5,7 +5,7 @@ from contextlib import ExitStack
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from clan_lib.api import ApiResponse
|
||||
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
|
||||
|
||||
@@ -43,7 +43,7 @@ class ApiBridge(ABC):
|
||||
|
||||
def process_request(self, request: BackendRequest) -> None:
|
||||
"""Process an API request through the middleware chain."""
|
||||
from .middleware import MiddlewareContext
|
||||
from .middleware import MiddlewareContext # noqa: PLC0415
|
||||
|
||||
with ExitStack() as stack:
|
||||
context = MiddlewareContext(
|
||||
@@ -59,7 +59,7 @@ class ApiBridge(ABC):
|
||||
f"{middleware.__class__.__name__} => {request.method_name}",
|
||||
)
|
||||
middleware.process(context)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: BLE001
|
||||
# If middleware fails, handle error
|
||||
self.send_api_error_response(
|
||||
request.op_key or "unknown",
|
||||
@@ -75,8 +75,6 @@ class ApiBridge(ABC):
|
||||
location: list[str],
|
||||
) -> None:
|
||||
"""Send an error response."""
|
||||
from clan_lib.api import ApiError, ErrorDataClass
|
||||
|
||||
error_data = ErrorDataClass(
|
||||
op_key=op_key,
|
||||
status="error",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user