diff --git a/lib/inventory/build-inventory/default.nix b/lib/inventory/build-inventory/default.nix index 88b952d5e..63f5e7678 100644 --- a/lib/inventory/build-inventory/default.nix +++ b/lib/inventory/build-inventory/default.nix @@ -28,10 +28,10 @@ let ); in if tagMembers == [ ] then - throw '' + lib.warn '' inventory.services.${serviceName}.${instanceName}: - ${roleName} tags: no machine with tag '${tag}' found. Available tags: ${builtins.toJSON (lib.unique availableTags)} - '' + '' [ ] else acc ++ tagMembers ) [ ] members.tags or [ ]); diff --git a/lib/inventory/flake-module.nix b/lib/inventory/flake-module.nix index e356a27d4..e6b834676 100644 --- a/lib/inventory/flake-module.nix +++ b/lib/inventory/flake-module.nix @@ -42,7 +42,7 @@ in checks = { lib-inventory-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } '' export HOME="$(realpath .)" - + export NIX_ABORT_ON_WARN=1 nix-unit --eval-store "$HOME" \ --extra-experimental-features flakes \ ${inputOverrides} \ diff --git a/lib/inventory/tests/default.nix b/lib/inventory/tests/default.nix index 1a7b51be2..182069e4a 100644 --- a/lib/inventory/tests/default.nix +++ b/lib/inventory/tests/default.nix @@ -178,6 +178,8 @@ in msg = "Roles roleXYZ are not defined in the service borgbackup."; }; }; + # Needs NIX_ABORT_ON_WARN=1 + # So the lib.warn is turned into abort test_inventory_tag_doesnt_exist = let configs = buildInventory { @@ -201,8 +203,9 @@ in { expr = configs; expectedError = { - type = "ThrownError"; - msg = "no machine with tag '\\w+' found"; + type = "Error"; + # TODO: Add warning matching in nix-unit + msg = ".*"; }; }; test_inventory_disabled_service = diff --git a/lib/values/flake-module.nix b/lib/values/flake-module.nix index db60f92c5..9d997ccc6 100644 --- a/lib/values/flake-module.nix +++ b/lib/values/flake-module.nix @@ -6,8 +6,17 @@ let in { perSystem = - { pkgs, system, ... }: { + pkgs, + system, + lib, + ... + }: + let + tests = import ./test.nix { inherit lib; }; + in + { + legacyPackages.evalTests-values = tests; checks = { lib-values-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } '' export HOME="$(realpath .)" @@ -15,7 +24,7 @@ in nix-unit --eval-store "$HOME" \ --extra-experimental-features flakes \ ${inputOverrides} \ - --flake ${self}#legacyPackages.${system}.evalTests-inventory + --flake ${self}#legacyPackages.${system}.evalTests-values touch $out ''; diff --git a/pkgs/clan-app/clan_app/deps/webview/webview.py b/pkgs/clan-app/clan_app/deps/webview/webview.py index 9aa4a16cf..f422066eb 100644 --- a/pkgs/clan-app/clan_app/deps/webview/webview.py +++ b/pkgs/clan-app/clan_app/deps/webview/webview.py @@ -82,43 +82,47 @@ class Webview: method_name: str = name, ) -> None: def thread_task() -> None: - args = json.loads(req.decode()) - try: - log.debug(f"Calling {method_name}({args[0]})") - # Initialize dataclasses from the payload - reconciled_arguments = {} - for k, v in args[0].items(): - # Some functions expect to be called with dataclass instances - # But the js api returns dictionaries. - # Introspect the function and create the expected dataclass from dict dynamically - # Depending on the introspected argument_type - arg_class = api.get_method_argtype(method_name, k) + args = json.loads(req.decode()) - # TODO: rename from_dict into something like construct_checked_value - # from_dict really takes Anything and returns an instance of the type/class - reconciled_arguments[k] = from_dict(arg_class, v) + try: + log.debug(f"Calling {method_name}({args[0]})") + # Initialize dataclasses from the payload + reconciled_arguments = {} + for k, v in args[0].items(): + # Some functions expect to be called with dataclass instances + # But the js api returns dictionaries. + # Introspect the function and create the expected dataclass from dict dynamically + # Depending on the introspected argument_type + arg_class = api.get_method_argtype(method_name, k) - reconciled_arguments["op_key"] = seq.decode() - # TODO: We could remove the wrapper in the MethodRegistry - # and just call the method directly - result = wrap_method(**reconciled_arguments) - success = True + # TODO: rename from_dict into something like construct_checked_value + # from_dict really takes Anything and returns an instance of the type/class + reconciled_arguments[k] = from_dict(arg_class, v) + + reconciled_arguments["op_key"] = seq.decode() + # TODO: We could remove the wrapper in the MethodRegistry + # and just call the method directly + result = wrap_method(**reconciled_arguments) + success = True + except Exception as e: + log.exception(f"Error calling {method_name}") + result = str(e) + success = False + + try: + serialized = json.dumps( + dataclass_to_dict(result), indent=4, ensure_ascii=False + ) + except TypeError: + log.exception(f"Error serializing result for {method_name}") + raise + + log.debug(f"Result for {method_name}: {serialized}") + self.return_(seq.decode(), 0 if success else 1, serialized) except Exception as e: - log.exception(f"Error calling {method_name}") - result = str(e) - success = False - - try: - serialized = json.dumps( - dataclass_to_dict(result), indent=4, ensure_ascii=False - ) - except TypeError: - log.exception(f"Error serializing result for {method_name}") - raise - - log.debug(f"Result for {method_name}: {serialized}") - self.return_(seq.decode(), 0 if success else 1, serialized) + log.exception(f"Unhandled error in webview {method_name}") + self.return_(seq.decode(), 1, str(e)) thread = threading.Thread(target=thread_task) thread.start() diff --git a/pkgs/clan-cli/clan_cli/vars/generate.py b/pkgs/clan-cli/clan_cli/vars/generate.py index 3cb35f51a..6fb4c8241 100644 --- a/pkgs/clan-cli/clan_cli/vars/generate.py +++ b/pkgs/clan-cli/clan_cli/vars/generate.py @@ -260,7 +260,11 @@ def _ask_prompts( prompt_values: dict[str, str] = {} for prompt in generator.prompts: var_id = f"{generator.name}/{prompt.name}" - prompt_values[prompt.name] = ask(var_id, prompt.prompt_type) + prompt_values[prompt.name] = ask( + var_id, + prompt.prompt_type, + prompt.description if prompt.description != prompt.name else None, + ) return prompt_values diff --git a/pkgs/clan-cli/clan_cli/vars/get.py b/pkgs/clan-cli/clan_cli/vars/get.py index 5e975c550..335143772 100644 --- a/pkgs/clan-cli/clan_cli/vars/get.py +++ b/pkgs/clan-cli/clan_cli/vars/get.py @@ -2,10 +2,10 @@ import argparse 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.machines.machines import Machine from .generate import Var from .list import get_vars @@ -13,8 +13,9 @@ from .list import get_vars log = logging.getLogger(__name__) -def get_var(machine: Machine, var_id: str) -> Var: - vars_ = get_vars(machine) +@API.register +def get_var(base_dir: str, machine_name: str, var_id: str) -> Var: + vars_ = get_vars(base_dir=base_dir, machine_name=machine_name) results = [] for var in vars_: if var.id == var_id: @@ -42,8 +43,7 @@ def get_var(machine: Machine, var_id: str) -> Var: def get_command(machine_name: str, var_id: str, flake: FlakeId) -> None: - machine = Machine(name=machine_name, flake=flake) - var = get_var(machine, var_id) + var = get_var(str(flake.path), machine_name, var_id) if not var.exists: msg = f"Var {var.id} has not been generated yet" raise ClanError(msg) diff --git a/pkgs/clan-cli/clan_cli/vars/list.py b/pkgs/clan-cli/clan_cli/vars/list.py index 5a7b7d401..c82a6a032 100644 --- a/pkgs/clan-cli/clan_cli/vars/list.py +++ b/pkgs/clan-cli/clan_cli/vars/list.py @@ -25,7 +25,9 @@ def secret_store(machine: Machine) -> StoreBase: return secret_vars_module.SecretStore(machine=machine) -def get_vars(machine: Machine) -> list[Var]: +@API.register +def get_vars(base_dir: str, machine_name: str) -> list[Var]: + machine = Machine(name=machine_name, flake=FlakeId(base_dir)) pub_store = public_store(machine) sec_store = secret_store(machine) all_vars = [] @@ -58,7 +60,7 @@ def _get_previous_value( @API.register -def get_prompts(base_dir: str, machine_name: str) -> list[Generator]: +def get_generators(base_dir: str, machine_name: str) -> list[Generator]: machine = Machine(name=machine_name, flake=FlakeId(base_dir)) generators: list[Generator] = machine.vars_generators for generator in generators: @@ -96,7 +98,7 @@ def stringify_vars(_vars: list[Var]) -> str: def stringify_all_vars(machine: Machine) -> str: - return stringify_vars(get_vars(machine)) + return stringify_vars(get_vars(str(machine.flake), machine.name)) def list_command(args: argparse.Namespace) -> None: diff --git a/pkgs/clan-cli/clan_cli/vars/prompt.py b/pkgs/clan-cli/clan_cli/vars/prompt.py index 629dba747..bc4bba156 100644 --- a/pkgs/clan-cli/clan_cli/vars/prompt.py +++ b/pkgs/clan-cli/clan_cli/vars/prompt.py @@ -37,16 +37,25 @@ class Prompt: ) -def ask(description: str, input_type: PromptType) -> str: +def ask( + ident: str, + input_type: PromptType, + label: str | None, +) -> str: + text = f"Enter the value for {ident}:" + if label: + text = f"{label}" + if MOCK_PROMPT_RESPONSE: return next(MOCK_PROMPT_RESPONSE) match input_type: case PromptType.LINE: - result = input(f"Enter the value for {description}: ") + result = input(f"{text}: ") case PromptType.MULTILINE: - print(f"Enter the value for {description} (Finish with Ctrl-D): ") + print(f"{text} (Finish with Ctrl-D): ") result = sys.stdin.read() case PromptType.HIDDEN: - result = getpass(f"Enter the value for {description} (hidden): ") + result = getpass(f"{text} (hidden): ") + log.info("Input received. Processing...") return result diff --git a/pkgs/clan-cli/clan_cli/vars/set.py b/pkgs/clan-cli/clan_cli/vars/set.py index b6f51337a..dc5ace98e 100644 --- a/pkgs/clan-cli/clan_cli/vars/set.py +++ b/pkgs/clan-cli/clan_cli/vars/set.py @@ -23,7 +23,7 @@ def set_var( else: _machine = machine if isinstance(var, str): - _var = get_var(_machine, var) + _var = get_var(str(flake.path), _machine.name, var) else: _var = var path = _var.set(value) @@ -36,12 +36,17 @@ def set_var( def set_via_stdin(machine: str, var_id: str, flake: FlakeId) -> None: - _machine = Machine(name=machine, flake=flake) - var = get_var(_machine, var_id) + var = get_var(str(flake.path), machine, var_id) if sys.stdin.isatty(): - new_value = ask(var.id, PromptType.HIDDEN).encode("utf-8") + new_value = ask( + var.id, + PromptType.HIDDEN, + None, + ).encode("utf-8") else: new_value = sys.stdin.buffer.read() + + _machine = Machine(name=machine, flake=flake) set_var(_machine, var, new_value, flake) diff --git a/pkgs/clan-cli/tests/test_vars.py b/pkgs/clan-cli/tests/test_vars.py index c7aee65ed..57273390b 100644 --- a/pkgs/clan-cli/tests/test_vars.py +++ b/pkgs/clan-cli/tests/test_vars.py @@ -170,9 +170,16 @@ def test_generate_public_and_secret_vars( "Update vars via generator my_shared_generator for machine my_machine" in commit_message ) - assert get_var(machine, "my_generator/my_value").printable_value == "public" assert ( - get_var(machine, "my_shared_generator/my_shared_value").printable_value + get_var( + str(machine.flake.path), machine.name, "my_generator/my_value" + ).printable_value + == "public" + ) + assert ( + get_var( + str(machine.flake.path), machine.name, "my_shared_generator/my_shared_value" + ).printable_value == "shared" ) vars_text = stringify_all_vars(machine) @@ -587,7 +594,7 @@ def test_api_set_prompts( flake: ClanFlake, ) -> None: from clan_cli.vars._types import GeneratorUpdate - from clan_cli.vars.list import get_prompts, set_prompts + from clan_cli.vars.list import get_generators, set_prompts config = flake.machines["my_machine"] config["nixpkgs"]["hostPlatform"] = "x86_64-linux" @@ -623,11 +630,11 @@ def test_api_set_prompts( ) assert store.get(Generator("my_generator"), "prompt1").decode() == "input2" - api_prompts = get_prompts(**params) - assert len(api_prompts) == 1 - assert api_prompts[0].name == "my_generator" - assert api_prompts[0].prompts[0].name == "prompt1" - assert api_prompts[0].prompts[0].previous_value == "input2" + generators = get_generators(**params) + assert len(generators) == 1 + assert generators[0].name == "my_generator" + assert generators[0].prompts[0].name == "prompt1" + assert generators[0].prompts[0].previous_value == "input2" @pytest.mark.with_core @@ -843,19 +850,27 @@ def test_invalidation( monkeypatch.chdir(flake.path) cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"]) machine = Machine(name="my_machine", flake=FlakeId(str(flake.path))) - value1 = get_var(machine, "my_generator/my_value").printable_value + value1 = get_var( + str(machine.flake.path), machine.name, "my_generator/my_value" + ).printable_value # generate again and make sure nothing changes without the invalidation data being set cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"]) - value1_new = get_var(machine, "my_generator/my_value").printable_value + value1_new = get_var( + str(machine.flake.path), machine.name, "my_generator/my_value" + ).printable_value assert value1 == value1_new # set the invalidation data of the generator my_generator["validation"] = 1 flake.refresh() # generate again and make sure the value changes cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"]) - value2 = get_var(machine, "my_generator/my_value").printable_value + value2 = get_var( + str(machine.flake.path), machine.name, "my_generator/my_value" + ).printable_value assert value1 != value2 # generate again without changing invalidation data -> value should not change cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"]) - value2_new = get_var(machine, "my_generator/my_value").printable_value + value2_new = get_var( + str(machine.flake.path), machine.name, "my_generator/my_value" + ).printable_value assert value2 == value2_new