Compare commits

...

16 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
Johannes Kirschbauer
347668a57f lib: remove unused facts utils 2025-10-12 17:49:05 +02:00
39 changed files with 221 additions and 255 deletions

View File

@@ -2,7 +2,6 @@
self, self,
lib, lib,
inputs, inputs,
privateInputs ? { },
... ...
}: }:
let let
@@ -129,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

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

18
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": 1760213549, "lastModified": 1760368011,
"narHash": "sha256-XosVRUEcdsoEdRtXyz9HrRc4Dt9Ke+viM5OVF7tLK50=", "narHash": "sha256-mLK2nwbfklfOGIVAKVNDwGyYz8mPh4fzsAqSK3BlCiI=",
"ref": "main", "ref": "clan-25.05",
"rev": "9c8797e77031d8d472d057894f18a53bdc9bbe1e", "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": 1760161054, "lastModified": 1760309387,
"narHash": "sha256-PO3cKHFIQEPI0dr/SzcZwG50cHXfjoIqP2uS5W78OXg=", "narHash": "sha256-e0lvQ7+B1Y8zjykYHAj9tBv10ggLqK0nmxwvMU3J0Eo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e18d8ec6fafaed55561b7a1b54eb1c1ce3ffa2c5", "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"
} }

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";

17
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": {

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

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

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

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

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

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

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