Compare commits

...

171 Commits

Author SHA1 Message Date
pinpox
97f5ffd813 clanServices/wireguard: add networking exports to controllers
Part of https://git.clan.lol/clan/clan-core/issues/5549
2025-10-31 12:38:37 +01:00
clan-bot
196b98da36 Merge pull request 'Update disko' (#5707) from update-disko into main 2025-10-31 10:10:34 +00:00
clan-bot
42acbe95b8 Update disko 2025-10-31 10:00:58 +00:00
clan-bot
b6b065e365 Merge pull request 'Update nixpkgs-dev in devFlake' (#5706) from update-devFlake-nixpkgs-dev into main 2025-10-31 00:08:41 +00:00
clan-bot
4b1955b189 Update nixpkgs-dev in devFlake 2025-10-31 00:02:00 +00:00
clan-bot
ef7ef8b843 Merge pull request 'Update nixpkgs-dev in devFlake' (#5704) from update-devFlake-nixpkgs-dev into main 2025-10-30 20:05:49 +00:00
clan-bot
38c1367322 Update nixpkgs-dev in devFlake 2025-10-30 20:01:49 +00:00
clan-bot
8e72c086fd Merge pull request 'Update nixpkgs-dev in devFlake' (#5702) from update-devFlake-nixpkgs-dev into main 2025-10-30 15:06:22 +00:00
clan-bot
c454b1339d Update nixpkgs-dev in devFlake 2025-10-30 15:01:51 +00:00
hsjobeki
d1b2d43e5b Merge pull request 'services: move into clan submodule' (#5701) from unify-clan into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5701
2025-10-30 13:00:14 +00:00
Johannes Kirschbauer
da98ca0f1c clanLib: remove unused mapInstances 2025-10-30 13:54:05 +01:00
Johannes Kirschbauer
1953540d08 tests: update inventory tests to use whole clan modules 2025-10-30 13:54:05 +01:00
Johannes Kirschbauer
be31b9ce21 docs: remove service options from nuschtSearch
These hacks are blocking the flake level vars and exports
Maybe we bring this back later
So far nobody seemed using nuschtSearch
2025-10-30 13:54:05 +01:00
Johannes Kirschbauer
169b4016e6 docs: set self to clan-core for docs 2025-10-30 13:53:49 +01:00
Johannes Kirschbauer
2e55028a1b services: move into clan submodule 2025-10-30 13:53:49 +01:00
hsjobeki
1d228231f2 Merge pull request 'clan/services: Reduce surface of services wrapper function' (#5700) from unify-clan into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5700
2025-10-30 09:49:56 +00:00
Johannes Kirschbauer
affb926450 services: remove duplicate module args 2025-10-30 10:10:55 +01:00
Johannes Kirschbauer
c7f65e929f inventoryAdapter: replace importedModulesEvaluated by equivalent config 2025-10-30 10:10:31 +01:00
hsjobeki
ba4ff493e8 Merge pull request 'revert: uniqueStrings' (#5699) from hsjobeki-patch-1 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5699
2025-10-30 08:34:59 +00:00
hsjobeki
eb08803e2a revert bfb30251e6
revert lib: replace uniqueStrings after upstreamed

TODO: Reapply after 25.11 release
2025-10-30 08:29:43 +00:00
clan-bot
bbc9486f0e Merge pull request 'Update nixpkgs-dev in devFlake' (#5697) from update-devFlake-nixpkgs-dev into main 2025-10-29 20:06:16 +00:00
clan-bot
999d709350 Update nixpkgs-dev in devFlake 2025-10-29 20:01:48 +00:00
clan-bot
0b1a330cc2 Merge pull request 'Update nixpkgs-dev in devFlake' (#5696) from update-devFlake-nixpkgs-dev into main 2025-10-29 15:06:14 +00:00
clan-bot
995b7cf50d Update nixpkgs-dev in devFlake 2025-10-29 15:01:49 +00:00
clan-bot
5477b13233 Merge pull request 'Update nuschtos in devFlake' (#5690) from update-devFlake-nuschtos into main 2025-10-29 10:08:23 +00:00
clan-bot
d6170e5efb Update nuschtos in devFlake 2025-10-29 10:01:53 +00:00
clan-bot
18fe117363 Merge pull request 'Update nixpkgs-dev in devFlake' (#5689) from update-devFlake-nixpkgs-dev into main 2025-10-29 00:07:47 +00:00
clan-bot
33a868acc2 Update nixpkgs-dev in devFlake 2025-10-29 00:03:27 +00:00
hsjobeki
11372d35e1 Merge pull request 'clan/checks: fix clanLib not checking' (#5685) from check into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5685
2025-10-28 10:51:17 +00:00
Johannes Kirschbauer
b7508b2b43 clan/checks: fix clanLib not checking 2025-10-28 11:46:49 +01:00
clan-bot
183817b769 Merge pull request 'Update nixpkgs-dev in devFlake' (#5684) from update-devFlake-nixpkgs-dev into main 2025-10-28 10:08:10 +00:00
clan-bot
591e53e9be Update nixpkgs-dev in devFlake 2025-10-28 10:01:54 +00:00
hsjobeki
a6a6415e31 Merge pull request 'clan/checks: move into lib function; add tests' (#5683) from role-settings into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5683
2025-10-28 08:46:41 +00:00
Johannes Kirschbauer
0060ead876 clan/checks: move into lib function; add tests 2025-10-28 09:40:31 +01:00
hsjobeki
224e41d3ad Merge pull request 'modules: clean up clan module' (#5679) from role-settings into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5679
2025-10-28 08:22:46 +00:00
Johannes Kirschbauer
b3323007b2 test: update test filesets 2025-10-28 09:04:50 +01:00
Johannes Kirschbauer
3e950bc66f docs: add doc-comment for template submodule 2025-10-28 09:04:50 +01:00
Johannes Kirschbauer
9503b46b21 modules: rename arbitrary interface.nix to 'top-level-interface' 2025-10-28 09:04:50 +01:00
Johannes Kirschbauer
a2cec323a2 modules: move nixos modules into nixosModules folder 2025-10-28 09:04:50 +01:00
Johannes Kirschbauer
4239f4d27f clan/module: explain throw 2025-10-28 09:04:50 +01:00
clan-bot
8ac8264997 Merge pull request 'Update nixpkgs-dev in devFlake' (#5681) from update-devFlake-nixpkgs-dev into main 2025-10-27 20:06:57 +00:00
clan-bot
544a53ae9c Update nixpkgs-dev in devFlake 2025-10-27 20:01:46 +00:00
Luis Hebendanz
89e18482ed Merge pull request 'checks: Fix flakey llm test, improve performance' (#5678) from Qubasa/clan-core:fix_slow_llm into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5678
2025-10-27 16:34:21 +00:00
Qubasa
a8217b5a32 llm checks: Skip parts of the test on aarch64 for performance 2025-10-27 17:25:06 +01:00
Qubasa
bdd5de5628 checks: Fix flakey llm test, improve performance 2025-10-27 17:12:12 +01:00
Mic92
61d8bfd0d1 Merge pull request 'fix: respect directory parameter in machines_dir' (#5677) from fix-custom-directory into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5677
Reviewed-by: hsjobeki <hsjobeki@gmail.com>
2025-10-27 15:42:44 +00:00
Jörg Thalheim
b8d79c7fc2 fix: respect directory parameter in machines_dir
The machines_dir() function was hardcoding "machines" without considering
the directory parameter from buildClan/clan configuration. This caused
update-hardware-config and other commands to write files to the wrong
location when a custom directory was specified (e.g., directory = ./clan).

Solution:
1. Added relativeDirectory to inventoryClass in Nix, computed where both
   self and directory have consistent store paths during evaluation
2. Updated machines_dir() to use this pre-computed relative path from Nix
   via flake.select("clanInternals.inventoryClass.relativeDirectory")

Fixes: https://git.clan.lol/clan/clan-core/issues/2906
2025-10-27 16:37:07 +01:00
hsjobeki
fb25ab028b Merge pull request 'services: add role settings with explicit warning' (#5676) from role-settings into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5676
2025-10-27 15:33:39 +00:00
clan-bot
5b136ecaf0 Merge pull request 'Update nixpkgs-dev in devFlake' (#5675) from update-devFlake-nixpkgs-dev into main 2025-10-27 15:08:23 +00:00
clan-bot
d4733dbb0a Update nixpkgs-dev in devFlake 2025-10-27 15:01:55 +00:00
Johannes Kirschbauer
bfb30251e6 lib: replace uniqueStrings after upstreamed 2025-10-27 14:00:46 +01:00
Johannes Kirschbauer
33115f76b7 services: add role settings with explicit warning 2025-10-27 13:31:44 +01:00
pinpox
9e9208e699 Merge pull request 'yggdrasil: read peers from exports' (#5657) from yggdrasil-export-peers into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5657
2025-10-27 12:13:59 +00:00
hsjobeki
6b3fd57174 Merge pull request 'extraModules: soft deprecation for string extraModules' (#5656) from inline into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5656
2025-10-27 12:12:17 +00:00
clan-bot
3be5237cf6 Merge pull request 'Update nixpkgs-dev in devFlake' (#5674) from update-devFlake-nixpkgs-dev into main 2025-10-26 20:06:09 +00:00
clan-bot
368f80eaae Merge pull request 'Update nix-darwin' (#5665) from update-nix-darwin into main 2025-10-26 20:04:12 +00:00
clan-bot
4d7079534c Update nixpkgs-dev in devFlake 2025-10-26 20:01:51 +00:00
clan-bot
7d4cf1c551 Update nix-darwin 2025-10-26 20:00:59 +00:00
pinpox
ea088b95e9 yggdrasil: read peers from exports 2025-10-26 12:07:52 +01:00
pinpox
a7a37f5320 Update readme 2025-10-26 12:02:17 +01:00
clan-bot
8bda4880a7 Merge pull request 'Update nixpkgs-dev in devFlake' (#5673) from update-devFlake-nixpkgs-dev into main 2025-10-26 10:06:09 +00:00
clan-bot
6eb83618c0 Update nixpkgs-dev in devFlake 2025-10-26 10:01:55 +00:00
pinpox
1fe3833779 Merge pull request 'clanServices/internet: set default for host export' (#5672) from issue-5671 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5672
2025-10-25 20:44:00 +00:00
pinpox
e63f5c966e clanServices/internet: set default for host export 2025-10-25 22:37:17 +02:00
pinpox
69241183ac Merge pull request 'Fix meta.tld option' (#5670) from issue-5669 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5670
2025-10-25 20:18:28 +00:00
pinpox
0a7c65cd27 Fix meta.tld option
The option curretly keeps it default value, even if set. This fixes the
value being passed through correctly, so that the user-specified value
is actually used.

Fixes: #5669
2025-10-25 22:08:44 +02:00
clan-bot
bd13eb3e23 Merge pull request 'Update treefmt-nix' (#5660) from update-treefmt-nix into main 2025-10-25 20:06:14 +00:00
clan-bot
1e5191a16c Update treefmt-nix 2025-10-25 20:01:34 +00:00
clan-bot
1e4bf0dd4e Merge pull request 'Update nixpkgs-dev in devFlake' (#5667) from update-devFlake-nixpkgs-dev into main 2025-10-25 10:07:40 +00:00
clan-bot
4d66dc59aa Update nixpkgs-dev in devFlake 2025-10-25 10:01:59 +00:00
hsjobeki
9a442c15e9 Merge pull request 'docs: add experimental note to 'exports'' (#5662) from exports into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5662
2025-10-24 15:11:27 +00:00
clan-bot
157af90a56 Merge pull request 'Update treefmt-nix in devFlake' (#5661) from update-devFlake-treefmt-nix into main 2025-10-24 15:08:50 +00:00
Johannes Kirschbauer
24b94965d8 docs: add experimental note to 'exports' 2025-10-24 17:05:47 +02:00
clan-bot
46bcad9267 Update treefmt-nix in devFlake 2025-10-24 15:01:55 +00:00
Johannes Kirschbauer
1aba0577dc schemas: filter 'extraModules' from python classes and derived schemas 2025-10-24 16:57:26 +02:00
Luis Hebendanz
383088af2d Merge pull request 'clan_lib/llm: get_llm_turn uses state transitions instead of callback function' (#5659) from Qubasa/clan-core:llm_no_callback2 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5659
2025-10-24 14:37:25 +00:00
Qubasa
c3456c1f0c clan_lib/llm: get_llm_turn uses state transitions instead of callback function 2025-10-24 16:31:09 +02:00
lassulus
183de9209f Merge pull request 'clan_lib select: fix maybe select storing miss as {}' (#5655) from select_cache_fix into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5655
2025-10-24 12:51:33 +00:00
hsjobeki
1df5c5ff60 Merge pull request 'clanTest: throw when usage of 'self' is triggered' (#5658) from specialArgs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5658
2025-10-24 12:16:34 +00:00
Johannes Kirschbauer
e55a3b4fc6 clanTest: throw when usage of 'self' is triggered
That means we rely on a certain structure of the user flake
these assumptions cannot be made. Their flake can have any shape and is not controlled by us
2025-10-24 14:10:17 +02:00
Johannes Kirschbauer
6ee4657da3 extraModules: soft deprecation for string extraModules 2025-10-24 12:54:17 +02:00
lassulus
7294d8bcbe clan_lib select: fix maybe select storing miss as {} 2025-10-24 12:46:48 +02:00
hsjobeki
3fec5aa5b3 Merge pull request 'api: services simplify update services; update tests' (#5654) from inline into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5654
2025-10-24 10:41:26 +00:00
Johannes Kirschbauer
63e55b8631 api: services simplify update services; update tests 2025-10-24 12:19:26 +02:00
clan-bot
dd771f8dd9 Merge pull request 'Update nixpkgs-dev in devFlake' (#5653) from update-devFlake-nixpkgs-dev into main 2025-10-24 10:05:38 +00:00
clan-bot
d31a4cc7d8 Update nixpkgs-dev in devFlake 2025-10-24 10:01:53 +00:00
hsjobeki
73d4cf51af Merge pull request 'services: allow inline modules' (#5652) from inline into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5652
2025-10-23 16:48:06 +00:00
Johannes Kirschbauer
d015218226 api: update models 2025-10-23 18:43:07 +02:00
Johannes Kirschbauer
f50475fcfd services: allow inline modules 2025-10-23 18:43:07 +02:00
Johannes Kirschbauer
ae5efd9e2f inventory: fix path filter wrong length 2025-10-23 18:43:07 +02:00
hsjobeki
c2c2874e82 Merge pull request 'modules: move clan docs into clan-module' (#5651) from lib-modules into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5651
2025-10-23 15:57:38 +00:00
Johannes Kirschbauer
1f8c2a3722 modules: move clan docs into clan-module 2025-10-23 17:52:39 +02:00
hgl
50aa7eb0cf Merge pull request 'ui: use storybook-solidjs-vite for storybook' (#5649) from hgl-sb-vite into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5649
2025-10-23 15:11:01 +00:00
clan-bot
920b39a124 Merge pull request 'Update nixpkgs-dev in devFlake' (#5635) from update-devFlake-nixpkgs-dev into main 2025-10-23 15:06:59 +00:00
Glen Huang
c159ef79d2 ui: use storybook-solidjs-vite for storybook 2025-10-23 23:04:09 +08:00
clan-bot
7a95b169c1 Update nixpkgs-dev in devFlake 2025-10-23 15:02:00 +00:00
hsjobeki
0c3fd40120 Merge pull request 'inventory: clean up unused modules and submodule names' (#5638) from lib-modules into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5638
2025-10-23 13:14:37 +00:00
hgl
7268697dc5 Merge pull request 'ui: not using wasm node' (#5642) from hgl-cleanup into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5642
2025-10-23 12:52:26 +00:00
Glen Huang
ec395bada4 ui: not using wasm node
Tested both on arm64 darwin and linux that build can succeed without them
2025-10-23 20:48:59 +08:00
hsjobeki
757552671c Merge pull request 'API: init delete instance' (#5641) from instance-delete into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5641
2025-10-23 12:46:24 +00:00
Johannes Kirschbauer
04b52d5bee inventory: clean up unused modules and submodule names 2025-10-23 14:44:38 +02:00
Johannes Kirschbauer
ffeb8b892a API: init delete instance 2025-10-23 14:40:52 +02:00
hgl
26806b5750 Merge pull request 'ui: clean up using knip' (#5633) from hgl-storybook into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5633
2025-10-23 12:39:46 +00:00
Glen Huang
6e70054566 ui: clean up using knip 2025-10-23 20:25:47 +08:00
hsjobeki
93268e8592 Merge pull request 'modules/inventory: use filtered serialization of inventory instead' (#5634) from modules-1 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5634
2025-10-23 12:14:48 +00:00
Johannes Kirschbauer
a420c6ca25 inventory/tests: fixup test fixture 2025-10-23 13:51:37 +02:00
Johannes Kirschbauer
69fd13a76f clan-core-for-checks: update 2025-10-23 13:31:47 +02:00
Johannes Kirschbauer
ab3f262c22 modules/inventory: use filtered serialization of inventory
Opens up inventory for partially non-serializable parts
2025-10-23 10:37:14 +02:00
hgl
aabbe0dfac Merge pull request 'run storybook in nix derivation' (#5589) from hgl-storybook into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5589
2025-10-23 07:23:26 +00:00
Glen Huang
35cb99a3a2 app: fix typo 2025-10-23 14:26:12 +08:00
Glen Huang
90e6d77e26 ui: fix running storybook tests in ci 2025-10-23 14:26:12 +08:00
Glen Huang
5fb4751bd8 ui: fail storybook tests if playwright version mismatch 2025-10-23 14:26:12 +08:00
Glen Huang
03640e44a4 ui: make ci run storybook tests 2025-10-23 14:26:12 +08:00
Glen Huang
51fd60917e ui: fix createInstaller.tsx fmt 2025-10-23 14:26:12 +08:00
Glen Huang
2d7e659953 ui: run storybook tests with nix 2025-10-23 14:26:12 +08:00
Brian McGee
c638df8ed9 wip(storybook): run storybook in nix derivation 2025-10-23 14:26:12 +08:00
Brian McGee
ec269a48f3 wip(storybook): run storybook in nix derivation 2025-10-23 14:26:12 +08:00
clan-bot
fc4c9287cb Merge pull request 'Update nixpkgs-dev in devFlake' (#5632) from update-devFlake-nixpkgs-dev into main 2025-10-23 05:07:25 +00:00
clan-bot
fee62373a9 Update nixpkgs-dev in devFlake 2025-10-23 05:02:06 +00:00
pinpox
f075b339b5 Merge pull request 'Add library function to read public vars' (#5628) from lib-vars-helper into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5628
Reviewed-by: Kenji Berthold <aks.kenji@protonmail.com>
2025-10-22 22:52:57 +00:00
pinpox
872a622f71 Add tests 2025-10-23 00:42:32 +02:00
pinpox
4a41c4cefb clanServices/data-mesher: use clanLib.getPublicValue 2025-10-22 23:26:04 +02:00
pinpox
a5cd36e845 clanServices/zerotier: use clanLib.getPublicValue 2025-10-22 23:18:00 +02:00
pinpox
0dd6c08e33 clanServices/wireguard: use clanLib.getPublicValue 2025-10-22 22:48:56 +02:00
pinpox
dc0b7fc3bf Add library function to read public vars 2025-10-22 19:56:10 +02:00
hsjobeki
0d4bbbd17e Merge pull request 'inventory: filter extraModules and remove unneded top-level options' (#5603) from modules-1 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5603
2025-10-22 17:20:20 +00:00
Johannes Kirschbauer
90797ffa7d inventory: filter extraModules and remove unneded top-level options 2025-10-22 19:13:31 +02:00
hsjobeki
7f2bd809d6 Merge pull request 'modules: simplify inventoryClass module' (#5627) from lib-modules into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5627
2025-10-22 16:56:29 +00:00
Johannes Kirschbauer
da7ff9a40a modules/inventory: distributed services output reduce lexical scope 2025-10-22 18:47:30 +02:00
Johannes Kirschbauer
410d0d0532 modules: move input-mapping into inventoryClass submodule 2025-10-22 18:46:10 +02:00
Johannes Kirschbauer
a25d983c87 modules: move introspection module into inventoryClass submodule 2025-10-22 18:28:09 +02:00
clan-bot
3953fa4047 Merge pull request 'Update nixpkgs-dev in devFlake' (#5630) from update-devFlake-nixpkgs-dev into main 2025-10-22 15:08:26 +00:00
clan-bot
ea93cb9987 Merge pull request 'Update nixos-facter-modules' (#5629) from update-nixos-facter-modules into main 2025-10-22 15:08:16 +00:00
clan-bot
c13278f3c2 Update nixpkgs-dev in devFlake 2025-10-22 15:02:13 +00:00
clan-bot
66fdf937e3 Update nixos-facter-modules 2025-10-22 15:01:13 +00:00
Johannes Kirschbauer
843f55f844 modules: simplify inventoryClass module 2025-10-22 16:29:33 +02:00
pinpox
5a5633d779 Merge pull request 'Add meta.tld clan option' (#5619) from add-tld into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5619
2025-10-22 14:19:16 +00:00
Luis Hebendanz
8310433342 Merge pull request 'clan-lib: Add llm API for clan service selection' (#5626) from Qubasa/clan-core:llm_api into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5626
2025-10-22 13:39:35 +00:00
Qubasa
51141772b3 clan_lib: Add llm integration tests 2025-10-22 15:36:11 +02:00
Qubasa
58b88e874f clan_lib: Add llm unit tests 2025-10-22 15:36:11 +02:00
Qubasa
5fb616efb4 clan_lib: Add llm container test data 2025-10-22 15:36:11 +02:00
Qubasa
6c6afd6f4b clan_lib: Move llm.py out of clan_lib/services towards it's own clan_lib/llm folder 2025-10-22 15:36:11 +02:00
Qubasa
4010953041 clan_lib: Add llm api 2025-10-22 15:36:11 +02:00
Luis Hebendanz
6b74c66292 Merge pull request 'clan-cli: Make select query reproducible, by sorting select keys' (#5625) from Qubasa/clan-core:repro_select into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5625
2025-10-22 13:04:01 +00:00
Luis Hebendanz
fd35adbc3e Merge pull request 'clan-cli: Add support for ForwardRef type in type_to_jsonschema and tests' (#5624) from Qubasa/clan-core:forward_ref_add into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5624
2025-10-22 12:58:34 +00:00
Qubasa
f86b0ec3da clan-cli: Make select query reproducible, by sorting select keys 2025-10-22 14:56:07 +02:00
Luis Hebendanz
5f6e0540cd Merge pull request 'clan-cli: Add passthru.sourceWithTest for container_test.py' (#5623) from Qubasa/clan-core:passthru_test into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5623
2025-10-22 12:54:37 +00:00
Qubasa
c7ec9a9715 clan-cli: Add support for ForwardRef type in type_to_jsonschema and tests 2025-10-22 14:45:04 +02:00
Qubasa
841e9135fe clan-cli: Add passthru.sourceWithTest for container_test.py 2025-10-22 14:36:16 +02:00
pinpox
9299cd9666 Add tld 2025-10-22 12:40:09 +02:00
Kenji Berthold
9851993b82 Merge pull request 'pkgs/cli: Autocomplete --build-host' (#5609) from ke-cli-complete-buildhost into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5609
2025-10-22 07:51:50 +00:00
Kenji Berthold
55d1807f07 Merge branch 'main' into ke-cli-complete-buildhost 2025-10-22 07:41:08 +00:00
clan-bot
ee0abdc7f4 Merge pull request 'Update nixpkgs-dev in devFlake' (#5616) from update-devFlake-nixpkgs-dev into main 2025-10-22 00:07:16 +00:00
clan-bot
6c9ab63842 Update nixpkgs-dev in devFlake 2025-10-22 00:02:06 +00:00
a-kenji
d70db5af79 pkgs/cli: Autocomplete buildhost 2025-10-21 22:40:37 +02:00
clan-bot
eae858dec6 Merge pull request 'Update nixos-facter-modules' (#5607) from update-nixos-facter-modules into main 2025-10-21 20:08:56 +00:00
clan-bot
2ec035a1cb Update nixos-facter-modules 2025-10-21 20:01:12 +00:00
hsjobeki
bd6c227bbe Merge pull request 'lib: move modules into a toplevel folder' (#5610) from lib-modules into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5610
2025-10-21 19:06:45 +00:00
Johannes Kirschbauer
02f3474a58 modules: add source to eval tests 2025-10-21 21:01:06 +02:00
Johannes Kirschbauer
c838e08d77 lib/modules: fixup paths 2025-10-21 20:41:28 +02:00
Johannes Kirschbauer
346e3d816a lib/modules: move modules out of lib 2025-10-21 19:35:50 +02:00
Mic92
a15959fad2 Merge pull request 'pkgs/cli: Fix dynamic shell completions' (#5599) from ke-cli-completion-fix into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5599
2025-10-21 14:36:10 +00:00
clan-bot
0e0c2ead1f Merge pull request 'Update sops-nix' (#5601) from update-sops-nix into main 2025-10-21 14:25:13 +00:00
a-kenji
c42381d810 pkgs/cli: Fix dynamic shell completions
Fix dynamic shell completions by correcting parameter names

Dynamic completions for the CLI were not working because completion
functions used `_prefix` as the first parameter name instead of `prefix`.
This is incompatible with how `argcomplete` passes it's arguments.

Add regression test for the prefix parameter as well as some basic shell
completion testing.
2025-10-21 14:19:30 +00:00
clan-bot
587ce7258a Update sops-nix 2025-10-21 14:19:11 +00:00
Mic92
d0bb804843 Merge pull request 'sshd: check searchDomains before accessing openssh-cert generator' (#5519) from fix-sshd-searchdomains-graceful-degrade into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5519
Reviewed-by: Luis Hebendanz <consulting@qube.email>
2025-10-21 14:18:58 +00:00
Jörg Thalheim
410eecb988 monitoring: regenerate test certificates with 100-year expiration
Regenerated telegraf test certificates to use the new 100-year
expiration period instead of the default 30-day expiration.

Related to #5605
2025-10-21 15:28:41 +02:00
Jörg Thalheim
98136142b4 monitoring: extend telegraf certificate expiration to 100 years
The default 30-day expiration was causing certificates to expire
frequently, breaking monitoring. Setting to 100 years provides a
temporary solution until automated certificate rotation is implemented.

Fixes #5605
2025-10-21 15:28:41 +02:00
Jörg Thalheim
37da9fb3e4 sshd: client role inherits searchDomains from all servers
The client role now automatically collects and merges searchDomains from
ALL servers in the instance when not explicitly configured. This eliminates
redundant configuration and ensures clients trust certificates from all
servers.

Also uses lib.mkIf with .exists check to safely handle the openssh-cert
generator access, checking searchDomains first to enable lazy evaluation.
2025-10-21 15:28:41 +02:00
clan-bot
4566ad9789 Merge pull request 'Update nixpkgs-dev in devFlake' (#5602) from update-devFlake-nixpkgs-dev into main 2025-10-21 12:47:33 +00:00
clan-bot
6faacc7dde Update nixpkgs-dev in devFlake 2025-10-21 10:02:01 +00:00
221 changed files with 9486 additions and 3533 deletions

View File

@@ -1,8 +1,10 @@
clanServices/.* @pinpox @kenji clanServices/.* @pinpox @kenji
lib/test/container-test-driver/.* @DavHau @mic92 lib/test/container-test-driver/.* @DavHau @mic92
lib/modules/inventory/.* @hsjobeki lib/inventory/.* @hsjobeki
lib/modules/inventoryClass/.* @hsjobeki lib/inventoryClass/.* @hsjobeki
modules/.* @hsjobeki
pkgs/clan-app/ui/.* @hsjobeki @brianmcgee pkgs/clan-app/ui/.* @hsjobeki @brianmcgee
pkgs/clan-app/clan_app/.* @qubasa @hsjobeki pkgs/clan-app/clan_app/.* @qubasa @hsjobeki

View File

@@ -87,6 +87,7 @@ in
# Container Tests # Container Tests
nixos-test-container = self.clanLib.test.containerTest ./container nixosTestArgs; nixos-test-container = self.clanLib.test.containerTest ./container nixosTestArgs;
nixos-systemd-abstraction = self.clanLib.test.containerTest ./systemd-abstraction nixosTestArgs; nixos-systemd-abstraction = self.clanLib.test.containerTest ./systemd-abstraction nixosTestArgs;
nixos-llm-test = self.clanLib.test.containerTest ./llm nixosTestArgs;
nixos-test-user-firewall-iptables = self.clanLib.test.containerTest ./user-firewall/iptables.nix nixosTestArgs; nixos-test-user-firewall-iptables = self.clanLib.test.containerTest ./user-firewall/iptables.nix nixosTestArgs;
nixos-test-user-firewall-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs; nixos-test-user-firewall-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs;
nixos-test-extra-python-packages = self.clanLib.test.containerTest ./test-extra-python-packages nixosTestArgs; nixos-test-extra-python-packages = self.clanLib.test.containerTest ./test-extra-python-packages nixosTestArgs;

82
checks/llm/default.nix Normal file
View File

@@ -0,0 +1,82 @@
{ self, pkgs, ... }:
let
cli = self.packages.${pkgs.hostPlatform.system}.clan-cli-full;
ollama-model = pkgs.callPackage ./qwen3-4b-instruct.nix { };
in
{
name = "llm";
nodes = {
peer1 =
{ pkgs, ... }:
{
users.users.text-user = {
isNormalUser = true;
linger = true;
uid = 1000;
extraGroups = [ "systemd-journal" ];
};
# Set environment variables for user systemd
environment.extraInit = ''
if [ "$(id -u)" = "1000" ]; then
export XDG_RUNTIME_DIR="/run/user/1000"
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
ollama_dir="$HOME/.ollama"
mkdir -p "$ollama_dir"
ln -sf ${ollama-model}/models "$ollama_dir"/models
fi
'';
# Enable PAM for user systemd sessions
security.pam.services.systemd-user = {
startSession = true;
# Workaround for containers - use pam_permit to avoid helper binary issues
text = pkgs.lib.mkForce ''
account required pam_permit.so
session required pam_permit.so
session required pam_env.so conffile=/etc/pam/environment readenv=0
session required ${pkgs.systemd}/lib/security/pam_systemd.so
'';
};
environment.systemPackages = [
cli
pkgs.ollama
(cli.pythonRuntime.withPackages (
ps: with ps; [
pytest
pytest-xdist
(cli.pythonRuntime.pkgs.toPythonModule cli)
self.legacyPackages.${pkgs.hostPlatform.system}.nixosTestLib
]
))
];
};
};
testScript =
{ ... }:
''
start_all()
peer1.wait_for_unit("multi-user.target")
peer1.wait_for_unit("user@1000.service")
# Fix user journal permissions so text-user can read their own logs
peer1.succeed("chown text-user:systemd-journal /var/log/journal/*/user-1000.journal*")
peer1.succeed("chmod 640 /var/log/journal/*/user-1000.journal*")
# the -o adopts="" is needed to overwrite any args coming from pyproject.toml
# -p no:cacheprovider disables pytest's cacheprovider which tries to write to the nix store in this case
cmd = "su - text-user -c 'pytest -s -n0 -m service_runner -p no:cacheprovider -o addopts="" ${cli.passthru.sourceWithTests}/clan_lib/llm'"
print("Running tests with command: " + cmd)
# Run tests as text-user (environment variables are set automatically)
peer1.succeed(cmd)
'';
}

View File

@@ -0,0 +1,70 @@
{ pkgs }:
let
# Got them from https://github.com/Gholamrezadar/ollama-direct-downloader
# Download manifest
manifest = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/manifests/4b-instruct";
# You'll need to calculate this hash - run the derivation once and it will tell you the correct hash
hash = "sha256-Dtze80WT6sGqK+nH0GxDLc+BlFrcpeyi8nZiwY8Wi6A=";
};
# Download blobs
blob1 = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:b72accf9724e93698c57cbd3b1af2d3341b3d05ec2089d86d273d97964853cd2";
hash = "sha256-tyrM+XJOk2mMV8vTsa8tM0Gz0F7CCJ2G0nPZeWSFPNI=";
};
blob2 = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:85e4a5b7b8ef0e48af0e8658f5aaab9c2324c76c1641493f4d1e25fce54b18b9";
hash = "sha256-heSlt7jvDkivDoZY9aqrnCMkx2wWQUk/TR4l/OVLGLk=";
};
blob3 = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:eade0a07cac7712787bbce23d12f9306adb4781d873d1df6e16f7840fa37afec";
hash = "sha256-6t4KB8rHcSeHu84j0S+TBq20eB2HPR324W94QPo3r+w=";
};
blob4 = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:d18a5cc71b84bc4af394a31116bd3932b42241de70c77d2b76d69a314ec8aa12";
hash = "sha256-0YpcxxuEvErzlKMRFr05MrQiQd5wx30rdtaaMU7IqhI=";
};
blob5 = pkgs.fetchurl {
url = "https://registry.ollama.ai/v2/library/qwen3/blobs/sha256:0914c7781e001948488d937994217538375b4fd8c1466c5e7a625221abd3ea7a";
hash = "sha256-CRTHeB4AGUhIjZN5lCF1ODdbT9jBRmxeemJSIavT6no=";
};
in
pkgs.stdenv.mkDerivation {
pname = "ollama-qwen3-4b-instruct";
version = "1.0";
dontUnpack = true;
buildPhase = ''
mkdir -p $out/models/manifests/registry.ollama.ai/library/qwen3
mkdir -p $out/models/blobs
# Copy manifest
cp ${manifest} $out/models/manifests/registry.ollama.ai/library/qwen3/4b-instruct
# Copy blobs with correct names
cp ${blob1} $out/models/blobs/sha256-b72accf9724e93698c57cbd3b1af2d3341b3d05ec2089d86d273d97964853cd2
cp ${blob2} $out/models/blobs/sha256-85e4a5b7b8ef0e48af0e8658f5aaab9c2324c76c1641493f4d1e25fce54b18b9
cp ${blob3} $out/models/blobs/sha256-eade0a07cac7712787bbce23d12f9306adb4781d873d1df6e16f7840fa37afec
cp ${blob4} $out/models/blobs/sha256-d18a5cc71b84bc4af394a31116bd3932b42241de70c77d2b76d69a314ec8aa12
cp ${blob5} $out/models/blobs/sha256-0914c7781e001948488d937994217538375b4fd8c1466c5e7a625221abd3ea7a
'';
installPhase = ''
# buildPhase already created everything in $out
:
'';
meta = with pkgs.lib; {
description = "Qwen3 4B Instruct model for Ollama";
license = "apache-2.0";
platforms = platforms.all;
};
}

View File

@@ -62,6 +62,6 @@ in
peer1.succeed("chmod 640 /var/log/journal/*/user-1000.journal*") peer1.succeed("chmod 640 /var/log/journal/*/user-1000.journal*")
# Run tests as text-user (environment variables are set automatically) # Run tests as text-user (environment variables are set automatically)
peer1.succeed("su - text-user -c 'pytest -s -n0 ${cli}/${cli.pythonRuntime.sitePackages}/clan_lib/service_runner'") peer1.succeed("su - text-user -c 'pytest -p no:cacheprovider -o addopts="" -s -n0 ${cli.passthru.sourceWithTests}/clan_lib/service_runner'")
''; '';
} }

View File

@@ -1,4 +1,7 @@
{ ... }: {
clanLib,
...
}:
let let
sharedInterface = sharedInterface =
{ lib, ... }: { lib, ... }:
@@ -51,15 +54,15 @@ let
builtins.foldl' ( builtins.foldl' (
urls: name: urls: name:
let let
ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value"; ip = clanLib.vars.getPublicValue {
flake = config.clan.core.settings.directory;
machine = name;
generator = "zerotier";
file = "zerotier-ip";
default = null;
};
in in
if builtins.pathExists ipPath then if ip != null then urls ++ [ "[${ip}]:${builtins.toString settings.network.port}" ] else urls
let
ip = builtins.readFile ipPath;
in
urls ++ [ "[${ip}]:${builtins.toString settings.network.port}" ]
else
urls
) [ ] (builtins.attrNames ((roles.admin.machines or { }) // (roles.signer.machines or { }))) ) [ ] (builtins.attrNames ((roles.admin.machines or { }) // (roles.signer.machines or { })))
); );
@@ -156,9 +159,14 @@ in
readHostKey = readHostKey =
machine: machine:
let let
path = "${config.clan.core.settings.directory}/vars/per-machine/${machine}/data-mesher-host-key/public_key/value"; publicKey = clanLib.vars.getPublicValue {
flake = config.clan.core.settings.directory;
inherit machine;
generator = "data-mesher-host-key";
file = "public_key";
};
in in
builtins.elemAt (lib.splitString "\n" (builtins.readFile path)) 1; builtins.elemAt (lib.splitString "\n" publicKey) 1;
in in
{ {
enable = true; enable = true;

View File

@@ -16,6 +16,7 @@
options = { options = {
host = lib.mkOption { host = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "";
description = '' description = ''
ip address or hostname (domain) of the machine ip address or hostname (domain) of the machine
''; '';

View File

@@ -44,8 +44,10 @@
pkgs.openssl pkgs.openssl
]; ];
# TODO: Implement automated certificate rotation instead of using a 100-year expiration
script = '' script = ''
openssl req -x509 -nodes -newkey rsa:4096 \ openssl req -x509 -nodes -newkey rsa:4096 \
-days 36500 \
-keyout "$out"/key \ -keyout "$out"/key \
-out "$out"/crt \ -out "$out"/crt \
-subj "/C=US/ST=CA/L=San Francisco/O=Example Corp/OU=IT/CN=example.com" -subj "/C=US/ST=CA/L=San Francisco/O=Example Corp/OU=IT/CN=example.com"

View File

@@ -1,33 +1,33 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
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
k1PNWTkgLsXO8DNkCudxcvPElXfmaw6zwaLrZys= D1hOs4gOQ853+83L/Ej4ESTj0B04NLVMlzMGtl3qcA==
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

@@ -1,19 +1,18 @@
{ {
"data": "ENC[AES256_GCM,data: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,iv:q3j8EAokyyxiszf+wyRqxEr2igaD1bX7YnFx/NbsGg8=,tag:HKKYWRJEUwW2/TxL+5dSng==,type:str]", "data": "ENC[AES256_GCM,data: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,iv:dGSmyDNBdVyF54bYS/Zxm2NNXZyGtLjkyYlrI9/nKvc=,tag:ip2fy76NjObWbW20HyuZUA==,type:str]",
"sops": { "sops": {
"age": [ "age": [
{ {
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck", "recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaeXRjU214aWk5ajl1aW9E\naGJlb1ViaVRmMTBHdkFDQUNDZS94WFZiNUNvCllmWTJBck9hR3U3V09VWDZwQ2xI\nd3ZEQnBIUG5ZSTVIdS8rQ2FMYVhyNk0KLS0tIEE1UG8rSzFyU01sVXhGVHpoaE9i\nSis4Qi9tMGFqbTNMTDZUVk1ZdXkrM28Km4VkfaOsZ69ckjvrg+os43H/O1IoWHzC\nt4LqZRz1Tk7/d1aLWavSPPjVYrCOMZeNBqGbQpGfjjuXrafClRNQdQ==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNQS96MUFubVdOR2VCc2xO\ncTc5QnNHNTFpdURnSnF3dVhBQXQ3bnBuRW1RCngzSVlhSW9rNUxoSWdKcEtKVXc3\nQitLZ2NDUXBSUmxtVWpYRUlvOHVXcW8KLS0tIGZaWlRVak9NYmt2elpwYStYenRE\nanlkT3BET1FjQ2lFZkp3SXFMSkJSaVkKKkr+MNNqs6Ve3K5OrZfBEGlnc7OAthqf\nOZrP9NYOTMgkvhFsZTVpUS0zskry0iwmTNt+KeluYf0Tko8K53Kx2A==\n-----END AGE ENCRYPTED FILE-----\n"
}, },
{ {
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3R1RHTGViTnRLVVkyM3J0\nbm96cGVPTlo4NXBNL0g1eEVSNG9DUkgwVFRBCmRKVTlMRmV3Tmg2RTZIclBlWlcr\ndzI5MUxhcllzbE1IMDNxa08zVkpITmsKLS0tIG01Y2dyQkY3UmRudFk2d0p6bThn\nemlaWnZoS3p4VHhMTFFwTm9VN0ttYzQKVbLFgtK6NIRIiryWHeeOPD45iwUds4QD\n7b8xYYoxlo+DETggxK6Vz3IdT/BSK5bFtgAxl864b5gW+Aw4c6AO5w==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXd2dVYmJIbUVVeXk5Nk1E\nekFiUldVVUhRTmE4dHRiTHNDdEMyS1pRV1RrCkNScGdXVSs4UU5id29DV0pZWDQr\nenV1QmpnOFk5aFpTTUxmb0hDVHZDdFkKLS0tIHpmalJtRC94bEhaUStmeUlHT21w\nd3o3UzJHZklxK0RCYUUxc2c3aG1XclkKEPq1ZgyGiAK/Hy4zT7wfdDfPEE3vMHpR\nzwQV5y3M3DmlnKQEvJu0DpQ334CyAcubZC7cswQdUrM8TPqJhb/TuA==\n-----END AGE ENCRYPTED FILE-----\n"
} }
], ],
"lastmodified": "2025-09-18T14:33:37Z", "lastmodified": "2025-10-21T13:17:17Z",
"mac": "ENC[AES256_GCM,data:XKCnd0QrAlOCECSeSvbLYHMLbmUh4fMRnLaTb5ARoP4Zc9joWGsCaRZxokc2/sG4BXA/6pkbQXHyIOudKbcBpVjjvs9E+6Mnzt53nfRoH/iOkYPbN2EO49okVZJXW0M1rlBxrxvGuiDlz2p2p6L7neKLy4EB482pYea5+dUr2Yw=,iv:oj/MkZCfkvCmAb79uzEvKwEAm1bKtWhS4rPRAWSgRgw=,tag:h5TPPILXkhJplnDT2Gqtfw==,type:str]", "mac": "ENC[AES256_GCM,data:wdAFURkJZvclbz3UFPSPV9fma7zrZVEhMhsRqylGQMLepX/WohEAr8nJgeHl05be1Q8M8biPXCCoL0vfwg4BRZOkhD8PusJh8iBI3+STNQe/S1qoIK1ByfBFhJD+tIsVsgduLp6G32e6SRNvkuX3UpJqyViuRUavfQd3b8LRU4I=,iv:S3sMNTz5Kg4TxHj1tnk/ayiFuO74dR4aPnnomtkGByo=,tag:uive2bYe42s6VtPd03jTMw==,type:str]",
"unencrypted_suffix": "_unencrypted", "version": "3.11.0"
"version": "3.10.2"
} }
} }

View File

@@ -29,7 +29,7 @@
}; };
perInstance = perInstance =
{ settings, ... }: { settings, roles, ... }:
{ {
nixosModule = nixosModule =
{ {
@@ -38,8 +38,19 @@
pkgs, pkgs,
... ...
}: }:
let
uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list);
# Collect searchDomains from all servers in this instance
allServerSearchDomains = lib.flatten (
lib.mapAttrsToList (_name: machineConfig: machineConfig.settings.certificate.searchDomains or [ ]) (
roles.server.machines or { }
)
);
# Merge client's searchDomains with all servers' searchDomains
searchDomains = uniqueStrings (settings.certificate.searchDomains ++ allServerSearchDomains);
in
{ {
clan.core.vars.generators.openssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) { clan.core.vars.generators.openssh-ca = lib.mkIf (searchDomains != [ ]) {
share = true; share = true;
files.id_ed25519.deploy = false; files.id_ed25519.deploy = false;
files."id_ed25519.pub" = { files."id_ed25519.pub" = {
@@ -54,9 +65,9 @@
''; '';
}; };
programs.ssh.knownHosts.ssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) { programs.ssh.knownHosts.ssh-ca = lib.mkIf (searchDomains != [ ]) {
certAuthority = true; certAuthority = true;
extraHostNames = builtins.map (domain: "*.${domain}") settings.certificate.searchDomains; extraHostNames = builtins.map (domain: "*.${domain}") searchDomains;
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value; publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
}; };
}; };

View File

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

View File

@@ -41,14 +41,14 @@ let
# In this case it is 'self-zerotier-redux' # In this case it is 'self-zerotier-redux'
# This is usually only used internally, but we can use it to test the evaluation of service module in isolation # This is usually only used internally, but we can use it to test the evaluation of service module in isolation
# evaluatedService = # evaluatedService =
# testFlake.clanInternals.inventoryClass.distributedServices.importedModulesEvaluated.self-zerotier-redux.config; # testFlake.clanInternals.inventoryClass.distributedServices.servicesEval.config.mappedServices.self-zerotier-redux.config;
in in
{ {
test_simple = { test_simple = {
inherit testFlake; inherit testFlake;
expr = expr =
testFlake.config.clan.clanInternals.inventoryClass.distributedServices.importedModulesEvaluated.self-wifi.config; testFlake.config.clan.clanInternals.inventoryClass.distributedServices.servicesEval.config.mappedServices.self-wifi.config;
expected = 1; expected = 1;
# expr = { # expr = {

View File

@@ -54,7 +54,12 @@
- For other controllers: The controller's /56 subnet - For other controllers: The controller's /56 subnet
*/ */
{ ... }: {
clanLib,
lib,
directory,
...
}:
let let
# Shared module for extraHosts configuration # Shared module for extraHosts configuration
extraHostsModule = extraHostsModule =
@@ -74,10 +79,12 @@ let
controllerHosts = lib.mapAttrsToList ( controllerHosts = lib.mapAttrsToList (
name: _value: name: _value:
let let
prefix = builtins.readFile ( prefix = clanLib.vars.getPublicValue {
config.clan.core.settings.directory flake = config.clan.core.settings.directory;
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value" machine = name;
); generator = "wireguard-network-${instanceName}";
file = "prefix";
};
# Controller IP is always ::1 in their subnet # Controller IP is always ::1 in their subnet
ip = prefix + "::1"; ip = prefix + "::1";
in in
@@ -88,20 +95,24 @@ let
peerHosts = lib.mapAttrsToList ( peerHosts = lib.mapAttrsToList (
peerName: peerValue: peerName: peerValue:
let let
peerSuffix = builtins.readFile ( peerSuffix = clanLib.vars.getPublicValue {
config.clan.core.settings.directory flake = config.clan.core.settings.directory;
+ "/vars/per-machine/${peerName}/wireguard-network-${instanceName}/suffix/value" machine = peerName;
); generator = "wireguard-network-${instanceName}";
file = "suffix";
};
# Determine designated controller # Determine designated controller
designatedController = designatedController =
if (builtins.length (builtins.attrNames roles.controller.machines) == 1) then if (builtins.length (builtins.attrNames roles.controller.machines) == 1) then
(builtins.head (builtins.attrNames roles.controller.machines)) (builtins.head (builtins.attrNames roles.controller.machines))
else else
peerValue.settings.controller; peerValue.settings.controller;
controllerPrefix = builtins.readFile ( controllerPrefix = clanLib.vars.getPublicValue {
config.clan.core.settings.directory flake = config.clan.core.settings.directory;
+ "/vars/per-machine/${designatedController}/wireguard-network-${instanceName}/prefix/value" machine = designatedController;
); generator = "wireguard-network-${instanceName}";
file = "prefix";
};
peerIP = controllerPrefix + ":" + peerSuffix; peerIP = controllerPrefix + ":" + peerSuffix;
in in
"${peerIP} ${peerName}.${domain}" "${peerIP} ${peerName}.${domain}"
@@ -220,10 +231,12 @@ in
lib.mapAttrsToList ( lib.mapAttrsToList (
ctrlName: _: ctrlName: _:
let let
controllerPrefix = builtins.readFile ( controllerPrefix = clanLib.vars.getPublicValue {
config.clan.core.settings.directory flake = config.clan.core.settings.directory;
+ "/vars/per-machine/${ctrlName}/wireguard-network-${instanceName}/prefix/value" machine = ctrlName;
); generator = "wireguard-network-${instanceName}";
file = "prefix";
};
peerIP = controllerPrefix + ":" + peerSuffix; peerIP = controllerPrefix + ":" + peerSuffix;
in in
"${peerIP}/56" "${peerIP}/56"
@@ -234,20 +247,22 @@ in
# Connect to all controllers # Connect to all controllers
peers = lib.mapAttrsToList (name: value: { peers = lib.mapAttrsToList (name: value: {
publicKey = ( publicKey = clanLib.vars.getPublicValue {
builtins.readFile ( flake = config.clan.core.settings.directory;
config.clan.core.settings.directory machine = name;
+ "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value" generator = "wireguard-keys-${instanceName}";
) file = "publickey";
); };
# Allow each controller's /56 subnet # Allow each controller's /56 subnet
allowedIPs = [ allowedIPs = [
"${ "${
builtins.readFile ( clanLib.vars.getPublicValue {
config.clan.core.settings.directory flake = config.clan.core.settings.directory;
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value" machine = name;
) generator = "wireguard-network-${instanceName}";
file = "prefix";
}
}::/56" }::/56"
]; ];
@@ -285,6 +300,18 @@ in
... ...
}: }:
{ {
exports.networking = {
peers = lib.mapAttrs (name: _machine: {
host.plain =
clanLib.vars.getPublicValue {
flake = directory;
machine = name;
generator = "wireguard-network-${instanceName}";
file = "prefix";
}
+ "::1";
}) roles.controller.machines;
};
# Controllers connect to all peers and other controllers # Controllers connect to all peers and other controllers
nixosModule = nixosModule =
@@ -349,25 +376,29 @@ in
if allPeers ? ${name} then if allPeers ? ${name} then
# For peers: they now have our entire /56 subnet # For peers: they now have our entire /56 subnet
{ {
publicKey = ( publicKey = clanLib.vars.getPublicValue {
builtins.readFile ( flake = config.clan.core.settings.directory;
config.clan.core.settings.directory machine = name;
+ "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value" generator = "wireguard-keys-${instanceName}";
) file = "publickey";
); };
# Allow the peer's /96 range in ALL controller subnets # Allow the peer's /96 range in ALL controller subnets
allowedIPs = lib.mapAttrsToList ( allowedIPs = lib.mapAttrsToList (
ctrlName: _: ctrlName: _:
let let
controllerPrefix = builtins.readFile ( controllerPrefix = clanLib.vars.getPublicValue {
config.clan.core.settings.directory flake = config.clan.core.settings.directory;
+ "/vars/per-machine/${ctrlName}/wireguard-network-${instanceName}/prefix/value" machine = ctrlName;
); generator = "wireguard-network-${instanceName}";
peerSuffix = builtins.readFile ( file = "prefix";
config.clan.core.settings.directory };
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/suffix/value" peerSuffix = clanLib.vars.getPublicValue {
); flake = config.clan.core.settings.directory;
machine = name;
generator = "wireguard-network-${instanceName}";
file = "suffix";
};
in in
"${controllerPrefix}:${peerSuffix}/96" "${controllerPrefix}:${peerSuffix}/96"
) roles.controller.machines; ) roles.controller.machines;
@@ -377,19 +408,21 @@ in
else else
# For other controllers: use their /56 subnet # For other controllers: use their /56 subnet
{ {
publicKey = ( publicKey = clanLib.vars.getPublicValue {
builtins.readFile ( flake = config.clan.core.settings.directory;
config.clan.core.settings.directory machine = name;
+ "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value" generator = "wireguard-keys-${instanceName}";
) file = "publickey";
); };
allowedIPs = [ allowedIPs = [
"${ "${
builtins.readFile ( clanLib.vars.getPublicValue {
config.clan.core.settings.directory flake = config.clan.core.settings.directory;
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value" machine = name;
) generator = "wireguard-network-${instanceName}";
file = "prefix";
}
}::/56" }::/56"
]; ];

View File

@@ -1,7 +1,23 @@
🚧🚧🚧 Experimental 🚧🚧🚧
Use at your own risk.
We are still refining its interfaces, instability and breakages are expected.
---
This module sets up [yggdrasil](https://yggdrasil-network.github.io/) across your clan. This module sets up [yggdrasil](https://yggdrasil-network.github.io/) across your clan.
Yggdrasil is designed to be a future-proof and decentralised alternative to Yggdrasil is designed to be a future-proof and decentralised alternative to the
the structured routing protocols commonly used today on the internet. Inside your clan, it will allow you to reach all of your machines. structured routing protocols commonly used today on the internet. Inside your
clan, it will allow you to reach all of your machines.
If you have other services in your inventory which export peers (e.g. the
`internet` or the services) as [service
exports](https://docs.clan.lol/reference/options/clan_service/#exports), they
will be added as yggdrasil peers automatically. This allows using the stable
yggdrasil IPv6 address to refer to other hosts and letting yggdrasil decide on
the best routing based on available connections.
## Example Usage ## Example Usage

View File

@@ -29,12 +29,13 @@
]; ];
}; };
options.peers = lib.mkOption { options.extraPeers = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = [ ]; default = [ ];
description = '' description = ''
Static peers to configure for this host. Additional static peers to configure for this host. If you use a
If not set, local peers will be auto-discovered VPN clan service, it will automatically be added as peers to other hosts.
Local peers are also auto-discovered and don't need to be added.
''; '';
example = [ example = [
"tcp://192.168.1.1:6443" "tcp://192.168.1.1:6443"
@@ -45,16 +46,67 @@
}; };
}; };
perInstance = perInstance =
{ settings, ... }: {
settings,
roles,
exports,
...
}:
{ {
nixosModule = nixosModule =
{ {
config, config,
pkgs, pkgs,
lib,
clan-core,
... ...
}: }:
let
mkPeers = ip: [
# "tcp://${ip}:6443"
"quic://${ip}:6443"
"ws://${ip}:6443"
"tls://${ip}:6443"
];
select' = clan-core.inputs.nix-select.lib.select;
# TODO make it nicer @lassulus, @picnoir wants microlens
# Get a list of all exported IPs from all VPN modules
exportedPeerIPs = builtins.foldl' (
acc: e:
if e == { } then
acc
else
acc ++ (lib.flatten (builtins.filter (s: s != "") (lib.attrValues (select' "peers.*.plain" e))))
) [ ] (lib.attrValues (select' "instances.*.networking.?peers.*.host.?plain" exports));
# Construct a list of peers in yggdrasil format
exportedPeers = lib.flatten (map mkPeers exportedPeerIPs);
in
{ {
# Set <yggdrasil ip> <hostname>.<tld> for all hosts.
# Networking modules will then add themselves as peers, so we can
# always use this to resolve a host via the best possible route,
# doing fail-over if needed.
networking.extraHosts = lib.strings.concatStringsSep "\n" (
lib.filter (n: n != "") (
map (
name:
let
ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/yggdrasil/address/value";
in
if builtins.pathExists ipPath then
"${builtins.readFile ipPath} ${name}.${config.clan.core.settings.tld}"
else
""
) (lib.attrNames roles.default.machines)
)
);
clan.core.vars.generators.yggdrasil = { clan.core.vars.generators.yggdrasil = {
files.privateKey = { }; files.privateKey = { };
@@ -99,7 +151,7 @@
settings = { settings = {
PrivateKeyPath = "/key"; PrivateKeyPath = "/key";
IfName = "ygg"; IfName = "ygg";
Peers = settings.peers; Peers = lib.lists.unique (exportedPeers ++ settings.extraPeers);
MulticastInterfaces = [ MulticastInterfaces = [
# Ethernet is preferred over WIFI # Ethernet is preferred over WIFI
{ {

View File

@@ -17,6 +17,13 @@
roles.default.machines.peer1 = { }; roles.default.machines.peer1 = { };
roles.default.machines.peer2 = { }; roles.default.machines.peer2 = { };
}; };
# Peers are set form exports of the internet service
instances."internet" = {
module.name = "internet";
roles.default.machines.peer1.settings.host = "peer1";
roles.default.machines.peer2.settings.host = "peer2";
};
}; };
}; };

View File

@@ -1,4 +1,7 @@
{ ... }: {
clanLib,
...
}:
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/zerotier"; manifest.name = "clan-core/zerotier";
@@ -39,6 +42,7 @@
imports = [ imports = [
(import ./shared.nix { (import ./shared.nix {
inherit inherit
clanLib
instanceName instanceName
roles roles
config config
@@ -90,6 +94,7 @@
imports = [ imports = [
(import ./shared.nix { (import ./shared.nix {
inherit inherit
clanLib
instanceName instanceName
roles roles
config config
@@ -142,6 +147,7 @@
imports = [ imports = [
(import ./shared.nix { (import ./shared.nix {
inherit inherit
clanLib
instanceName instanceName
roles roles
config config
@@ -160,15 +166,16 @@
); );
networkIps = builtins.foldl' ( networkIps = builtins.foldl' (
ips: name: ips: name:
if let
builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value" ztIp = clanLib.vars.getPublicValue {
then flake = config.clan.core.settings.directory;
ips machine = name;
++ [ generator = "zerotier";
(builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value") file = "zerotier-ip";
] default = null;
else };
ips in
if ztIp != null then ips ++ [ ztIp ] else ips
) [ ] machines; ) [ ] machines;
allHostIPs = settings.allowedIps ++ networkIps; allHostIPs = settings.allowedIps ++ networkIps;
in in

View File

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

View File

@@ -1,4 +1,5 @@
{ {
clanLib,
lib, lib,
config, config,
pkgs, pkgs,
@@ -8,20 +9,26 @@
}: }:
let let
controllerMachine = builtins.head (lib.attrNames roles.controller.machines or { }); controllerMachine = builtins.head (lib.attrNames roles.controller.machines or { });
networkIdPath = "${config.clan.core.settings.directory}/vars/per-machine/${controllerMachine}/zerotier/zerotier-network-id/value"; networkId = clanLib.vars.getPublicValue {
networkId = if builtins.pathExists networkIdPath then builtins.readFile networkIdPath else null; flake = config.clan.core.settings.directory;
machine = controllerMachine;
generator = "zerotier";
file = "zerotier-network-id";
default = null;
};
moons = lib.attrNames (roles.moon.machines or { }); moons = lib.attrNames (roles.moon.machines or { });
moonIps = builtins.foldl' ( moonIps = builtins.foldl' (
ips: name: ips: name:
if let
builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value" moonIp = clanLib.vars.getPublicValue {
then flake = config.clan.core.settings.directory;
ips machine = name;
++ [ generator = "zerotier";
(builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value") file = "zerotier-ip";
] default = null;
else };
ips in
if moonIp != null then ips ++ [ moonIp ] else ips
) [ ] moons; ) [ ] moons;
in in
{ {

24
devFlake/flake.lock generated
View File

@@ -3,10 +3,10 @@
"clan-core-for-checks": { "clan-core-for-checks": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1760361585, "lastModified": 1761204206,
"narHash": "sha256-v4PnSmt1hXW4dSgVWxcd1ZeEBlhO7NksNRC5cX7L5iw=", "narHash": "sha256-A4KDudGblln1yh8c95OVow2NRlHtbGZXr/pgNenyrNc=",
"ref": "main", "ref": "main",
"rev": "7e7e58eb64ef61beb0a938a6622ec0122382131b", "rev": "aabbe0dfac47b7cfbe2210bcb27fb7ecce93350f",
"shallow": true, "shallow": true,
"type": "git", "type": "git",
"url": "https://git.clan.lol/clan/clan-core" "url": "https://git.clan.lol/clan/clan-core"
@@ -105,11 +105,11 @@
}, },
"nixpkgs-dev": { "nixpkgs-dev": {
"locked": { "locked": {
"lastModified": 1760965023, "lastModified": 1761853358,
"narHash": "sha256-cpcgkeLApMGFCdp4jFqeIxTwlcGaSI+Zwmv8z2E85pY=", "narHash": "sha256-1tBdsBzYJOzVzNOmCFzFMWHw7UUbhkhiYCFGr+OjPTs=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "40ef6b9aa73f70b265c29df083fafae66b9df351", "rev": "262333bca9b49964f8e3cad3af655466597c01d4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -128,11 +128,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760652422, "lastModified": 1761730856,
"narHash": "sha256-C88Pgz38QIl9JxQceexqL2G7sw9vodHWx1Uaq+NRJrw=", "narHash": "sha256-t1i5p/vSWwueZSC0Z2BImxx3BjoUDNKyC2mk24krcMY=",
"owner": "NuschtOS", "owner": "NuschtOS",
"repo": "search", "repo": "search",
"rev": "3ebeebe8b6a49dfb11f771f761e0310f7c48d726", "rev": "e29de6db0cb3182e9aee75a3b1fd1919d995d85b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -208,11 +208,11 @@
"nixpkgs": [] "nixpkgs": []
}, },
"locked": { "locked": {
"lastModified": 1760945191, "lastModified": 1761311587,
"narHash": "sha256-ZRVs8UqikBa4Ki3X4KCnMBtBW0ux1DaT35tgsnB1jM4=", "narHash": "sha256-Msq86cR5SjozQGCnC6H8C+0cD4rnx91BPltZ9KK613Y=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "f56b1934f5f8fcab8deb5d38d42fd692632b47c2", "rev": "2eddae033e4e74bf581c2d1dfa101f9033dbd2dc",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -88,6 +88,7 @@ For the provide flake example, your flake should now look like this:
self = self; # this needs to point at the repository root self = self; # this needs to point at the repository root
specialArgs = {}; specialArgs = {};
meta.name = throw "Change me to something unique"; meta.name = throw "Change me to something unique";
meta.tld = throw "Change me to something unique";
machines = { machines = {
berlin = { berlin = {

View File

@@ -137,12 +137,13 @@ Description: None
This confirms your setup is working correctly. This confirms your setup is working correctly.
You can now change the default name by editing the `meta.name` field in your `clan.nix` file. You can now change the default name and tld by editing the `meta.name` and `meta.tld` fields in your `clan.nix` file.
```{.nix title="clan.nix" hl_lines="3"} ```{.nix title="clan.nix" hl_lines="3 4"}
{ {
# Ensure this is unique among all clans you want to use. # Ensure this is unique among all clans you want to use.
meta.name = "__CHANGE_ME__"; meta.name = "__CHANGE_ME__";
meta.tld = "changeme";
# ... # ...
# elided # elided

View File

@@ -10,10 +10,11 @@ and how to define a remote builder for your machine closures.
Set the machines `targetHost` to the reachable IP address of the new machine. Set the machines `targetHost` to the reachable IP address of the new machine.
This eliminates the need to specify `--target-host` in CLI commands. This eliminates the need to specify `--target-host` in CLI commands.
```{.nix title="clan.nix" hl_lines="9"} ```{.nix title="clan.nix" hl_lines="10"}
{ {
# Ensure this is unique among all clans you want to use. # Ensure this is unique among all clans you want to use.
meta.name = "my-clan"; meta.name = "my-clan";
meta.tld = "ccc";
inventory.machines = { inventory.machines = {
# Define machines here. # Define machines here.

View File

@@ -60,6 +60,7 @@ Configure Clan-wide settings and define machines. Here's an example `flake.nix`:
# Define your Clan # Define your Clan
clan = { clan = {
meta.name = ""; # Required and must be unique meta.name = ""; # Required and must be unique
meta.tld = ""; # Required and must be unique
machines = { machines = {
jon = { jon = {

View File

@@ -43,6 +43,7 @@ For the purpose of this guide we have two machines:
inherit self; inherit self;
meta.name = "myclan"; meta.name = "myclan";
meta.tld = "ccc";
inventory.machines = { inventory.machines = {
controller = {}; controller = {};

View File

@@ -63,6 +63,7 @@ To use `age` plugins with Clan, you need to configure them in your `flake.nix` f
inherit self; inherit self;
meta.name = "myclan"; meta.name = "myclan";
meta.tld = "ccc";
# Add YubiKey and FIDO2 HMAC plugins # Add YubiKey and FIDO2 HMAC plugins
# Note: Plugins must be available in nixpkgs. # Note: Plugins must be available in nixpkgs.

30
flake.lock generated
View File

@@ -31,11 +31,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760701190, "lastModified": 1761899396,
"narHash": "sha256-y7UhnWlER8r776JsySqsbTUh2Txf7K30smfHlqdaIQw=", "narHash": "sha256-XOpKBp6HLzzMCbzW50TEuXN35zN5WGQREC7n34DcNMM=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "3a9450b26e69dcb6f8de6e2b07b3fc1c288d85f5", "rev": "6f4cf5abbe318e4cd1e879506f6eeafd83f7b998",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -71,11 +71,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760721282, "lastModified": 1761339987,
"narHash": "sha256-aAHphQbU9t/b2RRy2Eb8oMv+I08isXv2KUGFAFn7nCo=", "narHash": "sha256-IUaawVwItZKi64IA6kF6wQCLCzpXbk2R46dHn8sHkig=",
"owner": "nix-darwin", "owner": "nix-darwin",
"repo": "nix-darwin", "repo": "nix-darwin",
"rev": "c3211fcd0c56c11ff110d346d4487b18f7365168", "rev": "7cd9aac79ee2924a85c211d21fafd394b06a38de",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -99,11 +99,11 @@
}, },
"nixos-facter-modules": { "nixos-facter-modules": {
"locked": { "locked": {
"lastModified": 1756491981, "lastModified": 1761137276,
"narHash": "sha256-lXyDAWPw/UngVtQfgQ8/nrubs2r+waGEYIba5UX62+k=", "narHash": "sha256-4lDjGnWRBLwqKQ4UWSUq6Mvxu9r8DSqCCydodW/Jsi8=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixos-facter-modules", "repo": "nixos-facter-modules",
"rev": "c1b29520945d3e148cd96618c8a0d1f850965d8c", "rev": "70bcd64225d167c7af9b475c4df7b5abba5c7de8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -146,11 +146,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760845571, "lastModified": 1760998189,
"narHash": "sha256-PwGzU3EOU65Ef1VvuNnVLie+l+P0g/fzf/PGUG82KbM=", "narHash": "sha256-ee2e1/AeGL5X8oy/HXsZQvZnae6XfEVdstGopKucYLY=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "9c9a9798be331ed3f4b2902933d7677d0659ee61", "rev": "5a7d18b5c55642df5c432aadb757140edfeb70b3",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -181,11 +181,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760945191, "lastModified": 1761311587,
"narHash": "sha256-ZRVs8UqikBa4Ki3X4KCnMBtBW0ux1DaT35tgsnB1jM4=", "narHash": "sha256-Msq86cR5SjozQGCnC6H8C+0cD4rnx91BPltZ9KK613Y=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "f56b1934f5f8fcab8deb5d38d42fd692632b47c2", "rev": "2eddae033e4e74bf581c2d1dfa101f9033dbd2dc",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -68,6 +68,7 @@
( (
{ ... }: { ... }:
{ {
debug = true;
clan = { clan = {
meta.name = "clan-core"; meta.name = "clan-core";
inventory = { inventory = {
@@ -98,6 +99,7 @@
./lib/filter-clan-core/flake-module.nix ./lib/filter-clan-core/flake-module.nix
./lib/flake-module.nix ./lib/flake-module.nix
./lib/flake-parts/clan-nixos-test.nix ./lib/flake-parts/clan-nixos-test.nix
./modules/flake-module.nix
./nixosModules/clanCore/vars/flake-module.nix ./nixosModules/clanCore/vars/flake-module.nix
./nixosModules/flake-module.nix ./nixosModules/flake-module.nix
./pkgs/clan-cli/clan_cli/tests/flake-module.nix ./pkgs/clan-cli/clan_cli/tests/flake-module.nix

View File

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

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

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

View File

@@ -33,8 +33,8 @@
let let
nixpkgs = self.inputs.nixpkgs or clan-core.inputs.nixpkgs; nixpkgs = self.inputs.nixpkgs or clan-core.inputs.nixpkgs;
nix-darwin = self.inputs.nix-darwin or clan-core.inputs.nix-darwin; nix-darwin = self.inputs.nix-darwin or clan-core.inputs.nix-darwin;
in configuration = (
lib.evalModules { lib.evalModules {
class = "clan"; class = "clan";
specialArgs = { specialArgs = {
inherit inherit
@@ -49,4 +49,7 @@ lib.evalModules {
clan-core.modules.clan.default clan-core.modules.clan.default
m m
]; ];
} }
);
in
clan-core.clanLib.checkConfig configuration.config.checks configuration

View File

@@ -137,6 +137,12 @@ in
default = { }; default = { };
type = types.submoduleWith { type = types.submoduleWith {
specialArgs = { specialArgs = {
self = throw ''
'self' is banned in the use of clan.services
Use 'exports' instead: https://docs.clan.lol/reference/options/clan_service/#exports
---
If you really need to used 'self' here, that makes the module less portable
'';
inherit (config.clanSettings) inherit (config.clanSettings)
clan-core clan-core
nixpkgs nixpkgs

View File

@@ -16,10 +16,12 @@ lib.fix (
*/ */
callLib = file: args: import file ({ inherit lib clanLib; } // args); callLib = file: args: import file ({ inherit lib clanLib; } // args);
evalService = clanLib.callLib ./modules/inventory/distributed-service/evalService.nix { }; checkConfig = clanLib.callLib ./clan/checkConfig.nix { };
evalService = clanLib.callLib ./evalService.nix { };
# ------------------------------------ # ------------------------------------
# ClanLib functions # ClanLib functions
inventory = clanLib.callLib ./modules/inventory { }; inventory = clanLib.callLib ./inventory { };
test = clanLib.callLib ./test { }; test = clanLib.callLib ./test { };
flake-inputs = clanLib.callLib ./flake-inputs.nix { }; flake-inputs = clanLib.callLib ./flake-inputs.nix { };
# Custom types # Custom types
@@ -30,6 +32,8 @@ lib.fix (
jsonschema = import ./jsonschema { inherit lib; }; jsonschema = import ./jsonschema { inherit lib; };
docs = import ./docs.nix { inherit lib; }; docs = import ./docs.nix { inherit lib; };
vars = import ./vars.nix { inherit lib; };
# flakes # flakes
flakes = clanLib.callLib ./flakes.nix { }; flakes = clanLib.callLib ./flakes.nix { };

View File

@@ -2,7 +2,7 @@
lib ? import <nixpkgs/lib>, lib ? import <nixpkgs/lib>,
}: }:
let let
clanLibOrig = (import ./.. { inherit lib; }).__unfix__; clanLibOrig = (import ./. { inherit lib; }).__unfix__;
clanLibWithFs = clanLibWithFs =
{ virtual_fs }: { virtual_fs }:
lib.fix ( lib.fix (
@@ -11,19 +11,19 @@ let
let let
clan-core = { clan-core = {
clanLib = final; clanLib = final;
modules.clan.default = lib.modules.importApply ./clan { inherit clan-core; }; modules.clan.default = lib.modules.importApply ../modules/clan { inherit clan-core; };
# Note: Can add other things to "clan-core" # Note: Can add other things to "clan-core"
# ... Not needed for this test # ... Not needed for this test
}; };
in in
{ {
clan = import ../clan { clan = import ./clan {
inherit lib clan-core; inherit lib clan-core;
}; };
# Override clanLib.fs for unit-testing against a virtual filesystem # Override clanLib.fs for unit-testing against a virtual filesystem
fs = import ../clanTest/virtual-fs.nix { inherit lib; } { fs = import ./clanTest/virtual-fs.nix { inherit lib; } {
inherit rootPath virtual_fs; inherit rootPath virtual_fs;
# Example of a passthru # Example of a passthru
# passthru = [ # passthru = [
@@ -53,7 +53,12 @@ in
}; };
}; };
}).clan }).clan
{ config.directory = rootPath; }; {
directory = rootPath;
self = {
inputs.nixpkgs.lib.version = "25.11";
};
};
in in
{ {
inherit vclan; inherit vclan;
@@ -94,7 +99,12 @@ in
}; };
}; };
}).clan }).clan
{ config.directory = rootPath; }; {
directory = rootPath;
self = {
inputs.nixpkgs.lib.version = "25.11";
};
};
in in
{ {
inherit vclan; inherit vclan;

View File

@@ -17,9 +17,9 @@ lib.evalModules {
specialArgs._ctx = prefix; specialArgs._ctx = prefix;
modules = [ modules = [
# Base module # Base module
./service-module.nix ./inventory/distributed-service/service-module.nix
# Feature modules # Feature modules
(lib.modules.importApply ./api-feature.nix { (lib.modules.importApply ./inventory/distributed-service/api-feature.nix {
inherit clanLib prefix; inherit clanLib prefix;
}) })
] ]

View File

@@ -10,12 +10,11 @@ in
rec { rec {
# TODO: automatically generate this from the directory conventions # TODO: automatically generate this from the directory conventions
imports = [ imports = [
./modules/flake-module.nix
./clanTest/flake-module.nix ./clanTest/flake-module.nix
./introspection/flake-module.nix ./introspection/flake-module.nix
./modules/inventory/flake-module.nix
./jsonschema/flake-module.nix ./jsonschema/flake-module.nix
./types/flake-module.nix ./types/flake-module.nix
./inventory/flake-module.nix
]; ];
flake.clanLib = flake.clanLib =
let let
@@ -78,9 +77,6 @@ rec {
../lib ../lib
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../.) (lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../.)
../flakeModules ../flakeModules
# ../../nixosModules/clanCore
# ../../machines
# ../../inventory.json
]; ];
}; };
in in
@@ -101,6 +97,36 @@ rec {
touch $out touch $out
''; '';
}; };
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests-build-clan
legacyPackages.evalTests-build-clan = import ./tests.nix {
inherit lib;
clan-core = self;
};
checks = {
eval-lib-build-clan = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
export HOME="$(realpath .)"
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${
self.filter {
include = [
"flakeModules"
"inventory.json"
"lib"
"machines"
"nixosModules"
"modules"
];
}
}#legacyPackages.${system}.evalTests-build-clan
touch $out
'';
};
}; };
} }

View File

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

View File

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

View File

@@ -13,16 +13,18 @@ in
let let
# Common filtered source for inventory tests # Common filtered source for inventory tests
inventoryTestsSrc = lib.fileset.toSource { inventoryTestsSrc = lib.fileset.toSource {
root = ../../../..; root = ../../..;
fileset = lib.fileset.unions [ fileset = lib.fileset.unions [
../../../../flake.nix ../../../flake.nix
../../../../flake.lock ../../../flake.lock
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../../..) (lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../..)
../../../../flakeModules ../../../flakeModules
../../../../lib ../../../lib
../../../../nixosModules/clanCore ../../../nixosModules/clanCore
../../../../machines ../../../nixosModules/machineModules
../../../../inventory.json ../../../machines
../../../inventory.json
../../../modules
]; ];
}; };
in in

View File

@@ -81,6 +81,7 @@ let
applySettings = applySettings =
instanceName: instance: instanceName: instance:
lib.mapAttrs (roleName: role: { lib.mapAttrs (roleName: role: {
settings = config.instances.${instanceName}.roles.${roleName}.finalSettings.config;
machines = lib.mapAttrs (machineName: _v: { machines = lib.mapAttrs (machineName: _v: {
settings = settings =
config.instances.${instanceName}.roles.${roleName}.machines.${machineName}.finalSettings.config; config.instances.${instanceName}.roles.${roleName}.machines.${machineName}.finalSettings.config;
@@ -158,6 +159,29 @@ in
( (
{ name, ... }@role: { name, ... }@role:
{ {
options.finalSettings = mkOption {
default = evalMachineSettings instance.name role.name null role.config.settings { };
type = types.raw;
description = ''
Final evaluated settings of the curent-machine
This contains the merged and evaluated settings of the role interface,
the role settings and the machine settings.
Type: 'configuration' as returned by 'lib.evalModules'
'';
apply = lib.warn ''
=== WANRING ===
'roles.<roleName>.settings' do not contain machine specific settings.
Prefer `machines.<machineName>.settings` instead. (i.e `perInstance: roles.<roleName>.machines.<machineName>.settings`)
If you have a use-case that requires access to the original role settings without machine overrides.
Contact us via matrix (https://matrix.to/#/#clan:clan.lol) or file an issue: https://git.clan.lol
This feature will be removed in the next release
'';
};
# instances.{instanceName}.roles.{roleName}.machines # instances.{instanceName}.roles.{roleName}.machines
options.machines = mkOption { options.machines = mkOption {
description = '' description = ''
@@ -216,7 +240,7 @@ in
options.extraModules = lib.mkOption { options.extraModules = lib.mkOption {
default = [ ]; default = [ ];
type = types.listOf (types.either types.deferredModule types.str); type = types.listOf types.deferredModule;
}; };
} }
) )
@@ -483,6 +507,9 @@ in
type = types.deferredModule; type = types.deferredModule;
default = { }; default = { };
description = '' description = ''
!!! Danger "Experimental Feature"
This feature is experimental and will change in the future.
export modules defined in 'perInstance' export modules defined in 'perInstance'
mapped to their instance name mapped to their instance name
@@ -611,6 +638,9 @@ in
type = types.deferredModule; type = types.deferredModule;
default = { }; default = { };
description = '' description = ''
!!! Danger "Experimental Feature"
This feature is experimental and will change in the future.
export modules defined in 'perMachine' export modules defined in 'perMachine'
mapped to their machine name mapped to their machine name
@@ -712,6 +742,9 @@ in
exports = mkOption { exports = mkOption {
description = '' description = ''
!!! Danger "Experimental Feature"
This feature is experimental and will change in the future.
This services exports. This services exports.
Gets merged with all other services exports. Gets merged with all other services exports.
@@ -850,7 +883,11 @@ in
instanceRes.nixosModule instanceRes.nixosModule
] ]
++ (map ( ++ (map (
s: if builtins.typeOf s == "string" then "${directory}/${s}" else s s:
if builtins.typeOf s == "string" then
lib.warn "String types for 'extraModules' will be deprecated - ${s}" "${directory}/${s}"
else
lib.setDefaultModuleLocation "via inventory.instances.${instanceName}.roles.${roleName}" s
) instanceCfg.roles.${roleName}.extraModules); ) instanceCfg.roles.${roleName}.extraModules);
}; };
} }

View File

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

View File

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

View File

@@ -58,7 +58,10 @@ let
sara = { }; sara = { };
}; };
res = callInventoryAdapter { res = callInventoryAdapter {
inherit modules machines; inherit modules;
inventory = {
inherit machines;
instances."instance_foo" = { instances."instance_foo" = {
module = { module = {
name = "A"; name = "A";
@@ -93,6 +96,7 @@ let
roles.peer.tags.all = { }; roles.peer.tags.all = { };
}; };
}; };
};
/* /*
1 { imports = [ { instanceName = "instance_foo"; machine = { name = "jon"; roles = [ "controller" "pe 1 null 1 { imports = [ { instanceName = "instance_foo"; machine = { name = "jon"; roles = [ "controller" "pe 1 null
@@ -105,9 +109,10 @@ in
{ {
# settings should evaluate # settings should evaluate
test_per_instance_arguments = { test_per_instance_arguments = {
inherit res;
expr = { expr = {
instanceName = instanceName =
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName; res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName;
# settings are specific. # settings are specific.
# Below we access: # Below we access:
@@ -115,11 +120,11 @@ in
# roles = peer # roles = peer
# machines = jon # machines = jon
settings = settings =
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings; res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings;
machine = machine =
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.machine; res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.machine;
roles = roles =
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.roles; res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.roles;
}; };
expected = { expected = {
instanceName = "instance_foo"; instanceName = "instance_foo";
@@ -137,6 +142,7 @@ in
settings = { }; settings = { };
}; };
}; };
settings = { };
}; };
peer = { peer = {
machines = { machines = {
@@ -146,6 +152,9 @@ in
}; };
}; };
}; };
settings = {
timeout = "foo-peer";
};
}; };
}; };
settings = { settings = {
@@ -156,9 +165,9 @@ in
# TODO: Cannot be tested like this anymore # TODO: Cannot be tested like this anymore
test_per_instance_settings_vendoring = { test_per_instance_settings_vendoring = {
x = res.importedModulesEvaluated.self-A; x = res.config._services.mappedServices.self-A;
expr = expr =
res.importedModulesEvaluated.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.vendoredSettings; res.config._services.mappedServices.self-A.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.vendoredSettings;
expected = { expected = {
timeout = "config.thing"; timeout = "config.thing";
}; };

View File

@@ -1,4 +1,4 @@
{ lib, callInventoryAdapter }: { lib, createTestClan }:
let let
# Authored module # Authored module
# A minimal module looks like this # A minimal module looks like this
@@ -39,8 +39,11 @@ let
jon = { }; jon = { };
sara = { }; sara = { };
}; };
res = callInventoryAdapter { res = createTestClan {
inherit modules machines; inherit modules;
inventory = {
inherit machines;
instances."instance_foo" = { instances."instance_foo" = {
module = { module = {
name = "A"; name = "A";
@@ -70,6 +73,7 @@ let
roles.peer.tags.all = { }; roles.peer.tags.all = { };
}; };
}; };
};
in in
{ {
@@ -79,7 +83,7 @@ in
inherit res; inherit res;
expr = { expr = {
hasMachineSettings = hasMachineSettings =
res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon
? settings; ? settings;
# settings are specific. # settings are specific.
@@ -88,10 +92,10 @@ in
# roles = peer # roles = peer
# machines = jon # machines = jon
specificMachineSettings = specificMachineSettings =
res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon.settings; res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon.settings;
hasRoleSettings = hasRoleSettings =
res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer
? settings; ? settings;
# settings are specific. # settings are specific.
@@ -100,21 +104,26 @@ in
# roles = peer # roles = peer
# machines = * # machines = *
specificRoleSettings = specificRoleSettings =
res.importedModulesEvaluated.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer; res.config._services.mappedServices.self-A.result.allMachines.jon.passthru.instances.instance_foo.roles.peer;
}; };
expected = rec { expected = {
hasMachineSettings = true; hasMachineSettings = true;
hasRoleSettings = false; hasRoleSettings = true;
specificMachineSettings = { specificMachineSettings = {
timeout = "foo-peer-jon"; timeout = "foo-peer-jon";
}; };
specificRoleSettings = { specificRoleSettings = {
machines = { machines = {
jon = { jon = {
settings = specificMachineSettings; settings = {
timeout = "foo-peer-jon";
}; };
}; };
}; };
settings = {
timeout = "foo-peer";
};
};
}; };
}; };
} }

View File

@@ -1,6 +1,6 @@
{ callInventoryAdapter, lib, ... }: { createTestClan, lib, ... }:
let let
res = callInventoryAdapter { res = createTestClan {
modules."A" = { modules."A" = {
_class = "clan.service"; _class = "clan.service";
manifest = { manifest = {
@@ -21,6 +21,8 @@ let
}; };
}; };
}; };
inventory = {
machines = { machines = {
jon = { }; jon = { };
sara = { }; sara = { };
@@ -41,8 +43,9 @@ let
roles.peer.machines.sara = { }; roles.peer.machines.sara = { };
}; };
}; };
};
config = res.servicesEval.config.mappedServices.self-A; config = res.config._services.mappedServices.self-A;
# #
applySettings = applySettings =

View File

@@ -1,6 +1,6 @@
{ callInventoryAdapter, lib, ... }: { createTestClan, lib, ... }:
let let
res = callInventoryAdapter { res = createTestClan {
modules."A" = m: { modules."A" = m: {
_class = "clan.service"; _class = "clan.service";
config = { config = {
@@ -14,6 +14,7 @@ let
default = m; default = m;
}; };
}; };
inventory = {
machines = { machines = {
jon = { }; jon = { };
}; };
@@ -25,8 +26,9 @@ let
roles.peer.machines.jon = { }; roles.peer.machines.jon = { };
}; };
}; };
};
specialArgs = lib.attrNames res.servicesEval.config.mappedServices.self-A.test.specialArgs; specialArgs = lib.attrNames res.config._services.mappedServices.self-A.test.specialArgs;
in in
{ {
test_simple = { test_simple = {

View File

@@ -1,12 +0,0 @@
{ clan-core }:
{
_class = "clan";
_module.args = {
inherit clan-core;
inherit (clan-core) clanLib;
};
imports = [
./module.nix
./interface.nix
];
}

View File

@@ -1,4 +0,0 @@
{ self, lib, ... }:
{
flake.modules.clan.default = lib.modules.importApply ./default.nix { clan-core = self; };
}

View File

@@ -1,58 +0,0 @@
{
self,
inputs,
...
}:
let
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
in
{
imports = [
./clan/flake-module.nix
];
perSystem =
{
pkgs,
lib,
system,
...
}:
let
jsonDocs = import ./eval-docs.nix {
inherit pkgs lib;
clan-core = self;
};
in
{
legacyPackages.clan-options = jsonDocs.optionsJSON;
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests-build-clan
legacyPackages.evalTests-build-clan = import ./tests.nix {
inherit lib;
clan-core = self;
};
checks = {
eval-lib-build-clan = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
export HOME="$(realpath .)"
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${
self.filter {
include = [
"flakeModules"
"inventory.json"
"lib"
"machines"
"nixosModules"
];
}
}#legacyPackages.${system}.evalTests-build-clan
touch $out
'';
};
};
}

View File

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

View File

@@ -1,5 +0,0 @@
{
imports = [
./interface.nix
];
}

View File

@@ -1,28 +0,0 @@
{ lib, ... }:
let
inherit (lib) types mkOption;
submodule = m: types.submoduleWith { modules = [ m ]; };
in
{
options = {
directory = mkOption {
type = types.path;
};
distributedServices = mkOption {
type = types.raw;
};
inventory = mkOption {
type = types.raw;
};
machines = mkOption {
type = types.attrsOf (submodule ({
options = {
machineImports = mkOption {
type = types.listOf types.raw;
};
};
}));
};
};
}

View File

@@ -1,20 +0,0 @@
{
config,
lib,
clanLib,
...
}:
{
options.introspection = lib.mkOption {
readOnly = true;
# TODO: use options.inventory instead of the evaluate config attribute
default =
builtins.removeAttrs (clanLib.introspection.getPrios { options = config.inventory.options; })
# tags are freeformType which is not supported yet.
# services is removed and throws an error if accessed.
[
"tags"
"services"
];
};
}

View File

@@ -1,70 +0,0 @@
{
flakeInputs,
clanLib,
}:
{ lib, config, ... }:
let
inspectModule =
inputName: moduleName: module:
let
eval = clanLib.evalService {
modules = [ module ];
prefix = [
inputName
"clan"
"modules"
moduleName
];
};
in
{
manifest = eval.config.manifest;
roles = lib.mapAttrs (_n: v: { inherit (v) description; }) eval.config.roles;
};
in
{
options.staticModules = lib.mkOption {
readOnly = true;
type = lib.types.raw;
apply = moduleSet: lib.mapAttrs (inspectModule "<clan-core>") moduleSet;
};
options.modulesPerSource = lib.mkOption {
# { sourceName :: { moduleName :: {} }}
readOnly = true;
type = lib.types.raw;
default =
let
inputsWithModules = lib.filterAttrs (_inputName: v: v ? clan.modules) flakeInputs;
in
lib.mapAttrs (
inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules
) inputsWithModules;
};
options.moduleSchemas = lib.mkOption {
# { sourceName :: { moduleName :: { roleName :: Schema }}}
readOnly = true;
type = lib.types.raw;
default = lib.mapAttrs (
_inputName: moduleSet:
lib.mapAttrs (
_moduleName: module:
(clanLib.evalService {
modules = [ module ];
prefix = [ ];
}).config.result.api.schema
) moduleSet
) config.modulesPerSource;
};
options.templatesPerSource = lib.mkOption {
# { sourceName :: { moduleName :: {} }}
readOnly = true;
type = lib.types.raw;
default =
let
inputsWithTemplates = lib.filterAttrs (_inputName: v: v ? clan.templates) flakeInputs;
in
lib.mapAttrs (_inputName: v: lib.mapAttrs (_n: t: t) v.clan.templates) inputsWithTemplates;
};
}

View File

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

View File

@@ -81,6 +81,7 @@ in
description = null; description = null;
icon = null; icon = null;
name = "test"; name = "test";
tld = "clan";
}; };
}; };
@@ -105,7 +106,7 @@ in
self = { self = {
inputs = { }; inputs = { };
}; };
directory = ../../.; directory = ../.;
meta.name = "test-clan-core"; meta.name = "test-clan-core";
}; };
in in
@@ -123,7 +124,7 @@ in
self = { self = {
inputs = { }; inputs = { };
}; };
directory = ../../.; directory = ../.;
meta.name = "test-clan-core"; meta.name = "test-clan-core";
}; };
in in
@@ -211,6 +212,87 @@ in
}; };
}; };
test_clan_check_simple_fail =
let
eval = clan {
checks.constFail = {
assertion = false;
message = "This is a constant failure";
};
};
in
{
result = eval;
expr = eval.config;
expectedError.type = "ThrownError";
expectedError.msg = "This is a constant failure";
};
test_clan_check_simple_pass =
let
eval = clan {
checks.constFail = {
assertion = true;
message = "This is a constant success";
};
};
in
{
result = eval;
expr = lib.seq eval.config 42;
expected = 42;
};
test_get_var_machine =
let
varsLib = import ./vars.nix { };
in
{
expr = varsLib.getPublicValue {
backend = "in_repo";
default = "test";
shared = false;
generator = "test-generator";
machine = "test-machine";
file = "test-file";
flake = ./vars-test-flake;
};
expected = "foo-machine";
};
test_get_var_shared =
let
varsLib = import ./vars.nix { };
in
{
expr = varsLib.getPublicValue {
backend = "in_repo";
default = "test";
shared = true;
generator = "test-generator";
machine = "test-machine";
file = "test-file";
flake = ./vars-test-flake;
};
expected = "foo-shared";
};
test_get_var_default =
let
varsLib = import ./vars.nix { };
in
{
expr = varsLib.getPublicValue {
backend = "in_repo";
default = "test-default";
shared = true;
generator = "test-generator-wrong";
machine = "test-machine";
file = "test-file";
flake = ./vars-test-flake;
};
expected = "test-default";
};
test_clan_all_machines_laziness = test_clan_all_machines_laziness =
let let
eval = clan { eval = clan {

View File

@@ -0,0 +1 @@
foo-shared

25
lib/vars.nix Normal file
View File

@@ -0,0 +1,25 @@
_: {
getPublicValue =
{
backend ? "in_repo",
default ? throw "getPublicValue: Public value ${machine}/${generator}/${file} not found!",
shared ? false,
generator,
machine,
file,
flake,
}:
if backend == "in_repo" then
let
path =
if shared then
"${flake}/vars/shared/${generator}/${file}/value"
else
"${flake}/vars/per-machine/${machine}/${generator}/${file}/value";
in
if builtins.pathExists path then builtins.readFile path else default
else
throw "backend ${backend} does not implement getPublicValue";
}

3
lib/vars_test.nix Normal file
View File

@@ -0,0 +1,3 @@
{
}

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

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

25
modules/clan/default.nix Normal file
View File

@@ -0,0 +1,25 @@
/**
Root 'clan' Module
Defines lib.clan and flake-parts.clan options
and all common logic for the 'clan' module.
- has Class _class = "clan"
- _module.args.clan-core: reference to clan-core flake
- _module.args.clanLib: reference to lib.clan function
*/
{ clan-core }:
{
_class = "clan";
_module.args = {
inherit clan-core;
inherit (clan-core) clanLib;
};
imports = [
./top-level-interface.nix
./module.nix
./distributed-services.nix
./checks.nix
];
}

View File

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

View File

@@ -1,19 +1,24 @@
{ {
pkgs, pkgs,
lib, lib,
clanModule,
clanLib,
clan-core, clan-core,
}: }:
let let
eval = lib.evalModules { eval = lib.evalModules {
modules = [ modules = [
clan-core.modules.clan.default clanModule
]; ];
specialArgs = {
self = clan-core;
};
}; };
evalDocs = pkgs.nixosOptionsDoc { evalDocs = pkgs.nixosOptionsDoc {
options = eval.options; options = eval.options;
warningsAreErrors = false; warningsAreErrors = false;
transformOptions = clan-core.clanLib.docs.stripStorePathsFromDeclarations; transformOptions = clanLib.docs.stripStorePathsFromDeclarations;
}; };
in in
{ {

View File

@@ -0,0 +1,27 @@
{ self, lib, ... }:
let
clanModule = lib.modules.importApply ./default.nix { clan-core = self; };
in
{
flake.modules.clan.default = clanModule;
perSystem =
{
pkgs,
lib,
...
}:
let
jsonDocs = import ./eval-docs.nix {
clan-core = self;
inherit
pkgs
lib
clanModule
;
clanLib = self.clanLib;
};
in
{
legacyPackages.clan-options = jsonDocs.optionsJSON;
};
}

View File

@@ -100,7 +100,7 @@ let
_: machine: _: machine:
machine.extendModules { machine.extendModules {
modules = [ modules = [
(lib.modules.importApply ../machineModules/overridePkgs.nix { (lib.modules.importApply ../../nixosModules/machineModules/overridePkgs.nix {
pkgs = pkgsFor.${system}; pkgs = pkgsFor.${system};
}) })
]; ];
@@ -167,6 +167,9 @@ in
{ ... }@args: { ... }@args:
let let
_class = _class =
# _class was added in https://github.com/NixOS/nixpkgs/pull/395141
# Clan relies on it to determine which modules to load
# people need to use at least that version of nixpkgs
args._class or (throw '' args._class or (throw ''
Your version of nixpkgs is incompatible with the latest clan. Your version of nixpkgs is incompatible with the latest clan.
Please update nixpkgs input to the latest nixos-unstable or nixpkgs-unstable. Please update nixpkgs input to the latest nixos-unstable or nixpkgs-unstable.
@@ -176,7 +179,7 @@ in
in in
{ {
imports = [ imports = [
(lib.modules.importApply ../machineModules/forName.nix { (lib.modules.importApply ../../nixosModules/machineModules/forName.nix {
inherit (config.inventory) meta; inherit (config.inventory) meta;
inherit inherit
name name
@@ -216,43 +219,44 @@ in
inherit nixosConfigurations; inherit nixosConfigurations;
inherit darwinConfigurations; inherit darwinConfigurations;
exports = config.clanInternals.inventoryClass.distributedServices.servicesEval.config.exports;
clanInternals = { clanInternals = {
inventoryClass = inventoryClass =
let let
flakeInputs = config.self.inputs; flakeInputs = config.self.inputs;
# Compute the relative directory path
selfStr = toString config.self;
dirStr = toString directory;
relativeDirectory =
if selfStr == dirStr then
""
else if lib.hasPrefix selfStr dirStr then
lib.removePrefix (selfStr + "/") dirStr
else
# This shouldn't happen in normal usage, but can occur when
# the flake is copied (e.g., in tests). Fall back to empty string.
"";
in in
{ {
_module.args = { _module.args = {
inherit clanLib; inherit clanLib;
}; };
imports = [ imports = [
../inventoryClass/builder/default.nix ../inventoryClass/default.nix
(lib.modules.importApply ../inventoryClass/service-list-from-inputs.nix {
inherit flakeInputs clanLib;
})
{ {
inherit inventory directory; inherit
inventory
directory
flakeInputs
relativeDirectory
;
exportsModule = config.exportsModule;
} }
( (
let { ... }:
clanConfig = config;
in
{ config, ... }:
{ {
staticModules = clan-core.clan.modules; staticModules = clan-core.clan.modules;
distributedServices = clanLib.inventory.mapInstances {
inherit (clanConfig) inventory exportsModule;
inherit flakeInputs directory;
clanCoreModules = clan-core.clan.modules;
prefix = [ "distributedServices" ];
};
machines = config.distributedServices.allMachines;
} }
) )
../inventoryClass/inventory-introspection.nix
]; ];
}; };

View File

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

View File

@@ -288,7 +288,7 @@ in
Global information about the clan. Global information about the clan.
''; '';
type = types.deferredModuleWith { type = types.deferredModuleWith {
staticModules = [ ../inventoryClass/meta-interface.nix ]; staticModules = [ ../inventoryClass/meta.nix ];
}; };
default = { }; default = { };
}; };

5
modules/flake-module.nix Normal file
View File

@@ -0,0 +1,5 @@
{
imports = [
./clan/flake-module.nix
];
}

View File

@@ -0,0 +1,156 @@
{
lib,
clanLib,
config,
...
}:
let
inherit (lib) types mkOption;
submodule = m: types.submoduleWith { modules = [ m ]; };
inspectModule =
inputName: moduleName: module:
let
eval = clanLib.evalService {
modules = [ module ];
prefix = [
inputName
"clan"
"modules"
moduleName
];
};
in
{
manifest = eval.config.manifest;
roles = lib.mapAttrs (_n: v: { inherit (v) description; }) eval.config.roles;
};
exposedInventory = lib.intersectAttrs {
meta = null;
machines = null;
instances = null;
tags = null;
} config.inventory;
filterAttrsRecursive' =
path: pred: set:
lib.listToAttrs (
lib.concatMap (
name:
let
v = set.${name};
loc = path ++ [ name ];
in
if pred loc v then
[
(lib.nameValuePair name (if lib.isAttrs v then filterAttrsRecursive' loc pred v else v))
]
else
[ ]
) (lib.attrNames set)
);
filteredInventory = filterAttrsRecursive' [ ] (
# Remove extraModules from serialization,
# identified by: prefix + pathLength + name
# inventory.instances.*.roles.*.extraModules
path: _value: !(lib.length path == 5 && ((lib.last path)) == "extraModules")
) exposedInventory;
in
{
options = {
flakeInputs = mkOption {
type = types.raw;
};
exportsModule = mkOption {
type = types.raw;
};
inventory = mkOption {
type = types.raw;
};
inventorySerialization = mkOption {
type = types.raw;
readOnly = true;
default = filteredInventory;
};
directory = mkOption {
type = types.path;
};
relativeDirectory = mkOption {
type = types.str;
internal = true;
description = ''
The relative directory path from the flake root to the clan directory.
Empty string if directory equals the flake root.
'';
};
machines = mkOption {
type = types.attrsOf (submodule ({
options = {
machineImports = mkOption {
type = types.listOf types.raw;
};
};
}));
};
introspection = lib.mkOption {
readOnly = true;
# TODO: use options.inventory instead of the evaluate config attribute
default =
builtins.removeAttrs (clanLib.introspection.getPrios { options = config.inventory.options; })
# tags are freeformType which is not supported yet.
# services is removed and throws an error if accessed.
[
"tags"
"services"
];
};
staticModules = lib.mkOption {
readOnly = true;
type = lib.types.raw;
apply = moduleSet: lib.mapAttrs (inspectModule "<clan-core>") moduleSet;
};
modulesPerSource = lib.mkOption {
# { sourceName :: { moduleName :: {} }}
readOnly = true;
type = lib.types.raw;
default =
let
inputsWithModules = lib.filterAttrs (_inputName: v: v ? clan.modules) config.flakeInputs;
in
lib.mapAttrs (
inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules
) inputsWithModules;
};
moduleSchemas = lib.mkOption {
# { sourceName :: { moduleName :: { roleName :: Schema }}}
readOnly = true;
type = lib.types.raw;
default = lib.mapAttrs (
_inputName: moduleSet:
lib.mapAttrs (
_moduleName: module:
(clanLib.evalService {
modules = [ module ];
prefix = [ ];
}).config.result.api.schema
) moduleSet
) config.modulesPerSource;
};
templatesPerSource = lib.mkOption {
# { sourceName :: { moduleName :: {} }}
readOnly = true;
type = lib.types.raw;
default =
let
inputsWithTemplates = lib.filterAttrs (_inputName: v: v ? clan.templates) config.flakeInputs;
in
lib.mapAttrs (_inputName: v: lib.mapAttrs (_n: t: t) v.clan.templates) inputsWithTemplates;
};
};
}

View File

@@ -115,7 +115,7 @@ in
meta = lib.mkOption { meta = lib.mkOption {
type = lib.types.submoduleWith { type = lib.types.submoduleWith {
modules = [ modules = [
./meta-interface.nix ./meta.nix
]; ];
}; };
}; };
@@ -167,7 +167,7 @@ in
''; '';
type = types.submoduleWith { type = types.submoduleWith {
specialArgs = { specialArgs = {
inherit (config) machines; inherit (config) machines clanLib;
}; };
modules = [ modules = [
{ {
@@ -359,7 +359,7 @@ in
inherit clanLib; inherit clanLib;
}; };
} }
(import ./roles-interface.nix { }) (import ./role.nix { })
]; ];
} }
); );

View File

@@ -31,6 +31,20 @@ let
Under construction, will be used for the UI Under construction, will be used for the UI
''; '';
}; };
tld = lib.mkOption {
type = types.strMatching "[a-z]+";
default = "clan";
example = "ccc";
description = ''
Top level domain (TLD) of the clan. It should be set to a valid, but
not already existing TLD.
It will be used to provide clan-internal services and resolve each host of the
clan with:
<hostname>.<tld>
'';
};
}; };
in in
{ {

View File

@@ -44,12 +44,6 @@ in
description = '' description = ''
List of additionally imported `.nix` expressions. List of additionally imported `.nix` expressions.
Supported types:
- **Strings**: Interpreted relative to the 'directory' passed to `lib.clan`.
- **Paths**: should be relative to the current file.
- **Any**: Nix expression must be serializable to JSON.
!!! Note !!! Note
**The import only happens if the machine is part of the service or role.** **The import only happens if the machine is part of the service or role.**
@@ -73,15 +67,8 @@ in
} }
``` ```
''; '';
apply = value: if lib.isString value then value else builtins.seq (builtins.toJSON value) value;
default = [ ]; default = [ ];
type = types.listOf ( type = types.listOf types.raw;
types.oneOf [
types.str
types.path
(types.attrsOf types.anything)
]
);
}; };
}; };
} }

View File

@@ -106,6 +106,14 @@ in
# Set by the flake, so it's read-only in the machine # Set by the flake, so it's read-only in the machine
readOnly = true; readOnly = true;
}; };
tld = lib.mkOption {
type = types.strMatching "[a-z]+";
description = ''
the TLD for the clan
'';
# Set by the flake, so it's read-only in the machine
readOnly = true;
};
machine = mkOption { machine = mkOption {
description = '' description = ''
Settings of the machine. Settings of the machine.

View File

@@ -3,6 +3,7 @@
directory, directory,
meta, meta,
}: }:
# The following is a nixos/darwin module
{ {
_class, _class,
lib, lib,
@@ -20,7 +21,7 @@
); );
clan.core.settings = { clan.core.settings = {
inherit (meta) name icon; inherit (meta) name icon tld;
inherit directory; inherit directory;
machine = { machine = {
inherit name; inherit name;

View File

@@ -1,9 +1,11 @@
{ lib, ... }:
{ {
perSystem = perSystem =
{ {
self', self',
pkgs, pkgs,
config, config,
system,
... ...
}: }:
{ {
@@ -20,7 +22,6 @@
clan-ts-api = config.packages.clan-ts-api; clan-ts-api = config.packages.clan-ts-api;
fonts = config.packages.fonts; fonts = config.packages.fonts;
}; };
}; };
# // # //
# todo add darwin support # todo add darwin support
@@ -41,6 +42,11 @@
inherit (config.packages) clan-ts-api; inherit (config.packages) clan-ts-api;
}; };
checks = config.packages.clan-app.tests; checks =
config.packages.clan-app.tests
# Sandboxed Darwin nix build can't spawn a headless brwoser
// lib.optionalAttrs (!lib.hasSuffix "darwin" system) {
inherit (config.packages.clan-app-ui.tests) clan-app-ui-storybook;
};
}; };
} }

View File

@@ -12,11 +12,13 @@
fetchzip, fetchzip,
process-compose, process-compose,
json2ts, json2ts,
playwright-driver, playwright,
luakit, luakit,
jq,
self', self',
}: }:
let let
RED = "\\033[1;31m";
GREEN = "\\033[1;32m"; GREEN = "\\033[1;32m";
NC = "\\033[0m"; NC = "\\033[0m";
@@ -108,32 +110,39 @@ mkShell {
export PC_CONFIG_FILES="$CLAN_CORE_PATH/pkgs/clan-app/process-compose.yaml" export PC_CONFIG_FILES="$CLAN_CORE_PATH/pkgs/clan-app/process-compose.yaml"
echo -e "${GREEN}To launch a qemu VM for testing, run:\n start-vm <number of VMs>${NC}" echo -e "${GREEN}To launch a qemu VM for testing, run:\n start-vm <number of VMs>${NC}"
''
+
# todo darwin support needs some work
(lib.optionalString stdenv.hostPlatform.isLinux ''
# configure playwright for storybook snapshot testing # configure playwright for storybook snapshot testing
# we only want webkit as that matches what the app is rendered with # we only want webkit as that matches what the app is rendered with
playwright_ver=$(${jq}/bin/jq --raw-output .devDependencies.playwright ${./ui/package.json})
if [[ $playwright_ver != '${playwright.version}' ]]; then
echo >&2 -en '${RED}'
echo >&2 "Error: playwright npm package version ($playwright_ver) is different from that from the nixpkgs (${playwright.version})"
echo >&2 "Run this command to update the npm package version"
echo >&2
echo >&2 " npm i -D --save-exact playwright@${playwright.version}"
echo >&2
echo >&2 -en '${NC}'
else
export PLAYWRIGHT_BROWSERS_PATH=${ export PLAYWRIGHT_BROWSERS_PATH=${
playwright-driver.browsers.override { playwright.browsers.override {
withFfmpeg = false; withFfmpeg = false;
withFirefox = false; withFirefox = true;
withWebkit = true; withWebkit = false;
withChromium = false; withChromium = false;
withChromiumHeadlessShell = false; withChromiumHeadlessShell = false;
} }
} }
# This is needed to disable revisionOverrides in browsers.json which
# the playwright nix package does not support
# https://github.com/NixOS/nixpkgs/blob/f9c3b27aa3f9caac6717973abcc549dbde16bdd4/pkgs/development/web/playwright/driver.nix#L261
export PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=nixos
# stop playwright from trying to validate it has downloaded the necessary browsers # stop playwright from trying to validate it has downloaded the necessary browsers
# we are providing them manually via nix # we are providing them manually via nix
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=1
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true fi
'';
# playwright browser drivers are versioned e.g. webkit-2191
# this helps us avoid having to update the playwright js dependency everytime we update nixpkgs and vice versa
# see vitest.config.js for corresponding launch configuration
export PLAYWRIGHT_WEBKIT_EXECUTABLE=$(find -L "$PLAYWRIGHT_BROWSERS_PATH" -type f -name "pw_run.sh")
'');
} }

View File

@@ -4,8 +4,11 @@
importNpmLock, importNpmLock,
clan-ts-api, clan-ts-api,
fonts, fonts,
ps,
jq,
playwright,
}: }:
buildNpmPackage (_finalAttrs: { buildNpmPackage (finalAttrs: {
pname = "clan-app-ui"; pname = "clan-app-ui";
version = "0.0.1"; version = "0.0.1";
nodejs = nodejs_22; nodejs = nodejs_22;
@@ -32,36 +35,53 @@ buildNpmPackage (_finalAttrs: {
# todo figure out why this fails only inside of Nix # todo figure out why this fails only inside of Nix
# Something about passing orientation in any of the Form stories is causing the browser to crash # Something about passing orientation in any of the Form stories is causing the browser to crash
# `npm run test-storybook-static` works fine in the devshell # `npm run test-storybook-static` works fine in the devshell
#
# passthru = rec { passthru = {
# storybook = buildNpmPackage { tests = {
# pname = "${finalAttrs.pname}-storybook"; "${finalAttrs.pname}-storybook" = buildNpmPackage {
# inherit (finalAttrs) pname = "${finalAttrs.pname}-storybook";
# version inherit (finalAttrs)
# nodejs version
# src nodejs
# npmDeps src
# npmConfigHook npmDeps
# preBuild npmConfigHook
# ; ;
#
# nativeBuildInputs = finalAttrs.nativeBuildInputs ++ [ nativeBuildInputs = finalAttrs.nativeBuildInputs ++ [
# ps ps
# ]; jq
# ];
# npmBuildScript = "test-storybook-static";
# npmBuildScript = "test-storybook";
# env = finalAttrs.env // {
# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = 1; env = {
# PLAYWRIGHT_BROWSERS_PATH = "${playwright-driver.browsers.override { PLAYWRIGHT_BROWSERS_PATH = "${playwright.browsers.override {
# withChromiumHeadlessShell = true; withFfmpeg = false;
# }}"; withFirefox = true;
# PLAYWRIGHT_HOST_PLATFORM_OVERRIDE = "ubuntu-24.04"; withWebkit = false;
# }; withChromium = false;
# withChromiumHeadlessShell = false;
# postBuild = '' }}";
# mv storybook-static $out PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = true;
# ''; # This is needed to disable revisionOverrides in browsers.json which
# }; # the playwright nix package does not support:
# }; # https://github.com/NixOS/nixpkgs/blob/f9c3b27aa3f9caac6717973abcc549dbde16bdd4/pkgs/development/web/playwright/driver.nix#L261
PLAYWRIGHT_HOST_PLATFORM_OVERRIDE = "nixos";
DEBUG = "vitest:*";
};
preBuild = finalAttrs.preBuild + ''
playwright_ver=$(jq --raw-output .devDependencies.playwright ${./ui/package.json})
if [[ $playwright_ver != '${playwright.version}' ]]; then
echo >&2 "playwright npm package version ($playwright_ver) is different from that from the nixpkgs (${playwright.version})"
echo >&2 "Run this command to update the npm package version"
echo >&2
echo >&2 " npm i -D --save-exact playwright@${playwright.version}"
echo >&2
exit 1
fi
'';
};
};
};
}) })

View File

@@ -2,5 +2,4 @@ app/api
app/.fonts app/.fonts
.vite .vite
storybook-static
*.css.d.ts *.css.d.ts

View File

@@ -1,31 +1,20 @@
import { mergeConfig } from "vite"; import type { StorybookConfig } from "storybook-solidjs-vite";
import type { StorybookConfig } from "@kachurun/storybook-solid-vite";
const config: StorybookConfig = { export default {
framework: "@kachurun/storybook-solid-vite", framework: "storybook-solidjs-vite",
stories: ["../src/**/*.mdx", "../src/**/*.stories.tsx"], stories: ["../src/**/*.mdx", "../src/**/*.stories.tsx"],
addons: [ addons: [
"@storybook/addon-links", "@storybook/addon-links",
"@storybook/addon-docs", "@storybook/addon-docs",
"@storybook/addon-a11y", "@storybook/addon-a11y",
], {
async viteFinal(config) { name: "@storybook/addon-vitest",
return mergeConfig(config, { options: {
define: { "process.env": {} }, cli: false,
});
}, },
},
],
core: { core: {
disableTelemetry: true, disableTelemetry: true,
}, },
typescript: { } satisfies StorybookConfig;
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
// 👇 Default prop filter, which excludes props from node_modules
propFilter: (prop: any) =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
},
},
};
export default config;

View File

@@ -1,4 +1,4 @@
import type { Preview } from "@kachurun/storybook-solid-vite"; import type { Preview } from "storybook-solidjs-vite";
import "../src/index.css"; import "../src/index.css";
import "./preview.css"; import "./preview.css";

View File

@@ -1,4 +1,4 @@
import { setProjectAnnotations } from "@kachurun/storybook-solid-vite"; import { setProjectAnnotations } from "storybook-solidjs-vite";
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
import * as projectAnnotations from "./preview"; import * as projectAnnotations from "./preview";

View File

@@ -1,19 +1,9 @@
{ {
"ignore": [ "ignore": [
"gtk.webview.js", "gtk.webview.js",
"src/api/clan/client-fetch.ts",
"stylelint.config.js", "stylelint.config.js",
"util.ts",
"src/components/v2/**",
"api/**", "api/**",
"tailwind/**" "tailwind/**"
],
"ignoreDependencies": [
"@babel/plugin-syntax-import-attributes",
"@storybook/addon-viewport",
"@typescript-eslint/parser",
"@vitest/coverage-v8",
"http-server",
"playwright",
"wait-on"
] ]
} }

File diff suppressed because it is too large Load Diff

View File

@@ -12,50 +12,35 @@
"check": "tsc --noEmit --skipLibCheck && eslint ./src --fix", "check": "tsc --noEmit --skipLibCheck && eslint ./src --fix",
"test": "vitest run --project unit --typecheck", "test": "vitest run --project unit --typecheck",
"vite": "vite", "vite": "vite",
"storybook": "storybook",
"knip": "knip --fix", "knip": "knip --fix",
"storybook-build": "storybook build", "storybook": "storybook dev -p 6006",
"storybook-dev": "storybook dev -p 6006",
"test-storybook": "vitest run --project storybook", "test-storybook": "vitest run --project storybook",
"test-storybook-update-snapshots": "vitest run --project storybook --update", "test-storybook-update-snapshots": "vitest run --project storybook --update"
"test-storybook-static": "npm run storybook-build && concurrently -k -s first -n 'SB,TEST' -c 'magenta,blue' 'http-server storybook-static --port 6006 --silent' 'wait-on tcp:127.0.0.1:6006 && npm run test-storybook'"
}, },
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@babel/plugin-syntax-import-attributes": "^7.27.1",
"@eslint/js": "^9.3.0", "@eslint/js": "^9.3.0",
"@kachurun/storybook-solid-vite": "^9.0.11",
"@linaria/core": "^6.3.0", "@linaria/core": "^6.3.0",
"@sinonjs/fake-timers": "^14.0.0", "@storybook/addon-a11y": "^9.1.13",
"@storybook/addon-a11y": "^9.0.8", "@storybook/addon-docs": "^9.1.13",
"@storybook/addon-docs": "^9.0.8", "@storybook/addon-links": "^9.1.13",
"@storybook/addon-links": "^9.0.8", "@storybook/addon-vitest": "^9.1.13",
"@storybook/addon-viewport": "^9.0.8",
"@storybook/addon-vitest": "^9.0.8",
"@types/node": "^22.15.19", "@types/node": "^22.15.19",
"@types/sinonjs__fake-timers": "^8.1.5",
"@types/three": "^0.176.0", "@types/three": "^0.176.0",
"@typescript-eslint/parser": "^8.32.1",
"@vitest/browser": "^3.2.3", "@vitest/browser": "^3.2.3",
"@vitest/coverage-v8": "^3.2.3",
"@wyw-in-js/vite": "^0.7.0", "@wyw-in-js/vite": "^0.7.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"concurrently": "^9.1.2",
"eslint": "^9.27.0", "eslint": "^9.27.0",
"eslint-plugin-tailwindcss": "^3.17.0", "eslint-plugin-tailwindcss": "^3.17.0",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"extend": "^3.0.2",
"http-server": "^14.1.1",
"jsdom": "^26.1.0",
"knip": "^5.61.2", "knip": "^5.61.2",
"markdown-to-jsx": "^7.7.10", "playwright": "1.54.1",
"playwright": "~1.55.1",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"postcss-url": "^10.1.3", "postcss-url": "^10.1.3",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"storybook": "^9.0.8", "storybook": "^9.1.13",
"swagger-ui-dist": "^5.26.2", "storybook-solidjs-vite": "^9.0.3",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"typescript-eslint": "^8.32.1", "typescript-eslint": "^8.32.1",
@@ -64,11 +49,9 @@
"vite-css-modules": "^1.10.0", "vite-css-modules": "^1.10.0",
"vite-plugin-solid": "^2.8.2", "vite-plugin-solid": "^2.8.2",
"vite-plugin-solid-svg": "^0.8.1", "vite-plugin-solid-svg": "^0.8.1",
"vitest": "^3.2.3", "vitest": "^3.2.4"
"wait-on": "^8.0.3"
}, },
"dependencies": { "dependencies": {
"@floating-ui/dom": "^1.6.8",
"@kobalte/core": "^0.13.10", "@kobalte/core": "^0.13.10",
"@kobalte/tailwindcss": "^0.9.0", "@kobalte/tailwindcss": "^0.9.0",
"@modular-forms/solid": "^0.25.1", "@modular-forms/solid": "^0.25.1",
@@ -82,22 +65,6 @@
"solid-js": "^1.9.7", "solid-js": "^1.9.7",
"solid-toast": "^0.5.0", "solid-toast": "^0.5.0",
"three": "^0.176.0", "three": "^0.176.0",
"troika-three-text": "^0.52.4",
"valibot": "^1.1.0" "valibot": "^1.1.0"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.25.4",
"@esbuild/darwin-x64": "^0.25.4",
"@esbuild/linux-arm64": "^0.25.4",
"@esbuild/linux-x64": "^0.25.4"
},
"overrides": {
"vite": {
"rollup": "npm:@rollup/wasm-node@^4.34.9"
},
"@rollup/rollup-darwin-x64": "npm:@rollup/wasm-node@^4.34.9",
"@rollup/rollup-linux-x64": "npm:@rollup/wasm-node@^4.34.9",
"@rollup/rollup-darwin-arm64": "npm:@rollup/wasm-node@^4.34.9",
"@rollup/rollup-linux-arm64": "npm:@rollup/wasm-node@^4.34.9"
} }
} }

View File

@@ -1,7 +1,6 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid"; import type { Meta, StoryObj } from "storybook-solidjs-vite";
import { Alert, AlertProps } from "@/src/components/Alert/Alert"; import { Alert, AlertProps } from "@/src/components/Alert/Alert";
import { expect, fn } from "storybook/test"; import { expect, fn } from "storybook/test";
import { StoryContext } from "@kachurun/storybook-solid-vite";
const AlertExamples = (props: AlertProps) => ( const AlertExamples = (props: AlertProps) => (
<div class="grid w-fit grid-cols-2 gap-8"> <div class="grid w-fit grid-cols-2 gap-8">
@@ -20,14 +19,14 @@ const AlertExamples = (props: AlertProps) => (
</div> </div>
); );
const meta: Meta<AlertProps> = { const meta: Meta<typeof AlertExamples> = {
title: "Components/Alert", title: "Components/Alert",
component: AlertExamples, component: AlertExamples,
}; };
export default meta; export default meta;
type Story = StoryObj<AlertProps>; type Story = StoryObj<typeof meta>;
export const Info: Story = { export const Info: Story = {
args: { args: {
@@ -92,11 +91,14 @@ export const InfoDismiss: Story = {
args: { args: {
...Info.args, ...Info.args,
onDismiss: fn(), onDismiss: fn(),
play: async ({ canvas, step, userEvent, args }: StoryContext) => { },
render(args) {
return <Alert {...args} />;
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(canvas.getByRole("button")); await userEvent.click(canvas.getByRole("button"));
await expect(args.onDismiss).toHaveBeenCalled(); await expect(args.onDismiss).toHaveBeenCalled();
}, },
},
}; };
export const ErrorDismiss: Story = { export const ErrorDismiss: Story = {

View File

@@ -1,8 +1,7 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid"; import type { Meta, StoryObj } from "storybook-solidjs-vite";
import { Button, ButtonProps } from "./Button"; import { Button, ButtonProps } from "./Button";
import { Component } from "solid-js"; import { Component } from "solid-js";
import { expect, fn, waitFor, within } from "storybook/test"; import { expect, fn, within } from "storybook/test";
import { StoryContext } from "@kachurun/storybook-solid-vite";
const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor; const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor;
@@ -202,7 +201,7 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
</> </>
); );
const meta: Meta<ButtonProps> = { const meta: Meta<typeof ButtonExamples> = {
title: "Components/Button", title: "Components/Button",
component: ButtonExamples, component: ButtonExamples,
}; };
@@ -211,15 +210,13 @@ export default meta;
type Story = StoryObj<ButtonProps>; type Story = StoryObj<ButtonProps>;
const timeout = process.env.NODE_ENV === "test" ? 500 : 2000;
export const Primary: Story = { export const Primary: Story = {
args: { args: {
hierarchy: "primary", hierarchy: "primary",
onClick: fn(), onClick: fn(),
}, },
play: async ({ canvasElement, step, userEvent, args }: StoryContext) => { play: async ({ canvasElement, step, userEvent, args }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
const buttons = await canvas.findAllByRole("button"); const buttons = await canvas.findAllByRole("button");
@@ -264,7 +261,7 @@ export const GhostPrimary: Story = {
}, },
play: Primary.play, play: Primary.play,
decorators: [ decorators: [
(Story: StoryObj) => ( (Story) => (
<div class="p-10 bg-def-3"> <div class="p-10 bg-def-3">
<Story /> <Story />
</div> </div>

View File

@@ -8,11 +8,11 @@ import Icon, { IconVariant } from "@/src/components/Icon/Icon";
import { Loader } from "@/src/components/Loader/Loader"; import { Loader } from "@/src/components/Loader/Loader";
import { getInClasses, joinByDash, keepTruthy } from "@/src/util"; import { getInClasses, joinByDash, keepTruthy } from "@/src/util";
export type Size = "default" | "s" | "xs"; type Size = "default" | "s" | "xs";
export type Hierarchy = "primary" | "secondary"; type Hierarchy = "primary" | "secondary";
export type Elasticity = "default" | "fit"; type Elasticity = "default" | "fit";
export type Action = () => Promise<void>; type Action = () => Promise<void>;
export interface ButtonProps export interface ButtonProps
extends JSX.ButtonHTMLAttributes<HTMLButtonElement> { extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {

View File

@@ -1,7 +1,7 @@
import { CubeConstruction } from "./CubeConstruction"; import { CubeConstruction } from "./CubeConstruction";
import { Meta, StoryObj } from "@kachurun/storybook-solid"; import { Meta, StoryObj } from "storybook-solidjs-vite";
const meta: Meta = { const meta: Meta<typeof CubeConstruction> = {
title: "Components/CubeConstruction", title: "Components/CubeConstruction",
component: CubeConstruction, component: CubeConstruction,
globals: { globals: {
@@ -12,7 +12,7 @@ const meta: Meta = {
export default meta; export default meta;
type Story = StoryObj; type Story = StoryObj<typeof meta>;
export const Default: Story = { export const Default: Story = {
args: {}, args: {},

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