Compare commits

...

86 Commits

Author SHA1 Message Date
Johannes Kirschbauer
9e0efcef8b vars: move logic from vars-to-sops into single file 2025-10-13 16:27:31 +02:00
Johannes Kirschbauer
1c3282bb63 vars: simplify collectFiles 2025-10-13 10:05:53 +02:00
Johannes Kirschbauer
3c4b3e180e facts: add bigger migration warnings 2025-10-13 10:05:53 +02:00
Johannes Kirschbauer
7b95fa039f clan-cli: remove unused test fixture 2025-10-12 18:00:52 +02:00
hsjobeki
38712d6fe0 Merge pull request 'clan-core/nixos: remove autoloading magic in favour of simple code' (#5476) from fix-a into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5476
2025-10-12 14:39:17 +00:00
Johannes Kirschbauer
1d38ffa9c2 inventory: unit test autoloading with a virtual fs 2025-10-12 16:32:55 +02:00
clan-bot
665f036dec Merge pull request 'Update clan-core-for-checks in devFlake' (#5478) from update-devFlake-clan-core-for-checks into main 2025-10-12 00:12:04 +00:00
clan-bot
b74b6ff449 Update clan-core-for-checks in devFlake 2025-10-12 00:01:53 +00:00
clan-bot
9c8797e770 Merge pull request 'Update clan-core-for-checks in devFlake' (#5477) from update-devFlake-clan-core-for-checks into main 2025-10-11 20:12:29 +00:00
clan-bot
2be6cedec4 Update clan-core-for-checks in devFlake 2025-10-11 20:01:49 +00:00
Johannes Kirschbauer
7f49449f94 clan-core/nixos: remove autoloading magic in favour of simple code 2025-10-11 18:02:32 +02:00
hsjobeki
1f7bfa4e34 Merge pull request 'inventory: wrap autoloaded machines with correct file' (#5474) from fix-a into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5474
2025-10-11 16:00:37 +00:00
clan-bot
67fab4b11d Merge pull request 'Update clan-core-for-checks in devFlake' (#5475) from update-devFlake-clan-core-for-checks into main 2025-10-11 15:11:33 +00:00
clan-bot
18e3c72ef0 Update clan-core-for-checks in devFlake 2025-10-11 15:01:51 +00:00
Johannes Kirschbauer
84d4660a8d inventory: wrap autoloaded machines with correct file 2025-10-11 15:57:42 +02:00
clan-bot
13c3e1411a Merge pull request 'Update nixpkgs-dev in devFlake' (#5472) from update-devFlake-nixpkgs-dev into main 2025-10-11 10:14:29 +00:00
clan-bot
3c3a505aca Merge pull request 'Update clan-core-for-checks in devFlake' (#5471) from update-devFlake-clan-core-for-checks into main 2025-10-11 10:13:33 +00:00
clan-bot
f33c8e98fe Update nixpkgs-dev in devFlake 2025-10-11 10:02:05 +00:00
clan-bot
869a04e5af Update clan-core-for-checks in devFlake 2025-10-11 10:01:50 +00:00
clan-bot
d09fdc3528 Merge pull request 'Update clan-core-for-checks in devFlake' (#5470) from update-devFlake-clan-core-for-checks into main 2025-10-11 05:09:16 +00:00
clan-bot
652677d06f Update clan-core-for-checks in devFlake 2025-10-11 05:01:53 +00:00
clan-bot
ec163657cd Merge pull request 'Update clan-core-for-checks in devFlake' (#5469) from update-devFlake-clan-core-for-checks into main 2025-10-11 00:09:33 +00:00
clan-bot
7d3aa5936d Update clan-core-for-checks in devFlake 2025-10-11 00:01:51 +00:00
clan-bot
f8f8efbb88 Merge pull request 'Update treefmt-nix' (#5466) from update-treefmt-nix into main 2025-10-10 20:12:14 +00:00
clan-bot
8887e209d6 Merge pull request 'Update clan-core-for-checks in devFlake' (#5467) from update-devFlake-clan-core-for-checks into main 2025-10-10 20:10:50 +00:00
clan-bot
a72f74a36e Merge pull request 'Update treefmt-nix in devFlake' (#5468) from update-devFlake-treefmt-nix into main 2025-10-10 20:10:42 +00:00
clan-bot
0e0f8e73ec Update treefmt-nix in devFlake 2025-10-10 20:02:13 +00:00
clan-bot
f15a113f52 Update clan-core-for-checks in devFlake 2025-10-10 20:01:50 +00:00
clan-bot
1fbb4f5014 Update treefmt-nix 2025-10-10 20:01:49 +00:00
Michael Hoang
980a3c90b5 Merge pull request 'cli: ensure init-hardware-config passes Nix options to nixos-anywhere' (#5465) from push-mwotvwkqsluy into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5465
2025-10-10 15:40:34 +00:00
clan-bot
c01b14aef5 Merge pull request 'Update clan-core-for-checks in devFlake' (#5464) from update-devFlake-clan-core-for-checks into main 2025-10-10 15:10:05 +00:00
clan-bot
0a3e564ec0 Update clan-core-for-checks in devFlake 2025-10-10 15:01:52 +00:00
Michael Hoang
bc09d5c886 cli: ensure init-hardware-config passes Nix options to nixos-anywhere 2025-10-10 17:00:10 +02:00
Michael Hoang
f6b8d660d8 Merge pull request 'checks: fix SSH debugging over vsock not working' (#5463) from push-yplypuoxymkt into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5463
2025-10-10 14:40:10 +00:00
Michael Hoang
6014ddcd9a checks: fix SSH debugging over vsock not working 2025-10-10 16:32:54 +02:00
hsjobeki
551f5144c7 Merge pull request 'docs: Remove surprising statement on the front of documentation' (#5460) from kenji/ke-docs-fix into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5460
2025-10-10 12:24:49 +00:00
a-kenji
9a664c323c docs: Remove surprising statement on the front of documentation 2025-10-10 13:35:29 +02:00
clan-bot
7572dc8c2b Merge pull request 'Update clan-core-for-checks in devFlake' (#5454) from update-devFlake-clan-core-for-checks into main 2025-10-10 10:09:30 +00:00
clan-bot
e22f0d9e36 Merge pull request 'Update nixpkgs-dev in devFlake' (#5455) from update-devFlake-nixpkgs-dev into main 2025-10-10 10:07:47 +00:00
clan-bot
f93ae13448 Update nixpkgs-dev in devFlake 2025-10-10 10:02:12 +00:00
clan-bot
749bac63f4 Update clan-core-for-checks in devFlake 2025-10-10 10:01:53 +00:00
clan-bot
2bac2ec7ee Merge pull request 'Update clan-core-for-checks in devFlake' (#5452) from update-devFlake-clan-core-for-checks into main 2025-10-10 05:09:28 +00:00
clan-bot
f224d4b20c Update clan-core-for-checks in devFlake 2025-10-10 05:01:54 +00:00
clan-bot
47aa0a3b8e Merge pull request 'Update clan-core-for-checks in devFlake' (#5451) from update-devFlake-clan-core-for-checks into main 2025-10-10 00:11:09 +00:00
clan-bot
dd1cab5daa Update clan-core-for-checks in devFlake 2025-10-10 00:01:51 +00:00
clan-bot
32edae4ebd Merge pull request 'Update clan-core-for-checks in devFlake' (#5450) from update-devFlake-clan-core-for-checks into main 2025-10-09 20:09:43 +00:00
clan-bot
d829aa5838 Update clan-core-for-checks in devFlake 2025-10-09 20:01:50 +00:00
clan-bot
fd6619668b Merge pull request 'Update clan-core-for-checks in devFlake' (#5449) from update-devFlake-clan-core-for-checks into main 2025-10-09 15:09:37 +00:00
clan-bot
50a26ece32 Update clan-core-for-checks in devFlake 2025-10-09 15:01:53 +00:00
brianmcgee
8f224b00a6 Merge pull request 'various-ui-fixes' (#5448) from various-ui-fixes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5448
2025-10-09 14:22:06 +00:00
Brian McGee
27d43ee21d fix(storybook): disable Sidebar story until we have a better mock data approach 2025-10-09 14:57:22 +01:00
Brian McGee
9626e22db7 fix(storybook): adjust flash installer on mount
It needs to handle possible missing state in the store on mount.
2025-10-09 14:57:22 +01:00
Brian McGee
1df329fe0d fix(storybook): disable service workflow stories
Temporary until we can decide how best to mock state.
2025-10-09 14:57:21 +01:00
Brian McGee
9da38abc77 fix(storybook): clan settings mock data shape changed 2025-10-09 14:57:20 +01:00
Brian McGee
2814c46e68 fix(storybook): button stories
- role="button" was removed at some point during refactoring which broke how the story was finding buttons
- button no longer has automatic loading state, instead it is now controlled.
2025-10-09 14:56:39 +01:00
Brian McGee
feef0a513e fix(storybook): remove cubes storybook
It wasn't adding much value and requires a mock Clan context which is a lot of effort at the min.
2025-10-09 14:56:38 +01:00
Brian McGee
9cc85b36c6 feat(ui): switch to webkit for storybook tests 2025-10-09 14:56:38 +01:00
hsjobeki
1465b18820 Merge pull request 'app: fix ClanSettings story' (#5447) from ui-cleanup into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5447
2025-10-09 13:27:56 +00:00
Johannes Kirschbauer
6fa0062573 app: fix ClanSettings story 2025-10-09 15:24:30 +02:00
clan-bot
6cd68c23f5 Merge pull request 'Update clan-core-for-checks in devFlake' (#5444) from update-devFlake-clan-core-for-checks into main 2025-10-09 10:09:50 +00:00
clan-bot
fdddc60676 Merge pull request 'Update nixpkgs-dev in devFlake' (#5445) from update-devFlake-nixpkgs-dev into main 2025-10-09 10:08:18 +00:00
clan-bot
684aa27068 Update nixpkgs-dev in devFlake 2025-10-09 10:02:12 +00:00
clan-bot
35d8deb393 Update clan-core-for-checks in devFlake 2025-10-09 10:01:53 +00:00
DavHau
e2f20b5ffc Merge pull request 'vars: refactor - make shared generators carry machines list' (#5443) from dave into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5443
2025-10-09 09:03:09 +00:00
DavHau
fd5d7934a0 vars: refactor - make shared generators carry machines list
This should make it simpler to improve the implementation of granting a new machine access to a shared secret.
The current approach using the health_check is  pretty hacky
2025-10-09 15:41:04 +07:00
Kenji Berthold
f194c31e0e Merge pull request 'Fix typo in "Authoring a 'clan.service' module"' (#5439) from nickdichev/clan-core:nickdichev-patch-1 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5439
2025-10-09 08:32:40 +00:00
DavHau
061b598adf Merge pull request 'vars: cleanup + fix wording' (#5442) from dave into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5442
2025-10-09 05:44:14 +00:00
DavHau
744f35e0cc vars: cleanup + fix wording 2025-10-09 07:38:00 +02:00
clan-bot
4a6d46198c Merge pull request 'Update clan-core-for-checks in devFlake' (#5441) from update-devFlake-clan-core-for-checks into main 2025-10-09 05:11:10 +00:00
clan-bot
82d5ca9a0b Update clan-core-for-checks in devFlake 2025-10-09 05:01:51 +00:00
clan-bot
28d8a91a30 Merge pull request 'Update clan-core-for-checks in devFlake' (#5440) from update-devFlake-clan-core-for-checks into main 2025-10-09 00:09:59 +00:00
clan-bot
18f8d69728 Update clan-core-for-checks in devFlake 2025-10-09 00:01:50 +00:00
nickdichev
1feead4ce4 Fix typo in "Authoring a 'clan.service' module" 2025-10-08 20:16:16 +00:00
clan-bot
7f28110558 Merge pull request 'Update clan-core-for-checks in devFlake' (#5438) from update-devFlake-clan-core-for-checks into main 2025-10-08 20:09:55 +00:00
clan-bot
38787da891 Update clan-core-for-checks in devFlake 2025-10-08 20:01:48 +00:00
clan-bot
2b587da9fe Merge pull request 'Update clan-core-for-checks in devFlake' (#5437) from update-devFlake-clan-core-for-checks into main 2025-10-08 15:10:06 +00:00
clan-bot
acd2c1654b Update clan-core-for-checks in devFlake 2025-10-08 15:01:52 +00:00
hsjobeki
2ecb1399c3 Merge pull request 'docs: move generated markdown into a package' (#5436) from docs-source into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5436
2025-10-08 14:40:41 +00:00
Johannes Kirschbauer
46ae6b49c1 docs: move generated markdown into a package 2025-10-08 16:37:31 +02:00
hsjobeki
50a8a69719 Merge pull request 'fix: pull request template folder' (#5435) from fix-j into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5435
2025-10-08 14:28:00 +00:00
Johannes Kirschbauer
203761a99c fix: pull request template folder 2025-10-08 16:24:52 +02:00
hsjobeki
990b4e0223 Merge pull request 'docs: move option-search into own package' (#5434) from docs-source into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5434
2025-10-08 14:05:38 +00:00
Johannes Kirschbauer
032f54cbfb docs: fix links 2025-10-08 16:02:31 +02:00
hsjobeki
47146efa0f Merge pull request 'PR: add pull request template' (#5428) from team-workflow into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5428
2025-10-08 13:44:14 +00:00
Johannes Kirschbauer
c031abcd9e docs: move option-search into own package 2025-10-08 15:42:18 +02:00
Johannes Kirschbauer
0390d5999d PR: add pull request template 2025-10-08 12:44:36 +02:00
49 changed files with 653 additions and 458 deletions

View File

@@ -0,0 +1,12 @@
## Description of the change
<!-- Brief summary of the change if not already clear from the title -->
## Checklist
- [ ] Updated Documentation
- [ ] Added tests
- [ ] Doesn't affect backwards compatibility - or check the next points
- [ ] Add the breaking change and migration details to docs/release-notes.md
- !!! Review from another person is required *BEFORE* merge !!!
- [ ] Add introduction of major feature to docs/release-notes.md

View File

@@ -19,28 +19,19 @@ let
nixosLib = import (self.inputs.nixpkgs + "/nixos/lib") { };
in
{
imports =
let
clanCoreModulesDir = ../nixosModules/clanCore;
getClanCoreTestModules =
let
moduleNames = attrNames (builtins.readDir clanCoreModulesDir);
testPaths = map (
moduleName: clanCoreModulesDir + "/${moduleName}/tests/flake-module.nix"
) moduleNames;
in
filter pathExists testPaths;
in
getClanCoreTestModules
++ filter pathExists [
./devshell/flake-module.nix
./flash/flake-module.nix
./installation/flake-module.nix
./update/flake-module.nix
./morph/flake-module.nix
./nixos-documentation/flake-module.nix
./dont-depend-on-repo-root.nix
];
imports = filter pathExists [
./devshell/flake-module.nix
./flash/flake-module.nix
./installation/flake-module.nix
./update/flake-module.nix
./morph/flake-module.nix
./nixos-documentation/flake-module.nix
./dont-depend-on-repo-root.nix
# clan core submodule tests
../nixosModules/clanCore/machine-id/tests/flake-module.nix
../nixosModules/clanCore/postgresql/tests/flake-module.nix
../nixosModules/clanCore/state-version/tests/flake-module.nix
];
flake.check = genAttrs [ "x86_64-linux" "aarch64-darwin" ] (
system:
let
@@ -120,7 +111,7 @@ in
) (self.darwinConfigurations or { })
// lib.mapAttrs' (n: lib.nameValuePair "package-${n}") (
if system == "aarch64-darwin" then
lib.filterAttrs (n: _: n != "docs" && n != "deploy-docs" && n != "docs-options") packagesToBuild
lib.filterAttrs (n: _: n != "docs" && n != "deploy-docs" && n != "option-search") packagesToBuild
else
packagesToBuild
)

View File

@@ -15,7 +15,6 @@ let
networking.useNetworkd = true;
services.openssh.enable = true;
services.openssh.settings.UseDns = false;
services.openssh.settings.PasswordAuthentication = false;
system.nixos.variant_id = "installer";
environment.systemPackages = [
pkgs.nixos-facter

18
devFlake/flake.lock generated
View File

@@ -3,10 +3,10 @@
"clan-core-for-checks": {
"flake": false,
"locked": {
"lastModified": 1759915474,
"narHash": "sha256-ef7awwmx2onWuA83FNE29B3tTZ+tQxEWLD926ckMiF8=",
"lastModified": 1760213549,
"narHash": "sha256-XosVRUEcdsoEdRtXyz9HrRc4Dt9Ke+viM5OVF7tLK50=",
"ref": "main",
"rev": "81e15cab34f9ae00b6f2df5f2e53ee07cd3a0af3",
"rev": "9c8797e77031d8d472d057894f18a53bdc9bbe1e",
"shallow": true,
"type": "git",
"url": "https://git.clan.lol/clan/clan-core"
@@ -105,11 +105,11 @@
},
"nixpkgs-dev": {
"locked": {
"lastModified": 1759860509,
"narHash": "sha256-c7eJvqAlWLhwNc9raHkQ7mvoFbHLUO/cLMrww1ds4Zg=",
"lastModified": 1760161054,
"narHash": "sha256-PO3cKHFIQEPI0dr/SzcZwG50cHXfjoIqP2uS5W78OXg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b574dcadf3fb578dee8d104b565bd745a5a9edc0",
"rev": "e18d8ec6fafaed55561b7a1b54eb1c1ce3ffa2c5",
"type": "github"
},
"original": {
@@ -208,11 +208,11 @@
"nixpkgs": []
},
"locked": {
"lastModified": 1758728421,
"narHash": "sha256-ySNJ008muQAds2JemiyrWYbwbG+V7S5wg3ZVKGHSFu8=",
"lastModified": 1760120816,
"narHash": "sha256-gq9rdocpmRZCwLS5vsHozwB6b5nrOBDNc2kkEaTXHfg=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "5eda4ee8121f97b218f7cc73f5172098d458f1d1",
"rev": "761ae7aff00907b607125b2f57338b74177697ed",
"type": "github"
},
"original": {

2
docs/.gitignore vendored
View File

@@ -1,6 +1,6 @@
/site/reference
/site/services/official
/site/static
/site/options
/site/option-search
/site/openapi.json
!/site/static/extra.css

View File

@@ -180,7 +180,7 @@ nav:
- services/official/zerotier.md
- services/community.md
- Search Clan Options: "/options"
- Search Clan Options: "/option-search"
docs_dir: site
site_dir: out

View File

@@ -5,7 +5,7 @@
clan-lib-openapi,
roboto,
fira-code,
docs-options,
option-search,
...
}:
let
@@ -51,9 +51,9 @@ pkgs.stdenv.mkDerivation {
chmod -R +w ./site
echo "Generated API documentation in './site/reference/' "
rm -rf ./site/options
cp -r ${docs-options} ./site/options
chmod -R +w ./site/options
rm -rf ./site/option-search
cp -r ${option-search} ./site/option-search
chmod -R +w ./site/option-search
# Link to fonts
ln -snf ${roboto}/share/fonts/truetype/Roboto-Regular.ttf ./site/static/

View File

@@ -1,8 +1,5 @@
{ inputs, self, ... }:
{ inputs, ... }:
{
imports = [
./options/flake-module.nix
];
perSystem =
{
config,
@@ -10,74 +7,7 @@
pkgs,
...
}:
let
# Simply evaluated options (JSON)
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
jsonDocs = pkgs.callPackage ./get-module-docs.nix {
inherit (self) clanModules;
clan-core = self;
inherit pkgs;
};
# clan service options
clanModulesViaService = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaService);
# Simply evaluated options (JSON)
renderOptions =
pkgs.runCommand "render-options"
{
# TODO: ruff does not splice properly in nativeBuildInputs
depsBuildBuild = [ pkgs.ruff ];
nativeBuildInputs = [
pkgs.python3
pkgs.mypy
self'.packages.clan-cli
];
}
''
install -D -m755 ${./render_options}/__init__.py $out/bin/render-options
patchShebangs --build $out/bin/render-options
ruff format --check --diff $out/bin/render-options
ruff check --line-length 88 $out/bin/render-options
mypy --strict $out/bin/render-options
'';
module-docs =
pkgs.runCommand "rendered"
{
buildInputs = [
pkgs.python3
self'.packages.clan-cli
];
}
''
export CLAN_CORE_PATH=${
inputs.nixpkgs.lib.fileset.toSource {
root = ../..;
fileset = ../../clanModules;
}
}
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
# A file that contains the links to all clanModule docs
export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService}
export CLAN_SERVICE_INTERFACE=${self'.legacyPackages.clan-service-module-interface}/share/doc/nixos/options.json
export CLAN_OPTIONS_PATH=${self'.legacyPackages.clan-options}/share/doc/nixos/options.json
mkdir $out
# The python script will place mkDocs files in the output directory
exec python3 ${renderOptions}/bin/render-options
'';
in
{
legacyPackages = {
inherit
jsonDocs
clanModulesViaService
;
};
devShells.docs = self'.packages.docs.overrideAttrs (_old: {
nativeBuildInputs = [
# Run: htmlproofer --disable-external
@@ -96,15 +26,14 @@
docs = pkgs.python3.pkgs.callPackage ./default.nix {
inherit (self'.packages)
clan-cli-docs
docs-options
option-search
inventory-api-docs
clan-lib-openapi
module-docs
;
inherit (inputs) nixpkgs;
inherit module-docs;
};
deploy-docs = pkgs.callPackage ./deploy-docs.nix { inherit (config.packages) docs; };
inherit module-docs;
};
checks.docs-integrity =
pkgs.runCommand "docs-integrity"

9
docs/release-notes.md Normal file
View File

@@ -0,0 +1,9 @@
# clan-core release notes 25.11
<!-- This is not rendered yet -->
## New features
## Breaking Changes
## Misc

View File

@@ -288,7 +288,7 @@ of their type.
In the inventory we the assign machines to a type, e.g. by using tags
```nix title="flake.nix"
instnaces.machine-type = {
instances.machine-type = {
module.input = "self";
module.name = "@pinpox/machine-type";
roles.desktop.tags.desktop = { };

View File

@@ -122,7 +122,7 @@ hide:
command line interface
- [Clan Options](/options)
- [Clan Options](./reference/options/clan.md)
---

View File

@@ -4,10 +4,10 @@ This section of the site provides an overview of available options and commands
---
- [Clan Configuration Option](/options) - for defining a Clan
- Learn how to use the [Clan CLI](../reference/cli/index.md)
- Explore available [services](../services/definition.md)
- [NixOS Configuration Options](../reference/clan.core/index.md) - Additional options avilable on a NixOS machine.
- [Search Clan Option](/option-search) - for defining a Clan
---

6
flake.lock generated
View File

@@ -181,11 +181,11 @@
]
},
"locked": {
"lastModified": 1758728421,
"narHash": "sha256-ySNJ008muQAds2JemiyrWYbwbG+V7S5wg3ZVKGHSFu8=",
"lastModified": 1760120816,
"narHash": "sha256-gq9rdocpmRZCwLS5vsHozwB6b5nrOBDNc2kkEaTXHfg=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "5eda4ee8121f97b218f7cc73f5172098d458f1d1",
"rev": "761ae7aff00907b607125b2f57338b74177697ed",
"type": "github"
},
"original": {

View File

@@ -0,0 +1,51 @@
{ lib }:
let
sanitizePath =
rootPath: path:
let
storePrefix = builtins.unsafeDiscardStringContext ("${rootPath}");
pathStr = lib.removePrefix "/" (
lib.removePrefix storePrefix (builtins.unsafeDiscardStringContext (toString path))
);
in
pathStr;
mkFunctions = rootPath: passthru: virtual_fs: {
# Some functions to override lib functions
pathExists =
path:
let
pathStr = sanitizePath rootPath path;
isPassthru = builtins.any (exclude: (builtins.match exclude pathStr) != null) passthru;
in
if isPassthru then
builtins.pathExists path
else
let
res = virtual_fs ? ${pathStr};
in
lib.trace "pathExists: '${pathStr}' -> '${lib.generators.toPretty { } res}'" res;
readDir =
path:
let
pathStr = sanitizePath rootPath path;
base = (pathStr + "/");
res = lib.mapAttrs' (name: fileInfo: {
name = lib.removePrefix base name;
value = fileInfo.type;
}) (lib.filterAttrs (n: _: lib.hasPrefix base n) virtual_fs);
isPassthru = builtins.any (exclude: (builtins.match exclude pathStr) != null) passthru;
in
if isPassthru then
builtins.readDir path
else
lib.trace "readDir: '${pathStr}' -> '${lib.generators.toPretty { } res}'" res;
};
in
{
virtual_fs,
rootPath,
# Patterns
passthru ? [ ],
}:
mkFunctions rootPath passthru virtual_fs

View File

@@ -36,6 +36,10 @@ lib.fix (
# TODO: Flatten our lib functions like this:
resolveModule = clanLib.callLib ./resolve-module { };
fs = {
inherit (builtins) pathExists readDir;
};
};
in
f

View File

@@ -133,12 +133,13 @@ in
}
)
{
# TODO: Figure out why this causes infinite recursion
inventory.machines = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
builtins.mapAttrs (_n: _v: { }) (
lib.filterAttrs (_: t: t == "directory") (builtins.readDir "${directory}/machines")
)
);
# Note: we use clanLib.fs here, so that we can override it in tests
inventory = lib.optionalAttrs (clanLib.fs.pathExists "${directory}/machines") ({
imports = lib.mapAttrsToList (name: _t: {
_file = "${directory}/machines/${name}";
machines.${name} = { };
}) ((lib.filterAttrs (_: t: t == "directory") (clanLib.fs.readDir "${directory}/machines")));
});
}
{
inventory.machines = lib.mapAttrs (_n: _: { }) config.machines;

108
lib/modules/dir_test.nix Normal file
View File

@@ -0,0 +1,108 @@
{
lib ? import <nixpkgs/lib>,
}:
let
clanLibOrig = (import ./.. { inherit lib; }).__unfix__;
clanLibWithFs =
{ virtual_fs }:
lib.fix (
lib.extends (
final: _:
let
clan-core = {
clanLib = final;
modules.clan.default = lib.modules.importApply ./clan { inherit clan-core; };
# Note: Can add other things to "clan-core"
# ... Not needed for this test
};
in
{
clan = import ../clan {
inherit lib clan-core;
};
# Override clanLib.fs for unit-testing against a virtual filesystem
fs = import ../clanTest/virtual-fs.nix { inherit lib; } {
inherit rootPath virtual_fs;
# Example of a passthru
# passthru = [
# ".*inventory\.json$"
# ];
};
}
) clanLibOrig
);
rootPath = ./.;
in
{
test_autoload_directories =
let
vclan =
(clanLibWithFs {
virtual_fs = {
"machines" = {
type = "directory";
};
"machines/foo-machine" = {
type = "directory";
};
"machines/bar-machine" = {
type = "directory";
};
};
}).clan
{ config.directory = rootPath; };
in
{
inherit vclan;
expr = {
machines = lib.attrNames vclan.config.inventory.machines;
definedInMachinesDir = map (
p: lib.hasInfix "/machines/" p
) vclan.options.inventory.valueMeta.configuration.options.machines.files;
};
expected = {
machines = [
"bar-machine"
"foo-machine"
];
definedInMachinesDir = [
true # /machines/foo-machine
true # /machines/bar-machine
false # <clan-core>/module.nix defines "machines" without members
];
};
};
# Could probably be unified with the previous test
# This is here for the sake to show that 'virtual_fs' is a test parameter
test_files_are_not_machines =
let
vclan =
(clanLibWithFs {
virtual_fs = {
"machines" = {
type = "directory";
};
"machines/foo.nix" = {
type = "file";
};
"machines/bar.nix" = {
type = "file";
};
};
}).clan
{ config.directory = rootPath; };
in
{
inherit vclan;
expr = {
machines = lib.attrNames vclan.config.inventory.machines;
};
expected = {
machines = [ ];
};
};
}

View File

@@ -12,6 +12,7 @@ let
in
#######
{
autoloading = import ./dir_test.nix { inherit lib; };
test_missing_self =
let
eval = clan {

View File

@@ -164,13 +164,25 @@
config = lib.mkIf (config.clan.core.secrets != { }) {
clan.core.facts.services = lib.mapAttrs' (
name: service:
lib.warn "clan.core.secrets.${name} is deprecated, use clan.core.facts.services.${name} instead" (
lib.nameValuePair name ({
secret = service.secrets;
public = service.facts;
generator = service.generator;
})
)
lib.warn
''
###############################################################################
# #
# clan.core.secrets.${name} clan.core.facts.services.${name} is deprecated #
# in favor of "vars" #
# #
# Refer to https://docs.clan.lol/guides/migrations/migration-facts-vars/ #
# for migration instructions. #
# #
###############################################################################
''
(
lib.nameValuePair name ({
secret = service.secrets;
public = service.facts;
generator = service.generator;
})
)
) config.clan.core.secrets;
};
}

View File

@@ -6,7 +6,17 @@
}:
{
config.warnings = lib.optionals (config.clan.core.facts.services != { }) [
"Facts are deprecated, please migrate them to vars instead, see: https://docs.clan.lol/guides/migrations/migration-facts-vars/"
''
###############################################################################
# #
# Facts are deprecated please migrate any usages to vars instead #
# #
# #
# Refer to https://docs.clan.lol/guides/migrations/migration-facts-vars/ #
# for migration instructions. #
# #
###############################################################################
''
];
options.clan.core.facts = {

View File

@@ -1,37 +0,0 @@
# collectFiles helper function
{
lib ? import <nixpkgs/lib>,
}:
let
inherit (lib)
filterAttrs
flatten
mapAttrsToList
;
in
generators:
let
relevantFiles =
generator:
filterAttrs (
_name: f: f.secret && f.deploy && (f.neededFor == "users" || f.neededFor == "services")
) generator.files;
allFiles = flatten (
mapAttrsToList (
gen_name: generator:
mapAttrsToList (fname: file: {
name = fname;
generator = gen_name;
neededForUsers = file.neededFor == "users";
inherit (generator) share;
inherit (file)
owner
group
mode
restartUnits
;
}) (relevantFiles generator)
) generators
);
in
allFiles

View File

@@ -7,19 +7,9 @@
}:
let
collectFiles = import ./collectFiles.nix { inherit lib; };
mapGeneratorsToSopsSecrets = import ./generators-to-sops.nix { inherit lib; };
machineName = config.clan.core.settings.machine.name;
secretPath =
secret:
if secret.share then
config.clan.core.settings.directory + "/vars/shared/${secret.generator}/${secret.name}/secret"
else
config.clan.core.settings.directory
+ "/vars/per-machine/${machineName}/${secret.generator}/${secret.name}/secret";
vars = collectFiles config.clan.core.vars.generators;
in
{
config.clan.core.vars.settings = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") {
@@ -39,28 +29,13 @@ in
};
config.sops = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") {
secrets = lib.listToAttrs (
map (secret: {
name = "vars/${secret.generator}/${secret.name}";
value = {
inherit (secret)
owner
group
mode
neededForUsers
;
sopsFile = builtins.path {
name = "${secret.generator}_${secret.name}";
path = secretPath secret;
};
format = "binary";
}
// (lib.optionalAttrs (_class == "nixos") {
inherit (secret) restartUnits;
});
}) (builtins.filter (x: builtins.pathExists (secretPath x)) vars)
);
#
secrets = mapGeneratorsToSopsSecrets {
inherit machineName;
directory = config.clan.core.settings.directory;
class = _class;
generators = config.clan.core.vars.generators;
};
# To get proper error messages about missing secrets we need a dummy secret file that is always present
defaultSopsFile = lib.mkIf config.sops.validateSopsFiles (

View File

@@ -0,0 +1,77 @@
# This file maps generators to sops.secrets
# TODO(@davHau): add tests
{
lib ? import <nixpkgs/lib>,
# Can be mocked for testing
pathExists ? builtins.pathExists,
}:
let
inherit (lib)
filterAttrs
mapAttrsToList
;
relevantFiles = filterAttrs (
_name: f: f.secret && f.deploy && (f.neededFor == "users" || f.neededFor == "services")
);
extractSecretDefinitions =
generators:
builtins.concatLists (
mapAttrsToList (
gen_name: generator:
mapAttrsToList (fname: file: {
name = fname;
generator = gen_name;
neededForUsers = file.neededFor == "users";
inherit (generator) share;
inherit (file)
owner
group
mode
restartUnits
;
}) (relevantFiles generator.files)
) generators
);
mapGeneratorsToSopsSecrets =
{
machineName,
directory,
class,
generators,
}:
assert lib.assertMsg (class == "nixos" || class == "darwin")
"Error trying to map 'var.generators' to 'sops.secrets': class must be 'nixos' or 'darwin', got: ${class}";
let
getSecretPath =
secret:
let
scope = if secret.share then "shared" else "per-machine/${machineName}";
in
"${directory}/vars/${scope}/${secret.generator}/${secret.name}/secret";
in
lib.listToAttrs (
map (secret: {
name = "vars/${secret.generator}/${secret.name}";
value = {
inherit (secret)
owner
group
mode
neededForUsers
;
sopsFile = builtins.path {
name = "${secret.generator}_${secret.name}";
path = getSecretPath secret;
};
format = "binary";
}
// (lib.optionalAttrs (class == "nixos") {
inherit (secret) restartUnits;
});
}) (builtins.filter (x: pathExists (getSecretPath x)) (extractSecretDefinitions generators))
);
in
mapGeneratorsToSopsSecrets

View File

@@ -113,15 +113,27 @@ mkShell {
# todo darwin support needs some work
(lib.optionalString stdenv.hostPlatform.isLinux ''
# configure playwright for storybook snapshot testing
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# we only want webkit as that matches what the app is rendered with
export PLAYWRIGHT_BROWSERS_PATH=${
playwright-driver.browsers.override {
withFfmpeg = false;
withFirefox = false;
withWebkit = true;
withChromium = false;
withChromiumHeadlessShell = true;
withChromiumHeadlessShell = false;
}
}
export PLAYWRIGHT_HOST_PLATFORM_OVERRIDE="ubuntu-24.04"
# stop playwright from trying to validate it has downloaded the necessary browsers
# we are providing them manually via nix
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true
# 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

@@ -53,7 +53,7 @@
"jsdom": "^26.1.0",
"knip": "^5.61.2",
"markdown-to-jsx": "^7.7.10",
"playwright": "~1.53.2",
"playwright": "~1.55.1",
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",
@@ -6956,13 +6956,13 @@
}
},
"node_modules/playwright": {
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",
"integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==",
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
"integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.53.2"
"playwright-core": "1.55.1"
},
"bin": {
"playwright": "cli.js"
@@ -6975,9 +6975,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz",
"integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==",
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
"integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -48,7 +48,7 @@
"jsdom": "^26.1.0",
"knip": "^5.61.2",
"markdown-to-jsx": "^7.7.10",
"playwright": "~1.53.2",
"playwright": "~1.55.1",
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",

View File

@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Button, ButtonProps } from "./Button";
import { Component } from "solid-js";
import { expect, fn, waitFor } from "storybook/test";
import { expect, fn, waitFor, within } from "storybook/test";
import { StoryContext } from "@kachurun/storybook-solid-vite";
const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor;
@@ -216,17 +216,11 @@ const timeout = process.env.NODE_ENV === "test" ? 500 : 2000;
export const Primary: Story = {
args: {
hierarchy: "primary",
onAction: fn(async () => {
// wait 500 ms to simulate an action
await new Promise((resolve) => setTimeout(resolve, timeout));
// randomly fail to check that the loading state still returns to normal
if (Math.random() > 0.5) {
throw new Error("Action failure");
}
}),
onClick: fn(),
},
play: async ({ canvas, step, userEvent, args }: StoryContext) => {
play: async ({ canvasElement, step, userEvent, args }: StoryContext) => {
const canvas = within(canvasElement);
const buttons = await canvas.findAllByRole("button");
for (const button of buttons) {
@@ -238,14 +232,6 @@ export const Primary: Story = {
}
await step(`Click on ${testID}`, async () => {
// check for the loader
const loaders = button.getElementsByClassName("loader");
await expect(loaders.length).toEqual(1);
// assert its width is 0 before we click
const [loader] = loaders;
await expect(loader.clientWidth).toEqual(0);
// move the mouse over the button
await userEvent.hover(button);
@@ -255,33 +241,8 @@ export const Primary: Story = {
// click the button
await userEvent.click(button);
// check the button has changed
await waitFor(
async () => {
// the action handler should have been called
await expect(args.onAction).toHaveBeenCalled();
// the button should have a loading class
await expect(button).toHaveClass("loading");
// the loader should be visible
await expect(loader.clientWidth).toBeGreaterThan(0);
// the pointer should have changed to wait
await expect(getCursorStyle(button)).toEqual("wait");
},
{ timeout: timeout + 500 },
);
// wait for the action handler to finish
await waitFor(
async () => {
// the loading class should be removed
await expect(button).not.toHaveClass("loading");
// the loader should be hidden
await expect(loader.clientWidth).toEqual(0);
// the pointer should be normal
await expect(getCursorStyle(button)).toEqual("pointer");
},
{ timeout: timeout + 500 },
);
// the click handler should have been called
await expect(args.onClick).toHaveBeenCalled();
});
}
},

View File

@@ -57,6 +57,7 @@ export const Button = (props: ButtonProps) => {
return (
<KobalteButton
role="button"
class={cx(
styles.button, // default button class
local.size != "default" && styles[local.size],

View File

@@ -160,47 +160,47 @@ const mockFetcher = <K extends OperationNames>(
},
}) satisfies ApiCall<K>;
export const Default: Story = {
args: {},
decorators: [
(Story: StoryObj) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: Infinity,
},
},
});
Object.entries(queryData).forEach(([clanURI, clan]) => {
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "details"],
clan.details,
);
const machines = clan.machines || {};
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "machines"],
machines,
);
Object.entries(machines).forEach(([name, machine]) => {
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "machine", name, "state"],
machine.state,
);
});
});
return (
<ApiClientProvider client={{ fetch: mockFetcher }}>
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
</ApiClientProvider>
);
},
],
};
// export const Default: Story = {
// args: {},
// decorators: [
// (Story: StoryObj) => {
// const queryClient = new QueryClient({
// defaultOptions: {
// queries: {
// retry: false,
// staleTime: Infinity,
// },
// },
// });
//
// Object.entries(queryData).forEach(([clanURI, clan]) => {
// queryClient.setQueryData(
// ["clans", encodeBase64(clanURI), "details"],
// clan.details,
// );
//
// const machines = clan.machines || {};
//
// queryClient.setQueryData(
// ["clans", encodeBase64(clanURI), "machines"],
// machines,
// );
//
// Object.entries(machines).forEach(([name, machine]) => {
// queryClient.setQueryData(
// ["clans", encodeBase64(clanURI), "machine", name, "state"],
// machine.state,
// );
// });
// });
//
// return (
// <ApiClientProvider client={{ fetch: mockFetcher }}>
// <QueryClientProvider client={queryClient}>
// <Story />
// </QueryClientProvider>
// </ApiClientProvider>
// );
// },
// ],
// };

View File

@@ -11,28 +11,35 @@ export default meta;
type Story = StoryObj<ClanSettingsModalProps>;
export const Default: Story = {
args: {
onClose: fn(),
model: {
uri: "/home/foo/my-clan",
const props: ClanSettingsModalProps = {
onClose: fn(),
model: {
uri: "/home/foo/my-clan",
details: {
name: "Sol",
description: null,
icon: null,
fieldsSchema: {
name: {
readonly: true,
reason: null,
},
description: {
readonly: false,
reason: null,
},
icon: {
readonly: false,
reason: null,
},
},
fieldsSchema: {
name: {
readonly: true,
reason: null,
readonly_members: [],
},
description: {
readonly: false,
reason: null,
readonly_members: [],
},
icon: {
readonly: false,
reason: null,
readonly_members: [],
},
},
},
};
export const Default: Story = {
args: props,
};

View File

@@ -22,9 +22,9 @@ import { Alert } from "@/src/components/Alert/Alert";
import { removeClanURI } from "@/src/stores/clan";
const schema = v.object({
name: v.pipe(v.optional(v.string())),
description: v.nullish(v.string()),
icon: v.pipe(v.nullish(v.string())),
name: v.string(),
description: v.optional(v.string()),
icon: v.optional(v.string()),
});
export interface ClanSettingsModalProps {

View File

@@ -1,15 +0,0 @@
import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { CubeScene } from "./cubes";
const meta: Meta = {
title: "scene/cubes",
component: CubeScene,
};
export default meta;
type Story = StoryObj;
export const Default: Story = {
args: {},
};

View File

@@ -304,11 +304,10 @@ const FlashProgress = () => {
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
onMount(async () => {
const result = await store.flash.progress.result;
if (result.status == "success") {
console.log("Flashing Success");
const result = await store.flash?.progress?.result;
if (result?.status == "success") {
stepSignal.next();
}
stepSignal.next();
});
const handleCancel = async () => {

View File

@@ -165,23 +165,23 @@ export default meta;
type Story = StoryObj<typeof ServiceWorkflow>;
export const Default: Story = {
args: {},
};
export const SelectRoleMembers: Story = {
render: () => (
<ServiceWorkflow
handleSubmit={(instance) => {
console.log("Submitted instance:", instance);
}}
onClose={() => {
console.log("Closed");
}}
initialStep="select:members"
initialStore={{
currentRole: "peer",
}}
/>
),
};
// export const Default: Story = {
// args: {},
// };
//
// export const SelectRoleMembers: Story = {
// render: () => (
// <ServiceWorkflow
// handleSubmit={(instance) => {
// console.log("Submitted instance:", instance);
// }}
// onClose={() => {
// console.log("Closed");
// }}
// initialStep="select:members"
// initialStore={{
// currentRole: "peer",
// }}
// />
// ),
// };

View File

@@ -9,7 +9,11 @@
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client", "vite-plugin-solid-svg/types-component-solid"],
"types": [
"vite/client",
"vite-plugin-solid-svg/types-component-solid",
"@vitest/browser/providers/playwright"
],
"noEmit": true,
"resolveJsonModule": true,
"allowJs": true,

View File

@@ -40,7 +40,14 @@ export default mergeConfig(
enabled: true,
headless: true,
provider: "playwright",
instances: [{ browser: "chromium" }],
instances: [
{
browser: "webkit",
launch: {
executablePath: process.env.PLAYWRIGHT_WEBKIT_EXECUTABLE,
},
},
],
},
// This setup file applies Storybook project annotations for Vitest
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations

View File

@@ -1,24 +0,0 @@
{
# Use this path to our repo root e.g. for UI test
# inputs.clan-core.url = "../../../../.";
# this placeholder is replaced by the path to nixpkgs
inputs.clan-core.url = "__CLAN_CORE__";
outputs =
{ self, clan-core }:
let
clan = clan-core.lib.clan {
inherit self;
meta.name = "test_flake_with_core_dynamic_machines";
machines =
let
machineModules = builtins.readDir (self + "/machines");
in
builtins.mapAttrs (name: _type: import (self + "/machines/${name}")) machineModules;
};
in
{
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
};
}

View File

@@ -166,16 +166,16 @@ def test_generate_public_and_secret_vars(
assert shared_value.startswith("shared")
vars_text = stringify_all_vars(machine)
flake_obj = Flake(str(flake.path))
my_generator = Generator("my_generator", machine="my_machine", _flake=flake_obj)
my_generator = Generator("my_generator", machines=["my_machine"], _flake=flake_obj)
shared_generator = Generator(
"my_shared_generator",
share=True,
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
dependent_generator = Generator(
"dependent_generator",
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
in_repo_store = in_repo.FactStore(flake=flake_obj)
@@ -340,12 +340,12 @@ def test_generate_secret_var_sops_with_default_group(
flake_obj = Flake(str(flake.path))
first_generator = Generator(
"first_generator",
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
second_generator = Generator(
"second_generator",
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
in_repo_store = in_repo.FactStore(flake=flake_obj)
@@ -375,13 +375,13 @@ def test_generate_secret_var_sops_with_default_group(
first_generator_with_share = Generator(
"first_generator",
share=False,
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
second_generator_with_share = Generator(
"second_generator",
share=False,
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
assert sops_store.user_has_access("user2", first_generator_with_share, "my_secret")
@@ -432,7 +432,6 @@ def test_generated_shared_secret_sops(
assert check_vars(machine1.name, machine1.flake)
cli.run(["vars", "generate", "--flake", str(flake.path), "machine2"])
assert check_vars(machine2.name, machine2.flake)
assert check_vars(machine2.name, machine2.flake)
m1_sops_store = sops.SecretStore(machine1.flake)
m2_sops_store = sops.SecretStore(machine2.flake)
# Create generators with machine context for testing
@@ -513,28 +512,28 @@ def test_generate_secret_var_password_store(
"my_generator",
share=False,
files=[],
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
my_generator_shared = Generator(
"my_generator",
share=True,
files=[],
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
my_shared_generator = Generator(
"my_shared_generator",
share=True,
files=[],
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
my_shared_generator_not_shared = Generator(
"my_shared_generator",
share=False,
files=[],
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
assert store.exists(my_generator, "my_secret")
@@ -546,7 +545,7 @@ def test_generate_secret_var_password_store(
name="my_generator",
share=False,
files=[],
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
assert store.get(generator, "my_secret").decode() == "hello\n"
@@ -557,7 +556,7 @@ def test_generate_secret_var_password_store(
"my_generator",
share=False,
files=[],
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
var_name = "my_secret"
@@ -570,7 +569,7 @@ def test_generate_secret_var_password_store(
"my_generator2",
share=False,
files=[],
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
var_name = "my_secret2"
@@ -582,7 +581,7 @@ def test_generate_secret_var_password_store(
"my_shared_generator",
share=True,
files=[],
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
var_name = "my_shared_secret"
@@ -629,8 +628,8 @@ def test_generate_secret_for_multiple_machines(
in_repo_store2 = in_repo.FactStore(flake=flake_obj)
# Create generators for each machine
gen1 = Generator("my_generator", machine="machine1", _flake=flake_obj)
gen2 = Generator("my_generator", machine="machine2", _flake=flake_obj)
gen1 = Generator("my_generator", machines=["machine1"], _flake=flake_obj)
gen2 = Generator("my_generator", machines=["machine2"], _flake=flake_obj)
assert in_repo_store1.exists(gen1, "my_value")
assert in_repo_store2.exists(gen2, "my_value")
@@ -694,12 +693,12 @@ def test_prompt(
# Set up objects for testing the results
flake_obj = Flake(str(flake.path))
my_generator = Generator("my_generator", machine="my_machine", _flake=flake_obj)
my_generator = Generator("my_generator", machines=["my_machine"], _flake=flake_obj)
my_generator_with_details = Generator(
name="my_generator",
share=False,
files=[],
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
@@ -784,10 +783,10 @@ def test_shared_vars_regeneration(
in_repo_store_2 = in_repo.FactStore(machine2.flake)
# Create generators with machine context for testing
child_gen_m1 = Generator(
"child_generator", share=False, machine="machine1", _flake=machine1.flake
"child_generator", share=False, machines=["machine1"], _flake=machine1.flake
)
child_gen_m2 = Generator(
"child_generator", share=False, machine="machine2", _flake=machine2.flake
"child_generator", share=False, machines=["machine2"], _flake=machine2.flake
)
# generate for machine 1
cli.run(["vars", "generate", "--flake", str(flake.path), "machine1"])
@@ -855,13 +854,13 @@ def test_multi_machine_shared_vars(
generator_m1 = Generator(
"shared_generator",
share=True,
machine="machine1",
machines=["machine1"],
_flake=machine1.flake,
)
generator_m2 = Generator(
"shared_generator",
share=True,
machine="machine2",
machines=["machine2"],
_flake=machine2.flake,
)
# generate for machine 1
@@ -917,7 +916,9 @@ def test_api_set_prompts(
)
machine = Machine(name="my_machine", flake=Flake(str(flake.path)))
store = in_repo.FactStore(machine.flake)
my_generator = Generator("my_generator", machine="my_machine", _flake=machine.flake)
my_generator = Generator(
"my_generator", machines=["my_machine"], _flake=machine.flake
)
assert store.exists(my_generator, "prompt1")
assert store.get(my_generator, "prompt1").decode() == "input1"
run_generators(
@@ -1061,10 +1062,10 @@ def test_migration(
assert "Migrated var my_generator/my_value" in caplog.text
assert "Migrated secret var my_generator/my_secret" in caplog.text
flake_obj = Flake(str(flake.path))
my_generator = Generator("my_generator", machine="my_machine", _flake=flake_obj)
my_generator = Generator("my_generator", machines=["my_machine"], _flake=flake_obj)
other_generator = Generator(
"other_generator",
machine="my_machine",
machines=["my_machine"],
_flake=flake_obj,
)
in_repo_store = in_repo.FactStore(flake=flake_obj)
@@ -1210,7 +1211,7 @@ def test_share_mode_switch_regenerates_secret(
sops_store = sops.SecretStore(flake=flake_obj)
generator_not_shared = Generator(
"my_generator", share=False, machine="my_machine", _flake=flake_obj
"my_generator", share=False, machines=["my_machine"], _flake=flake_obj
)
initial_public = in_repo_store.get(generator_not_shared, "my_value").decode()
@@ -1229,7 +1230,7 @@ def test_share_mode_switch_regenerates_secret(
# Read the new values with shared generator
generator_shared = Generator(
"my_generator", share=True, machine="my_machine", _flake=flake_obj
"my_generator", share=True, machines=["my_machine"], _flake=flake_obj
)
new_public = in_repo_store.get(generator_shared, "my_value").decode()

View File

@@ -40,12 +40,15 @@ class StoreBase(ABC):
def get_machine(self, generator: "Generator") -> str:
"""Get machine name from generator, asserting it's not None for now."""
if generator.machine is None:
if generator.share:
return "__shared"
if generator.share:
return "__shared"
if not generator.machines:
msg = f"Generator '{generator.name}' has no machine associated"
raise ClanError(msg)
return generator.machine
if len(generator.machines) != 1:
msg = f"Generator '{generator.name}' has {len(generator.machines)} machines, expected exactly 1"
raise ClanError(msg)
return generator.machines[0]
# get a single fact
@abstractmethod
@@ -147,7 +150,7 @@ class StoreBase(ABC):
prev_generator = dataclasses.replace(
generator,
share=not generator.share,
machine=machine if generator.share else None,
machines=[] if not generator.share else [machine],
)
if self.exists(prev_generator, var.name):
changed_files += self.delete(prev_generator, var.name)
@@ -165,12 +168,12 @@ class StoreBase(ABC):
new_file = self._set(generator, var, value, machine)
action_str = "Migrated" if is_migration else "Updated"
log_info: Callable
if generator.machine is None:
if generator.share:
log_info = log.info
else:
from clan_lib.machines.machines import Machine # noqa: PLC0415
machine_obj = Machine(name=generator.machine, flake=self.flake)
machine_obj = Machine(name=generator.machines[0], flake=self.flake)
log_info = machine_obj.info
if self.is_secret_store:
log.info(f"{action_str} secret var {generator.name}/{var.name}\n")

View File

@@ -61,14 +61,22 @@ class Generator:
migrate_fact: str | None = None
validation_hash: str | None = None
machine: str | None = None
machines: list[str] = field(default_factory=list)
_flake: "Flake | None" = None
_public_store: "StoreBase | None" = None
_secret_store: "StoreBase | None" = None
@property
def key(self) -> GeneratorKey:
return GeneratorKey(machine=self.machine, name=self.name)
if self.share:
# must be a shared generator
machine = None
elif len(self.machines) != 1:
msg = f"Shared generator {self.name} must have exactly one machine, but has {len(self.machines)}: {', '.join(self.machines)}"
raise ClanError(msg)
else:
machine = self.machines[0]
return GeneratorKey(machine=machine, name=self.name)
def __hash__(self) -> int:
return hash(self.key)
@@ -143,7 +151,7 @@ class Generator:
files_selector = "config.clan.core.vars.generators.*.files.*.{secret,deploy,owner,group,mode,neededFor}"
flake.precache(cls.get_machine_selectors(machine_names))
generators = []
generators: list[Generator] = []
shared_generators_raw: dict[
str, tuple[str, dict, dict]
] = {} # name -> (machine_name, gen_data, files_data)
@@ -244,15 +252,27 @@ class Generator:
migrate_fact=gen_data.get("migrateFact"),
validation_hash=gen_data.get("validationHash"),
prompts=prompts,
# only set machine for machine-specific generators
# this is essential for the graph algorithms to work correctly
machine=None if share else machine_name,
# shared generators can have multiple machines, machine-specific have one
machines=[machine_name],
_flake=flake,
_public_store=pub_store,
_secret_store=sec_store,
)
generators.append(generator)
if share:
# For shared generators, check if we already created it
existing = next(
(g for g in generators if g.name == gen_name and g.share), None
)
if existing:
# Just append the machine to the existing generator
existing.machines.append(machine_name)
else:
# Add the new shared generator
generators.append(generator)
else:
# Always add per-machine generators
generators.append(generator)
# TODO: This should be done in a non-mutable way.
if include_previous_values:

View File

@@ -49,28 +49,28 @@ def test_required_generators() -> None:
gen_1 = Generator(
name="gen_1",
dependencies=[],
machine=machine_name,
machines=[machine_name],
_public_store=public_store,
_secret_store=secret_store,
)
gen_2 = Generator(
name="gen_2",
dependencies=[gen_1.key],
machine=machine_name,
machines=[machine_name],
_public_store=public_store,
_secret_store=secret_store,
)
gen_2a = Generator(
name="gen_2a",
dependencies=[gen_2.key],
machine=machine_name,
machines=[machine_name],
_public_store=public_store,
_secret_store=secret_store,
)
gen_2b = Generator(
name="gen_2b",
dependencies=[gen_2.key],
machine=machine_name,
machines=[machine_name],
_public_store=public_store,
_secret_store=secret_store,
)
@@ -118,21 +118,22 @@ def test_shared_generator_invalidates_multiple_machines_dependents() -> None:
shared_gen = Generator(
name="shared_gen",
dependencies=[],
machine=None, # Shared generator
share=True, # Mark as shared generator
machines=[machine_1, machine_2], # Shared across both machines
_public_store=public_store,
_secret_store=secret_store,
)
gen_1 = Generator(
name="gen_1",
dependencies=[shared_gen.key],
machine=machine_1,
machines=[machine_1],
_public_store=public_store,
_secret_store=secret_store,
)
gen_2 = Generator(
name="gen_2",
dependencies=[shared_gen.key],
machine=machine_2,
machines=[machine_2],
_public_store=public_store,
_secret_store=secret_store,
)

View File

@@ -119,6 +119,9 @@ def run_machine_hardware_info_init(
if opts.debug:
cmd += ["--debug"]
# Add nix options to nixos-anywhere
cmd.extend(opts.machine.flake.nix_options or [])
cmd += [target_host.target]
cmd = nix_shell(
["nixos-anywhere"],

View File

@@ -93,21 +93,21 @@ def _ensure_healthy(
if generators is None:
generators = Generator.get_machine_generators([machine.name], machine.flake)
pub_healtcheck_msg = machine.public_vars_store.health_check(
public_health_check_msg = machine.public_vars_store.health_check(
machine.name,
generators,
)
sec_healtcheck_msg = machine.secret_vars_store.health_check(
secret_health_check_msg = machine.secret_vars_store.health_check(
machine.name,
generators,
)
if pub_healtcheck_msg or sec_healtcheck_msg:
if public_health_check_msg or secret_health_check_msg:
msg = f"Health check failed for machine {machine.name}:\n"
if pub_healtcheck_msg:
msg += f"Public vars store: {pub_healtcheck_msg}\n"
if sec_healtcheck_msg:
msg += f"Secret vars store: {sec_healtcheck_msg}"
if public_health_check_msg:
msg += f"Public vars store: {public_health_check_msg}\n"
if secret_health_check_msg:
msg += f"Secret vars store: {secret_health_check_msg}"
raise ClanError(msg)
@@ -181,10 +181,10 @@ def run_generators(
flake = machines[0].flake
def get_generator_machine(generator: Generator) -> Machine:
if generator.machine is None:
# return first machine if generator is not tied to a specific one
if generator.share:
# return first machine if generator is shared
return machines[0]
return Machine(name=generator.machine, flake=flake)
return Machine(name=generator.machines[0], flake=flake)
# preheat the select cache, to reduce repeated calls during execution
selectors = []

View File

@@ -0,0 +1,71 @@
{ self, inputs, ... }:
{
perSystem =
{ pkgs, self', ... }:
let
# Simply evaluated options (JSON)
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
jsonDocs = pkgs.callPackage ./get-module-docs.nix {
inherit (self) clanModules;
clan-core = self;
inherit pkgs;
};
# clan service options
clanModulesViaService = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaService);
# Simply evaluated options (JSON)
renderOptions =
pkgs.runCommand "render-options"
{
# TODO: ruff does not splice properly in nativeBuildInputs
depsBuildBuild = [ pkgs.ruff ];
nativeBuildInputs = [
pkgs.python3
pkgs.mypy
self'.packages.clan-cli
];
}
''
install -D -m755 ${./generate}/__init__.py $out/bin/render-options
patchShebangs --build $out/bin/render-options
ruff format --check --diff $out/bin/render-options
ruff check --line-length 88 $out/bin/render-options
mypy --strict $out/bin/render-options
'';
module-docs =
pkgs.runCommand "rendered"
{
buildInputs = [
pkgs.python3
self'.packages.clan-cli
];
}
''
export CLAN_CORE_PATH=${
inputs.nixpkgs.lib.fileset.toSource {
root = ../..;
fileset = ../../clanModules;
}
}
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
# A file that contains the links to all clanModule docs
export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService}
export CLAN_SERVICE_INTERFACE=${self'.legacyPackages.clan-service-module-interface}/share/doc/nixos/options.json
export CLAN_OPTIONS_PATH=${self'.legacyPackages.clan-options}/share/doc/nixos/options.json
mkdir $out
# The python script will place mkDocs files in the output directory
exec python3 ${renderOptions}/bin/render-options
'';
in
{
packages = {
inherit module-docs;
};
};
}

View File

@@ -2,12 +2,14 @@
{
imports = [
./clan-cli/flake-module.nix
./clan-vm-manager/flake-module.nix
./installer/flake-module.nix
./icon-update/flake-module.nix
./clan-core-flake/flake-module.nix
./clan-app/flake-module.nix
./clan-cli/flake-module.nix
./clan-core-flake/flake-module.nix
./clan-vm-manager/flake-module.nix
./icon-update/flake-module.nix
./installer/flake-module.nix
./option-search/flake-module.nix
./docs-from-code/flake-module.nix
./testing/flake-module.nix
];

View File

@@ -24,7 +24,7 @@
serviceModules = self.clan.modules;
baseHref = "/options/";
baseHref = "/option-search/";
getRoles =
module:
@@ -118,7 +118,7 @@
_file = "docs flake-module";
imports = [
{ _module.args = { inherit clanLib; }; }
(import ../../../lib/modules/inventoryClass/roles-interface.nix {
(import ../../lib/modules/inventoryClass/roles-interface.nix {
nestedSettingsOption = mkOption {
type = types.raw;
description = ''
@@ -201,7 +201,7 @@
# };
packages = {
docs-options =
option-search =
if privateInputs ? nuschtos then
privateInputs.nuschtos.packages.${pkgs.stdenv.hostPlatform.system}.mkMultiSearch {
inherit baseHref;