Merge pull request 'import nixos-facter by default' (#2178) from nixos-facter into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/2178
This commit is contained in:
Mic92
2024-10-09 10:43:17 +00:00
37 changed files with 246 additions and 888 deletions

View File

@@ -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" ];

View File

@@ -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,
...

View File

@@ -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
];

View File

@@ -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": {

View File

@@ -26,36 +26,10 @@ let
}
);
machineSettings =
machineName:
let
warn = lib.warn ''
The use of ./machines/<machine>/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/<machine>/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"

View File

@@ -146,6 +146,7 @@ in
{ foo, ... }:
{
networking.hostName = foo;
nixpkgs.hostPlatform = "x86_64-linux";
};
};
in

View File

@@ -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;
})

View File

@@ -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;

View File

@@ -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;
};
};

View File

@@ -0,0 +1,4 @@
{
"version": 1,
"system": "x86_64-linux"
}

View File

@@ -9,6 +9,7 @@
./meta/interface.nix
./metadata.nix
./networking.nix
./nixos-facter.nix
./nix-settings.nix
./options.nix
./outputs.nix

View File

@@ -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}'.
''
];
}

View File

@@ -10,6 +10,7 @@
];
clanCore.imports = [
inputs.sops-nix.nixosModules.sops
inputs.nixos-facter-modules.nixosModules.facter
inputs.disko.nixosModules.default
./clanCore
(

View File

@@ -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}.<name>"
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>.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.<name>.baz.<name>"
# we can still find the option
first = option_path[0]
regex = rf"({first}|<name>)"
for elem in option_path[1:]:
regex += rf"\.({elem}|<name>)"
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>.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}",
)

View File

@@ -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}(?<!-)$"
if not re.match(hostname_regex, machine_name):
msg = "Machine name must be a valid hostname"
raise ClanError(msg)
if "networking" in config and "hostName" in config["networking"]:
if machine_name != config["networking"]["hostName"]:
raise ClanHttpError(
msg="Machine name does not match the 'networking.hostName' setting in the config",
status_code=400,
)
config["networking"]["hostName"] = machine_name
# create machine folder if it doesn't exist
# write the config to a json file located at {flake}/machines/{machine_name}/settings.json
settings_path = machine_settings_file(flake_dir, machine_name)
settings_path.parent.mkdir(parents=True, exist_ok=True)
with settings_path.open("w") as f:
json.dump(config, f)
if flake_dir is not None:
commit_file(settings_path, flake_dir)

View File

@@ -1,111 +0,0 @@
import json
from pathlib import Path
from typing import Any
from clan_cli.cmd import run
from clan_cli.errors import ClanError
from clan_cli.nix import nix_eval
script_dir = Path(__file__).parent
type_map: dict[str, type] = {
"array": list,
"boolean": bool,
"integer": int,
"number": float,
"string": str,
}
def schema_from_module_file(
file: str | Path = f"{script_dir}/jsonschema/example-schema.json",
) -> 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 <nixpkgs/lib>;
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}.<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

View File

@@ -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

View File

@@ -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)

View File

@@ -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 {

View File

@@ -15,6 +15,7 @@ pytest_plugins = [
"host_group",
"fixtures_flakes",
"stdout",
"nix_config",
]

View File

@@ -0,0 +1,29 @@
This is a revocation certificate for the OpenPGP key:
pub rsa1024 2024-09-29 [SCEAR]
9A9B2741C8062D3D3DF1302D8B049E262A5CA255
uid Root Superuser <test@local>
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-----

View File

@@ -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#)))

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,6 @@
Key-Type: 1
Key-Length: 1024
Name-Real: Root Superuser
Name-Email: test@local
Expire-Date: 0
%no-protection

View File

@@ -0,0 +1 @@
test@local

View File

@@ -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,
)

View File

@@ -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()}

View File

@@ -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.<name>": str, # <name> 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>.name": {"type": "str"}},
("users.users.<name>.name", ["my-name"]),
),
(
"foo.bar.baz.bum",
["val"],
{"foo.<name>.baz": {"type": "attrs"}},
("foo.<name>.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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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},
)

View File

@@ -83,6 +83,7 @@ def test_vm_persistence(
flake = generate_flake(
temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs=config,
)

View File

@@ -23,7 +23,7 @@
modulename: _: jsonLib.parseOptions (optionsFromModule modulename) { }
) clanModules;
clanModuleFunctionSchemas = lib.mapAttrsFlatten (
clanModuleFunctionSchemas = lib.attrsets.mapAttrsToList (
modulename: _:
(self.lib.modules.getFrontmatter modulename)
// {

View File

@@ -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;
};
};
}