From 32748c14f4789cc57450d84f9dfaf333318889b1 Mon Sep 17 00:00:00 2001 From: lassulus Date: Thu, 6 Feb 2025 04:51:28 +0100 Subject: [PATCH] clan_cli machines: use Flake instead of FlakeId --- pkgs/clan-cli/clan_cli/__init__.py | 12 +-- pkgs/clan-cli/clan_cli/clan/create.py | 4 +- pkgs/clan-cli/clan_cli/clan/inspect.py | 14 ++-- pkgs/clan-cli/clan_cli/clan_uri.py | 63 ++------------ pkgs/clan-cli/clan_cli/completions.py | 4 +- pkgs/clan-cli/clan_cli/dirs.py | 4 +- .../clan_cli/facts/public_modules/in_repo.py | 2 +- .../clan_cli/facts/public_modules/vm.py | 2 +- .../clan_cli/facts/secret_modules/vm.py | 2 +- pkgs/clan-cli/clan_cli/flake.py | 64 ++++++++++++-- pkgs/clan-cli/clan_cli/flash/flash_cmd.py | 4 +- pkgs/clan-cli/clan_cli/machines/create.py | 8 +- pkgs/clan-cli/clan_cli/machines/delete.py | 4 +- pkgs/clan-cli/clan_cli/machines/hardware.py | 4 +- pkgs/clan-cli/clan_cli/machines/inventory.py | 6 +- pkgs/clan-cli/clan_cli/machines/machines.py | 11 +-- pkgs/clan-cli/clan_cli/machines/update.py | 7 +- pkgs/clan-cli/clan_cli/secrets/secrets.py | 4 +- pkgs/clan-cli/clan_cli/select.py | 5 ++ pkgs/clan-cli/clan_cli/templates.py | 10 +-- pkgs/clan-cli/clan_cli/vars/get.py | 4 +- pkgs/clan-cli/clan_cli/vars/keygen.py | 4 +- pkgs/clan-cli/clan_cli/vars/list.py | 8 +- .../clan_cli/vars/public_modules/in_repo.py | 2 +- .../clan_cli/vars/public_modules/vm.py | 2 +- .../clan_cli/vars/secret_modules/vm.py | 2 +- pkgs/clan-cli/clan_cli/vars/set.py | 8 +- pkgs/clan-cli/clan_cli/vms/inspect.py | 10 +-- pkgs/clan-cli/clan_cli/vms/run.py | 2 +- .../tests/test_api_dataclass_compat.py | 1 - pkgs/clan-cli/tests/test_clan_nix_attrset.py | 4 +- pkgs/clan-cli/tests/test_clan_uri.py | 55 ++++++------ pkgs/clan-cli/tests/test_deserializers.py | 4 +- pkgs/clan-cli/tests/test_dirs.py | 9 +- pkgs/clan-cli/tests/test_modules.py | 6 +- pkgs/clan-cli/tests/test_secrets_generate.py | 6 +- .../tests/test_secrets_password_store.py | 4 +- pkgs/clan-cli/tests/test_vars.py | 83 +++++++++---------- pkgs/clan-cli/tests/test_vars_deployment.py | 6 +- pkgs/clan-cli/tests/test_vms_cli.py | 4 +- .../clan_vm_manager/components/vmobj.py | 16 ++-- .../clan_vm_manager/singletons/use_vms.py | 7 +- .../app/src/components/MachineListItem.tsx | 2 +- pkgs/webview-ui/app/src/routes/flash/view.tsx | 2 +- .../app/src/routes/machines/create.tsx | 4 +- .../app/src/routes/machines/details.tsx | 2 +- .../routes/machines/install/hardware-step.tsx | 2 +- 47 files changed, 242 insertions(+), 251 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index affd2c70c..3f39c4e88 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -22,11 +22,11 @@ from . import ( state, vms, ) -from .clan_uri import FlakeId from .custom_logger import setup_logging from .dirs import get_clan_flake_toplevel_or_env from .errors import ClanError from .facts import cli as facts +from .flake import Flake from .flash import cli as flash_cli from .hyperlink import help_hyperlink from .machines import cli as machines @@ -41,17 +41,17 @@ with contextlib.suppress(ImportError): import argcomplete # type: ignore[no-redef] -def flake_path(arg: str) -> FlakeId: +def flake_path(arg: str) -> Flake: flake_dir = Path(arg).resolve() if flake_dir.exists() and flake_dir.is_dir(): - return FlakeId(str(flake_dir)) - return FlakeId(arg) + return Flake(str(flake_dir)) + return Flake(arg) -def default_flake() -> FlakeId | None: +def default_flake() -> Flake | None: val = get_clan_flake_toplevel_or_env() if val: - return FlakeId(str(val)) + return Flake(str(val)) return None diff --git a/pkgs/clan-cli/clan_cli/clan/create.py b/pkgs/clan-cli/clan_cli/clan/create.py index f949f9a4a..bdec765b2 100644 --- a/pkgs/clan-cli/clan_cli/clan/create.py +++ b/pkgs/clan-cli/clan_cli/clan/create.py @@ -5,9 +5,9 @@ from dataclasses import dataclass from pathlib import Path from clan_cli.api import API -from clan_cli.clan_uri import FlakeId from clan_cli.cmd import CmdOut, RunOpts, run from clan_cli.errors import ClanError +from clan_cli.flake import Flake from clan_cli.inventory import Inventory, init_inventory from clan_cli.nix import nix_shell from clan_cli.templates import ( @@ -33,7 +33,7 @@ class CreateClanResponse: class CreateOptions: dest: Path template_name: str - src_flake: FlakeId | None = None + src_flake: Flake | None = None input_prio: InputPrio | None = None setup_git: bool = True initial: Inventory | None = None diff --git a/pkgs/clan-cli/clan_cli/clan/inspect.py b/pkgs/clan-cli/clan_cli/clan/inspect.py index 940f02885..32e36299d 100644 --- a/pkgs/clan-cli/clan_cli/clan/inspect.py +++ b/pkgs/clan-cli/clan_cli/clan/inspect.py @@ -3,10 +3,10 @@ from dataclasses import dataclass from pathlib import Path from typing import Any -from clan_cli.clan_uri import FlakeId from clan_cli.cmd import run from clan_cli.dirs import machine_gcroot from clan_cli.errors import ClanError +from clan_cli.flake import Flake from clan_cli.machines.list import list_nixos_machines from clan_cli.machines.machines import Machine from clan_cli.nix import ( @@ -21,7 +21,7 @@ from clan_cli.vms.inspect import VmConfig, inspect_vm @dataclass class FlakeConfig: - flake_url: FlakeId + flake_url: Flake flake_attr: str clan_name: str @@ -35,7 +35,7 @@ class FlakeConfig: @classmethod def from_json(cls: type["FlakeConfig"], data: dict[str, Any]) -> "FlakeConfig": return cls( - flake_url=FlakeId.from_json(data["flake_url"]), + flake_url=Flake.from_json(data["flake_url"]), flake_attr=data["flake_attr"], clan_name=data["clan_name"], nar_hash=data["nar_hash"], @@ -62,7 +62,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig: msg = f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}" raise ClanError(msg) - machine = Machine(machine_name, FlakeId(str(flake_url))) + machine = Machine(machine_name, Flake(str(flake_url))) vm = inspect_vm(machine) # Make symlink to gcroots from vm.machine_icon @@ -105,7 +105,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig: meta = nix_metadata(flake_url) return FlakeConfig( vm=vm, - flake_url=FlakeId(str(flake_url)), + flake_url=Flake(str(flake_url)), clan_name=clan_name, flake_attr=machine_name, nar_hash=meta["locked"]["narHash"], @@ -119,13 +119,13 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig: @dataclass class InspectOptions: machine: str - flake: FlakeId + flake: Flake def inspect_command(args: argparse.Namespace) -> None: inspect_options = InspectOptions( machine=args.machine, - flake=args.flake or FlakeId(str(Path.cwd())), + flake=args.flake or Flake(str(Path.cwd())), ) res = inspect_flake( flake_url=str(inspect_options.flake), machine_name=inspect_options.machine diff --git a/pkgs/clan-cli/clan_cli/clan_uri.py b/pkgs/clan-cli/clan_cli/clan_uri.py index c34d780cb..62431a667 100644 --- a/pkgs/clan-cli/clan_cli/clan_uri.py +++ b/pkgs/clan-cli/clan_cli/clan_uri.py @@ -3,68 +3,14 @@ import urllib.parse import urllib.request from dataclasses import dataclass from pathlib import Path -from typing import Any - -@dataclass -class FlakeId: - loc: str - - @classmethod - def from_json(cls: type["FlakeId"], data: dict[str, Any]) -> "FlakeId": - return cls(loc=data["loc"]) - - def __str__(self) -> str: - return str(self.loc) - - def __hash__(self) -> int: - return hash(str(self.loc)) - - @property - def path(self) -> Path: - assert self.is_local(), f"Flake {self.loc} is not a local path" - return Path(self.loc) - - @property - def url(self) -> str: - assert self.is_remote(), f"Flake {self.loc} is not a remote url" - return str(self.loc) - - def is_local(self) -> bool: - """ - https://nix.dev/manual/nix/2.22/language/builtins.html?highlight=urlS#source-types - - Examples: - - - file:///home/eelco/nix/README.md file LOCAL - - git+file://git:github.com:NixOS/nixpkgs git+file LOCAL - - https://example.com/index.html https REMOTE - - github:nixos/nixpkgs github REMOTE - - ftp://serv.file ftp REMOTE - - ./. '' LOCAL - - """ - x = urllib.parse.urlparse(str(self.loc)) - # See above *file* or empty are the only local schemas - return x.scheme == "" or "file" in x.scheme - - def is_remote(self) -> bool: - return not self.is_local() - - -def _parse_url(comps: urllib.parse.ParseResult) -> FlakeId: - if comps.scheme == "" or "file" in comps.scheme: - res_p = Path(comps.path).expanduser().resolve() - flake_id = FlakeId(str(res_p)) - else: - flake_id = FlakeId(comps.geturl()) - return flake_id +from clan_cli.flake import Flake # Define the ClanURI class @dataclass class ClanURI: - flake: FlakeId + flake: Flake machine_name: str def get_url(self) -> str: @@ -104,7 +50,10 @@ class ClanURI: clean_comps = components._replace(query=components.query, fragment="") # Parse the URL into a ClanUrl object - flake = _parse_url(clean_comps) + if clean_comps.path and Path(clean_comps.path).exists(): + flake = Flake(clean_comps.path) + else: + flake = Flake(clean_comps.geturl()) machine_name = "defaultVM" if components.fragment: machine_name = components.fragment diff --git a/pkgs/clan-cli/clan_cli/completions.py b/pkgs/clan-cli/clan_cli/completions.py index a9efb4c46..c224b6ef5 100644 --- a/pkgs/clan-cli/clan_cli/completions.py +++ b/pkgs/clan-cli/clan_cli/completions.py @@ -207,13 +207,13 @@ def complete_secrets( """ Provides completion functionality for clan secrets """ - from .clan_uri import FlakeId + from .clan_uri import Flake from .secrets.secrets import ListSecretsOptions, list_secrets flake = clan_dir_result if (clan_dir_result := clan_dir(None)) is not None else "." options = ListSecretsOptions( - flake=FlakeId(flake), + flake=Flake(flake), pattern=None, ) diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index 6f3c6f137..6cbf23e66 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -5,8 +5,6 @@ import urllib from enum import Enum from pathlib import Path -from clan_cli.clan_uri import FlakeId - from .errors import ClanError log = logging.getLogger(__name__) @@ -110,7 +108,7 @@ def user_history_file() -> Path: return user_config_dir() / "clan" / "history" -def vm_state_dir(flake_url: FlakeId, vm_name: str) -> Path: +def vm_state_dir(flake_url: str, vm_name: str) -> Path: clan_key = clan_key_safe(str(flake_url)) return user_data_dir() / "clan" / "vmstate" / clan_key / vm_name diff --git a/pkgs/clan-cli/clan_cli/facts/public_modules/in_repo.py b/pkgs/clan-cli/clan_cli/facts/public_modules/in_repo.py index 756525d5a..8f4ba71da 100644 --- a/pkgs/clan-cli/clan_cli/facts/public_modules/in_repo.py +++ b/pkgs/clan-cli/clan_cli/facts/public_modules/in_repo.py @@ -12,7 +12,7 @@ class FactStore(FactStoreBase): self.works_remotely = False def set(self, service: str, name: str, value: bytes) -> Path | None: - if self.machine.flake.is_local(): + if self.machine.flake.is_local: fact_path = ( self.machine.flake.path / "machines" diff --git a/pkgs/clan-cli/clan_cli/facts/public_modules/vm.py b/pkgs/clan-cli/clan_cli/facts/public_modules/vm.py index ae458ff5d..8788a199d 100644 --- a/pkgs/clan-cli/clan_cli/facts/public_modules/vm.py +++ b/pkgs/clan-cli/clan_cli/facts/public_modules/vm.py @@ -14,7 +14,7 @@ class FactStore(FactStoreBase): def __init__(self, machine: Machine) -> None: self.machine = machine self.works_remotely = False - self.dir = vm_state_dir(machine.flake, machine.name) / "facts" + self.dir = vm_state_dir(machine.flake.identifier, machine.name) / "facts" machine.debug(f"FactStore initialized with dir {self.dir}") def exists(self, service: str, name: str) -> bool: diff --git a/pkgs/clan-cli/clan_cli/facts/secret_modules/vm.py b/pkgs/clan-cli/clan_cli/facts/secret_modules/vm.py index befcdf766..5e0f4892a 100644 --- a/pkgs/clan-cli/clan_cli/facts/secret_modules/vm.py +++ b/pkgs/clan-cli/clan_cli/facts/secret_modules/vm.py @@ -10,7 +10,7 @@ from . import SecretStoreBase class SecretStore(SecretStoreBase): def __init__(self, machine: Machine) -> None: self.machine = machine - self.dir = vm_state_dir(machine.flake, machine.name) / "secrets" + self.dir = vm_state_dir(machine.flake.identifier, machine.name) / "secrets" self.dir.mkdir(parents=True, exist_ok=True) def set( diff --git a/pkgs/clan-cli/clan_cli/flake.py b/pkgs/clan-cli/clan_cli/flake.py index 9d1d70fa3..68b7e73bb 100644 --- a/pkgs/clan-cli/clan_cli/flake.py +++ b/pkgs/clan-cli/clan_cli/flake.py @@ -1,7 +1,7 @@ import json import logging import re -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from typing import Any @@ -275,9 +275,43 @@ class Flake: """ identifier: str - cache: FlakeCache = field(default_factory=FlakeCache) def __post_init__(self) -> None: + self._cache: FlakeCache | None = None + self._path: Path | None = None + self._is_local: bool | None = None + + @classmethod + def from_json(cls: type["Flake"], data: dict[str, Any]) -> "Flake": + return cls(data["identifier"]) + + def __str__(self) -> str: + return self.identifier + + def __hash__(self) -> int: + return hash(self.identifier) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Flake): + return NotImplemented + return self.identifier == other.identifier + + @property + def is_local(self) -> bool: + if self._is_local is None: + self.prefetch() + assert isinstance(self._is_local, bool) + return self._is_local + + @property + def path(self) -> Path: + if self._path is None: + self.prefetch() + assert isinstance(self._path, Path) + return self._path + + def prefetch(self) -> None: + self._cache = FlakeCache() flake_prefetch = run( nix_command( [ @@ -294,9 +328,23 @@ class Flake: flake_metadata = json.loads(flake_prefetch.stdout) self.store_path = flake_metadata["storePath"] self.hash = flake_metadata["hash"] - self.cache = FlakeCache() + if flake_metadata["original"].get("url", "").startswith("file:"): + self._is_local = True + path = flake_metadata["original"]["url"].removeprefix("file://") + path = path.removeprefix("file:") + self._path = Path(path) + elif flake_metadata["original"].get("path"): + self._is_local = True + self._path = Path(flake_metadata["original"]["path"]) + else: + self._is_local = False + self._path = Path(self.store_path) def prepare_cache(self, selectors: list[str]) -> None: + if self._cache is None: + self.prefetch() + assert self._cache is not None + config = nix_config() nix_code = f""" let @@ -312,10 +360,14 @@ class Flake: msg = f"flake_prepare_cache: Expected {len(outputs)} outputs, got {len(outputs)}" raise ClanError(msg) for i, selector in enumerate(selectors): - self.cache.insert(outputs[i], selector) + self._cache.insert(outputs[i], selector) def select(self, selector: str) -> Any: - if not self.cache.is_cached(selector): + if self._cache is None: + self.prefetch() + assert self._cache is not None + + if not self._cache.is_cached(selector): log.info(f"Cache miss for {selector}") self.prepare_cache([selector]) - return self.cache.select(selector) + return self._cache.select(selector) diff --git a/pkgs/clan-cli/clan_cli/flash/flash_cmd.py b/pkgs/clan-cli/clan_cli/flash/flash_cmd.py index f661ac3dd..75259dccf 100644 --- a/pkgs/clan-cli/clan_cli/flash/flash_cmd.py +++ b/pkgs/clan-cli/clan_cli/flash/flash_cmd.py @@ -6,8 +6,8 @@ from dataclasses import dataclass from pathlib import Path from typing import Any -from clan_cli.clan_uri import FlakeId from clan_cli.completions import add_dynamic_completer, complete_machines +from clan_cli.flake import Flake from clan_cli.machines.machines import Machine from .flash import Disk, SystemConfig, flash_machine @@ -17,7 +17,7 @@ log = logging.getLogger(__name__) @dataclass class FlashOptions: - flake: FlakeId + flake: Flake machine: str disks: list[Disk] dry_run: bool diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py index d145dd6ef..a0f3e5e82 100644 --- a/pkgs/clan-cli/clan_cli/machines/create.py +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -5,10 +5,10 @@ from dataclasses import dataclass from pathlib import Path from clan_cli.api import API -from clan_cli.clan_uri import FlakeId from clan_cli.completions import add_dynamic_completer, complete_tags from clan_cli.dirs import get_clan_flake_toplevel_or_env from clan_cli.errors import ClanError +from clan_cli.flake import Flake from clan_cli.git import commit_file from clan_cli.inventory import Machine as InventoryMachine from clan_cli.inventory import ( @@ -29,7 +29,7 @@ log = logging.getLogger(__name__) @dataclass class CreateOptions: - clan_dir: FlakeId + clan_dir: Flake machine: InventoryMachine target_host: str | None = None input_prio: InputPrio | None = None @@ -38,7 +38,7 @@ class CreateOptions: @API.register def create_machine(opts: CreateOptions) -> None: - if not opts.clan_dir.is_local(): + if not opts.clan_dir.is_local: msg = f"Clan {opts.clan_dir} is not a local clan." description = "Import machine only works on local clans" raise ClanError(msg, description=description) @@ -127,7 +127,7 @@ def create_command(args: argparse.Namespace) -> None: clan_dir = args.flake else: tmp = get_clan_flake_toplevel_or_env() - clan_dir = FlakeId(str(tmp)) if tmp else None + clan_dir = Flake(str(tmp)) if tmp else None if not clan_dir: msg = "No clan found." diff --git a/pkgs/clan-cli/clan_cli/machines/delete.py b/pkgs/clan-cli/clan_cli/machines/delete.py index d9db53b7e..5234fa487 100644 --- a/pkgs/clan-cli/clan_cli/machines/delete.py +++ b/pkgs/clan-cli/clan_cli/machines/delete.py @@ -3,7 +3,7 @@ import logging import shutil from clan_cli.api import API -from clan_cli.clan_uri import FlakeId +from clan_cli.clan_uri import Flake from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.dirs import specific_machine_dir from clan_cli.errors import ClanError @@ -13,7 +13,7 @@ log = logging.getLogger(__name__) @API.register -def delete_machine(flake: FlakeId, name: str) -> None: +def delete_machine(flake: Flake, name: str) -> None: inventory = get_inventory(flake.path) if "machines" not in inventory: diff --git a/pkgs/clan-cli/clan_cli/machines/hardware.py b/pkgs/clan-cli/clan_cli/machines/hardware.py index e7b62431f..abe5f8e24 100644 --- a/pkgs/clan-cli/clan_cli/machines/hardware.py +++ b/pkgs/clan-cli/clan_cli/machines/hardware.py @@ -6,11 +6,11 @@ from enum import Enum from pathlib import Path from clan_cli.api import API -from clan_cli.clan_uri import FlakeId from clan_cli.cmd import RunOpts, run, run_no_stdout from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.dirs import specific_machine_dir from clan_cli.errors import ClanCmdError, ClanError +from clan_cli.flake import Flake from clan_cli.git import commit_file from clan_cli.machines.machines import Machine from clan_cli.nix import nix_config, nix_eval, nix_shell @@ -102,7 +102,7 @@ def show_machine_hardware_platform(clan_dir: Path, machine_name: str) -> str | N @dataclass class HardwareGenerateOptions: - flake: FlakeId + flake: Flake machine: str backend: HardwareConfig target_host: str | None = None diff --git a/pkgs/clan-cli/clan_cli/machines/inventory.py b/pkgs/clan-cli/clan_cli/machines/inventory.py index ea8229fe7..28b1b5fe3 100644 --- a/pkgs/clan-cli/clan_cli/machines/inventory.py +++ b/pkgs/clan-cli/clan_cli/machines/inventory.py @@ -1,15 +1,15 @@ import json from pathlib import Path -from clan_cli.clan_uri import FlakeId from clan_cli.cmd import run +from clan_cli.flake import Flake from clan_cli.nix import nix_build, nix_config, nix_test_store from .machines import Machine # function to speedup eval if we want to evaluate all machines -def get_all_machines(flake: FlakeId, nix_options: list[str]) -> list[Machine]: +def get_all_machines(flake: Flake, nix_options: list[str]) -> list[Machine]: config = nix_config() system = config["system"] json_path = Path( @@ -37,7 +37,7 @@ def get_all_machines(flake: FlakeId, nix_options: list[str]) -> list[Machine]: def get_selected_machines( - flake: FlakeId, nix_options: list[str], machine_names: list[str] + flake: Flake, nix_options: list[str], machine_names: list[str] ) -> list[Machine]: machines = [] for name in machine_names: diff --git a/pkgs/clan-cli/clan_cli/machines/machines.py b/pkgs/clan-cli/clan_cli/machines/machines.py index 2b5e46fee..ce8b78c98 100644 --- a/pkgs/clan-cli/clan_cli/machines/machines.py +++ b/pkgs/clan-cli/clan_cli/machines/machines.py @@ -8,11 +8,11 @@ from tempfile import NamedTemporaryFile from time import time from typing import TYPE_CHECKING, Any, Literal -from clan_cli.clan_uri import FlakeId from clan_cli.cmd import RunOpts, run_no_stdout from clan_cli.errors import ClanError from clan_cli.facts import public_modules as facts_public_modules from clan_cli.facts import secret_modules as facts_secret_modules +from clan_cli.flake import Flake from clan_cli.nix import nix_build, nix_config, nix_eval, nix_metadata, nix_test_store from clan_cli.ssh.host import Host from clan_cli.ssh.host_key import HostKeyCheck @@ -28,7 +28,7 @@ if TYPE_CHECKING: @dataclass class Machine: name: str - flake: FlakeId + flake: Flake nix_options: list[str] = field(default_factory=list) cached_deployment: None | dict[str, Any] = None override_target_host: None | str = None @@ -201,12 +201,7 @@ class Machine: @property def flake_dir(self) -> Path: - if self.flake.is_local(): - return self.flake.path - if self.flake.is_remote(): - return Path(nix_metadata(self.flake.url)["path"]) - msg = f"Unsupported flake url: {self.flake}" - raise ClanError(msg) + return self.flake.path @property def target_host(self) -> Host: diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index 17e16f64b..ad39304df 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -8,7 +8,6 @@ import sys from clan_cli.api import API from clan_cli.async_run import AsyncContext, AsyncOpts, AsyncRuntime, is_async_cancelled -from clan_cli.clan_uri import FlakeId from clan_cli.cmd import MsgColor, RunOpts, run from clan_cli.colors import AnsiColor from clan_cli.completions import ( @@ -18,6 +17,7 @@ from clan_cli.completions import ( from clan_cli.errors import ClanError from clan_cli.facts.generate import generate_facts from clan_cli.facts.upload import upload_secrets +from clan_cli.flake import Flake from clan_cli.inventory import Machine as InventoryMachine from clan_cli.machines.machines import Machine from clan_cli.nix import nix_command, nix_metadata @@ -46,7 +46,7 @@ def upload_sources(machine: Machine) -> str: env = host.nix_ssh_env(os.environ.copy()) flake_url = ( - str(machine.flake.path) if machine.flake.is_local() else machine.flake.url + str(machine.flake.path) if machine.flake.is_local else machine.flake.identifier ) flake_data = nix_metadata(flake_url) has_path_inputs = any( @@ -96,6 +96,7 @@ def update_machines(base_path: str, machines: list[InventoryMachine]) -> None: group_machines: list[Machine] = [] # Convert InventoryMachine to Machine + flake = Flake(base_path) for machine in machines: name = machine.get("name") if not name: @@ -103,7 +104,7 @@ def update_machines(base_path: str, machines: list[InventoryMachine]) -> None: raise ClanError(msg) m = Machine( name, - flake=FlakeId(base_path), + flake=flake, ) if not machine.get("deploy", {}).get("targetHost"): msg = f"'TargetHost' is not set for machine '{name}'" diff --git a/pkgs/clan-cli/clan_cli/secrets/secrets.py b/pkgs/clan-cli/clan_cli/secrets/secrets.py index d0c5786f1..bf5489070 100644 --- a/pkgs/clan-cli/clan_cli/secrets/secrets.py +++ b/pkgs/clan-cli/clan_cli/secrets/secrets.py @@ -10,7 +10,6 @@ from dataclasses import dataclass from pathlib import Path from typing import IO -from clan_cli.clan_uri import FlakeId from clan_cli.completions import ( add_dynamic_completer, complete_groups, @@ -19,6 +18,7 @@ from clan_cli.completions import ( complete_users, ) from clan_cli.errors import ClanError +from clan_cli.flake import Flake from clan_cli.git import commit_files from . import sops @@ -341,7 +341,7 @@ def list_secrets(flake_dir: Path, pattern: str | None = None) -> list[str]: @dataclass class ListSecretsOptions: - flake: FlakeId + flake: Flake pattern: str | None diff --git a/pkgs/clan-cli/clan_cli/select.py b/pkgs/clan-cli/clan_cli/select.py index 7dd68985e..0cba83991 100644 --- a/pkgs/clan-cli/clan_cli/select.py +++ b/pkgs/clan-cli/clan_cli/select.py @@ -15,3 +15,8 @@ def register_parser(parser: argparse.ArgumentParser) -> None: "selector", help="select from a flake", ) + parser.add_argument( + "--impure", + action="store_true", + default=False, + ) diff --git a/pkgs/clan-cli/clan_cli/templates.py b/pkgs/clan-cli/clan_cli/templates.py index 196d9c766..1f80b2a5f 100644 --- a/pkgs/clan-cli/clan_cli/templates.py +++ b/pkgs/clan-cli/clan_cli/templates.py @@ -6,9 +6,9 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Literal, NewType, TypedDict -from clan_cli.clan_uri import FlakeId from clan_cli.cmd import run from clan_cli.errors import ClanError +from clan_cli.flake import Flake from clan_cli.nix import nix_eval log = logging.getLogger(__name__) @@ -59,7 +59,7 @@ class ClanExports(TypedDict): self: ClanAttrset -def get_clan_nix_attrset(clan_dir: FlakeId | None = None) -> ClanExports: +def get_clan_nix_attrset(clan_dir: Flake | None = None) -> ClanExports: # Check if the clan directory is provided, otherwise use the environment variable if not clan_dir: # TODO: Quickfix, templates dir seems to be missing in CLAN_CORE_PATH?? @@ -69,7 +69,7 @@ def get_clan_nix_attrset(clan_dir: FlakeId | None = None) -> ClanExports: # msg = "Environment var CLAN_CORE_PATH is not set, this shouldn't happen" # raise ClanError(msg) - clan_dir = FlakeId(clan_core_path) + clan_dir = Flake(clan_core_path) log.debug(f"Evaluating flake {clan_dir} for Clan attrsets") @@ -170,7 +170,7 @@ class TemplateList: def list_templates( - template_type: TemplateType, clan_dir: FlakeId | None = None + template_type: TemplateType, clan_dir: Flake | None = None ) -> TemplateList: clan_exports = get_clan_nix_attrset(clan_dir) result = TemplateList() @@ -204,7 +204,7 @@ def get_template( template_type: TemplateType, *, input_prio: InputPrio | None = None, - clan_dir: FlakeId | None = None, + clan_dir: Flake | None = None, ) -> FoundTemplate: log.info(f"Searching for template '{template_name}' of type '{template_type}'") diff --git a/pkgs/clan-cli/clan_cli/vars/get.py b/pkgs/clan-cli/clan_cli/vars/get.py index 335143772..66cdbad02 100644 --- a/pkgs/clan-cli/clan_cli/vars/get.py +++ b/pkgs/clan-cli/clan_cli/vars/get.py @@ -3,9 +3,9 @@ import logging import sys from clan_cli.api import API -from clan_cli.clan_uri import FlakeId from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.errors import ClanError +from clan_cli.flake import Flake from .generate import Var from .list import get_vars @@ -42,7 +42,7 @@ def get_var(base_dir: str, machine_name: str, var_id: str) -> Var: raise ClanError(msg) -def get_command(machine_name: str, var_id: str, flake: FlakeId) -> None: +def get_command(machine_name: str, var_id: str, flake: Flake) -> None: var = get_var(str(flake.path), machine_name, var_id) if not var.exists: msg = f"Var {var.id} has not been generated yet" diff --git a/pkgs/clan-cli/clan_cli/vars/keygen.py b/pkgs/clan-cli/clan_cli/vars/keygen.py index 995f9f9a5..6bf8290c5 100644 --- a/pkgs/clan-cli/clan_cli/vars/keygen.py +++ b/pkgs/clan-cli/clan_cli/vars/keygen.py @@ -2,8 +2,8 @@ import argparse import logging import os -from clan_cli.clan_uri import FlakeId from clan_cli.errors import ClanError +from clan_cli.flake import Flake from clan_cli.secrets.key import generate_key from clan_cli.secrets.sops import KeyType, maybe_get_admin_public_key from clan_cli.secrets.users import add_user @@ -11,7 +11,7 @@ from clan_cli.secrets.users import add_user log = logging.getLogger(__name__) -def keygen(user: str | None, flake: FlakeId, force: bool) -> None: +def keygen(user: str | None, flake: Flake, force: bool) -> None: if user is None: user = os.getenv("USER", None) if not user: diff --git a/pkgs/clan-cli/clan_cli/vars/list.py b/pkgs/clan-cli/clan_cli/vars/list.py index 6db537afe..669a3cdab 100644 --- a/pkgs/clan-cli/clan_cli/vars/list.py +++ b/pkgs/clan-cli/clan_cli/vars/list.py @@ -3,9 +3,9 @@ import importlib import logging from clan_cli.api import API -from clan_cli.clan_uri import FlakeId from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.errors import ClanError +from clan_cli.flake import Flake from clan_cli.machines.machines import Machine from clan_cli.vars._types import StoreBase @@ -27,7 +27,7 @@ def secret_store(machine: Machine) -> StoreBase: @API.register def get_vars(base_dir: str, machine_name: str) -> list[Var]: - machine = Machine(name=machine_name, flake=FlakeId(base_dir)) + machine = Machine(name=machine_name, flake=Flake(base_dir)) pub_store = public_store(machine) sec_store = secret_store(machine) all_vars = [] @@ -61,7 +61,7 @@ def _get_previous_value( @API.register def get_generators(base_dir: str, machine_name: str) -> list[Generator]: - machine = Machine(name=machine_name, flake=FlakeId(base_dir)) + machine = Machine(name=machine_name, flake=Flake(base_dir)) generators: list[Generator] = machine.vars_generators for generator in generators: for prompt in generator.prompts: @@ -76,7 +76,7 @@ def get_generators(base_dir: str, machine_name: str) -> list[Generator]: def set_prompts( base_dir: str, machine_name: str, updates: list[GeneratorUpdate] ) -> None: - machine = Machine(name=machine_name, flake=FlakeId(base_dir)) + machine = Machine(name=machine_name, flake=Flake(base_dir)) for update in updates: for generator in machine.vars_generators: if generator.name == update.generator: diff --git a/pkgs/clan-cli/clan_cli/vars/public_modules/in_repo.py b/pkgs/clan-cli/clan_cli/vars/public_modules/in_repo.py index a4a5a2697..51219635a 100644 --- a/pkgs/clan-cli/clan_cli/vars/public_modules/in_repo.py +++ b/pkgs/clan-cli/clan_cli/vars/public_modules/in_repo.py @@ -26,7 +26,7 @@ class FactStore(StoreBase): var: Var, value: bytes, ) -> Path | None: - if not self.machine.flake.is_local(): + if not self.machine.flake.is_local: msg = f"in_flake fact storage is only supported for local flakes: {self.machine.flake}" raise ClanError(msg) folder = self.directory(generator, var.name) diff --git a/pkgs/clan-cli/clan_cli/vars/public_modules/vm.py b/pkgs/clan-cli/clan_cli/vars/public_modules/vm.py index cb5619e9c..315a4e9b6 100644 --- a/pkgs/clan-cli/clan_cli/vars/public_modules/vm.py +++ b/pkgs/clan-cli/clan_cli/vars/public_modules/vm.py @@ -18,7 +18,7 @@ class FactStore(StoreBase): def __init__(self, machine: Machine) -> None: self.machine = machine self.works_remotely = False - self.dir = vm_state_dir(machine.flake, machine.name) / "facts" + self.dir = vm_state_dir(machine.flake.identifier, machine.name) / "facts" machine.debug(f"FactStore initialized with dir {self.dir}") @property diff --git a/pkgs/clan-cli/clan_cli/vars/secret_modules/vm.py b/pkgs/clan-cli/clan_cli/vars/secret_modules/vm.py index e623f0f96..2fefcce5f 100644 --- a/pkgs/clan-cli/clan_cli/vars/secret_modules/vm.py +++ b/pkgs/clan-cli/clan_cli/vars/secret_modules/vm.py @@ -14,7 +14,7 @@ class SecretStore(StoreBase): def __init__(self, machine: Machine) -> None: self.machine = machine - self.dir = vm_state_dir(machine.flake, machine.name) / "secrets" + self.dir = vm_state_dir(machine.flake.identifier, machine.name) / "secrets" self.dir.mkdir(parents=True, exist_ok=True) @property diff --git a/pkgs/clan-cli/clan_cli/vars/set.py b/pkgs/clan-cli/clan_cli/vars/set.py index dc5ace98e..ccef00e92 100644 --- a/pkgs/clan-cli/clan_cli/vars/set.py +++ b/pkgs/clan-cli/clan_cli/vars/set.py @@ -2,8 +2,8 @@ import argparse import logging import sys -from clan_cli.clan_uri import FlakeId from clan_cli.completions import add_dynamic_completer, complete_machines +from clan_cli.flake import Flake from clan_cli.git import commit_files from clan_cli.machines.machines import Machine from clan_cli.vars.get import get_var @@ -15,9 +15,7 @@ from .prompt import ask log = logging.getLogger(__name__) -def set_var( - machine: str | Machine, var: str | Var, value: bytes, flake: FlakeId -) -> None: +def set_var(machine: str | Machine, var: str | Var, value: bytes, flake: Flake) -> None: if isinstance(machine, str): _machine = Machine(name=machine, flake=flake) else: @@ -35,7 +33,7 @@ def set_var( ) -def set_via_stdin(machine: str, var_id: str, flake: FlakeId) -> None: +def set_via_stdin(machine: str, var_id: str, flake: Flake) -> None: var = get_var(str(flake.path), machine, var_id) if sys.stdin.isatty(): new_value = ask( diff --git a/pkgs/clan-cli/clan_cli/vms/inspect.py b/pkgs/clan-cli/clan_cli/vms/inspect.py index 79a6d29a5..b86d97c3d 100644 --- a/pkgs/clan-cli/clan_cli/vms/inspect.py +++ b/pkgs/clan-cli/clan_cli/vms/inspect.py @@ -5,8 +5,8 @@ from dataclasses import dataclass from pathlib import Path from typing import Any -from clan_cli.clan_uri import FlakeId from clan_cli.completions import add_dynamic_completer, complete_machines +from clan_cli.flake import Flake from clan_cli.machines.machines import Machine @@ -26,7 +26,7 @@ class WaypipeConfig: @dataclass class VmConfig: machine_name: str - flake_url: FlakeId + flake_url: Flake cores: int memory_size: int @@ -43,7 +43,7 @@ class VmConfig: def from_json(cls: type["VmConfig"], data: dict[str, Any]) -> "VmConfig": return cls( machine_name=data["machine_name"], - flake_url=FlakeId.from_json(data["flake_url"]), + flake_url=Flake.from_json(data["flake_url"]), cores=data["cores"], memory_size=data["memory_size"], graphics=data["graphics"], @@ -64,13 +64,13 @@ def inspect_vm(machine: Machine) -> VmConfig: @dataclass class InspectOptions: machine: str - flake: FlakeId + flake: Flake def inspect_command(args: argparse.Namespace) -> None: inspect_options = InspectOptions( machine=args.machine, - flake=args.flake or FlakeId(str(Path.cwd())), + flake=args.flake or Flake(str(Path.cwd())), ) machine = Machine(inspect_options.machine, inspect_options.flake) diff --git a/pkgs/clan-cli/clan_cli/vms/run.py b/pkgs/clan-cli/clan_cli/vms/run.py index 768688acd..595d03ce2 100644 --- a/pkgs/clan-cli/clan_cli/vms/run.py +++ b/pkgs/clan-cli/clan_cli/vms/run.py @@ -236,7 +236,7 @@ def spawn_vm( # TODO: We should get this from the vm argument nixos_config = build_vm(machine, cachedir, nix_options) - state_dir = vm_state_dir(vm.flake_url, machine.name) + state_dir = vm_state_dir(vm.flake_url.identifier, machine.name) state_dir.mkdir(parents=True, exist_ok=True) # specify socket files for qmp and qga diff --git a/pkgs/clan-cli/tests/test_api_dataclass_compat.py b/pkgs/clan-cli/tests/test_api_dataclass_compat.py index 474c7e9b4..8f63b9722 100644 --- a/pkgs/clan-cli/tests/test_api_dataclass_compat.py +++ b/pkgs/clan-cli/tests/test_api_dataclass_compat.py @@ -121,7 +121,6 @@ def test_all_dataclasses() -> None: "api/__init__.py", "cmd.py", # We don't want the UI to have access to the cmd module anyway "async_run.py", # We don't want the UI to have access to the async_run module anyway - "flake.py", # Not compatible yet with the UI, unclear semantics, maybe it's just an internal thing ] cli_path = Path("clan_cli").resolve() diff --git a/pkgs/clan-cli/tests/test_clan_nix_attrset.py b/pkgs/clan-cli/tests/test_clan_nix_attrset.py index 31e4756fd..ab9bd3170 100644 --- a/pkgs/clan-cli/tests/test_clan_nix_attrset.py +++ b/pkgs/clan-cli/tests/test_clan_nix_attrset.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any import pytest -from clan_cli.clan_uri import FlakeId +from clan_cli.flake import Flake from clan_cli.locked_open import locked_open from clan_cli.templates import get_clan_nix_attrset from fixtures_flakes import FlakeForTest @@ -26,7 +26,7 @@ def nix_attr_tester( test_number: int, ) -> None: write_clan_attr(injected, test_flake) - nix_attrset = get_clan_nix_attrset(FlakeId(str(test_flake.path))) + nix_attrset = get_clan_nix_attrset(Flake(str(test_flake.path))) assert json.dumps(nix_attrset, indent=2) == json.dumps(expected, indent=2) diff --git a/pkgs/clan-cli/tests/test_clan_uri.py b/pkgs/clan-cli/tests/test_clan_uri.py index dc321c25e..b221092ae 100644 --- a/pkgs/clan-cli/tests/test_clan_uri.py +++ b/pkgs/clan-cli/tests/test_clan_uri.py @@ -1,4 +1,5 @@ from pathlib import Path +from tempfile import TemporaryDirectory from clan_cli.clan_uri import ClanURI @@ -15,26 +16,28 @@ def test_get_url() -> None: assert uri.get_url() == "/home/user/Downloads" uri = ClanURI.from_str("clan://file:///home/user/Downloads") - assert uri.get_url() == "/home/user/Downloads" + assert uri.get_url() == "file:///home/user/Downloads" def test_firefox_strip_uri() -> None: - uri = ClanURI.from_str("clan://https//git.clan.lol/clan/democlan") - assert uri.get_url() == "https://git.clan.lol/clan/democlan" uri = ClanURI.from_str("clan://git+https//git.clan.lol/clan/democlan.git") assert uri.get_url() == "git+https://git.clan.lol/clan/democlan.git" def test_local_uri() -> None: - # Create a ClanURI object from a local URI - uri = ClanURI.from_str("clan://file:///home/user/Downloads") - assert uri.flake.path == Path("/home/user/Downloads") + with TemporaryDirectory(prefix="clan_test") as tempdir: + flake_nix = Path(tempdir) / "flake.nix" + flake_nix.write_text("outputs = _: {}") + + # Create a ClanURI object from a local URI + uri = ClanURI.from_str(f"clan://file://{tempdir}") + assert uri.flake.path == Path(tempdir) def test_is_remote() -> None: # Create a ClanURI object from a remote URI uri = ClanURI.from_str("clan://https://example.com") - assert uri.flake.url == "https://example.com" + assert uri.flake.identifier == "https://example.com" def test_direct_local_path() -> None: @@ -54,35 +57,35 @@ def test_remote_with_clanparams() -> None: uri = ClanURI.from_str("clan://https://example.com") assert uri.machine_name == "defaultVM" - assert uri.flake.url == "https://example.com" + assert uri.flake.identifier == "https://example.com" def test_from_str_remote() -> None: uri = ClanURI.from_str(url="https://example.com", machine_name="myVM") assert uri.get_url() == "https://example.com" assert uri.machine_name == "myVM" - assert uri.flake.url == "https://example.com" + assert uri.flake.identifier == "https://example.com" def test_from_str_local() -> None: - uri = ClanURI.from_str(url="~/Projects/democlan", machine_name="myVM") - assert uri.get_url().endswith("/Projects/democlan") - assert uri.machine_name == "myVM" - assert uri.flake.is_local() - assert str(uri.flake).endswith("/Projects/democlan") # type: ignore + with TemporaryDirectory(prefix="clan_test") as tempdir: + flake_nix = Path(tempdir) / "flake.nix" + flake_nix.write_text("outputs = _: {}") + + uri = ClanURI.from_str(url=tempdir, machine_name="myVM") + assert uri.get_url().endswith(tempdir) + assert uri.machine_name == "myVM" + assert uri.flake.is_local + assert str(uri.flake).endswith(tempdir) # type: ignore def test_from_str_local_no_machine() -> None: - uri = ClanURI.from_str("~/Projects/democlan") - assert uri.get_url().endswith("/Projects/democlan") - assert uri.machine_name == "defaultVM" - assert uri.flake.is_local() - assert str(uri.flake).endswith("/Projects/democlan") # type: ignore + with TemporaryDirectory(prefix="clan_test") as tempdir: + flake_nix = Path(tempdir) / "flake.nix" + flake_nix.write_text("outputs = _: {}") - -def test_from_str_local_no_machine2() -> None: - uri = ClanURI.from_str("~/Projects/democlan#syncthing-peer1") - assert uri.get_url().endswith("/Projects/democlan") - assert uri.machine_name == "syncthing-peer1" - assert uri.flake.is_local() - assert str(uri.flake).endswith("/Projects/democlan") # type: ignore + uri = ClanURI.from_str(tempdir) + assert uri.get_url().endswith(tempdir) + assert uri.machine_name == "defaultVM" + assert uri.flake.is_local + assert str(uri.flake).endswith(tempdir) # type: ignore diff --git a/pkgs/clan-cli/tests/test_deserializers.py b/pkgs/clan-cli/tests/test_deserializers.py index bd6e577ff..2a23159dd 100644 --- a/pkgs/clan-cli/tests/test_deserializers.py +++ b/pkgs/clan-cli/tests/test_deserializers.py @@ -82,7 +82,7 @@ def test_nested_nullable() -> None: data = { "machine": { "name": "flash-installer", - "flake": {"loc": "git+https://git.clan.lol/clan/clan-core"}, + "flake": {"identifier": "git+https://git.clan.lol/clan/clan-core"}, }, "mode": "format", "disks": {"main": "/dev/sda"}, @@ -96,7 +96,7 @@ def test_nested_nullable() -> None: expected = FlashOptions( machine=machines.Machine( name="flash-installer", - flake=machines.FlakeId("git+https://git.clan.lol/clan/clan-core"), + flake=machines.Flake("git+https://git.clan.lol/clan/clan-core"), ), mode="format", disks={"main": "/dev/sda"}, diff --git a/pkgs/clan-cli/tests/test_dirs.py b/pkgs/clan-cli/tests/test_dirs.py index bc1e48e64..ac2626834 100644 --- a/pkgs/clan-cli/tests/test_dirs.py +++ b/pkgs/clan-cli/tests/test_dirs.py @@ -16,7 +16,6 @@ # (subdir / ".clan-flake").touch() # assert _get_clan_flake_toplevel() == subdir -from clan_cli.clan_uri import FlakeId from clan_cli.dirs import clan_key_safe, vm_state_dir @@ -25,12 +24,12 @@ def test_clan_key_safe() -> None: def test_vm_state_dir_identity() -> None: - dir1 = vm_state_dir(FlakeId("https://some.clan"), "vm1") - dir2 = vm_state_dir(FlakeId("https://some.clan"), "vm1") + dir1 = vm_state_dir("https://some.clan", "vm1") + dir2 = vm_state_dir("https://some.clan", "vm1") assert str(dir1) == str(dir2) def test_vm_state_dir_no_collision() -> None: - dir1 = vm_state_dir(FlakeId("/foo/bar"), "vm1") - dir2 = vm_state_dir(FlakeId("https://some.clan"), "vm1") + dir1 = vm_state_dir("/foo/bar", "vm1") + dir2 = vm_state_dir("https://some.clan", "vm1") assert str(dir1) != str(dir2) diff --git a/pkgs/clan-cli/tests/test_modules.py b/pkgs/clan-cli/tests/test_modules.py index 842e30aa0..fac5ade4b 100644 --- a/pkgs/clan-cli/tests/test_modules.py +++ b/pkgs/clan-cli/tests/test_modules.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING import pytest from clan_cli.api.modules import list_modules -from clan_cli.clan_uri import FlakeId +from clan_cli.flake import Flake from clan_cli.inventory import ( Inventory, Machine, @@ -56,7 +56,7 @@ def test_add_module_to_inventory( ] ) opts = CreateOptions( - clan_dir=FlakeId(str(base_path)), + clan_dir=Flake(str(base_path)), machine=Machine(name="machine1", tags=[], deploy=MachineDeploy()), ) @@ -93,7 +93,7 @@ def test_add_module_to_inventory( cli.run(cmd) machine = MachineMachine( - name="machine1", flake=FlakeId(str(test_flake_with_core.path)) + name="machine1", flake=Flake(str(test_flake_with_core.path)) ) generator = None diff --git a/pkgs/clan-cli/tests/test_secrets_generate.py b/pkgs/clan-cli/tests/test_secrets_generate.py index 0a5f79c5a..bfd3f95a5 100644 --- a/pkgs/clan-cli/tests/test_secrets_generate.py +++ b/pkgs/clan-cli/tests/test_secrets_generate.py @@ -2,8 +2,8 @@ import ipaddress from typing import TYPE_CHECKING import pytest -from clan_cli.clan_uri import FlakeId from clan_cli.facts.secret_modules.sops import SecretStore +from clan_cli.flake import Flake from clan_cli.machines.facts import machine_get_fact from clan_cli.machines.machines import Machine from clan_cli.secrets.folders import sops_secrets_folder @@ -48,7 +48,7 @@ def test_generate_secret( cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "vm1"] cli.run(cmd) store1 = SecretStore( - Machine(name="vm1", flake=FlakeId(str(test_flake_with_core.path))) + Machine(name="vm1", flake=Flake(str(test_flake_with_core.path))) ) assert store1.exists("", "age.key") @@ -78,7 +78,7 @@ def test_generate_secret( ).exists() store2 = SecretStore( - Machine(name="vm2", flake=FlakeId(str(test_flake_with_core.path))) + Machine(name="vm2", flake=Flake(str(test_flake_with_core.path))) ) # Should not exist clan facts generate diff --git a/pkgs/clan-cli/tests/test_secrets_password_store.py b/pkgs/clan-cli/tests/test_secrets_password_store.py index 8229147a2..2103e9000 100644 --- a/pkgs/clan-cli/tests/test_secrets_password_store.py +++ b/pkgs/clan-cli/tests/test_secrets_password_store.py @@ -2,8 +2,8 @@ import subprocess from pathlib import Path import pytest -from clan_cli.clan_uri import FlakeId from clan_cli.facts.secret_modules.password_store import SecretStore +from clan_cli.flake import Flake from clan_cli.machines.facts import machine_get_fact from clan_cli.machines.machines import Machine from clan_cli.nix import nix_shell @@ -68,7 +68,7 @@ def test_upload_secret( ) cli.run(["facts", "generate", "vm1", "--flake", str(flake.path)]) - store = SecretStore(Machine(name="vm1", flake=FlakeId(str(flake.path)))) + store = SecretStore(Machine(name="vm1", flake=Flake(str(flake.path)))) network_id = machine_get_fact(flake.path, "vm1", "zerotier-network-id") assert len(network_id) == 16 diff --git a/pkgs/clan-cli/tests/test_vars.py b/pkgs/clan-cli/tests/test_vars.py index 9598fd68c..26e443114 100644 --- a/pkgs/clan-cli/tests/test_vars.py +++ b/pkgs/clan-cli/tests/test_vars.py @@ -6,8 +6,8 @@ from typing import TYPE_CHECKING import pytest from age_keys import SopsSetup -from clan_cli.clan_uri import FlakeId from clan_cli.errors import ClanError +from clan_cli.flake import Flake from clan_cli.machines.machines import Machine from clan_cli.nix import nix_eval, run from clan_cli.vars.check import check_vars @@ -138,7 +138,7 @@ def test_generate_public_and_secret_vars( monkeypatch.chdir(flake.path) sops_setup.init() - machine = Machine(name="my_machine", flake=FlakeId(str(flake.path))) + machine = Machine(name="my_machine", flake=Flake(str(flake.path))) assert not check_vars(machine) vars_text = stringify_all_vars(machine) assert "my_generator/my_value: " in vars_text @@ -184,11 +184,11 @@ def test_generate_public_and_secret_vars( ) vars_text = stringify_all_vars(machine) in_repo_store = in_repo.FactStore( - Machine(name="my_machine", flake=FlakeId(str(flake.path))) + Machine(name="my_machine", flake=Flake(str(flake.path))) ) assert not in_repo_store.exists(Generator("my_generator"), "my_secret") sops_store = sops.SecretStore( - Machine(name="my_machine", flake=FlakeId(str(flake.path))) + Machine(name="my_machine", flake=Flake(str(flake.path))) ) assert sops_store.exists(Generator("my_generator"), "my_secret") assert sops_store.get(Generator("my_generator"), "my_secret").decode() == "secret" @@ -246,11 +246,11 @@ def test_generate_secret_var_sops_with_default_group( cli.run(["secrets", "groups", "add-user", "my_group", sops_setup.user]) cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"]) in_repo_store = in_repo.FactStore( - Machine(name="my_machine", flake=FlakeId(str(flake.path))) + Machine(name="my_machine", flake=Flake(str(flake.path))) ) assert not in_repo_store.exists(Generator("my_generator"), "my_secret") sops_store = sops.SecretStore( - Machine(name="my_machine", flake=FlakeId(str(flake.path))) + Machine(name="my_machine", flake=Flake(str(flake.path))) ) assert sops_store.exists(Generator("my_generator"), "my_secret") assert sops_store.get(Generator("my_generator"), "my_secret").decode() == "hello\n" @@ -317,8 +317,8 @@ def test_generated_shared_secret_sops( flake.refresh() monkeypatch.chdir(flake.path) sops_setup.init() - machine1 = Machine(name="machine1", flake=FlakeId(str(flake.path))) - machine2 = Machine(name="machine2", flake=FlakeId(str(flake.path))) + machine1 = Machine(name="machine1", flake=Flake(str(flake.path))) + machine2 = Machine(name="machine2", flake=Flake(str(flake.path))) cli.run(["vars", "generate", "--flake", str(flake.path), "machine1"]) assert check_vars(machine1) cli.run(["vars", "generate", "--flake", str(flake.path), "machine2"]) @@ -368,12 +368,12 @@ def test_generate_secret_var_password_store( shutil.copytree(test_root / "data" / "password-store", password_store_dir) monkeypatch.setenv("PASSWORD_STORE_DIR", str(flake.path / "pass")) - machine = Machine(name="my_machine", flake=FlakeId(str(flake.path))) + machine = Machine(name="my_machine", flake=Flake(str(flake.path))) assert not check_vars(machine) cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"]) assert check_vars(machine) store = password_store.SecretStore( - Machine(name="my_machine", flake=FlakeId(str(flake.path))) + Machine(name="my_machine", flake=Flake(str(flake.path))) ) assert store.exists(Generator("my_generator", share=False, files=[]), "my_secret") assert not store.exists( @@ -432,10 +432,10 @@ def test_generate_secret_for_multiple_machines( cli.run(["vars", "generate", "--flake", str(flake.path)]) # check if public vars have been created correctly in_repo_store1 = in_repo.FactStore( - Machine(name="machine1", flake=FlakeId(str(flake.path))) + Machine(name="machine1", flake=Flake(str(flake.path))) ) in_repo_store2 = in_repo.FactStore( - Machine(name="machine2", flake=FlakeId(str(flake.path))) + Machine(name="machine2", flake=Flake(str(flake.path))) ) assert in_repo_store1.exists(Generator("my_generator"), "my_value") assert in_repo_store2.exists(Generator("my_generator"), "my_value") @@ -449,10 +449,10 @@ def test_generate_secret_for_multiple_machines( ) # check if secret vars have been created correctly sops_store1 = sops.SecretStore( - Machine(name="machine1", flake=FlakeId(str(flake.path))) + Machine(name="machine1", flake=Flake(str(flake.path))) ) sops_store2 = sops.SecretStore( - Machine(name="machine2", flake=FlakeId(str(flake.path))) + Machine(name="machine2", flake=Flake(str(flake.path))) ) assert sops_store1.exists(Generator("my_generator"), "my_secret") assert sops_store2.exists(Generator("my_generator"), "my_secret") @@ -498,7 +498,7 @@ def test_prompt( ) cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"]) in_repo_store = in_repo.FactStore( - Machine(name="my_machine", flake=FlakeId(str(flake.path))) + Machine(name="my_machine", flake=Flake(str(flake.path))) ) assert in_repo_store.exists(Generator("my_generator"), "line_value") assert ( @@ -512,7 +512,7 @@ def test_prompt( == "my\nmultiline\ninput\n" ) sops_store = sops.SecretStore( - Machine(name="my_machine", flake=FlakeId(str(flake.path))) + Machine(name="my_machine", flake=Flake(str(flake.path))) ) assert sops_store.exists( Generator(name="my_generator", share=False, files=[]), "prompt_persist" @@ -553,8 +553,8 @@ def test_multi_machine_shared_vars( flake.refresh() monkeypatch.chdir(flake.path) sops_setup.init() - machine1 = Machine(name="machine1", flake=FlakeId(str(flake.path))) - machine2 = Machine(name="machine2", flake=FlakeId(str(flake.path))) + machine1 = Machine(name="machine1", flake=Flake(str(flake.path))) + machine2 = Machine(name="machine2", flake=Flake(str(flake.path))) sops_store_1 = sops.SecretStore(machine1) sops_store_2 = sops.SecretStore(machine2) in_repo_store_1 = in_repo.FactStore(machine1) @@ -616,7 +616,7 @@ def test_api_set_prompts( ) ], ) - machine = Machine(name="my_machine", flake=FlakeId(str(flake.path))) + machine = Machine(name="my_machine", flake=Flake(str(flake.path))) store = in_repo.FactStore(machine) assert store.exists(Generator("my_generator"), "prompt1") assert store.get(Generator("my_generator"), "prompt1").decode() == "input1" @@ -663,7 +663,7 @@ def test_stdout_of_generate( # with capture_output as output: with caplog.at_level(logging.INFO): generate_vars_for_machine( - Machine(name="my_machine", flake=FlakeId(str(flake.path))), + Machine(name="my_machine", flake=Flake(str(flake.path))), "my_generator", regenerate=False, ) @@ -673,10 +673,10 @@ def test_stdout_of_generate( assert "new: hello" in caplog.text caplog.clear() - set_var("my_machine", "my_generator/my_value", b"world", FlakeId(str(flake.path))) + set_var("my_machine", "my_generator/my_value", b"world", Flake(str(flake.path))) with caplog.at_level(logging.INFO): generate_vars_for_machine( - Machine(name="my_machine", flake=FlakeId(str(flake.path))), + Machine(name="my_machine", flake=Flake(str(flake.path))), "my_generator", regenerate=True, ) @@ -687,7 +687,7 @@ def test_stdout_of_generate( # check the output when nothing gets regenerated with caplog.at_level(logging.INFO): generate_vars_for_machine( - Machine(name="my_machine", flake=FlakeId(str(flake.path))), + Machine(name="my_machine", flake=Flake(str(flake.path))), "my_generator", regenerate=True, ) @@ -696,7 +696,7 @@ def test_stdout_of_generate( caplog.clear() with caplog.at_level(logging.INFO): generate_vars_for_machine( - Machine(name="my_machine", flake=FlakeId(str(flake.path))), + Machine(name="my_machine", flake=Flake(str(flake.path))), "my_secret_generator", regenerate=False, ) @@ -707,11 +707,11 @@ def test_stdout_of_generate( "my_machine", "my_secret_generator/my_secret", b"world", - FlakeId(str(flake.path)), + Flake(str(flake.path)), ) with caplog.at_level(logging.INFO): generate_vars_for_machine( - Machine(name="my_machine", flake=FlakeId(str(flake.path))), + Machine(name="my_machine", flake=Flake(str(flake.path))), "my_secret_generator", regenerate=True, ) @@ -760,10 +760,10 @@ def test_migration( assert "Migrated var my_generator/my_value" in caplog.text assert "Migrated secret var my_generator/my_secret" in caplog.text in_repo_store = in_repo.FactStore( - Machine(name="my_machine", flake=FlakeId(str(flake.path))) + Machine(name="my_machine", flake=Flake(str(flake.path))) ) sops_store = sops.SecretStore( - Machine(name="my_machine", flake=FlakeId(str(flake.path))) + Machine(name="my_machine", flake=Flake(str(flake.path))) ) assert in_repo_store.exists(Generator("my_generator"), "my_value") assert in_repo_store.get(Generator("my_generator"), "my_value").decode() == "hello" @@ -800,7 +800,7 @@ def test_fails_when_files_are_left_from_other_backend( sops_setup.init() for generator in ["my_secret_generator", "my_value_generator"]: generate_vars_for_machine( - Machine(name="my_machine", flake=FlakeId(str(flake.path))), + Machine(name="my_machine", flake=Flake(str(flake.path))), generator, regenerate=False, ) @@ -817,36 +817,33 @@ def test_fails_when_files_are_left_from_other_backend( if generator == "my_secret_generator": with pytest.raises(ClanError): generate_vars_for_machine( - Machine(name="my_machine", flake=FlakeId(str(flake.path))), + Machine(name="my_machine", flake=Flake(str(flake.path))), generator, regenerate=False, ) else: generate_vars_for_machine( - Machine(name="my_machine", flake=FlakeId(str(flake.path))), + Machine(name="my_machine", flake=Flake(str(flake.path))), generator, regenerate=False, ) @pytest.mark.with_core -def test_keygen( - monkeypatch: pytest.MonkeyPatch, - temporary_home: Path, -) -> None: - monkeypatch.chdir(temporary_home) - cli.run(["vars", "keygen", "--flake", str(temporary_home), "--user", "user"]) +def test_keygen(monkeypatch: pytest.MonkeyPatch, flake: ClanFlake) -> None: + monkeypatch.chdir(flake.path) + cli.run(["vars", "keygen", "--flake", str(flake.path), "--user", "user"]) # check public key exists - assert (temporary_home / "sops" / "users" / "user").is_dir() + assert (flake.path / "sops" / "users" / "user").is_dir() # check private key exists - assert (temporary_home / ".config" / "sops" / "age" / "keys.txt").is_file() + assert (flake.temporary_home / ".config" / "sops" / "age" / "keys.txt").is_file() # it should still work, even if the keys already exist import shutil - shutil.rmtree(temporary_home / "sops" / "users" / "user") - cli.run(["vars", "keygen", "--flake", str(temporary_home), "--user", "user"]) + shutil.rmtree(flake.path / "sops" / "users" / "user") + cli.run(["vars", "keygen", "--flake", str(flake.path), "--user", "user"]) # check public key exists - assert (temporary_home / "sops" / "users" / "user").is_dir() + assert (flake.path / "sops" / "users" / "user").is_dir() @pytest.mark.with_core @@ -862,7 +859,7 @@ def test_invalidation( flake.refresh() monkeypatch.chdir(flake.path) cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"]) - machine = Machine(name="my_machine", flake=FlakeId(str(flake.path))) + machine = Machine(name="my_machine", flake=Flake(str(flake.path))) value1 = get_var( str(machine.flake.path), machine.name, "my_generator/my_value" ).printable_value diff --git a/pkgs/clan-cli/tests/test_vars_deployment.py b/pkgs/clan-cli/tests/test_vars_deployment.py index e8a0f35d4..f6923c59a 100644 --- a/pkgs/clan-cli/tests/test_vars_deployment.py +++ b/pkgs/clan-cli/tests/test_vars_deployment.py @@ -5,7 +5,7 @@ from contextlib import ExitStack import pytest from age_keys import SopsSetup from clan_cli import cmd -from clan_cli.clan_uri import FlakeId +from clan_cli.flake import Flake from clan_cli.machines.machines import Machine from clan_cli.nix import nix_eval, run from clan_cli.vms.run import inspect_vm, spawn_vm @@ -97,8 +97,8 @@ def test_vm_deployment( # run nix flake lock cmd.run(["nix", "flake", "lock"], cmd.RunOpts(cwd=flake.path)) - vm1_config = inspect_vm(machine=Machine("m1_machine", FlakeId(str(flake.path)))) - vm2_config = inspect_vm(machine=Machine("m2_machine", FlakeId(str(flake.path)))) + vm1_config = inspect_vm(machine=Machine("m1_machine", Flake(str(flake.path)))) + vm2_config = inspect_vm(machine=Machine("m2_machine", Flake(str(flake.path)))) with ExitStack() as stack: vm1 = stack.enter_context(spawn_vm(vm1_config, stdin=subprocess.DEVNULL)) vm2 = stack.enter_context(spawn_vm(vm2_config, stdin=subprocess.DEVNULL)) diff --git a/pkgs/clan-cli/tests/test_vms_cli.py b/pkgs/clan-cli/tests/test_vms_cli.py index 6b30db315..65fd4bb50 100644 --- a/pkgs/clan-cli/tests/test_vms_cli.py +++ b/pkgs/clan-cli/tests/test_vms_cli.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import TYPE_CHECKING import pytest -from clan_cli.clan_uri import FlakeId +from clan_cli.flake import Flake from clan_cli.machines.machines import Machine from clan_cli.vms.run import inspect_vm, spawn_vm from fixtures_flakes import ClanFlake, FlakeForTest @@ -80,7 +80,7 @@ def test_vm_persistence( flake.refresh() - vm_config = inspect_vm(machine=Machine("my_machine", FlakeId(str(flake.path)))) + vm_config = inspect_vm(machine=Machine("my_machine", Flake(str(flake.path)))) with spawn_vm(vm_config) as vm, vm.qga_connect() as qga: # create state via qmp command instead of systemd service diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py index c7e03e616..f83945ea7 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py @@ -147,20 +147,14 @@ class VMObject(GObject.Object): uri = ClanURI.from_str( url=str(self.data.flake.flake_url), machine_name=self.data.flake.flake_attr ) - if uri.flake.is_local(): - self.machine = Machine( - name=self.data.flake.flake_attr, - flake=uri.flake, - ) - if uri.flake.is_remote(): - self.machine = Machine( - name=self.data.flake.flake_attr, - flake=uri.flake, - ) + self.machine = Machine( + name=self.data.flake.flake_attr, + flake=uri.flake, + ) assert self.machine is not None state_dir = vm_state_dir( - flake_url=self.machine.flake, vm_name=self.machine.name + flake_url=self.machine.flake.identifier, vm_name=self.machine.name ) self.qmp_wrap = QMPWrapper(state_dir) assert self.machine is not None diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py index e3a9c6ac8..83418c084 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py @@ -4,7 +4,8 @@ from pathlib import Path from typing import Any, ClassVar import gi -from clan_cli.clan_uri import ClanURI, FlakeId +from clan_cli.clan_uri import ClanURI +from clan_cli.flake import Flake from clan_cli.history.add import HistoryEntry from clan_cli.machines.machines import Machine @@ -34,7 +35,7 @@ class Emitter(GObject.GObject): class ClanStore: _instance: "None | ClanStore" = None - _clan_store: GKVStore[FlakeId, VMStore] + _clan_store: GKVStore[Flake, VMStore] _emitter: Emitter @@ -94,7 +95,7 @@ class ClanStore: self.clan_store.register_on_change(on_clanstore_change) @property - def clan_store(self) -> GKVStore[FlakeId, VMStore]: + def clan_store(self) -> GKVStore[Flake, VMStore]: return self._clan_store def create_vm_task(self, vm: HistoryEntry) -> bool: diff --git a/pkgs/webview-ui/app/src/components/MachineListItem.tsx b/pkgs/webview-ui/app/src/components/MachineListItem.tsx index 00c209731..8e6101767 100644 --- a/pkgs/webview-ui/app/src/components/MachineListItem.tsx +++ b/pkgs/webview-ui/app/src/components/MachineListItem.tsx @@ -51,7 +51,7 @@ export const MachineListItem = (props: MachineListItemProps) => { machine: { name: name, flake: { - loc: active_clan, + identifier: active_clan, }, }, no_reboot: true, diff --git a/pkgs/webview-ui/app/src/routes/flash/view.tsx b/pkgs/webview-ui/app/src/routes/flash/view.tsx index 9572841c9..b97c639df 100644 --- a/pkgs/webview-ui/app/src/routes/flash/view.tsx +++ b/pkgs/webview-ui/app/src/routes/flash/view.tsx @@ -163,7 +163,7 @@ export const Flash = () => { machine: { name: values.machine.devicePath, flake: { - loc: values.machine.flake, + identifier: values.machine.flake, }, }, mode: "format", diff --git a/pkgs/webview-ui/app/src/routes/machines/create.tsx b/pkgs/webview-ui/app/src/routes/machines/create.tsx index 462833e91..2f489d7fe 100644 --- a/pkgs/webview-ui/app/src/routes/machines/create.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/create.tsx @@ -20,7 +20,7 @@ export function CreateMachine() { initialValues: { opts: { clan_dir: { - loc: activeURI() || "", + identifier: activeURI() || "", }, machine: { tags: ["all"], @@ -49,7 +49,7 @@ export function CreateMachine() { opts: { ...values.opts, clan_dir: { - loc: active_dir, + identifier: active_dir, }, }, }); diff --git a/pkgs/webview-ui/app/src/routes/machines/details.tsx b/pkgs/webview-ui/app/src/routes/machines/details.tsx index 06097b3b6..d647001ed 100644 --- a/pkgs/webview-ui/app/src/routes/machines/details.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/details.tsx @@ -140,7 +140,7 @@ const InstallMachine = (props: InstallMachineProps) => { machine: { name: props.name, flake: { - loc: curr_uri, + identifier: curr_uri, }, }, target_host: target, diff --git a/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx b/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx index 80110e35c..c413ff899 100644 --- a/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx @@ -83,7 +83,7 @@ export const HWStep = (props: StepProps) => { setIsGenerating(true); const r = await callApi("generate_machine_hardware_info", { opts: { - flake: { loc: curr_uri }, + flake: { identifier: curr_uri }, machine: props.machine_id, target_host: target, backend: "nixos-facter",