Compare commits
2 Commits
push-tnkqq
...
feat/snaps
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60279fffa9 | ||
|
|
5886cc3330 |
@@ -2,6 +2,7 @@
|
|||||||
self,
|
self,
|
||||||
lib,
|
lib,
|
||||||
inputs,
|
inputs,
|
||||||
|
privateInputs ? { },
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -128,7 +129,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 ${self} $out
|
cp -r ${privateInputs.clan-core-for-checks} $out
|
||||||
chmod -R +w $out
|
chmod -R +w $out
|
||||||
cp ${../flake.lock} $out/flake.lock
|
cp ${../flake.lock} $out/flake.lock
|
||||||
|
|
||||||
|
|||||||
@@ -50,13 +50,13 @@
|
|||||||
dns =
|
dns =
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
{
|
{
|
||||||
environment.systemPackages = [ pkgs.nettools ];
|
environment.systemPackages = [ pkgs.net-tools ];
|
||||||
};
|
};
|
||||||
|
|
||||||
client =
|
client =
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
{
|
{
|
||||||
environment.systemPackages = [ pkgs.nettools ];
|
environment.systemPackages = [ pkgs.net-tools ];
|
||||||
};
|
};
|
||||||
|
|
||||||
server01 = {
|
server01 = {
|
||||||
|
|||||||
@@ -1,39 +1,91 @@
|
|||||||
The `sshd` Clan service manages SSH to make it easy to securely access your
|
# Clan service: sshd
|
||||||
machines over the internet. The service uses `vars` to store the SSH host keys
|
What it does
|
||||||
for each machine to ensure they remain stable across deployments.
|
- Generates and persists SSH host keys via `vars`.
|
||||||
|
- Optionally issues CA‑signed host certificates for servers.
|
||||||
|
- Installs the `server` CA public key into `clients` `known_hosts` for TOFU‑less verification.
|
||||||
|
|
||||||
`sshd` also generates SSH certificates for both servers and clients allowing for
|
|
||||||
certificate-based authentication for SSH.
|
|
||||||
|
|
||||||
The service also disables password-based authentication over SSH, to access your
|
When to use it
|
||||||
machines you'll need to use public key authentication or certificate-based
|
- Zero‑TOFU SSH for dynamic fleets: admins/CI can connect to frequently rebuilt hosts (e.g., server-1.example.com) without prompts or per‑host `known_hosts` churn.
|
||||||
authentication.
|
|
||||||
|
|
||||||
## Usage
|
Roles
|
||||||
|
- Server: runs sshd, presents a CA‑signed 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";
|
|
||||||
input = "clan-core";
|
|
||||||
};
|
|
||||||
roles.server.tags.all = { };
|
|
||||||
roles.server.settings = {
|
|
||||||
hostKeys.rsa.enable = true;
|
|
||||||
};
|
|
||||||
roles.client.tags.all = { };
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example: selective trust per environment
|
||||||
|
Admins should trust only production; CI should trust prod and staging. Servers are reachable under both domains.
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inventory.instances = {
|
||||||
|
sshd-env-scoped = {
|
||||||
|
module = { name = "sshd"; input = "clan-core"; };
|
||||||
|
|
||||||
|
# Servers present certs for both prod and staging FQDNs
|
||||||
|
roles.server.tags.all = { };
|
||||||
|
roles.server.settings = {
|
||||||
|
certificate.searchDomains = [ "prod.example.com" "staging.example.com" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Admin laptop: trust prod only
|
||||||
|
roles.client.machines."admin-laptop".settings = {
|
||||||
|
certificate.searchDomains = [ "prod.example.com" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# CI runner: trust prod and staging
|
||||||
|
roles.client.machines."ci-runner-1".settings = {
|
||||||
|
certificate.searchDomains = [ "prod.example.com" "staging.example.com" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Admin -> server1.prod.example.com: zero‑TOFU (verified via cert).
|
||||||
|
- Admin -> server1.staging.example.com: falls back to TOFU (or is blocked by policy).
|
||||||
|
- CI -> either prod or staging: zero‑TOFU for both.
|
||||||
|
Note: server and client searchDomains don’t 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).
|
||||||
@@ -11,7 +11,9 @@
|
|||||||
pkgs.syncthing
|
pkgs.syncthing
|
||||||
];
|
];
|
||||||
script = ''
|
script = ''
|
||||||
syncthing generate --config "$out"
|
export TMPDIR=/tmp
|
||||||
|
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
18
devFlake/flake.lock
generated
@@ -3,16 +3,16 @@
|
|||||||
"clan-core-for-checks": {
|
"clan-core-for-checks": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1760368011,
|
"lastModified": 1760213549,
|
||||||
"narHash": "sha256-mLK2nwbfklfOGIVAKVNDwGyYz8mPh4fzsAqSK3BlCiI=",
|
"narHash": "sha256-XosVRUEcdsoEdRtXyz9HrRc4Dt9Ke+viM5OVF7tLK50=",
|
||||||
"ref": "clan-25.05",
|
"ref": "main",
|
||||||
"rev": "1b3c129aa9741d99b27810652ca888b3fbfc3a11",
|
"rev": "9c8797e77031d8d472d057894f18a53bdc9bbe1e",
|
||||||
"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": "clan-25.05",
|
"ref": "main",
|
||||||
"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": 1760309387,
|
"lastModified": 1760161054,
|
||||||
"narHash": "sha256-e0lvQ7+B1Y8zjykYHAj9tBv10ggLqK0nmxwvMU3J0Eo=",
|
"narHash": "sha256-PO3cKHFIQEPI0dr/SzcZwG50cHXfjoIqP2uS5W78OXg=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "6cd95994a9c8f7c6f8c1f1161be94119afdcb305",
|
"rev": "e18d8ec6fafaed55561b7a1b54eb1c1ce3ffa2c5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-25.05-small",
|
"ref": "nixos-unstable-small",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
description = "private dev inputs";
|
description = "private dev inputs";
|
||||||
|
|
||||||
# Dev dependencies
|
# Dev dependencies
|
||||||
inputs.nixpkgs-dev.url = "github:NixOS/nixpkgs/nixos-25.05-small";
|
inputs.nixpkgs-dev.url = "github:NixOS/nixpkgs/nixos-unstable-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=clan-25.05&shallow=1";
|
inputs.clan-core-for-checks.url = "git+https://git.clan.lol/clan/clan-core?ref=main&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
17
flake.lock
generated
@@ -71,16 +71,15 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1759509947,
|
"lastModified": 1758805352,
|
||||||
"narHash": "sha256-4XifSIHfpJKcCf5bZZRhj8C4aCpjNBaE3kXr02s4rHU=",
|
"narHash": "sha256-BHdc43Lkayd+72W/NXRKHzX5AZ+28F3xaUs3a88/Uew=",
|
||||||
"owner": "nix-darwin",
|
"owner": "nix-darwin",
|
||||||
"repo": "nix-darwin",
|
"repo": "nix-darwin",
|
||||||
"rev": "000eadb231812ad6ea6aebd7526974aaf4e79355",
|
"rev": "c48e963a5558eb1c3827d59d21c5193622a1477c",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -115,15 +114,15 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1760324802,
|
"lastModified": 315532800,
|
||||||
"narHash": "sha256-VWlJtLQ5EQQj45Wj0yTExtSjwRyZ59/qMqEwus/Exlg=",
|
"narHash": "sha256-1tUpklZsKzMGI3gjo/dWD+hS8cf+5Jji8TF5Cfz7i3I=",
|
||||||
"rev": "7e297ddff44a3cc93673bb38d0374df8d0ad73e4",
|
"rev": "08b8f92ac6354983f5382124fef6006cade4a1c1",
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://releases.nixos.org/nixos/25.05/nixos-25.05.811135.7e297ddff44a/nixexprs.tar.xz"
|
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre862603.08b8f92ac635/nixexprs.tar.xz"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://nixos.org/channels/nixos-25.05/nixexprs.tar.xz"
|
"url": "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
description = "clan.lol base operating system";
|
description = "clan.lol base operating system";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "https://nixos.org/channels/nixos-25.05/nixexprs.tar.xz";
|
nixpkgs.url = "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz";
|
||||||
|
|
||||||
nix-darwin.url = "github:nix-darwin/nix-darwin/nix-darwin-25.05";
|
nix-darwin.url = "github:nix-darwin/nix-darwin";
|
||||||
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";
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
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"
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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
|
||||||
|
|||||||
71
lib/facts.nix
Normal file
71
lib/facts.nix
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{ 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
|
||||||
|
;
|
||||||
|
}
|
||||||
@@ -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
|
from clan_app.middleware.base import MiddlewareContext # noqa: PLC0415
|
||||||
|
|
||||||
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 (
|
from clan_app.middleware.base import ( # noqa: PLC0415
|
||||||
MiddlewareError,
|
MiddlewareError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -191,13 +191,13 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
return file_data
|
return file_data
|
||||||
|
|
||||||
def do_OPTIONS(self) -> None: # noqa: N802
|
def do_OPTIONS(self) -> None:
|
||||||
"""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: # noqa: N802
|
def do_GET(self) -> None:
|
||||||
"""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: # noqa: N802
|
def do_POST(self) -> None:
|
||||||
"""Handle POST requests."""
|
"""Handle POST requests."""
|
||||||
parsed_url = urlparse(self.path)
|
parsed_url = urlparse(self.path)
|
||||||
path = parsed_url.path
|
path = parsed_url.path
|
||||||
|
|||||||
@@ -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
|
from .webview import FuncStatus # noqa: PLC0415
|
||||||
|
|
||||||
self.webview.return_(response._op_key, FuncStatus.SUCCESS, serialized) # noqa: SLF001
|
self.webview.return_(response._op_key, FuncStatus.SUCCESS, serialized) # noqa: SLF001
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ mkShell {
|
|||||||
withFirefox = false;
|
withFirefox = false;
|
||||||
withWebkit = true;
|
withWebkit = true;
|
||||||
withChromium = false;
|
withChromium = false;
|
||||||
withChromiumHeadlessShell = false;
|
withChromiumHeadlessShell = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +134,6 @@ mkShell {
|
|||||||
# this helps us avoid having to update the playwright js dependency everytime we update nixpkgs and vice versa
|
# 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
|
# see vitest.config.js for corresponding launch configuration
|
||||||
|
|
||||||
export PLAYWRIGHT_WEBKIT_EXECUTABLE=$(find -L "$PLAYWRIGHT_BROWSERS_PATH" -type f -name "pw_run.sh")
|
export PLAYWRIGHT_CHROMIUM_EXECUTABLE=$(find -L "$PLAYWRIGHT_BROWSERS_PATH" -type f -name "headless_shell")
|
||||||
'');
|
'');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
importNpmLock,
|
importNpmLock,
|
||||||
clan-ts-api,
|
clan-ts-api,
|
||||||
fonts,
|
fonts,
|
||||||
|
ps,
|
||||||
|
playwright-driver,
|
||||||
}:
|
}:
|
||||||
buildNpmPackage (_finalAttrs: {
|
buildNpmPackage (finalAttrs: {
|
||||||
pname = "clan-app-ui";
|
pname = "clan-app-ui";
|
||||||
version = "0.0.1";
|
version = "0.0.1";
|
||||||
nodejs = nodejs_22;
|
nodejs = nodejs_22;
|
||||||
@@ -32,36 +34,38 @@ buildNpmPackage (_finalAttrs: {
|
|||||||
# todo figure out why this fails only inside of Nix
|
# todo figure out why this fails only inside of Nix
|
||||||
# Something about passing orientation in any of the Form stories is causing the browser to crash
|
# Something about passing orientation in any of the Form stories is causing the browser to crash
|
||||||
# `npm run test-storybook-static` works fine in the devshell
|
# `npm run test-storybook-static` works fine in the devshell
|
||||||
#
|
|
||||||
# passthru = rec {
|
passthru = rec {
|
||||||
# storybook = buildNpmPackage {
|
storybook = buildNpmPackage {
|
||||||
# pname = "${finalAttrs.pname}-storybook";
|
pname = "${finalAttrs.pname}-storybook";
|
||||||
# inherit (finalAttrs)
|
inherit (finalAttrs)
|
||||||
# version
|
version
|
||||||
# nodejs
|
nodejs
|
||||||
# src
|
src
|
||||||
# npmDeps
|
npmDeps
|
||||||
# npmConfigHook
|
npmConfigHook
|
||||||
# preBuild
|
;
|
||||||
# ;
|
|
||||||
#
|
nativeBuildInputs = finalAttrs.nativeBuildInputs ++ [
|
||||||
# nativeBuildInputs = finalAttrs.nativeBuildInputs ++ [
|
ps
|
||||||
# ps
|
];
|
||||||
# ];
|
|
||||||
#
|
npmBuildScript = "test-storybook-static";
|
||||||
# npmBuildScript = "test-storybook-static";
|
|
||||||
#
|
env = {
|
||||||
# env = finalAttrs.env // {
|
PLAYWRIGHT_BROWSERS_PATH = "${playwright-driver.browsers.override {
|
||||||
# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = 1;
|
withChromiumHeadlessShell = true;
|
||||||
# PLAYWRIGHT_BROWSERS_PATH = "${playwright-driver.browsers.override {
|
}}";
|
||||||
# withChromiumHeadlessShell = true;
|
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = true;
|
||||||
# }}";
|
};
|
||||||
# PLAYWRIGHT_HOST_PLATFORM_OVERRIDE = "ubuntu-24.04";
|
|
||||||
# };
|
preBuild = finalAttrs.preBuild + ''
|
||||||
#
|
export PLAYWRIGHT_CHROMIUM_EXECUTABLE=$(find -L "$PLAYWRIGHT_BROWSERS_PATH" -type f -name "headless_shell")
|
||||||
# postBuild = ''
|
'';
|
||||||
# mv storybook-static $out
|
|
||||||
# '';
|
postBuild = ''
|
||||||
# };
|
mv storybook-static $out
|
||||||
# };
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
"knip": "knip --fix",
|
"knip": "knip --fix",
|
||||||
"storybook-build": "storybook build",
|
"storybook-build": "storybook build",
|
||||||
"storybook-dev": "storybook dev -p 6006",
|
"storybook-dev": "storybook dev -p 6006",
|
||||||
"test-storybook": "vitest run --project storybook",
|
"test-storybook": "vitest run --project storybook --reporter verbose",
|
||||||
"test-storybook-update-snapshots": "vitest run --project storybook --update",
|
"test-storybook-update-snapshots": "vitest run --project storybook --update",
|
||||||
"test-storybook-static": "npm run storybook-build && concurrently -k -s first -n 'SB,TEST' -c 'magenta,blue' 'http-server storybook-static --port 6006 --silent' 'wait-on tcp:127.0.0.1:6006 && npm run test-storybook'"
|
"test-storybook-static": "npm run storybook-build && concurrently -k -s first -n 'SB,TEST' -c 'magenta,blue' 'http-server storybook-static -a 127.0.0.1 -p 6006 --silent' 'wait-on tcp:127.0.0.1:6006 && npm run test-storybook'"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -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, within } from "storybook/test";
|
import { expect, fn, 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;
|
||||||
|
|||||||
@@ -11,6 +11,59 @@ import { Button } from "../Button/Button";
|
|||||||
const meta: Meta<ModalProps> = {
|
const meta: Meta<ModalProps> = {
|
||||||
title: "Components/Modal",
|
title: "Components/Modal",
|
||||||
component: Modal,
|
component: Modal,
|
||||||
|
render: (args: ModalProps) => (
|
||||||
|
<Modal
|
||||||
|
{...args}
|
||||||
|
children={
|
||||||
|
<form class="flex flex-col gap-5">
|
||||||
|
<Fieldset legend="General">
|
||||||
|
{(props: FieldsetFieldProps) => (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
{...props}
|
||||||
|
label="First Name"
|
||||||
|
size="s"
|
||||||
|
required={true}
|
||||||
|
input={{ placeholder: "Ron" }}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
{...props}
|
||||||
|
label="Last Name"
|
||||||
|
size="s"
|
||||||
|
required={true}
|
||||||
|
input={{ placeholder: "Burgundy" }}
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
{...props}
|
||||||
|
label="Bio"
|
||||||
|
size="s"
|
||||||
|
input={{
|
||||||
|
placeholder: "Tell us a bit about yourself",
|
||||||
|
rows: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
{...props}
|
||||||
|
size="s"
|
||||||
|
label="Accept Terms"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
<div class="flex w-full items-center justify-end gap-4">
|
||||||
|
<Button size="s" hierarchy="secondary" onClick={close}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="s" type="submit" hierarchy="primary" onClick={close}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@@ -21,50 +74,5 @@ export const Default: Story = {
|
|||||||
args: {
|
args: {
|
||||||
title: "Example Modal",
|
title: "Example Modal",
|
||||||
onClose: fn(),
|
onClose: fn(),
|
||||||
children: (
|
|
||||||
<form class="flex flex-col gap-5">
|
|
||||||
<Fieldset legend="General">
|
|
||||||
{(props: FieldsetFieldProps) => (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
{...props}
|
|
||||||
label="First Name"
|
|
||||||
size="s"
|
|
||||||
required={true}
|
|
||||||
input={{ placeholder: "Ron" }}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
{...props}
|
|
||||||
label="Last Name"
|
|
||||||
size="s"
|
|
||||||
required={true}
|
|
||||||
input={{ placeholder: "Burgundy" }}
|
|
||||||
/>
|
|
||||||
<TextArea
|
|
||||||
{...props}
|
|
||||||
label="Bio"
|
|
||||||
size="s"
|
|
||||||
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
{...props}
|
|
||||||
size="s"
|
|
||||||
label="Accept Terms"
|
|
||||||
required={true}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Fieldset>
|
|
||||||
|
|
||||||
<div class="flex w-full items-center justify-end gap-4">
|
|
||||||
<Button size="s" hierarchy="secondary" onClick={close}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button size="s" type="submit" hierarchy="primary" onClick={close}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,11 +7,9 @@ import {
|
|||||||
} from "@solidjs/router";
|
} from "@solidjs/router";
|
||||||
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
|
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
|
||||||
import { Suspense } from "solid-js";
|
import { Suspense } from "solid-js";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
|
||||||
import { addClanURI, resetStore } from "@/src/stores/clan";
|
import { addClanURI, resetStore } from "@/src/stores/clan";
|
||||||
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
|
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
|
||||||
import { encodeBase64 } from "@/src/hooks/clan";
|
import { encodeBase64 } from "@/src/hooks/clan";
|
||||||
import { ApiClientProvider } from "@/src/hooks/ApiClient";
|
|
||||||
import {
|
import {
|
||||||
ApiCall,
|
ApiCall,
|
||||||
OperationArgs,
|
OperationArgs,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { splitProps } from "solid-js";
|
|||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { MachineTags } from "@/src/components/Form/MachineTags";
|
import { MachineTags } from "@/src/components/Form/MachineTags";
|
||||||
import { setValue } from "@modular-forms/solid";
|
import { setValue } from "@modular-forms/solid";
|
||||||
|
import { StoryContext } from "@kachurun/storybook-solid-vite";
|
||||||
|
|
||||||
type Story = StoryObj<SidebarPaneProps>;
|
type Story = StoryObj<SidebarPaneProps>;
|
||||||
|
|
||||||
@@ -30,6 +31,13 @@ const profiles = {
|
|||||||
const meta: Meta<SidebarPaneProps> = {
|
const meta: Meta<SidebarPaneProps> = {
|
||||||
title: "Components/SidebarPane",
|
title: "Components/SidebarPane",
|
||||||
component: SidebarPane,
|
component: SidebarPane,
|
||||||
|
decorators: [
|
||||||
|
(
|
||||||
|
Story: StoryObj<SidebarPaneProps>,
|
||||||
|
context: StoryContext<SidebarPaneProps>,
|
||||||
|
) =>
|
||||||
|
() => <Story {...context.args} />,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@@ -40,133 +48,140 @@ export const Default: Story = {
|
|||||||
onClose: () => {
|
onClose: () => {
|
||||||
console.log("closing");
|
console.log("closing");
|
||||||
},
|
},
|
||||||
children: (
|
},
|
||||||
<>
|
// We have to provide children within a custom render function to ensure we aren't creating any reactivity outside the
|
||||||
<SidebarSectionForm
|
// solid-js scope.
|
||||||
title="General"
|
render: (args: SidebarPaneProps) => (
|
||||||
schema={v.object({
|
<SidebarPane
|
||||||
firstName: v.pipe(
|
{...args}
|
||||||
v.string(),
|
children={
|
||||||
v.nonEmpty("Please enter a first name."),
|
<>
|
||||||
),
|
<SidebarSectionForm
|
||||||
lastName: v.pipe(
|
title="General"
|
||||||
v.string(),
|
schema={v.object({
|
||||||
v.nonEmpty("Please enter a last name."),
|
firstName: v.pipe(
|
||||||
),
|
v.string(),
|
||||||
bio: v.string(),
|
v.nonEmpty("Please enter a first name."),
|
||||||
shareProfile: v.optional(v.boolean()),
|
),
|
||||||
})}
|
lastName: v.pipe(
|
||||||
initialValues={profiles.ron}
|
v.string(),
|
||||||
onSubmit={async () => {
|
v.nonEmpty("Please enter a last name."),
|
||||||
console.log("saving general");
|
),
|
||||||
}}
|
bio: v.string(),
|
||||||
>
|
shareProfile: v.optional(v.boolean()),
|
||||||
{({ editing, Field }) => (
|
})}
|
||||||
<div class="flex flex-col gap-3">
|
initialValues={profiles.ron}
|
||||||
<Field name="firstName">
|
onSubmit={async () => {
|
||||||
{(field, input) => (
|
console.log("saving general");
|
||||||
<TextInput
|
}}
|
||||||
{...field}
|
>
|
||||||
size="s"
|
{({ editing, Field }) => (
|
||||||
inverted
|
<div class="flex flex-col gap-3">
|
||||||
label="First Name"
|
<Field name="firstName">
|
||||||
value={field.value}
|
{(field, input) => (
|
||||||
required
|
<TextInput
|
||||||
readOnly={!editing}
|
{...field}
|
||||||
orientation="horizontal"
|
|
||||||
input={input}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Divider />
|
|
||||||
<Field name="lastName">
|
|
||||||
{(field, input) => (
|
|
||||||
<TextInput
|
|
||||||
{...field}
|
|
||||||
size="s"
|
|
||||||
inverted
|
|
||||||
label="Last Name"
|
|
||||||
value={field.value}
|
|
||||||
required
|
|
||||||
readOnly={!editing}
|
|
||||||
orientation="horizontal"
|
|
||||||
input={input}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Divider />
|
|
||||||
<Field name="bio">
|
|
||||||
{(field, input) => (
|
|
||||||
<TextArea
|
|
||||||
{...field}
|
|
||||||
value={field.value}
|
|
||||||
size="s"
|
|
||||||
label="Bio"
|
|
||||||
inverted
|
|
||||||
readOnly={!editing}
|
|
||||||
orientation="horizontal"
|
|
||||||
input={{ ...input, rows: 4 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field name="shareProfile" type="boolean">
|
|
||||||
{(field, input) => {
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
{...splitProps(field, ["value"])[1]}
|
|
||||||
defaultChecked={field.value}
|
|
||||||
size="s"
|
size="s"
|
||||||
label="Share"
|
|
||||||
inverted
|
inverted
|
||||||
|
label="First Name"
|
||||||
|
value={field.value}
|
||||||
|
required
|
||||||
readOnly={!editing}
|
readOnly={!editing}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
input={input}
|
input={input}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
}}
|
</Field>
|
||||||
</Field>
|
<Divider />
|
||||||
</div>
|
<Field name="lastName">
|
||||||
)}
|
{(field, input) => (
|
||||||
</SidebarSectionForm>
|
<TextInput
|
||||||
<SidebarSectionForm
|
{...field}
|
||||||
title="Tags"
|
size="s"
|
||||||
schema={v.object({
|
inverted
|
||||||
tags: v.pipe(v.array(v.string()), v.nonEmpty()),
|
label="Last Name"
|
||||||
})}
|
value={field.value}
|
||||||
initialValues={profiles.ron}
|
required
|
||||||
onSubmit={async (values) => {
|
readOnly={!editing}
|
||||||
console.log("saving tags", values);
|
orientation="horizontal"
|
||||||
}}
|
input={input}
|
||||||
>
|
/>
|
||||||
{({ editing, Field, formStore }) => (
|
)}
|
||||||
<Field name="tags" type="string[]">
|
</Field>
|
||||||
{(field, props) => (
|
<Divider />
|
||||||
<MachineTags
|
<Field name="bio">
|
||||||
{...splitProps(field, ["value"])[1]}
|
{(field, input) => (
|
||||||
size="s"
|
<TextArea
|
||||||
onChange={(newVal) => {
|
{...field}
|
||||||
// Workaround for now, until we manage to use native events
|
value={field.value}
|
||||||
setValue(formStore, field.name, newVal);
|
size="s"
|
||||||
|
label="Bio"
|
||||||
|
inverted
|
||||||
|
readOnly={!editing}
|
||||||
|
orientation="horizontal"
|
||||||
|
input={{ ...input, rows: 4 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name="shareProfile" type="boolean">
|
||||||
|
{(field, input) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
{...splitProps(field, ["value"])[1]}
|
||||||
|
defaultChecked={field.value}
|
||||||
|
size="s"
|
||||||
|
label="Share"
|
||||||
|
inverted
|
||||||
|
readOnly={!editing}
|
||||||
|
orientation="horizontal"
|
||||||
|
input={input}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
inverted
|
</Field>
|
||||||
required
|
</div>
|
||||||
readOnly={!editing}
|
)}
|
||||||
orientation="horizontal"
|
</SidebarSectionForm>
|
||||||
defaultValue={field.value}
|
<SidebarSectionForm
|
||||||
/>
|
title="Tags"
|
||||||
)}
|
schema={v.object({
|
||||||
</Field>
|
tags: v.pipe(v.array(v.string()), v.nonEmpty()),
|
||||||
)}
|
})}
|
||||||
</SidebarSectionForm>
|
initialValues={profiles.ron}
|
||||||
<SidebarSection title="Simple">
|
onSubmit={async (values) => {
|
||||||
<Typography tag="h2" hierarchy="title" size="m" inverted>
|
console.log("saving tags", values);
|
||||||
Static Content
|
}}
|
||||||
</Typography>
|
>
|
||||||
<Typography hierarchy="label" size="s" inverted>
|
{({ editing, Field, formStore }) => (
|
||||||
This is a non-form section with static content
|
<Field name="tags" type="string[]">
|
||||||
</Typography>
|
{(field, props) => (
|
||||||
</SidebarSection>
|
<MachineTags
|
||||||
</>
|
{...splitProps(field, ["value"])[1]}
|
||||||
),
|
size="s"
|
||||||
},
|
onChange={(newVal) => {
|
||||||
|
// Workaround for now, until we manage to use native events
|
||||||
|
setValue(formStore, field.name, newVal);
|
||||||
|
}}
|
||||||
|
inverted
|
||||||
|
required
|
||||||
|
readOnly={!editing}
|
||||||
|
orientation="horizontal"
|
||||||
|
defaultValue={field.value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</SidebarSectionForm>
|
||||||
|
<SidebarSection title="Simple">
|
||||||
|
<Typography tag="h2" hierarchy="title" size="m" inverted>
|
||||||
|
Static Content
|
||||||
|
</Typography>
|
||||||
|
<Typography hierarchy="label" size="s" inverted>
|
||||||
|
This is a non-form section with static content
|
||||||
|
</Typography>
|
||||||
|
</SidebarSection>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||||
import { Toolbar, ToolbarProps } from "@/src/components/Toolbar/Toolbar";
|
import { Toolbar, ToolbarProps } from "@/src/components/Toolbar/Toolbar";
|
||||||
import { Divider } from "@/src/components/Divider/Divider";
|
|
||||||
import { ToolbarButton } from "./ToolbarButton";
|
import { ToolbarButton } from "./ToolbarButton";
|
||||||
|
|
||||||
const meta: Meta<ToolbarProps> = {
|
const meta: Meta<ToolbarProps> = {
|
||||||
@@ -13,61 +12,35 @@ export default meta;
|
|||||||
type Story = StoryObj<ToolbarProps>;
|
type Story = StoryObj<ToolbarProps>;
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
// We have to specify children inside a render function to avoid issues with reactivity outside a solid-js context.
|
||||||
children: (
|
|
||||||
<>
|
|
||||||
<ToolbarButton
|
|
||||||
name="select"
|
|
||||||
icon="Cursor"
|
|
||||||
description="Select my thing"
|
|
||||||
/>
|
|
||||||
<ToolbarButton
|
|
||||||
name="new-machine"
|
|
||||||
icon="NewMachine"
|
|
||||||
description="Select this thing"
|
|
||||||
/>
|
|
||||||
<Divider orientation="vertical" />
|
|
||||||
<ToolbarButton
|
|
||||||
name="modules"
|
|
||||||
icon="Modules"
|
|
||||||
selected={true}
|
|
||||||
description="Add service"
|
|
||||||
/>
|
|
||||||
<ToolbarButton name="ai" icon="AI" description="Call your AI Manager" />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithTooltip: Story = {
|
|
||||||
// @ts-expect-error: args in storybook is not typed correctly. This is a storybook issue.
|
// @ts-expect-error: args in storybook is not typed correctly. This is a storybook issue.
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
<div class="flex h-[80vh]">
|
<div class="flex h-[80vh]">
|
||||||
<div class="mt-auto">
|
<div class="mt-auto">
|
||||||
<Toolbar {...args} />
|
<Toolbar
|
||||||
|
{...args}
|
||||||
|
children={
|
||||||
|
<>
|
||||||
|
<ToolbarButton name="select" icon="Cursor" description="Select" />
|
||||||
|
|
||||||
|
<ToolbarButton
|
||||||
|
name="new-machine"
|
||||||
|
icon="NewMachine"
|
||||||
|
description="Select"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ToolbarButton
|
||||||
|
name="modules"
|
||||||
|
icon="Modules"
|
||||||
|
selected={true}
|
||||||
|
description="Select"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ToolbarButton name="ai" icon="AI" description="Select" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
args: {
|
|
||||||
children: (
|
|
||||||
<>
|
|
||||||
<ToolbarButton name="select" icon="Cursor" description="Select" />
|
|
||||||
|
|
||||||
<ToolbarButton
|
|
||||||
name="new-machine"
|
|
||||||
icon="NewMachine"
|
|
||||||
description="Select"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ToolbarButton
|
|
||||||
name="modules"
|
|
||||||
icon="Modules"
|
|
||||||
selected={true}
|
|
||||||
description="Select"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ToolbarButton name="ai" icon="AI" description="Select" />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||||
import { Tooltip, TooltipProps } from "@/src/components/Tooltip/Tooltip";
|
import { Tooltip, TooltipProps } from "@/src/components/Tooltip/Tooltip";
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { Button } from "@/src/components/Button/Button";
|
|
||||||
|
|
||||||
const meta: Meta<TooltipProps> = {
|
const meta: Meta<TooltipProps> = {
|
||||||
title: "Components/Tooltip",
|
title: "Components/Tooltip",
|
||||||
@@ -13,6 +12,23 @@ const meta: Meta<TooltipProps> = {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
render: (args: TooltipProps) => (
|
||||||
|
<div class="p-16">
|
||||||
|
<Tooltip
|
||||||
|
{...args}
|
||||||
|
children={
|
||||||
|
<Typography
|
||||||
|
hierarchy="body"
|
||||||
|
size="xs"
|
||||||
|
inverted={true}
|
||||||
|
weight="medium"
|
||||||
|
>
|
||||||
|
Your Clan is being created
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@@ -23,12 +39,6 @@ export const Default: Story = {
|
|||||||
args: {
|
args: {
|
||||||
placement: "top",
|
placement: "top",
|
||||||
inverted: false,
|
inverted: false,
|
||||||
trigger: <Button hierarchy="primary">Trigger</Button>,
|
|
||||||
children: (
|
|
||||||
<Typography hierarchy="body" size="xs" inverted={true} weight="medium">
|
|
||||||
Your Clan is being created
|
|
||||||
</Typography>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getStepStore, useStepper } from "@/src/hooks/stepper";
|
import { getStepStore, useStepper } from "@/src/hooks/stepper";
|
||||||
import {
|
import {
|
||||||
createForm,
|
createForm,
|
||||||
getError,
|
|
||||||
SubmitHandler,
|
SubmitHandler,
|
||||||
valiForm,
|
valiForm,
|
||||||
} from "@modular-forms/solid";
|
} from "@modular-forms/solid";
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const dirname =
|
|||||||
|
|
||||||
import viteConfig from "./vite.config";
|
import viteConfig from "./vite.config";
|
||||||
|
|
||||||
|
const browser = process.env.BROWSER || "chromium";
|
||||||
|
|
||||||
export default mergeConfig(
|
export default mergeConfig(
|
||||||
viteConfig,
|
viteConfig,
|
||||||
defineConfig({
|
defineConfig({
|
||||||
@@ -42,9 +44,10 @@ export default mergeConfig(
|
|||||||
provider: "playwright",
|
provider: "playwright",
|
||||||
instances: [
|
instances: [
|
||||||
{
|
{
|
||||||
browser: "webkit",
|
browser: "chromium",
|
||||||
launch: {
|
launch: {
|
||||||
executablePath: process.env.PLAYWRIGHT_WEBKIT_EXECUTABLE,
|
// we specify this explicitly to avoid the version matching that playwright tries to do
|
||||||
|
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
|
from clan_lib.api import API # noqa: PLC0415
|
||||||
|
|
||||||
schema = API.to_json_schema()
|
schema = API.to_json_schema()
|
||||||
print(f"""{json.dumps(schema, indent=2)}""")
|
print(f"""{json.dumps(schema, indent=2)}""")
|
||||||
|
|||||||
@@ -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 (
|
from clan_lib.machines.actions import ( # noqa: PLC0415
|
||||||
InventoryMachine,
|
InventoryMachine,
|
||||||
MachineResponse,
|
MachineResponse,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
add_dynamic_completer,
|
add_dynamic_completer,
|
||||||
complete_secrets,
|
complete_secrets,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
add_dynamic_completer,
|
add_dynamic_completer,
|
||||||
complete_users,
|
complete_users,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
add_dynamic_completer,
|
add_dynamic_completer,
|
||||||
complete_machines,
|
complete_machines,
|
||||||
complete_secrets,
|
complete_secrets,
|
||||||
|
|||||||
@@ -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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
add_dynamic_completer,
|
add_dynamic_completer,
|
||||||
complete_users,
|
complete_users,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
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 (
|
from clan_cli.completions import ( # noqa: PLC0415
|
||||||
add_dynamic_completer,
|
add_dynamic_completer,
|
||||||
complete_users,
|
complete_users,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
@@ -430,43 +429,9 @@ 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
|
||||||
|
|||||||
@@ -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
|
from clan_lib.machines.machines import Machine # noqa: PLC0415
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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
|
||||||
@@ -27,33 +26,13 @@ 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
|
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
||||||
|
|
||||||
machine = Machine(name=machine_name, flake=flake)
|
machine = Machine(name=machine_name, flake=flake)
|
||||||
missing_secret_vars = []
|
missing_secret_vars = []
|
||||||
@@ -87,32 +66,15 @@ 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:
|
||||||
health_msg = machine.secret_vars_store.health_check(
|
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 health_msg is not None:
|
if msg:
|
||||||
machine.info(
|
machine.info(
|
||||||
f"Secret var '{file.name}' for service '{generator.name}' in machine {machine.name} needs update: {health_msg}",
|
f"Secret var '{file.name}' for service '{generator.name}' in machine {machine.name} needs update: {msg}",
|
||||||
)
|
)
|
||||||
unfixed_secret_vars.append(file)
|
unfixed_secret_vars.append(file)
|
||||||
|
|
||||||
@@ -144,7 +106,6 @@ 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
|
||||||
|
|||||||
@@ -259,10 +259,6 @@ 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(
|
||||||
@@ -482,7 +478,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
|
from clan_lib.sandbox_exec import sandbox_exec_cmd # noqa: PLC0415
|
||||||
|
|
||||||
cmd = stack.enter_context(sandbox_exec_cmd(str(final_script), tmpdir))
|
cmd = stack.enter_context(sandbox_exec_cmd(str(final_script), tmpdir))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -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
|
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
||||||
|
|
||||||
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,8 +98,7 @@ 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:
|
||||||
if not has_machine(self.flake.path, machine):
|
self.ensure_machine_key(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)
|
||||||
|
|
||||||
@@ -143,7 +142,7 @@ class SecretStore(StoreBase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if generators is None:
|
if generators is None:
|
||||||
from clan_cli.vars.generator import Generator
|
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
||||||
|
|
||||||
generators = Generator.get_machine_generators([machine], self.flake)
|
generators = Generator.get_machine_generators([machine], self.flake)
|
||||||
file_found = False
|
file_found = False
|
||||||
@@ -157,6 +156,8 @@ 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))
|
||||||
@@ -218,7 +219,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
|
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
||||||
|
|
||||||
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:
|
||||||
@@ -282,7 +283,6 @@ 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 (
|
from clan_cli.secrets.secrets import ( # noqa: PLC0415
|
||||||
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
|
from clan_cli.secrets.secrets import update_keys # noqa: PLC0415
|
||||||
|
|
||||||
if generators is None:
|
if generators is None:
|
||||||
from clan_cli.vars.generator import Generator
|
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
||||||
|
|
||||||
generators = Generator.get_machine_generators([machine], self.flake)
|
generators = Generator.get_machine_generators([machine], self.flake)
|
||||||
file_found = False
|
file_found = False
|
||||||
|
|||||||
@@ -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 # Avoid circular imports - many modules import from clan_lib.api
|
import clan_cli # 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 clan_lib # noqa: PLC0415 # 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)
|
||||||
|
|||||||
@@ -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
|
from clan_lib.nix import nix_shell # noqa: PLC0415
|
||||||
|
|
||||||
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
|
from clan_lib.dirs import get_clan_directories # noqa: PLC0415
|
||||||
|
|
||||||
_, relative_dir = get_clan_directories(flake)
|
_, relative_dir = get_clan_directories(flake)
|
||||||
return relative_dir
|
return relative_dir
|
||||||
|
|||||||
@@ -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
|
from clan_lib.machines.actions import list_machines # noqa: PLC0415
|
||||||
|
|
||||||
return list_machines(self, opts)
|
return list_machines(self, opts)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
from clan_lib.dirs import user_history_file # noqa: PLC0415
|
||||||
|
|
||||||
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
|
from clan_lib.dirs import user_history_file # noqa: PLC0415
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
@@ -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
|
from clan_lib.machines.actions import get_machine # noqa: PLC0415
|
||||||
|
|
||||||
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
|
from clan_cli.vars.secret_modules import password_store # noqa: PLC0415
|
||||||
|
|
||||||
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
|
from clan_lib.network.network import get_best_remote # noqa: PLC0415
|
||||||
|
|
||||||
with get_best_remote(self) as remote:
|
with get_best_remote(self) as remote:
|
||||||
return remote
|
return remote
|
||||||
|
|||||||
@@ -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
|
from clan_lib.machines.list import list_machines # noqa: PLC0415
|
||||||
|
|
||||||
machines = list_machines(flake)
|
machines = list_machines(flake)
|
||||||
return list(machines.keys())
|
return list(machines.keys())
|
||||||
|
|||||||
@@ -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
|
from clan_lib.machines.machines import Machine # noqa: PLC0415
|
||||||
|
|
||||||
machine = Machine(name=machine_name, flake=self.flake)
|
machine = Machine(name=machine_name, flake=self.flake)
|
||||||
var = get_machine_var(
|
var = get_machine_var(
|
||||||
|
|||||||
@@ -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
|
from clan_lib.dirs import nixpkgs_source # noqa: PLC0415
|
||||||
|
|
||||||
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
|
from clan_lib.dirs import nixpkgs_flake # noqa: PLC0415
|
||||||
|
|
||||||
return [
|
return [
|
||||||
*nix_command(["shell", "--inputs-from", f"{nixpkgs_flake()!s}"]),
|
*nix_command(["shell", "--inputs-from", f"{nixpkgs_flake()!s}"]),
|
||||||
|
|||||||
@@ -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
|
from clan_lib.network.check import check_machine_ssh_reachable # noqa: PLC0415
|
||||||
|
|
||||||
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
|
from clan_lib.network.check import check_machine_ssh_login # noqa: PLC0415
|
||||||
|
|
||||||
return check_machine_ssh_login(self)
|
return check_machine_ssh_login(self)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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
|
||||||
@@ -153,15 +152,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
|
||||||
generators_to_run = [g for g in all_generators if g.key.name in generators]
|
all_generators = get_generators(machines, full_closure=True)
|
||||||
|
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
|
||||||
generators_to_run = get_generators(
|
generator_objects = get_generators(
|
||||||
machines,
|
machines,
|
||||||
full_closure=full_closure,
|
full_closure=full_closure,
|
||||||
generator_name=generators,
|
generator_name=generators,
|
||||||
@@ -171,30 +170,13 @@ 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 generators_to_run
|
generator.name: prompt_values(generator) for generator in generator_objects
|
||||||
}
|
}
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
@@ -206,13 +188,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 generators_to_run:
|
for generator in generator_objects:
|
||||||
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 generators_to_run:
|
for generator in generator_objects:
|
||||||
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)
|
||||||
|
|||||||
@@ -290,7 +290,9 @@ 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)
|
||||||
|
|
||||||
return sorted(result, key=weight_cmd_groups)
|
result = sorted(result, key=weight_cmd_groups)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def build_command_reference() -> None:
|
def build_command_reference() -> None:
|
||||||
|
|||||||
@@ -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
|
import ctypes # noqa: PLC0415
|
||||||
|
|
||||||
# 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")
|
||||||
|
|||||||
@@ -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
|
from ctypes import Structure # noqa: PLC0415
|
||||||
|
|
||||||
class WNDCLASSW(Structure):
|
class WNDCLASSW(Structure):
|
||||||
"""Windows class structure for window registration."""
|
"""Windows class structure for window registration."""
|
||||||
|
|
||||||
from ctypes import CFUNCTYPE, wintypes
|
from ctypes import CFUNCTYPE, wintypes # noqa: PLC0415
|
||||||
|
|
||||||
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
|
from ctypes import wintypes # noqa: PLC0415
|
||||||
|
|
||||||
_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
|
from ctypes import wintypes # noqa: PLC0415
|
||||||
|
|
||||||
_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
|
from ctypes import wintypes # noqa: PLC0415
|
||||||
|
|
||||||
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
|
from ctypes import wintypes # noqa: PLC0415
|
||||||
|
|
||||||
if msg == self.WM_TRAYICON:
|
if msg == self.WM_TRAYICON:
|
||||||
if l_param == self.WM_RBUTTONUP:
|
if l_param == self.WM_RBUTTONUP:
|
||||||
|
|||||||
Reference in New Issue
Block a user