Compare commits

..

65 Commits

Author SHA1 Message Date
8bef2e6b2e Drop macOS-specific remote-program param from nix copy command 2025-11-06 11:11:57 +08:00
clan-bot
8eaca289ad Merge pull request 'Update treefmt-nix' (#5745) from update-treefmt-nix into main 2025-11-05 20:08:44 +00:00
clan-bot
6f2d482187 Merge pull request 'Update treefmt-nix in devFlake' (#5756) from update-devFlake-treefmt-nix into main 2025-11-05 20:08:18 +00:00
clan-bot
4c30418f12 Update treefmt-nix in devFlake 2025-11-05 20:02:31 +00:00
clan-bot
3c66094d89 Update treefmt-nix 2025-11-05 20:02:02 +00:00
clan-bot
a8f180f8da Merge pull request 'Update treefmt-nix in devFlake' (#5753) from update-devFlake-treefmt-nix into main 2025-11-05 15:09:20 +00:00
clan-bot
e22218d589 Merge pull request 'Update nixpkgs-dev in devFlake' (#5752) from update-devFlake-nixpkgs-dev into main 2025-11-05 15:09:02 +00:00
clan-bot
228c60bcf7 Update treefmt-nix in devFlake 2025-11-05 15:02:30 +00:00
clan-bot
ed2b2d9df9 Update nixpkgs-dev in devFlake 2025-11-05 15:02:24 +00:00
Kenji Berthold
7e2a127d11 Merge pull request 'pkgs/clan-vm-manager: wrapGAppsHook -> wrapGAppsHook3' (#5748) from ke-wrap-gapps into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5748
2025-11-05 12:27:32 +00:00
a-kenji
8c8bacb1ab pkgs/clan-vm-manager: wrapGAppsHook -> wrapGAppsHook3 2025-11-05 12:50:48 +01:00
clan-bot
8ba71144b6 Merge pull request 'Update nix-darwin' (#5744) from update-nix-darwin into main 2025-11-05 10:04:33 +00:00
clan-bot
7f2d15c8a1 Update nix-darwin 2025-11-05 10:01:31 +00:00
clan-bot
486463c793 Merge pull request 'Update treefmt-nix in devFlake' (#5746) from update-devFlake-treefmt-nix into main 2025-11-05 05:16:48 +00:00
clan-bot
071603d688 Update treefmt-nix in devFlake 2025-11-05 05:02:33 +00:00
clan-bot
c612561ec3 Merge pull request 'Update disko' (#5742) from update-disko into main 2025-11-05 00:10:58 +00:00
clan-bot
a88cd2be40 Update disko 2025-11-05 00:01:25 +00:00
clan-bot
7140b417d3 Merge pull request 'Update nixos-facter-modules' (#5738) from update-nixos-facter-modules into main 2025-11-04 20:10:12 +00:00
clan-bot
c7a42cca7f Update nixos-facter-modules 2025-11-04 20:01:33 +00:00
clan-bot
29ca23c629 Merge pull request 'Update nixpkgs-dev in devFlake' (#5740) from update-devFlake-nixpkgs-dev into main 2025-11-04 15:08:00 +00:00
clan-bot
cd7210de1b Update nixpkgs-dev in devFlake 2025-11-04 15:02:30 +00:00
Mic92
c2ebafcf92 Merge pull request 'zfsUnstable -> zfs_unstable' (#5737) from zfs-fix into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5737
2025-11-04 14:46:19 +00:00
Jörg Thalheim
2a9e4e7860 zfsUnstable -> zfs_unstable
nixpkgs has a new path for this.
2025-11-04 15:41:50 +01:00
hsjobeki
43a7652624 Merge pull request 'App: init delete machine' (#5734) from jpy-scene into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5734
2025-11-04 11:03:26 +00:00
Johannes Kirschbauer
65fd25bc2e App: init delete machine 2025-11-04 11:37:29 +01:00
Kenji Berthold
f89ea15749 Merge pull request 'pkgs/cli/vars: Add dependency validation' (#5727) from ke-vars-dependency-validation into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5727
Reviewed-by: Mic92 <joerg@thalheim.io>
2025-11-04 09:55:55 +00:00
hsjobeki
19d4833be8 Merge pull request 'UI: clean up unused scene code' (#5730) from jpy-scene into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5730
2025-11-04 08:39:04 +00:00
Johannes Kirschbauer
82f12eaf6f UI: clean up unused scene code 2025-11-04 09:34:17 +01:00
clan-bot
0b5a8e98de Merge pull request 'Update nix-darwin' (#5729) from update-nix-darwin into main 2025-11-04 05:05:29 +00:00
clan-bot
c5bddada05 Update nix-darwin 2025-11-04 05:01:02 +00:00
clan-bot
62b64c3b3e Merge pull request 'Update nixpkgs-dev in devFlake' (#5728) from update-devFlake-nixpkgs-dev into main 2025-11-03 15:07:53 +00:00
clan-bot
19a1ad6081 Update nixpkgs-dev in devFlake 2025-11-03 15:01:50 +00:00
Kenji Berthold
a2df5db3d6 Merge pull request 'docs/testing: Document requirements for our container testing system' (#5693) from ke-docs-testing-container into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5693
2025-11-03 13:13:53 +00:00
Kenji Berthold
ac46f890ea Merge branch 'main' into ke-docs-testing-container 2025-11-03 13:06:14 +00:00
a-kenji
83f78d9f59 pkgs/cli/vars: Add dependency validation
Add explicit dependency validation to vars, so that proper error
messages can be surfaced to the user.

Instead of:
```
Traceback (most recent call last):
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_lib/async_run/__init__.py", line 154, in run
    self.result = AsyncResult(_result=self.function(*self.args, **self.kwargs))
                                      ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_cli/machines/update.py", line 62, in run_update_wit
h_network
    run_machine_update(
    ~~~~~~~~~~~~~~~~~~^
        machine=machine,
        ^^^^^^^^^^^^^^^^
    ...<2 lines>...
        upload_inputs=upload_inputs,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_lib/machines/update.py", line 158, in run_machine_u
pdate
    run_generators([machine], generators=None, full_closure=False)
    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_lib/vars/generate.py", line 156, in run_generators
    all_generators = get_generators(machines, full_closure=True)
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_lib/vars/generate.py", line 50, in get_generators
    all_generators_list = Generator.get_machine_generators(
        all_machines,
        flake,
        include_previous_values=include_previous_values,
    )
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_cli/vars/generator.py", line 246, in get_machine_ge
nerators
    if generators_data[dep]["share"]
       ~~~~~~~~~~~~~~~^^^^^
KeyError: 'bla'
```

We now get:
```
$> Generator 'my_generator' on machine 'my_machine' depends on generator 'non_existing_generator', but 'non_existing_generator' does not exist
```

Closes: #5698
2025-11-03 14:00:38 +01:00
clan-bot
19abf8d288 Merge pull request 'Update nixpkgs-dev in devFlake' (#5726) from update-devFlake-nixpkgs-dev into main 2025-11-03 10:06:31 +00:00
clan-bot
e5105e31c4 Update nixpkgs-dev in devFlake 2025-11-03 10:01:47 +00:00
clan-bot
0f847b4799 Merge pull request 'Update nixpkgs-dev in devFlake' (#5724) from update-devFlake-nixpkgs-dev into main 2025-11-02 20:06:24 +00:00
clan-bot
40a8a823b8 Update nixpkgs-dev in devFlake 2025-11-02 20:01:50 +00:00
Mic92
e3adb3fc71 Merge pull request 'Fix vars upload for public vars with neededFor activation/partitioning' (#5723) from vars into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5723
2025-11-02 16:00:13 +00:00
Jörg Thalheim
a569a1d147 Fix vars upload for public vars with neededFor activation/partitioning
When vars are marked with neededFor="activation" or "partitioning", they
need to be available early in the boot process. However, the populate_dir
methods in both sops and password_store secret backends were only calling
self.get() which only retrieves secret vars from the .../secret path.

This caused public vars (stored at .../value) to fail with "Secret does
not exist" errors when trying to upload them.

The fix uses file.value property instead, which properly delegates to the
correct store (SecretStore or FactStore) based on whether the file is
marked as secret or public.

Fixes affected all neededFor phases in both backends:
- sops: activation and partitioning phases
- password_store: activation and partitioning phases
2025-11-02 16:49:49 +01:00
Mic92
64718b77ca Merge pull request 'readme fix' (#5722) from i18n/clan-core:main into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5722
2025-11-02 15:49:16 +00:00
i18n
7b34c39736 Merge pull request '更新 docs/site/getting-started/creating-your-first-clan.md' (#1) from i18n-patch-1 into main
Reviewed-on: https://git.clan.lol/i18n/clan-core/pulls/1
2025-11-02 13:24:05 +00:00
i18n
4d6ab60793 更新 docs/site/getting-started/creating-your-first-clan.md 2025-11-02 13:23:04 +00:00
clan-bot
35bffee544 Merge pull request 'Update nixpkgs-dev in devFlake' (#5721) from update-devFlake-nixpkgs-dev into main 2025-11-02 10:05:51 +00:00
clan-bot
16917fd79b Update nixpkgs-dev in devFlake 2025-11-02 10:01:50 +00:00
clan-bot
895c116c01 Merge pull request 'Update nix-darwin' (#5720) from update-nix-darwin into main 2025-11-02 05:06:00 +00:00
clan-bot
e67151f7b9 Merge pull request 'Update flake-parts' (#5719) from update-flake-parts into main 2025-11-02 05:05:06 +00:00
clan-bot
8d26ec1760 Update nix-darwin 2025-11-02 05:01:04 +00:00
clan-bot
7a9062b629 Update flake-parts 2025-11-02 05:01:01 +00:00
clan-bot
de07454a0a Merge pull request 'Update nix-darwin' (#5718) from update-nix-darwin into main 2025-11-01 20:06:16 +00:00
clan-bot
6fe60f61cf Update nix-darwin 2025-11-01 20:00:58 +00:00
clan-bot
3fa74847e4 Merge pull request 'Update nixpkgs-dev in devFlake' (#5716) from update-devFlake-nixpkgs-dev into main 2025-11-01 15:06:01 +00:00
clan-bot
fc37140b52 Update nixpkgs-dev in devFlake 2025-11-01 15:01:52 +00:00
hsjobeki
83406c61f3 Merge pull request 'services: update hello-world readme and tests' (#5714) from update-docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5714
2025-11-01 11:37:39 +00:00
hsjobeki
6d736e7e80 Merge pull request 'docs: update experimental notes as planned in release-notes' (#5715) from ex-docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5715
2025-11-01 11:35:29 +00:00
Johannes Kirschbauer
7b6cec4100 services: update hello-world readme and tests 2025-11-01 12:33:15 +01:00
Johannes Kirschbauer
e21a6516b5 docs: update experimental notes as planned in release-notes 2025-11-01 12:30:01 +01:00
Mic92
6ffe8ea5f6 Merge pull request 'treewide: replace pkgs.hostPlatform with pkgs.stdenv.hostPlatform' (#5713) from fix-eval into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5713
2025-10-31 17:57:38 +00:00
Jörg Thalheim
0a2fefd141 treewide: replace pkgs.hostPlatform with pkgs.stdenv.hostPlatform
nixpkgs now throws an error for this, the other variant in stdenv also
exists in the previous release
2025-10-31 18:52:31 +01:00
Luis Hebendanz
0c885d05b6 Merge pull request 'clan_lib/flake: Improve select error message' (#5711) from Qubasa/clan-core:improve_clan_select_error_message into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5711
2025-10-31 15:11:29 +00:00
Qubasa
58d85b117a clan_lib/flake: Improve select error message 2025-10-31 16:05:54 +01:00
clan-bot
ad58d7b6e9 Merge pull request 'Update nixpkgs-dev in devFlake' (#5710) from update-devFlake-nixpkgs-dev into main 2025-10-31 15:05:13 +00:00
clan-bot
7a63cb9642 Update nixpkgs-dev in devFlake 2025-10-31 15:01:50 +00:00
a-kenji
bc290fe59f docs/testing: Document requirements for our container testing system
Document the requirements for our container testing system:
- uid-range
- auto-allocate-uids

Further document that the container tests are used by default and how to
switch to the more traditional and more supported / featureful VM
testing framework.
2025-10-29 13:47:26 +01:00
36 changed files with 393 additions and 213 deletions

View File

@@ -58,20 +58,22 @@
pkgs.buildPackages.xorg.lndir pkgs.buildPackages.xorg.lndir
pkgs.glibcLocales pkgs.glibcLocales
pkgs.kbd.out pkgs.kbd.out
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.ConfigIniFiles self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".pkgs.perlPackages.ConfigIniFiles
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.FileSlurp self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".pkgs.perlPackages.FileSlurp
pkgs.bubblewrap pkgs.bubblewrap
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.toplevel self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.toplevel
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.diskoScript self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.diskoScript
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.diskoScript.drvPath self.nixosConfigurations."test-flash-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.diskoScript.drvPath
] ]
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs); ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
closureInfo = pkgs.closureInfo { rootPaths = dependencies; }; closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
in in
{ {
# Skip flash test on aarch64-linux for now as it's too slow # Skip flash test on aarch64-linux for now as it's too slow
checks = lib.optionalAttrs (pkgs.stdenv.isLinux && pkgs.hostPlatform.system != "aarch64-linux") { checks =
lib.optionalAttrs (pkgs.stdenv.isLinux && pkgs.stdenv.hostPlatform.system != "aarch64-linux")
{
nixos-test-flash = self.clanLib.test.baseTest { nixos-test-flash = self.clanLib.test.baseTest {
name = "flash"; name = "flash";
nodes.target = { nodes.target = {
@@ -100,7 +102,7 @@
machine.succeed("echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRWUusawhlIorx7VFeQJHmMkhl9X3QpnvOdhnV/bQNG root@target' > ./test_id_ed25519.pub") machine.succeed("echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRWUusawhlIorx7VFeQJHmMkhl9X3QpnvOdhnV/bQNG root@target' > ./test_id_ed25519.pub")
# Some distros like to automount disks with spaces # Some distros like to automount disks with spaces
machine.succeed('mkdir -p "/mnt/with spaces" && mkfs.ext4 /dev/vdc && mount /dev/vdc "/mnt/with spaces"') machine.succeed('mkdir -p "/mnt/with spaces" && mkfs.ext4 /dev/vdc && mount /dev/vdc "/mnt/with spaces"')
machine.succeed("clan flash write --ssh-pubkey ./test_id_ed25519.pub --keymap de --language de_DE.UTF-8 --debug --flake ${self.checks.x86_64-linux.clan-core-for-checks} --yes --disk main /dev/vdc test-flash-machine-${pkgs.hostPlatform.system}") machine.succeed("clan flash write --ssh-pubkey ./test_id_ed25519.pub --keymap de --language de_DE.UTF-8 --debug --flake ${self.checks.x86_64-linux.clan-core-for-checks} --yes --disk main /dev/vdc test-flash-machine-${pkgs.stdenv.hostPlatform.system}")
''; '';
} { inherit pkgs self; }; } { inherit pkgs self; };
}; };

View File

@@ -160,9 +160,9 @@
closureInfo = pkgs.closureInfo { closureInfo = pkgs.closureInfo {
rootPaths = [ rootPaths = [
privateInputs.clan-core-for-checks privateInputs.clan-core-for-checks
self.nixosConfigurations."test-install-machine-${pkgs.hostPlatform.system}".config.system.build.toplevel self.nixosConfigurations."test-install-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.toplevel
self.nixosConfigurations."test-install-machine-${pkgs.hostPlatform.system}".config.system.build.initialRamdisk self.nixosConfigurations."test-install-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.initialRamdisk
self.nixosConfigurations."test-install-machine-${pkgs.hostPlatform.system}".config.system.build.diskoScript self.nixosConfigurations."test-install-machine-${pkgs.stdenv.hostPlatform.system}".config.system.build.diskoScript
pkgs.stdenv.drvPath pkgs.stdenv.drvPath
pkgs.bash.drvPath pkgs.bash.drvPath
pkgs.buildPackages.xorg.lndir pkgs.buildPackages.xorg.lndir
@@ -215,7 +215,7 @@
# Prepare test flake and Nix store # Prepare test flake and Nix store
flake_dir = prepare_test_flake( flake_dir = prepare_test_flake(
temp_dir, temp_dir,
"${self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks}", "${self.checks.${pkgs.stdenv.hostPlatform.system}.clan-core-for-checks}",
"${closureInfo}" "${closureInfo}"
) )
@@ -296,7 +296,7 @@
# Prepare test flake and Nix store # Prepare test flake and Nix store
flake_dir = prepare_test_flake( flake_dir = prepare_test_flake(
temp_dir, temp_dir,
"${self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks}", "${self.checks.${pkgs.stdenv.hostPlatform.system}.clan-core-for-checks}",
"${closureInfo}" "${closureInfo}"
) )

View File

@@ -2,7 +2,7 @@
let let
cli = self.packages.${pkgs.hostPlatform.system}.clan-cli-full; cli = self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli-full;
ollama-model = pkgs.callPackage ./qwen3-4b-instruct.nix { }; ollama-model = pkgs.callPackage ./qwen3-4b-instruct.nix { };
in in
@@ -53,7 +53,7 @@ in
pytest pytest
pytest-xdist pytest-xdist
(cli.pythonRuntime.pkgs.toPythonModule cli) (cli.pythonRuntime.pkgs.toPythonModule cli)
self.legacyPackages.${pkgs.hostPlatform.system}.nixosTestLib self.legacyPackages.${pkgs.stdenv.hostPlatform.system}.nixosTestLib
] ]
)) ))
]; ];

View File

@@ -2,7 +2,7 @@
let let
cli = self.packages.${pkgs.hostPlatform.system}.clan-cli-full; cli = self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli-full;
in in
{ {
name = "systemd-abstraction"; name = "systemd-abstraction";

View File

@@ -115,9 +115,9 @@
let let
closureInfo = pkgs.closureInfo { closureInfo = pkgs.closureInfo {
rootPaths = [ rootPaths = [
self.packages.${pkgs.hostPlatform.system}.clan-cli self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli
self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks self.checks.${pkgs.stdenv.hostPlatform.system}.clan-core-for-checks
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-update-machine.config.system.build.toplevel self.clanInternals.machines.${pkgs.stdenv.hostPlatform.system}.test-update-machine.config.system.build.toplevel
pkgs.stdenv.drvPath pkgs.stdenv.drvPath
pkgs.bash.drvPath pkgs.bash.drvPath
pkgs.buildPackages.xorg.lndir pkgs.buildPackages.xorg.lndir
@@ -132,7 +132,7 @@
imports = [ self.nixosModules.test-update-machine ]; imports = [ self.nixosModules.test-update-machine ];
}; };
extraPythonPackages = _p: [ extraPythonPackages = _p: [
self.legacyPackages.${pkgs.hostPlatform.system}.nixosTestLib self.legacyPackages.${pkgs.stdenv.hostPlatform.system}.nixosTestLib
]; ];
testScript = '' testScript = ''
@@ -154,7 +154,7 @@
# Prepare test flake and Nix store # Prepare test flake and Nix store
flake_dir = prepare_test_flake( flake_dir = prepare_test_flake(
temp_dir, temp_dir,
"${self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks}", "${self.checks.${pkgs.stdenv.hostPlatform.system}.clan-core-for-checks}",
"${closureInfo}" "${closureInfo}"
) )
(flake_dir / ".clan-flake").write_text("") # Ensure .clan-flake exists (flake_dir / ".clan-flake").write_text("") # Ensure .clan-flake exists
@@ -226,7 +226,7 @@
"--to", "--to",
"ssh://root@192.168.1.1", "ssh://root@192.168.1.1",
"--no-check-sigs", "--no-check-sigs",
f"${self.packages.${pkgs.hostPlatform.system}.clan-cli}", f"${self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli}",
"--extra-experimental-features", "nix-command flakes", "--extra-experimental-features", "nix-command flakes",
], ],
check=True, check=True,
@@ -242,7 +242,7 @@
"-o", "UserKnownHostsFile=/dev/null", "-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no", "-o", "StrictHostKeyChecking=no",
f"root@192.168.1.1", f"root@192.168.1.1",
"${self.packages.${pkgs.hostPlatform.system}.clan-cli}/bin/clan", "${self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli}/bin/clan",
"machines", "machines",
"update", "update",
"--debug", "--debug",
@@ -270,7 +270,7 @@
# Run clan update command # Run clan update command
subprocess.run([ subprocess.run([
"${self.packages.${pkgs.hostPlatform.system}.clan-cli-full}/bin/clan", "${self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli-full}/bin/clan",
"machines", "machines",
"update", "update",
"--debug", "--debug",
@@ -297,7 +297,7 @@
# Run clan update command with --build-host # Run clan update command with --build-host
subprocess.run([ subprocess.run([
"${self.packages.${pkgs.hostPlatform.system}.clan-cli-full}/bin/clan", "${self.packages.${pkgs.stdenv.hostPlatform.system}.clan-cli-full}/bin/clan",
"machines", "machines",
"update", "update",
"--debug", "--debug",

View File

@@ -1,3 +1,6 @@
!!! Danger "Experimental"
This service is experimental and will change in the future.
This service sets up a certificate authority (CA) that can issue certificates to This service sets up a certificate authority (CA) that can issue certificates to
other machines in your clan. For this the `ca` role is used. other machines in your clan. For this the `ca` role is used.
It additionally provides a `default` role, that can be applied to all machines It additionally provides a `default` role, that can be applied to all machines

View File

@@ -1,3 +1,6 @@
!!! Danger "Experimental"
This service is experimental and will change in the future.
This module enables hosting clan-internal services easily, which can be resolved This module enables hosting clan-internal services easily, which can be resolved
inside your VPN. This allows defining a custom top-level domain (e.g. `.clan`) inside your VPN. This allows defining a custom top-level domain (e.g. `.clan`)
and exposing endpoints from a machine to others, which will be and exposing endpoints from a machine to others, which will be

View File

@@ -1 +1,83 @@
This a test README just to appease the eval warnings if we don't have one !!! Danger "Experimental"
This service is for demonstration purpose only and may change in the future.
The Hello-World Clan Service is a minimal example showing how to build and register your own service.
It serves as a reference implementation and is used in clan-core CI tests to ensure compatibility.
## What it demonstrates
- How to define a basic Clan-compatible service.
- How to structure your service for discovery and configuration.
- How Clan services interact with nixos.
## Testing
This service demonstrates two levels of testing to ensure quality and stability across releases:
1. **Unit & Integration Testing** — via [`nix-unit`](https://github.com/nix-community/nix-unit)
2. **End-to-End Testing** — via **NixOS VM tests**, which we extended to support **container virtualization** for better performance.
We highly advocate following the [Practical Testing Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html):
* Write **unit tests** for core logic and invariants.
* Add **one or two end-to-end (E2E)** tests to confirm your service starts and behaves correctly in a real NixOS environment.
NixOS is **untyped** and frequently changes; tests are the safest way to ensure long-term stability of services.
```
/ \
/ \
/ E2E \
/-------\
/ \
/Integration\
/-------------\
/ \
/ Unit Tests \
-------------------
```
### nix-unit
We highly advocate the usage of
[nix-unit](https://github.com/nix-community/nix-unit)
Example in: tests/eval-tests.nix
If you use flake-parts you can use the [native integration](https://flake.parts/options/nix-unit.html)
If nix-unit succeeds you'r nixos evaluation should be mostly correct.
!!! Tip
- Ensure most used 'settings' and variants are tested.
- Think about some important edge-cases your system should handle.
### NixOS VM / Container Test
!!! Warning "Early Vars & clanTest"
The testing system around vars is experimental
`clanTest` is still experimental and enables container virtualization by default.
This is still early and might have some limitations.
Some minimal boilerplate is needed to use `clanTest`
```nix
nixosLib = import (inputs.nixpkgs + "/nixos/lib") { }
nixosLib.runTest (
{ ... }:
{
imports = [
self.modules.nixosTest.clanTest
# Example in tests/vm/default.nix
testModule
];
hostPkgs = pkgs;
# Uncomment if you don't want or cannot use containers
# test.useContainers = false;
}
)
```

View File

@@ -8,7 +8,7 @@
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/hello-word"; manifest.name = "clan-core/hello-word";
manifest.description = "This is a test"; manifest.description = "Minimal example clan service that greets the world";
manifest.readme = builtins.readFile ./README.md; manifest.readme = builtins.readFile ./README.md;
# This service provides two roles: "morning" and "evening". Roles can be # This service provides two roles: "morning" and "evening". Roles can be

View File

@@ -26,7 +26,7 @@ in
# The hello-world service being tested # The hello-world service being tested
../../clanServices/hello-world ../../clanServices/hello-world
# Required modules # Required modules
../../nixosModules/clanCore ../../nixosModules
]; ];
testName = "hello-world"; testName = "hello-world";
tests = ./tests/eval-tests.nix; tests = ./tests/eval-tests.nix;

View File

@@ -4,7 +4,7 @@
... ...
}: }:
let let
testFlake = clanLib.clan { testClan = clanLib.clan {
self = { }; self = { };
# Point to the folder of the module # Point to the folder of the module
# TODO: make this optional # TODO: make this optional
@@ -33,10 +33,20 @@ let
}; };
in in
{ {
test_simple = { /**
config = testFlake.config; We highly advocate the usage of:
https://github.com/nix-community/nix-unit
expr = { }; If you use flake-parts you can use the native integration: https://flake.parts/options/nix-unit.html
expected = { }; */
test_simple = {
# Allows inspection via the nix-repl
# Ignored by nix-unit; it only looks at 'expr' and 'expected'
inherit testClan;
# Assert that jon has the
# configured greeting in 'environment.etc.hello.text'
expr = testClan.config.nixosConfigurations.jon.config.environment.etc."hello".text;
expected = "Good evening World!";
}; };
} }

View File

@@ -1,8 +1,5 @@
🚧🚧🚧 Experimental 🚧🚧🚧 !!! Danger "Experimental"
This service is experimental and will change in the future.
Use at your own risk.
We are still refining its interfaces, instability and breakages are expected.
--- ---

View File

@@ -1,3 +1,6 @@
!!! Danger "Experimental"
This service is experimental and will change in the future.
## Usage ## Usage
``` ```

View File

@@ -1,8 +1,5 @@
🚧🚧🚧 Experimental 🚧🚧🚧 !!! Danger "Experimental"
This service is experimental and will change in the future.
Use at your own risk.
We are still refining its interfaces, instability and breakages are expected.
--- ---

View File

@@ -56,8 +56,6 @@
{ {
clanLib, clanLib,
lib,
directory,
... ...
}: }:
let let
@@ -300,18 +298,6 @@ in
... ...
}: }:
{ {
exports.networking = {
peers = lib.mapAttrs (name: _machine: {
host.plain =
clanLib.vars.getPublicValue {
flake = directory;
machine = name;
generator = "wireguard-network-${instanceName}";
file = "prefix";
}
+ "::1";
}) roles.controller.machines;
};
# Controllers connect to all peers and other controllers # Controllers connect to all peers and other controllers
nixosModule = nixosModule =

View File

@@ -1,8 +1,5 @@
🚧🚧🚧 Experimental 🚧🚧🚧 !!! Danger "Experimental"
This service is experimental and will change in the future.
Use at your own risk.
We are still refining its interfaces, instability and breakages are expected.
--- ---

12
devFlake/flake.lock generated
View File

@@ -105,11 +105,11 @@
}, },
"nixpkgs-dev": { "nixpkgs-dev": {
"locked": { "locked": {
"lastModified": 1761853358, "lastModified": 1762328495,
"narHash": "sha256-1tBdsBzYJOzVzNOmCFzFMWHw7UUbhkhiYCFGr+OjPTs=", "narHash": "sha256-IUZvw5kvLiExApP9+SK/styzEKSqfe0NPclu9/z85OQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "262333bca9b49964f8e3cad3af655466597c01d4", "rev": "4c621660e393922cf68cdbfc40eb5a2d54d3989a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -208,11 +208,11 @@
"nixpkgs": [] "nixpkgs": []
}, },
"locked": { "locked": {
"lastModified": 1761311587, "lastModified": 1762366246,
"narHash": "sha256-Msq86cR5SjozQGCnC6H8C+0cD4rnx91BPltZ9KK613Y=", "narHash": "sha256-3xc/f/ZNb5ma9Fc9knIzEwygXotA+0BZFQ5V5XovSOQ=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "2eddae033e4e74bf581c2d1dfa101f9033dbd2dc", "rev": "a82c779ca992190109e431d7d680860e6723e048",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -51,7 +51,7 @@ Make sure you have the following:
**Note:** This creates a new directory in your current location **Note:** This creates a new directory in your current location
```shellSession ```shellSession
nix run https://git.clan.lol/clan/clan-core/archive/main.tar.gz#clan-cli --refresh -- flakes create nix run "https://git.clan.lol/clan/clan-core/archive/main.tar.gz#clan-cli" --refresh -- flakes create
``` ```
3. Enter a **name** in the prompt: 3. Enter a **name** in the prompt:

View File

@@ -150,10 +150,61 @@ Those are very similar to NixOS VM tests, as in they run virtualized nixos machi
As of now the container test driver is a downstream development in clan-core. As of now the container test driver is a downstream development in clan-core.
Basically everything stated under the NixOS VM tests sections applies here, except some limitations. Basically everything stated under the NixOS VM tests sections applies here, except some limitations.
Limitations: ### Using Container Tests vs VM Tests
- Cannot run in interactive mode, however while the container test runs, it logs a nsenter command that can be used to log into each of the container. Container tests are **enabled by default** for all tests using the clan testing framework.
- setuid binaries don't work They offer significant performance advantages over VM tests:
- **Faster startup**
- **Lower resource usage**: No full kernel boot or hardware emulation overhead
To control whether a test uses containers or VMs, use the `clan.test.useContainers` option:
```nix
{
clan = {
directory = ./.;
test.useContainers = true; # Use containers (default)
# test.useContainers = false; # Use VMs instead
};
}
```
**When to use VM tests instead of container tests:**
- Testing kernel features, modules, or boot processes
- Testing hardware-specific features
- When you need full system isolation
### System Requirements for Container Tests
Container tests require the **`uid-range`** system feature** in the Nix sandbox.
This feature allows Nix to allocate a range of UIDs for containers to use, enabling `systemd-nspawn` containers to run properly inside the Nix build sandbox.
**Configuration:**
The `uid-range` feature requires the `auto-allocate-uids` setting to be enabled in your Nix configuration.
To verify or enable it, add to your `/etc/nix/nix.conf` or NixOS configuration:
```nix
settings.experimental-features = [
"auto-allocate-uids"
];
nix.settings.auto-allocate-uids = true;
nix.settings.system-features = [ "uid-range" ];
```
**Technical details:**
- Container tests set `requiredSystemFeatures = [ "uid-range" ];` in their derivation (see `lib/test/container-test-driver/driver-module.nix:98`)
- Without this feature, containers cannot properly manage user namespaces and will fail to start
### Limitations
- Cannot run in interactive mode, however while the container test runs, it logs a nsenter command that can be used to log into each of the containers.
- Early implementation and limited by features.
### Where to find examples for NixOS container tests ### Where to find examples for NixOS container tests

36
flake.lock generated
View File

@@ -31,11 +31,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1761899396, "lastModified": 1762276996,
"narHash": "sha256-XOpKBp6HLzzMCbzW50TEuXN35zN5WGQREC7n34DcNMM=", "narHash": "sha256-TtcPgPmp2f0FAnc+DMEw4ardEgv1SGNR3/WFGH0N19M=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "6f4cf5abbe318e4cd1e879506f6eeafd83f7b998", "rev": "af087d076d3860760b3323f6b583f4d828c1ac17",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -51,11 +51,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760948891, "lastModified": 1762040540,
"narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", "narHash": "sha256-z5PlZ47j50VNF3R+IMS9LmzI5fYRGY/Z5O5tol1c9I4=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", "rev": "0010412d62a25d959151790968765a70c436598b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -71,11 +71,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1761339987, "lastModified": 1762304480,
"narHash": "sha256-IUaawVwItZKi64IA6kF6wQCLCzpXbk2R46dHn8sHkig=", "narHash": "sha256-ikVIPB/ea/BAODk6aksgkup9k2jQdrwr4+ZRXtBgmSs=",
"owner": "nix-darwin", "owner": "nix-darwin",
"repo": "nix-darwin", "repo": "nix-darwin",
"rev": "7cd9aac79ee2924a85c211d21fafd394b06a38de", "rev": "b8c7ac030211f18bd1f41eae0b815571853db7a2",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -99,11 +99,11 @@
}, },
"nixos-facter-modules": { "nixos-facter-modules": {
"locked": { "locked": {
"lastModified": 1761137276, "lastModified": 1762264948,
"narHash": "sha256-4lDjGnWRBLwqKQ4UWSUq6Mvxu9r8DSqCCydodW/Jsi8=", "narHash": "sha256-iaRf6n0KPl9hndnIft3blm1YTAyxSREV1oX0MFZ6Tk4=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixos-facter-modules", "repo": "nixos-facter-modules",
"rev": "70bcd64225d167c7af9b475c4df7b5abba5c7de8", "rev": "fa695bff9ec37fd5bbd7ee3181dbeb5f97f53c96",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -115,10 +115,10 @@
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 315532800, "lastModified": 315532800,
"narHash": "sha256-yDxtm0PESdgNetiJN5+MFxgubBcLDTiuSjjrJiyvsvM=", "narHash": "sha256-LDT9wuUZtjPfmviCcVWif5+7j4kBI2mWaZwjNNeg4eg=",
"rev": "d7f52a7a640bc54c7bb414cca603835bf8dd4b10", "rev": "a7fc11be66bdfb5cdde611ee5ce381c183da8386",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre871443.d7f52a7a640b/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre887438.a7fc11be66bd/nixexprs.tar.xz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
@@ -181,11 +181,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1761311587, "lastModified": 1762366246,
"narHash": "sha256-Msq86cR5SjozQGCnC6H8C+0cD4rnx91BPltZ9KK613Y=", "narHash": "sha256-3xc/f/ZNb5ma9Fc9knIzEwygXotA+0BZFQ5V5XovSOQ=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "2eddae033e4e74bf581c2d1dfa101f9033dbd2dc", "rev": "a82c779ca992190109e431d7d680860e6723e048",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -5,7 +5,7 @@
}: }:
{ {
# If we also need zfs, we can use the unstable version as we otherwise don't have a new enough kernel version # If we also need zfs, we can use the unstable version as we otherwise don't have a new enough kernel version
boot.zfs.package = pkgs.zfsUnstable; boot.zfs.package = pkgs.zfs_unstable or pkgs.zfsUnstable;
# Enable bcachefs support # Enable bcachefs support
boot.supportedFilesystems.bcachefs = lib.mkDefault true; boot.supportedFilesystems.bcachefs = lib.mkDefault true;

View File

@@ -18,7 +18,7 @@ let
inputs.data-mesher.nixosModules.data-mesher inputs.data-mesher.nixosModules.data-mesher
]; ];
config = { config = {
clan.core.clanPkgs = lib.mkDefault self.packages.${pkgs.hostPlatform.system}; clan.core.clanPkgs = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system};
}; };
}; };
in in

View File

@@ -6,7 +6,7 @@
}: }:
let let
isUnstable = config.boot.zfs.package == pkgs.zfsUnstable; isUnstable = config.boot.zfs.package == pkgs.zfs_unstable or pkgs.zfsUnstable;
zfsCompatibleKernelPackages = lib.filterAttrs ( zfsCompatibleKernelPackages = lib.filterAttrs (
name: kernelPackages: name: kernelPackages:
(builtins.match "linux_[0-9]+_[0-9]+" name) != null (builtins.match "linux_[0-9]+_[0-9]+" name) != null
@@ -30,5 +30,5 @@ let
in in
{ {
# Note this might jump back and worth as kernel get added or removed. # Note this might jump back and worth as kernel get added or removed.
boot.kernelPackages = lib.mkIf (lib.meta.availableOn pkgs.hostPlatform pkgs.zfs) latestKernelPackage; boot.kernelPackages = lib.mkIf (lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.zfs) latestKernelPackage;
} }

View File

@@ -4,6 +4,7 @@
padding: 8px; padding: 8px;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 4px;
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--clr-border-def-2, #d8e8eb); border: 1px solid var(--clr-border-def-2, #d8e8eb);

View File

@@ -1,11 +1,13 @@
import { onCleanup, onMount } from "solid-js"; import { onCleanup, onMount } from "solid-js";
import styles from "./ContextMenu.module.css"; import styles from "./ContextMenu.module.css";
import { Typography } from "../Typography/Typography"; import { Typography } from "../Typography/Typography";
import { Divider } from "../Divider/Divider";
import Icon from "../Icon/Icon";
export const Menu = (props: { export const Menu = (props: {
x: number; x: number;
y: number; y: number;
onSelect: (option: "move") => void; onSelect: (option: "move" | "delete") => void;
close: () => void; close: () => void;
intersect: string[]; intersect: string[];
}) => { }) => {
@@ -54,13 +56,31 @@ export const Menu = (props: {
> >
<Typography <Typography
hierarchy="label" hierarchy="label"
size="s"
weight="bold"
color={currentMachine() ? "primary" : "quaternary"} color={currentMachine() ? "primary" : "quaternary"}
> >
Move Move
</Typography> </Typography>
</li> </li>
<Divider />
<li
class={styles.item}
aria-disabled={!currentMachine()}
onClick={() => {
console.log("Delete clicked", currentMachine());
props.onSelect("delete");
props.close();
}}
>
<Typography
hierarchy="label"
color={currentMachine() ? "primary" : "quaternary"}
>
<span class="flex items-center gap-2">
Delete
<Icon icon="Trash" font-size="inherit" />
</span>
</Typography>
</li>
</ul> </ul>
); );
}; };

View File

@@ -71,7 +71,7 @@ const Machines = () => {
} }
const result = ctx.machinesQuery.data; const result = ctx.machinesQuery.data;
return Object.keys(result).length > 0 ? result : undefined; return Object.keys(result).length > 0 ? result : [];
}; };
return ( return (
@@ -117,7 +117,7 @@ const Machines = () => {
} }
> >
<nav> <nav>
<For each={Object.entries(machines()!)}> <For each={Object.entries(machines())}>
{([id, machine]) => ( {([id, machine]) => (
<MachineRoute <MachineRoute
clanURI={clanURI} clanURI={clanURI}

View File

@@ -206,8 +206,8 @@ const ClanSceneController = (props: RouteSectionProps) => {
<AddMachine <AddMachine
onCreated={async (id) => { onCreated={async (id) => {
const promise = currentPromise(); const promise = currentPromise();
if (promise) {
await ctx.machinesQuery.refetch(); await ctx.machinesQuery.refetch();
if (promise) {
promise.resolve({ id }); promise.resolve({ id });
setCurrentPromise(null); setCurrentPromise(null);
} }

View File

@@ -18,12 +18,12 @@ export class MachineManager {
private disposeRoot: () => void; private disposeRoot: () => void;
private machinePositionsSignal: Accessor<SceneData>; private machinePositionsSignal: Accessor<SceneData | undefined>;
constructor( constructor(
scene: THREE.Scene, scene: THREE.Scene,
registry: ObjectRegistry, registry: ObjectRegistry,
machinePositionsSignal: Accessor<SceneData>, machinePositionsSignal: Accessor<SceneData | undefined>,
machinesQueryResult: MachinesQueryResult, machinesQueryResult: MachinesQueryResult,
selectedIds: Accessor<Set<string>>, selectedIds: Accessor<Set<string>>,
setMachinePos: (id: string, position: [number, number] | null) => void, setMachinePos: (id: string, position: [number, number] | null) => void,
@@ -39,8 +39,9 @@ export class MachineManager {
if (!machinesQueryResult.data) return; if (!machinesQueryResult.data) return;
const actualIds = Object.keys(machinesQueryResult.data); const actualIds = Object.keys(machinesQueryResult.data);
const machinePositions = machinePositionsSignal();
// Remove stale const machinePositions = machinePositionsSignal() || {};
for (const id of Object.keys(machinePositions)) { for (const id of Object.keys(machinePositions)) {
if (!actualIds.includes(id)) { if (!actualIds.includes(id)) {
console.log("Removing stale machine", id); console.log("Removing stale machine", id);
@@ -61,8 +62,7 @@ export class MachineManager {
// Effect 2: sync store → scene // Effect 2: sync store → scene
// //
createEffect(() => { createEffect(() => {
const positions = machinePositionsSignal(); const positions = machinePositionsSignal() || {};
if (!positions) return;
// Remove machines from scene // Remove machines from scene
for (const [id, repr] of this.machines) { for (const [id, repr] of this.machines) {
@@ -103,7 +103,7 @@ export class MachineManager {
nextGridPos(): [number, number] { nextGridPos(): [number, number] {
const occupiedPositions = new Set( const occupiedPositions = new Set(
Object.values(this.machinePositionsSignal()).map((data) => Object.values(this.machinePositionsSignal() || {}).map((data) =>
keyFromPos(data.position), keyFromPos(data.position),
), ),
); );

View File

@@ -32,6 +32,9 @@ import {
} from "./highlightStore"; } from "./highlightStore";
import { createMachineMesh } from "./MachineRepr"; import { createMachineMesh } from "./MachineRepr";
import { useClanContext } from "@/src/routes/Clan/Clan"; import { useClanContext } from "@/src/routes/Clan/Clan";
import client from "@api/clan/client";
import { navigateToClan } from "../hooks/clan";
import { useNavigate } from "@solidjs/router";
function intersectMachines( function intersectMachines(
event: MouseEvent, event: MouseEvent,
@@ -100,7 +103,7 @@ export function CubeScene(props: {
onCreate: () => Promise<{ id: string }>; onCreate: () => Promise<{ id: string }>;
selectedIds: Accessor<Set<string>>; selectedIds: Accessor<Set<string>>;
onSelect: (v: Set<string>) => void; onSelect: (v: Set<string>) => void;
sceneStore: Accessor<SceneData>; sceneStore: Accessor<SceneData | undefined>;
setMachinePos: (machineId: string, pos: [number, number] | null) => void; setMachinePos: (machineId: string, pos: [number, number] | null) => void;
isLoading: boolean; isLoading: boolean;
clanURI: string; clanURI: string;
@@ -131,9 +134,6 @@ export function CubeScene(props: {
let machineManager: MachineManager; let machineManager: MachineManager;
const [positionMode, setPositionMode] = createSignal<"grid" | "circle">(
"grid",
);
// Managed by controls // Managed by controls
const [isDragging, setIsDragging] = createSignal(false); const [isDragging, setIsDragging] = createSignal(false);
@@ -142,10 +142,6 @@ export function CubeScene(props: {
// TODO: Unify this with actionRepr position // TODO: Unify this with actionRepr position
const [cursorPosition, setCursorPosition] = createSignal<[number, number]>(); const [cursorPosition, setCursorPosition] = createSignal<[number, number]>();
const [cameraInfo, setCameraInfo] = createSignal({
position: { x: 0, y: 0, z: 0 },
spherical: { radius: 0, theta: 0, phi: 0 },
});
// Context menu state // Context menu state
const [contextOpen, setContextOpen] = createSignal(false); const [contextOpen, setContextOpen] = createSignal(false);
const [menuPos, setMenuPos] = createSignal<{ x: number; y: number }>(); const [menuPos, setMenuPos] = createSignal<{ x: number; y: number }>();
@@ -157,7 +153,6 @@ export function CubeScene(props: {
const BASE_SIZE = 0.9; // Height of the cube above the ground const BASE_SIZE = 0.9; // Height of the cube above the ground
const CUBE_SIZE = BASE_SIZE / 1.5; // const CUBE_SIZE = BASE_SIZE / 1.5; //
const BASE_HEIGHT = 0.05; // Height of the cube above the ground const BASE_HEIGHT = 0.05; // Height of the cube above the ground
const CUBE_Y = 0 + CUBE_SIZE / 2 + BASE_HEIGHT / 2; // Y position of the cube above the ground
const CUBE_SEGMENT_HEIGHT = CUBE_SIZE / 1; const CUBE_SEGMENT_HEIGHT = CUBE_SIZE / 1;
const FLOOR_COLOR = 0xcdd8d9; const FLOOR_COLOR = 0xcdd8d9;
@@ -201,6 +196,8 @@ export function CubeScene(props: {
const grid = new THREE.GridHelper(1000, 1000 / 1, 0xe1edef, 0xe1edef); const grid = new THREE.GridHelper(1000, 1000 / 1, 0xe1edef, 0xe1edef);
const navigate = useNavigate();
onMount(() => { onMount(() => {
// Scene setup // Scene setup
scene = new THREE.Scene(); scene = new THREE.Scene();
@@ -311,21 +308,12 @@ export function CubeScene(props: {
bgCamera, bgCamera,
); );
// controls.addEventListener("start", (e) => {
// setIsDragging(true);
// });
// controls.addEventListener("end", (e) => {
// setIsDragging(false);
// });
// Lighting // Lighting
const ambientLight = new THREE.AmbientLight(0xd9f2f7, 0.72); const ambientLight = new THREE.AmbientLight(0xd9f2f7, 0.72);
scene.add(ambientLight); scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 3.5); const directionalLight = new THREE.DirectionalLight(0xffffff, 3.5);
// scene.add(new THREE.DirectionalLightHelper(directionalLight));
// scene.add(new THREE.CameraHelper(camera));
const lightPos = new THREE.Spherical( const lightPos = new THREE.Spherical(
15, 15,
initialSphericalCameraPosition.phi - Math.PI / 8, initialSphericalCameraPosition.phi - Math.PI / 8,
@@ -412,30 +400,6 @@ export function CubeScene(props: {
actionMachine = createActionMachine(); actionMachine = createActionMachine();
scene.add(actionMachine); scene.add(actionMachine);
// const spherical = new THREE.Spherical();
// spherical.setFromVector3(camera.position);
// Function to update camera info
const updateCameraInfo = () => {
const spherical = new THREE.Spherical();
spherical.setFromVector3(camera.position);
setCameraInfo({
position: {
x: Math.round(camera.position.x * 100) / 100,
y: Math.round(camera.position.y * 100) / 100,
z: Math.round(camera.position.z * 100) / 100,
},
spherical: {
radius: Math.round(spherical.radius * 100) / 100,
theta: Math.round(spherical.theta * 100) / 100,
phi: Math.round(spherical.phi * 100) / 100,
},
});
};
// Initial camera info update
updateCameraInfo();
createEffect( createEffect(
on(ctx.worldMode, (mode) => { on(ctx.worldMode, (mode) => {
if (mode === "create") { if (mode === "create") {
@@ -661,7 +625,8 @@ export function CubeScene(props: {
}); });
const snapToGrid = (point: THREE.Vector3) => { const snapToGrid = (point: THREE.Vector3) => {
if (!props.sceneStore) return; const store = props.sceneStore() || {};
// Snap to grid // Snap to grid
const snapped = new THREE.Vector3( const snapped = new THREE.Vector3(
Math.round(point.x / GRID_SIZE) * GRID_SIZE, Math.round(point.x / GRID_SIZE) * GRID_SIZE,
@@ -670,7 +635,7 @@ export function CubeScene(props: {
); );
// Skip snapping if there's already a cube at this position // Skip snapping if there's already a cube at this position
const positions = Object.entries(props.sceneStore()); const positions = Object.entries(store);
const intersects = positions.some( const intersects = positions.some(
([_id, p]) => p.position[0] === snapped.x && p.position[1] === snapped.z, ([_id, p]) => p.position[0] === snapped.x && p.position[1] === snapped.z,
); );
@@ -694,7 +659,6 @@ export function CubeScene(props: {
}; };
const onAddClick = (event: MouseEvent) => { const onAddClick = (event: MouseEvent) => {
setPositionMode("grid");
ctx.setWorldMode("create"); ctx.setWorldMode("create");
renderLoop.requestRender(); renderLoop.requestRender();
}; };
@@ -706,9 +670,6 @@ export function CubeScene(props: {
if (!actionRepr) return; if (!actionRepr) return;
actionRepr.visible = true; actionRepr.visible = true;
// (actionRepr.material as THREE.MeshPhongMaterial).emissive.set(
// worldMode() === "create" ? CREATE_BASE_EMISSIVE : MOVE_BASE_EMISSIVE,
// );
// Calculate mouse position in normalized device coordinates // Calculate mouse position in normalized device coordinates
// (-1 to +1) for both components // (-1 to +1) for both components
@@ -736,15 +697,31 @@ export function CubeScene(props: {
} }
} }
}; };
const handleMenuSelect = (mode: "move") => { const handleMenuSelect = async (mode: "move" | "delete") => {
const firstId = menuIntersection()[0];
if (!firstId) {
return;
}
const machine = machineManager.machines.get(firstId);
if (mode === "delete") {
console.log("deleting machine", firstId);
await client.post("delete_machine", {
body: {
machine: { flake: { identifier: props.clanURI }, name: firstId },
},
});
navigateToClan(navigate, props.clanURI);
ctx.machinesQuery.refetch();
ctx.serviceInstancesQuery.refetch();
return;
}
// Else "move" mode
ctx.setWorldMode(mode); ctx.setWorldMode(mode);
setHighlightGroups({ move: new Set(menuIntersection()) }); setHighlightGroups({ move: new Set(menuIntersection()) });
// Find the position of the first selected machine // Find the position of the first selected machine
// Set the actionMachine position to that // Set the actionMachine position to that
const firstId = menuIntersection()[0];
if (firstId) {
const machine = machineManager.machines.get(firstId);
if (machine && actionMachine) { if (machine && actionMachine) {
actionMachine.position.set( actionMachine.position.set(
machine.group.position.x, machine.group.position.x,
@@ -753,7 +730,6 @@ export function CubeScene(props: {
); );
setCursorPosition([machine.group.position.x, machine.group.position.z]); setCursorPosition([machine.group.position.x, machine.group.position.z]);
} }
}
}; };
createEffect( createEffect(

View File

@@ -766,6 +766,28 @@ def test_prompt(
assert sops_store.get(my_generator, "prompt_persist").decode() == "prompt_persist" assert sops_store.get(my_generator, "prompt_persist").decode() == "prompt_persist"
@pytest.mark.with_core
def test_non_existing_dependency_raises_error(
monkeypatch: pytest.MonkeyPatch,
flake_with_sops: ClanFlake,
) -> None:
"""Ensure that a generator with a non-existing dependency raises a clear error."""
flake = flake_with_sops
config = flake.machines["my_machine"] = create_test_machine_config()
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_value"]["secret"] = False
my_generator["script"] = 'echo "$RANDOM" > "$out"/my_value'
my_generator["dependencies"] = ["non_existing_generator"]
flake.refresh()
monkeypatch.chdir(flake.path)
with pytest.raises(
ClanError,
match="Generator 'my_generator' on machine 'my_machine' depends on generator 'non_existing_generator', but 'non_existing_generator' does not exist",
):
cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"])
@pytest.mark.with_core @pytest.mark.with_core
def test_shared_vars_must_never_depend_on_machine_specific_vars( def test_shared_vars_must_never_depend_on_machine_specific_vars(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,

View File

@@ -66,6 +66,41 @@ class Generator:
_public_store: "StoreBase | None" = None _public_store: "StoreBase | None" = None
_secret_store: "StoreBase | None" = None _secret_store: "StoreBase | None" = None
@staticmethod
def validate_dependencies(
generator_name: str,
machine_name: str,
dependencies: list[str],
generators_data: dict[str, dict],
) -> list[GeneratorKey]:
"""Validate and build dependency keys for a generator.
Args:
generator_name: Name of the generator that has dependencies
machine_name: Name of the machine the generator belongs to
dependencies: List of dependency generator names
generators_data: Dictionary of all available generators for this machine
Returns:
List of GeneratorKey objects
Raises:
ClanError: If a dependency does not exist
"""
deps_list = []
for dep in dependencies:
if dep not in generators_data:
msg = f"Generator '{generator_name}' on machine '{machine_name}' depends on generator '{dep}', but '{dep}' does not exist. Please check your configuration."
raise ClanError(msg)
deps_list.append(
GeneratorKey(
machine=None if generators_data[dep]["share"] else machine_name,
name=dep,
)
)
return deps_list
@property @property
def key(self) -> GeneratorKey: def key(self) -> GeneratorKey:
if self.share: if self.share:
@@ -240,15 +275,12 @@ class Generator:
name=gen_name, name=gen_name,
share=share, share=share,
files=files, files=files,
dependencies=[ dependencies=cls.validate_dependencies(
GeneratorKey( gen_name,
machine=None machine_name,
if generators_data[dep]["share"] gen_data["dependencies"],
else machine_name, generators_data,
name=dep, ),
)
for dep in gen_data["dependencies"]
],
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,

View File

@@ -245,7 +245,7 @@ class SecretStore(StoreBase):
output_dir / "activation" / generator.name / file.name output_dir / "activation" / generator.name / file.name
) )
out_file.parent.mkdir(parents=True, exist_ok=True) out_file.parent.mkdir(parents=True, exist_ok=True)
out_file.write_bytes(self.get(generator, file.name)) out_file.write_bytes(file.value)
if "partitioning" in phases: if "partitioning" in phases:
for generator in vars_generators: for generator in vars_generators:
for file in generator.files: for file in generator.files:
@@ -254,7 +254,7 @@ class SecretStore(StoreBase):
output_dir / "partitioning" / generator.name / file.name output_dir / "partitioning" / generator.name / file.name
) )
out_file.parent.mkdir(parents=True, exist_ok=True) out_file.parent.mkdir(parents=True, exist_ok=True)
out_file.write_bytes(self.get(generator, file.name)) out_file.write_bytes(file.value)
hash_data = self.generate_hash(machine) hash_data = self.generate_hash(machine)
if hash_data: if hash_data:

View File

@@ -246,7 +246,7 @@ class SecretStore(StoreBase):
) )
# chmod after in case it doesn't have u+w # chmod after in case it doesn't have u+w
target_path.touch(mode=0o600) target_path.touch(mode=0o600)
target_path.write_bytes(self.get(generator, file.name)) target_path.write_bytes(file.value)
target_path.chmod(file.mode) target_path.chmod(file.mode)
if "partitioning" in phases: if "partitioning" in phases:
@@ -260,7 +260,7 @@ class SecretStore(StoreBase):
) )
# chmod after in case it doesn't have u+w # chmod after in case it doesn't have u+w
target_path.touch(mode=0o600) target_path.touch(mode=0o600)
target_path.write_bytes(self.get(generator, file.name)) target_path.write_bytes(file.value)
target_path.chmod(file.mode) target_path.chmod(file.mode)
@override @override

View File

@@ -211,7 +211,7 @@ class ClanSelectError(ClanError):
def __str__(self) -> str: def __str__(self) -> str:
if self.description: if self.description:
return f"{self.msg} Reason: {self.description}" return f"{self.msg} Reason: {self.description}. Use flag '--debug' to see full nix trace."
return self.msg return self.msg
def __repr__(self) -> str: def __repr__(self) -> str:

View File

@@ -59,9 +59,7 @@ def upload_sources(machine: Machine, ssh: Host, upload_inputs: bool) -> str:
if not has_path_inputs and not upload_inputs: if not has_path_inputs and not upload_inputs:
# Just copy the flake to the remote machine, we can substitute other inputs there. # Just copy the flake to the remote machine, we can substitute other inputs there.
path = flake_data["path"] path = flake_data["path"]
if machine._class_ == "darwin": remote_url = f"ssh-ng://{remote_url_base}"
remote_program_params = "?remote-program=bash -lc 'exec nix-daemon --stdio'"
remote_url = f"ssh-ng://{remote_url_base}{remote_program_params}"
cmd = nix_command( cmd = nix_command(
[ [
"copy", "copy",

View File

@@ -17,7 +17,7 @@
runCommand, runCommand,
setuptools, setuptools,
webkitgtk_6_0, webkitgtk_6_0,
wrapGAppsHook, wrapGAppsHook3,
python, python,
lib, lib,
stdenv, stdenv,
@@ -87,7 +87,7 @@ buildPythonApplication rec {
nativeBuildInputs = [ nativeBuildInputs = [
setuptools setuptools
copyDesktopItems copyDesktopItems
wrapGAppsHook wrapGAppsHook3
gobject-introspection gobject-introspection
]; ];