Files
clan-core/pkgs/generate-test-vars/generate_test_vars/cli.py
2025-07-09 16:20:37 +07:00

232 lines
6.7 KiB
Python
Executable File

#! /usr/bin/env python3
import argparse
import json
import logging
import os
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Any, override
from clan_cli.vars.generate import generate_vars
from clan_lib.dirs import find_git_repo_root
from clan_lib.flake.flake import Flake
from clan_lib.machines.machines import Machine
from clan_lib.nix import nix_config, nix_eval, nix_test_store
log = logging.getLogger(__name__)
sops_priv_key = (
"AGE-SECRET-KEY-1PL0M9CWRCG3PZ9DXRTTLMCVD57U6JDFE8K7DNVQ35F4JENZ6G3MQ0RQLRV"
)
sops_pub_key = "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg"
def get_machine_names(repo_root: Path, check_attr: str, system: str) -> list[str]:
"""
Get the machine names from the test flake
"""
nix_options = []
if tmp_store := nix_test_store():
nix_options += ["--store", str(tmp_store)]
cmd = nix_eval(
[
f"path://{repo_root}#checks.{system}.{check_attr}.nodes",
"--apply",
"builtins.attrNames",
*nix_options,
]
)
out = subprocess.run(cmd, check=True, text=True, stdout=subprocess.PIPE)
return json.loads(out.stdout.strip())
class TestFlake(Flake):
"""
Flake class which is able to deal with not having an actual flake.
All nix build and eval calls will be forwarded to:
clan-core#checks.<system>.<test_name>
"""
def __init__(self, check_attr: str, *args: Any, **kwargs: Any) -> None:
"""
Initialize the TestFlake with the check attribute.
"""
super().__init__(*args, **kwargs)
self.check_attr = check_attr
def select_machine(self, machine_name: str, selector: str) -> Any:
"""
Select a nix attribute for a specific machine.
Args:
machine_name: The name of the machine
selector: The attribute selector string relative to the machine config
apply: Optional function to apply to the result
"""
from clan_lib.nix import nix_config
config = nix_config()
system = config["system"]
test_system = system
if system.endswith("-darwin"):
test_system = system.rstrip("darwin") + "linux"
full_selector = f'checks."{test_system}".{self.check_attr}.machinesCross.{system}."{machine_name}".{selector}'
return self.select(full_selector)
class TestMachine(Machine):
"""
Machine class which is able to deal with not having an actual flake.
All nix build and eval calls will be forwarded to:
clan-core#checks.<system>.<test_name>.nodes.<machine_name>.<attr>
"""
@override
def __init__(
self, name: str, flake: Flake, test_dir: Path, check_attr: str
) -> None:
super().__init__(name, flake)
self.check_attr = check_attr
self.test_dir = test_dir
@property
def flake_dir(self) -> Path:
return self.test_dir
def select(self, attr: str) -> Any:
"""
Build the machine and return the path to the result
accepts a secret store and a facts store # TODO
"""
config = nix_config()
system = config["system"]
test_system = system
if system.endswith("-darwin"):
test_system = system.rstrip("darwin") + "linux"
return self.flake.select(
f'checks."{test_system}".{self.check_attr}.machinesCross.{system}.{self.name}.{attr}'
)
@dataclass
class Options:
repo_root: Path
test_dir: Path
check_attr: str
clean: bool
def parse_args() -> Options:
parser = argparse.ArgumentParser(
description="""
Update the vars of a 'clanTest' integration test.
See 'clanLib.test.clanTest' for more information on how to create such a test.
""",
)
parser.add_argument(
"--repo-root",
type=Path,
help="""
Should be an absolute path to the repo root.
This path is used as root to evaluate and build attributes using the nix commands.
i.e. 'nix eval <repo_root>#checks ...'
""",
required=False,
default=os.environ.get("PRJ_ROOT", find_git_repo_root()),
)
parser.add_argument(
"--clean",
help="wipe vars and sops directories before generating new vars",
action="store_true",
default=False,
)
parser.add_argument(
"test_dir",
type=Path,
help="""
The folder of the test. Usually passed as 'directory' to clan in the test.
Must be relative to the repo_root.
""",
)
parser.add_argument(
"check_attr",
type=str,
help="The attribute name of the flake#checks to update",
)
args = parser.parse_args()
return Options(
repo_root=args.repo_root,
test_dir=args.test_dir,
check_attr=args.check_attr,
clean=args.clean,
)
def main() -> None:
logging.basicConfig(level=logging.DEBUG)
os.environ["CLAN_NO_COMMIT"] = "1"
opts = parse_args()
test_dir = opts.test_dir
if opts.clean:
shutil.rmtree(test_dir / "vars", ignore_errors=True)
shutil.rmtree(test_dir / "sops", ignore_errors=True)
config = nix_config()
system = config["system"]
test_system = system
if system.endswith("-darwin"):
test_system = system.rstrip("darwin") + "linux"
flake = TestFlake(opts.check_attr, str(opts.repo_root))
machine_names = get_machine_names(
opts.repo_root,
opts.check_attr,
test_system,
)
flake.precache(
[
f"checks.{test_system}.{opts.check_attr}.machinesCross.{system}.{{{','.join(machine_names)}}}.config.clan.core.vars.generators.*.validationHash",
]
)
# This hack is necessary because the sops store uses flake.path to find the machine keys
flake._path = opts.test_dir # noqa: SLF001
machines = [
TestMachine(name, flake, test_dir, opts.check_attr) for name in machine_names
]
user = "admin"
admin_key_path = Path(test_dir.resolve() / "sops" / "users" / user / "key.json")
admin_key_path.parent.mkdir(parents=True, exist_ok=True)
admin_key_path.write_text(
json.dumps(
{
"publickey": sops_pub_key,
"type": "age",
},
indent=2,
)
+ "\n"
)
with NamedTemporaryFile("w") as f:
f.write("# created: 2023-07-17T10:51:45+02:00\n")
f.write(f"# public key: {sops_pub_key}\n")
f.write(sops_priv_key)
f.seek(0)
os.environ["SOPS_AGE_KEY_FILE"] = f.name
generate_vars(list(machines), fake_prompts=True)
if __name__ == "__main__":
main()