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, self,
lib, lib,
inputs, inputs,
privateInputs ? { },
... ...
}: }:
let let
@@ -19,28 +18,19 @@ let
nixosLib = import (self.inputs.nixpkgs + "/nixos/lib") { }; nixosLib = import (self.inputs.nixpkgs + "/nixos/lib") { };
in in
{ {
imports = imports = filter pathExists [
let ./devshell/flake-module.nix
clanCoreModulesDir = ../nixosModules/clanCore; ./flash/flake-module.nix
getClanCoreTestModules = ./installation/flake-module.nix
let ./update/flake-module.nix
moduleNames = attrNames (builtins.readDir clanCoreModulesDir); ./morph/flake-module.nix
testPaths = map ( ./nixos-documentation/flake-module.nix
moduleName: clanCoreModulesDir + "/${moduleName}/tests/flake-module.nix" ./dont-depend-on-repo-root.nix
) moduleNames; # clan core submodule tests
in ../nixosModules/clanCore/machine-id/tests/flake-module.nix
filter pathExists testPaths; ../nixosModules/clanCore/postgresql/tests/flake-module.nix
in ../nixosModules/clanCore/state-version/tests/flake-module.nix
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
];
flake.check = genAttrs [ "x86_64-linux" "aarch64-darwin" ] ( flake.check = genAttrs [ "x86_64-linux" "aarch64-darwin" ] (
system: system:
let let
@@ -138,7 +128,7 @@ in
// flakeOutputs // flakeOutputs
// { // {
clan-core-for-checks = pkgs.runCommand "clan-core-for-checks" { } '' 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 chmod -R +w $out
cp ${../flake.lock} $out/flake.lock cp ${../flake.lock} $out/flake.lock

View File

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

View File

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

View File

@@ -1,91 +1,39 @@
# Clan service: sshd The `sshd` Clan service manages SSH to make it easy to securely access your
What it does machines over the internet. The service uses `vars` to store the SSH host keys
- Generates and persists SSH host keys via `vars`. for each machine to ensure they remain stable across deployments.
- Optionally issues CAsigned host certificates for servers.
- Installs the `server` CA public key into `clients` `known_hosts` for TOFUless verification.
`sshd` also generates SSH certificates for both servers and clients allowing for
certificate-based authentication for SSH.
When to use it The service also disables password-based authentication over SSH, to access your
- 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. machines you'll need to use public key authentication or certificate-based
authentication.
Roles ## Usage
- 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.
```nix ```nix
{ {
inventory.instances = { inventory.instances = {
# By default this service only generates ed25519 host keys
sshd-basic = { sshd-basic = {
module = { module = {
name = "sshd"; name = "sshd";
input = "clan-core"; input = "clan-core";
}; };
roles.server.tags.all = { }; roles.server.tags.all = { };
roles.client.tags.all = { };
}; };
}; # Also generate RSA host keys for all servers
} sshd-with-rsa = {
``` module = {
name = "sshd";
Example: selective trust per environment input = "clan-core";
Admins should trust only production; CI should trust prod and staging. Servers are reachable under both domains. };
```nix
{
inventory.instances = {
sshd-env-scoped = {
module = { name = "sshd"; input = "clan-core"; };
# Servers present certs for both prod and staging FQDNs
roles.server.tags.all = { }; roles.server.tags.all = { };
roles.server.settings = { roles.server.settings = {
certificate.searchDomains = [ "prod.example.com" "staging.example.com" ]; hostKeys.rsa.enable = true;
};
# 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" ];
}; };
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 pkgs.syncthing
]; ];
script = '' script = ''
export TMPDIR=/tmp syncthing generate --config "$out"
TEMPORARY=$(mktemp -d)
syncthing generate --config "$out" --data "$TEMPORARY"
mv "$out"/key.pem "$out"/key mv "$out"/key.pem "$out"/key
mv "$out"/cert.pem "$out"/cert mv "$out"/cert.pem "$out"/cert
cat "$out"/config.xml | grep -oP '(?<=<device id=")[^"]+' | uniq > "$out"/id 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": { "clan-core-for-checks": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1760000589, "lastModified": 1760368011,
"narHash": "sha256-9xBwxeb8x5XOo3alaJvv2ZwL7UhW3/oYUUBK+odWGrk=", "narHash": "sha256-mLK2nwbfklfOGIVAKVNDwGyYz8mPh4fzsAqSK3BlCiI=",
"ref": "main", "ref": "clan-25.05",
"rev": "e2f20b5ffcd4ff59e2528d29649056e3eb8d22bb", "rev": "1b3c129aa9741d99b27810652ca888b3fbfc3a11",
"shallow": true, "shallow": true,
"type": "git", "type": "git",
"url": "https://git.clan.lol/clan/clan-core" "url": "https://git.clan.lol/clan/clan-core"
}, },
"original": { "original": {
"ref": "main", "ref": "clan-25.05",
"shallow": true, "shallow": true,
"type": "git", "type": "git",
"url": "https://git.clan.lol/clan/clan-core" "url": "https://git.clan.lol/clan/clan-core"
@@ -105,16 +105,16 @@
}, },
"nixpkgs-dev": { "nixpkgs-dev": {
"locked": { "locked": {
"lastModified": 1759989671, "lastModified": 1760309387,
"narHash": "sha256-3Wk0I5TYsd7cyIO8vYGxjOuQ8zraZEUFZqEhSSIhQLs=", "narHash": "sha256-e0lvQ7+B1Y8zjykYHAj9tBv10ggLqK0nmxwvMU3J0Eo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "837076de579c67aa0c2ce2ab49948b24d907d449", "rev": "6cd95994a9c8f7c6f8c1f1161be94119afdcb305",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-unstable-small", "ref": "nixos-25.05-small",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@@ -208,11 +208,11 @@
"nixpkgs": [] "nixpkgs": []
}, },
"locked": { "locked": {
"lastModified": 1758728421, "lastModified": 1760120816,
"narHash": "sha256-ySNJ008muQAds2JemiyrWYbwbG+V7S5wg3ZVKGHSFu8=", "narHash": "sha256-gq9rdocpmRZCwLS5vsHozwB6b5nrOBDNc2kkEaTXHfg=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "5eda4ee8121f97b218f7cc73f5172098d458f1d1", "rev": "761ae7aff00907b607125b2f57338b74177697ed",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -2,7 +2,7 @@
description = "private dev inputs"; description = "private dev inputs";
# Dev dependencies # 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.url = "github:numtide/flake-utils";
inputs.flake-utils.inputs.systems.follows = "systems"; inputs.flake-utils.inputs.systems.follows = "systems";
@@ -15,7 +15,7 @@
inputs.systems.url = "github:nix-systems/default"; 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.clan-core-for-checks.flake = false;
inputs.test-fixtures.url = "git+https://git.clan.lol/clan/test-fixtures"; 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"; } .clamp-toggle:checked ~ .clamp-more::after { content: "Read less"; }
</style> </style>
trivial change
<div class="clamp-wrap" style="--lines: 3;"> <div class="clamp-wrap" style="--lines: 3;">
<input type="checkbox" id="clan-readmore" class="clamp-toggle" /> <input type="checkbox" id="clan-readmore" class="clamp-toggle" />
<div class="clamp-content"> <div class="clamp-content">

23
flake.lock generated
View File

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

View File

@@ -2,9 +2,9 @@
description = "clan.lol base operating system"; description = "clan.lol base operating system";
inputs = { 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"; nix-darwin.inputs.nixpkgs.follows = "nixpkgs";
flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts.url = "github:hercules-ci/flake-parts";

View File

@@ -11,8 +11,6 @@
treefmt.programs.nixfmt.enable = true; treefmt.programs.nixfmt.enable = true;
treefmt.programs.nixfmt.package = pkgs.nixfmt-rfc-style; treefmt.programs.nixfmt.package = pkgs.nixfmt-rfc-style;
treefmt.programs.deadnix.enable = true; treefmt.programs.deadnix.enable = true;
treefmt.programs.sizelint.enable = true;
treefmt.programs.sizelint.failOnWarn = true;
treefmt.programs.clang-format.enable = true; treefmt.programs.clang-format.enable = true;
treefmt.settings.global.excludes = [ treefmt.settings.global.excludes = [
"*.png" "*.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. # Plain imports.
introspection = import ./introspection { inherit lib; }; introspection = import ./introspection { inherit lib; };
jsonschema = import ./jsonschema { inherit lib; }; jsonschema = import ./jsonschema { inherit lib; };
facts = import ./facts.nix { inherit lib; };
docs = import ./docs.nix { inherit lib; }; docs = import ./docs.nix { inherit lib; };
# flakes # flakes
@@ -36,6 +35,10 @@ lib.fix (
# TODO: Flatten our lib functions like this: # TODO: Flatten our lib functions like this:
resolveModule = clanLib.callLib ./resolve-module { }; resolveModule = clanLib.callLib ./resolve-module { };
fs = {
inherit (builtins) pathExists readDir;
};
}; };
in in
f 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 # tests for the nixos options to jsonschema converter
# run these tests via `nix-unit ./test.nix` # 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; }), slib ? (import ./. { inherit lib; }),
}: }:
let let
@@ -68,38 +67,31 @@ in
}; };
}; };
}; };
test_no_default = test_no_default = {
let expr = stableView (
slib.getPrios {
configuration = ( options =
eval [ (eval [
{ {
options.foo.bar = lib.mkOption { options.foo.bar = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
}; };
} }
] ]).options;
); }
in );
{ expected = {
inherit configuration; foo = {
expr = stableView ( bar = {
slib.getPrios { __this = {
options = configuration.options; files = [ ];
} prio = 9999;
); total = false;
expected = {
foo = {
bar = {
__this = {
files = [ ];
prio = 9999;
total = false;
};
}; };
}; };
}; };
}; };
};
test_submodule = { test_submodule = {
expr = stableView ( expr = stableView (

View File

@@ -133,12 +133,13 @@ in
} }
) )
{ {
# TODO: Figure out why this causes infinite recursion # Note: we use clanLib.fs here, so that we can override it in tests
inventory.machines = lib.optionalAttrs (builtins.pathExists "${directory}/machines") ( inventory = lib.optionalAttrs (clanLib.fs.pathExists "${directory}/machines") ({
builtins.mapAttrs (_n: _v: { }) ( imports = lib.mapAttrsToList (name: _t: {
lib.filterAttrs (_: t: t == "directory") (builtins.readDir "${directory}/machines") _file = "${directory}/machines/${name}";
) machines.${name} = { };
); }) ((lib.filterAttrs (_: t: t == "directory") (clanLib.fs.readDir "${directory}/machines")));
});
} }
{ {
inventory.machines = lib.mapAttrs (_n: _: { }) config.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 in
####### #######
{ {
autoloading = import ./dir_test.nix { inherit lib; };
test_missing_self = test_missing_self =
let let
eval = clan { eval = clan {

View File

@@ -164,13 +164,25 @@
config = lib.mkIf (config.clan.core.secrets != { }) { config = lib.mkIf (config.clan.core.secrets != { }) {
clan.core.facts.services = lib.mapAttrs' ( clan.core.facts.services = lib.mapAttrs' (
name: service: name: service:
lib.warn "clan.core.secrets.${name} is deprecated, use clan.core.facts.services.${name} instead" ( lib.warn
lib.nameValuePair name ({ ''
secret = service.secrets; ###############################################################################
public = service.facts; # #
generator = service.generator; # 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; ) config.clan.core.secrets;
}; };
} }

View File

@@ -6,7 +6,17 @@
}: }:
{ {
config.warnings = lib.optionals (config.clan.core.facts.services != { }) [ 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 = { options.clan.core.facts = {

View File

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

View File

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

View File

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

View File

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

View File

@@ -113,15 +113,27 @@ mkShell {
# todo darwin support needs some work # todo darwin support needs some work
(lib.optionalString stdenv.hostPlatform.isLinux '' (lib.optionalString stdenv.hostPlatform.isLinux ''
# configure playwright for storybook snapshot testing # 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=${ export PLAYWRIGHT_BROWSERS_PATH=${
playwright-driver.browsers.override { playwright-driver.browsers.override {
withFfmpeg = false; withFfmpeg = false;
withFirefox = false; withFirefox = false;
withWebkit = true;
withChromium = false; 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", "jsdom": "^26.1.0",
"knip": "^5.61.2", "knip": "^5.61.2",
"markdown-to-jsx": "^7.7.10", "markdown-to-jsx": "^7.7.10",
"playwright": "~1.53.2", "playwright": "~1.55.1",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"postcss-url": "^10.1.3", "postcss-url": "^10.1.3",
"prettier": "^3.2.5", "prettier": "^3.2.5",
@@ -6956,13 +6956,13 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.53.2", "version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
"integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==", "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.53.2" "playwright-core": "1.55.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -6975,9 +6975,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.53.2", "version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
"integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==", "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {

View File

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

View File

@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid"; import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Button, ButtonProps } from "./Button"; import { Button, ButtonProps } from "./Button";
import { Component } from "solid-js"; 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"; import { StoryContext } from "@kachurun/storybook-solid-vite";
const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor; 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 = { export const Primary: Story = {
args: { args: {
hierarchy: "primary", hierarchy: "primary",
onAction: fn(async () => { onClick: fn(),
// 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");
}
}),
}, },
play: async ({ canvas, step, userEvent, args }: StoryContext) => { play: async ({ canvasElement, step, userEvent, args }: StoryContext) => {
const canvas = within(canvasElement);
const buttons = await canvas.findAllByRole("button"); const buttons = await canvas.findAllByRole("button");
for (const button of buttons) { for (const button of buttons) {
@@ -238,14 +232,6 @@ export const Primary: Story = {
} }
await step(`Click on ${testID}`, async () => { 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 // move the mouse over the button
await userEvent.hover(button); await userEvent.hover(button);
@@ -255,33 +241,8 @@ export const Primary: Story = {
// click the button // click the button
await userEvent.click(button); await userEvent.click(button);
// check the button has changed // the click handler should have been called
await waitFor( await expect(args.onClick).toHaveBeenCalled();
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 },
);
}); });
} }
}, },

View File

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

View File

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

View File

@@ -22,9 +22,9 @@ import { Alert } from "@/src/components/Alert/Alert";
import { removeClanURI } from "@/src/stores/clan"; import { removeClanURI } from "@/src/stores/clan";
const schema = v.object({ const schema = v.object({
name: v.pipe(v.optional(v.string())), name: v.string(),
description: v.nullish(v.string()), description: v.optional(v.string()),
icon: v.pipe(v.nullish(v.string())), icon: v.optional(v.string()),
}); });
export interface ClanSettingsModalProps { 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); const [store, set] = getStepStore<InstallStoreType>(stepSignal);
onMount(async () => { onMount(async () => {
const result = await store.flash.progress.result; const result = await store.flash?.progress?.result;
if (result.status == "success") { if (result?.status == "success") {
console.log("Flashing Success"); stepSignal.next();
} }
stepSignal.next();
}); });
const handleCancel = async () => { const handleCancel = async () => {

View File

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

View File

@@ -9,7 +9,11 @@
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "preserve", "jsx": "preserve",
"jsxImportSource": "solid-js", "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, "noEmit": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"allowJs": true, "allowJs": true,

View File

@@ -40,7 +40,14 @@ export default mergeConfig(
enabled: true, enabled: true,
headless: true, headless: true,
provider: "playwright", provider: "playwright",
instances: [{ browser: "chromium" }], instances: [
{
browser: "webkit",
launch: {
executablePath: process.env.PLAYWRIGHT_WEBKIT_EXECUTABLE,
},
},
],
}, },
// This setup file applies Storybook project annotations for Vitest // This setup file applies Storybook project annotations for Vitest
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations // 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() load_in_all_api_functions()
# import lazily since we otherwise we do not have all api functions loaded according to Qubasa # 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() schema = API.to_json_schema()
print(f"""{json.dumps(schema, indent=2)}""") print(f"""{json.dumps(schema, indent=2)}""")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -281,7 +281,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user", help="the name of the user",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( # noqa: PLC0415 from clan_cli.completions import (
add_dynamic_completer, add_dynamic_completer,
complete_users, complete_users,
) )
@@ -295,7 +295,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user", help="the name of the user",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( # noqa: PLC0415 from clan_cli.completions import (
add_dynamic_completer, add_dynamic_completer,
complete_users, complete_users,
) )
@@ -312,7 +312,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user", help="the name of the user",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( # noqa: PLC0415 from clan_cli.completions import (
add_dynamic_completer, add_dynamic_completer,
complete_secrets, complete_secrets,
complete_users, complete_users,
@@ -336,7 +336,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the group", help="the name of the group",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( # noqa: PLC0415 from clan_cli.completions import (
add_dynamic_completer, add_dynamic_completer,
complete_secrets, complete_secrets,
complete_users, complete_users,
@@ -360,7 +360,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user", help="the name of the user",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( # noqa: PLC0415 from clan_cli.completions import (
add_dynamic_completer, add_dynamic_completer,
complete_users, complete_users,
) )
@@ -378,7 +378,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user", help="the name of the user",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( # noqa: PLC0415 from clan_cli.completions import (
add_dynamic_completer, add_dynamic_completer,
complete_users, 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 json
import logging import logging
import os
import shutil import shutil
import subprocess import subprocess
import time import time
@@ -429,9 +430,43 @@ def test_generated_shared_secret_sops(
machine1 = Machine(name="machine1", flake=Flake(str(flake.path))) machine1 = Machine(name="machine1", flake=Flake(str(flake.path)))
machine2 = Machine(name="machine2", flake=Flake(str(flake.path))) machine2 = Machine(name="machine2", flake=Flake(str(flake.path)))
cli.run(["vars", "generate", "--flake", str(flake.path), "machine1"]) 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"]) cli.run(["vars", "generate", "--flake", str(flake.path), "machine2"])
assert check_vars(machine2.name, machine2.flake)
m1_sops_store = sops.SecretStore(machine1.flake) m1_sops_store = sops.SecretStore(machine1.flake)
m2_sops_store = sops.SecretStore(machine2.flake) m2_sops_store = sops.SecretStore(machine2.flake)
# Create generators with machine context for testing # Create generators with machine context for testing

View File

@@ -171,7 +171,7 @@ class StoreBase(ABC):
if generator.share: if generator.share:
log_info = log.info log_info = log.info
else: 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) machine_obj = Machine(name=generator.machines[0], flake=self.flake)
log_info = machine_obj.info log_info = machine_obj.info

View File

@@ -3,6 +3,7 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from clan_cli.completions import add_dynamic_completer, complete_machines 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.errors import ClanError
from clan_lib.flake import Flake, require_flake from clan_lib.flake import Flake, require_flake
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine
@@ -26,13 +27,33 @@ class VarStatus:
self.unfixed_secret_vars = unfixed_secret_vars self.unfixed_secret_vars = unfixed_secret_vars
self.invalid_generators = invalid_generators 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( def vars_status(
machine_name: str, machine_name: str,
flake: Flake, flake: Flake,
generator_name: None | str = None, generator_name: None | str = None,
) -> VarStatus: ) -> VarStatus:
from clan_cli.vars.generator import Generator # noqa: PLC0415 from clan_cli.vars.generator import Generator
machine = Machine(name=machine_name, flake=flake) machine = Machine(name=machine_name, flake=flake)
missing_secret_vars = [] 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.", f"Secret var '{file.name}' for service '{generator.name}' in machine {machine.name} is missing.",
) )
missing_secret_vars.append(file) 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: else:
msg = machine.secret_vars_store.health_check( health_msg = machine.secret_vars_store.health_check(
machine=machine.name, machine=machine.name,
generators=[generator], generators=[generator],
file_name=file.name, file_name=file.name,
) )
if msg: if health_msg is not None:
machine.info( 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) unfixed_secret_vars.append(file)
@@ -106,6 +144,7 @@ def check_vars(
generator_name: None | str = None, generator_name: None | str = None,
) -> bool: ) -> bool:
status = vars_status(machine_name, flake, generator_name=generator_name) status = vars_status(machine_name, flake, generator_name=generator_name)
log.info(f"Check results for machine '{machine_name}': \n{status.text()}")
return not ( return not (
status.missing_secret_vars status.missing_secret_vars
or status.missing_public_vars or status.missing_public_vars

View File

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

View File

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

View File

@@ -1162,7 +1162,7 @@ class Flake:
opts: "ListOptions | None" = None, opts: "ListOptions | None" = None,
) -> "dict[str, MachineResponse]": ) -> "dict[str, MachineResponse]":
"""List machines of a clan""" """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) 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: 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: with locked_open(user_history_file(), "w+") as f:
f.write(json.dumps(data, cls=ClanJSONEncoder, indent=4)) f.write(json.dumps(data, cls=ClanJSONEncoder, indent=4))
def read_history_file() -> list[dict]: 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: with locked_open(user_history_file(), "r") as f:
content: str = f.read() content: str = f.read()

View File

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

View File

@@ -33,7 +33,7 @@ class Machine:
def get_inv_machine(self) -> "InventoryMachine": def get_inv_machine(self) -> "InventoryMachine":
# Import on demand to avoid circular imports # 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) return get_machine(self.flake, self.name)
@@ -95,7 +95,7 @@ class Machine:
@cached_property @cached_property
def secret_vars_store(self) -> StoreBase: 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") secret_module = self.select("config.clan.core.vars.settings.secretModule")
module = importlib.import_module(secret_module) module = importlib.import_module(secret_module)
@@ -126,7 +126,7 @@ class Machine:
return self.flake.path return self.flake.path
def target_host(self) -> Remote: 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: with get_best_remote(self) as remote:
return remote return remote

View File

@@ -42,7 +42,7 @@ def _suggest_similar_names(
def get_available_machines(flake: Flake) -> list[str]: 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) machines = list_machines(flake)
return list(machines.keys()) return list(machines.keys())

View File

@@ -34,7 +34,7 @@ class Peer:
_var: dict[str, str] = self._host["var"] _var: dict[str, str] = self._host["var"]
machine_name = _var["machine"] machine_name = _var["machine"]
generator = _var["generator"] 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) machine = Machine(name=machine_name, flake=self.flake)
var = get_machine_var( 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"): if os.environ.get("IN_NIX_SANDBOX"):
from clan_lib.dirs import nixpkgs_source # noqa: PLC0415 from clan_lib.dirs import nixpkgs_source
return [ return [
*default_flags, *default_flags,
@@ -169,7 +169,7 @@ def nix_shell(packages: list[str], cmd: list[str]) -> list[str]:
if not missing_packages: if not missing_packages:
return cmd return cmd
from clan_lib.dirs import nixpkgs_flake # noqa: PLC0415 from clan_lib.dirs import nixpkgs_flake
return [ return [
*nix_command(["shell", "--inputs-from", f"{nixpkgs_flake()!s}"]), *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 typing import Any, TypedDict
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
from clan_lib.persist.path_utils import ( from clan_lib.persist.path_utils import PathTuple, path_to_string
PathTuple,
path_to_string,
)
WRITABLE_PRIORITY_THRESHOLD = 100 # Values below this are not writeable 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) 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.flake.flake import Flake
from clan_lib.persist.inventory_store import InventoryStore from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.write_rules import ( from clan_lib.persist.write_rules import compute_write_map
compute_write_map,
transform_attribute_properties,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from clan_lib.nix_models.clan import Clan 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 # Integration test
@pytest.mark.with_core @pytest.mark.with_core

View File

@@ -464,12 +464,12 @@ class Remote:
self, self,
opts: "ConnectionOptions | None" = None, opts: "ConnectionOptions | None" = 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) return check_machine_ssh_reachable(self, opts)
def check_machine_ssh_login(self) -> None: 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) 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.generator import Generator
from clan_cli.vars.graph import requested_closure from clan_cli.vars.graph import requested_closure
from clan_cli.vars.migration import check_can_migrate, migrate_files 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.api import API
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
@@ -152,15 +153,15 @@ def run_generators(
if not machines: if not machines:
msg = "At least one machine must be provided" msg = "At least one machine must be provided"
raise ClanError(msg) raise ClanError(msg)
all_generators = get_generators(machines, full_closure=True)
if isinstance(generators, list): if isinstance(generators, list):
# List of generator names - use them exactly as provided # List of generator names - use them exactly as provided
if len(generators) == 0: if len(generators) == 0:
return return
all_generators = get_generators(machines, full_closure=True) generators_to_run = [g for g in all_generators if g.key.name in generators]
generator_objects = [g for g in all_generators if g.key.name in generators]
else: else:
# None or single string - use get_generators with closure parameter # None or single string - use get_generators with closure parameter
generator_objects = get_generators( generators_to_run = get_generators(
machines, machines,
full_closure=full_closure, full_closure=full_closure,
generator_name=generators, generator_name=generators,
@@ -170,13 +171,30 @@ def run_generators(
# TODO: make this more lazy and ask for every generator on execution # TODO: make this more lazy and ask for every generator on execution
if callable(prompt_values): if callable(prompt_values):
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 # execute health check
for machine in machines: for machine in machines:
_ensure_healthy(machine=machine) _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) # get the flake via any machine (they are all the same)
flake = machines[0].flake flake = machines[0].flake
@@ -188,13 +206,13 @@ def run_generators(
# preheat the select cache, to reduce repeated calls during execution # preheat the select cache, to reduce repeated calls during execution
selectors = [] selectors = []
for generator in generator_objects: for generator in generators_to_run:
machine = get_generator_machine(generator) machine = get_generator_machine(generator)
selectors.append(generator.final_script_selector(machine.name)) selectors.append(generator.final_script_selector(machine.name))
flake.precache(selectors) flake.precache(selectors)
# execute generators # execute generators
for generator in generator_objects: for generator in generators_to_run:
machine = get_generator_machine(generator) machine = get_generator_machine(generator)
if check_can_migrate(machine, generator): if check_can_migrate(machine, generator):
migrate_files(machine, generator) migrate_files(machine, generator)

View File

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

View File

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

View File

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