Compare commits
3 Commits
feat/snaps
...
ke-fix-cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb2054ff10 | ||
|
|
841a8edbdd | ||
|
|
27d9a805d9 |
@@ -1,12 +0,0 @@
|
|||||||
## Description of the change
|
|
||||||
|
|
||||||
<!-- Brief summary of the change if not already clear from the title -->
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [ ] Updated Documentation
|
|
||||||
- [ ] Added tests
|
|
||||||
- [ ] Doesn't affect backwards compatibility - or check the next points
|
|
||||||
- [ ] Add the breaking change and migration details to docs/release-notes.md
|
|
||||||
- !!! Review from another person is required *BEFORE* merge !!!
|
|
||||||
- [ ] Add introduction of major feature to docs/release-notes.md
|
|
||||||
@@ -19,19 +19,28 @@ let
|
|||||||
nixosLib = import (self.inputs.nixpkgs + "/nixos/lib") { };
|
nixosLib = import (self.inputs.nixpkgs + "/nixos/lib") { };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = filter pathExists [
|
imports =
|
||||||
./devshell/flake-module.nix
|
let
|
||||||
./flash/flake-module.nix
|
clanCoreModulesDir = ../nixosModules/clanCore;
|
||||||
./installation/flake-module.nix
|
getClanCoreTestModules =
|
||||||
./update/flake-module.nix
|
let
|
||||||
./morph/flake-module.nix
|
moduleNames = attrNames (builtins.readDir clanCoreModulesDir);
|
||||||
./nixos-documentation/flake-module.nix
|
testPaths = map (
|
||||||
./dont-depend-on-repo-root.nix
|
moduleName: clanCoreModulesDir + "/${moduleName}/tests/flake-module.nix"
|
||||||
# clan core submodule tests
|
) moduleNames;
|
||||||
../nixosModules/clanCore/machine-id/tests/flake-module.nix
|
in
|
||||||
../nixosModules/clanCore/postgresql/tests/flake-module.nix
|
filter pathExists testPaths;
|
||||||
../nixosModules/clanCore/state-version/tests/flake-module.nix
|
in
|
||||||
];
|
getClanCoreTestModules
|
||||||
|
++ filter pathExists [
|
||||||
|
./devshell/flake-module.nix
|
||||||
|
./flash/flake-module.nix
|
||||||
|
./installation/flake-module.nix
|
||||||
|
./update/flake-module.nix
|
||||||
|
./morph/flake-module.nix
|
||||||
|
./nixos-documentation/flake-module.nix
|
||||||
|
./dont-depend-on-repo-root.nix
|
||||||
|
];
|
||||||
flake.check = genAttrs [ "x86_64-linux" "aarch64-darwin" ] (
|
flake.check = genAttrs [ "x86_64-linux" "aarch64-darwin" ] (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
@@ -111,7 +120,7 @@ in
|
|||||||
) (self.darwinConfigurations or { })
|
) (self.darwinConfigurations or { })
|
||||||
// lib.mapAttrs' (n: lib.nameValuePair "package-${n}") (
|
// lib.mapAttrs' (n: lib.nameValuePair "package-${n}") (
|
||||||
if system == "aarch64-darwin" then
|
if system == "aarch64-darwin" then
|
||||||
lib.filterAttrs (n: _: n != "docs" && n != "deploy-docs" && n != "option-search") packagesToBuild
|
lib.filterAttrs (n: _: n != "docs" && n != "deploy-docs" && n != "docs-options") packagesToBuild
|
||||||
else
|
else
|
||||||
packagesToBuild
|
packagesToBuild
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ let
|
|||||||
networking.useNetworkd = true;
|
networking.useNetworkd = true;
|
||||||
services.openssh.enable = true;
|
services.openssh.enable = true;
|
||||||
services.openssh.settings.UseDns = false;
|
services.openssh.settings.UseDns = false;
|
||||||
|
services.openssh.settings.PasswordAuthentication = false;
|
||||||
system.nixos.variant_id = "installer";
|
system.nixos.variant_id = "installer";
|
||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
pkgs.nixos-facter
|
pkgs.nixos-facter
|
||||||
|
|||||||
18
devFlake/flake.lock
generated
18
devFlake/flake.lock
generated
@@ -3,10 +3,10 @@
|
|||||||
"clan-core-for-checks": {
|
"clan-core-for-checks": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1760213549,
|
"lastModified": 1759727242,
|
||||||
"narHash": "sha256-XosVRUEcdsoEdRtXyz9HrRc4Dt9Ke+viM5OVF7tLK50=",
|
"narHash": "sha256-15Q9eXbfsLmzIbYWasZ3Nuqafnc5o9al9RmGuBGVK74=",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"rev": "9c8797e77031d8d472d057894f18a53bdc9bbe1e",
|
"rev": "c737271585ff3df308feab22c09967fce8f278d3",
|
||||||
"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,11 +105,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-dev": {
|
"nixpkgs-dev": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1760161054,
|
"lastModified": 1759670943,
|
||||||
"narHash": "sha256-PO3cKHFIQEPI0dr/SzcZwG50cHXfjoIqP2uS5W78OXg=",
|
"narHash": "sha256-JBjTDfwzAwtd8+5X/Weg27WE/3hVYOP3uggP2JPaQVQ=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e18d8ec6fafaed55561b7a1b54eb1c1ce3ffa2c5",
|
"rev": "21980a9c20f34648121f60bda15f419fa568db21",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -208,11 +208,11 @@
|
|||||||
"nixpkgs": []
|
"nixpkgs": []
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1760120816,
|
"lastModified": 1758728421,
|
||||||
"narHash": "sha256-gq9rdocpmRZCwLS5vsHozwB6b5nrOBDNc2kkEaTXHfg=",
|
"narHash": "sha256-ySNJ008muQAds2JemiyrWYbwbG+V7S5wg3ZVKGHSFu8=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "761ae7aff00907b607125b2f57338b74177697ed",
|
"rev": "5eda4ee8121f97b218f7cc73f5172098d458f1d1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
2
docs/.gitignore
vendored
2
docs/.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
/site/reference
|
/site/reference
|
||||||
/site/services/official
|
/site/services/official
|
||||||
/site/static
|
/site/static
|
||||||
/site/option-search
|
/site/options
|
||||||
/site/openapi.json
|
/site/openapi.json
|
||||||
!/site/static/extra.css
|
!/site/static/extra.css
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Serve documentation locally
|
# Serve documentation locally
|
||||||
|
|
||||||
```
|
```
|
||||||
nix develop .#docs -c mkdocs serve
|
$ nix develop .#docs -c mkdocs serve
|
||||||
```
|
```
|
||||||
|
|||||||
41
docs/main.py
Normal file
41
docs/main.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def define_env(env: Any) -> None:
|
||||||
|
static_dir = "/static/"
|
||||||
|
video_dir = "https://clan.lol/" + "videos/"
|
||||||
|
asciinema_dir = static_dir + "asciinema-player/"
|
||||||
|
|
||||||
|
@env.macro
|
||||||
|
def video(name: str) -> str:
|
||||||
|
return f"""<video loop muted autoplay id="{name}">
|
||||||
|
<source src={video_dir + name} type="video/webm">
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>"""
|
||||||
|
|
||||||
|
@env.macro
|
||||||
|
def asciinema(name: str) -> str:
|
||||||
|
return f"""<div id="{name}">
|
||||||
|
<script>
|
||||||
|
// Function to load the script and then create the Asciinema player
|
||||||
|
function loadAsciinemaPlayer() {{
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = "{asciinema_dir}/asciinema-player.min.js";
|
||||||
|
script.onload = function() {{
|
||||||
|
AsciinemaPlayer.create('{video_dir + name}', document.getElementById("{name}"), {{
|
||||||
|
loop: true,
|
||||||
|
autoPlay: true,
|
||||||
|
controls: false,
|
||||||
|
speed: 1.5,
|
||||||
|
theme: "solarized-light"
|
||||||
|
}});
|
||||||
|
}};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Load the Asciinema player script
|
||||||
|
loadAsciinemaPlayer();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="{asciinema_dir}/asciinema-player.css" />
|
||||||
|
</div>"""
|
||||||
@@ -58,7 +58,7 @@ nav:
|
|||||||
- getting-started/configure-disk.md
|
- getting-started/configure-disk.md
|
||||||
- getting-started/update-machines.md
|
- getting-started/update-machines.md
|
||||||
- getting-started/continuous-integration.md
|
- getting-started/continuous-integration.md
|
||||||
- Convert existing NixOS configurations: getting-started/convert-existing-NixOS-configuration.md
|
- getting-started/convert-existing-NixOS-configuration.md
|
||||||
- Guides:
|
- Guides:
|
||||||
- Inventory:
|
- Inventory:
|
||||||
- Introduction to Inventory: guides/inventory/inventory.md
|
- Introduction to Inventory: guides/inventory/inventory.md
|
||||||
@@ -66,7 +66,6 @@ nav:
|
|||||||
- Services:
|
- Services:
|
||||||
- Introduction to Services: guides/services/introduction-to-services.md
|
- Introduction to Services: guides/services/introduction-to-services.md
|
||||||
- Author Your Own Service: guides/services/community.md
|
- Author Your Own Service: guides/services/community.md
|
||||||
- Internal Services with SSL: guides/internal-ssl-services.md
|
|
||||||
- Vars:
|
- Vars:
|
||||||
- Introduction to Vars: guides/vars/vars-overview.md
|
- Introduction to Vars: guides/vars/vars-overview.md
|
||||||
- Minimal Example: guides/vars/vars-backend.md
|
- Minimal Example: guides/vars/vars-backend.md
|
||||||
@@ -180,7 +179,7 @@ nav:
|
|||||||
- services/official/zerotier.md
|
- services/official/zerotier.md
|
||||||
- services/community.md
|
- services/community.md
|
||||||
|
|
||||||
- Search Clan Options: "/option-search"
|
- Search Clan Options: "/options"
|
||||||
|
|
||||||
docs_dir: site
|
docs_dir: site
|
||||||
site_dir: out
|
site_dir: out
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
module-docs,
|
module-docs,
|
||||||
clan-cli-docs,
|
clan-cli-docs,
|
||||||
clan-lib-openapi,
|
clan-lib-openapi,
|
||||||
|
asciinema-player-js,
|
||||||
|
asciinema-player-css,
|
||||||
roboto,
|
roboto,
|
||||||
fira-code,
|
fira-code,
|
||||||
option-search,
|
docs-options,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -51,9 +53,13 @@ pkgs.stdenv.mkDerivation {
|
|||||||
chmod -R +w ./site
|
chmod -R +w ./site
|
||||||
echo "Generated API documentation in './site/reference/' "
|
echo "Generated API documentation in './site/reference/' "
|
||||||
|
|
||||||
rm -rf ./site/option-search
|
rm -rf ./site/options
|
||||||
cp -r ${option-search} ./site/option-search
|
cp -r ${docs-options} ./site/options
|
||||||
chmod -R +w ./site/option-search
|
chmod -R +w ./site/options
|
||||||
|
|
||||||
|
mkdir -p ./site/static/asciinema-player
|
||||||
|
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
|
||||||
|
ln -snf ${asciinema-player-css} ./site/static/asciinema-player/asciinema-player.css
|
||||||
|
|
||||||
# Link to fonts
|
# Link to fonts
|
||||||
ln -snf ${roboto}/share/fonts/truetype/Roboto-Regular.ttf ./site/static/
|
ln -snf ${roboto}/share/fonts/truetype/Roboto-Regular.ttf ./site/static/
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
{ inputs, ... }:
|
{ inputs, self, ... }:
|
||||||
{
|
{
|
||||||
|
imports = [
|
||||||
|
./options/flake-module.nix
|
||||||
|
];
|
||||||
perSystem =
|
perSystem =
|
||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
@@ -7,7 +10,83 @@
|
|||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
# Simply evaluated options (JSON)
|
||||||
|
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
|
||||||
|
jsonDocs = pkgs.callPackage ./get-module-docs.nix {
|
||||||
|
inherit (self) clanModules;
|
||||||
|
clan-core = self;
|
||||||
|
inherit pkgs;
|
||||||
|
};
|
||||||
|
|
||||||
|
# clan service options
|
||||||
|
clanModulesViaService = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaService);
|
||||||
|
|
||||||
|
# Simply evaluated options (JSON)
|
||||||
|
renderOptions =
|
||||||
|
pkgs.runCommand "render-options"
|
||||||
|
{
|
||||||
|
# TODO: ruff does not splice properly in nativeBuildInputs
|
||||||
|
depsBuildBuild = [ pkgs.ruff ];
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.python3
|
||||||
|
pkgs.mypy
|
||||||
|
self'.packages.clan-cli
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
install -D -m755 ${./render_options}/__init__.py $out/bin/render-options
|
||||||
|
patchShebangs --build $out/bin/render-options
|
||||||
|
|
||||||
|
ruff format --check --diff $out/bin/render-options
|
||||||
|
ruff check --line-length 88 $out/bin/render-options
|
||||||
|
mypy --strict $out/bin/render-options
|
||||||
|
'';
|
||||||
|
|
||||||
|
asciinema-player-js = pkgs.fetchurl {
|
||||||
|
url = "https://github.com/asciinema/asciinema-player/releases/download/v3.7.0/asciinema-player.min.js";
|
||||||
|
sha256 = "sha256-Ymco/+FinDr5YOrV72ehclpp4amrczjo5EU3jfr/zxs=";
|
||||||
|
};
|
||||||
|
asciinema-player-css = pkgs.fetchurl {
|
||||||
|
url = "https://github.com/asciinema/asciinema-player/releases/download/v3.7.0/asciinema-player.css";
|
||||||
|
sha256 = "sha256-GZMeZFFGvP5GMqqh516mjJKfQaiJ6bL38bSYOXkaohc=";
|
||||||
|
};
|
||||||
|
|
||||||
|
module-docs =
|
||||||
|
pkgs.runCommand "rendered"
|
||||||
|
{
|
||||||
|
buildInputs = [
|
||||||
|
pkgs.python3
|
||||||
|
self'.packages.clan-cli
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
export CLAN_CORE_PATH=${
|
||||||
|
inputs.nixpkgs.lib.fileset.toSource {
|
||||||
|
root = ../..;
|
||||||
|
fileset = ../../clanModules;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
|
||||||
|
|
||||||
|
# A file that contains the links to all clanModule docs
|
||||||
|
export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService}
|
||||||
|
export CLAN_SERVICE_INTERFACE=${self'.legacyPackages.clan-service-module-interface}/share/doc/nixos/options.json
|
||||||
|
export CLAN_OPTIONS_PATH=${self'.legacyPackages.clan-options}/share/doc/nixos/options.json
|
||||||
|
|
||||||
|
mkdir $out
|
||||||
|
|
||||||
|
# The python script will place mkDocs files in the output directory
|
||||||
|
exec python3 ${renderOptions}/bin/render-options
|
||||||
|
'';
|
||||||
|
in
|
||||||
{
|
{
|
||||||
|
legacyPackages = {
|
||||||
|
inherit
|
||||||
|
jsonDocs
|
||||||
|
clanModulesViaService
|
||||||
|
;
|
||||||
|
};
|
||||||
devShells.docs = self'.packages.docs.overrideAttrs (_old: {
|
devShells.docs = self'.packages.docs.overrideAttrs (_old: {
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
# Run: htmlproofer --disable-external
|
# Run: htmlproofer --disable-external
|
||||||
@@ -26,20 +105,22 @@
|
|||||||
docs = pkgs.python3.pkgs.callPackage ./default.nix {
|
docs = pkgs.python3.pkgs.callPackage ./default.nix {
|
||||||
inherit (self'.packages)
|
inherit (self'.packages)
|
||||||
clan-cli-docs
|
clan-cli-docs
|
||||||
option-search
|
docs-options
|
||||||
inventory-api-docs
|
inventory-api-docs
|
||||||
clan-lib-openapi
|
clan-lib-openapi
|
||||||
module-docs
|
|
||||||
;
|
;
|
||||||
inherit (inputs) nixpkgs;
|
inherit (inputs) nixpkgs;
|
||||||
|
inherit module-docs;
|
||||||
|
inherit asciinema-player-js;
|
||||||
|
inherit asciinema-player-css;
|
||||||
};
|
};
|
||||||
deploy-docs = pkgs.callPackage ./deploy-docs.nix { inherit (config.packages) docs; };
|
deploy-docs = pkgs.callPackage ./deploy-docs.nix { inherit (config.packages) docs; };
|
||||||
|
inherit module-docs;
|
||||||
};
|
};
|
||||||
checks.docs-integrity =
|
checks.docs-integrity =
|
||||||
pkgs.runCommand "docs-integrity"
|
pkgs.runCommand "docs-integrity"
|
||||||
{
|
{
|
||||||
nativeBuildInputs = [ pkgs.html-proofer ];
|
nativeBuildInputs = [ pkgs.html-proofer ];
|
||||||
LANG = "C.UTF-8";
|
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
# External links should be avoided in the docs, because they often break
|
# External links should be avoided in the docs, because they often break
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
serviceModules = self.clan.modules;
|
serviceModules = self.clan.modules;
|
||||||
|
|
||||||
baseHref = "/option-search/";
|
baseHref = "/options/";
|
||||||
|
|
||||||
getRoles =
|
getRoles =
|
||||||
module:
|
module:
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
_file = "docs flake-module";
|
_file = "docs flake-module";
|
||||||
imports = [
|
imports = [
|
||||||
{ _module.args = { inherit clanLib; }; }
|
{ _module.args = { inherit clanLib; }; }
|
||||||
(import ../../lib/modules/inventoryClass/roles-interface.nix {
|
(import ../../../lib/modules/inventoryClass/roles-interface.nix {
|
||||||
nestedSettingsOption = mkOption {
|
nestedSettingsOption = mkOption {
|
||||||
type = types.raw;
|
type = types.raw;
|
||||||
description = ''
|
description = ''
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
# };
|
# };
|
||||||
|
|
||||||
packages = {
|
packages = {
|
||||||
option-search =
|
docs-options =
|
||||||
if privateInputs ? nuschtos then
|
if privateInputs ? nuschtos then
|
||||||
privateInputs.nuschtos.packages.${pkgs.stdenv.hostPlatform.system}.mkMultiSearch {
|
privateInputs.nuschtos.packages.${pkgs.stdenv.hostPlatform.system}.mkMultiSearch {
|
||||||
inherit baseHref;
|
inherit baseHref;
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# clan-core release notes 25.11
|
|
||||||
|
|
||||||
<!-- This is not rendered yet -->
|
|
||||||
|
|
||||||
## New features
|
|
||||||
|
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
## Misc
|
|
||||||
@@ -4,14 +4,14 @@ This guide will help you convert your existing NixOS configurations into a Clan.
|
|||||||
Migrating instead of starting new can be trickier and might lead to bugs or
|
Migrating instead of starting new can be trickier and might lead to bugs or
|
||||||
unexpected issues. We recommend reading the [Getting Started](../getting-started/creating-your-first-clan.md) guide first.
|
unexpected issues. We recommend reading the [Getting Started](../getting-started/creating-your-first-clan.md) guide first.
|
||||||
|
|
||||||
Once you have a working setup and understand the concepts transferring your NixOS configurations over is easy.
|
Once you have a working setup and understand the concepts transfering your NixOS configurations over is easy.
|
||||||
|
|
||||||
## Back up your existing configuration
|
## Back up your existing configuration
|
||||||
|
|
||||||
Before you start, it is strongly recommended to back up your existing
|
Before you start, it is strongly recommended to back up your existing
|
||||||
configuration in any form you see fit. If you use version control to manage
|
configuration in any form you see fit. If you use version control to manage
|
||||||
your configuration changes, it is also a good idea to follow the migration
|
your configuration changes, it is also a good idea to follow the migration
|
||||||
guide in a separate branch until everything works as expected.
|
guide in a separte branch until everything works as expected.
|
||||||
|
|
||||||
## Starting Point
|
## Starting Point
|
||||||
|
|
||||||
|
|||||||
@@ -67,59 +67,6 @@ nix build .#checks.x86_64-linux.{test-attr-name}
|
|||||||
```
|
```
|
||||||
(replace `{test-attr-name}` with the name of the test)
|
(replace `{test-attr-name}` with the name of the test)
|
||||||
|
|
||||||
### Testing services with vars
|
|
||||||
|
|
||||||
Services that define their own vars (using `clan.core.vars.generators`) require generating test vars before running the tests.
|
|
||||||
|
|
||||||
#### Understanding the `clan.directory` setting
|
|
||||||
|
|
||||||
The `clan.directory` option is critical for vars generation and loading in tests. This setting determines:
|
|
||||||
|
|
||||||
1. **Where vars are generated**: When you run `update-vars`, it creates `vars/` and `sops/` directories inside the path specified by `clan.directory`
|
|
||||||
2. **Where vars are loaded from**: During test execution, machines look for their vars and secrets relative to `clan.directory`
|
|
||||||
|
|
||||||
#### Generating test vars
|
|
||||||
|
|
||||||
For services that define vars, you must first run:
|
|
||||||
|
|
||||||
```shellSession
|
|
||||||
nix run .#checks.x86_64-linux.{test-attr-name}.update-vars
|
|
||||||
```
|
|
||||||
|
|
||||||
This generates the necessary var files in the directory specified by `clan.directory`. After running this command, you can run the test normally:
|
|
||||||
|
|
||||||
```shellSession
|
|
||||||
nix run .#checks.x86_64-linux.{test-attr-name}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example: service-dummy-test
|
|
||||||
|
|
||||||
The `service-dummy-test` is a good example of a test that uses vars. To run it:
|
|
||||||
|
|
||||||
```shellSession
|
|
||||||
# First, generate the test vars
|
|
||||||
nix run .#checks.x86_64-linux.service-dummy-test.update-vars
|
|
||||||
|
|
||||||
# Then run the test
|
|
||||||
nix run .#checks.x86_64-linux.service-dummy-test
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Common issues
|
|
||||||
|
|
||||||
If `update-vars` fails, you may need to ensure that:
|
|
||||||
|
|
||||||
- **`clan.directory` is set correctly**: It should point to the directory where you want vars to be generated (typically `clan.directory = ./.;` in your test definition)
|
|
||||||
- **Your test defines machines**: Machines must be defined in `clan.inventory.machines` or through the inventory system
|
|
||||||
- **Machine definitions are complete**: Each machine should have the necessary service configuration that defines the vars generators
|
|
||||||
|
|
||||||
**If vars are not found during test execution:**
|
|
||||||
|
|
||||||
- Verify that `clan.directory` points to the same location where you ran `update-vars`
|
|
||||||
- Check that the `vars/` and `sops/` directories exist in that location
|
|
||||||
- Ensure the generated files match the machines and generators defined in your test
|
|
||||||
|
|
||||||
You can reference `/checks/service-dummy-test/` to see a complete working example of a test with vars, including the correct directory structure.
|
|
||||||
|
|
||||||
### Debugging VM tests
|
### Debugging VM tests
|
||||||
|
|
||||||
The following techniques can be used to debug a VM test:
|
The following techniques can be used to debug a VM test:
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
A common use case you might have is to host services and applications which are
|
|
||||||
only reachable within your clan.
|
|
||||||
|
|
||||||
This guide explains how to set up such secure, clan-internal web services using
|
|
||||||
a custom top-level domain (TLD) with SSL certificates.
|
|
||||||
|
|
||||||
Your services will be accessible only within your clan network and secured with
|
|
||||||
proper SSL certificates that all clan machines trust.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
By combining the `coredns` and `certificates` clan services, you can:
|
|
||||||
|
|
||||||
- Create a custom TLD for your clan (e.g. `.c`)
|
|
||||||
- Host internal web services accessible via HTTPS (e.g. `https://api.c`, `https://dashboard.c`)
|
|
||||||
- Automatically provision and trust SSL certificates across all clan machines
|
|
||||||
- Keep internal services secure and isolated from the public internet
|
|
||||||
|
|
||||||
The setup uses two clan services working together:
|
|
||||||
|
|
||||||
- **coredns service**: Provides DNS resolution for your custom TLD within the clan
|
|
||||||
- **certificates service**: Creates a certificate authority (CA) and issues SSL certificates for your TLD
|
|
||||||
|
|
||||||
### DNS Resolution Flow
|
|
||||||
|
|
||||||
1. A clan machine tries to access `https://service.c`
|
|
||||||
2. The machine queries its local DNS resolver (unbound)
|
|
||||||
3. For `.c` domains, the query is forwarded to your clan's CoreDNS server. All
|
|
||||||
other domains will be resolved as usual.
|
|
||||||
4. CoreDNS returns the IP address of the machine hosting the service
|
|
||||||
5. The machine connects directly to the service over HTTPS
|
|
||||||
6. The SSL certificate is trusted because all machines trust your clan's CA
|
|
||||||
|
|
||||||
## Step-by-Step Setup
|
|
||||||
|
|
||||||
The following setup assumes you have a VPN (e.g. Zerotier) already running. The
|
|
||||||
IPs configured in the options below will probably the Zerotier-IPs of the
|
|
||||||
respective machines.
|
|
||||||
|
|
||||||
### Configure the CoreDNS Service
|
|
||||||
|
|
||||||
The CoreDNS service has two roles:
|
|
||||||
- `server`: Runs the DNS server for your custom TLD
|
|
||||||
- `default`: Makes machines use the DNS server for TLD resolution and allows exposing services
|
|
||||||
|
|
||||||
Add this to your inventory:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
inventory = {
|
|
||||||
machines = {
|
|
||||||
dns-server = { }; # Machine that will run the DNS server
|
|
||||||
web-server = { }; # Machine that will host web services
|
|
||||||
client = { }; # Any other machines in your clan
|
|
||||||
};
|
|
||||||
|
|
||||||
instances = {
|
|
||||||
coredns = {
|
|
||||||
|
|
||||||
# Add the default role to all machines
|
|
||||||
roles.default.tags = [ "all" ];
|
|
||||||
|
|
||||||
# DNS server for the .c TLD
|
|
||||||
roles.server.machines.dns-server.settings = {
|
|
||||||
ip = "192.168.1.10"; # IP of your DNS server machine
|
|
||||||
tld = "c";
|
|
||||||
};
|
|
||||||
|
|
||||||
# Machine hosting services (example: ca.c and admin.c)
|
|
||||||
roles.default.machines.web-server.settings = {
|
|
||||||
ip = "192.168.1.20"; # IP of your web server
|
|
||||||
services = [ "ca" "admin" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configure the Certificates Service
|
|
||||||
|
|
||||||
The certificates service also has two roles:
|
|
||||||
- `ca`: Sets up the certificate authority on a server
|
|
||||||
- `default`: Makes machines trust the CA and allows them to request certificates
|
|
||||||
|
|
||||||
Add this to your inventory:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
inventory = {
|
|
||||||
instances = {
|
|
||||||
# ... coredns configuration from above ...
|
|
||||||
|
|
||||||
certificates = {
|
|
||||||
|
|
||||||
# Set up CA for .c domain
|
|
||||||
roles.ca.machines.dns-server.settings = {
|
|
||||||
tlds = [ "c" ];
|
|
||||||
acmeEmail = "admin@example.com"; # Optional: your email
|
|
||||||
};
|
|
||||||
|
|
||||||
# Add default role to all machines to trust the CA
|
|
||||||
roles.default.tags = [ "all" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Complete Example Configuration
|
|
||||||
|
|
||||||
Here's a complete working example:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
nventory = {
|
|
||||||
machines = {
|
|
||||||
caserver = { }; # DNS server + CA + web services
|
|
||||||
webserver = { }; # Additional web services
|
|
||||||
client = { }; # Client machine
|
|
||||||
};
|
|
||||||
|
|
||||||
instances = {
|
|
||||||
coredns = {
|
|
||||||
|
|
||||||
# Add the default role to all machines
|
|
||||||
roles.default.tags = [ "all" ];
|
|
||||||
|
|
||||||
# DNS server for the .c TLD
|
|
||||||
roles.server.machines.caserver.settings = {
|
|
||||||
ip = "192.168.8.5";
|
|
||||||
tld = "c";
|
|
||||||
};
|
|
||||||
|
|
||||||
# machine hosting https://ca.c (our CA for SSL)
|
|
||||||
roles.default.machines.caserver.settings = {
|
|
||||||
ip = "192.168.8.5";
|
|
||||||
services = [ "ca" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
# machine hosting https://blub.c (some internal web-service)
|
|
||||||
roles.default.machines.webserver.settings = {
|
|
||||||
ip = "192.168.8.6";
|
|
||||||
services = [ "blub" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Provide https for the .c top-level domain
|
|
||||||
certificates = {
|
|
||||||
|
|
||||||
roles.ca.machines.caserver.settings = {
|
|
||||||
tlds = [ "c" ];
|
|
||||||
acmeEmail = "admin@example.com";
|
|
||||||
};
|
|
||||||
|
|
||||||
roles.default.tags = [ "all" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Your Configuration
|
|
||||||
|
|
||||||
DNS resolution can be tested with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# On any clan machine, test DNS resolution
|
|
||||||
nslookup ca.c
|
|
||||||
nslookup blub.c
|
|
||||||
```
|
|
||||||
|
|
||||||
You should also now be able to visit `https://ca.c` to access the certificate authority or visit `https://blub.c` to access your web service.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### DNS Resolution Issues
|
|
||||||
|
|
||||||
1. **Check if DNS server is running**:
|
|
||||||
```bash
|
|
||||||
# On the DNS server machine
|
|
||||||
systemctl status coredns
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Verify DNS configuration**:
|
|
||||||
```bash
|
|
||||||
# Check if the right nameservers are configured
|
|
||||||
cat /etc/resolv.conf
|
|
||||||
systemctl status systemd-resolved
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Test DNS directly**:
|
|
||||||
```bash
|
|
||||||
# Query the DNS server directly
|
|
||||||
dig @192.168.8.5 ca.c
|
|
||||||
```
|
|
||||||
|
|
||||||
### Certificate Issues
|
|
||||||
|
|
||||||
1. **Check CA status**:
|
|
||||||
```bash
|
|
||||||
# On the CA machine
|
|
||||||
systemctl status step-ca
|
|
||||||
systemctl status nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Verify certificate trust**:
|
|
||||||
```bash
|
|
||||||
# Test certificate trust
|
|
||||||
curl -v https://ca.c
|
|
||||||
openssl s_client -connect ca.c:443 -verify_return_error
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Check ACME configuration**:
|
|
||||||
```bash
|
|
||||||
# View ACME certificates
|
|
||||||
ls /var/lib/acme/
|
|
||||||
journalctl -u acme-ca.c.service
|
|
||||||
```
|
|
||||||
@@ -5,11 +5,11 @@
|
|||||||
## Option 1: Follow `clan-core`
|
## Option 1: Follow `clan-core`
|
||||||
|
|
||||||
- **Pros**:
|
- **Pros**:
|
||||||
- Recommended for most users.
|
- Recommended for most users.
|
||||||
- Verified by our CI and widely used by others.
|
- Verified by our CI and widely used by others.
|
||||||
- **Cons**:
|
- **Cons**:
|
||||||
- Coupled to version bumps in `clan-core`.
|
- Coupled to version bumps in `clan-core`.
|
||||||
- Upstream features and packages may take longer to land.
|
- Upstream features and packages may take longer to land.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -24,10 +24,10 @@ inputs = {
|
|||||||
## Option 2: Use Your Own `nixpkgs` Version
|
## Option 2: Use Your Own `nixpkgs` Version
|
||||||
|
|
||||||
- **Pros**:
|
- **Pros**:
|
||||||
- Faster access to new upstream features and packages.
|
- Faster access to new upstream features and packages.
|
||||||
- **Cons**:
|
- **Cons**:
|
||||||
- Recommended for advanced users.
|
- Recommended for advanced users.
|
||||||
- Not covered by our CI — you’re on the frontier.
|
- Not covered by our CI — you’re on the frontier.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ of their type.
|
|||||||
In the inventory we the assign machines to a type, e.g. by using tags
|
In the inventory we the assign machines to a type, e.g. by using tags
|
||||||
|
|
||||||
```nix title="flake.nix"
|
```nix title="flake.nix"
|
||||||
instances.machine-type = {
|
instnaces.machine-type = {
|
||||||
module.input = "self";
|
module.input = "self";
|
||||||
module.name = "@pinpox/machine-type";
|
module.name = "@pinpox/machine-type";
|
||||||
roles.desktop.tags.desktop = { };
|
roles.desktop.tags.desktop = { };
|
||||||
@@ -303,4 +303,3 @@ instances.machine-type = {
|
|||||||
- [Reference Documentation for Service Authors](../../reference/options/clan_service.md)
|
- [Reference Documentation for Service Authors](../../reference/options/clan_service.md)
|
||||||
- [Migration Guide from ClanModules to ClanServices](../../guides/migrations/migrate-inventory-services.md)
|
- [Migration Guide from ClanModules to ClanServices](../../guides/migrations/migrate-inventory-services.md)
|
||||||
- [Decision that lead to ClanServices](../../decisions/01-Clan-Modules.md)
|
- [Decision that lead to ClanServices](../../decisions/01-Clan-Modules.md)
|
||||||
- [Testing Guide for Services with Vars](../contributing/testing.md#testing-services-with-vars)
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ hide:
|
|||||||
|
|
||||||
command line interface
|
command line interface
|
||||||
|
|
||||||
- [Clan Options](./reference/options/clan.md)
|
- [Clan Options](/options)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ This section of the site provides an overview of available options and commands
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
- [Clan Configuration Option](/options) - for defining a Clan
|
||||||
- Learn how to use the [Clan CLI](../reference/cli/index.md)
|
- Learn how to use the [Clan CLI](../reference/cli/index.md)
|
||||||
- Explore available [services](../services/definition.md)
|
- Explore available [services](../services/definition.md)
|
||||||
- [NixOS Configuration Options](../reference/clan.core/index.md) - Additional options avilable on a NixOS machine.
|
- [NixOS Configuration Options](../reference/clan.core/index.md) - Additional options avilable on a NixOS machine.
|
||||||
- [Search Clan Option](/option-search) - for defining a Clan
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
6
flake.lock
generated
6
flake.lock
generated
@@ -181,11 +181,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1760120816,
|
"lastModified": 1758728421,
|
||||||
"narHash": "sha256-gq9rdocpmRZCwLS5vsHozwB6b5nrOBDNc2kkEaTXHfg=",
|
"narHash": "sha256-ySNJ008muQAds2JemiyrWYbwbG+V7S5wg3ZVKGHSFu8=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "761ae7aff00907b607125b2f57338b74177697ed",
|
"rev": "5eda4ee8121f97b218f7cc73f5172098d458f1d1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -77,8 +77,6 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
# Allows downstream users to inject "unsupported" nixpkgs versions
|
|
||||||
checks.minNixpkgsVersion.ignore = true;
|
|
||||||
};
|
};
|
||||||
systems = import systems;
|
systems = import systems;
|
||||||
imports = [
|
imports = [
|
||||||
|
|||||||
@@ -52,6 +52,8 @@
|
|||||||
"checks/secrets/sops/groups/group/machines/machine"
|
"checks/secrets/sops/groups/group/machines/machine"
|
||||||
"checks/syncthing/introducer/introducer_device_id"
|
"checks/syncthing/introducer/introducer_device_id"
|
||||||
"checks/syncthing/introducer/introducer_test_api"
|
"checks/syncthing/introducer/introducer_test_api"
|
||||||
|
"docs/site/static/asciinema-player/asciinema-player.css"
|
||||||
|
"docs/site/static/asciinema-player/asciinema-player.min.js"
|
||||||
"nixosModules/clanCore/vars/secret/sops/eval-tests/populated/vars/my_machine/my_generator/my_secret"
|
"nixosModules/clanCore/vars/secret/sops/eval-tests/populated/vars/my_machine/my_generator/my_secret"
|
||||||
"pkgs/clan-cli/clan_cli/tests/data/gnupg.conf"
|
"pkgs/clan-cli/clan_cli/tests/data/gnupg.conf"
|
||||||
"pkgs/clan-cli/clan_cli/tests/data/password-store/.gpg-id"
|
"pkgs/clan-cli/clan_cli/tests/data/password-store/.gpg-id"
|
||||||
@@ -92,6 +94,9 @@
|
|||||||
"*.yaml"
|
"*.yaml"
|
||||||
"*.yml"
|
"*.yml"
|
||||||
];
|
];
|
||||||
|
excludes = [
|
||||||
|
"*/asciinema-player/*"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
treefmt.programs.mypy.directories = {
|
treefmt.programs.mypy.directories = {
|
||||||
"clan-cli" = {
|
"clan-cli" = {
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
{ lib }:
|
|
||||||
let
|
|
||||||
sanitizePath =
|
|
||||||
rootPath: path:
|
|
||||||
let
|
|
||||||
storePrefix = builtins.unsafeDiscardStringContext ("${rootPath}");
|
|
||||||
pathStr = lib.removePrefix "/" (
|
|
||||||
lib.removePrefix storePrefix (builtins.unsafeDiscardStringContext (toString path))
|
|
||||||
);
|
|
||||||
in
|
|
||||||
pathStr;
|
|
||||||
|
|
||||||
mkFunctions = rootPath: passthru: virtual_fs: {
|
|
||||||
# Some functions to override lib functions
|
|
||||||
pathExists =
|
|
||||||
path:
|
|
||||||
let
|
|
||||||
pathStr = sanitizePath rootPath path;
|
|
||||||
isPassthru = builtins.any (exclude: (builtins.match exclude pathStr) != null) passthru;
|
|
||||||
in
|
|
||||||
if isPassthru then
|
|
||||||
builtins.pathExists path
|
|
||||||
else
|
|
||||||
let
|
|
||||||
res = virtual_fs ? ${pathStr};
|
|
||||||
in
|
|
||||||
lib.trace "pathExists: '${pathStr}' -> '${lib.generators.toPretty { } res}'" res;
|
|
||||||
readDir =
|
|
||||||
path:
|
|
||||||
let
|
|
||||||
pathStr = sanitizePath rootPath path;
|
|
||||||
base = (pathStr + "/");
|
|
||||||
res = lib.mapAttrs' (name: fileInfo: {
|
|
||||||
name = lib.removePrefix base name;
|
|
||||||
value = fileInfo.type;
|
|
||||||
}) (lib.filterAttrs (n: _: lib.hasPrefix base n) virtual_fs);
|
|
||||||
isPassthru = builtins.any (exclude: (builtins.match exclude pathStr) != null) passthru;
|
|
||||||
in
|
|
||||||
if isPassthru then
|
|
||||||
builtins.readDir path
|
|
||||||
else
|
|
||||||
lib.trace "readDir: '${pathStr}' -> '${lib.generators.toPretty { } res}'" res;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
virtual_fs,
|
|
||||||
rootPath,
|
|
||||||
# Patterns
|
|
||||||
passthru ? [ ],
|
|
||||||
}:
|
|
||||||
mkFunctions rootPath passthru virtual_fs
|
|
||||||
@@ -36,10 +36,6 @@ lib.fix (
|
|||||||
|
|
||||||
# TODO: Flatten our lib functions like this:
|
# TODO: Flatten our lib functions like this:
|
||||||
resolveModule = clanLib.callLib ./resolve-module { };
|
resolveModule = clanLib.callLib ./resolve-module { };
|
||||||
|
|
||||||
fs = {
|
|
||||||
inherit (builtins) pathExists readDir;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
f
|
f
|
||||||
|
|||||||
@@ -149,13 +149,6 @@ let
|
|||||||
# TODO: Add index support in nixpkgs first
|
# TODO: Add index support in nixpkgs first
|
||||||
# else if type.name == "listOf" then
|
# else if type.name == "listOf" then
|
||||||
# handleListOf meta.list
|
# handleListOf meta.list
|
||||||
else if type.name == "either" then
|
|
||||||
# For either(oneOf) types, we skip introspection as we cannot
|
|
||||||
# determine which branch of the union was taken without more context
|
|
||||||
# This *should* be safe, as it can currently mostly be triggered through
|
|
||||||
# The `extraModules` setting of inventory modules and seems to be better
|
|
||||||
# than just aborting entirely.
|
|
||||||
{ }
|
|
||||||
else
|
else
|
||||||
throw "Yet Unsupported type: ${type.name}";
|
throw "Yet Unsupported type: ${type.name}";
|
||||||
in
|
in
|
||||||
|
|||||||
@@ -699,44 +699,4 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
test_listOf_either =
|
|
||||||
let
|
|
||||||
evaluated = eval [
|
|
||||||
{
|
|
||||||
options.extraModules = lib.mkOption {
|
|
||||||
description = "List of modules that can be strings, paths, or attrsets";
|
|
||||||
default = [ ];
|
|
||||||
type = lib.types.listOf (
|
|
||||||
lib.types.oneOf [
|
|
||||||
lib.types.str
|
|
||||||
lib.types.path
|
|
||||||
(lib.types.attrsOf lib.types.anything)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
({
|
|
||||||
_file = "config.nix";
|
|
||||||
extraModules = [
|
|
||||||
"modules/common.nix"
|
|
||||||
./some/path.nix
|
|
||||||
{ config = { }; }
|
|
||||||
];
|
|
||||||
})
|
|
||||||
];
|
|
||||||
result = slib.getPrios { options = evaluated.options; };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit evaluated;
|
|
||||||
# Test that either types in list items return empty objects
|
|
||||||
# This is a behavioral test and not necessarily the correct
|
|
||||||
# behavior. But this is better than crashing on people directly.
|
|
||||||
expr = result.extraModules.__list;
|
|
||||||
expected = [
|
|
||||||
{ }
|
|
||||||
{ }
|
|
||||||
{ }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,13 +133,12 @@ in
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
# Note: we use clanLib.fs here, so that we can override it in tests
|
# TODO: Figure out why this causes infinite recursion
|
||||||
inventory = lib.optionalAttrs (clanLib.fs.pathExists "${directory}/machines") ({
|
inventory.machines = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
|
||||||
imports = lib.mapAttrsToList (name: _t: {
|
builtins.mapAttrs (_n: _v: { }) (
|
||||||
_file = "${directory}/machines/${name}";
|
lib.filterAttrs (_: t: t == "directory") (builtins.readDir "${directory}/machines")
|
||||||
machines.${name} = { };
|
)
|
||||||
}) ((lib.filterAttrs (_: t: t == "directory") (clanLib.fs.readDir "${directory}/machines")));
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
inventory.machines = lib.mapAttrs (_n: _: { }) config.machines;
|
inventory.machines = lib.mapAttrs (_n: _: { }) config.machines;
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
{
|
|
||||||
lib ? import <nixpkgs/lib>,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
clanLibOrig = (import ./.. { inherit lib; }).__unfix__;
|
|
||||||
clanLibWithFs =
|
|
||||||
{ virtual_fs }:
|
|
||||||
lib.fix (
|
|
||||||
lib.extends (
|
|
||||||
final: _:
|
|
||||||
let
|
|
||||||
clan-core = {
|
|
||||||
clanLib = final;
|
|
||||||
modules.clan.default = lib.modules.importApply ./clan { inherit clan-core; };
|
|
||||||
|
|
||||||
# Note: Can add other things to "clan-core"
|
|
||||||
# ... Not needed for this test
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
clan = import ../clan {
|
|
||||||
inherit lib clan-core;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Override clanLib.fs for unit-testing against a virtual filesystem
|
|
||||||
fs = import ../clanTest/virtual-fs.nix { inherit lib; } {
|
|
||||||
inherit rootPath virtual_fs;
|
|
||||||
# Example of a passthru
|
|
||||||
# passthru = [
|
|
||||||
# ".*inventory\.json$"
|
|
||||||
# ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
) clanLibOrig
|
|
||||||
);
|
|
||||||
|
|
||||||
rootPath = ./.;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
test_autoload_directories =
|
|
||||||
let
|
|
||||||
vclan =
|
|
||||||
(clanLibWithFs {
|
|
||||||
virtual_fs = {
|
|
||||||
"machines" = {
|
|
||||||
type = "directory";
|
|
||||||
};
|
|
||||||
"machines/foo-machine" = {
|
|
||||||
type = "directory";
|
|
||||||
};
|
|
||||||
"machines/bar-machine" = {
|
|
||||||
type = "directory";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}).clan
|
|
||||||
{ config.directory = rootPath; };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit vclan;
|
|
||||||
expr = {
|
|
||||||
machines = lib.attrNames vclan.config.inventory.machines;
|
|
||||||
definedInMachinesDir = map (
|
|
||||||
p: lib.hasInfix "/machines/" p
|
|
||||||
) vclan.options.inventory.valueMeta.configuration.options.machines.files;
|
|
||||||
};
|
|
||||||
expected = {
|
|
||||||
machines = [
|
|
||||||
"bar-machine"
|
|
||||||
"foo-machine"
|
|
||||||
];
|
|
||||||
definedInMachinesDir = [
|
|
||||||
true # /machines/foo-machine
|
|
||||||
true # /machines/bar-machine
|
|
||||||
false # <clan-core>/module.nix defines "machines" without members
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Could probably be unified with the previous test
|
|
||||||
# This is here for the sake to show that 'virtual_fs' is a test parameter
|
|
||||||
test_files_are_not_machines =
|
|
||||||
let
|
|
||||||
vclan =
|
|
||||||
(clanLibWithFs {
|
|
||||||
virtual_fs = {
|
|
||||||
"machines" = {
|
|
||||||
type = "directory";
|
|
||||||
};
|
|
||||||
"machines/foo.nix" = {
|
|
||||||
type = "file";
|
|
||||||
};
|
|
||||||
"machines/bar.nix" = {
|
|
||||||
type = "file";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}).clan
|
|
||||||
{ config.directory = rootPath; };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit vclan;
|
|
||||||
expr = {
|
|
||||||
machines = lib.attrNames vclan.config.inventory.machines;
|
|
||||||
};
|
|
||||||
expected = {
|
|
||||||
machines = [ ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ let
|
|||||||
in
|
in
|
||||||
#######
|
#######
|
||||||
{
|
{
|
||||||
autoloading = import ./dir_test.nix { inherit lib; };
|
|
||||||
test_missing_self =
|
test_missing_self =
|
||||||
let
|
let
|
||||||
eval = clan {
|
eval = clan {
|
||||||
|
|||||||
@@ -164,25 +164,13 @@
|
|||||||
config = lib.mkIf (config.clan.core.secrets != { }) {
|
config = lib.mkIf (config.clan.core.secrets != { }) {
|
||||||
clan.core.facts.services = lib.mapAttrs' (
|
clan.core.facts.services = lib.mapAttrs' (
|
||||||
name: service:
|
name: service:
|
||||||
lib.warn
|
lib.warn "clan.core.secrets.${name} is deprecated, use clan.core.facts.services.${name} instead" (
|
||||||
''
|
lib.nameValuePair name ({
|
||||||
###############################################################################
|
secret = service.secrets;
|
||||||
# #
|
public = service.facts;
|
||||||
# clan.core.secrets.${name} clan.core.facts.services.${name} is deprecated #
|
generator = service.generator;
|
||||||
# in favor of "vars" #
|
})
|
||||||
# #
|
)
|
||||||
# Refer to https://docs.clan.lol/guides/migrations/migration-facts-vars/ #
|
|
||||||
# for migration instructions. #
|
|
||||||
# #
|
|
||||||
###############################################################################
|
|
||||||
''
|
|
||||||
(
|
|
||||||
lib.nameValuePair name ({
|
|
||||||
secret = service.secrets;
|
|
||||||
public = service.facts;
|
|
||||||
generator = service.generator;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
) config.clan.core.secrets;
|
) config.clan.core.secrets;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
config.warnings = lib.optionals (config.clan.core.facts.services != { }) [
|
config.warnings = lib.optionals (config.clan.core.facts.services != { }) [
|
||||||
''
|
"Facts are deprecated, please migrate them to vars instead, see: https://docs.clan.lol/guides/migrations/migration-facts-vars/"
|
||||||
###############################################################################
|
|
||||||
# #
|
|
||||||
# Facts are deprecated please migrate any usages to vars instead #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# Refer to https://docs.clan.lol/guides/migrations/migration-facts-vars/ #
|
|
||||||
# for migration instructions. #
|
|
||||||
# #
|
|
||||||
###############################################################################
|
|
||||||
''
|
|
||||||
];
|
];
|
||||||
|
|
||||||
options.clan.core.facts = {
|
options.clan.core.facts = {
|
||||||
|
|||||||
@@ -5,31 +5,33 @@
|
|||||||
let
|
let
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
filterAttrs
|
filterAttrs
|
||||||
|
flatten
|
||||||
mapAttrsToList
|
mapAttrsToList
|
||||||
;
|
;
|
||||||
|
|
||||||
relevantFiles = filterAttrs (
|
|
||||||
_name: f: f.secret && f.deploy && (f.neededFor == "users" || f.neededFor == "services")
|
|
||||||
);
|
|
||||||
|
|
||||||
collectFiles =
|
|
||||||
generators:
|
|
||||||
builtins.concatLists (
|
|
||||||
mapAttrsToList (
|
|
||||||
gen_name: generator:
|
|
||||||
mapAttrsToList (fname: file: {
|
|
||||||
name = fname;
|
|
||||||
generator = gen_name;
|
|
||||||
neededForUsers = file.neededFor == "users";
|
|
||||||
inherit (generator) share;
|
|
||||||
inherit (file)
|
|
||||||
owner
|
|
||||||
group
|
|
||||||
mode
|
|
||||||
restartUnits
|
|
||||||
;
|
|
||||||
}) (relevantFiles generator.files)
|
|
||||||
) generators
|
|
||||||
);
|
|
||||||
in
|
in
|
||||||
collectFiles
|
generators:
|
||||||
|
let
|
||||||
|
relevantFiles =
|
||||||
|
generator:
|
||||||
|
filterAttrs (
|
||||||
|
_name: f: f.secret && f.deploy && (f.neededFor == "users" || f.neededFor == "services")
|
||||||
|
) generator.files;
|
||||||
|
allFiles = flatten (
|
||||||
|
mapAttrsToList (
|
||||||
|
gen_name: generator:
|
||||||
|
mapAttrsToList (fname: file: {
|
||||||
|
name = fname;
|
||||||
|
generator = gen_name;
|
||||||
|
neededForUsers = file.neededFor == "users";
|
||||||
|
inherit (generator) share;
|
||||||
|
inherit (file)
|
||||||
|
owner
|
||||||
|
group
|
||||||
|
mode
|
||||||
|
restartUnits
|
||||||
|
;
|
||||||
|
}) (relevantFiles generator)
|
||||||
|
) generators
|
||||||
|
);
|
||||||
|
in
|
||||||
|
allFiles
|
||||||
|
|||||||
@@ -113,27 +113,15 @@ mkShell {
|
|||||||
# todo darwin support needs some work
|
# todo darwin support needs some work
|
||||||
(lib.optionalString stdenv.hostPlatform.isLinux ''
|
(lib.optionalString stdenv.hostPlatform.isLinux ''
|
||||||
# configure playwright for storybook snapshot testing
|
# configure playwright for storybook snapshot testing
|
||||||
# we only want webkit as that matches what the app is rendered with
|
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||||
|
|
||||||
export PLAYWRIGHT_BROWSERS_PATH=${
|
export PLAYWRIGHT_BROWSERS_PATH=${
|
||||||
playwright-driver.browsers.override {
|
playwright-driver.browsers.override {
|
||||||
withFfmpeg = false;
|
withFfmpeg = false;
|
||||||
withFirefox = false;
|
withFirefox = false;
|
||||||
withWebkit = true;
|
|
||||||
withChromium = false;
|
withChromium = false;
|
||||||
withChromiumHeadlessShell = true;
|
withChromiumHeadlessShell = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export PLAYWRIGHT_HOST_PLATFORM_OVERRIDE="ubuntu-24.04"
|
||||||
# stop playwright from trying to validate it has downloaded the necessary browsers
|
|
||||||
# we are providing them manually via nix
|
|
||||||
|
|
||||||
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true
|
|
||||||
|
|
||||||
# playwright browser drivers are versioned e.g. webkit-2191
|
|
||||||
# this helps us avoid having to update the playwright js dependency everytime we update nixpkgs and vice versa
|
|
||||||
# see vitest.config.js for corresponding launch configuration
|
|
||||||
|
|
||||||
export PLAYWRIGHT_CHROMIUM_EXECUTABLE=$(find -L "$PLAYWRIGHT_BROWSERS_PATH" -type f -name "headless_shell")
|
|
||||||
'');
|
'');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,8 @@
|
|||||||
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;
|
||||||
@@ -34,38 +32,36 @@ 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 ++ [
|
#
|
||||||
ps
|
# nativeBuildInputs = finalAttrs.nativeBuildInputs ++ [
|
||||||
];
|
# ps
|
||||||
|
# ];
|
||||||
npmBuildScript = "test-storybook-static";
|
#
|
||||||
|
# npmBuildScript = "test-storybook-static";
|
||||||
env = {
|
#
|
||||||
PLAYWRIGHT_BROWSERS_PATH = "${playwright-driver.browsers.override {
|
# env = finalAttrs.env // {
|
||||||
withChromiumHeadlessShell = true;
|
# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = 1;
|
||||||
}}";
|
# PLAYWRIGHT_BROWSERS_PATH = "${playwright-driver.browsers.override {
|
||||||
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = true;
|
# withChromiumHeadlessShell = 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
pkgs/clan-app/ui/package-lock.json
generated
16
pkgs/clan-app/ui/package-lock.json
generated
@@ -53,7 +53,7 @@
|
|||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"knip": "^5.61.2",
|
"knip": "^5.61.2",
|
||||||
"markdown-to-jsx": "^7.7.10",
|
"markdown-to-jsx": "^7.7.10",
|
||||||
"playwright": "~1.55.1",
|
"playwright": "~1.53.2",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"postcss-url": "^10.1.3",
|
"postcss-url": "^10.1.3",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
@@ -6956,13 +6956,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.55.1",
|
"version": "1.53.2",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",
|
||||||
"integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
|
"integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.55.1"
|
"playwright-core": "1.53.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -6975,9 +6975,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.55.1",
|
"version": "1.53.2",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz",
|
||||||
"integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
|
"integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -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 --reporter verbose",
|
"test-storybook": "vitest run --project storybook",
|
||||||
"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 -a 127.0.0.1 -p 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 --port 6006 --silent' 'wait-on tcp:127.0.0.1:6006 && npm run test-storybook'"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"knip": "^5.61.2",
|
"knip": "^5.61.2",
|
||||||
"markdown-to-jsx": "^7.7.10",
|
"markdown-to-jsx": "^7.7.10",
|
||||||
"playwright": "~1.55.1",
|
"playwright": "~1.53.2",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"postcss-url": "^10.1.3",
|
"postcss-url": "^10.1.3",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
|
|||||||
@@ -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, within } from "storybook/test";
|
import { expect, fn, waitFor } from "storybook/test";
|
||||||
import { StoryContext } from "@kachurun/storybook-solid-vite";
|
import { StoryContext } from "@kachurun/storybook-solid-vite";
|
||||||
|
|
||||||
const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor;
|
const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor;
|
||||||
@@ -216,11 +216,17 @@ const timeout = process.env.NODE_ENV === "test" ? 500 : 2000;
|
|||||||
export const Primary: Story = {
|
export const Primary: Story = {
|
||||||
args: {
|
args: {
|
||||||
hierarchy: "primary",
|
hierarchy: "primary",
|
||||||
onClick: fn(),
|
onAction: fn(async () => {
|
||||||
|
// wait 500 ms to simulate an action
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||||
|
// randomly fail to check that the loading state still returns to normal
|
||||||
|
if (Math.random() > 0.5) {
|
||||||
|
throw new Error("Action failure");
|
||||||
|
}
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
play: async ({ canvasElement, step, userEvent, args }: StoryContext) => {
|
play: async ({ canvas, step, userEvent, args }: StoryContext) => {
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const buttons = await canvas.findAllByRole("button");
|
const buttons = await canvas.findAllByRole("button");
|
||||||
|
|
||||||
for (const button of buttons) {
|
for (const button of buttons) {
|
||||||
@@ -232,6 +238,14 @@ export const Primary: Story = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await step(`Click on ${testID}`, async () => {
|
await step(`Click on ${testID}`, async () => {
|
||||||
|
// check for the loader
|
||||||
|
const loaders = button.getElementsByClassName("loader");
|
||||||
|
await expect(loaders.length).toEqual(1);
|
||||||
|
|
||||||
|
// assert its width is 0 before we click
|
||||||
|
const [loader] = loaders;
|
||||||
|
await expect(loader.clientWidth).toEqual(0);
|
||||||
|
|
||||||
// move the mouse over the button
|
// move the mouse over the button
|
||||||
await userEvent.hover(button);
|
await userEvent.hover(button);
|
||||||
|
|
||||||
@@ -241,8 +255,33 @@ export const Primary: Story = {
|
|||||||
// click the button
|
// click the button
|
||||||
await userEvent.click(button);
|
await userEvent.click(button);
|
||||||
|
|
||||||
// the click handler should have been called
|
// check the button has changed
|
||||||
await expect(args.onClick).toHaveBeenCalled();
|
await waitFor(
|
||||||
|
async () => {
|
||||||
|
// the action handler should have been called
|
||||||
|
await expect(args.onAction).toHaveBeenCalled();
|
||||||
|
// the button should have a loading class
|
||||||
|
await expect(button).toHaveClass("loading");
|
||||||
|
// the loader should be visible
|
||||||
|
await expect(loader.clientWidth).toBeGreaterThan(0);
|
||||||
|
// the pointer should have changed to wait
|
||||||
|
await expect(getCursorStyle(button)).toEqual("wait");
|
||||||
|
},
|
||||||
|
{ timeout: timeout + 500 },
|
||||||
|
);
|
||||||
|
|
||||||
|
// wait for the action handler to finish
|
||||||
|
await waitFor(
|
||||||
|
async () => {
|
||||||
|
// the loading class should be removed
|
||||||
|
await expect(button).not.toHaveClass("loading");
|
||||||
|
// the loader should be hidden
|
||||||
|
await expect(loader.clientWidth).toEqual(0);
|
||||||
|
// the pointer should be normal
|
||||||
|
await expect(getCursorStyle(button)).toEqual("pointer");
|
||||||
|
},
|
||||||
|
{ timeout: timeout + 500 },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ export const Button = (props: ButtonProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<KobalteButton
|
<KobalteButton
|
||||||
role="button"
|
|
||||||
class={cx(
|
class={cx(
|
||||||
styles.button, // default button class
|
styles.button, // default button class
|
||||||
local.size != "default" && styles[local.size],
|
local.size != "default" && styles[local.size],
|
||||||
|
|||||||
@@ -11,59 +11,6 @@ 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;
|
||||||
@@ -74,5 +21,50 @@ 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,9 +7,11 @@ 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,
|
||||||
@@ -158,47 +160,47 @@ const mockFetcher = <K extends OperationNames>(
|
|||||||
},
|
},
|
||||||
}) satisfies ApiCall<K>;
|
}) satisfies ApiCall<K>;
|
||||||
|
|
||||||
// export const Default: Story = {
|
export const Default: Story = {
|
||||||
// args: {},
|
args: {},
|
||||||
// decorators: [
|
decorators: [
|
||||||
// (Story: StoryObj) => {
|
(Story: StoryObj) => {
|
||||||
// const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
// defaultOptions: {
|
defaultOptions: {
|
||||||
// queries: {
|
queries: {
|
||||||
// retry: false,
|
retry: false,
|
||||||
// staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
// });
|
});
|
||||||
//
|
|
||||||
// Object.entries(queryData).forEach(([clanURI, clan]) => {
|
Object.entries(queryData).forEach(([clanURI, clan]) => {
|
||||||
// queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
// ["clans", encodeBase64(clanURI), "details"],
|
["clans", encodeBase64(clanURI), "details"],
|
||||||
// clan.details,
|
clan.details,
|
||||||
// );
|
);
|
||||||
//
|
|
||||||
// const machines = clan.machines || {};
|
const machines = clan.machines || {};
|
||||||
//
|
|
||||||
// queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
// ["clans", encodeBase64(clanURI), "machines"],
|
["clans", encodeBase64(clanURI), "machines"],
|
||||||
// machines,
|
machines,
|
||||||
// );
|
);
|
||||||
//
|
|
||||||
// Object.entries(machines).forEach(([name, machine]) => {
|
Object.entries(machines).forEach(([name, machine]) => {
|
||||||
// queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
// ["clans", encodeBase64(clanURI), "machine", name, "state"],
|
["clans", encodeBase64(clanURI), "machine", name, "state"],
|
||||||
// machine.state,
|
machine.state,
|
||||||
// );
|
);
|
||||||
// });
|
});
|
||||||
// });
|
});
|
||||||
//
|
|
||||||
// return (
|
return (
|
||||||
// <ApiClientProvider client={{ fetch: mockFetcher }}>
|
<ApiClientProvider client={{ fetch: mockFetcher }}>
|
||||||
// <QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
// <Story />
|
<Story />
|
||||||
// </QueryClientProvider>
|
</QueryClientProvider>
|
||||||
// </ApiClientProvider>
|
</ApiClientProvider>
|
||||||
// );
|
);
|
||||||
// },
|
},
|
||||||
// ],
|
],
|
||||||
// };
|
};
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ 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>;
|
||||||
|
|
||||||
@@ -31,13 +30,6 @@ 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;
|
||||||
@@ -48,140 +40,133 @@ 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
|
<>
|
||||||
// solid-js scope.
|
<SidebarSectionForm
|
||||||
render: (args: SidebarPaneProps) => (
|
title="General"
|
||||||
<SidebarPane
|
schema={v.object({
|
||||||
{...args}
|
firstName: v.pipe(
|
||||||
children={
|
v.string(),
|
||||||
<>
|
v.nonEmpty("Please enter a first name."),
|
||||||
<SidebarSectionForm
|
),
|
||||||
title="General"
|
lastName: v.pipe(
|
||||||
schema={v.object({
|
v.string(),
|
||||||
firstName: v.pipe(
|
v.nonEmpty("Please enter a last name."),
|
||||||
v.string(),
|
),
|
||||||
v.nonEmpty("Please enter a first name."),
|
bio: v.string(),
|
||||||
),
|
shareProfile: v.optional(v.boolean()),
|
||||||
lastName: v.pipe(
|
})}
|
||||||
v.string(),
|
initialValues={profiles.ron}
|
||||||
v.nonEmpty("Please enter a last name."),
|
onSubmit={async () => {
|
||||||
),
|
console.log("saving general");
|
||||||
bio: v.string(),
|
}}
|
||||||
shareProfile: v.optional(v.boolean()),
|
>
|
||||||
})}
|
{({ editing, Field }) => (
|
||||||
initialValues={profiles.ron}
|
<div class="flex flex-col gap-3">
|
||||||
onSubmit={async () => {
|
<Field name="firstName">
|
||||||
console.log("saving general");
|
{(field, input) => (
|
||||||
}}
|
<TextInput
|
||||||
>
|
{...field}
|
||||||
{({ editing, Field }) => (
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<Field name="firstName">
|
|
||||||
{(field, input) => (
|
|
||||||
<TextInput
|
|
||||||
{...field}
|
|
||||||
size="s"
|
|
||||||
inverted
|
|
||||||
label="First Name"
|
|
||||||
value={field.value}
|
|
||||||
required
|
|
||||||
readOnly={!editing}
|
|
||||||
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"
|
|
||||||
label="Share"
|
|
||||||
inverted
|
|
||||||
readOnly={!editing}
|
|
||||||
orientation="horizontal"
|
|
||||||
input={input}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SidebarSectionForm>
|
|
||||||
<SidebarSectionForm
|
|
||||||
title="Tags"
|
|
||||||
schema={v.object({
|
|
||||||
tags: v.pipe(v.array(v.string()), v.nonEmpty()),
|
|
||||||
})}
|
|
||||||
initialValues={profiles.ron}
|
|
||||||
onSubmit={async (values) => {
|
|
||||||
console.log("saving tags", values);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ editing, Field, formStore }) => (
|
|
||||||
<Field name="tags" type="string[]">
|
|
||||||
{(field, props) => (
|
|
||||||
<MachineTags
|
|
||||||
{...splitProps(field, ["value"])[1]}
|
|
||||||
size="s"
|
size="s"
|
||||||
onChange={(newVal) => {
|
|
||||||
// Workaround for now, until we manage to use native events
|
|
||||||
setValue(formStore, field.name, newVal);
|
|
||||||
}}
|
|
||||||
inverted
|
inverted
|
||||||
|
label="First Name"
|
||||||
|
value={field.value}
|
||||||
required
|
required
|
||||||
readOnly={!editing}
|
readOnly={!editing}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
defaultValue={field.value}
|
input={input}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
<Divider />
|
||||||
</SidebarSectionForm>
|
<Field name="lastName">
|
||||||
<SidebarSection title="Simple">
|
{(field, input) => (
|
||||||
<Typography tag="h2" hierarchy="title" size="m" inverted>
|
<TextInput
|
||||||
Static Content
|
{...field}
|
||||||
</Typography>
|
size="s"
|
||||||
<Typography hierarchy="label" size="s" inverted>
|
inverted
|
||||||
This is a non-form section with static content
|
label="Last Name"
|
||||||
</Typography>
|
value={field.value}
|
||||||
</SidebarSection>
|
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"
|
||||||
|
label="Share"
|
||||||
|
inverted
|
||||||
|
readOnly={!editing}
|
||||||
|
orientation="horizontal"
|
||||||
|
input={input}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SidebarSectionForm>
|
||||||
|
<SidebarSectionForm
|
||||||
|
title="Tags"
|
||||||
|
schema={v.object({
|
||||||
|
tags: v.pipe(v.array(v.string()), v.nonEmpty()),
|
||||||
|
})}
|
||||||
|
initialValues={profiles.ron}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
console.log("saving tags", values);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ editing, Field, formStore }) => (
|
||||||
|
<Field name="tags" type="string[]">
|
||||||
|
{(field, props) => (
|
||||||
|
<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,5 +1,6 @@
|
|||||||
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> = {
|
||||||
@@ -12,35 +13,61 @@ export default meta;
|
|||||||
type Story = StoryObj<ToolbarProps>;
|
type Story = StoryObj<ToolbarProps>;
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
// We have to specify children inside a render function to avoid issues with reactivity outside a solid-js context.
|
args: {
|
||||||
|
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
|
<Toolbar {...args} />
|
||||||
{...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,6 +1,7 @@
|
|||||||
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",
|
||||||
@@ -12,23 +13,6 @@ 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;
|
||||||
@@ -39,6 +23,12 @@ 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>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,35 +11,28 @@ export default meta;
|
|||||||
|
|
||||||
type Story = StoryObj<ClanSettingsModalProps>;
|
type Story = StoryObj<ClanSettingsModalProps>;
|
||||||
|
|
||||||
const props: ClanSettingsModalProps = {
|
export const Default: Story = {
|
||||||
onClose: fn(),
|
args: {
|
||||||
model: {
|
onClose: fn(),
|
||||||
uri: "/home/foo/my-clan",
|
model: {
|
||||||
details: {
|
uri: "/home/foo/my-clan",
|
||||||
name: "Sol",
|
name: "Sol",
|
||||||
description: null,
|
description: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
},
|
fieldsSchema: {
|
||||||
fieldsSchema: {
|
name: {
|
||||||
name: {
|
readonly: true,
|
||||||
readonly: true,
|
reason: null,
|
||||||
reason: null,
|
},
|
||||||
readonly_members: [],
|
description: {
|
||||||
},
|
readonly: false,
|
||||||
description: {
|
reason: null,
|
||||||
readonly: false,
|
},
|
||||||
reason: null,
|
icon: {
|
||||||
readonly_members: [],
|
readonly: false,
|
||||||
},
|
reason: null,
|
||||||
icon: {
|
},
|
||||||
readonly: false,
|
|
||||||
reason: null,
|
|
||||||
readonly_members: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
args: props,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import { Alert } from "@/src/components/Alert/Alert";
|
|||||||
import { removeClanURI } from "@/src/stores/clan";
|
import { removeClanURI } from "@/src/stores/clan";
|
||||||
|
|
||||||
const schema = v.object({
|
const schema = v.object({
|
||||||
name: v.string(),
|
name: v.pipe(v.optional(v.string())),
|
||||||
description: v.optional(v.string()),
|
description: v.nullish(v.string()),
|
||||||
icon: v.optional(v.string()),
|
icon: v.pipe(v.nullish(v.string())),
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface ClanSettingsModalProps {
|
export interface ClanSettingsModalProps {
|
||||||
|
|||||||
15
pkgs/clan-app/ui/src/scene/cubes.stories.tsx
Normal file
15
pkgs/clan-app/ui/src/scene/cubes.stories.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||||
|
import { CubeScene } from "./cubes";
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: "scene/cubes",
|
||||||
|
component: CubeScene,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
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";
|
||||||
@@ -303,10 +304,11 @@ const FlashProgress = () => {
|
|||||||
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
|
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const result = await store.flash?.progress?.result;
|
const result = await store.flash.progress.result;
|
||||||
if (result?.status == "success") {
|
if (result.status == "success") {
|
||||||
stepSignal.next();
|
console.log("Flashing Success");
|
||||||
}
|
}
|
||||||
|
stepSignal.next();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
|
|||||||
@@ -165,23 +165,23 @@ export default meta;
|
|||||||
|
|
||||||
type Story = StoryObj<typeof ServiceWorkflow>;
|
type Story = StoryObj<typeof ServiceWorkflow>;
|
||||||
|
|
||||||
// export const Default: Story = {
|
export const Default: Story = {
|
||||||
// args: {},
|
args: {},
|
||||||
// };
|
};
|
||||||
//
|
|
||||||
// export const SelectRoleMembers: Story = {
|
export const SelectRoleMembers: Story = {
|
||||||
// render: () => (
|
render: () => (
|
||||||
// <ServiceWorkflow
|
<ServiceWorkflow
|
||||||
// handleSubmit={(instance) => {
|
handleSubmit={(instance) => {
|
||||||
// console.log("Submitted instance:", instance);
|
console.log("Submitted instance:", instance);
|
||||||
// }}
|
}}
|
||||||
// onClose={() => {
|
onClose={() => {
|
||||||
// console.log("Closed");
|
console.log("Closed");
|
||||||
// }}
|
}}
|
||||||
// initialStep="select:members"
|
initialStep="select:members"
|
||||||
// initialStore={{
|
initialStore={{
|
||||||
// currentRole: "peer",
|
currentRole: "peer",
|
||||||
// }}
|
}}
|
||||||
// />
|
/>
|
||||||
// ),
|
),
|
||||||
// };
|
};
|
||||||
|
|||||||
@@ -9,11 +9,7 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"jsxImportSource": "solid-js",
|
"jsxImportSource": "solid-js",
|
||||||
"types": [
|
"types": ["vite/client", "vite-plugin-solid-svg/types-component-solid"],
|
||||||
"vite/client",
|
|
||||||
"vite-plugin-solid-svg/types-component-solid",
|
|
||||||
"@vitest/browser/providers/playwright"
|
|
||||||
],
|
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ 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,15 +40,7 @@ export default mergeConfig(
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
headless: true,
|
headless: true,
|
||||||
provider: "playwright",
|
provider: "playwright",
|
||||||
instances: [
|
instances: [{ browser: "chromium" }],
|
||||||
{
|
|
||||||
browser: "chromium",
|
|
||||||
launch: {
|
|
||||||
// we specify this explicitly to avoid the version matching that playwright tries to do
|
|
||||||
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
// This setup file applies Storybook project annotations for Vitest
|
// This setup file applies Storybook project annotations for Vitest
|
||||||
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
||||||
|
|||||||
@@ -75,14 +75,13 @@ class TestFlake(Flake):
|
|||||||
def path(self) -> Path:
|
def path(self) -> Path:
|
||||||
return self.test_dir
|
return self.test_dir
|
||||||
|
|
||||||
def machine_selector(self, machine_name: str, selector: str) -> str:
|
def select_machine(self, machine_name: str, selector: str) -> Any:
|
||||||
"""Create a selector for a specific machine.
|
"""Select a nix attribute for a specific machine.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
machine_name: The name of the machine
|
machine_name: The name of the machine
|
||||||
selector: The attribute selector string relative to the machine config
|
selector: The attribute selector string relative to the machine config
|
||||||
Returns:
|
apply: Optional function to apply to the result
|
||||||
The full selector string for the machine
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
config = nix_config()
|
config = nix_config()
|
||||||
@@ -90,7 +89,9 @@ class TestFlake(Flake):
|
|||||||
test_system = system
|
test_system = system
|
||||||
if system.endswith("-darwin"):
|
if system.endswith("-darwin"):
|
||||||
test_system = system.rstrip("darwin") + "linux"
|
test_system = system.rstrip("darwin") + "linux"
|
||||||
return f'checks."{test_system}".{self.check_attr}.machinesCross."{system}"."{machine_name}".{selector}'
|
|
||||||
|
full_selector = f'checks."{test_system}".{self.check_attr}.machinesCross.{system}."{machine_name}".{selector}'
|
||||||
|
return self.select(full_selector)
|
||||||
|
|
||||||
# we don't want to evaluate all machines of the flake. Only the ones defined in the test
|
# we don't want to evaluate all machines of the flake. Only the ones defined in the test
|
||||||
def set_machine_names(self, machine_names: list[str]) -> None:
|
def set_machine_names(self, machine_names: list[str]) -> None:
|
||||||
|
|||||||
@@ -158,10 +158,8 @@ def encrypt_secret(
|
|||||||
admin_keys = sops.ensure_admin_public_keys(flake_dir)
|
admin_keys = sops.ensure_admin_public_keys(flake_dir)
|
||||||
|
|
||||||
if not admin_keys:
|
if not admin_keys:
|
||||||
msg = (
|
# TODO double check the correct command to run
|
||||||
"No admin keys found.\n\n"
|
msg = "No keys found. Please run 'clan secrets add-key' to add a key."
|
||||||
"Please run 'clan vars keygen' to generate and set up keys."
|
|
||||||
)
|
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
username = next(iter(admin_keys)).username
|
username = next(iter(admin_keys)).username
|
||||||
|
|||||||
@@ -355,10 +355,7 @@ def get_public_age_key_from_private_key(privkey: str) -> str:
|
|||||||
cmd = nix_shell(["age"], ["age-keygen", "-y"])
|
cmd = nix_shell(["age"], ["age-keygen", "-y"])
|
||||||
|
|
||||||
error_msg = "Failed to get public key for age private key. Is the key malformed?"
|
error_msg = "Failed to get public key for age private key. Is the key malformed?"
|
||||||
res = run(
|
res = run(cmd, RunOpts(input=privkey.encode(), error_msg=error_msg))
|
||||||
cmd,
|
|
||||||
RunOpts(input=privkey.encode(), error_msg=error_msg, sensitive_input=True),
|
|
||||||
)
|
|
||||||
return res.stdout.rstrip(os.linesep).rstrip()
|
return res.stdout.rstrip(os.linesep).rstrip()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
# Use this path to our repo root e.g. for UI test
|
||||||
|
# inputs.clan-core.url = "../../../../.";
|
||||||
|
|
||||||
|
# this placeholder is replaced by the path to nixpkgs
|
||||||
|
inputs.clan-core.url = "__CLAN_CORE__";
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ self, clan-core }:
|
||||||
|
let
|
||||||
|
clan = clan-core.lib.clan {
|
||||||
|
inherit self;
|
||||||
|
meta.name = "test_flake_with_core_dynamic_machines";
|
||||||
|
machines =
|
||||||
|
let
|
||||||
|
machineModules = builtins.readDir (self + "/machines");
|
||||||
|
in
|
||||||
|
builtins.mapAttrs (name: _type: import (self + "/machines/${name}")) machineModules;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -166,16 +166,16 @@ def test_generate_public_and_secret_vars(
|
|||||||
assert shared_value.startswith("shared")
|
assert shared_value.startswith("shared")
|
||||||
vars_text = stringify_all_vars(machine)
|
vars_text = stringify_all_vars(machine)
|
||||||
flake_obj = Flake(str(flake.path))
|
flake_obj = Flake(str(flake.path))
|
||||||
my_generator = Generator("my_generator", machines=["my_machine"], _flake=flake_obj)
|
my_generator = Generator("my_generator", machine="my_machine", _flake=flake_obj)
|
||||||
shared_generator = Generator(
|
shared_generator = Generator(
|
||||||
"my_shared_generator",
|
"my_shared_generator",
|
||||||
share=True,
|
share=True,
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
dependent_generator = Generator(
|
dependent_generator = Generator(
|
||||||
"dependent_generator",
|
"dependent_generator",
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
in_repo_store = in_repo.FactStore(flake=flake_obj)
|
in_repo_store = in_repo.FactStore(flake=flake_obj)
|
||||||
@@ -340,12 +340,12 @@ def test_generate_secret_var_sops_with_default_group(
|
|||||||
flake_obj = Flake(str(flake.path))
|
flake_obj = Flake(str(flake.path))
|
||||||
first_generator = Generator(
|
first_generator = Generator(
|
||||||
"first_generator",
|
"first_generator",
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
second_generator = Generator(
|
second_generator = Generator(
|
||||||
"second_generator",
|
"second_generator",
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
in_repo_store = in_repo.FactStore(flake=flake_obj)
|
in_repo_store = in_repo.FactStore(flake=flake_obj)
|
||||||
@@ -375,13 +375,13 @@ def test_generate_secret_var_sops_with_default_group(
|
|||||||
first_generator_with_share = Generator(
|
first_generator_with_share = Generator(
|
||||||
"first_generator",
|
"first_generator",
|
||||||
share=False,
|
share=False,
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
second_generator_with_share = Generator(
|
second_generator_with_share = Generator(
|
||||||
"second_generator",
|
"second_generator",
|
||||||
share=False,
|
share=False,
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
assert sops_store.user_has_access("user2", first_generator_with_share, "my_secret")
|
assert sops_store.user_has_access("user2", first_generator_with_share, "my_secret")
|
||||||
@@ -432,6 +432,7 @@ def test_generated_shared_secret_sops(
|
|||||||
assert check_vars(machine1.name, machine1.flake)
|
assert check_vars(machine1.name, machine1.flake)
|
||||||
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)
|
assert check_vars(machine2.name, machine2.flake)
|
||||||
|
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
|
||||||
@@ -512,28 +513,28 @@ def test_generate_secret_var_password_store(
|
|||||||
"my_generator",
|
"my_generator",
|
||||||
share=False,
|
share=False,
|
||||||
files=[],
|
files=[],
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
my_generator_shared = Generator(
|
my_generator_shared = Generator(
|
||||||
"my_generator",
|
"my_generator",
|
||||||
share=True,
|
share=True,
|
||||||
files=[],
|
files=[],
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
my_shared_generator = Generator(
|
my_shared_generator = Generator(
|
||||||
"my_shared_generator",
|
"my_shared_generator",
|
||||||
share=True,
|
share=True,
|
||||||
files=[],
|
files=[],
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
my_shared_generator_not_shared = Generator(
|
my_shared_generator_not_shared = Generator(
|
||||||
"my_shared_generator",
|
"my_shared_generator",
|
||||||
share=False,
|
share=False,
|
||||||
files=[],
|
files=[],
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
assert store.exists(my_generator, "my_secret")
|
assert store.exists(my_generator, "my_secret")
|
||||||
@@ -545,7 +546,7 @@ def test_generate_secret_var_password_store(
|
|||||||
name="my_generator",
|
name="my_generator",
|
||||||
share=False,
|
share=False,
|
||||||
files=[],
|
files=[],
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
assert store.get(generator, "my_secret").decode() == "hello\n"
|
assert store.get(generator, "my_secret").decode() == "hello\n"
|
||||||
@@ -556,7 +557,7 @@ def test_generate_secret_var_password_store(
|
|||||||
"my_generator",
|
"my_generator",
|
||||||
share=False,
|
share=False,
|
||||||
files=[],
|
files=[],
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
var_name = "my_secret"
|
var_name = "my_secret"
|
||||||
@@ -569,7 +570,7 @@ def test_generate_secret_var_password_store(
|
|||||||
"my_generator2",
|
"my_generator2",
|
||||||
share=False,
|
share=False,
|
||||||
files=[],
|
files=[],
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
var_name = "my_secret2"
|
var_name = "my_secret2"
|
||||||
@@ -581,7 +582,7 @@ def test_generate_secret_var_password_store(
|
|||||||
"my_shared_generator",
|
"my_shared_generator",
|
||||||
share=True,
|
share=True,
|
||||||
files=[],
|
files=[],
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
var_name = "my_shared_secret"
|
var_name = "my_shared_secret"
|
||||||
@@ -628,8 +629,8 @@ def test_generate_secret_for_multiple_machines(
|
|||||||
in_repo_store2 = in_repo.FactStore(flake=flake_obj)
|
in_repo_store2 = in_repo.FactStore(flake=flake_obj)
|
||||||
|
|
||||||
# Create generators for each machine
|
# Create generators for each machine
|
||||||
gen1 = Generator("my_generator", machines=["machine1"], _flake=flake_obj)
|
gen1 = Generator("my_generator", machine="machine1", _flake=flake_obj)
|
||||||
gen2 = Generator("my_generator", machines=["machine2"], _flake=flake_obj)
|
gen2 = Generator("my_generator", machine="machine2", _flake=flake_obj)
|
||||||
|
|
||||||
assert in_repo_store1.exists(gen1, "my_value")
|
assert in_repo_store1.exists(gen1, "my_value")
|
||||||
assert in_repo_store2.exists(gen2, "my_value")
|
assert in_repo_store2.exists(gen2, "my_value")
|
||||||
@@ -693,12 +694,12 @@ def test_prompt(
|
|||||||
|
|
||||||
# Set up objects for testing the results
|
# Set up objects for testing the results
|
||||||
flake_obj = Flake(str(flake.path))
|
flake_obj = Flake(str(flake.path))
|
||||||
my_generator = Generator("my_generator", machines=["my_machine"], _flake=flake_obj)
|
my_generator = Generator("my_generator", machine="my_machine", _flake=flake_obj)
|
||||||
my_generator_with_details = Generator(
|
my_generator_with_details = Generator(
|
||||||
name="my_generator",
|
name="my_generator",
|
||||||
share=False,
|
share=False,
|
||||||
files=[],
|
files=[],
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -783,10 +784,10 @@ def test_shared_vars_regeneration(
|
|||||||
in_repo_store_2 = in_repo.FactStore(machine2.flake)
|
in_repo_store_2 = in_repo.FactStore(machine2.flake)
|
||||||
# Create generators with machine context for testing
|
# Create generators with machine context for testing
|
||||||
child_gen_m1 = Generator(
|
child_gen_m1 = Generator(
|
||||||
"child_generator", share=False, machines=["machine1"], _flake=machine1.flake
|
"child_generator", share=False, machine="machine1", _flake=machine1.flake
|
||||||
)
|
)
|
||||||
child_gen_m2 = Generator(
|
child_gen_m2 = Generator(
|
||||||
"child_generator", share=False, machines=["machine2"], _flake=machine2.flake
|
"child_generator", share=False, machine="machine2", _flake=machine2.flake
|
||||||
)
|
)
|
||||||
# generate for machine 1
|
# generate for machine 1
|
||||||
cli.run(["vars", "generate", "--flake", str(flake.path), "machine1"])
|
cli.run(["vars", "generate", "--flake", str(flake.path), "machine1"])
|
||||||
@@ -854,13 +855,13 @@ def test_multi_machine_shared_vars(
|
|||||||
generator_m1 = Generator(
|
generator_m1 = Generator(
|
||||||
"shared_generator",
|
"shared_generator",
|
||||||
share=True,
|
share=True,
|
||||||
machines=["machine1"],
|
machine="machine1",
|
||||||
_flake=machine1.flake,
|
_flake=machine1.flake,
|
||||||
)
|
)
|
||||||
generator_m2 = Generator(
|
generator_m2 = Generator(
|
||||||
"shared_generator",
|
"shared_generator",
|
||||||
share=True,
|
share=True,
|
||||||
machines=["machine2"],
|
machine="machine2",
|
||||||
_flake=machine2.flake,
|
_flake=machine2.flake,
|
||||||
)
|
)
|
||||||
# generate for machine 1
|
# generate for machine 1
|
||||||
@@ -916,9 +917,7 @@ def test_api_set_prompts(
|
|||||||
)
|
)
|
||||||
machine = Machine(name="my_machine", flake=Flake(str(flake.path)))
|
machine = Machine(name="my_machine", flake=Flake(str(flake.path)))
|
||||||
store = in_repo.FactStore(machine.flake)
|
store = in_repo.FactStore(machine.flake)
|
||||||
my_generator = Generator(
|
my_generator = Generator("my_generator", machine="my_machine", _flake=machine.flake)
|
||||||
"my_generator", machines=["my_machine"], _flake=machine.flake
|
|
||||||
)
|
|
||||||
assert store.exists(my_generator, "prompt1")
|
assert store.exists(my_generator, "prompt1")
|
||||||
assert store.get(my_generator, "prompt1").decode() == "input1"
|
assert store.get(my_generator, "prompt1").decode() == "input1"
|
||||||
run_generators(
|
run_generators(
|
||||||
@@ -1062,10 +1061,10 @@ def test_migration(
|
|||||||
assert "Migrated var my_generator/my_value" in caplog.text
|
assert "Migrated var my_generator/my_value" in caplog.text
|
||||||
assert "Migrated secret var my_generator/my_secret" in caplog.text
|
assert "Migrated secret var my_generator/my_secret" in caplog.text
|
||||||
flake_obj = Flake(str(flake.path))
|
flake_obj = Flake(str(flake.path))
|
||||||
my_generator = Generator("my_generator", machines=["my_machine"], _flake=flake_obj)
|
my_generator = Generator("my_generator", machine="my_machine", _flake=flake_obj)
|
||||||
other_generator = Generator(
|
other_generator = Generator(
|
||||||
"other_generator",
|
"other_generator",
|
||||||
machines=["my_machine"],
|
machine="my_machine",
|
||||||
_flake=flake_obj,
|
_flake=flake_obj,
|
||||||
)
|
)
|
||||||
in_repo_store = in_repo.FactStore(flake=flake_obj)
|
in_repo_store = in_repo.FactStore(flake=flake_obj)
|
||||||
@@ -1211,7 +1210,7 @@ def test_share_mode_switch_regenerates_secret(
|
|||||||
sops_store = sops.SecretStore(flake=flake_obj)
|
sops_store = sops.SecretStore(flake=flake_obj)
|
||||||
|
|
||||||
generator_not_shared = Generator(
|
generator_not_shared = Generator(
|
||||||
"my_generator", share=False, machines=["my_machine"], _flake=flake_obj
|
"my_generator", share=False, machine="my_machine", _flake=flake_obj
|
||||||
)
|
)
|
||||||
|
|
||||||
initial_public = in_repo_store.get(generator_not_shared, "my_value").decode()
|
initial_public = in_repo_store.get(generator_not_shared, "my_value").decode()
|
||||||
@@ -1230,7 +1229,7 @@ def test_share_mode_switch_regenerates_secret(
|
|||||||
|
|
||||||
# Read the new values with shared generator
|
# Read the new values with shared generator
|
||||||
generator_shared = Generator(
|
generator_shared = Generator(
|
||||||
"my_generator", share=True, machines=["my_machine"], _flake=flake_obj
|
"my_generator", share=True, machine="my_machine", _flake=flake_obj
|
||||||
)
|
)
|
||||||
|
|
||||||
new_public = in_repo_store.get(generator_shared, "my_value").decode()
|
new_public = in_repo_store.get(generator_shared, "my_value").decode()
|
||||||
@@ -1265,117 +1264,68 @@ def test_cache_misses_for_vars_operations(
|
|||||||
flake: ClanFlake,
|
flake: ClanFlake,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that vars operations result in minimal cache misses."""
|
"""Test that vars operations result in minimal cache misses."""
|
||||||
# Set up first machine with two generators
|
|
||||||
config = flake.machines["my_machine"] = create_test_machine_config()
|
config = flake.machines["my_machine"] = create_test_machine_config()
|
||||||
|
|
||||||
# Set up two generators with public values
|
# Set up a simple generator with a public value
|
||||||
gen1 = config["clan"]["core"]["vars"]["generators"]["gen1"]
|
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
|
||||||
gen1["files"]["value1"]["secret"] = False
|
my_generator["files"]["my_value"]["secret"] = False
|
||||||
gen1["script"] = 'echo -n "test_value1" > "$out"/value1'
|
my_generator["script"] = 'echo -n "test_value" > "$out"/my_value'
|
||||||
|
|
||||||
gen2 = config["clan"]["core"]["vars"]["generators"]["gen2"]
|
|
||||||
gen2["files"]["value2"]["secret"] = False
|
|
||||||
gen2["script"] = 'echo -n "test_value2" > "$out"/value2'
|
|
||||||
|
|
||||||
# Add a second machine with the same generator configuration
|
|
||||||
flake.machines["other_machine"] = config.copy()
|
|
||||||
|
|
||||||
flake.refresh()
|
flake.refresh()
|
||||||
monkeypatch.chdir(flake.path)
|
monkeypatch.chdir(flake.path)
|
||||||
|
|
||||||
# Create fresh machine objects to ensure clean cache state
|
# Create a fresh machine object to ensure clean cache state
|
||||||
flake_obj = Flake(str(flake.path))
|
machine = Machine(name="my_machine", flake=Flake(str(flake.path)))
|
||||||
machine1 = Machine(name="my_machine", flake=flake_obj)
|
|
||||||
machine2 = Machine(name="other_machine", flake=flake_obj)
|
|
||||||
|
|
||||||
# Test 1: Running vars generate for BOTH machines simultaneously should still result in exactly 2 cache misses
|
# Test 1: Running vars generate with a fresh cache should result in exactly 3 cache misses
|
||||||
# Even though we have:
|
# Expected cache misses:
|
||||||
# - 2 machines (my_machine and other_machine)
|
# 1. One for getting the list of generators
|
||||||
# - 2 generators per machine (gen1 and gen2)
|
# 2. One for getting the final script of our test generator (my_generator)
|
||||||
# We still only get 2 cache misses when generating for both machines:
|
# 3. One for getting the final script of the state version generator (added by default)
|
||||||
# 1. One for getting the list of generators for both machines
|
# TODO: The third cache miss is undesired in tests. disable state version module for tests
|
||||||
# 2. One batched evaluation for getting all generator scripts for both machines
|
|
||||||
# The key insight: the system should batch ALL evaluations across ALL machines into a single nix eval
|
|
||||||
|
|
||||||
run_generators(
|
run_generators(
|
||||||
machines=[machine1, machine2],
|
machines=[machine],
|
||||||
generators=None, # Generate all
|
generators=None, # Generate all
|
||||||
)
|
)
|
||||||
|
|
||||||
# Print stack traces if we have more than 2 cache misses
|
# Print stack traces if we have more than 3 cache misses
|
||||||
if flake_obj._cache_misses != 2:
|
if machine.flake._cache_misses != 3:
|
||||||
flake_obj.print_cache_miss_analysis(
|
machine.flake.print_cache_miss_analysis(
|
||||||
title="Cache miss analysis for vars generate"
|
title="Cache miss analysis for vars generate"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert flake_obj._cache_misses == 2, (
|
assert machine.flake._cache_misses == 2, (
|
||||||
f"Expected exactly 2 cache misses for vars generate, got {flake_obj._cache_misses}"
|
f"Expected exactly 2 cache misses for vars generate, got {machine.flake._cache_misses}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Verify the value was generated correctly
|
||||||
|
var_value = get_machine_var(machine, "my_generator/my_value")
|
||||||
|
assert var_value.printable_value == "test_value"
|
||||||
|
|
||||||
# Test 2: List all vars should result in exactly 1 cache miss
|
# Test 2: List all vars should result in exactly 1 cache miss
|
||||||
# Force cache invalidation (this also resets cache miss tracking)
|
# Force cache invalidation (this also resets cache miss tracking)
|
||||||
invalidate_flake_cache(flake.path)
|
invalidate_flake_cache(flake.path)
|
||||||
flake_obj.invalidate_cache()
|
machine.flake.invalidate_cache()
|
||||||
|
|
||||||
stringify_all_vars(machine1)
|
stringify_all_vars(machine)
|
||||||
assert flake_obj._cache_misses == 1, (
|
assert machine.flake._cache_misses == 1, (
|
||||||
f"Expected exactly 1 cache miss for vars list, got {flake_obj._cache_misses}"
|
f"Expected exactly 1 cache miss for vars list, got {machine.flake._cache_misses}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test 3: Getting a specific var with a fresh cache should result in exactly 1 cache miss
|
# Test 3: Getting a specific var with a fresh cache should result in exactly 1 cache miss
|
||||||
# Force cache invalidation (this also resets cache miss tracking)
|
# Force cache invalidation (this also resets cache miss tracking)
|
||||||
invalidate_flake_cache(flake.path)
|
invalidate_flake_cache(flake.path)
|
||||||
flake_obj.invalidate_cache()
|
machine.flake.invalidate_cache()
|
||||||
|
|
||||||
# Only test gen1 for the get operation
|
var_value = get_machine_var(machine, "my_generator/my_value")
|
||||||
var_value = get_machine_var(machine1, "gen1/value1")
|
assert var_value.printable_value == "test_value"
|
||||||
assert var_value.printable_value == "test_value1"
|
|
||||||
|
|
||||||
assert flake_obj._cache_misses == 1, (
|
assert machine.flake._cache_misses == 1, (
|
||||||
f"Expected exactly 1 cache miss for vars get with fresh cache, got {flake_obj._cache_misses}"
|
f"Expected exactly 1 cache miss for vars get with fresh cache, got {machine.flake._cache_misses}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.with_core
|
|
||||||
def test_shared_generator_conflicting_definition_raises_error(
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
flake_with_sops: ClanFlake,
|
|
||||||
) -> None:
|
|
||||||
"""Test that vars generation raises an error when two machines have different
|
|
||||||
definitions for the same shared generator.
|
|
||||||
"""
|
|
||||||
flake = flake_with_sops
|
|
||||||
|
|
||||||
# Create machine1 with a shared generator
|
|
||||||
machine1_config = flake.machines["machine1"] = create_test_machine_config()
|
|
||||||
shared_gen1 = machine1_config["clan"]["core"]["vars"]["generators"][
|
|
||||||
"shared_generator"
|
|
||||||
]
|
|
||||||
shared_gen1["share"] = True
|
|
||||||
shared_gen1["files"]["file1"]["secret"] = False
|
|
||||||
shared_gen1["script"] = 'echo "test" > "$out"/file1'
|
|
||||||
|
|
||||||
# Create machine2 with the same shared generator but different files
|
|
||||||
machine2_config = flake.machines["machine2"] = create_test_machine_config()
|
|
||||||
shared_gen2 = machine2_config["clan"]["core"]["vars"]["generators"][
|
|
||||||
"shared_generator"
|
|
||||||
]
|
|
||||||
shared_gen2["share"] = True
|
|
||||||
shared_gen2["files"]["file2"]["secret"] = False # Different file name
|
|
||||||
shared_gen2["script"] = 'echo "test" > "$out"/file2'
|
|
||||||
|
|
||||||
flake.refresh()
|
|
||||||
monkeypatch.chdir(flake.path)
|
|
||||||
|
|
||||||
# Attempting to generate vars for both machines should raise an error
|
|
||||||
# because they have conflicting definitions for the same shared generator
|
|
||||||
with pytest.raises(
|
|
||||||
ClanError,
|
|
||||||
match=".*differ.*",
|
|
||||||
):
|
|
||||||
cli.run(["vars", "generate", "--flake", str(flake.path)])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.with_core
|
@pytest.mark.with_core
|
||||||
def test_dynamic_invalidation(
|
def test_dynamic_invalidation(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
|||||||
@@ -40,15 +40,12 @@ class StoreBase(ABC):
|
|||||||
|
|
||||||
def get_machine(self, generator: "Generator") -> str:
|
def get_machine(self, generator: "Generator") -> str:
|
||||||
"""Get machine name from generator, asserting it's not None for now."""
|
"""Get machine name from generator, asserting it's not None for now."""
|
||||||
if generator.share:
|
if generator.machine is None:
|
||||||
return "__shared"
|
if generator.share:
|
||||||
if not generator.machines:
|
return "__shared"
|
||||||
msg = f"Generator '{generator.name}' has no machine associated"
|
msg = f"Generator '{generator.name}' has no machine associated"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
if len(generator.machines) != 1:
|
return generator.machine
|
||||||
msg = f"Generator '{generator.name}' has {len(generator.machines)} machines, expected exactly 1"
|
|
||||||
raise ClanError(msg)
|
|
||||||
return generator.machines[0]
|
|
||||||
|
|
||||||
# get a single fact
|
# get a single fact
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -150,7 +147,7 @@ class StoreBase(ABC):
|
|||||||
prev_generator = dataclasses.replace(
|
prev_generator = dataclasses.replace(
|
||||||
generator,
|
generator,
|
||||||
share=not generator.share,
|
share=not generator.share,
|
||||||
machines=[] if not generator.share else [machine],
|
machine=machine if generator.share else None,
|
||||||
)
|
)
|
||||||
if self.exists(prev_generator, var.name):
|
if self.exists(prev_generator, var.name):
|
||||||
changed_files += self.delete(prev_generator, var.name)
|
changed_files += self.delete(prev_generator, var.name)
|
||||||
@@ -168,12 +165,12 @@ class StoreBase(ABC):
|
|||||||
new_file = self._set(generator, var, value, machine)
|
new_file = self._set(generator, var, value, machine)
|
||||||
action_str = "Migrated" if is_migration else "Updated"
|
action_str = "Migrated" if is_migration else "Updated"
|
||||||
log_info: Callable
|
log_info: Callable
|
||||||
if generator.share:
|
if generator.machine is None:
|
||||||
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 # noqa: PLC0415
|
||||||
|
|
||||||
machine_obj = Machine(name=generator.machines[0], flake=self.flake)
|
machine_obj = Machine(name=generator.machine, flake=self.flake)
|
||||||
log_info = machine_obj.info
|
log_info = machine_obj.info
|
||||||
if self.is_secret_store:
|
if self.is_secret_store:
|
||||||
log.info(f"{action_str} secret var {generator.name}/{var.name}\n")
|
log.info(f"{action_str} secret var {generator.name}/{var.name}\n")
|
||||||
|
|||||||
@@ -61,22 +61,14 @@ class Generator:
|
|||||||
migrate_fact: str | None = None
|
migrate_fact: str | None = None
|
||||||
validation_hash: str | None = None
|
validation_hash: str | None = None
|
||||||
|
|
||||||
machines: list[str] = field(default_factory=list)
|
machine: str | None = None
|
||||||
_flake: "Flake | None" = None
|
_flake: "Flake | None" = None
|
||||||
_public_store: "StoreBase | None" = None
|
_public_store: "StoreBase | None" = None
|
||||||
_secret_store: "StoreBase | None" = None
|
_secret_store: "StoreBase | None" = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def key(self) -> GeneratorKey:
|
def key(self) -> GeneratorKey:
|
||||||
if self.share:
|
return GeneratorKey(machine=self.machine, name=self.name)
|
||||||
# must be a shared generator
|
|
||||||
machine = None
|
|
||||||
elif len(self.machines) != 1:
|
|
||||||
msg = f"Shared generator {self.name} must have exactly one machine, but has {len(self.machines)}: {', '.join(self.machines)}"
|
|
||||||
raise ClanError(msg)
|
|
||||||
else:
|
|
||||||
machine = self.machines[0]
|
|
||||||
return GeneratorKey(machine=machine, name=self.name)
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return hash(self.key)
|
return hash(self.key)
|
||||||
@@ -151,10 +143,7 @@ class Generator:
|
|||||||
files_selector = "config.clan.core.vars.generators.*.files.*.{secret,deploy,owner,group,mode,neededFor}"
|
files_selector = "config.clan.core.vars.generators.*.files.*.{secret,deploy,owner,group,mode,neededFor}"
|
||||||
flake.precache(cls.get_machine_selectors(machine_names))
|
flake.precache(cls.get_machine_selectors(machine_names))
|
||||||
|
|
||||||
generators: list[Generator] = []
|
generators = []
|
||||||
shared_generators_raw: dict[
|
|
||||||
str, tuple[str, dict, dict]
|
|
||||||
] = {} # name -> (machine_name, gen_data, files_data)
|
|
||||||
|
|
||||||
for machine_name in machine_names:
|
for machine_name in machine_names:
|
||||||
# Get all generator metadata in one select (safe fields only)
|
# Get all generator metadata in one select (safe fields only)
|
||||||
@@ -176,38 +165,6 @@ class Generator:
|
|||||||
sec_store = machine.secret_vars_store
|
sec_store = machine.secret_vars_store
|
||||||
|
|
||||||
for gen_name, gen_data in generators_data.items():
|
for gen_name, gen_data in generators_data.items():
|
||||||
# Check for conflicts in shared generator definitions using raw data
|
|
||||||
if gen_data["share"]:
|
|
||||||
if gen_name in shared_generators_raw:
|
|
||||||
prev_machine, prev_gen_data, prev_files_data = (
|
|
||||||
shared_generators_raw[gen_name]
|
|
||||||
)
|
|
||||||
# Compare raw data
|
|
||||||
prev_gen_files = prev_files_data.get(gen_name, {})
|
|
||||||
curr_gen_files = files_data.get(gen_name, {})
|
|
||||||
# Build list of differences with details
|
|
||||||
differences = []
|
|
||||||
if prev_gen_files != curr_gen_files:
|
|
||||||
differences.append("files")
|
|
||||||
if prev_gen_data.get("prompts") != gen_data.get("prompts"):
|
|
||||||
differences.append("prompts")
|
|
||||||
if prev_gen_data.get("dependencies") != gen_data.get(
|
|
||||||
"dependencies"
|
|
||||||
):
|
|
||||||
differences.append("dependencies")
|
|
||||||
if prev_gen_data.get("validationHash") != gen_data.get(
|
|
||||||
"validationHash"
|
|
||||||
):
|
|
||||||
differences.append("validation_hash")
|
|
||||||
if differences:
|
|
||||||
msg = f"Machines {prev_machine} and {machine_name} have different definitions for shared generator '{gen_name}' (differ in: {', '.join(differences)})"
|
|
||||||
raise ClanError(msg)
|
|
||||||
else:
|
|
||||||
shared_generators_raw[gen_name] = (
|
|
||||||
machine_name,
|
|
||||||
gen_data,
|
|
||||||
files_data,
|
|
||||||
)
|
|
||||||
# Build files from the files_data
|
# Build files from the files_data
|
||||||
files = []
|
files = []
|
||||||
gen_files = files_data.get(gen_name, {})
|
gen_files = files_data.get(gen_name, {})
|
||||||
@@ -252,27 +209,14 @@ class Generator:
|
|||||||
migrate_fact=gen_data.get("migrateFact"),
|
migrate_fact=gen_data.get("migrateFact"),
|
||||||
validation_hash=gen_data.get("validationHash"),
|
validation_hash=gen_data.get("validationHash"),
|
||||||
prompts=prompts,
|
prompts=prompts,
|
||||||
# shared generators can have multiple machines, machine-specific have one
|
# only set machine for machine-specific generators
|
||||||
machines=[machine_name],
|
# this is essential for the graph algorithms to work correctly
|
||||||
|
machine=None if share else machine_name,
|
||||||
_flake=flake,
|
_flake=flake,
|
||||||
_public_store=pub_store,
|
_public_store=pub_store,
|
||||||
_secret_store=sec_store,
|
_secret_store=sec_store,
|
||||||
)
|
)
|
||||||
|
generators.append(generator)
|
||||||
if share:
|
|
||||||
# For shared generators, check if we already created it
|
|
||||||
existing = next(
|
|
||||||
(g for g in generators if g.name == gen_name and g.share), None
|
|
||||||
)
|
|
||||||
if existing:
|
|
||||||
# Just append the machine to the existing generator
|
|
||||||
existing.machines.append(machine_name)
|
|
||||||
else:
|
|
||||||
# Add the new shared generator
|
|
||||||
generators.append(generator)
|
|
||||||
else:
|
|
||||||
# Always add per-machine generators
|
|
||||||
generators.append(generator)
|
|
||||||
|
|
||||||
# TODO: This should be done in a non-mutable way.
|
# TODO: This should be done in a non-mutable way.
|
||||||
if include_previous_values:
|
if include_previous_values:
|
||||||
@@ -301,19 +245,15 @@ class Generator:
|
|||||||
return sec_store.get(self, prompt.name).decode()
|
return sec_store.get(self, prompt.name).decode()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def final_script_selector(self, machine_name: str) -> str:
|
|
||||||
if self._flake is None:
|
|
||||||
msg = "Flake cannot be None"
|
|
||||||
raise ClanError(msg)
|
|
||||||
return self._flake.machine_selector(
|
|
||||||
machine_name, f'config.clan.core.vars.generators."{self.name}".finalScript'
|
|
||||||
)
|
|
||||||
|
|
||||||
def final_script(self, machine: "Machine") -> Path:
|
def final_script(self, machine: "Machine") -> Path:
|
||||||
if self._flake is None:
|
if self._flake is None:
|
||||||
msg = "Flake cannot be None"
|
msg = "Flake cannot be None"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
output = Path(self._flake.select(self.final_script_selector(machine.name)))
|
output = Path(
|
||||||
|
machine.select(
|
||||||
|
f'config.clan.core.vars.generators."{self.name}".finalScript',
|
||||||
|
),
|
||||||
|
)
|
||||||
if tmp_store := nix_test_store():
|
if tmp_store := nix_test_store():
|
||||||
output = tmp_store.joinpath(*output.parts[1:])
|
output = tmp_store.joinpath(*output.parts[1:])
|
||||||
return output
|
return output
|
||||||
|
|||||||
@@ -49,28 +49,28 @@ def test_required_generators() -> None:
|
|||||||
gen_1 = Generator(
|
gen_1 = Generator(
|
||||||
name="gen_1",
|
name="gen_1",
|
||||||
dependencies=[],
|
dependencies=[],
|
||||||
machines=[machine_name],
|
machine=machine_name,
|
||||||
_public_store=public_store,
|
_public_store=public_store,
|
||||||
_secret_store=secret_store,
|
_secret_store=secret_store,
|
||||||
)
|
)
|
||||||
gen_2 = Generator(
|
gen_2 = Generator(
|
||||||
name="gen_2",
|
name="gen_2",
|
||||||
dependencies=[gen_1.key],
|
dependencies=[gen_1.key],
|
||||||
machines=[machine_name],
|
machine=machine_name,
|
||||||
_public_store=public_store,
|
_public_store=public_store,
|
||||||
_secret_store=secret_store,
|
_secret_store=secret_store,
|
||||||
)
|
)
|
||||||
gen_2a = Generator(
|
gen_2a = Generator(
|
||||||
name="gen_2a",
|
name="gen_2a",
|
||||||
dependencies=[gen_2.key],
|
dependencies=[gen_2.key],
|
||||||
machines=[machine_name],
|
machine=machine_name,
|
||||||
_public_store=public_store,
|
_public_store=public_store,
|
||||||
_secret_store=secret_store,
|
_secret_store=secret_store,
|
||||||
)
|
)
|
||||||
gen_2b = Generator(
|
gen_2b = Generator(
|
||||||
name="gen_2b",
|
name="gen_2b",
|
||||||
dependencies=[gen_2.key],
|
dependencies=[gen_2.key],
|
||||||
machines=[machine_name],
|
machine=machine_name,
|
||||||
_public_store=public_store,
|
_public_store=public_store,
|
||||||
_secret_store=secret_store,
|
_secret_store=secret_store,
|
||||||
)
|
)
|
||||||
@@ -118,22 +118,21 @@ def test_shared_generator_invalidates_multiple_machines_dependents() -> None:
|
|||||||
shared_gen = Generator(
|
shared_gen = Generator(
|
||||||
name="shared_gen",
|
name="shared_gen",
|
||||||
dependencies=[],
|
dependencies=[],
|
||||||
share=True, # Mark as shared generator
|
machine=None, # Shared generator
|
||||||
machines=[machine_1, machine_2], # Shared across both machines
|
|
||||||
_public_store=public_store,
|
_public_store=public_store,
|
||||||
_secret_store=secret_store,
|
_secret_store=secret_store,
|
||||||
)
|
)
|
||||||
gen_1 = Generator(
|
gen_1 = Generator(
|
||||||
name="gen_1",
|
name="gen_1",
|
||||||
dependencies=[shared_gen.key],
|
dependencies=[shared_gen.key],
|
||||||
machines=[machine_1],
|
machine=machine_1,
|
||||||
_public_store=public_store,
|
_public_store=public_store,
|
||||||
_secret_store=secret_store,
|
_secret_store=secret_store,
|
||||||
)
|
)
|
||||||
gen_2 = Generator(
|
gen_2 = Generator(
|
||||||
name="gen_2",
|
name="gen_2",
|
||||||
dependencies=[shared_gen.key],
|
dependencies=[shared_gen.key],
|
||||||
machines=[machine_2],
|
machine=machine_2,
|
||||||
_public_store=public_store,
|
_public_store=public_store,
|
||||||
_secret_store=secret_store,
|
_secret_store=secret_store,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ def handle_io(
|
|||||||
) # wlist is a list of file descriptors to be monitored for write events
|
) # wlist is a list of file descriptors to be monitored for write events
|
||||||
stdout_buf = b""
|
stdout_buf = b""
|
||||||
stderr_buf = b""
|
stderr_buf = b""
|
||||||
|
# Buffers for incomplete lines (no trailing newline yet)
|
||||||
|
stdout_line_buf = ""
|
||||||
|
stderr_line_buf = ""
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
# Function to handle file descriptors
|
# Function to handle file descriptors
|
||||||
@@ -85,6 +88,40 @@ def handle_io(
|
|||||||
rlist.remove(fd)
|
rlist.remove(fd)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
|
# Function to process output with proper carriage return handling
|
||||||
|
def process_output(
|
||||||
|
chunk: bytes, line_buf: str, extra: dict[str, str], cmdlog_func: Any
|
||||||
|
) -> str:
|
||||||
|
"""Process output chunk, handling carriage returns properly.
|
||||||
|
Returns the updated line buffer (incomplete lines).
|
||||||
|
"""
|
||||||
|
if not chunk:
|
||||||
|
return line_buf
|
||||||
|
|
||||||
|
# Decode the chunk and append to line buffer
|
||||||
|
decoded = chunk.decode("utf-8", "replace")
|
||||||
|
line_buf += decoded
|
||||||
|
|
||||||
|
# Split by newlines to get complete lines
|
||||||
|
lines = line_buf.split("\n")
|
||||||
|
|
||||||
|
# The last element might be an incomplete line
|
||||||
|
line_buf = lines[-1]
|
||||||
|
complete_lines = lines[:-1]
|
||||||
|
|
||||||
|
# Process each complete line
|
||||||
|
for line in complete_lines:
|
||||||
|
if "\r" in line:
|
||||||
|
# Handle carriage return: only keep the last segment after final \r
|
||||||
|
# This is what would be visible on a terminal
|
||||||
|
visible_line = line.split("\r")[-1]
|
||||||
|
if visible_line: # Only log non-empty lines
|
||||||
|
cmdlog_func(visible_line, extra=extra)
|
||||||
|
elif line: # Only log non-empty lines
|
||||||
|
cmdlog_func(line, extra=extra)
|
||||||
|
|
||||||
|
return line_buf
|
||||||
|
|
||||||
# Extra information passed to the logger
|
# Extra information passed to the logger
|
||||||
stdout_extra = {}
|
stdout_extra = {}
|
||||||
stderr_extra = {}
|
stderr_extra = {}
|
||||||
@@ -126,9 +163,9 @@ def handle_io(
|
|||||||
|
|
||||||
# If Log.STDOUT is set, log the stdout output
|
# If Log.STDOUT is set, log the stdout output
|
||||||
if ret and log in [Log.STDOUT, Log.BOTH]:
|
if ret and log in [Log.STDOUT, Log.BOTH]:
|
||||||
lines = ret.decode("utf-8", "replace").rstrip("\n").rstrip().split("\n")
|
stdout_line_buf = process_output(
|
||||||
for line in lines:
|
ret, stdout_line_buf, stdout_extra, cmdlog.info
|
||||||
cmdlog.info(line, extra=stdout_extra)
|
)
|
||||||
|
|
||||||
# If stdout file is set, stream the stdout output
|
# If stdout file is set, stream the stdout output
|
||||||
if ret and stdout:
|
if ret and stdout:
|
||||||
@@ -143,9 +180,9 @@ def handle_io(
|
|||||||
|
|
||||||
# If Log.STDERR is set, log the stderr output
|
# If Log.STDERR is set, log the stderr output
|
||||||
if ret and log in [Log.STDERR, Log.BOTH]:
|
if ret and log in [Log.STDERR, Log.BOTH]:
|
||||||
lines = ret.decode("utf-8", "replace").rstrip("\n").rstrip().split("\n")
|
stderr_line_buf = process_output(
|
||||||
for line in lines:
|
ret, stderr_line_buf, stderr_extra, cmdlog.info
|
||||||
cmdlog.info(line, extra=stderr_extra)
|
)
|
||||||
|
|
||||||
# If stderr file is set, stream the stderr output
|
# If stderr file is set, stream the stderr output
|
||||||
if ret and stderr:
|
if ret and stderr:
|
||||||
@@ -173,6 +210,24 @@ def handle_io(
|
|||||||
process.stdin.close()
|
process.stdin.close()
|
||||||
else:
|
else:
|
||||||
wlist.remove(process.stdin)
|
wlist.remove(process.stdin)
|
||||||
|
|
||||||
|
# Flush any remaining buffered lines at the end
|
||||||
|
if stdout_line_buf and log in [Log.STDOUT, Log.BOTH]:
|
||||||
|
if "\r" in stdout_line_buf:
|
||||||
|
visible_line = stdout_line_buf.split("\r")[-1]
|
||||||
|
if visible_line:
|
||||||
|
cmdlog.info(visible_line, extra=stdout_extra)
|
||||||
|
elif stdout_line_buf:
|
||||||
|
cmdlog.info(stdout_line_buf, extra=stdout_extra)
|
||||||
|
|
||||||
|
if stderr_line_buf and log in [Log.STDERR, Log.BOTH]:
|
||||||
|
if "\r" in stderr_line_buf:
|
||||||
|
visible_line = stderr_line_buf.split("\r")[-1]
|
||||||
|
if visible_line:
|
||||||
|
cmdlog.info(visible_line, extra=stderr_extra)
|
||||||
|
elif stderr_line_buf:
|
||||||
|
cmdlog.info(stderr_line_buf, extra=stderr_extra)
|
||||||
|
|
||||||
return stdout_buf.decode("utf-8", "replace"), stderr_buf.decode("utf-8", "replace")
|
return stdout_buf.decode("utf-8", "replace"), stderr_buf.decode("utf-8", "replace")
|
||||||
|
|
||||||
|
|
||||||
@@ -294,8 +349,6 @@ class RunOpts:
|
|||||||
# This is needed for GUI applications
|
# This is needed for GUI applications
|
||||||
graphical_perm: bool = False
|
graphical_perm: bool = False
|
||||||
trace: bool = True
|
trace: bool = True
|
||||||
# Mark input as sensitive to prevent it from being logged (e.g., private keys, passwords)
|
|
||||||
sensitive_input: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_with_root(cmd: list[str], graphical: bool = False) -> list[str]:
|
def cmd_with_root(cmd: list[str], graphical: bool = False) -> list[str]:
|
||||||
@@ -351,10 +404,7 @@ def run(
|
|||||||
|
|
||||||
if cmdlog.isEnabledFor(logging.DEBUG) and options.trace:
|
if cmdlog.isEnabledFor(logging.DEBUG) and options.trace:
|
||||||
if options.input and isinstance(options.input, bytes):
|
if options.input and isinstance(options.input, bytes):
|
||||||
# Always redact sensitive input (e.g., private keys, passwords)
|
if any(
|
||||||
if options.sensitive_input:
|
|
||||||
filtered_input = "<<REDACTED>>"
|
|
||||||
elif any(
|
|
||||||
not ch.isprintable() for ch in options.input.decode("ascii", "replace")
|
not ch.isprintable() for ch in options.input.decode("ascii", "replace")
|
||||||
):
|
):
|
||||||
filtered_input = "<<binary_blob>>"
|
filtered_input = "<<binary_blob>>"
|
||||||
|
|||||||
@@ -1132,20 +1132,6 @@ class Flake:
|
|||||||
|
|
||||||
return self._cache.select(selector)
|
return self._cache.select(selector)
|
||||||
|
|
||||||
def machine_selector(self, machine_name: str, selector: str) -> str:
|
|
||||||
"""Create a selector for a specific machine.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
machine_name: The name of the machine
|
|
||||||
selector: The attribute selector string relative to the machine config
|
|
||||||
Returns:
|
|
||||||
The full selector string for the machine
|
|
||||||
|
|
||||||
"""
|
|
||||||
config = nix_config()
|
|
||||||
system = config["system"]
|
|
||||||
return f'clanInternals.machines."{system}"."{machine_name}".{selector}'
|
|
||||||
|
|
||||||
def select_machine(self, machine_name: str, selector: str) -> Any:
|
def select_machine(self, machine_name: str, selector: str) -> Any:
|
||||||
"""Select a nix attribute for a specific machine.
|
"""Select a nix attribute for a specific machine.
|
||||||
|
|
||||||
@@ -1155,7 +1141,11 @@ class Flake:
|
|||||||
apply: Optional function to apply to the result
|
apply: Optional function to apply to the result
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.select(self.machine_selector(machine_name, selector))
|
config = nix_config()
|
||||||
|
system = config["system"]
|
||||||
|
|
||||||
|
full_selector = f'clanInternals.machines."{system}"."{machine_name}".{selector}'
|
||||||
|
return self.select(full_selector)
|
||||||
|
|
||||||
def list_machines(
|
def list_machines(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -119,9 +119,6 @@ def run_machine_hardware_info_init(
|
|||||||
if opts.debug:
|
if opts.debug:
|
||||||
cmd += ["--debug"]
|
cmd += ["--debug"]
|
||||||
|
|
||||||
# Add nix options to nixos-anywhere
|
|
||||||
cmd.extend(opts.machine.flake.nix_options or [])
|
|
||||||
|
|
||||||
cmd += [target_host.target]
|
cmd += [target_host.target]
|
||||||
cmd = nix_shell(
|
cmd = nix_shell(
|
||||||
["nixos-anywhere"],
|
["nixos-anywhere"],
|
||||||
|
|||||||
@@ -136,123 +136,92 @@ def networks_from_flake(flake: Flake) -> dict[str, Network]:
|
|||||||
return networks
|
return networks
|
||||||
|
|
||||||
|
|
||||||
class BestRemoteContext:
|
@contextmanager
|
||||||
"""Class-based context manager for establishing and maintaining network connections."""
|
def get_best_remote(machine: "Machine") -> Iterator["Remote"]:
|
||||||
|
"""Context manager that yields the best remote connection for a machine following this priority:
|
||||||
|
1. If machine has targetHost in inventory, return a direct connection
|
||||||
|
2. Return the highest priority network where machine is reachable
|
||||||
|
3. If no network works, try to get targetHost from machine nixos config
|
||||||
|
|
||||||
def __init__(self, machine: "Machine") -> None:
|
Args:
|
||||||
self.machine = machine
|
machine: Machine instance to connect to
|
||||||
self._network_ctx: Any = None
|
|
||||||
self._remote: Remote | None = None
|
|
||||||
|
|
||||||
def __enter__(self) -> "Remote":
|
Yields:
|
||||||
"""Establish the best remote connection for a machine following this priority:
|
Remote object for connecting to the machine
|
||||||
1. If machine has targetHost in inventory, return a direct connection
|
|
||||||
2. Return the highest priority network where machine is reachable
|
|
||||||
3. If no network works, try to get targetHost from machine nixos config
|
|
||||||
|
|
||||||
Returns:
|
Raises:
|
||||||
Remote object for connecting to the machine
|
ClanError: If no connection method works
|
||||||
|
|
||||||
Raises:
|
"""
|
||||||
ClanError: If no connection method works
|
# Step 1: Check if targetHost is set in inventory
|
||||||
|
inv_machine = machine.get_inv_machine()
|
||||||
|
target_host = inv_machine.get("deploy", {}).get("targetHost")
|
||||||
|
|
||||||
"""
|
if target_host:
|
||||||
# Step 1: Check if targetHost is set in inventory
|
log.debug(f"Using targetHost from inventory for {machine.name}: {target_host}")
|
||||||
inv_machine = self.machine.get_inv_machine()
|
# Create a direct network with just this machine
|
||||||
target_host = inv_machine.get("deploy", {}).get("targetHost")
|
remote = Remote.from_ssh_uri(machine_name=machine.name, address=target_host)
|
||||||
|
yield remote
|
||||||
|
return
|
||||||
|
|
||||||
if target_host:
|
# Step 2: Try existing networks by priority
|
||||||
log.debug(
|
try:
|
||||||
f"Using targetHost from inventory for {self.machine.name}: {target_host}"
|
networks = networks_from_flake(machine.flake)
|
||||||
)
|
|
||||||
self._remote = Remote.from_ssh_uri(
|
|
||||||
machine_name=self.machine.name, address=target_host
|
|
||||||
)
|
|
||||||
return self._remote
|
|
||||||
|
|
||||||
# Step 2: Try existing networks by priority
|
sorted_networks = sorted(networks.items(), key=lambda x: -x[1].priority)
|
||||||
try:
|
|
||||||
networks = networks_from_flake(self.machine.flake)
|
|
||||||
sorted_networks = sorted(networks.items(), key=lambda x: -x[1].priority)
|
|
||||||
|
|
||||||
for network_name, network in sorted_networks:
|
for network_name, network in sorted_networks:
|
||||||
if self.machine.name not in network.peers:
|
if machine.name not in network.peers:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
log.debug(f"trying to connect via {network_name}")
|
# Check if network is running and machine is reachable
|
||||||
if network.is_running():
|
log.debug(f"trying to connect via {network_name}")
|
||||||
try:
|
if network.is_running():
|
||||||
ping_time = network.ping(self.machine.name)
|
try:
|
||||||
|
ping_time = network.ping(machine.name)
|
||||||
|
if ping_time is not None:
|
||||||
|
log.info(
|
||||||
|
f"Machine {machine.name} reachable via {network_name} network",
|
||||||
|
)
|
||||||
|
yield network.remote(machine.name)
|
||||||
|
return
|
||||||
|
except ClanError as e:
|
||||||
|
log.debug(f"Failed to reach {machine.name} via {network_name}: {e}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
log.debug(f"Establishing connection for network {network_name}")
|
||||||
|
with network.module.connection(network) as connected_network:
|
||||||
|
ping_time = connected_network.ping(machine.name)
|
||||||
if ping_time is not None:
|
if ping_time is not None:
|
||||||
log.info(
|
log.info(
|
||||||
f"Machine {self.machine.name} reachable via {network_name} network",
|
f"Machine {machine.name} reachable via {network_name} network after connection",
|
||||||
)
|
)
|
||||||
self._remote = remote = network.remote(self.machine.name)
|
yield connected_network.remote(machine.name)
|
||||||
return remote
|
return
|
||||||
except ClanError as e:
|
except ClanError as e:
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Failed to reach {self.machine.name} via {network_name}: {e}"
|
f"Failed to establish connection to {machine.name} via {network_name}: {e}",
|
||||||
)
|
)
|
||||||
else:
|
except (ImportError, AttributeError, KeyError) as e:
|
||||||
try:
|
log.debug(f"Failed to use networking modules to determine machines remote: {e}")
|
||||||
log.debug(f"Establishing connection for network {network_name}")
|
|
||||||
# Enter the network context and keep it alive
|
|
||||||
self._network_ctx = network.module.connection(network)
|
|
||||||
connected_network = self._network_ctx.__enter__()
|
|
||||||
ping_time = connected_network.ping(self.machine.name)
|
|
||||||
if ping_time is not None:
|
|
||||||
log.info(
|
|
||||||
f"Machine {self.machine.name} reachable via {network_name} network after connection",
|
|
||||||
)
|
|
||||||
self._remote = remote = connected_network.remote(
|
|
||||||
self.machine.name
|
|
||||||
)
|
|
||||||
return remote
|
|
||||||
# Ping failed, clean up this connection attempt
|
|
||||||
self._network_ctx.__exit__(None, None, None)
|
|
||||||
self._network_ctx = None
|
|
||||||
except ClanError as e:
|
|
||||||
# Clean up failed connection attempt
|
|
||||||
if self._network_ctx is not None:
|
|
||||||
self._network_ctx.__exit__(None, None, None)
|
|
||||||
self._network_ctx = None
|
|
||||||
log.debug(
|
|
||||||
f"Failed to establish connection to {self.machine.name} via {network_name}: {e}",
|
|
||||||
)
|
|
||||||
except (ImportError, AttributeError, KeyError) as e:
|
|
||||||
log.debug(
|
|
||||||
f"Failed to use networking modules to determine machines remote: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Step 3: Try targetHost from machine nixos config
|
# Step 3: Try targetHost from machine nixos config
|
||||||
target_host = self.machine.select('config.clan.core.networking."targetHost"')
|
target_host = machine.select('config.clan.core.networking."targetHost"')
|
||||||
if target_host:
|
if target_host:
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Using targetHost from machine config for {self.machine.name}: {target_host}",
|
f"Using targetHost from machine config for {machine.name}: {target_host}",
|
||||||
)
|
)
|
||||||
self._remote = Remote.from_ssh_uri(
|
# Check if reachable
|
||||||
machine_name=self.machine.name,
|
remote = Remote.from_ssh_uri(
|
||||||
address=target_host,
|
machine_name=machine.name,
|
||||||
)
|
address=target_host,
|
||||||
return self._remote
|
)
|
||||||
|
yield remote
|
||||||
|
return
|
||||||
|
|
||||||
# No connection method found
|
# No connection method found
|
||||||
msg = f"Could not find any way to connect to machine '{self.machine.name}'. No targetHost configured and machine not reachable via any network."
|
msg = f"Could not find any way to connect to machine '{machine.name}'. No targetHost configured and machine not reachable via any network."
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
def __exit__(
|
|
||||||
self,
|
|
||||||
exc_type: type[BaseException] | None,
|
|
||||||
exc_val: BaseException | None,
|
|
||||||
exc_tb: object,
|
|
||||||
) -> None:
|
|
||||||
"""Clean up network connection if one was established."""
|
|
||||||
if self._network_ctx is not None:
|
|
||||||
self._network_ctx.__exit__(exc_type, exc_val, exc_tb)
|
|
||||||
|
|
||||||
|
|
||||||
def get_best_remote(machine: "Machine") -> BestRemoteContext:
|
|
||||||
return BestRemoteContext(machine)
|
|
||||||
|
|
||||||
|
|
||||||
def get_network_overview(networks: dict[str, Network]) -> dict:
|
def get_network_overview(networks: dict[str, Network]) -> dict:
|
||||||
|
|||||||
@@ -93,21 +93,21 @@ def _ensure_healthy(
|
|||||||
if generators is None:
|
if generators is None:
|
||||||
generators = Generator.get_machine_generators([machine.name], machine.flake)
|
generators = Generator.get_machine_generators([machine.name], machine.flake)
|
||||||
|
|
||||||
public_health_check_msg = machine.public_vars_store.health_check(
|
pub_healtcheck_msg = machine.public_vars_store.health_check(
|
||||||
machine.name,
|
machine.name,
|
||||||
generators,
|
generators,
|
||||||
)
|
)
|
||||||
secret_health_check_msg = machine.secret_vars_store.health_check(
|
sec_healtcheck_msg = machine.secret_vars_store.health_check(
|
||||||
machine.name,
|
machine.name,
|
||||||
generators,
|
generators,
|
||||||
)
|
)
|
||||||
|
|
||||||
if public_health_check_msg or secret_health_check_msg:
|
if pub_healtcheck_msg or sec_healtcheck_msg:
|
||||||
msg = f"Health check failed for machine {machine.name}:\n"
|
msg = f"Health check failed for machine {machine.name}:\n"
|
||||||
if public_health_check_msg:
|
if pub_healtcheck_msg:
|
||||||
msg += f"Public vars store: {public_health_check_msg}\n"
|
msg += f"Public vars store: {pub_healtcheck_msg}\n"
|
||||||
if secret_health_check_msg:
|
if sec_healtcheck_msg:
|
||||||
msg += f"Secret vars store: {secret_health_check_msg}"
|
msg += f"Secret vars store: {sec_healtcheck_msg}"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
|
|
||||||
@@ -177,25 +177,13 @@ def run_generators(
|
|||||||
for machine in machines:
|
for machine in machines:
|
||||||
_ensure_healthy(machine=machine)
|
_ensure_healthy(machine=machine)
|
||||||
|
|
||||||
# get the flake via any machine (they are all the same)
|
|
||||||
flake = machines[0].flake
|
|
||||||
|
|
||||||
def get_generator_machine(generator: Generator) -> Machine:
|
|
||||||
if generator.share:
|
|
||||||
# return first machine if generator is shared
|
|
||||||
return machines[0]
|
|
||||||
return Machine(name=generator.machines[0], flake=flake)
|
|
||||||
|
|
||||||
# preheat the select cache, to reduce repeated calls during execution
|
|
||||||
selectors = []
|
|
||||||
for generator in generator_objects:
|
|
||||||
machine = get_generator_machine(generator)
|
|
||||||
selectors.append(generator.final_script_selector(machine.name))
|
|
||||||
flake.precache(selectors)
|
|
||||||
|
|
||||||
# execute generators
|
# execute generators
|
||||||
for generator in generator_objects:
|
for generator in generator_objects:
|
||||||
machine = get_generator_machine(generator)
|
machine = (
|
||||||
|
machines[0]
|
||||||
|
if generator.machine is None
|
||||||
|
else Machine(name=generator.machine, flake=machines[0].flake)
|
||||||
|
)
|
||||||
if check_can_migrate(machine, generator):
|
if check_can_migrate(machine, generator):
|
||||||
migrate_files(machine, generator)
|
migrate_files(machine, generator)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
{ self, inputs, ... }:
|
|
||||||
{
|
|
||||||
perSystem =
|
|
||||||
{ pkgs, self', ... }:
|
|
||||||
let
|
|
||||||
# Simply evaluated options (JSON)
|
|
||||||
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
|
|
||||||
jsonDocs = pkgs.callPackage ./get-module-docs.nix {
|
|
||||||
inherit (self) clanModules;
|
|
||||||
clan-core = self;
|
|
||||||
inherit pkgs;
|
|
||||||
};
|
|
||||||
|
|
||||||
# clan service options
|
|
||||||
clanModulesViaService = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaService);
|
|
||||||
|
|
||||||
# Simply evaluated options (JSON)
|
|
||||||
renderOptions =
|
|
||||||
pkgs.runCommand "render-options"
|
|
||||||
{
|
|
||||||
# TODO: ruff does not splice properly in nativeBuildInputs
|
|
||||||
depsBuildBuild = [ pkgs.ruff ];
|
|
||||||
nativeBuildInputs = [
|
|
||||||
pkgs.python3
|
|
||||||
pkgs.mypy
|
|
||||||
self'.packages.clan-cli
|
|
||||||
];
|
|
||||||
}
|
|
||||||
''
|
|
||||||
install -D -m755 ${./generate}/__init__.py $out/bin/render-options
|
|
||||||
patchShebangs --build $out/bin/render-options
|
|
||||||
|
|
||||||
ruff format --check --diff $out/bin/render-options
|
|
||||||
ruff check --line-length 88 $out/bin/render-options
|
|
||||||
mypy --strict $out/bin/render-options
|
|
||||||
'';
|
|
||||||
|
|
||||||
module-docs =
|
|
||||||
pkgs.runCommand "rendered"
|
|
||||||
{
|
|
||||||
buildInputs = [
|
|
||||||
pkgs.python3
|
|
||||||
self'.packages.clan-cli
|
|
||||||
];
|
|
||||||
}
|
|
||||||
''
|
|
||||||
export CLAN_CORE_PATH=${
|
|
||||||
inputs.nixpkgs.lib.fileset.toSource {
|
|
||||||
root = ../..;
|
|
||||||
fileset = ../../clanModules;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
|
|
||||||
|
|
||||||
# A file that contains the links to all clanModule docs
|
|
||||||
export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService}
|
|
||||||
export CLAN_SERVICE_INTERFACE=${self'.legacyPackages.clan-service-module-interface}/share/doc/nixos/options.json
|
|
||||||
export CLAN_OPTIONS_PATH=${self'.legacyPackages.clan-options}/share/doc/nixos/options.json
|
|
||||||
|
|
||||||
mkdir $out
|
|
||||||
|
|
||||||
# The python script will place mkDocs files in the output directory
|
|
||||||
exec python3 ${renderOptions}/bin/render-options
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
{
|
|
||||||
packages = {
|
|
||||||
inherit module-docs;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,12 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./clan-app/flake-module.nix
|
|
||||||
./clan-cli/flake-module.nix
|
./clan-cli/flake-module.nix
|
||||||
./clan-core-flake/flake-module.nix
|
|
||||||
./clan-vm-manager/flake-module.nix
|
./clan-vm-manager/flake-module.nix
|
||||||
./icon-update/flake-module.nix
|
|
||||||
./installer/flake-module.nix
|
./installer/flake-module.nix
|
||||||
./option-search/flake-module.nix
|
./icon-update/flake-module.nix
|
||||||
./docs-from-code/flake-module.nix
|
./clan-core-flake/flake-module.nix
|
||||||
|
./clan-app/flake-module.nix
|
||||||
./testing/flake-module.nix
|
./testing/flake-module.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user