diff --git a/checks/backups/flake-module.nix b/checks/backups/flake-module.nix index d237cb138..a48a3bd21 100644 --- a/checks/backups/flake-module.nix +++ b/checks/backups/flake-module.nix @@ -10,7 +10,6 @@ { pkgs, lib, - config, ... }: let @@ -30,6 +29,7 @@ clan.core.networking.targetHost = "machine"; networking.hostName = "machine"; services.openssh.settings.UseDns = false; + nixpkgs.hostPlatform = "x86_64-linux"; programs.ssh.knownHosts = { machine.hostNames = [ "machine" ]; diff --git a/checks/installation/flake-module.nix b/checks/installation/flake-module.nix index 3ab30c8be..dd39b94ed 100644 --- a/checks/installation/flake-module.nix +++ b/checks/installation/flake-module.nix @@ -20,13 +20,13 @@ environment.etc."install-successful".text = "ok"; + nixpkgs.hostPlatform = "x86_64-linux"; boot.consoleLogLevel = lib.mkForce 100; boot.kernelParams = [ "boot.shell_on_fail" ]; }; }; perSystem = { - nodes, pkgs, lib, ... diff --git a/checks/nixos-documentation/flake-module.nix b/checks/nixos-documentation/flake-module.nix index 743d97c19..b8351ac67 100644 --- a/checks/nixos-documentation/flake-module.nix +++ b/checks/nixos-documentation/flake-module.nix @@ -18,6 +18,7 @@ in # Dummy file system fileSystems."/".device = "/dev/null"; boot.loader.grub.device = "/dev/null"; + nixpkgs.hostPlatform = "x86_64-linux"; imports = [ documentationModule ]; diff --git a/inventory.json b/inventory.json index 2d815115e..8a8fe11ed 100644 --- a/inventory.json +++ b/inventory.json @@ -12,8 +12,7 @@ }, "description": "A nice thing", "icon": "./path/to/icon.png", - "tags": ["1", "2", "3"], - "system": "x86_64-linux" + "tags": ["1", "2", "3"] } }, "services": { diff --git a/lib/build-clan/module.nix b/lib/build-clan/module.nix index 4c3aed454..bf8fbe7b0 100644 --- a/lib/build-clan/module.nix +++ b/lib/build-clan/module.nix @@ -26,36 +26,10 @@ let } ); - machineSettings = - machineName: - let - warn = lib.warn '' - The use of ./machines//settings.json is deprecated. - If your settings.json is empty, you can safely remove it. - !!! Consider using the inventory system. !!! - - File: ${directory + /machines/${machineName}/settings.json} - - If there are still features missing in the inventory system, please open an issue on the clan-core repository. - ''; - in - # CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily - # This is useful for doing a dry-run before writing changes into the settings.json - # Using CLAN_MACHINE_SETTINGS_FILE requires passing --impure to nix eval - if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != "" then - warn (builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE"))) - else - lib.optionalAttrs (builtins.pathExists "${directory}/machines/${machineName}/settings.json") ( - warn (builtins.fromJSON (builtins.readFile (directory + /machines/${machineName}/settings.json))) - ); - - machineImports = - machineSettings: map (module: clan-core.clanModules.${module}) (machineSettings.clanImports or [ ]); - # TODO: remove default system once we have a hardware-config mechanism nixosConfiguration = { - system ? "x86_64-linux", + system ? null, name, pkgs ? null, extraConfig ? { }, @@ -63,40 +37,16 @@ let nixpkgs.lib.nixosSystem { modules = let - settings = machineSettings name; - facterJson = "${directory}/machines/${name}/facter.json"; hwConfig = "${directory}/machines/${name}/hardware-configuration.nix"; - - facterModules = lib.optionals (builtins.pathExists facterJson) [ - clan-core.inputs.nixos-facter-modules.nixosModules.facter - { config.facter.reportPath = facterJson; } - ]; in - (machineImports settings) - ++ facterModules - ++ [ + [ { # Autoinclude configuration.nix and hardware-configuration.nix imports = builtins.filter builtins.pathExists [ "${directory}/machines/${name}/configuration.nix" hwConfig ]; - config.warnings = - lib.optionals - (builtins.all builtins.pathExists [ - hwConfig - facterJson - ]) - [ - '' - Duplicate hardware facts: '${hwConfig}' and '${facterJson}' exist. - Using both is not recommended. - - It is recommended to use the hardware facts from '${facterJson}', please remove '${hwConfig}'. - '' - ]; } - settings clan-core.nixosModules.clanCore extraConfig (machines.${name} or { }) @@ -115,7 +65,7 @@ let # Machine specific settings clan.core.machineName = name; networking.hostName = lib.mkDefault name; - nixpkgs.hostPlatform = lib.mkDefault system; + nixpkgs.hostPlatform = lib.mkIf (system != null) (lib.mkDefault system); # speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs) nix.registry.nixpkgs.to = { @@ -132,38 +82,7 @@ let } // specialArgs; }; - # TODO: Will be deprecated - # We must migrate the tests, that create a settings.json to add a machine. - ################################################## - testMachines = - lib.mapAttrs - (name: _: { - inherit name; - system = (machineSettings name).nixpkgs.hostSystem or null; - }) - ( - lib.filterAttrs ( - machineName: _: - if builtins.pathExists "${directory}/machines/${machineName}/settings.json" then - lib.warn '' - The use of ./machines//settings.json is deprecated. - If your settings.json is empty, you can safely remove it. - !!! Consider using the inventory system. !!! - - File: ${directory + /machines/${machineName}/settings.json} - - If there are still features missing in the inventory system, please open an issue on the clan-core repository. - '' true - else - false - ) machinesDirs - ); - machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") ( - builtins.readDir (directory + /machines) - ); - ################################################## - - allMachines = inventory.machines or { } // machines // testMachines; + allMachines = inventory.machines or { } // machines; supportedSystems = [ "x86_64-linux" diff --git a/lib/build-clan/tests.nix b/lib/build-clan/tests.nix index 5bfe6aab5..f7617a783 100644 --- a/lib/build-clan/tests.nix +++ b/lib/build-clan/tests.nix @@ -146,6 +146,7 @@ in { foo, ... }: { networking.hostName = foo; + nixpkgs.hostPlatform = "x86_64-linux"; }; }; in diff --git a/lib/inventory/build-inventory/default.nix b/lib/inventory/build-inventory/default.nix index 5b5b53a4d..cf7b8394e 100644 --- a/lib/inventory/build-inventory/default.nix +++ b/lib/inventory/build-inventory/default.nix @@ -154,9 +154,6 @@ let ) [ ] inventory.services # Append each machine config ++ [ - (lib.optionalAttrs (machineConfig.system or null != null) { - config.nixpkgs.hostPlatform = machineConfig.system; - }) (lib.optionalAttrs (machineConfig.deploy.targetHost or null != null) { config.clan.core.networking.targetHost = machineConfig.deploy.targetHost; }) diff --git a/lib/inventory/build-inventory/interface.nix b/lib/inventory/build-inventory/interface.nix index 56c05fb3b..a0b72e3e0 100644 --- a/lib/inventory/build-inventory/interface.nix +++ b/lib/inventory/build-inventory/interface.nix @@ -142,10 +142,6 @@ in apply = lib.unique; type = types.listOf types.str; }; - system = lib.mkOption { - default = null; - type = types.nullOr types.str; - }; deploy.targetHost = lib.mkOption { description = "Configuration for the deployment of the machine"; default = null; diff --git a/lib/inventory/tests/default.nix b/lib/inventory/tests/default.nix index 2fea17c42..b275246a1 100644 --- a/lib/inventory/tests/default.nix +++ b/lib/inventory/tests/default.nix @@ -92,9 +92,9 @@ in not_used_machine = builtins.length configs.not_used_machine; }; expected = { - client_1_machine = 6; - client_2_machine = 6; - not_used_machine = 3; + client_1_machine = 5; + client_2_machine = 5; + not_used_machine = 2; }; }; diff --git a/machines/test-inventory-machine/facter.json b/machines/test-inventory-machine/facter.json new file mode 100644 index 000000000..7b6fc7a61 --- /dev/null +++ b/machines/test-inventory-machine/facter.json @@ -0,0 +1,4 @@ +{ + "version": 1, + "system": "x86_64-linux" +} diff --git a/nixosModules/clanCore/default.nix b/nixosModules/clanCore/default.nix index e7ebe87a9..5c8546122 100644 --- a/nixosModules/clanCore/default.nix +++ b/nixosModules/clanCore/default.nix @@ -9,6 +9,7 @@ ./meta/interface.nix ./metadata.nix ./networking.nix + ./nixos-facter.nix ./nix-settings.nix ./options.nix ./outputs.nix diff --git a/nixosModules/clanCore/nixos-facter.nix b/nixosModules/clanCore/nixos-facter.nix new file mode 100644 index 000000000..dbbb852c6 --- /dev/null +++ b/nixosModules/clanCore/nixos-facter.nix @@ -0,0 +1,24 @@ +{ lib, config, ... }: +let + directory = config.clan.core.clanDir; + inherit (config.clan.core) machineName; + facterJson = "${directory}/machines/${machineName}/facter.json"; + hwConfig = "${directory}/machines/${machineName}/hardware-configuration.nix"; +in +{ + facter.reportPath = lib.mkIf (builtins.pathExists facterJson) facterJson; + warnings = + lib.optionals + (builtins.all builtins.pathExists [ + hwConfig + facterJson + ]) + [ + '' + Duplicate hardware facts: '${hwConfig}' and '${facterJson}' exist. + Using both is not recommended. + + It is recommended to use the hardware facts from '${facterJson}', please remove '${hwConfig}'. + '' + ]; +} diff --git a/nixosModules/flake-module.nix b/nixosModules/flake-module.nix index d192af01d..cbf930274 100644 --- a/nixosModules/flake-module.nix +++ b/nixosModules/flake-module.nix @@ -10,6 +10,7 @@ ]; clanCore.imports = [ inputs.sops-nix.nixosModules.sops + inputs.nixos-facter-modules.nixosModules.facter inputs.disko.nixosModules.default ./clanCore ( diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py deleted file mode 100644 index 9715a84c6..000000000 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ /dev/null @@ -1,306 +0,0 @@ -# !/usr/bin/env python3 -import argparse -import json -import logging -import re -from pathlib import Path -from typing import Any, get_origin - -from clan_cli.cmd import run -from clan_cli.dirs import machine_settings_file -from clan_cli.errors import ClanError -from clan_cli.git import commit_file -from clan_cli.nix import nix_eval - -script_dir = Path(__file__).parent - -log = logging.getLogger(__name__) - - -# nixos option type description to python type -def map_type(nix_type: str) -> Any: - if nix_type == "boolean": - return bool - if nix_type in [ - "integer", - "signed integer", - "16 bit unsigned integer; between 0 and 65535 (both inclusive)", - ]: - return int - if nix_type.startswith("string"): - return str - if nix_type.startswith("null or "): - subtype = nix_type.removeprefix("null or ") - return map_type(subtype) | None - if nix_type.startswith("attribute set of"): - subtype = nix_type.removeprefix("attribute set of ") - return dict[str, map_type(subtype)] # type: ignore - if nix_type.startswith("list of"): - subtype = nix_type.removeprefix("list of ") - return list[map_type(subtype)] # type: ignore - msg = f"Unknown type {nix_type}" - raise ClanError(msg) - - -# merge two dicts recursively -def merge(a: dict, b: dict, path: list[str] | None = None) -> dict: - a = a.copy() - if path is None: - path = [] - for key in b: - if key in a: - if isinstance(a[key], dict) and isinstance(b[key], dict): - merge(a[key], b[key], [*path, str(key)]) - elif isinstance(a[key], list) and isinstance(b[key], list): - a[key].extend(b[key]) - elif a[key] != b[key]: - a[key] = b[key] - else: - a[key] = b[key] - return a - - -# A container inheriting from list, but overriding __contains__ to return True -# for all values. -# This is used to allow any value for the "choices" field of argparse -class AllContainer(list): - def __contains__(self, item: Any) -> bool: - return True - - -# value is always a list, as the arg parser cannot know the type upfront -# and therefore always allows multiple arguments. -def cast(value: Any, input_type: Any, opt_description: str) -> Any: - try: - # handle bools - if isinstance(input_type, bool): - if value[0] in ["true", "True", "yes", "y", "1"]: - return True - if value[0] in ["false", "False", "no", "n", "0"]: - return False - msg = f"Invalid value {value} for boolean" - raise ClanError(msg) - # handle lists - if get_origin(input_type) is list: - subtype = input_type.__args__[0] - return [cast([x], subtype, opt_description) for x in value] - # handle dicts - if get_origin(input_type) is dict: - if not isinstance(value, dict): - msg = f"Cannot set {opt_description} directly. Specify a suboption like {opt_description}." - raise ClanError(msg) - subtype = input_type.__args__[1] - return {k: cast(v, subtype, opt_description) for k, v in value.items()} - if str(input_type) == "str | None": - if value[0] in ["null", "None"]: - return None - return value[0] - if len(value) > 1: - msg = f"Too many values for {opt_description}" - raise ClanError(msg) - return input_type(value[0]) - except ValueError as e: - msg = f"Invalid type for option {opt_description} (expected {input_type.__name__})" - raise ClanError(msg) from e - - -def options_for_machine( - flake_dir: Path, machine_name: str, show_trace: bool = False -) -> dict: - clan_dir = flake_dir - flags = [] - if show_trace: - flags.append("--show-trace") - flags.append( - f"{clan_dir}#nixosConfigurations.{machine_name}.config.clan.core.optionsNix" - ) - cmd = nix_eval(flags=flags) - proc = run( - cmd, - error_msg=f"Failed to read options for machine {machine_name}", - ) - - return json.loads(proc.stdout) - - -def read_machine_option_value( - flake_dir: Path, machine_name: str, option: str, show_trace: bool = False -) -> str: - clan_dir = flake_dir - # use nix eval to read from .#nixosConfigurations.default.config.{option} - # this will give us the evaluated config with the options attribute - cmd = nix_eval( - flags=[ - "--show-trace", - f"{clan_dir}#nixosConfigurations.{machine_name}.config.{option}", - ], - ) - proc = run(cmd, error_msg=f"Failed to read option {option}") - - value = json.loads(proc.stdout) - # print the value so that the output can be copied and fed as an input. - # for example a list should be displayed as space separated values surrounded by quotes. - if isinstance(value, list): - out = " ".join([json.dumps(x) for x in value]) - elif isinstance(value, dict): - out = json.dumps(value, indent=2) - else: - out = json.dumps(value, indent=2) - return out - - -def get_option(args: argparse.Namespace) -> None: - print( - read_machine_option_value( - args.flake, args.machine, args.option, args.show_trace - ) - ) - - -# Currently writing is disabled -def get_or_set_option(args: argparse.Namespace) -> None: - if args.value == []: - print( - read_machine_option_value( - args.flake, args.machine, args.option, args.show_trace - ) - ) - else: - # load options - if args.options_file is None: - options = options_for_machine( - args.flake, machine_name=args.machine, show_trace=args.show_trace - ) - else: - with args.options_file.open() as f: - options = json.load(f) - # compute settings json file location - if args.settings_file is None: - settings_file = machine_settings_file(args.flake.path, args.machine) - else: - settings_file = args.settings_file - # set the option with the given value - set_option( - flake_dir=args.flake.path, - option=args.option, - value=args.value, - options=options, - settings_file=settings_file, - option_description=args.option, - show_trace=args.show_trace, - ) - if not args.quiet: - new_value = read_machine_option_value(args.flake, args.machine, args.option) - print(f"New Value for {args.option}:") - print(new_value) - - -def find_option( - option: str, value: Any, options: dict, option_description: str | None = None -) -> tuple[str, Any]: - """ - The option path specified by the user doesn't have to match exactly to an - entry in the options.json file. Examples - - Example 1: - $ clan config services.openssh.settings.SomeSetting 42 - This is a freeform option that does not appear in the options.json - The actual option is `services.openssh.settings` - And the value must be wrapped: {"SomeSettings": 42} - - Example 2: - $ clan config users.users.my-user.name my-name - The actual option is `users.users..name` - """ - - # option description is used for error messages - if option_description is None: - option_description = option - - option_path = option.split(".") - - # fuzzy search the option paths, so when - # specified option path: "foo.bar.baz.bum" - # available option path: "foo..baz." - # we can still find the option - first = option_path[0] - regex = rf"({first}|)" - for elem in option_path[1:]: - regex += rf"\.({elem}|)" - for opt in options: - if re.match(regex, opt): - return opt, value - - # if the regex search did not find the option, start stripping the last - # element of the option path and find matching parent option - # (see examples above for why this is needed) - if len(option_path) == 1: - msg = f"Option {option_description} not found" - raise ClanError(msg) - option_path_parent = option_path[:-1] - attr_prefix = option_path[-1] - return find_option( - option=".".join(option_path_parent), - value={attr_prefix: value}, - options=options, - option_description=option_description, - ) - - -def set_option( - flake_dir: Path, - option: str, - value: Any, - options: dict, - settings_file: Path, - option_description: str = "", - show_trace: bool = False, -) -> None: - option_path_orig = option.split(".") - - # returns for example: - # option: "users.users..name" - # value: "my-name" - option, value = find_option( - option=option, - value=value, - options=options, - option_description=option_description, - ) - option_path = option.split(".") - - option_path_store = option_path_orig[: len(option_path)] - - target_type = map_type(options[option]["type"]) - casted = cast(value, target_type, option) - - # construct a nested dict from the option path and set the value - result: dict[str, Any] = {} - current = result - for part in option_path_store[:-1]: - current[part] = {} - current = current[part] - current[option_path_store[-1]] = value - - current[option_path_store[-1]] = casted - - # check if there is an existing config file - if settings_file.exists(): - with settings_file.open() as f: - current_config = json.load(f) - else: - current_config = {} - - # merge and save the new config file - new_config = merge(current_config, result) - settings_file.parent.mkdir(parents=True, exist_ok=True) - with settings_file.open("w") as f: - json.dump(new_config, f, indent=2) - print(file=f) # add newline at the end of the file to make git happy - - if settings_file.resolve().is_relative_to(flake_dir): - commit_file( - settings_file, - repo_dir=flake_dir, - commit_message=f"Set option {option_description}", - ) diff --git a/pkgs/clan-cli/clan_cli/config/machine.py b/pkgs/clan-cli/clan_cli/config/machine.py deleted file mode 100644 index 5e6166463..000000000 --- a/pkgs/clan-cli/clan_cli/config/machine.py +++ /dev/null @@ -1,109 +0,0 @@ -import json -import os -import re -from pathlib import Path -from tempfile import NamedTemporaryFile - -from clan_cli.cmd import Log, run -from clan_cli.dirs import machine_settings_file, nixpkgs_source, specific_machine_dir -from clan_cli.errors import ClanError, ClanHttpError -from clan_cli.git import commit_file -from clan_cli.nix import nix_eval - - -def verify_machine_config( - flake_dir: Path, - machine_name: str, - config: dict | None = None, -) -> str | None: - """ - Verify that the machine evaluates successfully - Returns None, in case of success, or a String containing the error_message - """ - if config is None: - config = config_for_machine(flake_dir, machine_name) - flake = flake_dir - with NamedTemporaryFile(mode="w", dir=flake) as clan_machine_settings_file: - json.dump(config, clan_machine_settings_file, indent=2) - clan_machine_settings_file.seek(0) - env = os.environ.copy() - env["CLAN_MACHINE_SETTINGS_FILE"] = clan_machine_settings_file.name - cmd = nix_eval( - flags=[ - "--show-trace", - "--impure", # needed to access CLAN_MACHINE_SETTINGS_FILE - "--expr", - f""" - let - # hardcoding system for now, not sure where to get it from - system = "x86_64-linux"; - flake = builtins.getFlake (toString {flake}); - clan-core = flake.inputs.clan-core; - nixpkgsSrc = flake.inputs.nixpkgs or {nixpkgs_source()}; - lib = import (nixpkgsSrc + /lib); - pkgs = import nixpkgsSrc {{ inherit system; }}; - config = lib.importJSON (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE"); - fakeMachine = pkgs.nixos {{ - imports = - [ - clan-core.nixosModules.clanCore - # potentially the config might affect submodule options, - # therefore we need to import it - config - {{clan.core.clanDir = {flake};}} - ] - # add all clan modules specified via clanImports - ++ (map (name: clan-core.clanModules.${{name}}) config.clanImports or []); - }}; - in - fakeMachine.config.system.build.vm.outPath - """, - ], - ) - - proc = run( - cmd, - cwd=flake, - env=env, - log=Log.BOTH, - ) - if proc.returncode != 0: - return proc.stderr - return None - - -def config_for_machine(flake_dir: Path, machine_name: str) -> dict: - # read the config from a json file located at {flake}/machines/{machine_name}/settings.json - if not specific_machine_dir(flake_dir, machine_name).exists(): - raise ClanHttpError( - msg=f"Machine {machine_name} not found. Create the machine first`", - status_code=404, - ) - settings_path = machine_settings_file(flake_dir, machine_name) - if not settings_path.exists(): - return {} - with settings_path.open() as f: - return json.load(f) - - -def set_config_for_machine(flake_dir: Path, machine_name: str, config: dict) -> None: - hostname_regex = r"^(?!-)[A-Za-z0-9-]{1,63}(? dict[str, Any]: - absolute_path = Path(file).absolute() - # define a nix expression that loads the given module file using lib.evalModules - nix_expr = f""" - let - lib = import ; - slib = import {script_dir}/jsonschema {{inherit lib;}}; - in - slib.parseModule {absolute_path} - """ - # run the nix expression and parse the output as json - cmd = nix_eval(["--expr", nix_expr]) - proc = run(cmd) - return json.loads(proc.stdout) - - -def subtype_from_schema(schema: dict[str, Any]) -> type: - if schema["type"] == "object": - if "additionalProperties" in schema: - sub_type = subtype_from_schema(schema["additionalProperties"]) - return dict[str, sub_type] # type: ignore - if "properties" in schema: - msg = "Nested dicts are not supported" - raise ClanError(msg) - msg = "Unknown object type" - raise ClanError(msg) - if schema["type"] == "array": - if "items" not in schema: - msg = "Untyped arrays are not supported" - raise ClanError(msg) - sub_type = subtype_from_schema(schema["items"]) - return list[sub_type] # type: ignore - return type_map[schema["type"]] - - -def type_from_schema_path( - schema: dict[str, Any], - path: list[str], - full_path: list[str] | None = None, -) -> type: - if full_path is None: - full_path = path - if len(path) == 0: - return subtype_from_schema(schema) - if schema["type"] == "object": - if "properties" in schema: - subtype = type_from_schema_path(schema["properties"][path[0]], path[1:]) - return subtype - if "additionalProperties" in schema: - subtype = type_from_schema_path(schema["additionalProperties"], path[1:]) - return subtype - msg = f"Unknown type for path {path}" - raise ClanError(msg) - msg = f"Unknown type for path {path}" - raise ClanError(msg) - - -def options_types_from_schema(schema: dict[str, Any]) -> dict[str, type]: - result: dict[str, type] = {} - for name, value in schema.get("properties", {}).items(): - assert isinstance(value, dict) - type_ = value["type"] - if type_ == "object": - # handle additionalProperties - if "additionalProperties" in value: - sub_type = value["additionalProperties"].get("type") - if sub_type not in type_map: - msg = f"Unsupported object type {sub_type} (field {name})" - raise ClanError(msg) - result[f"{name}."] = type_map[sub_type] - continue - # handle properties - sub_result = options_types_from_schema(value) - for sub_name, sub_type in sub_result.items(): - result[f"{name}.{sub_name}"] = sub_type - continue - if type_ == "array": - if "items" not in value: - msg = f"Untyped arrays are not supported (field: {name})" - raise ClanError(msg) - sub_type = value["items"].get("type") - if sub_type not in type_map: - msg = f"Unsupported list type {sub_type} (field {name})" - raise ClanError(msg) - sub_type_: type = type_map[sub_type] - result[name] = list[sub_type_] # type: ignore - continue - result[name] = type_map[type_] - return result diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index 0786376d9..a39f64044 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -115,10 +115,6 @@ def specific_machine_dir(flake_dir: Path, machine: str) -> Path: return machines_dir(flake_dir) / machine -def machine_settings_file(flake_dir: Path, machine: str) -> Path: - return specific_machine_dir(flake_dir, machine) / "settings.json" - - def module_root() -> Path: return Path(__file__).parent diff --git a/pkgs/clan-cli/clan_cli/inventory/classes.py b/pkgs/clan-cli/clan_cli/inventory/classes.py index c66d5aa70..d723d93a5 100644 --- a/pkgs/clan-cli/clan_cli/inventory/classes.py +++ b/pkgs/clan-cli/clan_cli/inventory/classes.py @@ -19,7 +19,6 @@ class Machine: name: str description: None | str = field(default = None) icon: None | str = field(default = None) - system: None | str = field(default = None) tags: list[str] = field(default_factory = list) diff --git a/pkgs/clan-cli/flake-module.nix b/pkgs/clan-cli/flake-module.nix index 59e6bb093..28601e146 100644 --- a/pkgs/clan-cli/flake-module.nix +++ b/pkgs/clan-cli/flake-module.nix @@ -33,13 +33,11 @@ )); }; flakeLockFile = builtins.toFile "clan-core-flake.lock" (builtins.toJSON flakeLockVendoredDeps); - clanCoreWithVendoredDeps = - lib.trace flakeLockFile pkgs.runCommand "clan-core-with-vendored-deps" { } - '' - cp -r ${self} $out - chmod +w -R $out - cp ${flakeLockFile} $out/flake.lock - ''; + clanCoreWithVendoredDeps = pkgs.runCommand "clan-core-with-vendored-deps" { } '' + cp -r ${self} $out + chmod +w -R $out + cp ${flakeLockFile} $out/flake.lock + ''; in { devShells.clan-cli = pkgs.callPackage ./shell.nix { diff --git a/pkgs/clan-cli/tests/conftest.py b/pkgs/clan-cli/tests/conftest.py index eac2d6655..1532a8618 100644 --- a/pkgs/clan-cli/tests/conftest.py +++ b/pkgs/clan-cli/tests/conftest.py @@ -15,6 +15,7 @@ pytest_plugins = [ "host_group", "fixtures_flakes", "stdout", + "nix_config", ] diff --git a/pkgs/clan-cli/tests/data/gnupg-home/openpgp-revocs.d/9A9B2741C8062D3D3DF1302D8B049E262A5CA255.rev b/pkgs/clan-cli/tests/data/gnupg-home/openpgp-revocs.d/9A9B2741C8062D3D3DF1302D8B049E262A5CA255.rev new file mode 100644 index 000000000..c084ed320 --- /dev/null +++ b/pkgs/clan-cli/tests/data/gnupg-home/openpgp-revocs.d/9A9B2741C8062D3D3DF1302D8B049E262A5CA255.rev @@ -0,0 +1,29 @@ +This is a revocation certificate for the OpenPGP key: + +pub rsa1024 2024-09-29 [SCEAR] + 9A9B2741C8062D3D3DF1302D8B049E262A5CA255 +uid Root Superuser + +A revocation certificate is a kind of "kill switch" to publicly +declare that a key shall not anymore be used. It is not possible +to retract such a revocation certificate once it has been published. + +Use it to revoke this key in case of a compromise or loss of +the secret key. However, if the secret key is still accessible, +it is better to generate a new revocation certificate and give +a reason for the revocation. For details see the description of +of the gpg command "--generate-revocation" in the GnuPG manual. + +To avoid an accidental use of this file, a colon has been inserted +before the 5 dashes below. Remove this colon with a text editor +before importing and publishing this revocation certificate. + +:-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: This is a revocation certificate + +iLYEIAEIACAWIQSamydByAYtPT3xMC2LBJ4mKlyiVQUCZvl/cAIdAAAKCRCLBJ4m +KlyiVUWOA/9rDw6tSSw7Gh3vlaLZXSQvkftO3x9cJwePn6JPmM2nWLDcaOj+/Cd0 +guyakYt7Fsxa6fqcv5sYV50bPRqAnfOWbR7jRl4DF6pSYNCHPlkWuLghdYsBOBo2 +1MG/J+155aclsB8JQez1eGMe8KcpcJBcrYuZTAMekMGPrfyr9SwDUg== +=V2Jo +-----END PGP PUBLIC KEY BLOCK----- diff --git a/pkgs/clan-cli/tests/data/gnupg-home/private-keys-v1.d/893F0D3827CC473BAEFE4A6B3E910245CD2CCFF9.key b/pkgs/clan-cli/tests/data/gnupg-home/private-keys-v1.d/893F0D3827CC473BAEFE4A6B3E910245CD2CCFF9.key new file mode 100644 index 000000000..a0873e873 --- /dev/null +++ b/pkgs/clan-cli/tests/data/gnupg-home/private-keys-v1.d/893F0D3827CC473BAEFE4A6B3E910245CD2CCFF9.key @@ -0,0 +1,15 @@ +Created: 20240929T162520 +Key: (private-key (rsa (n #00B1BF3E8A8CEA6A68439F67CDCAF5616B50D99A9F88 + 6D9E879D3FE990854E9ADFC35D7D26DBC5BC1800B3FF7B814F4623C1DFC34CAB4D326C + 3E269C6059D567B5144659B3C895B52B428BA7B74CC2FA130D06C689C45B8FF8DA1D7C + 7A578C99C0F221189D6BE045AE2EC8D2389423994BA0D650A2EDD2B7664642BFBF9691 + 495F#)(e #010001#)(d #57605C65AE94F39EF293136BB23842DE06DE19A90FDF573D + 723B3F5D5872C626767AE831687B0116498E326AABABE51E61C9564FC3ABCCBC322737 + DB137E191EB3B012B9C142290050EBD8ADD40BC68CCB577521E3A76DFD668BC6E584C7 + 0DD3B6CE545CC392B1D893EFB959BE3BD0EB7DF73A1F7AFBD9693353BA4FD3C05AED#) + (p #00C169E9E1DF8F39E7B2140FD52723FC5D10CCFC62D8A0876D39641AB00441345C + FC239EF8551B5F39CE850EF2DD79B98D70D57AD933648C86B7DD536B1B3AD6CB#)(q + #00EB43872BDDA397AC02A32E7CB0061ACB26A30497031D24FA793DE9EE4EFBACB1A4 + 6BF1444DE47CB63A6E254F2E4928BB0BB1F5C51C5247EEA8FF2D84BE25F13D#)(u + #00CEBE9717B5F7B59393065F884ACCA692F64545F492E50DF9070ACA9FBDA8A1EC03 + 906FDB9C112A97FADBB273E69548C6B17E6BE3BB664B9D02FB2100EF19AF7D#))) diff --git a/pkgs/clan-cli/tests/data/gnupg-home/pubring.kbx b/pkgs/clan-cli/tests/data/gnupg-home/pubring.kbx new file mode 100644 index 000000000..b07f082d4 Binary files /dev/null and b/pkgs/clan-cli/tests/data/gnupg-home/pubring.kbx differ diff --git a/pkgs/clan-cli/tests/data/gnupg-home/random_seed b/pkgs/clan-cli/tests/data/gnupg-home/random_seed new file mode 100644 index 000000000..2f4c89c23 Binary files /dev/null and b/pkgs/clan-cli/tests/data/gnupg-home/random_seed differ diff --git a/pkgs/clan-cli/tests/data/gnupg-home/trustdb.gpg b/pkgs/clan-cli/tests/data/gnupg-home/trustdb.gpg new file mode 100644 index 000000000..973814c8e Binary files /dev/null and b/pkgs/clan-cli/tests/data/gnupg-home/trustdb.gpg differ diff --git a/pkgs/clan-cli/tests/data/gnupg.conf b/pkgs/clan-cli/tests/data/gnupg.conf new file mode 100644 index 000000000..eaa6ec764 --- /dev/null +++ b/pkgs/clan-cli/tests/data/gnupg.conf @@ -0,0 +1,6 @@ +Key-Type: 1 +Key-Length: 1024 +Name-Real: Root Superuser +Name-Email: test@local +Expire-Date: 0 +%no-protection diff --git a/pkgs/clan-cli/tests/data/password-store/.gpg-id b/pkgs/clan-cli/tests/data/password-store/.gpg-id new file mode 100644 index 000000000..f4fc704e0 --- /dev/null +++ b/pkgs/clan-cli/tests/data/password-store/.gpg-id @@ -0,0 +1 @@ +test@local diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index 28bd27260..02547afe0 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -55,14 +55,33 @@ def set_machine_settings( machine_name: str, machine_settings: dict, ) -> None: - settings_path = flake / "machines" / machine_name / "settings.json" - settings_path.parent.mkdir(parents=True, exist_ok=True) - settings_path.write_text(json.dumps(machine_settings, indent=2)) + config_path = flake / "machines" / machine_name / "configuration.json" + config_path.write_text(json.dumps(machine_settings, indent=2)) + + +def init_git(monkeypatch: pytest.MonkeyPatch, flake: Path) -> None: + monkeypatch.setenv("GIT_AUTHOR_NAME", "clan-tool") + monkeypatch.setenv("GIT_AUTHOR_EMAIL", "clan@example.com") + monkeypatch.setenv("GIT_COMMITTER_NAME", "clan-tool") + monkeypatch.setenv("GIT_COMMITTER_EMAIL", "clan@example.com") + + # TODO: Find out why test_vms_api.py fails in nix build + # but works in pytest when this bottom line is commented out + sp.run( + ["git", "config", "--global", "init.defaultBranch", "main"], + cwd=flake, + check=True, + ) + + sp.run(["git", "init"], cwd=flake, check=True) + sp.run(["git", "add", "."], cwd=flake, check=True) + sp.run(["git", "commit", "-a", "-m", "Initial commit"], cwd=flake, check=True) def generate_flake( temporary_home: Path, flake_template: Path, + monkeypatch: pytest.MonkeyPatch, substitutions: dict[str, str] | None = None, # define the machines directly including their config machine_configs: dict[str, dict] | None = None, @@ -90,13 +109,12 @@ def generate_flake( inventory = {} if machine_configs is None: machine_configs = {} - if substitutions is None: - substitutions = { - "__CHANGE_ME__": "_test_vm_persistence", - "git+https://git.clan.lol/clan/clan-core": "path://" + str(CLAN_CORE), - "https://git.clan.lol/clan/clan-core/archive/main.tar.gz": "path://" - + str(CLAN_CORE), - } + substitutions = { + "__CHANGE_ME__": "_test_vm_persistence", + "git+https://git.clan.lol/clan/clan-core": "path://" + str(CLAN_CORE), + "https://git.clan.lol/clan/clan-core/archive/main.tar.gz": "path://" + + str(CLAN_CORE), + } flake = temporary_home / "flake" shutil.copytree(flake_template, flake) sp.run(["chmod", "+w", "-R", str(flake)], check=True) @@ -121,6 +139,11 @@ def generate_flake( # generate machines from machineConfigs for machine_name, machine_config in machine_configs.items(): + configuration_nix = flake / "machines" / machine_name / "configuration.nix" + configuration_nix.parent.mkdir(parents=True, exist_ok=True) + configuration_nix.write_text(""" + { imports = [ (builtins.fromJSON (builtins.readFile ./configuration.json)) ]; } + """) set_machine_settings(flake, machine_name, machine_config) if "/tmp" not in str(os.environ.get("HOME")): @@ -135,17 +158,15 @@ def generate_flake( cwd=flake, check=True, ) - sp.run(["git", "init"], cwd=flake, check=True) - sp.run(["git", "add", "."], cwd=flake, check=True) - sp.run(["git", "config", "user.name", "clan-tool"], cwd=flake, check=True) - sp.run(["git", "config", "user.email", "clan@example.com"], cwd=flake, check=True) - sp.run(["git", "commit", "-a", "-m", "Initial commit"], cwd=flake, check=True) + init_git(monkeypatch, flake) + return FlakeForTest(flake) def create_flake( temporary_home: Path, flake_template: str | Path, + monkeypatch: pytest.MonkeyPatch, clan_core_flake: Path | None = None, # names referring to pre-defined machines from ../machines machines: list[str] | None = None, @@ -198,18 +219,7 @@ def create_flake( f"!! $HOME does not point to a temp directory!! HOME={os.environ['HOME']}" ) - # TODO: Find out why test_vms_api.py fails in nix build - # but works in pytest when this bottom line is commented out - sp.run( - ["git", "config", "--global", "init.defaultBranch", "main"], - cwd=flake, - check=True, - ) - sp.run(["git", "init"], cwd=flake, check=True) - sp.run(["git", "add", "."], cwd=flake, check=True) - sp.run(["git", "config", "user.name", "clan-tool"], cwd=flake, check=True) - sp.run(["git", "config", "user.email", "clan@example.com"], cwd=flake, check=True) - sp.run(["git", "commit", "-a", "-m", "Initial commit"], cwd=flake, check=True) + init_git(monkeypatch, flake) if remote: with tempfile.TemporaryDirectory(prefix="flake-"): @@ -222,7 +232,11 @@ def create_flake( def test_flake( monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> Iterator[FlakeForTest]: - yield from create_flake(temporary_home, "test_flake") + yield from create_flake( + temporary_home=temporary_home, + flake_template="test_flake", + monkeypatch=monkeypatch, + ) # check that git diff on ./sops is empty if (temporary_home / "test_flake" / "sops").exists(): git_proc = sp.run( @@ -244,9 +258,10 @@ def test_flake_with_core( msg = "clan-core flake not found. This test requires the clan-core flake to be present" raise FixtureError(msg) yield from create_flake( - temporary_home, - "test_flake_with_core", - CLAN_CORE, + temporary_home=temporary_home, + flake_template="test_flake_with_core", + clan_core_flake=CLAN_CORE, + monkeypatch=monkeypatch, ) @@ -276,9 +291,10 @@ def test_flake_with_core_and_pass( msg = "clan-core flake not found. This test requires the clan-core flake to be present" raise FixtureError(msg) yield from create_flake( - temporary_home, - "test_flake_with_core_and_pass", - CLAN_CORE, + temporary_home=temporary_home, + flake_template="test_flake_with_core_and_pass", + clan_core_flake=CLAN_CORE, + monkeypatch=monkeypatch, ) @@ -290,7 +306,8 @@ def test_flake_minimal( msg = "clan-core flake not found. This test requires the clan-core flake to be present" raise FixtureError(msg) yield from create_flake( - temporary_home, - CLAN_CORE / "templates" / "minimal", - CLAN_CORE, + temporary_home=temporary_home, + flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, + clan_core_flake=CLAN_CORE, ) diff --git a/pkgs/clan-cli/tests/nix_config.py b/pkgs/clan-cli/tests/nix_config.py new file mode 100644 index 000000000..2f0d753ae --- /dev/null +++ b/pkgs/clan-cli/tests/nix_config.py @@ -0,0 +1,24 @@ +import json +import subprocess +from dataclasses import dataclass + +import pytest + + +@dataclass +class ConfigItem: + aliases: list[str] + defaultValue: bool # noqa: N815 + description: str + documentDefault: bool # noqa: N815 + experimentalFeature: str # noqa: N815 + value: str | bool | list[str] | dict[str, str] + + +@pytest.fixture(scope="session") +def nix_config() -> dict[str, ConfigItem]: + proc = subprocess.run( + ["nix", "show-config", "--json"], check=True, stdout=subprocess.PIPE + ) + data = json.loads(proc.stdout) + return {name: ConfigItem(**c) for name, c in data.items()} diff --git a/pkgs/clan-cli/tests/test_config.py b/pkgs/clan-cli/tests/test_config.py deleted file mode 100644 index eba6bd7b6..000000000 --- a/pkgs/clan-cli/tests/test_config.py +++ /dev/null @@ -1,171 +0,0 @@ -from pathlib import Path - -import pytest -from clan_cli import config -from clan_cli.config import parsing -from clan_cli.errors import ClanError - -example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" - - -def test_walk_jsonschema_all_types() -> None: - schema = { - "type": "object", - "properties": { - "array": { - "type": "array", - "items": { - "type": "string", - }, - }, - "boolean": {"type": "boolean"}, - "integer": {"type": "integer"}, - "number": {"type": "number"}, - "string": {"type": "string"}, - }, - } - expected = { - "array": list[str], - "boolean": bool, - "integer": int, - "number": float, - "string": str, - } - assert config.parsing.options_types_from_schema(schema) == expected - - -def test_walk_jsonschema_nested() -> None: - schema = { - "type": "object", - "properties": { - "name": { - "type": "object", - "properties": { - "first": {"type": "string"}, - "last": {"type": "string"}, - }, - }, - "age": {"type": "integer"}, - }, - } - expected = { - "age": int, - "name.first": str, - "name.last": str, - } - assert config.parsing.options_types_from_schema(schema) == expected - - -# test walk_jsonschema with dynamic attributes (e.g. "additionalProperties") -def test_walk_jsonschema_dynamic_attrs() -> None: - schema = { - "type": "object", - "properties": { - "age": {"type": "integer"}, - "users": { - "type": "object", - "additionalProperties": {"type": "string"}, - }, - }, - } - expected = { - "age": int, - "users.": str, # is a placeholder for any string - } - assert config.parsing.options_types_from_schema(schema) == expected - - -def test_type_from_schema_path_simple() -> None: - schema = { - "type": "boolean", - } - assert parsing.type_from_schema_path(schema, []) is bool - - -def test_type_from_schema_path_nested() -> None: - schema = { - "type": "object", - "properties": { - "name": { - "type": "object", - "properties": { - "first": {"type": "string"}, - "last": {"type": "string"}, - }, - }, - "age": {"type": "integer"}, - }, - } - assert parsing.type_from_schema_path(schema, ["age"]) is int - assert parsing.type_from_schema_path(schema, ["name", "first"]) is str - - -def test_type_from_schema_path_dynamic_attrs() -> None: - schema = { - "type": "object", - "properties": { - "age": {"type": "integer"}, - "users": { - "type": "object", - "additionalProperties": {"type": "string"}, - }, - }, - } - assert parsing.type_from_schema_path(schema, ["age"]) is int - assert parsing.type_from_schema_path(schema, ["users", "foo"]) is str - - -def test_map_type() -> None: - with pytest.raises(ClanError): - config.map_type("foo") - assert config.map_type("string") is str - assert config.map_type("integer") is int - assert config.map_type("boolean") is bool - assert config.map_type("attribute set of string") == dict[str, str] - assert config.map_type("attribute set of integer") == dict[str, int] - assert config.map_type("null or string") == str | None - - -# test the cast function with simple types -def test_cast() -> None: - assert ( - config.cast(value=["true"], input_type=bool, opt_description="foo-option") - is True - ) - assert ( - config.cast(value=["null"], input_type=str | None, opt_description="foo-option") - is None - ) - assert ( - config.cast(value=["bar"], input_type=str | None, opt_description="foo-option") - == "bar" - ) - - -@pytest.mark.parametrize( - ("option", "value", "options", "expected"), - [ - ("foo.bar", ["baz"], {"foo.bar": {"type": "str"}}, ("foo.bar", ["baz"])), - ("foo.bar", ["baz"], {"foo": {"type": "attrs"}}, ("foo", {"bar": ["baz"]})), - ( - "users.users.my-user.name", - ["my-name"], - {"users.users..name": {"type": "str"}}, - ("users.users..name", ["my-name"]), - ), - ( - "foo.bar.baz.bum", - ["val"], - {"foo..baz": {"type": "attrs"}}, - ("foo..baz", {"bum": ["val"]}), - ), - ( - "userIds.DavHau", - ["42"], - {"userIds": {"type": "attrs"}}, - ("userIds", {"DavHau": ["42"]}), - ), - ], -) -def test_find_option(option: str, value: list, options: dict, expected: tuple) -> None: - assert config.find_option(option, value, options) == expected diff --git a/pkgs/clan-cli/tests/test_flake/flake.nix b/pkgs/clan-cli/tests/test_flake/flake.nix index 53f8a7bae..fbb8531b8 100644 --- a/pkgs/clan-cli/tests/test_flake/flake.nix +++ b/pkgs/clan-cli/tests/test_flake/flake.nix @@ -12,23 +12,11 @@ inputs = inputs' // { clan-core = fake-clan-core; }; - machineSettings = ( - if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != "" then - builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE")) - else if builtins.pathExists ./machines/machine1/settings.json then - builtins.fromJSON (builtins.readFile ./machines/machine1/settings.json) - else - { } - ); - machineImports = map (module: fake-clan-core.clanModules.${module}) ( - machineSettings.clanImports or [ ] - ); in { nixosConfigurations.machine1 = inputs.nixpkgs.lib.nixosSystem { - modules = machineImports ++ [ + modules = [ ./nixosModules/machine1.nix - machineSettings ( { lib, diff --git a/pkgs/clan-cli/tests/test_modules.py b/pkgs/clan-cli/tests/test_modules.py index 9d70b805f..162ef6285 100644 --- a/pkgs/clan-cli/tests/test_modules.py +++ b/pkgs/clan-cli/tests/test_modules.py @@ -1,4 +1,5 @@ import json +import subprocess from typing import TYPE_CHECKING import pytest @@ -55,12 +56,19 @@ def test_add_module_to_inventory( ) opts = CreateOptions( clan_dir=FlakeId(str(base_path)), - machine=Machine( - name="machine1", tags=[], system="x86_64-linux", deploy=MachineDeploy() - ), + machine=Machine(name="machine1", tags=[], deploy=MachineDeploy()), ) create_machine(opts) + (test_flake_with_core.path / "machines" / "machine1" / "facter.json").write_text( + json.dumps( + { + "version": 1, + "system": "x86_64-linux", + } + ) + ) + subprocess.run(["git", "add", "."], cwd=test_flake_with_core.path) inventory = load_inventory_json(base_path) diff --git a/pkgs/clan-cli/tests/test_vars.py b/pkgs/clan-cli/tests/test_vars.py index 21d176fef..5a7355acd 100644 --- a/pkgs/clan-cli/tests/test_vars.py +++ b/pkgs/clan-cli/tests/test_vars.py @@ -1,5 +1,5 @@ import json -import subprocess +import shutil from dataclasses import dataclass from io import StringIO from pathlib import Path @@ -9,7 +9,7 @@ from age_keys import SopsSetup from clan_cli.clan_uri import FlakeId from clan_cli.errors import ClanError from clan_cli.machines.machines import Machine -from clan_cli.nix import nix_eval, nix_shell, run +from clan_cli.nix import nix_eval, run from clan_cli.vars.check import check_vars from clan_cli.vars.generate import generate_vars_for_machine from clan_cli.vars.list import stringify_all_vars @@ -83,12 +83,14 @@ def test_generate_public_var( temporary_home: Path, ) -> None: config = nested_dict() + config["nixpkgs"]["hostPlatform"] = "x86_64-linux" my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator["files"]["my_value"]["secret"] = False my_generator["script"] = "echo hello > $out/my_value" flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -122,12 +124,14 @@ def test_generate_secret_var_sops( sops_setup: SopsSetup, ) -> None: config = nested_dict() + config["nixpkgs"]["hostPlatform"] = "x86_64-linux" my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator["files"]["my_secret"]["secret"] = True my_generator["script"] = "echo hello > $out/my_secret" flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -162,6 +166,7 @@ def test_generate_secret_var_sops_with_default_group( sops_setup: SopsSetup, ) -> None: config = nested_dict() + config["nixpkgs"]["hostPlatform"] = "x86_64-linux" config["clan"]["core"]["sops"]["defaultGroups"] = ["my_group"] my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator["files"]["my_secret"]["secret"] = True @@ -169,6 +174,7 @@ def test_generate_secret_var_sops_with_default_group( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -193,6 +199,7 @@ def test_generated_shared_secret_sops( sops_setup: SopsSetup, ) -> None: m1_config = nested_dict() + m1_config["nixpkgs"]["hostPlatform"] = "x86_64-linux" shared_generator = m1_config["clan"]["core"]["vars"]["generators"][ "my_shared_generator" ] @@ -200,12 +207,14 @@ def test_generated_shared_secret_sops( shared_generator["files"]["my_shared_secret"]["secret"] = True shared_generator["script"] = "echo hello > $out/my_shared_secret" m2_config = nested_dict() + m2_config["nixpkgs"]["hostPlatform"] = "x86_64-linux" m2_config["clan"]["core"]["vars"]["generators"]["my_shared_generator"] = ( shared_generator.copy() ) flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"machine1": m1_config, "machine2": m2_config}, ) monkeypatch.chdir(flake.path) @@ -233,8 +242,10 @@ def test_generated_shared_secret_sops( def test_generate_secret_var_password_store( monkeypatch: pytest.MonkeyPatch, temporary_home: Path, + test_root: Path, ) -> None: config = nested_dict() + config["nixpkgs"]["hostPlatform"] = "x86_64-linux" config["clan"]["core"]["vars"]["settings"]["secretStore"] = "password-store" my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator["files"]["my_secret"]["secret"] = True @@ -248,33 +259,18 @@ def test_generate_secret_var_password_store( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) gnupghome = temporary_home / "gpg" - gnupghome.mkdir(mode=0o700) + shutil.copytree(test_root / "data" / "gnupg-home", gnupghome) monkeypatch.setenv("GNUPGHOME", str(gnupghome)) + + password_store_dir = temporary_home / "pass" + shutil.copytree(test_root / "data" / "password-store", password_store_dir) monkeypatch.setenv("PASSWORD_STORE_DIR", str(temporary_home / "pass")) - gpg_key_spec = temporary_home / "gpg_key_spec" - gpg_key_spec.write_text( - """ - Key-Type: 1 - Key-Length: 1024 - Name-Real: Root Superuser - Name-Email: test@local - Expire-Date: 0 - %no-protection - """ - ) - subprocess.run( - nix_shell( - ["nixpkgs#gnupg"], ["gpg", "--batch", "--gen-key", str(gpg_key_spec)] - ), - check=True, - ) - subprocess.run( - nix_shell(["nixpkgs#pass"], ["pass", "init", "test@local"]), check=True - ) + machine = Machine(name="my_machine", flake=FlakeId(str(flake.path))) assert not check_vars(machine) cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"]) @@ -318,6 +314,7 @@ def test_generate_secret_for_multiple_machines( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"machine1": machine1_config, "machine2": machine2_config}, ) monkeypatch.chdir(flake.path) @@ -363,6 +360,7 @@ def test_dependant_generators( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -402,6 +400,7 @@ def test_prompt( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -421,6 +420,7 @@ def test_share_flag( sops_setup: SopsSetup, ) -> None: config = nested_dict() + config["nixpkgs"]["hostPlatform"] = "x86_64-linux" shared_generator = config["clan"]["core"]["vars"]["generators"]["shared_generator"] shared_generator["share"] = True shared_generator["files"]["my_secret"]["secret"] = True @@ -440,6 +440,7 @@ def test_share_flag( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -490,6 +491,7 @@ def test_prompt_create_file( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -518,6 +520,7 @@ def test_api_get_prompts( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -546,6 +549,7 @@ def test_api_set_prompts( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -592,6 +596,7 @@ def test_commit_message( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -641,6 +646,7 @@ def test_default_value( temporary_home: Path, ) -> None: config = nested_dict() + config["nixpkgs"]["hostPlatform"] = "x86_64-linux" my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator["files"]["my_value"]["secret"] = False my_generator["files"]["my_value"]["value"]["_type"] = "override" @@ -650,6 +656,7 @@ def test_default_value( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -683,6 +690,7 @@ def test_stdout_of_generate( sops_setup: SopsSetup, ) -> None: config = nested_dict() + config["nixpkgs"]["hostPlatform"] = "x86_64-linux" my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator["files"]["my_value"]["secret"] = False my_generator["script"] = "echo -n hello > $out/my_value" @@ -694,6 +702,7 @@ def test_stdout_of_generate( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -761,6 +770,7 @@ def test_migration_skip( sops_setup: SopsSetup, ) -> None: config = nested_dict() + config["nixpkgs"]["hostPlatform"] = "x86_64-linux" my_service = config["clan"]["core"]["facts"]["services"]["my_service"] my_service["secret"]["my_value"] = {} my_service["generator"]["script"] = "echo -n hello > $secrets/my_value" @@ -772,6 +782,7 @@ def test_migration_skip( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -792,6 +803,7 @@ def test_migration( sops_setup: SopsSetup, ) -> None: config = nested_dict() + config["nixpkgs"]["hostPlatform"] = "x86_64-linux" my_service = config["clan"]["core"]["facts"]["services"]["my_service"] my_service["public"]["my_value"] = {} my_service["generator"]["script"] = "echo -n hello > $facts/my_value" @@ -802,6 +814,7 @@ def test_migration( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) @@ -822,6 +835,7 @@ def test_fails_when_files_are_left_from_other_backend( sops_setup: SopsSetup, ) -> None: config = nested_dict() + config["nixpkgs"]["hostPlatform"] = "x86_64-linux" my_secret_generator = config["clan"]["core"]["vars"]["generators"][ "my_secret_generator" ] @@ -835,6 +849,7 @@ def test_fails_when_files_are_left_from_other_backend( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"my_machine": config}, ) monkeypatch.chdir(flake.path) diff --git a/pkgs/clan-cli/tests/test_vars_deployment.py b/pkgs/clan-cli/tests/test_vars_deployment.py index 996d92d09..4a7bc46d7 100644 --- a/pkgs/clan-cli/tests/test_vars_deployment.py +++ b/pkgs/clan-cli/tests/test_vars_deployment.py @@ -12,6 +12,7 @@ from clan_cli.vms.run import inspect_vm, spawn_vm from fixtures_flakes import generate_flake from helpers import cli from helpers.nixos_config import nested_dict +from nix_config import ConfigItem from root import CLAN_CORE @@ -19,10 +20,12 @@ from root import CLAN_CORE def test_vm_deployment( monkeypatch: pytest.MonkeyPatch, temporary_home: Path, + nix_config: dict[str, ConfigItem], sops_setup: SopsSetup, ) -> None: # machine 1 machine1_config = nested_dict() + machine1_config["nixpkgs"]["hostPlatform"] = nix_config["system"].value machine1_config["clan"]["virtualisation"]["graphics"] = False machine1_config["services"]["getty"]["autologinUser"] = "root" machine1_config["services"]["openssh"]["enable"] = True @@ -48,6 +51,7 @@ def test_vm_deployment( """ # machine 2 machine2_config = nested_dict() + machine2_config["nixpkgs"]["hostPlatform"] = nix_config["system"].value machine2_config["clan"]["virtualisation"]["graphics"] = False machine2_config["services"]["getty"]["autologinUser"] = "root" machine2_config["services"]["openssh"]["enable"] = True @@ -62,6 +66,7 @@ def test_vm_deployment( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs={"m1_machine": machine1_config, "m2_machine": machine2_config}, ) diff --git a/pkgs/clan-cli/tests/test_vms_cli.py b/pkgs/clan-cli/tests/test_vms_cli.py index 7957add0a..fc07c15e9 100644 --- a/pkgs/clan-cli/tests/test_vms_cli.py +++ b/pkgs/clan-cli/tests/test_vms_cli.py @@ -83,6 +83,7 @@ def test_vm_persistence( flake = generate_flake( temporary_home, flake_template=CLAN_CORE / "templates" / "minimal", + monkeypatch=monkeypatch, machine_configs=config, ) diff --git a/pkgs/schemas/flake-module.nix b/pkgs/schemas/flake-module.nix index f0e92a0f0..e9e186874 100644 --- a/pkgs/schemas/flake-module.nix +++ b/pkgs/schemas/flake-module.nix @@ -23,7 +23,7 @@ modulename: _: jsonLib.parseOptions (optionsFromModule modulename) { } ) clanModules; - clanModuleFunctionSchemas = lib.mapAttrsFlatten ( + clanModuleFunctionSchemas = lib.attrsets.mapAttrsToList ( modulename: _: (self.lib.modules.getFrontmatter modulename) // { diff --git a/templates/flake-module.nix b/templates/flake-module.nix index f25df79cd..639d800c1 100644 --- a/templates/flake-module.nix +++ b/templates/flake-module.nix @@ -7,10 +7,19 @@ initialized = inputs.nixpkgs.legacyPackages.x86_64-linux.runCommand "minimal-clan-flake" { } '' mkdir $out cp -r ${path}/* $out - mkdir -p $out/machines/foo + mkdir -p $out/machines/testmachine # TODO: Instead create a machine by calling the API, this wont break in future tests and is much closer to what the user performs - echo '{ "nixpkgs": { "hostPlatform": "x86_64-linux" } }' > $out/machines/foo/settings.json + cat > $out/machines/testmachine/hardware-configuration.nix << EOF + { lib, ... }: { + nixpkgs.hostPlatform = "x86_64-linux"; + system.stateVersion = lib.version; + documentation.enable = false; + users.users.root.initialPassword = "fnord23"; + boot.loader.grub.devices = lib.mkForce [ "/dev/sda" ]; + fileSystems."/".device = lib.mkDefault "/dev/sda"; + } + EOF ''; evaled = (import "${initialized}/flake.nix").outputs { self = evaled // { @@ -22,7 +31,7 @@ { type = "derivation"; name = "minimal-clan-flake-check"; - inherit (evaled.nixosConfigurations.foo.config.system.build.vm) drvPath outPath; + inherit (evaled.nixosConfigurations.testmachine.config.system.build.toplevel) drvPath outPath; }; }; }