Compare commits

..

74 Commits

Author SHA1 Message Date
Michael Hoang
05c7d885b6 DEBUG!!! clan-core-for-checks = self 2025-10-13 17:46:15 +02:00
Michael Hoang
6482094cb4 Revert "cli: fix installation test with latest release of nixos-anywhere"
This reverts commit 46f746d09c.
2025-10-13 17:45:51 +02:00
Michael Hoang
cbcfcd507d treewide: reformat 2025-10-13 17:45:51 +02:00
Michael Hoang
9b71f106f6 clanServices/coredns: fix evaluation on 25.05 2025-10-13 17:31:07 +02:00
Michael Hoang
1482bd571c Revert "syncthing: fix vars generator not working with latest Syncthing"
This reverts commit 1f9b44a4ad.
2025-10-13 17:24:49 +02:00
Michael Hoang
ec2537d088 formatter: drop sizelint as it is not available in 25.05 2025-10-13 17:24:49 +02:00
Michael Hoang
41229af93e treewide: use 25.05 2025-10-13 17:24:49 +02:00
Michael Hoang
7e7e58eb64 Merge pull request 'Update nixpkgs' (#5211) from update-nixpkgs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5211
2025-10-13 13:19:45 +00:00
Michael Hoang
46f746d09c cli: fix installation test with latest release of nixos-anywhere 2025-10-13 15:06:20 +02:00
clan-bot
56e03d1f25 Update nixpkgs 2025-10-13 14:51:00 +02:00
DavHau
dd783bdf85 Merge pull request 'vars/sops: stop writing on clan vars check' (#5490) from dave into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5490
2025-10-13 11:51:29 +00:00
DavHau
bf41a9ef00 vars/sops: stop writing on clan vars check
This fixes an issue where check_vars() would add machine keys or authorize machines for shared vars.

These write operations should only ever be done on a `clan vars generate`, which `clan vars check` should be a read-only operation
2025-10-13 18:43:49 +07:00
pinpox
f313ace19a Merge pull request 'Revert SSH docs' (#5488) from revert-ssh-docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5488
2025-10-13 10:56:54 +00:00
pinpox
fe8f7e919e Fix ssh docs 2025-10-13 12:51:42 +02:00
hsjobeki
c64276b64e Merge pull request 'lib: remove unused facts utils' (#5480) from fix-b into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5480
2025-10-13 10:06:42 +00:00
hsjobeki
436da16bf9 Merge pull request 'facts: add bigger migration warnings' (#5484) from fix-c into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5484
2025-10-13 08:11:38 +00: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
hsjobeki
3953715b48 Merge pull request 'clan-cli: remove unused test fixture' (#5482) from fix-c into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5482
2025-10-12 16:07:44 +00:00
Johannes Kirschbauer
7b95fa039f clan-cli: remove unused test fixture 2025-10-12 18:00:52 +02:00
Johannes Kirschbauer
347668a57f lib: remove unused facts utils 2025-10-12 17:49:05 +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
67 changed files with 606 additions and 749 deletions

View File

@@ -2,7 +2,6 @@
self,
lib,
inputs,
privateInputs ? { },
...
}:
let
@@ -19,28 +18,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
@@ -138,7 +128,7 @@ in
// flakeOutputs
// {
clan-core-for-checks = pkgs.runCommand "clan-core-for-checks" { } ''
cp -r ${privateInputs.clan-core-for-checks} $out
cp -r ${self} $out
chmod -R +w $out
cp ${../flake.lock} $out/flake.lock

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

View File

@@ -50,13 +50,13 @@
dns =
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.net-tools ];
environment.systemPackages = [ pkgs.nettools ];
};
client =
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.net-tools ];
environment.systemPackages = [ pkgs.nettools ];
};
server01 = {

View File

@@ -1,91 +1,39 @@
# Clan service: sshd
What it does
- Generates and persists SSH host keys via `vars`.
- Optionally issues CAsigned host certificates for servers.
- Installs the `server` CA public key into `clients` `known_hosts` for TOFUless verification.
The `sshd` Clan service manages SSH to make it easy to securely access your
machines over the internet. The service uses `vars` to store the SSH host keys
for each machine to ensure they remain stable across deployments.
`sshd` also generates SSH certificates for both servers and clients allowing for
certificate-based authentication for SSH.
When to use it
- ZeroTOFU SSH for dynamic fleets: admins/CI can connect to frequently rebuilt hosts (e.g., server-1.example.com) without prompts or perhost `known_hosts` churn.
The service also disables password-based authentication over SSH, to access your
machines you'll need to use public key authentication or certificate-based
authentication.
Roles
- Server: runs sshd, presents a CAsigned host certificate for `<machine>.<domain>`.
- Client: trusts the CA for the given domains to verify servers certificates.
Tip: assign both roles to a machine if it should both present a cert and verify others.
Quick start (with host certificates)
Useful if you never want to get a prompt about trusting the ssh fingerprint.
```nix
{
inventory.instances = {
sshd-with-certs = {
module = { name = "sshd"; input = "clan-core"; };
# Servers present certificates for <machine>.example.com
roles.server.tags.all = { };
roles.server.settings = {
certificate.searchDomains = [ "example.com" ];
# Optional: also add RSA host keys
# hostKeys.rsa.enable = true;
};
# Clients trust the CA for *.example.com
roles.client.tags.all = { };
roles.client.settings = {
certificate.searchDomains = [ "example.com" ];
};
};
};
}
```
Basic: only add persistent host keys (ed25519), no certificates
Useful if you want to get an ssh "trust this server" prompt once and then never again.
## Usage
```nix
{
inventory.instances = {
# By default this service only generates ed25519 host keys
sshd-basic = {
module = {
name = "sshd";
input = "clan-core";
};
roles.server.tags.all = { };
roles.client.tags.all = { };
};
};
}
```
Example: selective trust per environment
Admins should trust only production; CI should trust prod and staging. Servers are reachable under both domains.
```nix
{
inventory.instances = {
sshd-env-scoped = {
module = { name = "sshd"; input = "clan-core"; };
# Servers present certs for both prod and staging FQDNs
# Also generate RSA host keys for all servers
sshd-with-rsa = {
module = {
name = "sshd";
input = "clan-core";
};
roles.server.tags.all = { };
roles.server.settings = {
certificate.searchDomains = [ "prod.example.com" "staging.example.com" ];
};
# Admin laptop: trust prod only
roles.client.machines."admin-laptop".settings = {
certificate.searchDomains = [ "prod.example.com" ];
};
# CI runner: trust prod and staging
roles.client.machines."ci-runner-1".settings = {
certificate.searchDomains = [ "prod.example.com" "staging.example.com" ];
hostKeys.rsa.enable = true;
};
roles.client.tags.all = { };
};
};
}
```
- Admin -> server1.prod.example.com: zeroTOFU (verified via cert).
- Admin -> server1.staging.example.com: falls back to TOFU (or is blocked by policy).
- CI -> either prod or staging: zeroTOFU for both.
Note: server and client searchDomains dont have to be identical; they only need to overlap for the hostnames you actually use.
Notes
- Connect using a name that matches a cert principal (e.g., `server1.example.com`); wildcards are not allowed inside the certificate.
- CA private key stays in `vars` (not deployed); only the CA public key is distributed.
- Logins still require your user SSH keys on the server (passwords are disabled).

View File

@@ -11,9 +11,7 @@
pkgs.syncthing
];
script = ''
export TMPDIR=/tmp
TEMPORARY=$(mktemp -d)
syncthing generate --config "$out" --data "$TEMPORARY"
syncthing generate --config "$out"
mv "$out"/key.pem "$out"/key
mv "$out"/cert.pem "$out"/cert
cat "$out"/config.xml | grep -oP '(?<=<device id=")[^"]+' | uniq > "$out"/id

24
devFlake/flake.lock generated
View File

@@ -3,16 +3,16 @@
"clan-core-for-checks": {
"flake": false,
"locked": {
"lastModified": 1760000589,
"narHash": "sha256-9xBwxeb8x5XOo3alaJvv2ZwL7UhW3/oYUUBK+odWGrk=",
"ref": "main",
"rev": "e2f20b5ffcd4ff59e2528d29649056e3eb8d22bb",
"lastModified": 1760368011,
"narHash": "sha256-mLK2nwbfklfOGIVAKVNDwGyYz8mPh4fzsAqSK3BlCiI=",
"ref": "clan-25.05",
"rev": "1b3c129aa9741d99b27810652ca888b3fbfc3a11",
"shallow": true,
"type": "git",
"url": "https://git.clan.lol/clan/clan-core"
},
"original": {
"ref": "main",
"ref": "clan-25.05",
"shallow": true,
"type": "git",
"url": "https://git.clan.lol/clan/clan-core"
@@ -105,16 +105,16 @@
},
"nixpkgs-dev": {
"locked": {
"lastModified": 1759989671,
"narHash": "sha256-3Wk0I5TYsd7cyIO8vYGxjOuQ8zraZEUFZqEhSSIhQLs=",
"lastModified": 1760309387,
"narHash": "sha256-e0lvQ7+B1Y8zjykYHAj9tBv10ggLqK0nmxwvMU3J0Eo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "837076de579c67aa0c2ce2ab49948b24d907d449",
"rev": "6cd95994a9c8f7c6f8c1f1161be94119afdcb305",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable-small",
"ref": "nixos-25.05-small",
"repo": "nixpkgs",
"type": "github"
}
@@ -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": {

View File

@@ -2,7 +2,7 @@
description = "private dev inputs";
# Dev dependencies
inputs.nixpkgs-dev.url = "github:NixOS/nixpkgs/nixos-unstable-small";
inputs.nixpkgs-dev.url = "github:NixOS/nixpkgs/nixos-25.05-small";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.flake-utils.inputs.systems.follows = "systems";
@@ -15,7 +15,7 @@
inputs.systems.url = "github:nix-systems/default";
inputs.clan-core-for-checks.url = "git+https://git.clan.lol/clan/clan-core?ref=main&shallow=1";
inputs.clan-core-for-checks.url = "git+https://git.clan.lol/clan/clan-core?ref=clan-25.05&shallow=1";
inputs.clan-core-for-checks.flake = false;
inputs.test-fixtures.url = "git+https://git.clan.lol/clan/test-fixtures";

View File

@@ -70,8 +70,6 @@ hide:
.clamp-toggle:checked ~ .clamp-more::after { content: "Read less"; }
</style>
trivial change
<div class="clamp-wrap" style="--lines: 3;">
<input type="checkbox" id="clan-readmore" class="clamp-toggle" />
<div class="clamp-content">

23
flake.lock generated
View File

@@ -71,15 +71,16 @@
]
},
"locked": {
"lastModified": 1758805352,
"narHash": "sha256-BHdc43Lkayd+72W/NXRKHzX5AZ+28F3xaUs3a88/Uew=",
"lastModified": 1759509947,
"narHash": "sha256-4XifSIHfpJKcCf5bZZRhj8C4aCpjNBaE3kXr02s4rHU=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "c48e963a5558eb1c3827d59d21c5193622a1477c",
"rev": "000eadb231812ad6ea6aebd7526974aaf4e79355",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"ref": "nix-darwin-25.05",
"repo": "nix-darwin",
"type": "github"
}
@@ -114,15 +115,15 @@
},
"nixpkgs": {
"locked": {
"lastModified": 315532800,
"narHash": "sha256-1tUpklZsKzMGI3gjo/dWD+hS8cf+5Jji8TF5Cfz7i3I=",
"rev": "08b8f92ac6354983f5382124fef6006cade4a1c1",
"lastModified": 1760324802,
"narHash": "sha256-VWlJtLQ5EQQj45Wj0yTExtSjwRyZ59/qMqEwus/Exlg=",
"rev": "7e297ddff44a3cc93673bb38d0374df8d0ad73e4",
"type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre862603.08b8f92ac635/nixexprs.tar.xz"
"url": "https://releases.nixos.org/nixos/25.05/nixos-25.05.811135.7e297ddff44a/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz"
"url": "https://nixos.org/channels/nixos-25.05/nixexprs.tar.xz"
}
},
"root": {
@@ -181,11 +182,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

@@ -2,9 +2,9 @@
description = "clan.lol base operating system";
inputs = {
nixpkgs.url = "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz";
nixpkgs.url = "https://nixos.org/channels/nixos-25.05/nixexprs.tar.xz";
nix-darwin.url = "github:nix-darwin/nix-darwin";
nix-darwin.url = "github:nix-darwin/nix-darwin/nix-darwin-25.05";
nix-darwin.inputs.nixpkgs.follows = "nixpkgs";
flake-parts.url = "github:hercules-ci/flake-parts";

View File

@@ -11,8 +11,6 @@
treefmt.programs.nixfmt.enable = true;
treefmt.programs.nixfmt.package = pkgs.nixfmt-rfc-style;
treefmt.programs.deadnix.enable = true;
treefmt.programs.sizelint.enable = true;
treefmt.programs.sizelint.failOnWarn = true;
treefmt.programs.clang-format.enable = true;
treefmt.settings.global.excludes = [
"*.png"

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

@@ -28,7 +28,6 @@ lib.fix (
# Plain imports.
introspection = import ./introspection { inherit lib; };
jsonschema = import ./jsonschema { inherit lib; };
facts = import ./facts.nix { inherit lib; };
docs = import ./docs.nix { inherit lib; };
# flakes
@@ -36,6 +35,10 @@ lib.fix (
# TODO: Flatten our lib functions like this:
resolveModule = clanLib.callLib ./resolve-module { };
fs = {
inherit (builtins) pathExists readDir;
};
};
in
f

View File

@@ -1,71 +0,0 @@
{ lib, ... }:
clanDir:
let
allMachineNames = lib.mapAttrsToList (name: _: name) (builtins.readDir clanDir);
getFactPath = machine: fact: "${clanDir}/machines/${machine}/facts/${fact}";
readFact =
machine: fact:
let
path = getFactPath machine fact;
in
if builtins.pathExists path then builtins.readFile path else null;
# Example:
#
# readFactFromAllMachines zerotier-ip
# => {
# machineA = "1.2.3.4";
# machineB = "5.6.7.8";
# };
readFactFromAllMachines =
fact:
let
machines = allMachineNames;
facts = lib.genAttrs machines (machine: readFact machine fact);
filteredFacts = lib.filterAttrs (_machine: fact: fact != null) facts;
in
filteredFacts;
# all given facts are are set and factvalues are never null.
#
# Example:
#
# readFactsFromAllMachines [ "zerotier-ip" "syncthing.pub" ]
# => {
# machineA =
# {
# "zerotier-ip" = "1.2.3.4";
# "synching.pub" = "1234";
# };
# machineB =
# {
# "zerotier-ip" = "5.6.7.8";
# "synching.pub" = "23456719";
# };
# };
readFactsFromAllMachines =
facts:
let
# machine -> fact -> factvalue
machinesFactsAttrs = lib.genAttrs allMachineNames (
machine: lib.genAttrs facts (fact: readFact machine fact)
);
# remove all machines which don't have all facts set
filteredMachineFactAttrs = lib.filterAttrs (
_machine: values: builtins.all (fact: values.${fact} != null) facts
) machinesFactsAttrs;
in
filteredMachineFactAttrs;
in
{
inherit
allMachineNames
getFactPath
readFact
readFactFromAllMachines
readFactsFromAllMachines
;
}

View File

@@ -1,8 +1,7 @@
# tests for the nixos options to jsonschema converter
# run these tests via `nix-unit ./test.nix`
{
lib ? import /home/johannes/git/nixpkgs/lib,
# lib ? (import <nixpkgs> { }).lib,
lib ? (import <nixpkgs> { }).lib,
slib ? (import ./. { inherit lib; }),
}:
let
@@ -68,38 +67,31 @@ in
};
};
};
test_no_default =
let
configuration = (
eval [
{
options.foo.bar = lib.mkOption {
type = lib.types.bool;
};
}
]
);
in
{
inherit configuration;
expr = stableView (
slib.getPrios {
options = configuration.options;
}
);
expected = {
foo = {
bar = {
__this = {
files = [ ];
prio = 9999;
total = false;
};
test_no_default = {
expr = stableView (
slib.getPrios {
options =
(eval [
{
options.foo.bar = lib.mkOption {
type = lib.types.bool;
};
}
]).options;
}
);
expected = {
foo = {
bar = {
__this = {
files = [ ];
prio = 9999;
total = false;
};
};
};
};
};
test_submodule = {
expr = stableView (

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

@@ -5,33 +5,31 @@
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
relevantFiles = filterAttrs (
_name: f: f.secret && f.deploy && (f.neededFor == "users" || f.neededFor == "services")
);
collectFiles =
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
);
in
allFiles
collectFiles

View File

@@ -41,7 +41,7 @@ class ApiBridge(Protocol):
def process_request(self, request: BackendRequest) -> None:
"""Process an API request through the middleware chain."""
from clan_app.middleware.base import MiddlewareContext # noqa: PLC0415
from clan_app.middleware.base import MiddlewareContext
with ExitStack() as stack:
# Capture the current call stack up to this point
@@ -62,7 +62,7 @@ class ApiBridge(Protocol):
)
middleware.process(context)
except Exception as e:
from clan_app.middleware.base import ( # noqa: PLC0415
from clan_app.middleware.base import (
MiddlewareError,
)

View File

@@ -191,13 +191,13 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
return file_data
def do_OPTIONS(self) -> None:
def do_OPTIONS(self) -> None: # noqa: N802
"""Handle CORS preflight requests."""
self.send_response_only(200)
self._send_cors_headers()
self.end_headers()
def do_GET(self) -> None:
def do_GET(self) -> None: # noqa: N802
"""Handle GET requests."""
parsed_url = urlparse(self.path)
path = parsed_url.path
@@ -211,7 +211,7 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
else:
self.send_api_error_response("info", "Not Found", ["http_bridge", "GET"])
def do_POST(self) -> None:
def do_POST(self) -> None: # noqa: N802
"""Handle POST requests."""
parsed_url = urlparse(self.path)
path = parsed_url.path

View File

@@ -34,7 +34,7 @@ class WebviewBridge(ApiBridge):
log.debug(f"Sending response: {serialized}")
# Import FuncStatus locally to avoid circular import
from .webview import FuncStatus # noqa: PLC0415
from .webview import FuncStatus
self.webview.return_(response._op_key, FuncStatus.SUCCESS, serialized) # noqa: SLF001

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

@@ -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

@@ -9,7 +9,7 @@ def main() -> None:
load_in_all_api_functions()
# import lazily since we otherwise we do not have all api functions loaded according to Qubasa
from clan_lib.api import API # noqa: PLC0415
from clan_lib.api import API
schema = API.to_json_schema()
print(f"""{json.dumps(schema, indent=2)}""")

View File

@@ -102,7 +102,7 @@ class TestFlake(Flake):
opts: "ListOptions | None" = None, # noqa: ARG002
) -> "dict[str, MachineResponse]":
"""List machines of a clan"""
from clan_lib.machines.actions import ( # noqa: PLC0415
from clan_lib.machines.actions import (
InventoryMachine,
MachineResponse,
)

View File

@@ -231,7 +231,7 @@ def remove_machine_command(args: argparse.Namespace) -> None:
def add_group_argument(parser: argparse.ArgumentParser) -> None:
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_groups,
)
@@ -334,7 +334,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machines to add",
type=machine_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_machines,
)
@@ -353,7 +353,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machines to remove",
type=machine_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_machines,
)
@@ -369,7 +369,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user to add",
type=user_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_users,
)
@@ -388,7 +388,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user to remove",
type=user_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_users,
)
@@ -407,7 +407,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the secret",
type=secret_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_secrets,
)
@@ -426,7 +426,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the secret",
type=secret_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_secrets,
)

View File

@@ -69,7 +69,7 @@ def register_import_sops_parser(parser: argparse.ArgumentParser) -> None:
default=[],
help="the group to import the secrets to",
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_groups,
)
@@ -82,7 +82,7 @@ def register_import_sops_parser(parser: argparse.ArgumentParser) -> None:
default=[],
help="the machine to import the secrets to",
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_machines,
)
@@ -95,7 +95,7 @@ def register_import_sops_parser(parser: argparse.ArgumentParser) -> None:
default=[],
help="the user to import the secrets to",
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_users,
)

View File

@@ -172,7 +172,7 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machine",
type=machine_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_machines,
)
@@ -192,7 +192,7 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machine",
type=machine_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_machines,
)
@@ -207,7 +207,7 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machine",
type=machine_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_machines,
)
@@ -225,7 +225,7 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machine",
type=machine_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_machines,
complete_secrets,
@@ -250,7 +250,7 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machine",
type=machine_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_machines,
complete_secrets,

View File

@@ -255,7 +255,7 @@ def add_secret_argument(parser: argparse.ArgumentParser, autocomplete: bool) ->
type=secret_name_type,
)
if autocomplete:
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_secrets,
)
@@ -467,7 +467,7 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
default=[],
help="the group to import the secrets to (can be repeated)",
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_groups,
)
@@ -480,7 +480,7 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
default=[],
help="the machine to import the secrets to (can be repeated)",
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_machines,
)
@@ -493,7 +493,7 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
default=[],
help="the user to import the secrets to (can be repeated)",
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_users,
)

View File

@@ -281,7 +281,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user",
type=user_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_users,
)
@@ -295,7 +295,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user",
type=user_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_users,
)
@@ -312,7 +312,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user",
type=user_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_secrets,
complete_users,
@@ -336,7 +336,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the group",
type=user_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_secrets,
complete_users,
@@ -360,7 +360,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user",
type=user_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_users,
)
@@ -378,7 +378,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user",
type=user_name_type,
)
from clan_cli.completions import ( # noqa: PLC0415
from clan_cli.completions import (
add_dynamic_completer,
complete_users,
)

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

@@ -1,5 +1,6 @@
import json
import logging
import os
import shutil
import subprocess
import time
@@ -429,9 +430,43 @@ def test_generated_shared_secret_sops(
machine1 = Machine(name="machine1", flake=Flake(str(flake.path)))
machine2 = Machine(name="machine2", flake=Flake(str(flake.path)))
cli.run(["vars", "generate", "--flake", str(flake.path), "machine1"])
assert check_vars(machine1.name, machine1.flake)
# Get the initial state of the flake directory after generation
def get_file_mtimes(path: str) -> dict[str, float]:
"""Get modification times of all files in a directory tree."""
mtimes = {}
for root, _dirs, files in os.walk(path):
# Skip .git directory
if ".git" in root:
continue
for file in files:
filepath = Path(root) / file
mtimes[str(filepath)] = filepath.stat().st_mtime
return mtimes
initial_mtimes = get_file_mtimes(str(flake.path))
# First check_vars should not write anything
assert check_vars(machine1.name, machine1.flake), (
"machine1 has already generated vars, so check_vars should return True\n"
f"Check result:\n{check_vars(machine1.name, machine1.flake)}"
)
# Verify no files were modified
after_check_mtimes = get_file_mtimes(str(flake.path))
assert initial_mtimes == after_check_mtimes, (
"check_vars should not modify any files when vars are already valid"
)
assert not check_vars(machine2.name, machine2.flake), (
"machine2 has not generated vars yet, so check_vars should return False"
)
# Verify no files were modified
after_check_mtimes_2 = get_file_mtimes(str(flake.path))
assert initial_mtimes == after_check_mtimes_2, (
"check_vars should not modify any files when vars are not valid"
)
cli.run(["vars", "generate", "--flake", str(flake.path), "machine2"])
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

View File

@@ -171,7 +171,7 @@ class StoreBase(ABC):
if generator.share:
log_info = log.info
else:
from clan_lib.machines.machines import Machine # noqa: PLC0415
from clan_lib.machines.machines import Machine
machine_obj = Machine(name=generator.machines[0], flake=self.flake)
log_info = machine_obj.info

View File

@@ -3,6 +3,7 @@ import logging
from typing import TYPE_CHECKING
from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.vars.secret_modules import sops
from clan_lib.errors import ClanError
from clan_lib.flake import Flake, require_flake
from clan_lib.machines.machines import Machine
@@ -26,13 +27,33 @@ class VarStatus:
self.unfixed_secret_vars = unfixed_secret_vars
self.invalid_generators = invalid_generators
def text(self) -> str:
log = ""
if self.missing_secret_vars:
log += "Missing secret vars:\n"
for var in self.missing_secret_vars:
log += f" - {var.id}\n"
if self.missing_public_vars:
log += "Missing public vars:\n"
for var in self.missing_public_vars:
log += f" - {var.id}\n"
if self.unfixed_secret_vars:
log += "Unfixed secret vars:\n"
for var in self.unfixed_secret_vars:
log += f" - {var.id}\n"
if self.invalid_generators:
log += "Invalid generators (outdated invalidation hash):\n"
for gen in self.invalid_generators:
log += f" - {gen}\n"
return log if log else "All vars are present and valid."
def vars_status(
machine_name: str,
flake: Flake,
generator_name: None | str = None,
) -> VarStatus:
from clan_cli.vars.generator import Generator # noqa: PLC0415
from clan_cli.vars.generator import Generator
machine = Machine(name=machine_name, flake=flake)
missing_secret_vars = []
@@ -66,15 +87,32 @@ def vars_status(
f"Secret var '{file.name}' for service '{generator.name}' in machine {machine.name} is missing.",
)
missing_secret_vars.append(file)
if (
isinstance(machine.secret_vars_store, sops.SecretStore)
and generator.share
and file.exists
and not machine.secret_vars_store.machine_has_access(
generator=generator,
secret_name=file.name,
machine=machine.name,
)
):
msg = (
f"Secret var '{generator.name}/{file.name}' is marked for deployment to machine '{machine.name}', but the machine does not have access to it.\n"
f"Run 'clan vars generate {machine.name}' to fix this.\n"
)
machine.info(msg)
missing_secret_vars.append(file)
else:
msg = machine.secret_vars_store.health_check(
health_msg = machine.secret_vars_store.health_check(
machine=machine.name,
generators=[generator],
file_name=file.name,
)
if msg:
if health_msg is not None:
machine.info(
f"Secret var '{file.name}' for service '{generator.name}' in machine {machine.name} needs update: {msg}",
f"Secret var '{file.name}' for service '{generator.name}' in machine {machine.name} needs update: {health_msg}",
)
unfixed_secret_vars.append(file)
@@ -106,6 +144,7 @@ def check_vars(
generator_name: None | str = None,
) -> bool:
status = vars_status(machine_name, flake, generator_name=generator_name)
log.info(f"Check results for machine '{machine_name}': \n{status.text()}")
return not (
status.missing_secret_vars
or status.missing_public_vars

View File

@@ -259,6 +259,10 @@ class Generator:
_secret_store=sec_store,
)
# link generator to its files
for file in files:
file.generator(generator)
if share:
# For shared generators, check if we already created it
existing = next(
@@ -478,7 +482,7 @@ class Generator:
if sys.platform == "linux" and bwrap.bubblewrap_works():
cmd = bubblewrap_cmd(str(final_script), tmpdir)
elif sys.platform == "darwin":
from clan_lib.sandbox_exec import sandbox_exec_cmd # noqa: PLC0415
from clan_lib.sandbox_exec import sandbox_exec_cmd
cmd = stack.enter_context(sandbox_exec_cmd(str(final_script), tmpdir))
else:

View File

@@ -54,7 +54,7 @@ class SecretStore(StoreBase):
def ensure_machine_key(self, machine: str) -> None:
"""Ensure machine has sops keys initialized."""
# no need to generate keys if we don't manage secrets
from clan_cli.vars.generator import Generator # noqa: PLC0415
from clan_cli.vars.generator import Generator
vars_generators = Generator.get_machine_generators([machine], self.flake)
if not vars_generators:
@@ -98,7 +98,8 @@ class SecretStore(StoreBase):
def machine_has_access(
self, generator: Generator, secret_name: str, machine: str
) -> bool:
self.ensure_machine_key(machine)
if not has_machine(self.flake.path, machine):
return False
key_dir = sops_machines_folder(self.flake.path) / machine
return self.key_has_access(key_dir, generator, secret_name)
@@ -142,7 +143,7 @@ class SecretStore(StoreBase):
"""
if generators is None:
from clan_cli.vars.generator import Generator # noqa: PLC0415
from clan_cli.vars.generator import Generator
generators = Generator.get_machine_generators([machine], self.flake)
file_found = False
@@ -156,8 +157,6 @@ class SecretStore(StoreBase):
else:
continue
if file.secret and self.exists(generator, file.name):
if file.deploy:
self.ensure_machine_has_access(generator, file.name, machine)
needs_update, msg = self.needs_fix(generator, file.name, machine)
if needs_update:
outdated.append((generator.name, file.name, msg))
@@ -219,7 +218,7 @@ class SecretStore(StoreBase):
return [store_folder]
def populate_dir(self, machine: str, output_dir: Path, phases: list[str]) -> None:
from clan_cli.vars.generator import Generator # noqa: PLC0415
from clan_cli.vars.generator import Generator
vars_generators = Generator.get_machine_generators([machine], self.flake)
if "users" in phases or "services" in phases:
@@ -283,6 +282,7 @@ class SecretStore(StoreBase):
) -> None:
if self.machine_has_access(generator, name, machine):
return
self.ensure_machine_key(machine)
secret_folder = self.secret_path(generator, name)
add_secret(
self.flake.path,
@@ -292,7 +292,7 @@ class SecretStore(StoreBase):
)
def collect_keys_for_secret(self, machine: str, path: Path) -> set[sops.SopsKey]:
from clan_cli.secrets.secrets import ( # noqa: PLC0415
from clan_cli.secrets.secrets import (
collect_keys_for_path,
collect_keys_for_type,
)
@@ -354,10 +354,10 @@ class SecretStore(StoreBase):
ClanError: If the specified file_name is not found
"""
from clan_cli.secrets.secrets import update_keys # noqa: PLC0415
from clan_cli.secrets.secrets import update_keys
if generators is None:
from clan_cli.vars.generator import Generator # noqa: PLC0415
from clan_cli.vars.generator import Generator
generators = Generator.get_machine_generators([machine], self.flake)
file_found = False

View File

@@ -319,9 +319,9 @@ def load_in_all_api_functions() -> None:
We have to make sure python loads every wrapped function at least once.
This is done by importing all modules from the clan_lib and clan_cli packages.
"""
import clan_cli # noqa: PLC0415 # Avoid circular imports - many modules import from clan_lib.api
import clan_cli # Avoid circular imports - many modules import from clan_lib.api
import clan_lib # noqa: PLC0415 # Avoid circular imports - many modules import from clan_lib.api
import clan_lib # Avoid circular imports - many modules import from clan_lib.api
import_all_modules_from_package(clan_lib)
import_all_modules_from_package(clan_cli)

View File

@@ -88,7 +88,7 @@ def list_system_storage_devices() -> Blockdevices:
A list of detected block devices with metadata like size, path, type, etc.
"""
from clan_lib.nix import nix_shell # noqa: PLC0415
from clan_lib.nix import nix_shell
cmd = nix_shell(
["util-linux"],
@@ -124,7 +124,7 @@ def get_clan_directory_relative(flake: Flake) -> str:
ClanError: If the flake evaluation fails or directories cannot be found
"""
from clan_lib.dirs import get_clan_directories # noqa: PLC0415
from clan_lib.dirs import get_clan_directories
_, relative_dir = get_clan_directories(flake)
return relative_dir

View File

@@ -1162,7 +1162,7 @@ class Flake:
opts: "ListOptions | None" = None,
) -> "dict[str, MachineResponse]":
"""List machines of a clan"""
from clan_lib.machines.actions import list_machines # noqa: PLC0415
from clan_lib.machines.actions import list_machines
return list_machines(self, opts)

View File

@@ -18,14 +18,14 @@ def locked_open(filename: Path, mode: str = "r") -> Generator:
def write_history_file(data: Any) -> None:
from clan_lib.dirs import user_history_file # noqa: PLC0415
from clan_lib.dirs import user_history_file
with locked_open(user_history_file(), "w+") as f:
f.write(json.dumps(data, cls=ClanJSONEncoder, indent=4))
def read_history_file() -> list[dict]:
from clan_lib.dirs import user_history_file # noqa: PLC0415
from clan_lib.dirs import user_history_file
with locked_open(user_history_file(), "r") as f:
content: str = f.read()

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

@@ -33,7 +33,7 @@ class Machine:
def get_inv_machine(self) -> "InventoryMachine":
# Import on demand to avoid circular imports
from clan_lib.machines.actions import get_machine # noqa: PLC0415
from clan_lib.machines.actions import get_machine
return get_machine(self.flake, self.name)
@@ -95,7 +95,7 @@ class Machine:
@cached_property
def secret_vars_store(self) -> StoreBase:
from clan_cli.vars.secret_modules import password_store # noqa: PLC0415
from clan_cli.vars.secret_modules import password_store
secret_module = self.select("config.clan.core.vars.settings.secretModule")
module = importlib.import_module(secret_module)
@@ -126,7 +126,7 @@ class Machine:
return self.flake.path
def target_host(self) -> Remote:
from clan_lib.network.network import get_best_remote # noqa: PLC0415
from clan_lib.network.network import get_best_remote
with get_best_remote(self) as remote:
return remote

View File

@@ -42,7 +42,7 @@ def _suggest_similar_names(
def get_available_machines(flake: Flake) -> list[str]:
from clan_lib.machines.list import list_machines # noqa: PLC0415
from clan_lib.machines.list import list_machines
machines = list_machines(flake)
return list(machines.keys())

View File

@@ -34,7 +34,7 @@ class Peer:
_var: dict[str, str] = self._host["var"]
machine_name = _var["machine"]
generator = _var["generator"]
from clan_lib.machines.machines import Machine # noqa: PLC0415
from clan_lib.machines.machines import Machine
machine = Machine(name=machine_name, flake=self.flake)
var = get_machine_var(

View File

@@ -88,7 +88,7 @@ def nix_eval(flags: list[str]) -> list[str]:
],
)
if os.environ.get("IN_NIX_SANDBOX"):
from clan_lib.dirs import nixpkgs_source # noqa: PLC0415
from clan_lib.dirs import nixpkgs_source
return [
*default_flags,
@@ -169,7 +169,7 @@ def nix_shell(packages: list[str], cmd: list[str]) -> list[str]:
if not missing_packages:
return cmd
from clan_lib.dirs import nixpkgs_flake # noqa: PLC0415
from clan_lib.dirs import nixpkgs_flake
return [
*nix_command(["shell", "--inputs-from", f"{nixpkgs_flake()!s}"]),

View File

@@ -1,53 +0,0 @@
let
lib = import /home/johannes/git/nixpkgs/lib;
clanLib = import ../../../../lib { inherit lib; };
inherit (lib) evalModules mkOption types;
eval = evalModules {
modules = [
{
options.foos = mkOption {
type = types.attrsOf (
types.submodule {
options.bar = mkOption { };
}
);
};
# config.foos = lib.mkForce { this.bar = 42; };
config.instances.a = { };
# config.foo = lib.mkForce {
# bar = 42;
# };
}
{
_file = "inventory.json";
# instances.a = { setting = };
}
# {
# options.foo = mkOption {
# type = types.attrsOf (types.attrsOf (types.submoduleWith { modules = [
# {
# options.bar = mkOption {};
# }
# ]; }));
# default = { bar = { }; };
# };
# }
# {
# _file = "static.nix";
# foo.static.thing = { bar = 1; }; # <- Can: Op.Modify
# }
# {
# _file = "inventory.json";
# foo.managed.thing = { bar = 1; }; # <- Can: Op.Delete, Op.Modify
# #
# }
];
};
in
{
inherit clanLib eval;
}

View File

@@ -1,11 +1,7 @@
from enum import Enum
from typing import Any, TypedDict
from clan_lib.errors import ClanError
from clan_lib.persist.path_utils import (
PathTuple,
path_to_string,
)
from clan_lib.persist.path_utils import PathTuple, path_to_string
WRITABLE_PRIORITY_THRESHOLD = 100 # Values below this are not writeable
@@ -193,69 +189,3 @@ def compute_write_map(
"""
return _determine_writeability_recursive(priorities, all_values, persisted)
class RawAttributes(TypedDict):
headType: str
nullable: bool
prio: int
total: bool
files: list[str]
class OpType(Enum):
MODIFY = "modify"
DELETE = "delete"
def transform_attribute_properties(
introspection: dict[str, Any],
all_values: dict[str, Any],
persisted: dict[str, Any],
# Passthrough for recursion
curr_path: PathTuple = (),
parent_attributes: RawAttributes | None = None,
) -> dict[PathTuple, set[OpType]]:
"""Transform attribute properties to ensure correct types and defaults."""
results: dict[PathTuple, set[OpType]] = {}
for key, key_meta in introspection.items():
if key in {"__this", "__list"}:
continue
path = (*curr_path, key)
results[path] = set()
local_attributes: RawAttributes = key_meta.get("__this")
key_priority = local_attributes["prio"] or None
effective_priority = key_priority or (
parent_attributes["prio"] if parent_attributes else None
)
if effective_priority is None:
msg = f"Priority for path '{path_to_string(path)}' is not defined and no parent to inherit from. Cannot determine effective priority."
raise ClanError(msg)
if isinstance(key_meta, dict):
subattrs = transform_attribute_properties(
key_meta,
all_values.get(key, {}),
persisted.get(key, {}),
curr_path=path,
parent_attributes=local_attributes,
)
results.update(dict(subattrs.items()))
return results
# Only defined in inventory.json -> We might be able to delete it, because we defined it.
# But we could also have some option default somewhere else, so we cannot be sure.
# if all(f.endswith("inventory.json") for f in raw_attributes["files"]):
# operations.add(OpType.DELETE)
# if (
# raw_attributes["prio"] >= WRITABLE_PRIORITY_THRESHOLD
# or ".json" in raw_attributes["files"]
# ):
# operations.add(OpType.MODIFY)

View File

@@ -5,110 +5,11 @@ import pytest
from clan_lib.flake.flake import Flake
from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.write_rules import (
compute_write_map,
transform_attribute_properties,
)
from clan_lib.persist.write_rules import compute_write_map
if TYPE_CHECKING:
from clan_lib.nix_models.clan import Clan
# foos.this = lib.mkForce { bar = 42; };
# ->
# {
# foos = {
# __this = {
# files = [
# "inventory.json"
# "<unknown-file>"
# ];
# headType = "attrsOf";
# nullable = false;
# prio = 100;
# total = false;
# };
# this = {
# __this = {
# files = [ "<unknown-file>" ];
# headType = "submodule";
# nullable = false;
# prio = 50;
# total = true;
# };
# bar = {
# __this = {
# files = [ "<unknown-file>" ];
# headType = "unspecified";
# nullable = false;
# prio = 100;
# total = false;
# };
# };
# };
# };
# }
def test_write_new() -> None:
all_data: dict = {"foo": {"bar": 42}}
persisted_data: dict = {}
introspection: dict = {
"foo": {
"__this": {
"files": ["/dir/file.nix"],
"headType": "unspecified",
"nullable": False,
"prio": 100, # <- default prio
"total": False,
},
"bar": {
"__this": {
"files": ["/dir/file.nix"],
"headType": "int",
"nullable": False,
"prio": 100, # <- default prio
"total": False,
}
},
}
}
res = transform_attribute_properties(introspection, all_data, persisted_data)
breakpoint()
# No operations allowed, because mkForce
# We cannot modify this value in ANY possible way.
# inventory.json definitions and children definition are filtered out by the module system
# assert attributes == {"operations": set(), "path": ["foo", "bar"]}
# normal_prio_attrs: RawAttributes = {
# "files": ["/dir/file.nix"],
# "headType": "attrsOf",
# "nullable": False,
# "prio": 100, # <- default prio
# "total": False,
# }
# attributes = transform_attribute_properties(("foo", "bar"), normal_prio_attrs)
# # We can modify this value, because its a normal prio
# # This means keys can be added/removed/changed respecting their individual local constraints
# assert attributes == {"operations": { OpType.MODIFY }, "path": ["foo", "bar"]}
# default_prio_attrs: RawAttributes = {
# "files": ["/dir/file.nix"],
# "headType": "attrsOf",
# "nullable": False,
# "prio": 100, # <- default prio
# "total": False,
# }
# attributes = transform_attribute_properties(("foo", "bar"), default_prio_attrs)
# # We can modify this value, because its a normal prio
# # This means keys can be added/removed/changed respecting their individual local constraints
# assert attributes == {"operations": { OpType.MODIFY, OpType.DELETE }, "path": ["foo", "bar"]}
# Integration test
@pytest.mark.with_core

View File

@@ -464,12 +464,12 @@ class Remote:
self,
opts: "ConnectionOptions | None" = None,
) -> None:
from clan_lib.network.check import check_machine_ssh_reachable # noqa: PLC0415
from clan_lib.network.check import check_machine_ssh_reachable
return check_machine_ssh_reachable(self, opts)
def check_machine_ssh_login(self) -> None:
from clan_lib.network.check import check_machine_ssh_login # noqa: PLC0415
from clan_lib.network.check import check_machine_ssh_login
return check_machine_ssh_login(self)

View File

@@ -5,6 +5,7 @@ from clan_cli.vars import graph
from clan_cli.vars.generator import Generator
from clan_cli.vars.graph import requested_closure
from clan_cli.vars.migration import check_can_migrate, migrate_files
from clan_cli.vars.secret_modules import sops
from clan_lib.api import API
from clan_lib.errors import ClanError
@@ -152,15 +153,15 @@ def run_generators(
if not machines:
msg = "At least one machine must be provided"
raise ClanError(msg)
all_generators = get_generators(machines, full_closure=True)
if isinstance(generators, list):
# List of generator names - use them exactly as provided
if len(generators) == 0:
return
all_generators = get_generators(machines, full_closure=True)
generator_objects = [g for g in all_generators if g.key.name in generators]
generators_to_run = [g for g in all_generators if g.key.name in generators]
else:
# None or single string - use get_generators with closure parameter
generator_objects = get_generators(
generators_to_run = get_generators(
machines,
full_closure=full_closure,
generator_name=generators,
@@ -170,13 +171,30 @@ def run_generators(
# TODO: make this more lazy and ask for every generator on execution
if callable(prompt_values):
prompt_values = {
generator.name: prompt_values(generator) for generator in generator_objects
generator.name: prompt_values(generator) for generator in generators_to_run
}
# execute health check
for machine in machines:
_ensure_healthy(machine=machine)
# ensure all selected machines have access to all selected shared generators
for machine in machines:
# This is only relevant for the sops store
# TODO: improve store abstraction to use Protocols and introduce a proper SecretStore interface
if not isinstance(machine.secret_vars_store, sops.SecretStore):
continue
for generator in all_generators:
if generator.share:
for file in generator.files:
if not file.secret or not file.exists:
continue
machine.secret_vars_store.ensure_machine_has_access(
generator,
file.name,
machine.name,
)
# get the flake via any machine (they are all the same)
flake = machines[0].flake
@@ -188,13 +206,13 @@ def run_generators(
# preheat the select cache, to reduce repeated calls during execution
selectors = []
for generator in generator_objects:
for generator in generators_to_run:
machine = get_generator_machine(generator)
selectors.append(generator.final_script_selector(machine.name))
flake.precache(selectors)
# execute generators
for generator in generator_objects:
for generator in generators_to_run:
machine = get_generator_machine(generator)
if check_can_migrate(machine, generator):
migrate_files(machine, generator)

View File

@@ -290,9 +290,7 @@ def collect_commands() -> list[Category]:
# 3. sort by title alphabetically
return (c.title.split(" ")[0], c.title, weight)
result = sorted(result, key=weight_cmd_groups)
return result
return sorted(result, key=weight_cmd_groups)
def build_command_reference() -> None:

View File

@@ -36,7 +36,7 @@ class MPProcess:
def _set_proc_name(name: str) -> None:
if sys.platform != "linux":
return
import ctypes # noqa: PLC0415
import ctypes
# Define the prctl function with the appropriate arguments and return type
libc = ctypes.CDLL("libc.so.6")

View File

@@ -759,12 +759,12 @@ class Win32Implementation(BaseImplementation):
SM_CXSMICON = 49
if sys.platform == "win32":
from ctypes import Structure # noqa: PLC0415
from ctypes import Structure
class WNDCLASSW(Structure):
"""Windows class structure for window registration."""
from ctypes import CFUNCTYPE, wintypes # noqa: PLC0415
from ctypes import CFUNCTYPE, wintypes
LPFN_WND_PROC = CFUNCTYPE(
wintypes.INT,
@@ -789,7 +789,7 @@ class Win32Implementation(BaseImplementation):
class MENUITEMINFOW(Structure):
"""Windows menu item information structure."""
from ctypes import wintypes # noqa: PLC0415
from ctypes import wintypes
_fields_: ClassVar = [
("cb_size", wintypes.UINT),
@@ -809,7 +809,7 @@ class Win32Implementation(BaseImplementation):
class NOTIFYICONDATAW(Structure):
"""Windows notification icon data structure."""
from ctypes import wintypes # noqa: PLC0415
from ctypes import wintypes
_fields_: ClassVar = [
("cb_size", wintypes.DWORD),
@@ -1061,7 +1061,7 @@ class Win32Implementation(BaseImplementation):
if sys.platform != "win32":
return
from ctypes import wintypes # noqa: PLC0415
from ctypes import wintypes
if self._menu is None:
self.update_menu()
@@ -1110,7 +1110,7 @@ class Win32Implementation(BaseImplementation):
if sys.platform != "win32":
return 0
from ctypes import wintypes # noqa: PLC0415
from ctypes import wintypes
if msg == self.WM_TRAYICON:
if l_param == self.WM_RBUTTONUP: