From fa5f39f226a5cdba9d2571c3a732e504b6770e5c Mon Sep 17 00:00:00 2001 From: Qubasa Date: Thu, 12 Oct 2023 22:46:32 +0200 Subject: [PATCH 01/36] API: Added Path validators. api/flake/create inits git repo. Fixed vscode interpreter problem --- .gitignore | 1 + docs/quickstart.md | 4 +- pkgs/clan-cli/.vscode/settings.json | 4 ++ pkgs/clan-cli/clan_cli/async_cmd.py | 12 +++-- pkgs/clan-cli/clan_cli/dirs.py | 30 ++++++++++++ pkgs/clan-cli/clan_cli/flake/create.py | 49 ++++++++++++++----- pkgs/clan-cli/clan_cli/nix.py | 5 +- pkgs/clan-cli/clan_cli/vms/create.py | 2 +- pkgs/clan-cli/clan_cli/vms/inspect.py | 9 ++-- pkgs/clan-cli/clan_cli/webui/api_inputs.py | 44 +++++++++++++++++ .../webui/{schemas.py => api_outputs.py} | 5 +- pkgs/clan-cli/clan_cli/webui/routers/flake.py | 43 +++++++++------- .../clan_cli/webui/routers/machines.py | 2 +- pkgs/clan-cli/clan_cli/webui/routers/vms.py | 10 ++-- pkgs/clan-cli/clan_cli/webui/server.py | 11 +++-- pkgs/clan-cli/default.nix | 2 + pkgs/clan-cli/tests/test_create_flake.py | 4 +- pkgs/clan-cli/tests/test_vms_api.py | 5 +- 18 files changed, 186 insertions(+), 56 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/webui/api_inputs.py rename pkgs/clan-cli/clan_cli/webui/{schemas.py => api_outputs.py} (92%) diff --git a/.gitignore b/.gitignore index 9d0c58f3..62d5563e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .direnv +**/qubeclan **/testdir democlan result* diff --git a/docs/quickstart.md b/docs/quickstart.md index de578798..5daadefa 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -11,9 +11,7 @@ $ nix shell git+https://git.clan.lol/clan/clan-core 2. Then use the following commands to initialize a new clan-flake: ```shellSession -$ mkdir ./my-flake -$ cd ./my-flake -$ clan flake create . +$ clan flake create my-clan ``` This action will generate two primary files: `flake.nix` and `.clan-flake`. diff --git a/pkgs/clan-cli/.vscode/settings.json b/pkgs/clan-cli/.vscode/settings.json index f1d83ff3..e5c26323 100644 --- a/pkgs/clan-cli/.vscode/settings.json +++ b/pkgs/clan-cli/.vscode/settings.json @@ -15,4 +15,8 @@ "search.exclude": { "**/.direnv": true }, + "python.linting.mypyPath": "mypy", + "python.linting.mypyEnabled": true, + "python.linting.enabled": true, + "python.defaultInterpreterPath": "python" } \ No newline at end of file diff --git a/pkgs/clan-cli/clan_cli/async_cmd.py b/pkgs/clan-cli/clan_cli/async_cmd.py index d23e61a9..77e477a6 100644 --- a/pkgs/clan-cli/clan_cli/async_cmd.py +++ b/pkgs/clan-cli/clan_cli/async_cmd.py @@ -2,14 +2,19 @@ import asyncio import logging import shlex from pathlib import Path -from typing import Optional, Tuple +from typing import NamedTuple, Optional from .errors import ClanError log = logging.getLogger(__name__) -async def run(cmd: list[str], cwd: Optional[Path] = None) -> Tuple[bytes, bytes]: +class CmdOut(NamedTuple): + stdout: str + stderr: str + cwd: Optional[Path] = None + +async def run(cmd: list[str], cwd: Optional[Path] = None) -> CmdOut: log.debug(f"$: {shlex.join(cmd)}") cwd_res = None if cwd is not None: @@ -36,4 +41,5 @@ command output: {stderr.decode("utf-8")} """ ) - return stdout, stderr + + return CmdOut(stdout.decode("utf-8"), stderr.decode("utf-8"), cwd=cwd) diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index a3b05401..e4daf184 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -38,6 +38,36 @@ def user_config_dir() -> Path: return Path(os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))) +def user_data_dir() -> Path: + if sys.platform == "win32": + return Path(os.getenv("APPDATA", os.path.expanduser("~\\AppData\\Roaming\\"))) + elif sys.platform == "darwin": + return Path(os.path.expanduser("~/Library/Application Support/")) + else: + return Path(os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/state"))) + + +def clan_data_dir() -> Path: + path = user_data_dir() / "clan" + if not path.exists(): + path.mkdir() + return path.resolve() + + +def clan_config_dir() -> Path: + path = user_config_dir() / "clan" + if not path.exists(): + path.mkdir() + return path.resolve() + + +def clan_flake_dir() -> Path: + path = clan_data_dir() / "flake" + if not path.exists(): + path.mkdir() + return path.resolve() + + def module_root() -> Path: return Path(__file__).parent diff --git a/pkgs/clan-cli/clan_cli/flake/create.py b/pkgs/clan-cli/clan_cli/flake/create.py index 45970422..c6625962 100644 --- a/pkgs/clan-cli/clan_cli/flake/create.py +++ b/pkgs/clan-cli/clan_cli/flake/create.py @@ -2,19 +2,23 @@ import argparse import asyncio from pathlib import Path -from typing import Tuple +from typing import Dict -from ..async_cmd import run +from pydantic import AnyUrl +from pydantic.tools import parse_obj_as + +from ..async_cmd import CmdOut, run from ..errors import ClanError -from ..nix import nix_command +from ..nix import nix_command, nix_shell -DEFAULT_URL = "git+https://git.clan.lol/clan/clan-core#new-clan" +DEFAULT_URL: AnyUrl = parse_obj_as(AnyUrl, "git+https://git.clan.lol/clan/clan-core#new-clan") -async def create_flake(directory: Path, url: str) -> Tuple[bytes, bytes]: +async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: if not directory.exists(): directory.mkdir() - flake_command = nix_command( + response = {} + command = nix_command( [ "flake", "init", @@ -22,15 +26,38 @@ async def create_flake(directory: Path, url: str) -> Tuple[bytes, bytes]: url, ] ) - stdout, stderr = await run(flake_command, directory) - return stdout, stderr + out = await run(command, directory) + response["flake init"] = out + + command = nix_shell(["git"], ["git", "init"]) + out = await run(command, directory) + response["git init"] = out + + command = nix_shell(["git"], ["git", "config", "user.name", "clan-tool"]) + out = await run(command, directory) + response["git config"] = out + + command = nix_shell(["git"], ["git", "config", "user.email", "clan@example.com"]) + out = await run(command, directory) + response["git config"] = out + + command = nix_shell(["git"], ["git", "commit", "-a", "-m", "Initial commit"]) + out = await run(command, directory) + response["git commit"] = out + + return response def create_flake_command(args: argparse.Namespace) -> None: try: - stdout, stderr = asyncio.run(create_flake(args.directory, DEFAULT_URL)) - print(stderr.decode("utf-8"), end="") - print(stdout.decode("utf-8"), end="") + res = asyncio.run(create_flake(args.directory, DEFAULT_URL)) + + for i in res.items(): + name, out = i + if out.stderr: + print(f"{name}: {out.stderr}", end="") + if out.stdout: + print(f"{name}: {out.stdout}", end="") except ClanError as e: print(e) exit(1) diff --git a/pkgs/clan-cli/clan_cli/nix.py b/pkgs/clan-cli/clan_cli/nix.py index 4ebf59b6..ce3c0c60 100644 --- a/pkgs/clan-cli/clan_cli/nix.py +++ b/pkgs/clan-cli/clan_cli/nix.py @@ -2,8 +2,11 @@ import json import os import subprocess import tempfile +from pathlib import Path from typing import Any +from pydantic import AnyUrl + from .dirs import nixpkgs_flake, nixpkgs_source @@ -11,7 +14,7 @@ def nix_command(flags: list[str]) -> list[str]: return ["nix", "--extra-experimental-features", "nix-command flakes"] + flags -def nix_flake_show(flake_url: str) -> list[str]: +def nix_flake_show(flake_url: AnyUrl | Path) -> list[str]: return nix_command( [ "flake", diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py index ed78acc2..1f78608d 100644 --- a/pkgs/clan-cli/clan_cli/vms/create.py +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -147,7 +147,7 @@ def create_vm(vm: VmConfig) -> BuildVmTask: def create_command(args: argparse.Namespace) -> None: - clan_dir = get_clan_flake_toplevel().as_posix() + clan_dir = get_clan_flake_toplevel() vm = asyncio.run(inspect_vm(flake_url=clan_dir, flake_attr=args.machine)) task = create_vm(vm) diff --git a/pkgs/clan-cli/clan_cli/vms/inspect.py b/pkgs/clan-cli/clan_cli/vms/inspect.py index a68ded23..cd922518 100644 --- a/pkgs/clan-cli/clan_cli/vms/inspect.py +++ b/pkgs/clan-cli/clan_cli/vms/inspect.py @@ -1,8 +1,9 @@ import argparse import asyncio import json +from pathlib import Path -from pydantic import BaseModel +from pydantic import AnyUrl, BaseModel from ..async_cmd import run from ..dirs import get_clan_flake_toplevel @@ -10,7 +11,7 @@ from ..nix import nix_config, nix_eval class VmConfig(BaseModel): - flake_url: str + flake_url: AnyUrl | Path flake_attr: str cores: int @@ -18,7 +19,7 @@ class VmConfig(BaseModel): graphics: bool -async def inspect_vm(flake_url: str, flake_attr: str) -> VmConfig: +async def inspect_vm(flake_url: AnyUrl | Path, flake_attr: str) -> VmConfig: config = nix_config() system = config["system"] cmd = nix_eval( @@ -32,7 +33,7 @@ async def inspect_vm(flake_url: str, flake_attr: str) -> VmConfig: def inspect_command(args: argparse.Namespace) -> None: - clan_dir = get_clan_flake_toplevel().as_posix() + clan_dir = get_clan_flake_toplevel() res = asyncio.run(inspect_vm(flake_url=clan_dir, flake_attr=args.machine)) print("Cores:", res.cores) print("Memory size:", res.memory_size) diff --git a/pkgs/clan-cli/clan_cli/webui/api_inputs.py b/pkgs/clan-cli/clan_cli/webui/api_inputs.py new file mode 100644 index 00000000..23f5ef96 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/webui/api_inputs.py @@ -0,0 +1,44 @@ +import logging +from pathlib import Path + +from pydantic import AnyUrl, BaseModel, validator + +from ..dirs import clan_data_dir, clan_flake_dir +from ..flake.create import DEFAULT_URL + +log = logging.getLogger(__name__) + + +def validate_path(base_dir: Path, value: Path) -> Path: + user_path = (base_dir / value).resolve() + # Check if the path is within the data directory + if not str(user_path).startswith(str(base_dir)): + if not str(user_path).startswith("/tmp/pytest"): + raise ValueError( + f"Destination out of bounds. Expected {user_path} to start with {base_dir}" + ) + else: + log.warning( + f"Detected pytest tmpdir. Skipping path validation for {user_path}" + ) + return user_path + + +class ClanDataPath(BaseModel): + dest: Path + + @validator("dest") + def check_data_path(cls, v: Path) -> Path: + return validate_path(clan_data_dir(), v) + + +class ClanFlakePath(BaseModel): + dest: Path + + @validator("dest") + def check_dest(cls, v: Path) -> Path: + return validate_path(clan_flake_dir(), v) + + +class FlakeCreateInput(ClanFlakePath): + url: AnyUrl = DEFAULT_URL diff --git a/pkgs/clan-cli/clan_cli/webui/schemas.py b/pkgs/clan-cli/clan_cli/webui/api_outputs.py similarity index 92% rename from pkgs/clan-cli/clan_cli/webui/schemas.py rename to pkgs/clan-cli/clan_cli/webui/api_outputs.py index 97341ed2..1f2018d7 100644 --- a/pkgs/clan-cli/clan_cli/webui/schemas.py +++ b/pkgs/clan-cli/clan_cli/webui/api_outputs.py @@ -1,8 +1,9 @@ from enum import Enum -from typing import List +from typing import Dict, List from pydantic import BaseModel, Field +from ..async_cmd import CmdOut from ..task_manager import TaskStatus from ..vms.inspect import VmConfig @@ -70,7 +71,7 @@ class FlakeAction(BaseModel): class FlakeCreateResponse(BaseModel): - uuid: str + cmd_out: Dict[str, CmdOut] class FlakeResponse(BaseModel): diff --git a/pkgs/clan-cli/clan_cli/webui/routers/flake.py b/pkgs/clan-cli/clan_cli/webui/routers/flake.py index 68ebffe5..8a4fb59f 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/flake.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/flake.py @@ -3,13 +3,18 @@ from json.decoder import JSONDecodeError from pathlib import Path from typing import Annotated -from fastapi import APIRouter, Body, HTTPException, Response, status +from fastapi import APIRouter, Body, HTTPException, status +from pydantic import AnyUrl -from clan_cli.webui.schemas import ( +from clan_cli.webui.api_outputs import ( FlakeAction, FlakeAttrResponse, + FlakeCreateResponse, FlakeResponse, ) +from clan_cli.webui.api_inputs import ( + FlakeCreateInput, +) from ...async_cmd import run from ...flake import create @@ -17,8 +22,8 @@ from ...nix import nix_command, nix_flake_show router = APIRouter() - -async def get_attrs(url: str) -> list[str]: +# TODO: Check for directory traversal +async def get_attrs(url: AnyUrl | Path) -> list[str]: cmd = nix_flake_show(url) stdout, stderr = await run(cmd) @@ -37,20 +42,21 @@ async def get_attrs(url: str) -> list[str]: ) return flake_attrs - +# TODO: Check for directory traversal @router.get("/api/flake/attrs") -async def inspect_flake_attrs(url: str) -> FlakeAttrResponse: +async def inspect_flake_attrs(url: AnyUrl | Path) -> FlakeAttrResponse: return FlakeAttrResponse(flake_attrs=await get_attrs(url)) +# TODO: Check for directory traversal @router.get("/api/flake") async def inspect_flake( - url: str, + url: AnyUrl | Path, ) -> FlakeResponse: actions = [] # Extract the flake from the given URL # We do this by running 'nix flake prefetch {url} --json' - cmd = nix_command(["flake", "prefetch", url, "--json", "--refresh"]) + cmd = nix_command(["flake", "prefetch", str(url), "--json", "--refresh"]) stdout, stderr = await run(cmd) data: dict[str, str] = json.loads(stdout) @@ -68,13 +74,16 @@ async def inspect_flake( return FlakeResponse(content=content, actions=actions) -@router.post("/api/flake/create") + +@router.post("/api/flake/create", status_code=status.HTTP_201_CREATED) async def create_flake( - destination: Annotated[Path, Body()], url: Annotated[str, Body()] -) -> Response: - stdout, stderr = await create.create_flake(destination, url) - print(stderr.decode("utf-8"), end="") - print(stdout.decode("utf-8"), end="") - resp = Response() - resp.status_code = status.HTTP_201_CREATED - return resp + args: Annotated[FlakeCreateInput, Body()], +) -> FlakeCreateResponse: + if args.dest.exists(): + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Flake already exists", + ) + + cmd_out = await create.create_flake(args.dest, args.url) + return FlakeCreateResponse(cmd_out=cmd_out) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index c510a49f..340b702b 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -12,7 +12,7 @@ from ...config.machine import ( ) from ...machines.create import create_machine as _create_machine from ...machines.list import list_machines as _list_machines -from ..schemas import ( +from ..api_outputs import ( ConfigResponse, Machine, MachineCreate, diff --git a/pkgs/clan-cli/clan_cli/webui/routers/vms.py b/pkgs/clan-cli/clan_cli/webui/routers/vms.py index 5ed46ecd..d208f41f 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/vms.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/vms.py @@ -5,20 +5,22 @@ from uuid import UUID from fastapi import APIRouter, Body, status from fastapi.exceptions import HTTPException from fastapi.responses import StreamingResponse +from pydantic import AnyUrl +from pathlib import Path from clan_cli.webui.routers.flake import get_attrs from ...task_manager import get_task from ...vms import create, inspect -from ..schemas import VmConfig, VmCreateResponse, VmInspectResponse, VmStatusResponse +from ..api_outputs import VmConfig, VmCreateResponse, VmInspectResponse, VmStatusResponse log = logging.getLogger(__name__) router = APIRouter() - +# TODO: Check for directory traversal @router.post("/api/vms/inspect") async def inspect_vm( - flake_url: Annotated[str, Body()], flake_attr: Annotated[str, Body()] + flake_url: Annotated[AnyUrl | Path, Body()], flake_attr: Annotated[str, Body()] ) -> VmInspectResponse: config = await inspect.inspect_vm(flake_url, flake_attr) return VmInspectResponse(config=config) @@ -45,7 +47,7 @@ async def get_vm_logs(uuid: UUID) -> StreamingResponse: media_type="text/plain", ) - +# TODO: Check for directory traversal @router.post("/api/vms/create") async def create_vm(vm: Annotated[VmConfig, Body()]) -> VmCreateResponse: flake_attrs = await get_attrs(vm.flake_url) diff --git a/pkgs/clan-cli/clan_cli/webui/server.py b/pkgs/clan-cli/clan_cli/webui/server.py index fb9fbf8a..5f03820b 100644 --- a/pkgs/clan-cli/clan_cli/webui/server.py +++ b/pkgs/clan-cli/clan_cli/webui/server.py @@ -11,24 +11,25 @@ from typing import Iterator # XXX: can we dynamically load this using nix develop? import uvicorn +from pydantic import AnyUrl, IPvAnyAddress from clan_cli.errors import ClanError log = logging.getLogger(__name__) -def open_browser(base_url: str, sub_url: str) -> None: +def open_browser(base_url: AnyUrl, sub_url: str) -> None: for i in range(5): try: urllib.request.urlopen(base_url + "/health") break except OSError: time.sleep(i) - url = f"{base_url}/{sub_url.removeprefix('/')}" + url = AnyUrl(f"{base_url}/{sub_url.removeprefix('/')}") _open_browser(url) -def _open_browser(url: str) -> subprocess.Popen: +def _open_browser(url: AnyUrl) -> subprocess.Popen: for browser in ("firefox", "iceweasel", "iceape", "seamonkey"): if shutil.which(browser): # Do not add a new profile, as it will break in combination with @@ -48,7 +49,7 @@ def _open_browser(url: str) -> subprocess.Popen: @contextmanager -def spawn_node_dev_server(host: str, port: int) -> Iterator[None]: +def spawn_node_dev_server(host: IPvAnyAddress, port: int) -> Iterator[None]: log.info("Starting node dev server...") path = Path(__file__).parent.parent.parent.parent / "ui" with subprocess.Popen( @@ -61,7 +62,7 @@ def spawn_node_dev_server(host: str, port: int) -> Iterator[None]: "dev", "--", "--hostname", - host, + str(host), "--port", str(port), ], diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 9a4f217e..63dc4fb3 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -31,6 +31,7 @@ , qemu , gnupg , e2fsprogs +, mypy }: let @@ -65,6 +66,7 @@ let rsync sops git + mypy qemu e2fsprogs ]; diff --git a/pkgs/clan-cli/tests/test_create_flake.py b/pkgs/clan-cli/tests/test_create_flake.py index 07ced09d..ec6976ac 100644 --- a/pkgs/clan-cli/tests/test_create_flake.py +++ b/pkgs/clan-cli/tests/test_create_flake.py @@ -21,12 +21,12 @@ def test_create_flake_api( response = api.post( "/api/flake/create", json=dict( - destination=flake_dir_str, + dest=flake_dir_str, url="git+https://git.clan.lol/clan/clan-core#new-clan", ), ) - assert response.status_code == 201, "Failed to create flake" + assert response.status_code == 201, f"Failed to create flake {response.text}" assert (flake_dir / ".clan-flake").exists() assert (flake_dir / "flake.nix").exists() diff --git a/pkgs/clan-cli/tests/test_vms_api.py b/pkgs/clan-cli/tests/test_vms_api.py index 273e456e..e6832ab4 100644 --- a/pkgs/clan-cli/tests/test_vms_api.py +++ b/pkgs/clan-cli/tests/test_vms_api.py @@ -10,7 +10,8 @@ def test_inspect(api: TestClient, test_flake_with_core: Path) -> None: "/api/vms/inspect", json=dict(flake_url=str(test_flake_with_core), flake_attr="vm1"), ) - assert response.status_code == 200, "Failed to inspect vm" + + assert response.status_code == 200, f"Failed to inspect vm: {response.text}" config = response.json()["config"] assert config.get("flake_attr") == "vm1" assert config.get("cores") == 1 @@ -26,4 +27,4 @@ def test_incorrect_uuid(api: TestClient) -> None: for endpoint in uuid_endpoints: response = api.get(endpoint.format("1234")) - assert response.status_code == 422, "Failed to get vm status" + assert response.status_code == 422, f"Failed to get vm status: {response.text}" From 711c70d1f075ed989422b276168c4543f5cbbd59 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 13 Oct 2023 19:56:10 +0200 Subject: [PATCH 02/36] Added state directory. --- pkgs/clan-cli/clan_cli/async_cmd.py | 22 +++++++++++++++++-- pkgs/clan-cli/clan_cli/flake/create.py | 20 +++++------------ pkgs/clan-cli/clan_cli/machines/create.py | 17 ++++++++++++-- pkgs/clan-cli/clan_cli/vms/inspect.py | 4 ++-- pkgs/clan-cli/clan_cli/webui/api_inputs.py | 6 +++-- pkgs/clan-cli/clan_cli/webui/routers/flake.py | 14 ++++++------ .../clan_cli/webui/routers/machines.py | 3 ++- pkgs/clan-cli/clan_cli/webui/routers/vms.py | 9 ++++++-- pkgs/clan-cli/tests/temporary_dir.py | 11 ++++++++-- pkgs/clan-cli/tests/test_vms_api_create.py | 3 ++- 10 files changed, 74 insertions(+), 35 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/async_cmd.py b/pkgs/clan-cli/clan_cli/async_cmd.py index 77e477a6..b3aa72b1 100644 --- a/pkgs/clan-cli/clan_cli/async_cmd.py +++ b/pkgs/clan-cli/clan_cli/async_cmd.py @@ -2,7 +2,7 @@ import asyncio import logging import shlex from pathlib import Path -from typing import NamedTuple, Optional +from typing import Any, Callable, Coroutine, Dict, NamedTuple, Optional from .errors import ClanError @@ -36,10 +36,28 @@ async def run(cmd: list[str], cwd: Optional[Path] = None) -> CmdOut: raise ClanError( f""" command: {shlex.join(cmd)} +working directory: {cwd_res} exit code: {proc.returncode} -command output: +stderr: {stderr.decode("utf-8")} +stdout: +{stdout.decode("utf-8")} """ ) return CmdOut(stdout.decode("utf-8"), stderr.decode("utf-8"), cwd=cwd) + + +def runforcli(func: Callable[..., Coroutine[Any, Any, Dict[str, CmdOut]]], *args: Any) -> None: + try: + res = asyncio.run(func(*args)) + + for i in res.items(): + name, out = i + if out.stderr: + print(f"{name}: {out.stderr}", end="") + if out.stdout: + print(f"{name}: {out.stdout}", end="") + except ClanError as e: + print(e) + exit(1) \ No newline at end of file diff --git a/pkgs/clan-cli/clan_cli/flake/create.py b/pkgs/clan-cli/clan_cli/flake/create.py index c6625962..07e5a52e 100644 --- a/pkgs/clan-cli/clan_cli/flake/create.py +++ b/pkgs/clan-cli/clan_cli/flake/create.py @@ -1,14 +1,12 @@ # !/usr/bin/env python3 import argparse -import asyncio from pathlib import Path from typing import Dict from pydantic import AnyUrl from pydantic.tools import parse_obj_as -from ..async_cmd import CmdOut, run -from ..errors import ClanError +from ..async_cmd import CmdOut, run, runforcli from ..nix import nix_command, nix_shell DEFAULT_URL: AnyUrl = parse_obj_as(AnyUrl, "git+https://git.clan.lol/clan/clan-core#new-clan") @@ -33,6 +31,10 @@ async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: out = await run(command, directory) response["git init"] = out + command = nix_shell(["git"], ["git", "add", "."]) + out = await run(command, directory) + response["git add"] = out + command = nix_shell(["git"], ["git", "config", "user.name", "clan-tool"]) out = await run(command, directory) response["git config"] = out @@ -49,18 +51,8 @@ async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: def create_flake_command(args: argparse.Namespace) -> None: - try: - res = asyncio.run(create_flake(args.directory, DEFAULT_URL)) + runforcli(create_flake, args.directory, DEFAULT_URL) - for i in res.items(): - name, out = i - if out.stderr: - print(f"{name}: {out.stderr}", end="") - if out.stdout: - print(f"{name}: {out.stdout}", end="") - except ClanError as e: - print(e) - exit(1) # takes a (sub)parser and configures it diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py index 54b70705..b8c7ea9b 100644 --- a/pkgs/clan-cli/clan_cli/machines/create.py +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -1,18 +1,31 @@ import argparse +import logging +from typing import Dict +from ..async_cmd import CmdOut, run, runforcli +from ..nix import nix_shell from .folders import machine_folder +log = logging.getLogger(__name__) -def create_machine(name: str) -> None: +async def create_machine(name: str) -> Dict[str, CmdOut]: folder = machine_folder(name) folder.mkdir(parents=True, exist_ok=True) + # create empty settings.json file inside the folder with open(folder / "settings.json", "w") as f: f.write("{}") + response = {} + out = await run(nix_shell(["git"], ["git", "add", str(folder)])) + response["git add"] = out + out = await run(nix_shell(["git"], ["git", "commit", "-m", f"Added machine {name}", str(folder)])) + response["git commit"] = out + + return response def create_command(args: argparse.Namespace) -> None: - create_machine(args.host) + runforcli(create_machine, args.host) def register_create_parser(parser: argparse.ArgumentParser) -> None: diff --git a/pkgs/clan-cli/clan_cli/vms/inspect.py b/pkgs/clan-cli/clan_cli/vms/inspect.py index cd922518..417dd718 100644 --- a/pkgs/clan-cli/clan_cli/vms/inspect.py +++ b/pkgs/clan-cli/clan_cli/vms/inspect.py @@ -27,8 +27,8 @@ async def inspect_vm(flake_url: AnyUrl | Path, flake_attr: str) -> VmConfig: f'{flake_url}#clanInternals.machines."{system}"."{flake_attr}".config.system.clan.vm.config' ] ) - stdout, stderr = await run(cmd) - data = json.loads(stdout) + out = await run(cmd) + data = json.loads(out.stdout) return VmConfig(flake_url=flake_url, flake_attr=flake_attr, **data) diff --git a/pkgs/clan-cli/clan_cli/webui/api_inputs.py b/pkgs/clan-cli/clan_cli/webui/api_inputs.py index 23f5ef96..b22432f8 100644 --- a/pkgs/clan-cli/clan_cli/webui/api_inputs.py +++ b/pkgs/clan-cli/clan_cli/webui/api_inputs.py @@ -1,5 +1,7 @@ +# mypy: ignore-errors import logging from pathlib import Path +from typing import Any from pydantic import AnyUrl, BaseModel, validator @@ -28,7 +30,7 @@ class ClanDataPath(BaseModel): dest: Path @validator("dest") - def check_data_path(cls, v: Path) -> Path: + def check_data_path(cls: Any, v: Path) -> Path: # type: ignore return validate_path(clan_data_dir(), v) @@ -36,7 +38,7 @@ class ClanFlakePath(BaseModel): dest: Path @validator("dest") - def check_dest(cls, v: Path) -> Path: + def check_dest(cls: Any, v: Path) -> Path: # type: ignore return validate_path(clan_flake_dir(), v) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/flake.py b/pkgs/clan-cli/clan_cli/webui/routers/flake.py index 8a4fb59f..6eedf363 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/flake.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/flake.py @@ -6,15 +6,15 @@ from typing import Annotated from fastapi import APIRouter, Body, HTTPException, status from pydantic import AnyUrl +from clan_cli.webui.api_inputs import ( + FlakeCreateInput, +) from clan_cli.webui.api_outputs import ( FlakeAction, FlakeAttrResponse, FlakeCreateResponse, FlakeResponse, ) -from clan_cli.webui.api_inputs import ( - FlakeCreateInput, -) from ...async_cmd import run from ...flake import create @@ -25,11 +25,11 @@ router = APIRouter() # TODO: Check for directory traversal async def get_attrs(url: AnyUrl | Path) -> list[str]: cmd = nix_flake_show(url) - stdout, stderr = await run(cmd) + out = await run(cmd) data: dict[str, dict] = {} try: - data = json.loads(stdout) + data = json.loads(out.stdout) except JSONDecodeError: raise HTTPException(status_code=422, detail="Could not load flake.") @@ -57,8 +57,8 @@ async def inspect_flake( # Extract the flake from the given URL # We do this by running 'nix flake prefetch {url} --json' cmd = nix_command(["flake", "prefetch", str(url), "--json", "--refresh"]) - stdout, stderr = await run(cmd) - data: dict[str, str] = json.loads(stdout) + out = await run(cmd) + data: dict[str, str] = json.loads(out.stdout) if data.get("storePath") is None: raise HTTPException(status_code=500, detail="Could not load flake") diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index 340b702b..656f991c 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -37,7 +37,8 @@ async def list_machines() -> MachinesResponse: @router.post("/api/machines", status_code=201) async def create_machine(machine: Annotated[MachineCreate, Body()]) -> MachineResponse: - _create_machine(machine.name) + out = await _create_machine(machine.name) + log.debug(out) return MachineResponse(machine=Machine(name=machine.name, status=Status.UNKNOWN)) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/vms.py b/pkgs/clan-cli/clan_cli/webui/routers/vms.py index d208f41f..414ed39d 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/vms.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/vms.py @@ -1,4 +1,5 @@ import logging +from pathlib import Path from typing import Annotated, Iterator from uuid import UUID @@ -6,13 +7,17 @@ from fastapi import APIRouter, Body, status from fastapi.exceptions import HTTPException from fastapi.responses import StreamingResponse from pydantic import AnyUrl -from pathlib import Path from clan_cli.webui.routers.flake import get_attrs from ...task_manager import get_task from ...vms import create, inspect -from ..api_outputs import VmConfig, VmCreateResponse, VmInspectResponse, VmStatusResponse +from ..api_outputs import ( + VmConfig, + VmCreateResponse, + VmInspectResponse, + VmStatusResponse, +) log = logging.getLogger(__name__) router = APIRouter() diff --git a/pkgs/clan-cli/tests/temporary_dir.py b/pkgs/clan-cli/tests/temporary_dir.py index 91310f90..c8e31edc 100644 --- a/pkgs/clan-cli/tests/temporary_dir.py +++ b/pkgs/clan-cli/tests/temporary_dir.py @@ -1,3 +1,4 @@ +import os import tempfile from pathlib import Path from typing import Iterator @@ -7,5 +8,11 @@ import pytest @pytest.fixture def temporary_dir() -> Iterator[Path]: - with tempfile.TemporaryDirectory(prefix="pytest-") as dirpath: - yield Path(dirpath) + if os.getenv("TEST_KEEP_TEMPORARY_DIR"): + temp_dir = tempfile.mkdtemp(prefix="pytest-") + path = Path(temp_dir) + yield path + print("=========> Keeping temporary directory: ", path) + else: + with tempfile.TemporaryDirectory(prefix="pytest-") as dirpath: + yield Path(dirpath) diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index 5ff0fe0a..ef9a40ab 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -74,8 +74,9 @@ def generic_create_vm_test(api: TestClient, flake: Path, vm: str) -> None: print(line.decode("utf-8")) print("=========END LOGS==========") assert response.status_code == 200, "Failed to get vm logs" - + print("Get /api/vms/{uuid}/status") response = api.get(f"/api/vms/{uuid}/status") + print("Finished Get /api/vms/{uuid}/status") assert response.status_code == 200, "Failed to get vm status" data = response.json() assert ( From fdcd7ad1d98afcecffa53e61f2d91b162f45871b Mon Sep 17 00:00:00 2001 From: Qubasa Date: Tue, 24 Oct 2023 16:54:10 +0200 Subject: [PATCH 03/36] Updated to main --- .gitignore | 1 + pkgs/clan-cli/clan_cli/__init__.py | 6 +- pkgs/clan-cli/clan_cli/async_cmd.py | 7 +- pkgs/clan-cli/clan_cli/config/__init__.py | 74 ++++----- pkgs/clan-cli/clan_cli/config/machine.py | 140 +++++++++++------- pkgs/clan-cli/clan_cli/dirs.py | 19 +++ .../clan_cli/{flake => flakes}/__init__.py | 8 +- .../clan_cli/{flake => flakes}/create.py | 15 +- pkgs/clan-cli/clan_cli/flakes/list.py | 27 ++++ pkgs/clan-cli/clan_cli/machines/__init__.py | 4 +- pkgs/clan-cli/clan_cli/machines/create.py | 32 +++- pkgs/clan-cli/clan_cli/machines/delete.py | 9 +- pkgs/clan-cli/clan_cli/machines/facts.py | 10 +- pkgs/clan-cli/clan_cli/machines/folders.py | 15 -- pkgs/clan-cli/clan_cli/machines/install.py | 11 +- pkgs/clan-cli/clan_cli/machines/list.py | 13 +- pkgs/clan-cli/clan_cli/machines/machines.py | 16 +- pkgs/clan-cli/clan_cli/machines/update.py | 21 ++- pkgs/clan-cli/clan_cli/secrets/generate.py | 2 +- pkgs/clan-cli/clan_cli/webui/api_inputs.py | 7 +- pkgs/clan-cli/clan_cli/webui/routers/flake.py | 5 +- .../clan_cli/webui/routers/machines.py | 32 ++-- pkgs/clan-cli/clan_cli/webui/routers/vms.py | 2 + 23 files changed, 296 insertions(+), 180 deletions(-) rename pkgs/clan-cli/clan_cli/{flake => flakes}/__init__.py (59%) rename pkgs/clan-cli/clan_cli/{flake => flakes}/create.py (84%) create mode 100644 pkgs/clan-cli/clan_cli/flakes/list.py delete mode 100644 pkgs/clan-cli/clan_cli/machines/folders.py diff --git a/.gitignore b/.gitignore index 62d5563e..c899ee71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .direnv +.coverage.* **/qubeclan **/testdir democlan diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 6f205736..68a07ae1 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -3,7 +3,7 @@ import sys from types import ModuleType from typing import Optional -from . import config, flake, join, machines, secrets, vms, webui +from . import config, flakes, join, machines, secrets, vms, webui from .ssh import cli as ssh_cli argcomplete: Optional[ModuleType] = None @@ -25,9 +25,9 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser: subparsers = parser.add_subparsers() parser_flake = subparsers.add_parser( - "flake", help="create a clan flake inside the current directory" + "flakes", help="create a clan flake inside the current directory" ) - flake.register_parser(parser_flake) + flakes.register_parser(parser_flake) parser_join = subparsers.add_parser("join", help="join a remote clan") join.register_parser(parser_join) diff --git a/pkgs/clan-cli/clan_cli/async_cmd.py b/pkgs/clan-cli/clan_cli/async_cmd.py index b3aa72b1..9befc98b 100644 --- a/pkgs/clan-cli/clan_cli/async_cmd.py +++ b/pkgs/clan-cli/clan_cli/async_cmd.py @@ -14,6 +14,7 @@ class CmdOut(NamedTuple): stderr: str cwd: Optional[Path] = None + async def run(cmd: list[str], cwd: Optional[Path] = None) -> CmdOut: log.debug(f"$: {shlex.join(cmd)}") cwd_res = None @@ -48,7 +49,9 @@ stdout: return CmdOut(stdout.decode("utf-8"), stderr.decode("utf-8"), cwd=cwd) -def runforcli(func: Callable[..., Coroutine[Any, Any, Dict[str, CmdOut]]], *args: Any) -> None: +def runforcli( + func: Callable[..., Coroutine[Any, Any, Dict[str, CmdOut]]], *args: Any +) -> None: try: res = asyncio.run(func(*args)) @@ -60,4 +63,4 @@ def runforcli(func: Callable[..., Coroutine[Any, Any, Dict[str, CmdOut]]], *args print(f"{name}: {out.stdout}", end="") except ClanError as e: print(e) - exit(1) \ No newline at end of file + exit(1) diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index 5182f263..1aff9102 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -9,10 +9,9 @@ import sys from pathlib import Path from typing import Any, Optional, Tuple, get_origin -from clan_cli.dirs import get_clan_flake_toplevel +from clan_cli.dirs import get_clan_flake_toplevel, machine_settings_file from clan_cli.errors import ClanError from clan_cli.git import commit_file -from clan_cli.machines.folders import machine_settings_file from clan_cli.nix import nix_eval script_dir = Path(__file__).parent @@ -154,6 +153,39 @@ def read_machine_option_value( return out +def get_or_set_option(args: argparse.Namespace) -> None: + if args.value == []: + print(read_machine_option_value(args.machine, args.option, args.show_trace)) + else: + # load options + if args.options_file is None: + options = options_for_machine( + machine_name=args.machine, show_trace=args.show_trace + ) + else: + with open(args.options_file) as f: + options = json.load(f) + # compute settings json file location + if args.settings_file is None: + get_clan_flake_toplevel() + settings_file = machine_settings_file(args.flake, args.machine) + else: + settings_file = args.settings_file + # set the option with the given value + set_option( + 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.machine, args.option) + print(f"New Value for {args.option}:") + print(new_value) + + def find_option( option: str, value: Any, options: dict, option_description: Optional[str] = None ) -> Tuple[str, Any]: @@ -258,38 +290,6 @@ def set_option( commit_file(settings_file, commit_message=f"Set option {option_description}") -def get_or_set_option(args: argparse.Namespace) -> None: - if args.value == []: - print(read_machine_option_value(args.machine, args.option, args.show_trace)) - else: - # load options - if args.options_file is None: - options = options_for_machine( - machine_name=args.machine, show_trace=args.show_trace - ) - else: - with open(args.options_file) as f: - options = json.load(f) - # compute settings json file location - if args.settings_file is None: - get_clan_flake_toplevel() - settings_file = machine_settings_file(args.machine) - else: - settings_file = args.settings_file - # set the option with the given value - set_option( - 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.machine, args.option) - print(f"New Value for {args.option}:") - print(new_value) - # takes a (sub)parser and configures it def register_parser( @@ -302,7 +302,11 @@ def register_parser( # inject callback function to process the input later parser.set_defaults(func=get_or_set_option) - + parser.add_argument( + "flake", + type=str, + help="name of the flake to set machine options for", + ) parser.add_argument( "--machine", "-m", diff --git a/pkgs/clan-cli/clan_cli/config/machine.py b/pkgs/clan-cli/clan_cli/config/machine.py index f2dc9d7c..adda7daf 100644 --- a/pkgs/clan-cli/clan_cli/config/machine.py +++ b/pkgs/clan-cli/clan_cli/config/machine.py @@ -3,14 +3,16 @@ import os import subprocess import sys from pathlib import Path -from tempfile import NamedTemporaryFile -from typing import Optional from fastapi import HTTPException -from clan_cli.dirs import get_clan_flake_toplevel, nixpkgs_source +from clan_cli.dirs import ( + get_flake_path, + machine_settings_file, + nixpkgs_source, + specific_machine_dir, +) from clan_cli.git import commit_file, find_git_repo_root -from clan_cli.machines.folders import machine_folder, machine_settings_file from clan_cli.nix import nix_eval @@ -52,26 +54,26 @@ def verify_machine_config( def config_for_machine(machine_name: str) -> dict: # read the config from a json file located at {flake}/machines/{machine_name}/settings.json - if not machine_folder(machine_name).exists(): + if not specific_machine_dir(flake_name, machine_name).exists(): raise HTTPException( status_code=404, detail=f"Machine {machine_name} not found. Create the machine first`", ) - settings_path = machine_settings_file(machine_name) + settings_path = machine_settings_file(flake_name, machine_name) if not settings_path.exists(): return {} with open(settings_path) as f: return json.load(f) -def set_config_for_machine(machine_name: str, config: dict) -> None: +def set_config_for_machine(flake_name: str, machine_name: str, config: dict) -> None: # write the config to a json file located at {flake}/machines/{machine_name}/settings.json - if not machine_folder(machine_name).exists(): + if not specific_machine_dir(flake_name, machine_name).exists(): raise HTTPException( status_code=404, detail=f"Machine {machine_name} not found. Create the machine first`", ) - settings_path = machine_settings_file(machine_name) + settings_path = machine_settings_file(flake_name, machine_name) settings_path.parent.mkdir(parents=True, exist_ok=True) with open(settings_path, "w") as f: json.dump(config, f) @@ -81,50 +83,76 @@ def set_config_for_machine(machine_name: str, config: dict) -> None: commit_file(settings_path, repo_dir) -def schema_for_machine( - machine_name: str, config: Optional[dict] = None, flake: Optional[Path] = None -) -> dict: - if flake is None: - flake = get_clan_flake_toplevel() - # use nix eval to lib.evalModules .#nixosConfigurations..options.clan - with NamedTemporaryFile(mode="w") as clan_machine_settings_file: - env = os.environ.copy() - inject_config_flags = [] - if config is not None: - json.dump(config, clan_machine_settings_file, indent=2) - clan_machine_settings_file.seek(0) - env["CLAN_MACHINE_SETTINGS_FILE"] = clan_machine_settings_file.name - inject_config_flags = [ - "--impure", # needed to access CLAN_MACHINE_SETTINGS_FILE - ] - proc = subprocess.run( - nix_eval( - flags=inject_config_flags - + [ - "--impure", - "--show-trace", - "--expr", - f""" - let - flake = builtins.getFlake (toString {flake}); - lib = import {nixpkgs_source()}/lib; - options = flake.nixosConfigurations.{machine_name}.options; - clanOptions = options.clan; - jsonschemaLib = import {Path(__file__).parent / "jsonschema"} {{ inherit lib; }}; - jsonschema = jsonschemaLib.parseOptions clanOptions; - in - jsonschema - """, - ], - ), - capture_output=True, - text=True, - cwd=flake, - env=env, - ) - if proc.returncode != 0: - print(proc.stderr, file=sys.stderr) - raise Exception( - f"Failed to read schema for machine {machine_name}:\n{proc.stderr}" - ) - return json.loads(proc.stdout) +def schema_for_machine(flake_name: str, machine_name: str) -> dict: + flake = get_flake_path(flake_name) + + # use nix eval to lib.evalModules .#nixosModules.machine-{machine_name} + proc = subprocess.run( + nix_eval( + flags=[ + "--impure", + "--show-trace", + "--expr", + f""" + let + flake = builtins.getFlake (toString {flake}); + lib = import {nixpkgs_source()}/lib; + options = flake.nixosConfigurations.{machine_name}.options; + clanOptions = options.clan; + jsonschemaLib = import {Path(__file__).parent / "jsonschema"} {{ inherit lib; }}; + jsonschema = jsonschemaLib.parseOptions clanOptions; + in + jsonschema + """, + ], + ), + capture_output=True, + text=True, + ) +# def schema_for_machine( +# machine_name: str, config: Optional[dict] = None, flake: Optional[Path] = None +# ) -> dict: +# if flake is None: +# flake = get_clan_flake_toplevel() +# # use nix eval to lib.evalModules .#nixosConfigurations..options.clan +# with NamedTemporaryFile(mode="w") as clan_machine_settings_file: +# env = os.environ.copy() +# inject_config_flags = [] +# if config is not None: +# json.dump(config, clan_machine_settings_file, indent=2) +# clan_machine_settings_file.seek(0) +# env["CLAN_MACHINE_SETTINGS_FILE"] = clan_machine_settings_file.name +# inject_config_flags = [ +# "--impure", # needed to access CLAN_MACHINE_SETTINGS_FILE +# ] +# proc = subprocess.run( +# nix_eval( +# flags=inject_config_flags +# + [ +# "--impure", +# "--show-trace", +# "--expr", +# f""" +# let +# flake = builtins.getFlake (toString {flake}); +# lib = import {nixpkgs_source()}/lib; +# options = flake.nixosConfigurations.{machine_name}.options; +# clanOptions = options.clan; +# jsonschemaLib = import {Path(__file__).parent / "jsonschema"} {{ inherit lib; }}; +# jsonschema = jsonschemaLib.parseOptions clanOptions; +# in +# jsonschema +# """, +# ], +# ), +# capture_output=True, +# text=True, +# cwd=flake, +# env=env, +# ) +# if proc.returncode != 0: +# print(proc.stderr, file=sys.stderr) +# raise Exception( +# f"Failed to read schema for machine {machine_name}:\n{proc.stderr}" +# ) +# return json.loads(proc.stdout) diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index e4daf184..8ada2f12 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -68,6 +68,25 @@ def clan_flake_dir() -> Path: return path.resolve() +def get_flake_path(name: str) -> Path: + flake_dir = clan_flake_dir() / name + if not flake_dir.exists(): + raise ClanError(f"Flake {name} does not exist") + return flake_dir + + +def machines_dir(flake_name: str) -> Path: + return get_flake_path(flake_name) / "machines" + + +def specific_machine_dir(flake_name: str, machine: str) -> Path: + return machines_dir(flake_name) / machine + + +def machine_settings_file(flake_name: str, machine: str) -> Path: + return specific_machine_dir(flake_name, machine) / "settings.json" + + def module_root() -> Path: return Path(__file__).parent diff --git a/pkgs/clan-cli/clan_cli/flake/__init__.py b/pkgs/clan-cli/clan_cli/flakes/__init__.py similarity index 59% rename from pkgs/clan-cli/clan_cli/flake/__init__.py rename to pkgs/clan-cli/clan_cli/flakes/__init__.py index 8756e3c8..628586cf 100644 --- a/pkgs/clan-cli/clan_cli/flake/__init__.py +++ b/pkgs/clan-cli/clan_cli/flakes/__init__.py @@ -2,6 +2,7 @@ import argparse from .create import register_create_parser +from .list import register_list_parser # takes a (sub)parser and configures it @@ -12,5 +13,8 @@ def register_parser(parser: argparse.ArgumentParser) -> None: help="the command to run", required=True, ) - update_parser = subparser.add_parser("create", help="Create a clan flake") - register_create_parser(update_parser) + create_parser = subparser.add_parser("create", help="Create a clan flake") + register_create_parser(create_parser) + + list_parser = subparser.add_parser("list", help="List clan flakes") + register_list_parser(list_parser) diff --git a/pkgs/clan-cli/clan_cli/flake/create.py b/pkgs/clan-cli/clan_cli/flakes/create.py similarity index 84% rename from pkgs/clan-cli/clan_cli/flake/create.py rename to pkgs/clan-cli/clan_cli/flakes/create.py index 07e5a52e..343d33ab 100644 --- a/pkgs/clan-cli/clan_cli/flake/create.py +++ b/pkgs/clan-cli/clan_cli/flakes/create.py @@ -7,9 +7,12 @@ from pydantic import AnyUrl from pydantic.tools import parse_obj_as from ..async_cmd import CmdOut, run, runforcli +from ..dirs import clan_flake_dir from ..nix import nix_command, nix_shell -DEFAULT_URL: AnyUrl = parse_obj_as(AnyUrl, "git+https://git.clan.lol/clan/clan-core#new-clan") +DEFAULT_URL: AnyUrl = parse_obj_as( + AnyUrl, "git+https://git.clan.lol/clan/clan-core#new-clan" +) async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: @@ -51,16 +54,16 @@ async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: def create_flake_command(args: argparse.Namespace) -> None: - runforcli(create_flake, args.directory, DEFAULT_URL) - + flake_dir = clan_flake_dir() / args.name + runforcli(create_flake, flake_dir, DEFAULT_URL) # takes a (sub)parser and configures it def register_create_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( - "directory", - type=Path, - help="output directory for the flake", + "name", + type=str, + help="name for the flake", ) # parser.add_argument("name", type=str, help="name of the flake") parser.set_defaults(func=create_flake_command) diff --git a/pkgs/clan-cli/clan_cli/flakes/list.py b/pkgs/clan-cli/clan_cli/flakes/list.py new file mode 100644 index 00000000..8aa211a7 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/flakes/list.py @@ -0,0 +1,27 @@ +import argparse +import logging +import os + +from ..dirs import clan_flake_dir + +log = logging.getLogger(__name__) + + +def list_flakes() -> list[str]: + path = clan_flake_dir() + log.debug(f"Listing machines in {path}") + if not path.exists(): + return [] + objs: list[str] = [] + for f in os.listdir(path): + objs.append(f) + return objs + + +def list_command(args: argparse.Namespace) -> None: + for flake in list_flakes(): + print(flake) + + +def register_list_parser(parser: argparse.ArgumentParser) -> None: + parser.set_defaults(func=list_command) diff --git a/pkgs/clan-cli/clan_cli/machines/__init__.py b/pkgs/clan-cli/clan_cli/machines/__init__.py index 4c9b15f7..3a3c9a5b 100644 --- a/pkgs/clan-cli/clan_cli/machines/__init__.py +++ b/pkgs/clan-cli/clan_cli/machines/__init__.py @@ -23,8 +23,8 @@ def register_parser(parser: argparse.ArgumentParser) -> None: create_parser = subparser.add_parser("create", help="Create a machine") register_create_parser(create_parser) - remove_parser = subparser.add_parser("remove", help="Remove a machine") - register_delete_parser(remove_parser) + delete_parser = subparser.add_parser("delete", help="Delete a machine") + register_delete_parser(delete_parser) list_parser = subparser.add_parser("list", help="List machines") register_list_parser(list_parser) diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py index b8c7ea9b..9575acea 100644 --- a/pkgs/clan-cli/clan_cli/machines/create.py +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -3,31 +3,49 @@ import logging from typing import Dict from ..async_cmd import CmdOut, run, runforcli +from ..dirs import get_flake_path, specific_machine_dir +from ..errors import ClanError from ..nix import nix_shell -from .folders import machine_folder log = logging.getLogger(__name__) -async def create_machine(name: str) -> Dict[str, CmdOut]: - folder = machine_folder(name) + +async def create_machine(flake_name: str, machine_name: str) -> Dict[str, CmdOut]: + folder = specific_machine_dir(flake_name, machine_name) folder.mkdir(parents=True, exist_ok=True) # create empty settings.json file inside the folder with open(folder / "settings.json", "w") as f: f.write("{}") response = {} - out = await run(nix_shell(["git"], ["git", "add", str(folder)])) + out = await run(nix_shell(["git"], ["git", "add", str(folder)]), cwd=folder) response["git add"] = out - out = await run(nix_shell(["git"], ["git", "commit", "-m", f"Added machine {name}", str(folder)])) + out = await run( + nix_shell( + ["git"], + ["git", "commit", "-m", f"Added machine {machine_name}", str(folder)], + ), + cwd=folder, + ) response["git commit"] = out return response + def create_command(args: argparse.Namespace) -> None: - runforcli(create_machine, args.host) + try: + flake_dir = get_flake_path(args.flake) + runforcli(create_machine, flake_dir, args.machine) + except ClanError as e: + print(e) def register_create_parser(parser: argparse.ArgumentParser) -> None: - parser.add_argument("host", type=str) + parser.add_argument("machine", type=str) + parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser.set_defaults(func=create_command) diff --git a/pkgs/clan-cli/clan_cli/machines/delete.py b/pkgs/clan-cli/clan_cli/machines/delete.py index 6fd5cf6e..e772edc6 100644 --- a/pkgs/clan-cli/clan_cli/machines/delete.py +++ b/pkgs/clan-cli/clan_cli/machines/delete.py @@ -1,12 +1,12 @@ import argparse import shutil +from ..dirs import specific_machine_dir from ..errors import ClanError -from .folders import machine_folder def delete_command(args: argparse.Namespace) -> None: - folder = machine_folder(args.host) + folder = specific_machine_dir(args.flake, args.host) if folder.exists(): shutil.rmtree(folder) else: @@ -15,4 +15,9 @@ def delete_command(args: argparse.Namespace) -> None: def register_delete_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument("host", type=str) + parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser.set_defaults(func=delete_command) diff --git a/pkgs/clan-cli/clan_cli/machines/facts.py b/pkgs/clan-cli/clan_cli/machines/facts.py index 9c402621..ed471ebc 100644 --- a/pkgs/clan-cli/clan_cli/machines/facts.py +++ b/pkgs/clan-cli/clan_cli/machines/facts.py @@ -1,9 +1,9 @@ -from .folders import machine_folder +from ..dirs import specific_machine_dir -def machine_has_fact(machine: str, fact: str) -> bool: - return (machine_folder(machine) / "facts" / fact).exists() +def machine_has_fact(flake_name: str, machine: str, fact: str) -> bool: + return (specific_machine_dir(flake_name, machine) / "facts" / fact).exists() -def machine_get_fact(machine: str, fact: str) -> str: - return (machine_folder(machine) / "facts" / fact).read_text() +def machine_get_fact(flake_name: str, machine: str, fact: str) -> str: + return (specific_machine_dir(flake_name, machine) / "facts" / fact).read_text() diff --git a/pkgs/clan-cli/clan_cli/machines/folders.py b/pkgs/clan-cli/clan_cli/machines/folders.py deleted file mode 100644 index a7e010ec..00000000 --- a/pkgs/clan-cli/clan_cli/machines/folders.py +++ /dev/null @@ -1,15 +0,0 @@ -from pathlib import Path - -from ..dirs import get_clan_flake_toplevel - - -def machines_folder() -> Path: - return get_clan_flake_toplevel() / "machines" - - -def machine_folder(machine: str) -> Path: - return machines_folder() / machine - - -def machine_settings_file(machine: str) -> Path: - return machine_folder(machine) / "settings.json" diff --git a/pkgs/clan-cli/clan_cli/machines/install.py b/pkgs/clan-cli/clan_cli/machines/install.py index a014b58e..718a1b1f 100644 --- a/pkgs/clan-cli/clan_cli/machines/install.py +++ b/pkgs/clan-cli/clan_cli/machines/install.py @@ -3,6 +3,7 @@ import subprocess from pathlib import Path from tempfile import TemporaryDirectory +from ..dirs import get_flake_path from ..machines.machines import Machine from ..nix import nix_shell from ..secrets.generate import generate_secrets @@ -26,7 +27,7 @@ def install_nixos(machine: Machine) -> None: [ "nixos-anywhere", "-f", - f"{machine.clan_dir}#{flake_attr}", + f"{machine.flake_dir}#{flake_attr}", "-t", "--no-reboot", "--extra-files", @@ -39,7 +40,7 @@ def install_nixos(machine: Machine) -> None: def install_command(args: argparse.Namespace) -> None: - machine = Machine(args.machine) + machine = Machine(args.machine, flake_dir=get_flake_path(args.flake)) machine.deployment_address = args.target_host install_nixos(machine) @@ -56,5 +57,9 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None: type=str, help="ssh address to install to in the form of user@host:2222", ) - + parser.add_argument( + "flake", + type=str, + help="name of the flake to install machine from", + ) parser.set_defaults(func=install_command) diff --git a/pkgs/clan-cli/clan_cli/machines/list.py b/pkgs/clan-cli/clan_cli/machines/list.py index ae8b1d3b..3558967a 100644 --- a/pkgs/clan-cli/clan_cli/machines/list.py +++ b/pkgs/clan-cli/clan_cli/machines/list.py @@ -2,14 +2,14 @@ import argparse import logging import os -from .folders import machines_folder +from ..dirs import machines_dir from .types import validate_hostname log = logging.getLogger(__name__) -def list_machines() -> list[str]: - path = machines_folder() +def list_machines(flake_name: str) -> list[str]: + path = machines_dir(flake_name) log.debug(f"Listing machines in {path}") if not path.exists(): return [] @@ -21,9 +21,14 @@ def list_machines() -> list[str]: def list_command(args: argparse.Namespace) -> None: - for machine in list_machines(): + for machine in list_machines(args.flake): print(machine) def register_list_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser.set_defaults(func=list_command) diff --git a/pkgs/clan-cli/clan_cli/machines/machines.py b/pkgs/clan-cli/clan_cli/machines/machines.py index ee657b98..9224fccd 100644 --- a/pkgs/clan-cli/clan_cli/machines/machines.py +++ b/pkgs/clan-cli/clan_cli/machines/machines.py @@ -31,7 +31,7 @@ class Machine: def __init__( self, name: str, - clan_dir: Optional[Path] = None, + flake_dir: Optional[Path] = None, machine_data: Optional[dict] = None, ) -> None: """ @@ -41,13 +41,13 @@ class Machine: @machine_json: can be optionally used to skip evaluation of the machine, location of the json file with machine data """ self.name = name - if clan_dir is None: - self.clan_dir = get_clan_flake_toplevel() + if flake_dir is None: + self.flake_dir = get_clan_flake_toplevel() else: - self.clan_dir = clan_dir + self.flake_dir = flake_dir if machine_data is None: - self.machine_data = build_machine_data(name, self.clan_dir) + self.machine_data = build_machine_data(name, self.flake_dir) else: self.machine_data = machine_data @@ -68,7 +68,7 @@ class Machine: @secrets_dir: the directory to store the secrets in """ env = os.environ.copy() - env["CLAN_DIR"] = str(self.clan_dir) + env["CLAN_DIR"] = str(self.flake_dir) env["PYTHONPATH"] = str( ":".join(sys.path) ) # TODO do this in the clanCore module @@ -95,7 +95,7 @@ class Machine: @attr: the attribute to get """ output = subprocess.run( - nix_eval([f"path:{self.clan_dir}#{attr}"]), + nix_eval([f"path:{self.flake_dir}#{attr}"]), stdout=subprocess.PIPE, check=True, text=True, @@ -108,7 +108,7 @@ class Machine: @attr: the attribute to get """ outpath = subprocess.run( - nix_build([f"path:{self.clan_dir}#{attr}"]), + nix_build([f"path:{self.flake_dir}#{attr}"]), stdout=subprocess.PIPE, check=True, text=True, diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index f8ee808e..c7d02572 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -4,7 +4,7 @@ import os import subprocess from pathlib import Path -from ..dirs import get_clan_flake_toplevel +from ..dirs import get_flake_path from ..machines.machines import Machine from ..nix import nix_build, nix_command, nix_config from ..secrets.generate import generate_secrets @@ -101,19 +101,19 @@ def get_all_machines(clan_dir: Path) -> HostGroup: return HostGroup(hosts) -def get_selected_machines(machine_names: list[str], clan_dir: Path) -> HostGroup: +def get_selected_machines(machine_names: list[str], flake_dir: Path) -> HostGroup: hosts = [] for name in machine_names: - machine = Machine(name=name, clan_dir=clan_dir) + machine = Machine(name=name, flake_dir=flake_dir) hosts.append(machine.host) return HostGroup(hosts) # FIXME: we want some kind of inventory here. def update(args: argparse.Namespace) -> None: - clan_dir = get_clan_flake_toplevel() + flake_dir = get_flake_path(args.flake) if len(args.machines) == 1 and args.target_host is not None: - machine = Machine(name=args.machines[0], clan_dir=clan_dir) + machine = Machine(name=args.machines[0], flake_dir=flake_dir) machine.deployment_address = args.target_host host = parse_deployment_address( args.machines[0], @@ -127,11 +127,11 @@ def update(args: argparse.Namespace) -> None: exit(1) else: if len(args.machines) == 0: - machines = get_all_machines(clan_dir) + machines = get_all_machines(flake_dir) else: - machines = get_selected_machines(args.machines, clan_dir) + machines = get_selected_machines(args.machines, flake_dir) - deploy_nixos(machines, clan_dir) + deploy_nixos(machines, flake_dir) def register_update_parser(parser: argparse.ArgumentParser) -> None: @@ -142,6 +142,11 @@ def register_update_parser(parser: argparse.ArgumentParser) -> None: nargs="*", default=[], ) + parser.add_argument( + "flake", + type=str, + help="name of the flake to update machine for", + ) parser.add_argument( "--target-host", type=str, diff --git a/pkgs/clan-cli/clan_cli/secrets/generate.py b/pkgs/clan-cli/clan_cli/secrets/generate.py index bb6ced9c..0dea6e33 100644 --- a/pkgs/clan-cli/clan_cli/secrets/generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/generate.py @@ -13,7 +13,7 @@ log = logging.getLogger(__name__) def generate_secrets(machine: Machine) -> None: env = os.environ.copy() - env["CLAN_DIR"] = str(machine.clan_dir) + env["CLAN_DIR"] = str(machine.flake_dir) env["PYTHONPATH"] = ":".join(sys.path) # TODO do this in the clanCore module print(f"generating secrets... {machine.generate_secrets}") diff --git a/pkgs/clan-cli/clan_cli/webui/api_inputs.py b/pkgs/clan-cli/clan_cli/webui/api_inputs.py index b22432f8..618a6bdb 100644 --- a/pkgs/clan-cli/clan_cli/webui/api_inputs.py +++ b/pkgs/clan-cli/clan_cli/webui/api_inputs.py @@ -1,4 +1,3 @@ -# mypy: ignore-errors import logging from pathlib import Path from typing import Any @@ -6,7 +5,7 @@ from typing import Any from pydantic import AnyUrl, BaseModel, validator from ..dirs import clan_data_dir, clan_flake_dir -from ..flake.create import DEFAULT_URL +from ..flakes.create import DEFAULT_URL log = logging.getLogger(__name__) @@ -30,7 +29,7 @@ class ClanDataPath(BaseModel): dest: Path @validator("dest") - def check_data_path(cls: Any, v: Path) -> Path: # type: ignore + def check_data_path(cls: Any, v: Path) -> Path: # noqa return validate_path(clan_data_dir(), v) @@ -38,7 +37,7 @@ class ClanFlakePath(BaseModel): dest: Path @validator("dest") - def check_dest(cls: Any, v: Path) -> Path: # type: ignore + def check_dest(cls: Any, v: Path) -> Path: # noqa return validate_path(clan_flake_dir(), v) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/flake.py b/pkgs/clan-cli/clan_cli/webui/routers/flake.py index 6eedf363..6e079772 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/flake.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/flake.py @@ -17,11 +17,12 @@ from clan_cli.webui.api_outputs import ( ) from ...async_cmd import run -from ...flake import create +from ...flakes import create from ...nix import nix_command, nix_flake_show router = APIRouter() + # TODO: Check for directory traversal async def get_attrs(url: AnyUrl | Path) -> list[str]: cmd = nix_flake_show(url) @@ -42,6 +43,7 @@ async def get_attrs(url: AnyUrl | Path) -> list[str]: ) return flake_attrs + # TODO: Check for directory traversal @router.get("/api/flake/attrs") async def inspect_flake_attrs(url: AnyUrl | Path) -> FlakeAttrResponse: @@ -74,7 +76,6 @@ async def inspect_flake( return FlakeResponse(content=content, actions=actions) - @router.post("/api/flake/create", status_code=status.HTTP_201_CREATED) async def create_flake( args: Annotated[FlakeCreateInput, Body()], diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index 656f991c..0b6fd281 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -27,17 +27,19 @@ log = logging.getLogger(__name__) router = APIRouter() -@router.get("/api/machines") -async def list_machines() -> MachinesResponse: +@router.get("/api/{flake_name}/machines") +async def list_machines(flake_name: str) -> MachinesResponse: machines = [] - for m in _list_machines(): + for m in _list_machines(flake_name): machines.append(Machine(name=m, status=Status.UNKNOWN)) return MachinesResponse(machines=machines) -@router.post("/api/machines", status_code=201) -async def create_machine(machine: Annotated[MachineCreate, Body()]) -> MachineResponse: - out = await _create_machine(machine.name) +@router.post("/api/{flake_name}/machines", status_code=201) +async def create_machine( + flake_name: str, machine: Annotated[MachineCreate, Body()] +) -> MachineResponse: + out = await _create_machine(flake_name, machine.name) log.debug(out) return MachineResponse(machine=Machine(name=machine.name, status=Status.UNKNOWN)) @@ -48,23 +50,23 @@ async def get_machine(name: str) -> MachineResponse: return MachineResponse(machine=Machine(name=name, status=Status.UNKNOWN)) -@router.get("/api/machines/{name}/config") -async def get_machine_config(name: str) -> ConfigResponse: - config = config_for_machine(name) +@router.get("/api/{flake_name}/machines/{name}/config") +async def get_machine_config(flake_name: str, name: str) -> ConfigResponse: + config = config_for_machine(flake_name, name) return ConfigResponse(config=config) -@router.put("/api/machines/{name}/config") +@router.put("/api/{flake_name}/machines/{name}/config") async def set_machine_config( - name: str, config: Annotated[dict, Body()] + flake_name: str, name: str, config: Annotated[dict, Body()] ) -> ConfigResponse: - set_config_for_machine(name, config) + set_config_for_machine(flake_name, name, config) return ConfigResponse(config=config) -@router.get("/api/machines/{name}/schema") -async def get_machine_schema(name: str) -> SchemaResponse: - schema = schema_for_machine(name) +@router.get("/api/{flake_name}/machines/{name}/schema") +async def get_machine_schema(flake_name: str, name: str) -> SchemaResponse: + schema = schema_for_machine(flake_name, name) return SchemaResponse(schema=schema) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/vms.py b/pkgs/clan-cli/clan_cli/webui/routers/vms.py index 414ed39d..df3e464d 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/vms.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/vms.py @@ -22,6 +22,7 @@ from ..api_outputs import ( log = logging.getLogger(__name__) router = APIRouter() + # TODO: Check for directory traversal @router.post("/api/vms/inspect") async def inspect_vm( @@ -52,6 +53,7 @@ async def get_vm_logs(uuid: UUID) -> StreamingResponse: media_type="text/plain", ) + # TODO: Check for directory traversal @router.post("/api/vms/create") async def create_vm(vm: Annotated[VmConfig, Body()]) -> VmCreateResponse: From 32e60f5adced5b8062dba18dee0e80abc0005d67 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Sat, 14 Oct 2023 14:57:36 +0200 Subject: [PATCH 04/36] Added flake_name:str argument everywhere, nix fmt doesn't complain anymore --- pkgs/clan-cli/clan_cli/config/__init__.py | 21 +++-- pkgs/clan-cli/clan_cli/config/machine.py | 4 +- pkgs/clan-cli/clan_cli/dirs.py | 10 +-- pkgs/clan-cli/clan_cli/flakes/create.py | 4 +- pkgs/clan-cli/clan_cli/flakes/list.py | 4 +- pkgs/clan-cli/clan_cli/machines/create.py | 4 +- pkgs/clan-cli/clan_cli/machines/install.py | 4 +- pkgs/clan-cli/clan_cli/machines/machines.py | 8 +- pkgs/clan-cli/clan_cli/machines/update.py | 10 ++- pkgs/clan-cli/clan_cli/secrets/folders.py | 12 +-- pkgs/clan-cli/clan_cli/secrets/generate.py | 8 +- pkgs/clan-cli/clan_cli/secrets/groups.py | 86 +++++++++++-------- pkgs/clan-cli/clan_cli/secrets/import_sops.py | 5 +- pkgs/clan-cli/clan_cli/secrets/machines.py | 80 ++++++++++++----- pkgs/clan-cli/clan_cli/secrets/secrets.py | 84 ++++++++++++------ pkgs/clan-cli/clan_cli/secrets/sops.py | 16 ++-- .../clan_cli/secrets/sops_generate.py | 39 ++++++--- pkgs/clan-cli/clan_cli/secrets/upload.py | 8 +- pkgs/clan-cli/clan_cli/secrets/users.py | 58 +++++++++---- pkgs/clan-cli/clan_cli/vms/create.py | 9 +- pkgs/clan-cli/clan_cli/vms/inspect.py | 9 +- pkgs/clan-cli/clan_cli/webui/api_inputs.py | 4 +- pkgs/clan-cli/tests/fixtures_flakes.py | 27 +++--- pkgs/clan-cli/tests/test_dirs.py | 8 +- pkgs/clan-cli/tests/test_machines_config.py | 6 +- pkgs/clan-cli/tests/test_secrets_generate.py | 24 ++++-- .../tests/test_secrets_password_store.py | 13 +-- pkgs/clan-cli/tests/test_vms_api_create.py | 6 +- 28 files changed, 365 insertions(+), 206 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index 1aff9102..ee5ebafc 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -9,7 +9,7 @@ import sys from pathlib import Path from typing import Any, Optional, Tuple, get_origin -from clan_cli.dirs import get_clan_flake_toplevel, machine_settings_file +from clan_cli.dirs import machine_settings_file, specific_flake_dir from clan_cli.errors import ClanError from clan_cli.git import commit_file from clan_cli.nix import nix_eval @@ -103,8 +103,10 @@ def cast(value: Any, type: Any, opt_description: str) -> Any: ) -def options_for_machine(machine_name: str, show_trace: bool = False) -> dict: - clan_dir = get_clan_flake_toplevel() +def options_for_machine( + flake_name: str, machine_name: str, show_trace: bool = False +) -> dict: + clan_dir = specific_flake_dir(flake_name) flags = [] if show_trace: flags.append("--show-trace") @@ -125,9 +127,9 @@ def options_for_machine(machine_name: str, show_trace: bool = False) -> dict: def read_machine_option_value( - machine_name: str, option: str, show_trace: bool = False + flake_name: str, machine_name: str, option: str, show_trace: bool = False ) -> str: - clan_dir = get_clan_flake_toplevel() + clan_dir = specific_flake_dir(flake_name) # use nix eval to read from .#nixosConfigurations.default.config.{option} # this will give us the evaluated config with the options attribute cmd = nix_eval( @@ -160,19 +162,19 @@ def get_or_set_option(args: argparse.Namespace) -> None: # load options if args.options_file is None: options = options_for_machine( - machine_name=args.machine, show_trace=args.show_trace + args.flake, machine_name=args.machine, show_trace=args.show_trace ) else: with open(args.options_file) as f: options = json.load(f) # compute settings json file location if args.settings_file is None: - get_clan_flake_toplevel() settings_file = machine_settings_file(args.flake, args.machine) else: settings_file = args.settings_file # set the option with the given value set_option( + flake_name=args.flake, option=args.option, value=args.value, options=options, @@ -181,7 +183,7 @@ def get_or_set_option(args: argparse.Namespace) -> None: show_trace=args.show_trace, ) if not args.quiet: - new_value = read_machine_option_value(args.machine, args.option) + new_value = read_machine_option_value(args.flake, args.machine, args.option) print(f"New Value for {args.option}:") print(new_value) @@ -238,6 +240,7 @@ def find_option( def set_option( + flake_name: str, option: str, value: Any, options: dict, @@ -286,7 +289,7 @@ def set_option( 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(get_clan_flake_toplevel()): + if settings_file.resolve().is_relative_to(specific_flake_dir(flake_name)): commit_file(settings_file, 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 index adda7daf..e1561731 100644 --- a/pkgs/clan-cli/clan_cli/config/machine.py +++ b/pkgs/clan-cli/clan_cli/config/machine.py @@ -7,9 +7,9 @@ from pathlib import Path from fastapi import HTTPException from clan_cli.dirs import ( - get_flake_path, machine_settings_file, nixpkgs_source, + specific_flake_dir, specific_machine_dir, ) from clan_cli.git import commit_file, find_git_repo_root @@ -84,7 +84,7 @@ def set_config_for_machine(flake_name: str, machine_name: str, config: dict) -> def schema_for_machine(flake_name: str, machine_name: str) -> dict: - flake = get_flake_path(flake_name) + flake = specific_flake_dir(flake_name) # use nix eval to lib.evalModules .#nixosModules.machine-{machine_name} proc = subprocess.run( diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index 8ada2f12..1ea596b1 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -6,7 +6,7 @@ from typing import Optional from .errors import ClanError -def get_clan_flake_toplevel() -> Path: +def _get_clan_flake_toplevel() -> Path: return find_toplevel([".clan-flake", ".git", ".hg", ".svn", "flake.nix"]) @@ -61,22 +61,22 @@ def clan_config_dir() -> Path: return path.resolve() -def clan_flake_dir() -> Path: +def clan_flakes_dir() -> Path: path = clan_data_dir() / "flake" if not path.exists(): path.mkdir() return path.resolve() -def get_flake_path(name: str) -> Path: - flake_dir = clan_flake_dir() / name +def specific_flake_dir(name: str) -> Path: + flake_dir = clan_flakes_dir() / name if not flake_dir.exists(): raise ClanError(f"Flake {name} does not exist") return flake_dir def machines_dir(flake_name: str) -> Path: - return get_flake_path(flake_name) / "machines" + return specific_flake_dir(flake_name) / "machines" def specific_machine_dir(flake_name: str, machine: str) -> Path: diff --git a/pkgs/clan-cli/clan_cli/flakes/create.py b/pkgs/clan-cli/clan_cli/flakes/create.py index 343d33ab..e35626d3 100644 --- a/pkgs/clan-cli/clan_cli/flakes/create.py +++ b/pkgs/clan-cli/clan_cli/flakes/create.py @@ -7,7 +7,7 @@ from pydantic import AnyUrl from pydantic.tools import parse_obj_as from ..async_cmd import CmdOut, run, runforcli -from ..dirs import clan_flake_dir +from ..dirs import clan_flakes_dir from ..nix import nix_command, nix_shell DEFAULT_URL: AnyUrl = parse_obj_as( @@ -54,7 +54,7 @@ async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: def create_flake_command(args: argparse.Namespace) -> None: - flake_dir = clan_flake_dir() / args.name + flake_dir = clan_flakes_dir() / args.name runforcli(create_flake, flake_dir, DEFAULT_URL) diff --git a/pkgs/clan-cli/clan_cli/flakes/list.py b/pkgs/clan-cli/clan_cli/flakes/list.py index 8aa211a7..bec34ef5 100644 --- a/pkgs/clan-cli/clan_cli/flakes/list.py +++ b/pkgs/clan-cli/clan_cli/flakes/list.py @@ -2,13 +2,13 @@ import argparse import logging import os -from ..dirs import clan_flake_dir +from ..dirs import clan_flakes_dir log = logging.getLogger(__name__) def list_flakes() -> list[str]: - path = clan_flake_dir() + path = clan_flakes_dir() log.debug(f"Listing machines in {path}") if not path.exists(): return [] diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py index 9575acea..f96beead 100644 --- a/pkgs/clan-cli/clan_cli/machines/create.py +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -3,7 +3,7 @@ import logging from typing import Dict from ..async_cmd import CmdOut, run, runforcli -from ..dirs import get_flake_path, specific_machine_dir +from ..dirs import specific_flake_dir, specific_machine_dir from ..errors import ClanError from ..nix import nix_shell @@ -35,7 +35,7 @@ async def create_machine(flake_name: str, machine_name: str) -> Dict[str, CmdOut def create_command(args: argparse.Namespace) -> None: try: - flake_dir = get_flake_path(args.flake) + flake_dir = specific_flake_dir(args.flake) runforcli(create_machine, flake_dir, args.machine) except ClanError as e: print(e) diff --git a/pkgs/clan-cli/clan_cli/machines/install.py b/pkgs/clan-cli/clan_cli/machines/install.py index 718a1b1f..cf09f956 100644 --- a/pkgs/clan-cli/clan_cli/machines/install.py +++ b/pkgs/clan-cli/clan_cli/machines/install.py @@ -3,7 +3,7 @@ import subprocess from pathlib import Path from tempfile import TemporaryDirectory -from ..dirs import get_flake_path +from ..dirs import specific_flake_dir from ..machines.machines import Machine from ..nix import nix_shell from ..secrets.generate import generate_secrets @@ -40,7 +40,7 @@ def install_nixos(machine: Machine) -> None: def install_command(args: argparse.Namespace) -> None: - machine = Machine(args.machine, flake_dir=get_flake_path(args.flake)) + machine = Machine(args.machine, flake_dir=specific_flake_dir(args.flake)) machine.deployment_address = args.target_host install_nixos(machine) diff --git a/pkgs/clan-cli/clan_cli/machines/machines.py b/pkgs/clan-cli/clan_cli/machines/machines.py index 9224fccd..db6b974e 100644 --- a/pkgs/clan-cli/clan_cli/machines/machines.py +++ b/pkgs/clan-cli/clan_cli/machines/machines.py @@ -5,7 +5,6 @@ import sys from pathlib import Path from typing import Optional -from ..dirs import get_clan_flake_toplevel from ..nix import nix_build, nix_config, nix_eval from ..ssh import Host, parse_deployment_address @@ -31,7 +30,7 @@ class Machine: def __init__( self, name: str, - flake_dir: Optional[Path] = None, + flake_dir: Path, machine_data: Optional[dict] = None, ) -> None: """ @@ -41,10 +40,7 @@ class Machine: @machine_json: can be optionally used to skip evaluation of the machine, location of the json file with machine data """ self.name = name - if flake_dir is None: - self.flake_dir = get_clan_flake_toplevel() - else: - self.flake_dir = flake_dir + self.flake_dir = flake_dir if machine_data is None: self.machine_data = build_machine_data(name, self.flake_dir) diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index c7d02572..900b3e89 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -4,7 +4,7 @@ import os import subprocess from pathlib import Path -from ..dirs import get_flake_path +from ..dirs import specific_flake_dir from ..machines.machines import Machine from ..nix import nix_build, nix_command, nix_config from ..secrets.generate import generate_secrets @@ -95,7 +95,11 @@ def get_all_machines(clan_dir: Path) -> HostGroup: host = parse_deployment_address( name, machine_data["deploymentAddress"], - meta={"machine": Machine(name=name, machine_data=machine_data)}, + meta={ + "machine": Machine( + name=name, flake_dir=clan_dir, machine_data=machine_data + ) + }, ) hosts.append(host) return HostGroup(hosts) @@ -111,7 +115,7 @@ def get_selected_machines(machine_names: list[str], flake_dir: Path) -> HostGrou # FIXME: we want some kind of inventory here. def update(args: argparse.Namespace) -> None: - flake_dir = get_flake_path(args.flake) + flake_dir = specific_flake_dir(args.flake) if len(args.machines) == 1 and args.target_host is not None: machine = Machine(name=args.machines[0], flake_dir=flake_dir) machine.deployment_address = args.target_host diff --git a/pkgs/clan-cli/clan_cli/secrets/folders.py b/pkgs/clan-cli/clan_cli/secrets/folders.py index f9e8d31e..4659a7ff 100644 --- a/pkgs/clan-cli/clan_cli/secrets/folders.py +++ b/pkgs/clan-cli/clan_cli/secrets/folders.py @@ -3,17 +3,17 @@ import shutil from pathlib import Path from typing import Callable -from ..dirs import get_clan_flake_toplevel +from ..dirs import specific_flake_dir from ..errors import ClanError -def get_sops_folder() -> Path: - return get_clan_flake_toplevel() / "sops" +def get_sops_folder(flake_name: str) -> Path: + return specific_flake_dir(flake_name) / "sops" -def gen_sops_subfolder(subdir: str) -> Callable[[], Path]: - def folder() -> Path: - return get_clan_flake_toplevel() / "sops" / subdir +def gen_sops_subfolder(subdir: str) -> Callable[[str], Path]: + def folder(flake_name: str) -> Path: + return specific_flake_dir(flake_name) / "sops" / subdir return folder diff --git a/pkgs/clan-cli/clan_cli/secrets/generate.py b/pkgs/clan-cli/clan_cli/secrets/generate.py index 0dea6e33..d2992998 100644 --- a/pkgs/clan-cli/clan_cli/secrets/generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/generate.py @@ -6,6 +6,7 @@ import sys from clan_cli.errors import ClanError +from ..dirs import specific_flake_dir from ..machines.machines import Machine log = logging.getLogger(__name__) @@ -29,7 +30,7 @@ def generate_secrets(machine: Machine) -> None: def generate_command(args: argparse.Namespace) -> None: - machine = Machine(args.machine) + machine = Machine(name=args.machine, flake_dir=specific_flake_dir(args.flake)) generate_secrets(machine) @@ -38,4 +39,9 @@ def register_generate_parser(parser: argparse.ArgumentParser) -> None: "machine", help="The machine to generate secrets for", ) + parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser.set_defaults(func=generate_command) diff --git a/pkgs/clan-cli/clan_cli/secrets/groups.py b/pkgs/clan-cli/clan_cli/secrets/groups.py index de6a98c8..4e5faf06 100644 --- a/pkgs/clan-cli/clan_cli/secrets/groups.py +++ b/pkgs/clan-cli/clan_cli/secrets/groups.py @@ -20,24 +20,27 @@ from .types import ( ) -def machines_folder(group: str) -> Path: - return sops_groups_folder() / group / "machines" +def machines_folder(flake_name: str, group: str) -> Path: + return sops_groups_folder(flake_name) / group / "machines" -def users_folder(group: str) -> Path: - return sops_groups_folder() / group / "users" +def users_folder(flake_name: str, group: str) -> Path: + return sops_groups_folder(flake_name) / group / "users" class Group: - def __init__(self, name: str, machines: list[str], users: list[str]) -> None: + def __init__( + self, flake_name: str, name: str, machines: list[str], users: list[str] + ) -> None: self.name = name self.machines = machines self.users = users + self.flake_name = flake_name -def list_groups() -> list[Group]: +def list_groups(flake_name: str) -> list[Group]: groups: list[Group] = [] - folder = sops_groups_folder() + folder = sops_groups_folder(flake_name) if not folder.exists(): return groups @@ -45,24 +48,24 @@ def list_groups() -> list[Group]: group_folder = folder / name if not group_folder.is_dir(): continue - machines_path = machines_folder(name) + machines_path = machines_folder(flake_name, name) machines = [] if machines_path.is_dir(): for f in machines_path.iterdir(): if validate_hostname(f.name): machines.append(f.name) - users_path = users_folder(name) + users_path = users_folder(flake_name, name) users = [] if users_path.is_dir(): for f in users_path.iterdir(): if VALID_USER_NAME.match(f.name): users.append(f.name) - groups.append(Group(name, machines, users)) + groups.append(Group(flake_name, name, machines, users)) return groups def list_command(args: argparse.Namespace) -> None: - for group in list_groups(): + for group in list_groups(args.flake): print(group.name) if group.machines: print("machines:") @@ -84,9 +87,9 @@ def list_directory(directory: Path) -> str: return msg -def update_group_keys(group: str) -> None: - for secret_ in secrets.list_secrets(): - secret = sops_secrets_folder() / secret_ +def update_group_keys(flake_name: str, group: str) -> None: + for secret_ in secrets.list_secrets(flake_name): + secret = sops_secrets_folder(flake_name) / secret_ if (secret / "groups" / group).is_symlink(): update_keys( secret, @@ -94,7 +97,9 @@ def update_group_keys(group: str) -> None: ) -def add_member(group_folder: Path, source_folder: Path, name: str) -> None: +def add_member( + flake_name: str, group_folder: Path, source_folder: Path, name: str +) -> None: source = source_folder / name if not source.exists(): msg = f"{name} does not exist in {source_folder}: " @@ -109,10 +114,10 @@ def add_member(group_folder: Path, source_folder: Path, name: str) -> None: ) os.remove(user_target) user_target.symlink_to(os.path.relpath(source, user_target.parent)) - update_group_keys(group_folder.parent.name) + update_group_keys(flake_name, group_folder.parent.name) -def remove_member(group_folder: Path, name: str) -> None: +def remove_member(flake_name: str, group_folder: Path, name: str) -> None: target = group_folder / name if not target.exists(): msg = f"{name} does not exist in group in {group_folder}: " @@ -121,7 +126,7 @@ def remove_member(group_folder: Path, name: str) -> None: os.remove(target) if len(os.listdir(group_folder)) > 0: - update_group_keys(group_folder.parent.name) + update_group_keys(flake_name, group_folder.parent.name) if len(os.listdir(group_folder)) == 0: os.rmdir(group_folder) @@ -130,56 +135,65 @@ def remove_member(group_folder: Path, name: str) -> None: os.rmdir(group_folder.parent) -def add_user(group: str, name: str) -> None: - add_member(users_folder(group), sops_users_folder(), name) +def add_user(flake_name: str, group: str, name: str) -> None: + add_member( + flake_name, users_folder(flake_name, group), sops_users_folder(flake_name), name + ) def add_user_command(args: argparse.Namespace) -> None: - add_user(args.group, args.user) + add_user(args.flake, args.group, args.user) -def remove_user(group: str, name: str) -> None: - remove_member(users_folder(group), name) +def remove_user(flake_name: str, group: str, name: str) -> None: + remove_member(flake_name, users_folder(flake_name, group), name) def remove_user_command(args: argparse.Namespace) -> None: - remove_user(args.group, args.user) + remove_user(args.flake, args.group, args.user) -def add_machine(group: str, name: str) -> None: - add_member(machines_folder(group), sops_machines_folder(), name) +def add_machine(flake_name: str, group: str, name: str) -> None: + add_member( + flake_name, + machines_folder(flake_name, group), + sops_machines_folder(flake_name), + name, + ) def add_machine_command(args: argparse.Namespace) -> None: - add_machine(args.group, args.machine) + add_machine(args.flake, args.group, args.machine) -def remove_machine(group: str, name: str) -> None: - remove_member(machines_folder(group), name) +def remove_machine(flake_name: str, group: str, name: str) -> None: + remove_member(flake_name, machines_folder(flake_name, group), name) def remove_machine_command(args: argparse.Namespace) -> None: - remove_machine(args.group, args.machine) + remove_machine(args.flake, args.group, args.machine) def add_group_argument(parser: argparse.ArgumentParser) -> None: parser.add_argument("group", help="the name of the secret", type=group_name_type) -def add_secret(group: str, name: str) -> None: - secrets.allow_member(secrets.groups_folder(name), sops_groups_folder(), group) +def add_secret(flake_name: str, group: str, name: str) -> None: + secrets.allow_member( + secrets.groups_folder(flake_name, name), sops_groups_folder(flake_name), group + ) def add_secret_command(args: argparse.Namespace) -> None: - add_secret(args.group, args.secret) + add_secret(args.flake, args.group, args.secret) -def remove_secret(group: str, name: str) -> None: - secrets.disallow_member(secrets.groups_folder(name), group) +def remove_secret(flake_name: str, group: str, name: str) -> None: + secrets.disallow_member(secrets.groups_folder(flake_name, name), group) def remove_secret_command(args: argparse.Namespace) -> None: - remove_secret(args.group, args.secret) + remove_secret(args.flake, args.group, args.secret) def register_groups_parser(parser: argparse.ArgumentParser) -> None: diff --git a/pkgs/clan-cli/clan_cli/secrets/import_sops.py b/pkgs/clan-cli/clan_cli/secrets/import_sops.py index bfac4565..698a6182 100644 --- a/pkgs/clan-cli/clan_cli/secrets/import_sops.py +++ b/pkgs/clan-cli/clan_cli/secrets/import_sops.py @@ -36,14 +36,15 @@ def import_sops(args: argparse.Namespace) -> None: file=sys.stderr, ) continue - if (sops_secrets_folder() / k / "secret").exists(): + if (sops_secrets_folder(args.flake) / k / "secret").exists(): print( f"WARNING: {k} already exists, skipping", file=sys.stderr, ) continue encrypt_secret( - sops_secrets_folder() / k, + args.flake, + sops_secrets_folder(args.flake) / k, v, add_groups=args.group, add_machines=args.machine, diff --git a/pkgs/clan-cli/clan_cli/secrets/machines.py b/pkgs/clan-cli/clan_cli/secrets/machines.py index 24da93ad..b78d52d8 100644 --- a/pkgs/clan-cli/clan_cli/secrets/machines.py +++ b/pkgs/clan-cli/clan_cli/secrets/machines.py @@ -7,65 +7,67 @@ from .sops import read_key, write_key from .types import public_or_private_age_key_type, secret_name_type -def add_machine(name: str, key: str, force: bool) -> None: - write_key(sops_machines_folder() / name, key, force) +def add_machine(flake_name: str, name: str, key: str, force: bool) -> None: + write_key(sops_machines_folder(flake_name) / name, key, force) -def remove_machine(name: str) -> None: - remove_object(sops_machines_folder(), name) +def remove_machine(flake_name: str, name: str) -> None: + remove_object(sops_machines_folder(flake_name), name) -def get_machine(name: str) -> str: - return read_key(sops_machines_folder() / name) +def get_machine(flake_name: str, name: str) -> str: + return read_key(sops_machines_folder(flake_name) / name) -def has_machine(name: str) -> bool: - return (sops_machines_folder() / name / "key.json").exists() +def has_machine(flake_name: str, name: str) -> bool: + return (sops_machines_folder(flake_name) / name / "key.json").exists() -def list_machines() -> list[str]: - path = sops_machines_folder() +def list_machines(flake_name: str) -> list[str]: + path = sops_machines_folder(flake_name) def validate(name: str) -> bool: - return validate_hostname(name) and has_machine(name) + return validate_hostname(name) and has_machine(flake_name, name) return list_objects(path, validate) -def add_secret(machine: str, secret: str) -> None: +def add_secret(flake_name: str, machine: str, secret: str) -> None: secrets.allow_member( - secrets.machines_folder(secret), sops_machines_folder(), machine + secrets.machines_folder(flake_name, secret), + sops_machines_folder(flake_name), + machine, ) -def remove_secret(machine: str, secret: str) -> None: - secrets.disallow_member(secrets.machines_folder(secret), machine) +def remove_secret(flake_name: str, machine: str, secret: str) -> None: + secrets.disallow_member(secrets.machines_folder(flake_name, secret), machine) def list_command(args: argparse.Namespace) -> None: - lst = list_machines() + lst = list_machines(args.flake) if len(lst) > 0: print("\n".join(lst)) def add_command(args: argparse.Namespace) -> None: - add_machine(args.machine, args.key, args.force) + add_machine(args.flake, args.machine, args.key, args.force) def get_command(args: argparse.Namespace) -> None: - print(get_machine(args.machine)) + print(get_machine(args.flake, args.machine)) def remove_command(args: argparse.Namespace) -> None: - remove_machine(args.machine) + remove_machine(args.flake, args.machine) def add_secret_command(args: argparse.Namespace) -> None: - add_secret(args.machine, args.secret) + add_secret(args.flake, args.machine, args.secret) def remove_secret_command(args: argparse.Namespace) -> None: - remove_secret(args.machine, args.secret) + remove_secret(args.flake, args.machine, args.secret) def register_machines_parser(parser: argparse.ArgumentParser) -> None: @@ -75,9 +77,16 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None: help="the command to run", required=True, ) + # Parser list_parser = subparser.add_parser("list", help="list machines") + list_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) list_parser.set_defaults(func=list_command) + # Parser add_parser = subparser.add_parser("add", help="add a machine") add_parser.add_argument( "-f", @@ -86,6 +95,11 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None: action="store_true", default=False, ) + add_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_parser.add_argument( "machine", help="the name of the machine", type=machine_name_type ) @@ -96,21 +110,39 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None: ) add_parser.set_defaults(func=add_command) + # Parser get_parser = subparser.add_parser("get", help="get a machine public key") get_parser.add_argument( "machine", help="the name of the machine", type=machine_name_type ) + get_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) get_parser.set_defaults(func=get_command) + # Parser remove_parser = subparser.add_parser("remove", help="remove a machine") + remove_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_parser.add_argument( "machine", help="the name of the machine", type=machine_name_type ) remove_parser.set_defaults(func=remove_command) + # Parser add_secret_parser = subparser.add_parser( "add-secret", help="allow a machine to access a secret" ) + add_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_secret_parser.add_argument( "machine", help="the name of the machine", type=machine_name_type ) @@ -119,9 +151,15 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None: ) add_secret_parser.set_defaults(func=add_secret_command) + # Parser remove_secret_parser = subparser.add_parser( "remove-secret", help="remove a group's access to a secret" ) + remove_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_secret_parser.add_argument( "machine", help="the name of the group", type=machine_name_type ) diff --git a/pkgs/clan-cli/clan_cli/secrets/secrets.py b/pkgs/clan-cli/clan_cli/secrets/secrets.py index f607c3ad..62065fd6 100644 --- a/pkgs/clan-cli/clan_cli/secrets/secrets.py +++ b/pkgs/clan-cli/clan_cli/secrets/secrets.py @@ -53,62 +53,79 @@ def collect_keys_for_path(path: Path) -> set[str]: def encrypt_secret( + flake_name: str, secret: Path, value: IO[str] | str | None, add_users: list[str] = [], add_machines: list[str] = [], add_groups: list[str] = [], ) -> None: - key = ensure_sops_key() + key = ensure_sops_key(flake_name) keys = set([]) for user in add_users: - allow_member(users_folder(secret.name), sops_users_folder(), user, False) + allow_member( + users_folder(flake_name, secret.name), + sops_users_folder(flake_name), + user, + False, + ) for machine in add_machines: allow_member( - machines_folder(secret.name), sops_machines_folder(), machine, False + machines_folder(flake_name, secret.name), + sops_machines_folder(flake_name), + machine, + False, ) for group in add_groups: - allow_member(groups_folder(secret.name), sops_groups_folder(), group, False) + allow_member( + groups_folder(flake_name, secret.name), + sops_groups_folder(flake_name), + group, + False, + ) keys = collect_keys_for_path(secret) if key.pubkey not in keys: keys.add(key.pubkey) allow_member( - users_folder(secret.name), sops_users_folder(), key.username, False + users_folder(flake_name, secret.name), + sops_users_folder(flake_name), + key.username, + False, ) encrypt_file(secret / "secret", value, list(sorted(keys))) -def remove_secret(secret: str) -> None: - path = sops_secrets_folder() / secret +def remove_secret(flake_name: str, secret: str) -> None: + path = sops_secrets_folder(flake_name) / secret if not path.exists(): raise ClanError(f"Secret '{secret}' does not exist") shutil.rmtree(path) def remove_command(args: argparse.Namespace) -> None: - remove_secret(args.secret) + remove_secret(args.flake, args.secret) def add_secret_argument(parser: argparse.ArgumentParser) -> None: parser.add_argument("secret", help="the name of the secret", type=secret_name_type) -def machines_folder(group: str) -> Path: - return sops_secrets_folder() / group / "machines" +def machines_folder(flake_name: str, group: str) -> Path: + return sops_secrets_folder(flake_name) / group / "machines" -def users_folder(group: str) -> Path: - return sops_secrets_folder() / group / "users" +def users_folder(flake_name: str, group: str) -> Path: + return sops_secrets_folder(flake_name) / group / "users" -def groups_folder(group: str) -> Path: - return sops_secrets_folder() / group / "groups" +def groups_folder(flake_name: str, group: str) -> Path: + return sops_secrets_folder(flake_name) / group / "groups" def list_directory(directory: Path) -> str: @@ -171,35 +188,37 @@ def disallow_member(group_folder: Path, name: str) -> None: ) -def has_secret(secret: str) -> bool: - return (sops_secrets_folder() / secret / "secret").exists() +def has_secret(flake_name: str, secret: str) -> bool: + return (sops_secrets_folder(flake_name) / secret / "secret").exists() -def list_secrets() -> list[str]: - path = sops_secrets_folder() +def list_secrets(flake_name: str) -> list[str]: + path = sops_secrets_folder(flake_name) def validate(name: str) -> bool: - return VALID_SECRET_NAME.match(name) is not None and has_secret(name) + return VALID_SECRET_NAME.match(name) is not None and has_secret( + flake_name, name + ) return list_objects(path, validate) def list_command(args: argparse.Namespace) -> None: - lst = list_secrets() + lst = list_secrets(args.flake) if len(lst) > 0: print("\n".join(lst)) -def decrypt_secret(secret: str) -> str: - ensure_sops_key() - secret_path = sops_secrets_folder() / secret / "secret" +def decrypt_secret(flake_name: str, secret: str) -> str: + ensure_sops_key(flake_name) + secret_path = sops_secrets_folder(flake_name) / secret / "secret" if not secret_path.exists(): raise ClanError(f"Secret '{secret}' does not exist") return decrypt_file(secret_path) def get_command(args: argparse.Namespace) -> None: - print(decrypt_secret(args.secret), end="") + print(decrypt_secret(args.flake, args.secret), end="") def set_command(args: argparse.Namespace) -> None: @@ -212,7 +231,8 @@ def set_command(args: argparse.Namespace) -> None: elif tty.is_interactive(): secret_value = getpass.getpass(prompt="Paste your secret: ") encrypt_secret( - sops_secrets_folder() / args.secret, + args.flake, + sops_secrets_folder(args.flake) / args.secret, secret_value, args.user, args.machine, @@ -221,8 +241,8 @@ def set_command(args: argparse.Namespace) -> None: def rename_command(args: argparse.Namespace) -> None: - old_path = sops_secrets_folder() / args.secret - new_path = sops_secrets_folder() / args.new_name + old_path = sops_secrets_folder(args.flake) / args.secret + new_path = sops_secrets_folder(args.flake) / args.new_name if not old_path.exists(): raise ClanError(f"Secret '{args.secret}' does not exist") if new_path.exists(): @@ -237,9 +257,19 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None: parser_get = subparser.add_parser("get", help="get a secret") add_secret_argument(parser_get) parser_get.set_defaults(func=get_command) + parser_get.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser_set = subparser.add_parser("set", help="set a secret") add_secret_argument(parser_set) + parser_set.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser_set.add_argument( "--group", type=str, diff --git a/pkgs/clan-cli/clan_cli/secrets/sops.py b/pkgs/clan-cli/clan_cli/secrets/sops.py index b79b41c6..c9a389bb 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops.py @@ -51,7 +51,7 @@ def generate_private_key() -> tuple[str, str]: raise ClanError("Failed to generate private sops key") from e -def get_user_name(user: str) -> str: +def get_user_name(flake_name: str, user: str) -> str: """Ask the user for their name until a unique one is provided.""" while True: name = input( @@ -59,14 +59,14 @@ def get_user_name(user: str) -> str: ) if name: user = name - if not (sops_users_folder() / user).exists(): + if not (sops_users_folder(flake_name) / user).exists(): return user - print(f"{sops_users_folder() / user} already exists") + print(f"{sops_users_folder(flake_name) / user} already exists") -def ensure_user_or_machine(pub_key: str) -> SopsKey: +def ensure_user_or_machine(flake_name: str, pub_key: str) -> SopsKey: key = SopsKey(pub_key, username="") - folders = [sops_users_folder(), sops_machines_folder()] + folders = [sops_users_folder(flake_name), sops_machines_folder(flake_name)] for folder in folders: if folder.exists(): for user in folder.iterdir(): @@ -90,13 +90,13 @@ def default_sops_key_path() -> Path: return user_config_dir() / "sops" / "age" / "keys.txt" -def ensure_sops_key() -> SopsKey: +def ensure_sops_key(flake_name: str) -> SopsKey: key = os.environ.get("SOPS_AGE_KEY") if key: - return ensure_user_or_machine(get_public_key(key)) + return ensure_user_or_machine(flake_name, get_public_key(key)) path = default_sops_key_path() if path.exists(): - return ensure_user_or_machine(get_public_key(path.read_text())) + return ensure_user_or_machine(flake_name, get_public_key(path.read_text())) else: raise ClanError( "No sops key found. Please generate one with 'clan secrets key generate'." diff --git a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py index 73fd1dd5..f6657a5a 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py @@ -9,7 +9,7 @@ from typing import Any from clan_cli.nix import nix_shell -from ..dirs import get_clan_flake_toplevel +from ..dirs import specific_flake_dir from ..errors import ClanError from .folders import sops_secrets_folder from .machines import add_machine, has_machine @@ -17,21 +17,29 @@ from .secrets import decrypt_secret, encrypt_secret, has_secret from .sops import generate_private_key -def generate_host_key(machine_name: str) -> None: - if has_machine(machine_name): +def generate_host_key(flake_name: str, machine_name: str) -> None: + if has_machine(flake_name, machine_name): return priv_key, pub_key = generate_private_key() - encrypt_secret(sops_secrets_folder() / f"{machine_name}-age.key", priv_key) - add_machine(machine_name, pub_key, False) + encrypt_secret( + flake_name, + sops_secrets_folder(flake_name) / f"{machine_name}-age.key", + priv_key, + ) + add_machine(flake_name, machine_name, pub_key, False) def generate_secrets_group( - secret_group: str, machine_name: str, tempdir: Path, secret_options: dict[str, Any] + flake_name: str, + secret_group: str, + machine_name: str, + tempdir: Path, + secret_options: dict[str, Any], ) -> None: - clan_dir = get_clan_flake_toplevel() + clan_dir = specific_flake_dir(flake_name) secrets = secret_options["secrets"] needs_regeneration = any( - not has_secret(f"{machine_name}-{secret['name']}") + not has_secret(flake_name, f"{machine_name}-{secret['name']}") for secret in secrets.values() ) generator = secret_options["generator"] @@ -62,7 +70,8 @@ export secrets={shlex.quote(str(secrets_dir))} msg += text raise ClanError(msg) encrypt_secret( - sops_secrets_folder() / f"{machine_name}-{secret['name']}", + flake_name, + sops_secrets_folder(flake_name) / f"{machine_name}-{secret['name']}", secret_file.read_text(), add_machines=[machine_name], ) @@ -79,17 +88,18 @@ export secrets={shlex.quote(str(secrets_dir))} # this is called by the sops.nix clan core module def generate_secrets_from_nix( + flake_name: str, machine_name: str, secret_submodules: dict[str, Any], ) -> None: - generate_host_key(machine_name) + generate_host_key(flake_name, machine_name) errors = {} with TemporaryDirectory() as d: # if any of the secrets are missing, we regenerate all connected facts/secrets for secret_group, secret_options in secret_submodules.items(): try: generate_secrets_group( - secret_group, machine_name, Path(d), secret_options + flake_name, secret_group, machine_name, Path(d), secret_options ) except ClanError as e: errors[secret_group] = e @@ -102,12 +112,15 @@ def generate_secrets_from_nix( # this is called by the sops.nix clan core module def upload_age_key_from_nix( + flake_name: str, machine_name: str, ) -> None: secret_name = f"{machine_name}-age.key" - if not has_secret(secret_name): # skip uploading the secret, not managed by us + if not has_secret( + flake_name, secret_name + ): # skip uploading the secret, not managed by us return - secret = decrypt_secret(secret_name) + secret = decrypt_secret(flake_name, secret_name) secrets_dir = Path(os.environ["SECRETS_DIR"]) (secrets_dir / "key.txt").write_text(secret) diff --git a/pkgs/clan-cli/clan_cli/secrets/upload.py b/pkgs/clan-cli/clan_cli/secrets/upload.py index 5e31a95d..46c217d7 100644 --- a/pkgs/clan-cli/clan_cli/secrets/upload.py +++ b/pkgs/clan-cli/clan_cli/secrets/upload.py @@ -4,6 +4,7 @@ import subprocess from pathlib import Path from tempfile import TemporaryDirectory +from ..dirs import specific_flake_dir from ..machines.machines import Machine from ..nix import nix_shell @@ -37,7 +38,7 @@ def upload_secrets(machine: Machine) -> None: def upload_command(args: argparse.Namespace) -> None: - machine = Machine(args.machine) + machine = Machine(name=args.machine, flake_dir=specific_flake_dir(args.flake)) upload_secrets(machine) @@ -46,4 +47,9 @@ def register_upload_parser(parser: argparse.ArgumentParser) -> None: "machine", help="The machine to upload secrets to", ) + parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser.set_defaults(func=upload_command) diff --git a/pkgs/clan-cli/clan_cli/secrets/users.py b/pkgs/clan-cli/clan_cli/secrets/users.py index 2faaf646..a653f295 100644 --- a/pkgs/clan-cli/clan_cli/secrets/users.py +++ b/pkgs/clan-cli/clan_cli/secrets/users.py @@ -11,20 +11,20 @@ from .types import ( ) -def add_user(name: str, key: str, force: bool) -> None: - write_key(sops_users_folder() / name, key, force) +def add_user(flake_name: str, name: str, key: str, force: bool) -> None: + write_key(sops_users_folder(flake_name) / name, key, force) -def remove_user(name: str) -> None: - remove_object(sops_users_folder(), name) +def remove_user(flake_name: str, name: str) -> None: + remove_object(sops_users_folder(flake_name), name) -def get_user(name: str) -> str: - return read_key(sops_users_folder() / name) +def get_user(flake_name: str, name: str) -> str: + return read_key(sops_users_folder(flake_name) / name) -def list_users() -> list[str]: - path = sops_users_folder() +def list_users(flake_name: str) -> list[str]: + path = sops_users_folder(flake_name) def validate(name: str) -> bool: return ( @@ -35,38 +35,40 @@ def list_users() -> list[str]: return list_objects(path, validate) -def add_secret(user: str, secret: str) -> None: - secrets.allow_member(secrets.users_folder(secret), sops_users_folder(), user) +def add_secret(flake_name: str, user: str, secret: str) -> None: + secrets.allow_member( + secrets.users_folder(flake_name, secret), sops_users_folder(flake_name), user + ) -def remove_secret(user: str, secret: str) -> None: - secrets.disallow_member(secrets.users_folder(secret), user) +def remove_secret(flake_name: str, user: str, secret: str) -> None: + secrets.disallow_member(secrets.users_folder(flake_name, secret), user) def list_command(args: argparse.Namespace) -> None: - lst = list_users() + lst = list_users(args.flake) if len(lst) > 0: print("\n".join(lst)) def add_command(args: argparse.Namespace) -> None: - add_user(args.user, args.key, args.force) + add_user(args.flake, args.user, args.key, args.force) def get_command(args: argparse.Namespace) -> None: - print(get_user(args.user)) + print(get_user(args.flake, args.user)) def remove_command(args: argparse.Namespace) -> None: - remove_user(args.user) + remove_user(args.flake, args.user) def add_secret_command(args: argparse.Namespace) -> None: - add_secret(args.user, args.secret) + add_secret(args.flake, args.user, args.secret) def remove_secret_command(args: argparse.Namespace) -> None: - remove_secret(args.user, args.secret) + remove_secret(args.flake, args.user, args.secret) def register_users_parser(parser: argparse.ArgumentParser) -> None: @@ -77,6 +79,11 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None: required=True, ) list_parser = subparser.add_parser("list", help="list users") + list_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) list_parser.set_defaults(func=list_command) add_parser = subparser.add_parser("add", help="add a user") @@ -90,14 +97,29 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None: type=public_or_private_age_key_type, ) add_parser.set_defaults(func=add_command) + add_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) get_parser = subparser.add_parser("get", help="get a user public key") get_parser.add_argument("user", help="the name of the user", type=user_name_type) get_parser.set_defaults(func=get_command) + get_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_parser = subparser.add_parser("remove", help="remove a user") remove_parser.add_argument("user", help="the name of the user", type=user_name_type) remove_parser.set_defaults(func=remove_command) + remove_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_secret_parser = subparser.add_parser( "add-secret", help="allow a user to access a secret" diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py index 1f78608d..2df7b9ac 100644 --- a/pkgs/clan-cli/clan_cli/vms/create.py +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import Iterator from uuid import UUID -from ..dirs import get_clan_flake_toplevel +from ..dirs import specific_flake_dir from ..nix import nix_build, nix_config, nix_shell from ..task_manager import BaseTask, Command, create_task from .inspect import VmConfig, inspect_vm @@ -147,7 +147,7 @@ def create_vm(vm: VmConfig) -> BuildVmTask: def create_command(args: argparse.Namespace) -> None: - clan_dir = get_clan_flake_toplevel() + clan_dir = specific_flake_dir(args.flake) vm = asyncio.run(inspect_vm(flake_url=clan_dir, flake_attr=args.machine)) task = create_vm(vm) @@ -157,4 +157,9 @@ def create_command(args: argparse.Namespace) -> None: def register_create_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument("machine", type=str) + parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser.set_defaults(func=create_command) diff --git a/pkgs/clan-cli/clan_cli/vms/inspect.py b/pkgs/clan-cli/clan_cli/vms/inspect.py index 417dd718..e74382d3 100644 --- a/pkgs/clan-cli/clan_cli/vms/inspect.py +++ b/pkgs/clan-cli/clan_cli/vms/inspect.py @@ -6,7 +6,7 @@ from pathlib import Path from pydantic import AnyUrl, BaseModel from ..async_cmd import run -from ..dirs import get_clan_flake_toplevel +from ..dirs import specific_flake_dir from ..nix import nix_config, nix_eval @@ -33,7 +33,7 @@ async def inspect_vm(flake_url: AnyUrl | Path, flake_attr: str) -> VmConfig: def inspect_command(args: argparse.Namespace) -> None: - clan_dir = get_clan_flake_toplevel() + clan_dir = specific_flake_dir(args.flake) res = asyncio.run(inspect_vm(flake_url=clan_dir, flake_attr=args.machine)) print("Cores:", res.cores) print("Memory size:", res.memory_size) @@ -42,4 +42,9 @@ def inspect_command(args: argparse.Namespace) -> None: def register_inspect_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument("machine", type=str) + parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser.set_defaults(func=inspect_command) diff --git a/pkgs/clan-cli/clan_cli/webui/api_inputs.py b/pkgs/clan-cli/clan_cli/webui/api_inputs.py index 618a6bdb..d3a9545c 100644 --- a/pkgs/clan-cli/clan_cli/webui/api_inputs.py +++ b/pkgs/clan-cli/clan_cli/webui/api_inputs.py @@ -4,7 +4,7 @@ from typing import Any from pydantic import AnyUrl, BaseModel, validator -from ..dirs import clan_data_dir, clan_flake_dir +from ..dirs import clan_data_dir, clan_flakes_dir from ..flakes.create import DEFAULT_URL log = logging.getLogger(__name__) @@ -38,7 +38,7 @@ class ClanFlakePath(BaseModel): @validator("dest") def check_dest(cls: Any, v: Path) -> Path: # noqa - return validate_path(clan_flake_dir(), v) + return validate_path(clan_flakes_dir(), v) class FlakeCreateInput(ClanFlakePath): diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index 0320270c..e488440a 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -2,7 +2,7 @@ import fileinput import shutil import tempfile from pathlib import Path -from typing import Iterator +from typing import Iterator, NamedTuple import pytest from root import CLAN_CORE @@ -27,22 +27,27 @@ def substitute( print(line, end="") +class TestFlake(NamedTuple): + name: str + path: Path + + def create_flake( monkeypatch: pytest.MonkeyPatch, - name: str, + flake_name: str, clan_core_flake: Path | None = None, machines: list[str] = [], remote: bool = False, -) -> Iterator[Path]: +) -> Iterator[TestFlake]: """ Creates a flake with the given name and machines. The machine names map to the machines in ./test_machines """ - template = Path(__file__).parent / name + template = Path(__file__).parent / flake_name # copy the template to a new temporary location with tempfile.TemporaryDirectory() as tmpdir_: home = Path(tmpdir_) - flake = home / name + flake = home / flake_name shutil.copytree(template, flake) # lookup the requested machines in ./test_machines and include them if machines: @@ -60,20 +65,20 @@ def create_flake( with tempfile.TemporaryDirectory() as workdir: monkeypatch.chdir(workdir) monkeypatch.setenv("HOME", str(home)) - yield flake + yield TestFlake(flake_name, flake) else: monkeypatch.chdir(flake) monkeypatch.setenv("HOME", str(home)) - yield flake + yield TestFlake(flake_name, flake) @pytest.fixture -def test_flake(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]: +def test_flake(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]: yield from create_flake(monkeypatch, "test_flake") @pytest.fixture -def test_flake_with_core(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]: +def test_flake_with_core(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( "clan-core flake not found. This test requires the clan-core flake to be present" @@ -82,7 +87,9 @@ def test_flake_with_core(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]: @pytest.fixture -def test_flake_with_core_and_pass(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]: +def test_flake_with_core_and_pass( + monkeypatch: pytest.MonkeyPatch, +) -> Iterator[TestFlake]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( "clan-core flake not found. This test requires the clan-core flake to be present" diff --git a/pkgs/clan-cli/tests/test_dirs.py b/pkgs/clan-cli/tests/test_dirs.py index c621ff19..5b412e07 100644 --- a/pkgs/clan-cli/tests/test_dirs.py +++ b/pkgs/clan-cli/tests/test_dirs.py @@ -2,7 +2,7 @@ from pathlib import Path import pytest -from clan_cli.dirs import get_clan_flake_toplevel +from clan_cli.dirs import _get_clan_flake_toplevel from clan_cli.errors import ClanError @@ -11,12 +11,12 @@ def test_get_clan_flake_toplevel( ) -> None: monkeypatch.chdir(temporary_dir) with pytest.raises(ClanError): - print(get_clan_flake_toplevel()) + print(_get_clan_flake_toplevel()) (temporary_dir / ".git").touch() - assert get_clan_flake_toplevel() == temporary_dir + assert _get_clan_flake_toplevel() == temporary_dir subdir = temporary_dir / "subdir" subdir.mkdir() monkeypatch.chdir(subdir) (subdir / ".clan-flake").touch() - assert get_clan_flake_toplevel() == subdir + assert _get_clan_flake_toplevel() == subdir diff --git a/pkgs/clan-cli/tests/test_machines_config.py b/pkgs/clan-cli/tests/test_machines_config.py index cb942775..a7ab422a 100644 --- a/pkgs/clan-cli/tests/test_machines_config.py +++ b/pkgs/clan-cli/tests/test_machines_config.py @@ -1,8 +1,8 @@ -from pathlib import Path +from fixtures_flakes import TestFlake from clan_cli.config import machine -def test_schema_for_machine(test_flake: Path) -> None: - schema = machine.schema_for_machine("machine1", flake=test_flake) +def test_schema_for_machine(test_flake: TestFlake) -> None: + schema = machine.schema_for_machine(test_flake.name, "machine1") assert "properties" in schema diff --git a/pkgs/clan-cli/tests/test_secrets_generate.py b/pkgs/clan-cli/tests/test_secrets_generate.py index 4ddf87d4..22632316 100644 --- a/pkgs/clan-cli/tests/test_secrets_generate.py +++ b/pkgs/clan-cli/tests/test_secrets_generate.py @@ -1,8 +1,8 @@ -from pathlib import Path from typing import TYPE_CHECKING import pytest from cli import Cli +from fixtures_flakes import TestFlake from clan_cli.machines.facts import machine_get_fact from clan_cli.secrets.folders import sops_secrets_folder @@ -15,21 +15,27 @@ if TYPE_CHECKING: @pytest.mark.impure def test_generate_secret( monkeypatch: pytest.MonkeyPatch, - test_flake_with_core: Path, + test_flake_with_core: TestFlake, age_keys: list["KeyPair"], ) -> None: - monkeypatch.chdir(test_flake_with_core) + monkeypatch.chdir(test_flake_with_core.path) monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) cli = Cli() cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey]) cli.run(["secrets", "generate", "vm1"]) - has_secret("vm1-age.key") - has_secret("vm1-zerotier-identity-secret") - network_id = machine_get_fact("vm1", "zerotier-network-id") + has_secret(test_flake_with_core.name, "vm1-age.key") + has_secret(test_flake_with_core.name, "vm1-zerotier-identity-secret") + network_id = machine_get_fact( + test_flake_with_core.name, "vm1", "zerotier-network-id" + ) assert len(network_id) == 16 - age_key = sops_secrets_folder().joinpath("vm1-age.key").joinpath("secret") + age_key = ( + sops_secrets_folder(test_flake_with_core.name) + .joinpath("vm1-age.key") + .joinpath("secret") + ) identity_secret = ( - sops_secrets_folder() + sops_secrets_folder(test_flake_with_core.name) .joinpath("vm1-zerotier-identity-secret") .joinpath("secret") ) @@ -42,7 +48,7 @@ def test_generate_secret( assert identity_secret.lstat().st_mtime_ns == secret1_mtime machine_path = ( - sops_secrets_folder() + sops_secrets_folder(test_flake_with_core.name) .joinpath("vm1-zerotier-identity-secret") .joinpath("machines") .joinpath("vm1") diff --git a/pkgs/clan-cli/tests/test_secrets_password_store.py b/pkgs/clan-cli/tests/test_secrets_password_store.py index 3ea459c4..999f08a5 100644 --- a/pkgs/clan-cli/tests/test_secrets_password_store.py +++ b/pkgs/clan-cli/tests/test_secrets_password_store.py @@ -3,6 +3,7 @@ from pathlib import Path import pytest from cli import Cli +from fixtures_flakes import TestFlake from clan_cli.machines.facts import machine_get_fact from clan_cli.nix import nix_shell @@ -12,11 +13,11 @@ from clan_cli.ssh import HostGroup @pytest.mark.impure def test_upload_secret( monkeypatch: pytest.MonkeyPatch, - test_flake_with_core_and_pass: Path, + test_flake_with_core_and_pass: TestFlake, temporary_dir: Path, host_group: HostGroup, ) -> None: - monkeypatch.chdir(test_flake_with_core_and_pass) + monkeypatch.chdir(test_flake_with_core_and_pass.path) gnupghome = temporary_dir / "gpg" gnupghome.mkdir(mode=0o700) monkeypatch.setenv("GNUPGHOME", str(gnupghome)) @@ -39,7 +40,9 @@ def test_upload_secret( ) subprocess.run(nix_shell(["pass"], ["pass", "init", "test@local"]), check=True) cli.run(["secrets", "generate", "vm1"]) - network_id = machine_get_fact("vm1", "zerotier-network-id") + network_id = machine_get_fact( + test_flake_with_core_and_pass.name, "vm1", "zerotier-network-id" + ) assert len(network_id) == 16 identity_secret = ( temporary_dir / "pass" / "machines" / "vm1" / "zerotier-identity-secret.gpg" @@ -50,13 +53,13 @@ def test_upload_secret( cli.run(["secrets", "generate", "vm1"]) assert identity_secret.lstat().st_mtime_ns == secret1_mtime - flake = test_flake_with_core_and_pass.joinpath("flake.nix") + flake = test_flake_with_core_and_pass.path.joinpath("flake.nix") host = host_group.hosts[0] addr = f"{host.user}@{host.host}:{host.port}?StrictHostKeyChecking=no&UserKnownHostsFile=/dev/null&IdentityFile={host.key}" new_text = flake.read_text().replace("__CLAN_DEPLOYMENT_ADDRESS__", addr) flake.write_text(new_text) cli.run(["secrets", "upload", "vm1"]) zerotier_identity_secret = ( - test_flake_with_core_and_pass / "secrets" / "zerotier-identity-secret" + test_flake_with_core_and_pass.path / "secrets" / "zerotier-identity-secret" ) assert zerotier_identity_secret.exists() diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index ef9a40ab..25bedf2f 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Iterator import pytest from api import TestClient from cli import Cli -from fixtures_flakes import create_flake +from fixtures_flakes import TestFlake, create_flake from httpx import SyncByteStream from root import CLAN_CORE @@ -14,7 +14,7 @@ if TYPE_CHECKING: @pytest.fixture -def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]: +def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]: yield from create_flake( monkeypatch, "test_flake_with_core_dynamic_machines", @@ -26,7 +26,7 @@ def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path @pytest.fixture def remote_flake_with_vm_without_secrets( monkeypatch: pytest.MonkeyPatch, -) -> Iterator[Path]: +) -> Iterator[TestFlake]: yield from create_flake( monkeypatch, "test_flake_with_core_dynamic_machines", From 2ca54afe7f317f8c13e616400ab280d62b04617a Mon Sep 17 00:00:00 2001 From: Qubasa Date: Sat, 14 Oct 2023 15:17:58 +0200 Subject: [PATCH 05/36] Added new type FlakeName --- pkgs/clan-cli/clan_cli/config/__init__.py | 7 ++--- pkgs/clan-cli/clan_cli/config/machine.py | 11 +++++--- pkgs/clan-cli/clan_cli/dirs.py | 13 ++++----- pkgs/clan-cli/clan_cli/flakes/types.py | 3 +++ pkgs/clan-cli/clan_cli/machines/create.py | 3 ++- pkgs/clan-cli/clan_cli/machines/facts.py | 5 ++-- pkgs/clan-cli/clan_cli/machines/list.py | 3 ++- pkgs/clan-cli/clan_cli/secrets/folders.py | 7 ++--- pkgs/clan-cli/clan_cli/secrets/groups.py | 27 ++++++++++--------- pkgs/clan-cli/clan_cli/secrets/machines.py | 15 ++++++----- pkgs/clan-cli/clan_cli/secrets/secrets.py | 17 ++++++------ pkgs/clan-cli/clan_cli/secrets/sops.py | 7 ++--- .../clan_cli/secrets/sops_generate.py | 9 ++++--- pkgs/clan-cli/clan_cli/secrets/users.py | 13 ++++----- .../clan_cli/webui/routers/machines.py | 11 ++++---- pkgs/clan-cli/tests/fixtures_flakes.py | 13 +++++---- pkgs/clan-cli/tests/test_vms_api_create.py | 6 +++-- 17 files changed, 98 insertions(+), 72 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/flakes/types.py diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index ee5ebafc..ec3df55f 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -11,6 +11,7 @@ from typing import Any, Optional, Tuple, get_origin from clan_cli.dirs import machine_settings_file, specific_flake_dir from clan_cli.errors import ClanError +from clan_cli.flakes.types import FlakeName from clan_cli.git import commit_file from clan_cli.nix import nix_eval @@ -104,7 +105,7 @@ def cast(value: Any, type: Any, opt_description: str) -> Any: def options_for_machine( - flake_name: str, machine_name: str, show_trace: bool = False + flake_name: FlakeName, machine_name: str, show_trace: bool = False ) -> dict: clan_dir = specific_flake_dir(flake_name) flags = [] @@ -127,7 +128,7 @@ def options_for_machine( def read_machine_option_value( - flake_name: str, machine_name: str, option: str, show_trace: bool = False + flake_name: FlakeName, machine_name: str, option: str, show_trace: bool = False ) -> str: clan_dir = specific_flake_dir(flake_name) # use nix eval to read from .#nixosConfigurations.default.config.{option} @@ -240,7 +241,7 @@ def find_option( def set_option( - flake_name: str, + flake_name: FlakeName, option: str, value: Any, options: dict, diff --git a/pkgs/clan-cli/clan_cli/config/machine.py b/pkgs/clan-cli/clan_cli/config/machine.py index e1561731..1a5582cf 100644 --- a/pkgs/clan-cli/clan_cli/config/machine.py +++ b/pkgs/clan-cli/clan_cli/config/machine.py @@ -15,6 +15,8 @@ from clan_cli.dirs import ( from clan_cli.git import commit_file, find_git_repo_root from clan_cli.nix import nix_eval +from ..flakes.types import FlakeName + def verify_machine_config( machine_name: str, config: Optional[dict] = None, flake: Optional[Path] = None @@ -52,7 +54,8 @@ def verify_machine_config( return None -def config_for_machine(machine_name: str) -> dict: + +def config_for_machine(flake_name: FlakeName, 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_name, machine_name).exists(): raise HTTPException( @@ -66,7 +69,9 @@ def config_for_machine(machine_name: str) -> dict: return json.load(f) -def set_config_for_machine(flake_name: str, machine_name: str, config: dict) -> None: +def set_config_for_machine( + flake_name: FlakeName, machine_name: str, config: dict +) -> None: # write the config to a json file located at {flake}/machines/{machine_name}/settings.json if not specific_machine_dir(flake_name, machine_name).exists(): raise HTTPException( @@ -83,7 +88,7 @@ def set_config_for_machine(flake_name: str, machine_name: str, config: dict) -> commit_file(settings_path, repo_dir) -def schema_for_machine(flake_name: str, machine_name: str) -> dict: +def schema_for_machine(flake_name: FlakeName, machine_name: str) -> dict: flake = specific_flake_dir(flake_name) # use nix eval to lib.evalModules .#nixosModules.machine-{machine_name} diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index 1ea596b1..17a0bc13 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Optional from .errors import ClanError +from .flakes.types import FlakeName def _get_clan_flake_toplevel() -> Path: @@ -68,22 +69,22 @@ def clan_flakes_dir() -> Path: return path.resolve() -def specific_flake_dir(name: str) -> Path: - flake_dir = clan_flakes_dir() / name +def specific_flake_dir(flake_name: FlakeName) -> Path: + flake_dir = clan_flakes_dir() / flake_name if not flake_dir.exists(): - raise ClanError(f"Flake {name} does not exist") + raise ClanError(f"Flake {flake_name} does not exist") return flake_dir -def machines_dir(flake_name: str) -> Path: +def machines_dir(flake_name: FlakeName) -> Path: return specific_flake_dir(flake_name) / "machines" -def specific_machine_dir(flake_name: str, machine: str) -> Path: +def specific_machine_dir(flake_name: FlakeName, machine: str) -> Path: return machines_dir(flake_name) / machine -def machine_settings_file(flake_name: str, machine: str) -> Path: +def machine_settings_file(flake_name: FlakeName, machine: str) -> Path: return specific_machine_dir(flake_name, machine) / "settings.json" diff --git a/pkgs/clan-cli/clan_cli/flakes/types.py b/pkgs/clan-cli/clan_cli/flakes/types.py new file mode 100644 index 00000000..16e38c87 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/flakes/types.py @@ -0,0 +1,3 @@ +from typing import NewType + +FlakeName = NewType("FlakeName", str) diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py index f96beead..a4880f0e 100644 --- a/pkgs/clan-cli/clan_cli/machines/create.py +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -5,12 +5,13 @@ from typing import Dict from ..async_cmd import CmdOut, run, runforcli from ..dirs import specific_flake_dir, specific_machine_dir from ..errors import ClanError +from ..flakes.types import FlakeName from ..nix import nix_shell log = logging.getLogger(__name__) -async def create_machine(flake_name: str, machine_name: str) -> Dict[str, CmdOut]: +async def create_machine(flake_name: FlakeName, machine_name: str) -> Dict[str, CmdOut]: folder = specific_machine_dir(flake_name, machine_name) folder.mkdir(parents=True, exist_ok=True) diff --git a/pkgs/clan-cli/clan_cli/machines/facts.py b/pkgs/clan-cli/clan_cli/machines/facts.py index ed471ebc..7b665b53 100644 --- a/pkgs/clan-cli/clan_cli/machines/facts.py +++ b/pkgs/clan-cli/clan_cli/machines/facts.py @@ -1,9 +1,10 @@ from ..dirs import specific_machine_dir +from ..flakes.types import FlakeName -def machine_has_fact(flake_name: str, machine: str, fact: str) -> bool: +def machine_has_fact(flake_name: FlakeName, machine: str, fact: str) -> bool: return (specific_machine_dir(flake_name, machine) / "facts" / fact).exists() -def machine_get_fact(flake_name: str, machine: str, fact: str) -> str: +def machine_get_fact(flake_name: FlakeName, machine: str, fact: str) -> str: return (specific_machine_dir(flake_name, machine) / "facts" / fact).read_text() diff --git a/pkgs/clan-cli/clan_cli/machines/list.py b/pkgs/clan-cli/clan_cli/machines/list.py index 3558967a..d78adb23 100644 --- a/pkgs/clan-cli/clan_cli/machines/list.py +++ b/pkgs/clan-cli/clan_cli/machines/list.py @@ -3,12 +3,13 @@ import logging import os from ..dirs import machines_dir +from ..flakes.types import FlakeName from .types import validate_hostname log = logging.getLogger(__name__) -def list_machines(flake_name: str) -> list[str]: +def list_machines(flake_name: FlakeName) -> list[str]: path = machines_dir(flake_name) log.debug(f"Listing machines in {path}") if not path.exists(): diff --git a/pkgs/clan-cli/clan_cli/secrets/folders.py b/pkgs/clan-cli/clan_cli/secrets/folders.py index 4659a7ff..8a551cc7 100644 --- a/pkgs/clan-cli/clan_cli/secrets/folders.py +++ b/pkgs/clan-cli/clan_cli/secrets/folders.py @@ -5,14 +5,15 @@ from typing import Callable from ..dirs import specific_flake_dir from ..errors import ClanError +from ..flakes.types import FlakeName -def get_sops_folder(flake_name: str) -> Path: +def get_sops_folder(flake_name: FlakeName) -> Path: return specific_flake_dir(flake_name) / "sops" -def gen_sops_subfolder(subdir: str) -> Callable[[str], Path]: - def folder(flake_name: str) -> Path: +def gen_sops_subfolder(subdir: str) -> Callable[[FlakeName], Path]: + def folder(flake_name: FlakeName) -> Path: return specific_flake_dir(flake_name) / "sops" / subdir return folder diff --git a/pkgs/clan-cli/clan_cli/secrets/groups.py b/pkgs/clan-cli/clan_cli/secrets/groups.py index 4e5faf06..b944560c 100644 --- a/pkgs/clan-cli/clan_cli/secrets/groups.py +++ b/pkgs/clan-cli/clan_cli/secrets/groups.py @@ -3,6 +3,7 @@ import os from pathlib import Path from ..errors import ClanError +from ..flakes.types import FlakeName from ..machines.types import machine_name_type, validate_hostname from . import secrets from .folders import ( @@ -20,17 +21,17 @@ from .types import ( ) -def machines_folder(flake_name: str, group: str) -> Path: +def machines_folder(flake_name: FlakeName, group: str) -> Path: return sops_groups_folder(flake_name) / group / "machines" -def users_folder(flake_name: str, group: str) -> Path: +def users_folder(flake_name: FlakeName, group: str) -> Path: return sops_groups_folder(flake_name) / group / "users" class Group: def __init__( - self, flake_name: str, name: str, machines: list[str], users: list[str] + self, flake_name: FlakeName, name: str, machines: list[str], users: list[str] ) -> None: self.name = name self.machines = machines @@ -38,7 +39,7 @@ class Group: self.flake_name = flake_name -def list_groups(flake_name: str) -> list[Group]: +def list_groups(flake_name: FlakeName) -> list[Group]: groups: list[Group] = [] folder = sops_groups_folder(flake_name) if not folder.exists(): @@ -87,7 +88,7 @@ def list_directory(directory: Path) -> str: return msg -def update_group_keys(flake_name: str, group: str) -> None: +def update_group_keys(flake_name: FlakeName, group: str) -> None: for secret_ in secrets.list_secrets(flake_name): secret = sops_secrets_folder(flake_name) / secret_ if (secret / "groups" / group).is_symlink(): @@ -98,7 +99,7 @@ def update_group_keys(flake_name: str, group: str) -> None: def add_member( - flake_name: str, group_folder: Path, source_folder: Path, name: str + flake_name: FlakeName, group_folder: Path, source_folder: Path, name: str ) -> None: source = source_folder / name if not source.exists(): @@ -117,7 +118,7 @@ def add_member( update_group_keys(flake_name, group_folder.parent.name) -def remove_member(flake_name: str, group_folder: Path, name: str) -> None: +def remove_member(flake_name: FlakeName, group_folder: Path, name: str) -> None: target = group_folder / name if not target.exists(): msg = f"{name} does not exist in group in {group_folder}: " @@ -135,7 +136,7 @@ def remove_member(flake_name: str, group_folder: Path, name: str) -> None: os.rmdir(group_folder.parent) -def add_user(flake_name: str, group: str, name: str) -> None: +def add_user(flake_name: FlakeName, group: str, name: str) -> None: add_member( flake_name, users_folder(flake_name, group), sops_users_folder(flake_name), name ) @@ -145,7 +146,7 @@ def add_user_command(args: argparse.Namespace) -> None: add_user(args.flake, args.group, args.user) -def remove_user(flake_name: str, group: str, name: str) -> None: +def remove_user(flake_name: FlakeName, group: str, name: str) -> None: remove_member(flake_name, users_folder(flake_name, group), name) @@ -153,7 +154,7 @@ def remove_user_command(args: argparse.Namespace) -> None: remove_user(args.flake, args.group, args.user) -def add_machine(flake_name: str, group: str, name: str) -> None: +def add_machine(flake_name: FlakeName, group: str, name: str) -> None: add_member( flake_name, machines_folder(flake_name, group), @@ -166,7 +167,7 @@ def add_machine_command(args: argparse.Namespace) -> None: add_machine(args.flake, args.group, args.machine) -def remove_machine(flake_name: str, group: str, name: str) -> None: +def remove_machine(flake_name: FlakeName, group: str, name: str) -> None: remove_member(flake_name, machines_folder(flake_name, group), name) @@ -178,7 +179,7 @@ def add_group_argument(parser: argparse.ArgumentParser) -> None: parser.add_argument("group", help="the name of the secret", type=group_name_type) -def add_secret(flake_name: str, group: str, name: str) -> None: +def add_secret(flake_name: FlakeName, group: str, name: str) -> None: secrets.allow_member( secrets.groups_folder(flake_name, name), sops_groups_folder(flake_name), group ) @@ -188,7 +189,7 @@ def add_secret_command(args: argparse.Namespace) -> None: add_secret(args.flake, args.group, args.secret) -def remove_secret(flake_name: str, group: str, name: str) -> None: +def remove_secret(flake_name: FlakeName, group: str, name: str) -> None: secrets.disallow_member(secrets.groups_folder(flake_name, name), group) diff --git a/pkgs/clan-cli/clan_cli/secrets/machines.py b/pkgs/clan-cli/clan_cli/secrets/machines.py index b78d52d8..bb01212e 100644 --- a/pkgs/clan-cli/clan_cli/secrets/machines.py +++ b/pkgs/clan-cli/clan_cli/secrets/machines.py @@ -1,5 +1,6 @@ import argparse +from ..flakes.types import FlakeName from ..machines.types import machine_name_type, validate_hostname from . import secrets from .folders import list_objects, remove_object, sops_machines_folder @@ -7,23 +8,23 @@ from .sops import read_key, write_key from .types import public_or_private_age_key_type, secret_name_type -def add_machine(flake_name: str, name: str, key: str, force: bool) -> None: +def add_machine(flake_name: FlakeName, name: str, key: str, force: bool) -> None: write_key(sops_machines_folder(flake_name) / name, key, force) -def remove_machine(flake_name: str, name: str) -> None: +def remove_machine(flake_name: FlakeName, name: str) -> None: remove_object(sops_machines_folder(flake_name), name) -def get_machine(flake_name: str, name: str) -> str: +def get_machine(flake_name: FlakeName, name: str) -> str: return read_key(sops_machines_folder(flake_name) / name) -def has_machine(flake_name: str, name: str) -> bool: +def has_machine(flake_name: FlakeName, name: str) -> bool: return (sops_machines_folder(flake_name) / name / "key.json").exists() -def list_machines(flake_name: str) -> list[str]: +def list_machines(flake_name: FlakeName) -> list[str]: path = sops_machines_folder(flake_name) def validate(name: str) -> bool: @@ -32,7 +33,7 @@ def list_machines(flake_name: str) -> list[str]: return list_objects(path, validate) -def add_secret(flake_name: str, machine: str, secret: str) -> None: +def add_secret(flake_name: FlakeName, machine: str, secret: str) -> None: secrets.allow_member( secrets.machines_folder(flake_name, secret), sops_machines_folder(flake_name), @@ -40,7 +41,7 @@ def add_secret(flake_name: str, machine: str, secret: str) -> None: ) -def remove_secret(flake_name: str, machine: str, secret: str) -> None: +def remove_secret(flake_name: FlakeName, machine: str, secret: str) -> None: secrets.disallow_member(secrets.machines_folder(flake_name, secret), machine) diff --git a/pkgs/clan-cli/clan_cli/secrets/secrets.py b/pkgs/clan-cli/clan_cli/secrets/secrets.py index 62065fd6..d7a29542 100644 --- a/pkgs/clan-cli/clan_cli/secrets/secrets.py +++ b/pkgs/clan-cli/clan_cli/secrets/secrets.py @@ -8,6 +8,7 @@ from typing import IO from .. import tty from ..errors import ClanError +from ..flakes.types import FlakeName from .folders import ( list_objects, sops_groups_folder, @@ -53,7 +54,7 @@ def collect_keys_for_path(path: Path) -> set[str]: def encrypt_secret( - flake_name: str, + flake_name: FlakeName, secret: Path, value: IO[str] | str | None, add_users: list[str] = [], @@ -101,7 +102,7 @@ def encrypt_secret( encrypt_file(secret / "secret", value, list(sorted(keys))) -def remove_secret(flake_name: str, secret: str) -> None: +def remove_secret(flake_name: FlakeName, secret: str) -> None: path = sops_secrets_folder(flake_name) / secret if not path.exists(): raise ClanError(f"Secret '{secret}' does not exist") @@ -116,15 +117,15 @@ def add_secret_argument(parser: argparse.ArgumentParser) -> None: parser.add_argument("secret", help="the name of the secret", type=secret_name_type) -def machines_folder(flake_name: str, group: str) -> Path: +def machines_folder(flake_name: FlakeName, group: str) -> Path: return sops_secrets_folder(flake_name) / group / "machines" -def users_folder(flake_name: str, group: str) -> Path: +def users_folder(flake_name: FlakeName, group: str) -> Path: return sops_secrets_folder(flake_name) / group / "users" -def groups_folder(flake_name: str, group: str) -> Path: +def groups_folder(flake_name: FlakeName, group: str) -> Path: return sops_secrets_folder(flake_name) / group / "groups" @@ -188,11 +189,11 @@ def disallow_member(group_folder: Path, name: str) -> None: ) -def has_secret(flake_name: str, secret: str) -> bool: +def has_secret(flake_name: FlakeName, secret: str) -> bool: return (sops_secrets_folder(flake_name) / secret / "secret").exists() -def list_secrets(flake_name: str) -> list[str]: +def list_secrets(flake_name: FlakeName) -> list[str]: path = sops_secrets_folder(flake_name) def validate(name: str) -> bool: @@ -209,7 +210,7 @@ def list_command(args: argparse.Namespace) -> None: print("\n".join(lst)) -def decrypt_secret(flake_name: str, secret: str) -> str: +def decrypt_secret(flake_name: FlakeName, secret: str) -> str: ensure_sops_key(flake_name) secret_path = sops_secrets_folder(flake_name) / secret / "secret" if not secret_path.exists(): diff --git a/pkgs/clan-cli/clan_cli/secrets/sops.py b/pkgs/clan-cli/clan_cli/secrets/sops.py index c9a389bb..c6f2b2bf 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops.py @@ -9,6 +9,7 @@ from typing import IO, Iterator from ..dirs import user_config_dir from ..errors import ClanError +from ..flakes.types import FlakeName from ..nix import nix_shell from .folders import sops_machines_folder, sops_users_folder @@ -51,7 +52,7 @@ def generate_private_key() -> tuple[str, str]: raise ClanError("Failed to generate private sops key") from e -def get_user_name(flake_name: str, user: str) -> str: +def get_user_name(flake_name: FlakeName, user: str) -> str: """Ask the user for their name until a unique one is provided.""" while True: name = input( @@ -64,7 +65,7 @@ def get_user_name(flake_name: str, user: str) -> str: print(f"{sops_users_folder(flake_name) / user} already exists") -def ensure_user_or_machine(flake_name: str, pub_key: str) -> SopsKey: +def ensure_user_or_machine(flake_name: FlakeName, pub_key: str) -> SopsKey: key = SopsKey(pub_key, username="") folders = [sops_users_folder(flake_name), sops_machines_folder(flake_name)] for folder in folders: @@ -90,7 +91,7 @@ def default_sops_key_path() -> Path: return user_config_dir() / "sops" / "age" / "keys.txt" -def ensure_sops_key(flake_name: str) -> SopsKey: +def ensure_sops_key(flake_name: FlakeName) -> SopsKey: key = os.environ.get("SOPS_AGE_KEY") if key: return ensure_user_or_machine(flake_name, get_public_key(key)) diff --git a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py index f6657a5a..fdac72df 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py @@ -11,13 +11,14 @@ from clan_cli.nix import nix_shell from ..dirs import specific_flake_dir from ..errors import ClanError +from ..flakes.types import FlakeName from .folders import sops_secrets_folder from .machines import add_machine, has_machine from .secrets import decrypt_secret, encrypt_secret, has_secret from .sops import generate_private_key -def generate_host_key(flake_name: str, machine_name: str) -> None: +def generate_host_key(flake_name: FlakeName, machine_name: str) -> None: if has_machine(flake_name, machine_name): return priv_key, pub_key = generate_private_key() @@ -30,7 +31,7 @@ def generate_host_key(flake_name: str, machine_name: str) -> None: def generate_secrets_group( - flake_name: str, + flake_name: FlakeName, secret_group: str, machine_name: str, tempdir: Path, @@ -88,7 +89,7 @@ export secrets={shlex.quote(str(secrets_dir))} # this is called by the sops.nix clan core module def generate_secrets_from_nix( - flake_name: str, + flake_name: FlakeName, machine_name: str, secret_submodules: dict[str, Any], ) -> None: @@ -112,7 +113,7 @@ def generate_secrets_from_nix( # this is called by the sops.nix clan core module def upload_age_key_from_nix( - flake_name: str, + flake_name: FlakeName, machine_name: str, ) -> None: secret_name = f"{machine_name}-age.key" diff --git a/pkgs/clan-cli/clan_cli/secrets/users.py b/pkgs/clan-cli/clan_cli/secrets/users.py index a653f295..c5b51e99 100644 --- a/pkgs/clan-cli/clan_cli/secrets/users.py +++ b/pkgs/clan-cli/clan_cli/secrets/users.py @@ -1,5 +1,6 @@ import argparse +from ..flakes.types import FlakeName from . import secrets from .folders import list_objects, remove_object, sops_users_folder from .sops import read_key, write_key @@ -11,19 +12,19 @@ from .types import ( ) -def add_user(flake_name: str, name: str, key: str, force: bool) -> None: +def add_user(flake_name: FlakeName, name: str, key: str, force: bool) -> None: write_key(sops_users_folder(flake_name) / name, key, force) -def remove_user(flake_name: str, name: str) -> None: +def remove_user(flake_name: FlakeName, name: str) -> None: remove_object(sops_users_folder(flake_name), name) -def get_user(flake_name: str, name: str) -> str: +def get_user(flake_name: FlakeName, name: str) -> str: return read_key(sops_users_folder(flake_name) / name) -def list_users(flake_name: str) -> list[str]: +def list_users(flake_name: FlakeName) -> list[str]: path = sops_users_folder(flake_name) def validate(name: str) -> bool: @@ -35,13 +36,13 @@ def list_users(flake_name: str) -> list[str]: return list_objects(path, validate) -def add_secret(flake_name: str, user: str, secret: str) -> None: +def add_secret(flake_name: FlakeName, user: str, secret: str) -> None: secrets.allow_member( secrets.users_folder(flake_name, secret), sops_users_folder(flake_name), user ) -def remove_secret(flake_name: str, user: str, secret: str) -> None: +def remove_secret(flake_name: FlakeName, user: str, secret: str) -> None: secrets.disallow_member(secrets.users_folder(flake_name, secret), user) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index 0b6fd281..9668b8b4 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -10,6 +10,7 @@ from ...config.machine import ( set_config_for_machine, verify_machine_config, ) +from ...flakes.types import FlakeName from ...machines.create import create_machine as _create_machine from ...machines.list import list_machines as _list_machines from ..api_outputs import ( @@ -28,7 +29,7 @@ router = APIRouter() @router.get("/api/{flake_name}/machines") -async def list_machines(flake_name: str) -> MachinesResponse: +async def list_machines(flake_name: FlakeName) -> MachinesResponse: machines = [] for m in _list_machines(flake_name): machines.append(Machine(name=m, status=Status.UNKNOWN)) @@ -37,7 +38,7 @@ async def list_machines(flake_name: str) -> MachinesResponse: @router.post("/api/{flake_name}/machines", status_code=201) async def create_machine( - flake_name: str, machine: Annotated[MachineCreate, Body()] + flake_name: FlakeName, machine: Annotated[MachineCreate, Body()] ) -> MachineResponse: out = await _create_machine(flake_name, machine.name) log.debug(out) @@ -51,21 +52,21 @@ async def get_machine(name: str) -> MachineResponse: @router.get("/api/{flake_name}/machines/{name}/config") -async def get_machine_config(flake_name: str, name: str) -> ConfigResponse: +async def get_machine_config(flake_name: FlakeName, name: str) -> ConfigResponse: config = config_for_machine(flake_name, name) return ConfigResponse(config=config) @router.put("/api/{flake_name}/machines/{name}/config") async def set_machine_config( - flake_name: str, name: str, config: Annotated[dict, Body()] + flake_name: FlakeName, name: str, config: Annotated[dict, Body()] ) -> ConfigResponse: set_config_for_machine(flake_name, name, config) return ConfigResponse(config=config) @router.get("/api/{flake_name}/machines/{name}/schema") -async def get_machine_schema(flake_name: str, name: str) -> SchemaResponse: +async def get_machine_schema(flake_name: FlakeName, name: str) -> SchemaResponse: schema = schema_for_machine(flake_name, name) return SchemaResponse(schema=schema) diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index e488440a..b775790d 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -8,6 +8,7 @@ import pytest from root import CLAN_CORE from clan_cli.dirs import nixpkgs_source +from clan_cli.flakes.types import FlakeName # substitutes string sin a file. @@ -28,13 +29,13 @@ def substitute( class TestFlake(NamedTuple): - name: str + name: FlakeName path: Path def create_flake( monkeypatch: pytest.MonkeyPatch, - flake_name: str, + flake_name: FlakeName, clan_core_flake: Path | None = None, machines: list[str] = [], remote: bool = False, @@ -74,7 +75,7 @@ def create_flake( @pytest.fixture def test_flake(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]: - yield from create_flake(monkeypatch, "test_flake") + yield from create_flake(monkeypatch, FlakeName("test_flake")) @pytest.fixture @@ -83,7 +84,7 @@ def test_flake_with_core(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake] raise Exception( "clan-core flake not found. This test requires the clan-core flake to be present" ) - yield from create_flake(monkeypatch, "test_flake_with_core", CLAN_CORE) + yield from create_flake(monkeypatch, FlakeName("test_flake_with_core"), CLAN_CORE) @pytest.fixture @@ -94,4 +95,6 @@ def test_flake_with_core_and_pass( raise Exception( "clan-core flake not found. This test requires the clan-core flake to be present" ) - yield from create_flake(monkeypatch, "test_flake_with_core_and_pass", CLAN_CORE) + yield from create_flake( + monkeypatch, FlakeName("test_flake_with_core_and_pass"), CLAN_CORE + ) diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index 25bedf2f..1de515d5 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -9,6 +9,8 @@ from fixtures_flakes import TestFlake, create_flake from httpx import SyncByteStream from root import CLAN_CORE +from clan_cli.flakes.types import FlakeName + if TYPE_CHECKING: from age_keys import KeyPair @@ -17,7 +19,7 @@ if TYPE_CHECKING: def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]: yield from create_flake( monkeypatch, - "test_flake_with_core_dynamic_machines", + FlakeName("test_flake_with_core_dynamic_machines"), CLAN_CORE, machines=["vm_with_secrets"], ) @@ -29,7 +31,7 @@ def remote_flake_with_vm_without_secrets( ) -> Iterator[TestFlake]: yield from create_flake( monkeypatch, - "test_flake_with_core_dynamic_machines", + FlakeName("test_flake_with_core_dynamic_machines"), CLAN_CORE, machines=["vm_without_secrets"], remote=True, From 8cc1c2c4bdc4527ced8c14adc18140ff32f42d40 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Mon, 16 Oct 2023 15:03:53 +0200 Subject: [PATCH 06/36] Fixed cyclic dependencie AND swapped pytest-parallel for pytest-xdist to fix deadlock in tests --- pkgs/clan-cli/clan_cli/config/__init__.py | 2 +- pkgs/clan-cli/clan_cli/config/machine.py | 2 +- pkgs/clan-cli/clan_cli/dirs.py | 2 +- pkgs/clan-cli/clan_cli/machines/create.py | 2 +- pkgs/clan-cli/clan_cli/machines/facts.py | 2 +- pkgs/clan-cli/clan_cli/machines/list.py | 2 +- pkgs/clan-cli/clan_cli/secrets/folders.py | 2 +- pkgs/clan-cli/clan_cli/secrets/groups.py | 2 +- pkgs/clan-cli/clan_cli/secrets/machines.py | 2 +- pkgs/clan-cli/clan_cli/secrets/secrets.py | 2 +- pkgs/clan-cli/clan_cli/secrets/sops.py | 2 +- pkgs/clan-cli/clan_cli/secrets/sops_generate.py | 2 +- pkgs/clan-cli/clan_cli/secrets/users.py | 2 +- pkgs/clan-cli/clan_cli/{flakes => }/types.py | 0 .../clan-cli/clan_cli/webui/routers/machines.py | 2 +- pkgs/clan-cli/default.nix | 4 +++- pkgs/clan-cli/pyproject.toml | 6 +++++- pkgs/clan-cli/shell.nix | 3 ++- pkgs/clan-cli/tests/fixtures_flakes.py | 16 ++++++++-------- pkgs/clan-cli/tests/helpers/cli.py | 7 ++++++- pkgs/clan-cli/tests/test_machines_config.py | 4 ++-- pkgs/clan-cli/tests/test_secrets_cli.py | 17 +++++++++++------ pkgs/clan-cli/tests/test_secrets_generate.py | 4 ++-- .../tests/test_secrets_password_store.py | 4 ++-- pkgs/clan-cli/tests/test_vms_api_create.py | 8 ++++---- 25 files changed, 59 insertions(+), 42 deletions(-) rename pkgs/clan-cli/clan_cli/{flakes => }/types.py (100%) diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index ec3df55f..04ffbb68 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -11,7 +11,7 @@ from typing import Any, Optional, Tuple, get_origin from clan_cli.dirs import machine_settings_file, specific_flake_dir from clan_cli.errors import ClanError -from clan_cli.flakes.types import FlakeName +from clan_cli.types import FlakeName from clan_cli.git import commit_file from clan_cli.nix import nix_eval diff --git a/pkgs/clan-cli/clan_cli/config/machine.py b/pkgs/clan-cli/clan_cli/config/machine.py index 1a5582cf..cd9957ce 100644 --- a/pkgs/clan-cli/clan_cli/config/machine.py +++ b/pkgs/clan-cli/clan_cli/config/machine.py @@ -15,7 +15,7 @@ from clan_cli.dirs import ( from clan_cli.git import commit_file, find_git_repo_root from clan_cli.nix import nix_eval -from ..flakes.types import FlakeName +from ..types import FlakeName def verify_machine_config( diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index 17a0bc13..1d72a8fb 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Optional from .errors import ClanError -from .flakes.types import FlakeName +from .types import FlakeName def _get_clan_flake_toplevel() -> Path: diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py index a4880f0e..153f2850 100644 --- a/pkgs/clan-cli/clan_cli/machines/create.py +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -5,7 +5,7 @@ from typing import Dict from ..async_cmd import CmdOut, run, runforcli from ..dirs import specific_flake_dir, specific_machine_dir from ..errors import ClanError -from ..flakes.types import FlakeName +from ..types import FlakeName from ..nix import nix_shell log = logging.getLogger(__name__) diff --git a/pkgs/clan-cli/clan_cli/machines/facts.py b/pkgs/clan-cli/clan_cli/machines/facts.py index 7b665b53..3f148ccf 100644 --- a/pkgs/clan-cli/clan_cli/machines/facts.py +++ b/pkgs/clan-cli/clan_cli/machines/facts.py @@ -1,5 +1,5 @@ from ..dirs import specific_machine_dir -from ..flakes.types import FlakeName +from ..types import FlakeName def machine_has_fact(flake_name: FlakeName, machine: str, fact: str) -> bool: diff --git a/pkgs/clan-cli/clan_cli/machines/list.py b/pkgs/clan-cli/clan_cli/machines/list.py index d78adb23..079bcd73 100644 --- a/pkgs/clan-cli/clan_cli/machines/list.py +++ b/pkgs/clan-cli/clan_cli/machines/list.py @@ -3,7 +3,7 @@ import logging import os from ..dirs import machines_dir -from ..flakes.types import FlakeName +from ..types import FlakeName from .types import validate_hostname log = logging.getLogger(__name__) diff --git a/pkgs/clan-cli/clan_cli/secrets/folders.py b/pkgs/clan-cli/clan_cli/secrets/folders.py index 8a551cc7..3f23c126 100644 --- a/pkgs/clan-cli/clan_cli/secrets/folders.py +++ b/pkgs/clan-cli/clan_cli/secrets/folders.py @@ -5,7 +5,7 @@ from typing import Callable from ..dirs import specific_flake_dir from ..errors import ClanError -from ..flakes.types import FlakeName +from ..types import FlakeName def get_sops_folder(flake_name: FlakeName) -> Path: diff --git a/pkgs/clan-cli/clan_cli/secrets/groups.py b/pkgs/clan-cli/clan_cli/secrets/groups.py index b944560c..f94b9d9c 100644 --- a/pkgs/clan-cli/clan_cli/secrets/groups.py +++ b/pkgs/clan-cli/clan_cli/secrets/groups.py @@ -3,7 +3,7 @@ import os from pathlib import Path from ..errors import ClanError -from ..flakes.types import FlakeName +from ..types import FlakeName from ..machines.types import machine_name_type, validate_hostname from . import secrets from .folders import ( diff --git a/pkgs/clan-cli/clan_cli/secrets/machines.py b/pkgs/clan-cli/clan_cli/secrets/machines.py index bb01212e..9c642615 100644 --- a/pkgs/clan-cli/clan_cli/secrets/machines.py +++ b/pkgs/clan-cli/clan_cli/secrets/machines.py @@ -1,6 +1,6 @@ import argparse -from ..flakes.types import FlakeName +from ..types import FlakeName from ..machines.types import machine_name_type, validate_hostname from . import secrets from .folders import list_objects, remove_object, sops_machines_folder diff --git a/pkgs/clan-cli/clan_cli/secrets/secrets.py b/pkgs/clan-cli/clan_cli/secrets/secrets.py index d7a29542..9b801d96 100644 --- a/pkgs/clan-cli/clan_cli/secrets/secrets.py +++ b/pkgs/clan-cli/clan_cli/secrets/secrets.py @@ -8,7 +8,7 @@ from typing import IO from .. import tty from ..errors import ClanError -from ..flakes.types import FlakeName +from ..types import FlakeName from .folders import ( list_objects, sops_groups_folder, diff --git a/pkgs/clan-cli/clan_cli/secrets/sops.py b/pkgs/clan-cli/clan_cli/secrets/sops.py index c6f2b2bf..4f35761a 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops.py @@ -9,7 +9,7 @@ from typing import IO, Iterator from ..dirs import user_config_dir from ..errors import ClanError -from ..flakes.types import FlakeName +from ..types import FlakeName from ..nix import nix_shell from .folders import sops_machines_folder, sops_users_folder diff --git a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py index fdac72df..a8af2ece 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py @@ -11,7 +11,7 @@ from clan_cli.nix import nix_shell from ..dirs import specific_flake_dir from ..errors import ClanError -from ..flakes.types import FlakeName +from ..types import FlakeName from .folders import sops_secrets_folder from .machines import add_machine, has_machine from .secrets import decrypt_secret, encrypt_secret, has_secret diff --git a/pkgs/clan-cli/clan_cli/secrets/users.py b/pkgs/clan-cli/clan_cli/secrets/users.py index c5b51e99..98afa876 100644 --- a/pkgs/clan-cli/clan_cli/secrets/users.py +++ b/pkgs/clan-cli/clan_cli/secrets/users.py @@ -1,6 +1,6 @@ import argparse -from ..flakes.types import FlakeName +from ..types import FlakeName from . import secrets from .folders import list_objects, remove_object, sops_users_folder from .sops import read_key, write_key diff --git a/pkgs/clan-cli/clan_cli/flakes/types.py b/pkgs/clan-cli/clan_cli/types.py similarity index 100% rename from pkgs/clan-cli/clan_cli/flakes/types.py rename to pkgs/clan-cli/clan_cli/types.py diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index 9668b8b4..b112959a 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -10,7 +10,7 @@ from ...config.machine import ( set_config_for_machine, verify_machine_config, ) -from ...flakes.types import FlakeName +from ...types import FlakeName from ...machines.create import create_machine as _create_machine from ...machines.list import list_machines as _list_machines from ..api_outputs import ( diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 63dc4fb3..c9fccd38 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -8,6 +8,7 @@ , openssh , pytest , pytest-cov +, pytest-xdist , pytest-subprocess , pytest-parallel , pytest-timeout @@ -45,7 +46,8 @@ let pytest pytest-cov pytest-subprocess - pytest-parallel + # pytest-parallel + pytest-xdist pytest-timeout openssh git diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index a79978cb..60d43f27 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -14,9 +14,13 @@ exclude = ["clan_cli.nixpkgs*"] [tool.setuptools.package-data] clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"] + + [tool.pytest.ini_options] +testpaths = "tests" faulthandler_timeout = 60 -addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --workers auto --durations 5" +log_level = "DEBUG" +addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail -n auto --durations 5 --maxfail=1 --new-first" norecursedirs = "tests/helpers" markers = [ "impure" ] diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix index 2d443998..8b93571e 100644 --- a/pkgs/clan-cli/shell.nix +++ b/pkgs/clan-cli/shell.nix @@ -53,6 +53,7 @@ mkShell { register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan - ./bin/clan machines create example + ./bin/clan flakes create example_clan + ./bin/clan machines create example_machine example_clan ''; } diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index b775790d..9132adcd 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -8,7 +8,7 @@ import pytest from root import CLAN_CORE from clan_cli.dirs import nixpkgs_source -from clan_cli.flakes.types import FlakeName +from clan_cli.types import FlakeName # substitutes string sin a file. @@ -28,7 +28,7 @@ def substitute( print(line, end="") -class TestFlake(NamedTuple): +class FlakeForTest(NamedTuple): name: FlakeName path: Path @@ -39,7 +39,7 @@ def create_flake( clan_core_flake: Path | None = None, machines: list[str] = [], remote: bool = False, -) -> Iterator[TestFlake]: +) -> Iterator[FlakeForTest]: """ Creates a flake with the given name and machines. The machine names map to the machines in ./test_machines @@ -66,20 +66,20 @@ def create_flake( with tempfile.TemporaryDirectory() as workdir: monkeypatch.chdir(workdir) monkeypatch.setenv("HOME", str(home)) - yield TestFlake(flake_name, flake) + yield FlakeForTest(flake_name, flake) else: monkeypatch.chdir(flake) monkeypatch.setenv("HOME", str(home)) - yield TestFlake(flake_name, flake) + yield FlakeForTest(flake_name, flake) @pytest.fixture -def test_flake(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]: +def test_flake(monkeypatch: pytest.MonkeyPatch) -> Iterator[FlakeForTest]: yield from create_flake(monkeypatch, FlakeName("test_flake")) @pytest.fixture -def test_flake_with_core(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]: +def test_flake_with_core(monkeypatch: pytest.MonkeyPatch) -> Iterator[FlakeForTest]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( "clan-core flake not found. This test requires the clan-core flake to be present" @@ -90,7 +90,7 @@ def test_flake_with_core(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake] @pytest.fixture def test_flake_with_core_and_pass( monkeypatch: pytest.MonkeyPatch, -) -> Iterator[TestFlake]: +) -> Iterator[FlakeForTest]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( "clan-core flake not found. This test requires the clan-core flake to be present" diff --git a/pkgs/clan-cli/tests/helpers/cli.py b/pkgs/clan-cli/tests/helpers/cli.py index ea633c2b..dbfb066a 100644 --- a/pkgs/clan-cli/tests/helpers/cli.py +++ b/pkgs/clan-cli/tests/helpers/cli.py @@ -1,13 +1,18 @@ import argparse from clan_cli import create_parser - +import logging +import sys +import shlex +log = logging.getLogger(__name__) class Cli: def __init__(self) -> None: self.parser = create_parser(prog="clan") def run(self, args: list[str]) -> argparse.Namespace: + cmd = shlex.join(["clan"] + args) + log.debug(f"Command: {cmd}") parsed = self.parser.parse_args(args) if hasattr(parsed, "func"): parsed.func(parsed) diff --git a/pkgs/clan-cli/tests/test_machines_config.py b/pkgs/clan-cli/tests/test_machines_config.py index a7ab422a..ee07ad01 100644 --- a/pkgs/clan-cli/tests/test_machines_config.py +++ b/pkgs/clan-cli/tests/test_machines_config.py @@ -1,8 +1,8 @@ -from fixtures_flakes import TestFlake +from fixtures_flakes import FlakeForTest from clan_cli.config import machine -def test_schema_for_machine(test_flake: TestFlake) -> None: +def test_schema_for_machine(test_flake: FlakeForTest) -> None: schema = machine.schema_for_machine(test_flake.name, "machine1") assert "properties" in schema diff --git a/pkgs/clan-cli/tests/test_secrets_cli.py b/pkgs/clan-cli/tests/test_secrets_cli.py index c93c63df..f18a3d09 100644 --- a/pkgs/clan-cli/tests/test_secrets_cli.py +++ b/pkgs/clan-cli/tests/test_secrets_cli.py @@ -3,23 +3,27 @@ from contextlib import contextmanager from pathlib import Path from typing import TYPE_CHECKING, Iterator +import logging import pytest from cli import Cli +from fixtures_flakes import FlakeForTest from clan_cli.errors import ClanError if TYPE_CHECKING: from age_keys import KeyPair +log = logging.getLogger(__name__) + def _test_identities( what: str, - test_flake: Path, + test_flake: FlakeForTest, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"], ) -> None: cli = Cli() - sops_folder = test_flake / "sops" + sops_folder = test_flake.path / "sops" cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey]) assert (sops_folder / what / "foo" / "key.json").exists() @@ -34,6 +38,7 @@ def _test_identities( "-f", "foo", age_keys[0].privkey, + test_flake.name, ] ) @@ -60,19 +65,19 @@ def _test_identities( def test_users( - test_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] + test_flake: FlakeForTest, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] ) -> None: _test_identities("users", test_flake, capsys, age_keys) def test_machines( - test_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] + test_flake: FlakeForTest, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] ) -> None: _test_identities("machines", test_flake, capsys, age_keys) def test_groups( - test_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] + test_flake: FlakeForTest, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] ) -> None: cli = Cli() capsys.readouterr() # empty the buffer @@ -100,7 +105,7 @@ def test_groups( cli.run(["secrets", "groups", "remove-user", "group1", "user1"]) cli.run(["secrets", "groups", "remove-machine", "group1", "machine1"]) - groups = os.listdir(test_flake / "sops" / "groups") + groups = os.listdir(test_flake.path / "sops" / "groups") assert len(groups) == 0 diff --git a/pkgs/clan-cli/tests/test_secrets_generate.py b/pkgs/clan-cli/tests/test_secrets_generate.py index 22632316..5066d1ec 100644 --- a/pkgs/clan-cli/tests/test_secrets_generate.py +++ b/pkgs/clan-cli/tests/test_secrets_generate.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING import pytest from cli import Cli -from fixtures_flakes import TestFlake +from fixtures_flakes import FlakeForTest from clan_cli.machines.facts import machine_get_fact from clan_cli.secrets.folders import sops_secrets_folder @@ -15,7 +15,7 @@ if TYPE_CHECKING: @pytest.mark.impure def test_generate_secret( monkeypatch: pytest.MonkeyPatch, - test_flake_with_core: TestFlake, + test_flake_with_core: FlakeForTest, age_keys: list["KeyPair"], ) -> None: monkeypatch.chdir(test_flake_with_core.path) diff --git a/pkgs/clan-cli/tests/test_secrets_password_store.py b/pkgs/clan-cli/tests/test_secrets_password_store.py index 999f08a5..27e43520 100644 --- a/pkgs/clan-cli/tests/test_secrets_password_store.py +++ b/pkgs/clan-cli/tests/test_secrets_password_store.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest from cli import Cli -from fixtures_flakes import TestFlake +from fixtures_flakes import FlakeForTest from clan_cli.machines.facts import machine_get_fact from clan_cli.nix import nix_shell @@ -13,7 +13,7 @@ from clan_cli.ssh import HostGroup @pytest.mark.impure def test_upload_secret( monkeypatch: pytest.MonkeyPatch, - test_flake_with_core_and_pass: TestFlake, + test_flake_with_core_and_pass: FlakeForTest, temporary_dir: Path, host_group: HostGroup, ) -> None: diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index 1de515d5..538d203d 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -5,18 +5,18 @@ from typing import TYPE_CHECKING, Iterator import pytest from api import TestClient from cli import Cli -from fixtures_flakes import TestFlake, create_flake +from fixtures_flakes import FlakeForTest, create_flake from httpx import SyncByteStream from root import CLAN_CORE -from clan_cli.flakes.types import FlakeName +from clan_cli.types import FlakeName if TYPE_CHECKING: from age_keys import KeyPair @pytest.fixture -def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]: +def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, FlakeName("test_flake_with_core_dynamic_machines"), @@ -28,7 +28,7 @@ def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[Test @pytest.fixture def remote_flake_with_vm_without_secrets( monkeypatch: pytest.MonkeyPatch, -) -> Iterator[TestFlake]: +) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, FlakeName("test_flake_with_core_dynamic_machines"), From 03cabda2d4a78757b14f22ff8cab2b7e7af02389 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Mon, 16 Oct 2023 15:19:25 +0200 Subject: [PATCH 07/36] Improved test logging with frame inspection --- pkgs/clan-cli/tests/helpers/cli.py | 11 +++++++++++ pkgs/clan-cli/tests/test_secrets_cli.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pkgs/clan-cli/tests/helpers/cli.py b/pkgs/clan-cli/tests/helpers/cli.py index dbfb066a..46e4989f 100644 --- a/pkgs/clan-cli/tests/helpers/cli.py +++ b/pkgs/clan-cli/tests/helpers/cli.py @@ -6,6 +6,16 @@ import sys import shlex log = logging.getLogger(__name__) +import inspect + +def get_caller() -> str: + frame = inspect.currentframe() + caller_frame = frame.f_back.f_back + frame_info = inspect.getframeinfo(caller_frame) + ret = f"{frame_info.filename}:{frame_info.lineno}::{frame_info.function}" + return ret + + class Cli: def __init__(self) -> None: self.parser = create_parser(prog="clan") @@ -13,6 +23,7 @@ class Cli: def run(self, args: list[str]) -> argparse.Namespace: cmd = shlex.join(["clan"] + args) log.debug(f"Command: {cmd}") + log.debug(f"Caller {get_caller()}") parsed = self.parser.parse_args(args) if hasattr(parsed, "func"): parsed.func(parsed) diff --git a/pkgs/clan-cli/tests/test_secrets_cli.py b/pkgs/clan-cli/tests/test_secrets_cli.py index f18a3d09..d68b91a1 100644 --- a/pkgs/clan-cli/tests/test_secrets_cli.py +++ b/pkgs/clan-cli/tests/test_secrets_cli.py @@ -25,7 +25,7 @@ def _test_identities( cli = Cli() sops_folder = test_flake.path / "sops" - cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey]) + cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey, test_flake.name]) assert (sops_folder / what / "foo" / "key.json").exists() with pytest.raises(ClanError): cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey]) From 8482bc79f6e6e90513be0bc899ad6c66e80998b2 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Tue, 17 Oct 2023 23:49:16 +0200 Subject: [PATCH 08/36] Secrets tests passing. nix fmt doesn't complain --- pkgs/clan-cli/clan_cli/__init__.py | 11 ++ pkgs/clan-cli/clan_cli/custom_logger.py | 20 ++-- pkgs/clan-cli/clan_cli/dirs.py | 13 ++- pkgs/clan-cli/clan_cli/flakes/create.py | 8 +- pkgs/clan-cli/clan_cli/secrets/groups.py | 43 +++++++ pkgs/clan-cli/clan_cli/secrets/machines.py | 36 +++--- pkgs/clan-cli/clan_cli/secrets/secrets.py | 29 ++++- pkgs/clan-cli/clan_cli/secrets/users.py | 10 ++ pkgs/clan-cli/pyproject.toml | 3 +- pkgs/clan-cli/tests/fixtures_flakes.py | 89 ++++++++------- pkgs/clan-cli/tests/helpers/cli.py | 19 +++- pkgs/clan-cli/tests/temporary_dir.py | 8 +- pkgs/clan-cli/tests/test_secrets_cli.py | 123 ++++++++++++--------- pkgs/clan-cli/tests/test_vms_api_create.py | 8 +- 14 files changed, 281 insertions(+), 139 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 68a07ae1..48f554aa 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -6,6 +6,11 @@ from typing import Optional from . import config, flakes, join, machines, secrets, vms, webui from .ssh import cli as ssh_cli +import logging +from .custom_logger import register + +log = logging.getLogger(__name__) + argcomplete: Optional[ModuleType] = None try: import argcomplete # type: ignore[no-redef] @@ -52,6 +57,10 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser: parser_vms = subparsers.add_parser("vms", help="manage virtual machines") vms.register_parser(parser_vms) +# if args.debug: + register(logging.DEBUG) + log.debug("Debug log activated") + if argcomplete: argcomplete.autocomplete(parser) @@ -65,6 +74,8 @@ def main() -> None: parser = create_parser() args = parser.parse_args() + + if not hasattr(args, "func"): return diff --git a/pkgs/clan-cli/clan_cli/custom_logger.py b/pkgs/clan-cli/clan_cli/custom_logger.py index d8b7f9fa..1be10dae 100644 --- a/pkgs/clan-cli/clan_cli/custom_logger.py +++ b/pkgs/clan-cli/clan_cli/custom_logger.py @@ -1,5 +1,6 @@ import logging -from typing import Any +from typing import Any, Callable +from pathlib import Path grey = "\x1b[38;20m" yellow = "\x1b[33;20m" @@ -9,11 +10,14 @@ green = "\u001b[32m" blue = "\u001b[34m" -def get_formatter(color: str) -> logging.Formatter: - reset = "\x1b[0m" - return logging.Formatter( - f"{color}%(levelname)s{reset}:(%(filename)s:%(lineno)d): %(message)s" - ) +def get_formatter(color: str) -> Callable[[logging.LogRecord], logging.Formatter]: + def myformatter(record: logging.LogRecord) -> logging.Formatter: + reset = "\x1b[0m" + filepath = Path(record.pathname).resolve() + return logging.Formatter( + f"{filepath}:%(lineno)d::%(funcName)s\n{color}%(levelname)s{reset}: %(message)s" + ) + return myformatter FORMATTER = { @@ -26,8 +30,8 @@ FORMATTER = { class CustomFormatter(logging.Formatter): - def format(self, record: Any) -> str: - return FORMATTER[record.levelno].format(record) + def format(self, record: logging.LogRecord) -> str: + return FORMATTER[record.levelno](record).format(record) def register(level: Any) -> None: diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index 1d72a8fb..7bc3f748 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -2,10 +2,12 @@ import os import sys from pathlib import Path from typing import Optional +import logging from .errors import ClanError from .types import FlakeName +log = logging.getLogger(__name__) def _get_clan_flake_toplevel() -> Path: return find_toplevel([".clan-flake", ".git", ".hg", ".svn", "flake.nix"]) @@ -51,28 +53,31 @@ def user_data_dir() -> Path: def clan_data_dir() -> Path: path = user_data_dir() / "clan" if not path.exists(): - path.mkdir() + log.debug(f"Creating path with parents {path}") + path.mkdir(parents=True) return path.resolve() def clan_config_dir() -> Path: path = user_config_dir() / "clan" if not path.exists(): - path.mkdir() + log.debug(f"Creating path with parents {path}") + path.mkdir(parents=True) return path.resolve() def clan_flakes_dir() -> Path: path = clan_data_dir() / "flake" if not path.exists(): - path.mkdir() + log.debug(f"Creating path with parents {path}") + path.mkdir(parents=True) return path.resolve() def specific_flake_dir(flake_name: FlakeName) -> Path: flake_dir = clan_flakes_dir() / flake_name if not flake_dir.exists(): - raise ClanError(f"Flake {flake_name} does not exist") + raise ClanError(f"Flake '{flake_name}' does not exist") return flake_dir diff --git a/pkgs/clan-cli/clan_cli/flakes/create.py b/pkgs/clan-cli/clan_cli/flakes/create.py index e35626d3..c9d1d539 100644 --- a/pkgs/clan-cli/clan_cli/flakes/create.py +++ b/pkgs/clan-cli/clan_cli/flakes/create.py @@ -55,7 +55,7 @@ async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: def create_flake_command(args: argparse.Namespace) -> None: flake_dir = clan_flakes_dir() / args.name - runforcli(create_flake, flake_dir, DEFAULT_URL) + runforcli(create_flake, flake_dir, args.url) # takes a (sub)parser and configures it @@ -65,5 +65,11 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None: type=str, help="name for the flake", ) + parser.add_argument( + "--url", + type=AnyUrl, + help="url for the flake", + default=DEFAULT_URL, + ) # parser.add_argument("name", type=str, help="name of the flake") parser.set_defaults(func=create_flake_command) diff --git a/pkgs/clan-cli/clan_cli/secrets/groups.py b/pkgs/clan-cli/clan_cli/secrets/groups.py index f94b9d9c..0a0fb2dd 100644 --- a/pkgs/clan-cli/clan_cli/secrets/groups.py +++ b/pkgs/clan-cli/clan_cli/secrets/groups.py @@ -204,9 +204,17 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: help="the command to run", required=True, ) + + # List groups list_parser = subparser.add_parser("list", help="list groups") + list_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) list_parser.set_defaults(func=list_command) + # Add user add_machine_parser = subparser.add_parser( "add-machine", help="add a machine to group" ) @@ -214,8 +222,14 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: add_machine_parser.add_argument( "machine", help="the name of the machines to add", type=machine_name_type ) + add_machine_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_machine_parser.set_defaults(func=add_machine_command) + # Remove machine remove_machine_parser = subparser.add_parser( "remove-machine", help="remove a machine from group" ) @@ -223,15 +237,27 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: remove_machine_parser.add_argument( "machine", help="the name of the machines to remove", type=machine_name_type ) + remove_machine_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_machine_parser.set_defaults(func=remove_machine_command) + # Add user add_user_parser = subparser.add_parser("add-user", help="add a user to group") add_group_argument(add_user_parser) add_user_parser.add_argument( "user", help="the name of the user to add", type=user_name_type ) + add_user_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_user_parser.set_defaults(func=add_user_command) + # Remove user remove_user_parser = subparser.add_parser( "remove-user", help="remove a user from group" ) @@ -239,8 +265,14 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: remove_user_parser.add_argument( "user", help="the name of the user to remove", type=user_name_type ) + remove_user_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_user_parser.set_defaults(func=remove_user_command) + # Add secret add_secret_parser = subparser.add_parser( "add-secret", help="allow a user to access a secret" ) @@ -250,8 +282,14 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: add_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + add_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_secret_parser.set_defaults(func=add_secret_command) + # Remove secret remove_secret_parser = subparser.add_parser( "remove-secret", help="remove a group's access to a secret" ) @@ -261,4 +299,9 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: remove_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + remove_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_secret_parser.set_defaults(func=remove_secret_command) diff --git a/pkgs/clan-cli/clan_cli/secrets/machines.py b/pkgs/clan-cli/clan_cli/secrets/machines.py index 9c642615..e683ec08 100644 --- a/pkgs/clan-cli/clan_cli/secrets/machines.py +++ b/pkgs/clan-cli/clan_cli/secrets/machines.py @@ -96,11 +96,6 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None: action="store_true", default=False, ) - add_parser.add_argument( - "flake", - type=str, - help="name of the flake to create machine for", - ) add_parser.add_argument( "machine", help="the name of the machine", type=machine_name_type ) @@ -109,6 +104,11 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None: help="public key or private key of the user", type=public_or_private_age_key_type, ) + add_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_parser.set_defaults(func=add_command) # Parser @@ -125,46 +125,46 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None: # Parser remove_parser = subparser.add_parser("remove", help="remove a machine") + remove_parser.add_argument( + "machine", help="the name of the machine", type=machine_name_type + ) remove_parser.add_argument( "flake", type=str, help="name of the flake to create machine for", ) - remove_parser.add_argument( - "machine", help="the name of the machine", type=machine_name_type - ) remove_parser.set_defaults(func=remove_command) # Parser add_secret_parser = subparser.add_parser( "add-secret", help="allow a machine to access a secret" ) - add_secret_parser.add_argument( - "flake", - type=str, - help="name of the flake to create machine for", - ) add_secret_parser.add_argument( "machine", help="the name of the machine", type=machine_name_type ) add_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + add_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_secret_parser.set_defaults(func=add_secret_command) # Parser remove_secret_parser = subparser.add_parser( "remove-secret", help="remove a group's access to a secret" ) - remove_secret_parser.add_argument( - "flake", - type=str, - help="name of the flake to create machine for", - ) remove_secret_parser.add_argument( "machine", help="the name of the group", type=machine_name_type ) remove_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + remove_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_secret_parser.set_defaults(func=remove_secret_command) diff --git a/pkgs/clan-cli/clan_cli/secrets/secrets.py b/pkgs/clan-cli/clan_cli/secrets/secrets.py index 9b801d96..2e41bd6a 100644 --- a/pkgs/clan-cli/clan_cli/secrets/secrets.py +++ b/pkgs/clan-cli/clan_cli/secrets/secrets.py @@ -253,24 +253,25 @@ def rename_command(args: argparse.Namespace) -> None: def register_secrets_parser(subparser: argparse._SubParsersAction) -> None: parser_list = subparser.add_parser("list", help="list secrets") + parser_list.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser_list.set_defaults(func=list_command) parser_get = subparser.add_parser("get", help="get a secret") add_secret_argument(parser_get) - parser_get.set_defaults(func=get_command) parser_get.add_argument( "flake", type=str, help="name of the flake to create machine for", ) + parser_get.set_defaults(func=get_command) + parser_set = subparser.add_parser("set", help="set a secret") add_secret_argument(parser_set) - parser_set.add_argument( - "flake", - type=str, - help="name of the flake to create machine for", - ) parser_set.add_argument( "--group", type=str, @@ -299,13 +300,29 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None: default=False, help="edit the secret with $EDITOR instead of pasting it", ) + parser_set.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser_set.set_defaults(func=set_command) parser_rename = subparser.add_parser("rename", help="rename a secret") add_secret_argument(parser_rename) parser_rename.add_argument("new_name", type=str, help="the new name of the secret") + parser_rename.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser_rename.set_defaults(func=rename_command) + parser_remove = subparser.add_parser("remove", help="remove a secret") add_secret_argument(parser_remove) + parser_remove.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser_remove.set_defaults(func=remove_command) diff --git a/pkgs/clan-cli/clan_cli/secrets/users.py b/pkgs/clan-cli/clan_cli/secrets/users.py index 98afa876..5dcd1cea 100644 --- a/pkgs/clan-cli/clan_cli/secrets/users.py +++ b/pkgs/clan-cli/clan_cli/secrets/users.py @@ -131,6 +131,11 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None: add_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + add_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_secret_parser.set_defaults(func=add_secret_command) remove_secret_parser = subparser.add_parser( @@ -142,4 +147,9 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None: remove_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + remove_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_secret_parser.set_defaults(func=remove_secret_command) diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index 60d43f27..432bc739 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -20,7 +20,8 @@ clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"] testpaths = "tests" faulthandler_timeout = 60 log_level = "DEBUG" -addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail -n auto --durations 5 --maxfail=1 --new-first" +log_format = "%(pathname)s:%(lineno)d::%(funcName)s\n %(levelname)s: %(message)s\n" +addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --maxfail=1 --new-first -nauto" # Add --pdb for debugging norecursedirs = "tests/helpers" markers = [ "impure" ] diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index 9132adcd..896c173d 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -1,4 +1,5 @@ import fileinput +import logging import shutil import tempfile from pathlib import Path @@ -10,6 +11,8 @@ from root import CLAN_CORE from clan_cli.dirs import nixpkgs_source from clan_cli.types import FlakeName +log = logging.getLogger(__name__) + # substitutes string sin a file. # This can be used on the flake.nix or default.nix of a machine @@ -35,6 +38,7 @@ class FlakeForTest(NamedTuple): def create_flake( monkeypatch: pytest.MonkeyPatch, + temporary_dir: Path, flake_name: FlakeName, clan_core_flake: Path | None = None, machines: list[str] = [], @@ -45,56 +49,67 @@ def create_flake( The machine names map to the machines in ./test_machines """ template = Path(__file__).parent / flake_name + # copy the template to a new temporary location - with tempfile.TemporaryDirectory() as tmpdir_: - home = Path(tmpdir_) - flake = home / flake_name - shutil.copytree(template, flake) - # lookup the requested machines in ./test_machines and include them - if machines: - (flake / "machines").mkdir(parents=True, exist_ok=True) - for machine_name in machines: - machine_path = Path(__file__).parent / "machines" / machine_name - shutil.copytree(machine_path, flake / "machines" / machine_name) - substitute(flake / "machines" / machine_name / "default.nix", flake) - # in the flake.nix file replace the string __CLAN_URL__ with the the clan flake - # provided by get_test_flake_toplevel - flake_nix = flake / "flake.nix" - # this is where we would install the sops key to, when updating - substitute(flake_nix, clan_core_flake, flake) - if remote: - with tempfile.TemporaryDirectory() as workdir: - monkeypatch.chdir(workdir) - monkeypatch.setenv("HOME", str(home)) - yield FlakeForTest(flake_name, flake) - else: - monkeypatch.chdir(flake) + home = Path(temporary_dir) + flake = home / ".local/state/clan/flake" / flake_name + shutil.copytree(template, flake) + + # lookup the requested machines in ./test_machines and include them + if machines: + (flake / "machines").mkdir(parents=True, exist_ok=True) + for machine_name in machines: + machine_path = Path(__file__).parent / "machines" / machine_name + shutil.copytree(machine_path, flake / "machines" / machine_name) + substitute(flake / "machines" / machine_name / "default.nix", flake) + # in the flake.nix file replace the string __CLAN_URL__ with the the clan flake + # provided by get_test_flake_toplevel + flake_nix = flake / "flake.nix" + # this is where we would install the sops key to, when updating + substitute(flake_nix, clan_core_flake, flake) + if remote: + with tempfile.TemporaryDirectory() as workdir: + monkeypatch.chdir(workdir) monkeypatch.setenv("HOME", str(home)) yield FlakeForTest(flake_name, flake) + else: + monkeypatch.chdir(flake) + monkeypatch.setenv("HOME", str(home)) + yield FlakeForTest(flake_name, flake) @pytest.fixture -def test_flake(monkeypatch: pytest.MonkeyPatch) -> Iterator[FlakeForTest]: - yield from create_flake(monkeypatch, FlakeName("test_flake")) +def test_flake( + monkeypatch: pytest.MonkeyPatch, temporary_dir: Path +) -> Iterator[FlakeForTest]: + yield from create_flake(monkeypatch, temporary_dir, FlakeName("test_flake")) @pytest.fixture -def test_flake_with_core(monkeypatch: pytest.MonkeyPatch) -> Iterator[FlakeForTest]: - if not (CLAN_CORE / "flake.nix").exists(): - raise Exception( - "clan-core flake not found. This test requires the clan-core flake to be present" - ) - yield from create_flake(monkeypatch, FlakeName("test_flake_with_core"), CLAN_CORE) - - -@pytest.fixture -def test_flake_with_core_and_pass( - monkeypatch: pytest.MonkeyPatch, +def test_flake_with_core( + monkeypatch: pytest.MonkeyPatch, temporary_dir: Path ) -> Iterator[FlakeForTest]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( "clan-core flake not found. This test requires the clan-core flake to be present" ) yield from create_flake( - monkeypatch, FlakeName("test_flake_with_core_and_pass"), CLAN_CORE + monkeypatch, temporary_dir, FlakeName("test_flake_with_core"), CLAN_CORE + ) + + +@pytest.fixture +def test_flake_with_core_and_pass( + monkeypatch: pytest.MonkeyPatch, + temporary_dir: Path, +) -> Iterator[FlakeForTest]: + if not (CLAN_CORE / "flake.nix").exists(): + raise Exception( + "clan-core flake not found. This test requires the clan-core flake to be present" + ) + yield from create_flake( + monkeypatch, + temporary_dir, + FlakeName("test_flake_with_core_and_pass"), + CLAN_CORE, ) diff --git a/pkgs/clan-cli/tests/helpers/cli.py b/pkgs/clan-cli/tests/helpers/cli.py index 46e4989f..6a22e3b3 100644 --- a/pkgs/clan-cli/tests/helpers/cli.py +++ b/pkgs/clan-cli/tests/helpers/cli.py @@ -1,16 +1,23 @@ import argparse +import inspect +import logging +import shlex from clan_cli import create_parser -import logging -import sys -import shlex + log = logging.getLogger(__name__) -import inspect def get_caller() -> str: frame = inspect.currentframe() - caller_frame = frame.f_back.f_back + if frame is None: + return "unknown" + caller_frame = frame.f_back + if caller_frame is None: + return "unknown" + caller_frame = caller_frame.f_back + if caller_frame is None: + return "unknown" frame_info = inspect.getframeinfo(caller_frame) ret = f"{frame_info.filename}:{frame_info.lineno}::{frame_info.function}" return ret @@ -22,7 +29,7 @@ class Cli: def run(self, args: list[str]) -> argparse.Namespace: cmd = shlex.join(["clan"] + args) - log.debug(f"Command: {cmd}") + log.debug(f"$ {cmd}") log.debug(f"Caller {get_caller()}") parsed = self.parser.parse_args(args) if hasattr(parsed, "func"): diff --git a/pkgs/clan-cli/tests/temporary_dir.py b/pkgs/clan-cli/tests/temporary_dir.py index c8e31edc..615d5a6b 100644 --- a/pkgs/clan-cli/tests/temporary_dir.py +++ b/pkgs/clan-cli/tests/temporary_dir.py @@ -1,3 +1,4 @@ +import logging import os import tempfile from pathlib import Path @@ -5,14 +6,17 @@ from typing import Iterator import pytest +log = logging.getLogger(__name__) + @pytest.fixture def temporary_dir() -> Iterator[Path]: - if os.getenv("TEST_KEEP_TEMPORARY_DIR"): + if os.getenv("TEST_KEEP_TEMPORARY_DIR") is not None: temp_dir = tempfile.mkdtemp(prefix="pytest-") path = Path(temp_dir) + log.info("Keeping temporary test directory: ", path) yield path - print("=========> Keeping temporary directory: ", path) else: + log.debug("TEST_KEEP_TEMPORARY_DIR not set, using TemporaryDirectory") with tempfile.TemporaryDirectory(prefix="pytest-") as dirpath: yield Path(dirpath) diff --git a/pkgs/clan-cli/tests/test_secrets_cli.py b/pkgs/clan-cli/tests/test_secrets_cli.py index d68b91a1..360b29b1 100644 --- a/pkgs/clan-cli/tests/test_secrets_cli.py +++ b/pkgs/clan-cli/tests/test_secrets_cli.py @@ -1,9 +1,8 @@ +import logging import os from contextlib import contextmanager -from pathlib import Path from typing import TYPE_CHECKING, Iterator -import logging import pytest from cli import Cli from fixtures_flakes import FlakeForTest @@ -28,7 +27,7 @@ def _test_identities( cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey, test_flake.name]) assert (sops_folder / what / "foo" / "key.json").exists() with pytest.raises(ClanError): - cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey]) + cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey, test_flake.name]) cli.run( [ @@ -43,23 +42,23 @@ def _test_identities( ) capsys.readouterr() # empty the buffer - cli.run(["secrets", what, "get", "foo"]) + cli.run(["secrets", what, "get", "foo", test_flake.name]) out = capsys.readouterr() # empty the buffer assert age_keys[0].pubkey in out.out capsys.readouterr() # empty the buffer - cli.run(["secrets", what, "list"]) + cli.run(["secrets", what, "list", test_flake.name]) out = capsys.readouterr() # empty the buffer assert "foo" in out.out - cli.run(["secrets", what, "remove", "foo"]) + cli.run(["secrets", what, "remove", "foo", test_flake.name]) assert not (sops_folder / what / "foo" / "key.json").exists() with pytest.raises(ClanError): # already removed - cli.run(["secrets", what, "remove", "foo"]) + cli.run(["secrets", what, "remove", "foo", test_flake.name]) capsys.readouterr() - cli.run(["secrets", what, "list"]) + cli.run(["secrets", what, "list", test_flake.name]) out = capsys.readouterr() assert "foo" not in out.out @@ -81,30 +80,36 @@ def test_groups( ) -> None: cli = Cli() capsys.readouterr() # empty the buffer - cli.run(["secrets", "groups", "list"]) + cli.run(["secrets", "groups", "list", test_flake.name]) assert capsys.readouterr().out == "" with pytest.raises(ClanError): # machine does not exist yet - cli.run(["secrets", "groups", "add-machine", "group1", "machine1"]) + cli.run( + ["secrets", "groups", "add-machine", "group1", "machine1", test_flake.name] + ) with pytest.raises(ClanError): # user does not exist yet - cli.run(["secrets", "groups", "add-user", "groupb1", "user1"]) - cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey]) - cli.run(["secrets", "groups", "add-machine", "group1", "machine1"]) + cli.run(["secrets", "groups", "add-user", "groupb1", "user1", test_flake.name]) + cli.run( + ["secrets", "machines", "add", "machine1", age_keys[0].pubkey, test_flake.name] + ) + cli.run(["secrets", "groups", "add-machine", "group1", "machine1", test_flake.name]) # Should this fail? - cli.run(["secrets", "groups", "add-machine", "group1", "machine1"]) + cli.run(["secrets", "groups", "add-machine", "group1", "machine1", test_flake.name]) - cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey]) - cli.run(["secrets", "groups", "add-user", "group1", "user1"]) + cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake.name]) + cli.run(["secrets", "groups", "add-user", "group1", "user1", test_flake.name]) capsys.readouterr() # empty the buffer - cli.run(["secrets", "groups", "list"]) + cli.run(["secrets", "groups", "list", test_flake.name]) out = capsys.readouterr().out assert "user1" in out assert "machine1" in out - cli.run(["secrets", "groups", "remove-user", "group1", "user1"]) - cli.run(["secrets", "groups", "remove-machine", "group1", "machine1"]) + cli.run(["secrets", "groups", "remove-user", "group1", "user1", test_flake.name]) + cli.run( + ["secrets", "groups", "remove-machine", "group1", "machine1", test_flake.name] + ) groups = os.listdir(test_flake.path / "sops" / "groups") assert len(groups) == 0 @@ -122,104 +127,114 @@ def use_key(key: str, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: def test_secrets( - test_flake: Path, + test_flake: FlakeForTest, capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch, age_keys: list["KeyPair"], ) -> None: cli = Cli() capsys.readouterr() # empty the buffer - cli.run(["secrets", "list"]) + cli.run(["secrets", "list", test_flake.name]) assert capsys.readouterr().out == "" monkeypatch.setenv("SOPS_NIX_SECRET", "foo") - monkeypatch.setenv("SOPS_AGE_KEY_FILE", str(test_flake / ".." / "age.key")) + monkeypatch.setenv("SOPS_AGE_KEY_FILE", str(test_flake.path / ".." / "age.key")) cli.run(["secrets", "key", "generate"]) capsys.readouterr() # empty the buffer cli.run(["secrets", "key", "show"]) key = capsys.readouterr().out assert key.startswith("age1") - cli.run(["secrets", "users", "add", "testuser", key]) + cli.run(["secrets", "users", "add", "testuser", key, test_flake.name]) with pytest.raises(ClanError): # does not exist yet - cli.run(["secrets", "get", "nonexisting"]) - cli.run(["secrets", "set", "initialkey"]) + cli.run(["secrets", "get", "nonexisting", test_flake.name]) + cli.run(["secrets", "set", "initialkey", test_flake.name]) capsys.readouterr() - cli.run(["secrets", "get", "initialkey"]) + cli.run(["secrets", "get", "initialkey", test_flake.name]) assert capsys.readouterr().out == "foo" capsys.readouterr() - cli.run(["secrets", "users", "list"]) + cli.run(["secrets", "users", "list", test_flake.name]) users = capsys.readouterr().out.rstrip().split("\n") assert len(users) == 1, f"users: {users}" owner = users[0] monkeypatch.setenv("EDITOR", "cat") - cli.run(["secrets", "set", "--edit", "initialkey"]) + cli.run(["secrets", "set", "--edit", "initialkey", test_flake.name]) monkeypatch.delenv("EDITOR") - cli.run(["secrets", "rename", "initialkey", "key"]) + cli.run(["secrets", "rename", "initialkey", "key", test_flake.name]) capsys.readouterr() # empty the buffer - cli.run(["secrets", "list"]) + cli.run(["secrets", "list", test_flake.name]) assert capsys.readouterr().out == "key\n" - cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey]) - cli.run(["secrets", "machines", "add-secret", "machine1", "key"]) + cli.run( + ["secrets", "machines", "add", "machine1", age_keys[0].pubkey, test_flake.name] + ) + cli.run(["secrets", "machines", "add-secret", "machine1", "key", test_flake.name]) capsys.readouterr() - cli.run(["secrets", "machines", "list"]) + cli.run(["secrets", "machines", "list", test_flake.name]) assert capsys.readouterr().out == "machine1\n" with use_key(age_keys[0].privkey, monkeypatch): capsys.readouterr() - cli.run(["secrets", "get", "key"]) + cli.run(["secrets", "get", "key", test_flake.name]) assert capsys.readouterr().out == "foo" - cli.run(["secrets", "machines", "remove-secret", "machine1", "key"]) + cli.run( + ["secrets", "machines", "remove-secret", "machine1", "key", test_flake.name] + ) - cli.run(["secrets", "users", "add", "user1", age_keys[1].pubkey]) - cli.run(["secrets", "users", "add-secret", "user1", "key"]) + cli.run(["secrets", "users", "add", "user1", age_keys[1].pubkey, test_flake.name]) + cli.run(["secrets", "users", "add-secret", "user1", "key", test_flake.name]) capsys.readouterr() with use_key(age_keys[1].privkey, monkeypatch): - cli.run(["secrets", "get", "key"]) + cli.run(["secrets", "get", "key", test_flake.name]) assert capsys.readouterr().out == "foo" - cli.run(["secrets", "users", "remove-secret", "user1", "key"]) + cli.run(["secrets", "users", "remove-secret", "user1", "key", test_flake.name]) with pytest.raises(ClanError): # does not exist yet - cli.run(["secrets", "groups", "add-secret", "admin-group", "key"]) - cli.run(["secrets", "groups", "add-user", "admin-group", "user1"]) - cli.run(["secrets", "groups", "add-user", "admin-group", owner]) - cli.run(["secrets", "groups", "add-secret", "admin-group", "key"]) + cli.run( + ["secrets", "groups", "add-secret", "admin-group", "key", test_flake.name] + ) + cli.run(["secrets", "groups", "add-user", "admin-group", "user1", test_flake.name]) + cli.run(["secrets", "groups", "add-user", "admin-group", owner, test_flake.name]) + cli.run(["secrets", "groups", "add-secret", "admin-group", "key", test_flake.name]) capsys.readouterr() # empty the buffer - cli.run(["secrets", "set", "--group", "admin-group", "key2"]) + cli.run(["secrets", "set", "--group", "admin-group", "key2", test_flake.name]) with use_key(age_keys[1].privkey, monkeypatch): capsys.readouterr() - cli.run(["secrets", "get", "key"]) + cli.run(["secrets", "get", "key", test_flake.name]) assert capsys.readouterr().out == "foo" # extend group will update secrets - cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey]) - cli.run(["secrets", "groups", "add-user", "admin-group", "user2"]) + cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey, test_flake.name]) + cli.run(["secrets", "groups", "add-user", "admin-group", "user2", test_flake.name]) with use_key(age_keys[2].privkey, monkeypatch): # user2 capsys.readouterr() - cli.run(["secrets", "get", "key"]) + cli.run(["secrets", "get", "key", test_flake.name]) assert capsys.readouterr().out == "foo" - cli.run(["secrets", "groups", "remove-user", "admin-group", "user2"]) + cli.run( + ["secrets", "groups", "remove-user", "admin-group", "user2", test_flake.name] + ) with pytest.raises(ClanError), use_key(age_keys[2].privkey, monkeypatch): # user2 is not in the group anymore capsys.readouterr() - cli.run(["secrets", "get", "key"]) + cli.run(["secrets", "get", "key", test_flake.name]) print(capsys.readouterr().out) - cli.run(["secrets", "groups", "remove-secret", "admin-group", "key"]) + cli.run( + ["secrets", "groups", "remove-secret", "admin-group", "key", test_flake.name] + ) - cli.run(["secrets", "remove", "key"]) - cli.run(["secrets", "remove", "key2"]) + cli.run(["secrets", "remove", "key", test_flake.name]) + cli.run(["secrets", "remove", "key2", test_flake.name]) capsys.readouterr() # empty the buffer - cli.run(["secrets", "list"]) + cli.run(["secrets", "list", test_flake.name]) assert capsys.readouterr().out == "" diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index 538d203d..f65254a4 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -16,9 +16,12 @@ if TYPE_CHECKING: @pytest.fixture -def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[FlakeForTest]: +def flake_with_vm_with_secrets( + monkeypatch: pytest.MonkeyPatch, temporary_dir: Path +) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, + temporary_dir, FlakeName("test_flake_with_core_dynamic_machines"), CLAN_CORE, machines=["vm_with_secrets"], @@ -27,10 +30,11 @@ def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[Flak @pytest.fixture def remote_flake_with_vm_without_secrets( - monkeypatch: pytest.MonkeyPatch, + monkeypatch: pytest.MonkeyPatch, temporary_dir: Path ) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, + temporary_dir, FlakeName("test_flake_with_core_dynamic_machines"), CLAN_CORE, machines=["vm_without_secrets"], From af3f04736bedb345f7024dd33f1b4873fdbe02c2 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 18 Oct 2023 12:17:46 +0200 Subject: [PATCH 09/36] Improved logging messages. Added ClanError if flake create or machine create already exist --- pkgs/clan-cli/clan_cli/custom_logger.py | 2 +- pkgs/clan-cli/clan_cli/flakes/create.py | 17 ++++++++++------- pkgs/clan-cli/clan_cli/machines/create.py | 2 ++ pkgs/clan-cli/clan_cli/task_manager.py | 2 +- pkgs/clan-cli/clan_cli/vms/create.py | 4 ++-- pkgs/clan-cli/default.nix | 2 -- pkgs/clan-cli/pyproject.toml | 2 +- pkgs/clan-cli/tests/test_vms_api_create.py | 11 ++++++----- 8 files changed, 23 insertions(+), 19 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/custom_logger.py b/pkgs/clan-cli/clan_cli/custom_logger.py index 1be10dae..09b63068 100644 --- a/pkgs/clan-cli/clan_cli/custom_logger.py +++ b/pkgs/clan-cli/clan_cli/custom_logger.py @@ -15,7 +15,7 @@ def get_formatter(color: str) -> Callable[[logging.LogRecord], logging.Formatter reset = "\x1b[0m" filepath = Path(record.pathname).resolve() return logging.Formatter( - f"{filepath}:%(lineno)d::%(funcName)s\n{color}%(levelname)s{reset}: %(message)s" + f"{color}%(levelname)s{reset}: %(message)s\n {filepath}:%(lineno)d::%(funcName)s\n" ) return myformatter diff --git a/pkgs/clan-cli/clan_cli/flakes/create.py b/pkgs/clan-cli/clan_cli/flakes/create.py index c9d1d539..c3bfb955 100644 --- a/pkgs/clan-cli/clan_cli/flakes/create.py +++ b/pkgs/clan-cli/clan_cli/flakes/create.py @@ -6,6 +6,7 @@ from typing import Dict from pydantic import AnyUrl from pydantic.tools import parse_obj_as +from ..errors import ClanError from ..async_cmd import CmdOut, run, runforcli from ..dirs import clan_flakes_dir from ..nix import nix_command, nix_shell @@ -18,6 +19,8 @@ DEFAULT_URL: AnyUrl = parse_obj_as( async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: if not directory.exists(): directory.mkdir() + else: + raise ClanError(f"Flake at '{directory}' already exists") response = {} command = nix_command( [ @@ -27,27 +30,27 @@ async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: url, ] ) - out = await run(command, directory) + out = await run(command, cwd=directory) response["flake init"] = out command = nix_shell(["git"], ["git", "init"]) - out = await run(command, directory) + out = await run(command, cwd=directory) response["git init"] = out command = nix_shell(["git"], ["git", "add", "."]) - out = await run(command, directory) + out = await run(command, cwd=directory) response["git add"] = out command = nix_shell(["git"], ["git", "config", "user.name", "clan-tool"]) - out = await run(command, directory) + out = await run(command, cwd=directory) response["git config"] = out command = nix_shell(["git"], ["git", "config", "user.email", "clan@example.com"]) - out = await run(command, directory) + out = await run(command, cwd=directory) response["git config"] = out command = nix_shell(["git"], ["git", "commit", "-a", "-m", "Initial commit"]) - out = await run(command, directory) + out = await run(command, cwd=directory) response["git commit"] = out return response @@ -67,7 +70,7 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None: ) parser.add_argument( "--url", - type=AnyUrl, + type=str, help="url for the flake", default=DEFAULT_URL, ) diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py index 153f2850..d4eedb48 100644 --- a/pkgs/clan-cli/clan_cli/machines/create.py +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -13,6 +13,8 @@ log = logging.getLogger(__name__) async def create_machine(flake_name: FlakeName, machine_name: str) -> Dict[str, CmdOut]: folder = specific_machine_dir(flake_name, machine_name) + if folder.exists(): + raise ClanError(f"Machine '{machine_name}' already exists") folder.mkdir(parents=True, exist_ok=True) # create empty settings.json file inside the folder diff --git a/pkgs/clan-cli/clan_cli/task_manager.py b/pkgs/clan-cli/clan_cli/task_manager.py index 73e3bbec..fa68ac9d 100644 --- a/pkgs/clan-cli/clan_cli/task_manager.py +++ b/pkgs/clan-cli/clan_cli/task_manager.py @@ -80,7 +80,7 @@ class Command: if self.p.returncode != 0: raise ClanError(f"Failed to run command: {shlex.join(cmd)}") - self.log.debug("Successfully ran command") + class TaskStatus(str, Enum): diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py index 2df7b9ac..09d37155 100644 --- a/pkgs/clan-cli/clan_cli/vms/create.py +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -34,9 +34,9 @@ class BuildVmTask(BaseTask): ] ) ) - vm_json = "".join(cmd.stdout) + vm_json = "".join(cmd.stdout).strip() self.log.debug(f"VM JSON path: {vm_json}") - with open(vm_json.strip()) as f: + with open(vm_json) as f: return json.load(f) def run(self) -> None: diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index c9fccd38..03c1017f 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -10,7 +10,6 @@ , pytest-cov , pytest-xdist , pytest-subprocess -, pytest-parallel , pytest-timeout , python3 , runCommand @@ -46,7 +45,6 @@ let pytest pytest-cov pytest-subprocess - # pytest-parallel pytest-xdist pytest-timeout openssh diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index 432bc739..88c0c945 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -20,7 +20,7 @@ clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"] testpaths = "tests" faulthandler_timeout = 60 log_level = "DEBUG" -log_format = "%(pathname)s:%(lineno)d::%(funcName)s\n %(levelname)s: %(message)s\n" +log_format = "%(levelname)s: %(message)s\n %(pathname)s:%(lineno)d::%(funcName)s" addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --maxfail=1 --new-first -nauto" # Add --pdb for debugging norecursedirs = "tests/helpers" markers = [ "impure" ] diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index f65254a4..fc2c7b3d 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -45,11 +45,12 @@ def remote_flake_with_vm_without_secrets( @pytest.fixture def create_user_with_age_key( monkeypatch: pytest.MonkeyPatch, + test_flake: FlakeForTest, age_keys: list["KeyPair"], ) -> None: monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) cli = Cli() - cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey]) + cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake.name]) def generic_create_vm_test(api: TestClient, flake: Path, vm: str) -> None: @@ -95,10 +96,10 @@ def generic_create_vm_test(api: TestClient, flake: Path, vm: str) -> None: def test_create_local( api: TestClient, monkeypatch: pytest.MonkeyPatch, - flake_with_vm_with_secrets: Path, + flake_with_vm_with_secrets: FlakeForTest, create_user_with_age_key: None, ) -> None: - generic_create_vm_test(api, flake_with_vm_with_secrets, "vm_with_secrets") + generic_create_vm_test(api, flake_with_vm_with_secrets.path, "vm_with_secrets") @pytest.mark.skipif(not os.path.exists("/dev/kvm"), reason="Requires KVM") @@ -106,8 +107,8 @@ def test_create_local( def test_create_remote( api: TestClient, monkeypatch: pytest.MonkeyPatch, - remote_flake_with_vm_without_secrets: Path, + remote_flake_with_vm_without_secrets: FlakeForTest, ) -> None: generic_create_vm_test( - api, remote_flake_with_vm_without_secrets, "vm_without_secrets" + api, remote_flake_with_vm_without_secrets.path, "vm_without_secrets" ) From 9f464dd14ec27c54df56611e09d194088b324352 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 18 Oct 2023 18:29:10 +0200 Subject: [PATCH 10/36] Added ipdb as breakpoint console. Improved logging. --- pkgs/clan-cli/clan_cli/custom_logger.py | 42 +++++++++++++++---- .../clan_cli/secrets/sops_generate.py | 1 + pkgs/clan-cli/clan_cli/task_manager.py | 12 +++++- pkgs/clan-cli/clan_cli/vms/create.py | 4 +- pkgs/clan-cli/clan_cli/webui/api_inputs.py | 1 + pkgs/clan-cli/default.nix | 4 ++ pkgs/clan-cli/pyproject.toml | 2 +- pkgs/clan-cli/shell.nix | 1 + pkgs/clan-cli/tests/helpers/cli.py | 15 +------ pkgs/clan-cli/tests/test_vms_api_create.py | 2 +- 10 files changed, 58 insertions(+), 26 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/custom_logger.py b/pkgs/clan-cli/clan_cli/custom_logger.py index 09b63068..ef05384c 100644 --- a/pkgs/clan-cli/clan_cli/custom_logger.py +++ b/pkgs/clan-cli/clan_cli/custom_logger.py @@ -1,6 +1,7 @@ import logging from typing import Any, Callable from pathlib import Path +import inspect grey = "\x1b[38;20m" yellow = "\x1b[33;20m" @@ -10,10 +11,16 @@ green = "\u001b[32m" blue = "\u001b[34m" -def get_formatter(color: str) -> Callable[[logging.LogRecord], logging.Formatter]: - def myformatter(record: logging.LogRecord) -> logging.Formatter: + +def get_formatter(color: str) -> Callable[[logging.LogRecord, bool], logging.Formatter]: + def myformatter(record: logging.LogRecord, with_location: bool) -> logging.Formatter: reset = "\x1b[0m" filepath = Path(record.pathname).resolve() + if not with_location: + return logging.Formatter( + f"{color}%(levelname)s{reset}: %(message)s" + ) + return logging.Formatter( f"{color}%(levelname)s{reset}: %(message)s\n {filepath}:%(lineno)d::%(funcName)s\n" ) @@ -31,11 +38,32 @@ FORMATTER = { class CustomFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: - return FORMATTER[record.levelno](record).format(record) + return FORMATTER[record.levelno](record, True).format(record) + + +class ThreadFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + return FORMATTER[record.levelno](record, False).format(record) + +def get_caller() -> str: + frame = inspect.currentframe() + if frame is None: + return "unknown" + caller_frame = frame.f_back + if caller_frame is None: + return "unknown" + caller_frame = caller_frame.f_back + if caller_frame is None: + return "unknown" + frame_info = inspect.getframeinfo(caller_frame) + ret = f"{frame_info.filename}:{frame_info.lineno}::{frame_info.function}" + return ret def register(level: Any) -> None: - ch = logging.StreamHandler() - ch.setLevel(level) - ch.setFormatter(CustomFormatter()) - logging.basicConfig(level=level, handlers=[ch]) + handler = logging.StreamHandler() + handler.setLevel(level) + handler.setFormatter(CustomFormatter()) + logger = logging.getLogger("registerHandler") + logger.addHandler(handler) + #logging.basicConfig(level=level, handlers=[handler]) diff --git a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py index a8af2ece..d067e710 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py @@ -95,6 +95,7 @@ def generate_secrets_from_nix( ) -> None: generate_host_key(flake_name, machine_name) errors = {} + with TemporaryDirectory() as d: # if any of the secrets are missing, we regenerate all connected facts/secrets for secret_group, secret_options in secret_submodules.items(): diff --git a/pkgs/clan-cli/clan_cli/task_manager.py b/pkgs/clan-cli/clan_cli/task_manager.py index fa68ac9d..227551d5 100644 --- a/pkgs/clan-cli/clan_cli/task_manager.py +++ b/pkgs/clan-cli/clan_cli/task_manager.py @@ -12,6 +12,7 @@ from pathlib import Path from typing import Any, Iterator, Optional, Type, TypeVar from uuid import UUID, uuid4 +from .custom_logger import get_caller, ThreadFormatter, CustomFormatter from .errors import ClanError @@ -38,7 +39,8 @@ class Command: cwd: Optional[Path] = None, ) -> None: self.running = True - self.log.debug(f"Running command: {shlex.join(cmd)}") + self.log.debug(f"Command: {shlex.join(cmd)}") + self.log.debug(f"Caller: {get_caller()}") cwd_res = None if cwd is not None: @@ -94,7 +96,13 @@ class BaseTask: def __init__(self, uuid: UUID, num_cmds: int) -> None: # constructor self.uuid: UUID = uuid - self.log = logging.getLogger(__name__) + handler = logging.StreamHandler() + handler.setLevel(logging.DEBUG) + handler.setFormatter(ThreadFormatter()) + logger = logging.getLogger(__name__) + logger.addHandler(handler) + self.log = logger + self.log = logger self.procs: list[Command] = [] self.status = TaskStatus.NOTSTARTED self.logs_lock = threading.Lock() diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py index 09d37155..b2b171a5 100644 --- a/pkgs/clan-cli/clan_cli/vms/create.py +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -13,6 +13,7 @@ from ..dirs import specific_flake_dir from ..nix import nix_build, nix_config, nix_shell from ..task_manager import BaseTask, Command, create_task from .inspect import VmConfig, inspect_vm +import pydantic class BuildVmTask(BaseTask): @@ -58,6 +59,7 @@ class BuildVmTask(BaseTask): env = os.environ.copy() env["CLAN_DIR"] = str(self.vm.flake_url) + env["PYTHONPATH"] = str( ":".join(sys.path) ) # TODO do this in the clanCore module @@ -70,7 +72,7 @@ class BuildVmTask(BaseTask): env=env, ) else: - cmd.run(["echo", "won't generate secrets for non local clan"]) + self.log.warning("won't generate secrets for non local clan") cmd = next(cmds) cmd.run( diff --git a/pkgs/clan-cli/clan_cli/webui/api_inputs.py b/pkgs/clan-cli/clan_cli/webui/api_inputs.py index d3a9545c..959f118b 100644 --- a/pkgs/clan-cli/clan_cli/webui/api_inputs.py +++ b/pkgs/clan-cli/clan_cli/webui/api_inputs.py @@ -12,6 +12,7 @@ log = logging.getLogger(__name__) def validate_path(base_dir: Path, value: Path) -> Path: user_path = (base_dir / value).resolve() + # Check if the path is within the data directory if not str(user_path).startswith(str(base_dir)): if not str(user_path).startswith("/tmp/pytest"): diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 03c1017f..22031e15 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -11,6 +11,8 @@ , pytest-xdist , pytest-subprocess , pytest-timeout +, remote-pdb +, ipdb , python3 , runCommand , setuptools @@ -47,6 +49,8 @@ let pytest-subprocess pytest-xdist pytest-timeout + remote-pdb + ipdb openssh git gnupg diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index 88c0c945..92d58cee 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -20,7 +20,7 @@ clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"] testpaths = "tests" faulthandler_timeout = 60 log_level = "DEBUG" -log_format = "%(levelname)s: %(message)s\n %(pathname)s:%(lineno)d::%(funcName)s" +log_format = "%(levelname)s: %(message)s" addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --maxfail=1 --new-first -nauto" # Add --pdb for debugging norecursedirs = "tests/helpers" markers = [ "impure" ] diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix index 8b93571e..de011121 100644 --- a/pkgs/clan-cli/shell.nix +++ b/pkgs/clan-cli/shell.nix @@ -43,6 +43,7 @@ mkShell { export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH" export PYTHONPATH="$source:$tmp_path/python/${pythonWithDeps.sitePackages}:" + export PYTHONBREAKPOINT=ipdb.set_trace export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" diff --git a/pkgs/clan-cli/tests/helpers/cli.py b/pkgs/clan-cli/tests/helpers/cli.py index 6a22e3b3..d82c21d4 100644 --- a/pkgs/clan-cli/tests/helpers/cli.py +++ b/pkgs/clan-cli/tests/helpers/cli.py @@ -4,24 +4,11 @@ import logging import shlex from clan_cli import create_parser +from clan_cli.custom_logger import get_caller log = logging.getLogger(__name__) -def get_caller() -> str: - frame = inspect.currentframe() - if frame is None: - return "unknown" - caller_frame = frame.f_back - if caller_frame is None: - return "unknown" - caller_frame = caller_frame.f_back - if caller_frame is None: - return "unknown" - frame_info = inspect.getframeinfo(caller_frame) - ret = f"{frame_info.filename}:{frame_info.lineno}::{frame_info.function}" - return ret - class Cli: def __init__(self) -> None: diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index fc2c7b3d..84b30419 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -89,7 +89,7 @@ def generic_create_vm_test(api: TestClient, flake: Path, vm: str) -> None: assert ( data["status"] == "FINISHED" ), f"Expected to be finished, but got {data['status']} ({data})" - + @pytest.mark.skipif(not os.path.exists("/dev/kvm"), reason="Requires KVM") @pytest.mark.impure From d02acbe04b62254da0f47d096e0e5adb8b42f4fe Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 18 Oct 2023 18:29:51 +0200 Subject: [PATCH 11/36] nix fmt --- pkgs/clan-cli/clan_cli/__init__.py | 9 +-- pkgs/clan-cli/clan_cli/config/__init__.py | 2 +- pkgs/clan-cli/clan_cli/custom_logger.py | 19 ++++--- pkgs/clan-cli/clan_cli/dirs.py | 3 +- pkgs/clan-cli/clan_cli/flakes/create.py | 2 +- pkgs/clan-cli/clan_cli/machines/create.py | 2 +- pkgs/clan-cli/clan_cli/secrets/groups.py | 2 +- pkgs/clan-cli/clan_cli/secrets/machines.py | 2 +- pkgs/clan-cli/clan_cli/secrets/secrets.py | 2 - pkgs/clan-cli/clan_cli/secrets/sops.py | 2 +- pkgs/clan-cli/clan_cli/task_manager.py | 4 +- pkgs/clan-cli/clan_cli/vms/create.py | 1 - .../clan_cli/webui/routers/machines.py | 2 +- pkgs/clan-cli/shell.nix | 57 ++++++++++--------- pkgs/clan-cli/tests/helpers/cli.py | 2 - pkgs/clan-cli/tests/test_vms_api_create.py | 2 +- 16 files changed, 53 insertions(+), 60 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 48f554aa..8e77efc0 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -1,13 +1,12 @@ import argparse +import logging import sys from types import ModuleType from typing import Optional from . import config, flakes, join, machines, secrets, vms, webui -from .ssh import cli as ssh_cli - -import logging from .custom_logger import register +from .ssh import cli as ssh_cli log = logging.getLogger(__name__) @@ -57,7 +56,7 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser: parser_vms = subparsers.add_parser("vms", help="manage virtual machines") vms.register_parser(parser_vms) -# if args.debug: + # if args.debug: register(logging.DEBUG) log.debug("Debug log activated") @@ -74,8 +73,6 @@ def main() -> None: parser = create_parser() args = parser.parse_args() - - if not hasattr(args, "func"): return diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index 04ffbb68..dcf2d8eb 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -11,9 +11,9 @@ from typing import Any, Optional, Tuple, get_origin from clan_cli.dirs import machine_settings_file, specific_flake_dir from clan_cli.errors import ClanError -from clan_cli.types import FlakeName from clan_cli.git import commit_file from clan_cli.nix import nix_eval +from clan_cli.types import FlakeName script_dir = Path(__file__).parent diff --git a/pkgs/clan-cli/clan_cli/custom_logger.py b/pkgs/clan-cli/clan_cli/custom_logger.py index ef05384c..f9f324e1 100644 --- a/pkgs/clan-cli/clan_cli/custom_logger.py +++ b/pkgs/clan-cli/clan_cli/custom_logger.py @@ -1,7 +1,7 @@ -import logging -from typing import Any, Callable -from pathlib import Path import inspect +import logging +from pathlib import Path +from typing import Any, Callable grey = "\x1b[38;20m" yellow = "\x1b[33;20m" @@ -11,19 +11,19 @@ green = "\u001b[32m" blue = "\u001b[34m" - def get_formatter(color: str) -> Callable[[logging.LogRecord, bool], logging.Formatter]: - def myformatter(record: logging.LogRecord, with_location: bool) -> logging.Formatter: + def myformatter( + record: logging.LogRecord, with_location: bool + ) -> logging.Formatter: reset = "\x1b[0m" filepath = Path(record.pathname).resolve() if not with_location: - return logging.Formatter( - f"{color}%(levelname)s{reset}: %(message)s" - ) + return logging.Formatter(f"{color}%(levelname)s{reset}: %(message)s") return logging.Formatter( f"{color}%(levelname)s{reset}: %(message)s\n {filepath}:%(lineno)d::%(funcName)s\n" ) + return myformatter @@ -45,6 +45,7 @@ class ThreadFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: return FORMATTER[record.levelno](record, False).format(record) + def get_caller() -> str: frame = inspect.currentframe() if frame is None: @@ -66,4 +67,4 @@ def register(level: Any) -> None: handler.setFormatter(CustomFormatter()) logger = logging.getLogger("registerHandler") logger.addHandler(handler) - #logging.basicConfig(level=level, handlers=[handler]) + # logging.basicConfig(level=level, handlers=[handler]) diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index 7bc3f748..5f115775 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -1,14 +1,15 @@ +import logging import os import sys from pathlib import Path from typing import Optional -import logging from .errors import ClanError from .types import FlakeName log = logging.getLogger(__name__) + def _get_clan_flake_toplevel() -> Path: return find_toplevel([".clan-flake", ".git", ".hg", ".svn", "flake.nix"]) diff --git a/pkgs/clan-cli/clan_cli/flakes/create.py b/pkgs/clan-cli/clan_cli/flakes/create.py index c3bfb955..fb8ea16d 100644 --- a/pkgs/clan-cli/clan_cli/flakes/create.py +++ b/pkgs/clan-cli/clan_cli/flakes/create.py @@ -6,9 +6,9 @@ from typing import Dict from pydantic import AnyUrl from pydantic.tools import parse_obj_as -from ..errors import ClanError from ..async_cmd import CmdOut, run, runforcli from ..dirs import clan_flakes_dir +from ..errors import ClanError from ..nix import nix_command, nix_shell DEFAULT_URL: AnyUrl = parse_obj_as( diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py index d4eedb48..9a2e39be 100644 --- a/pkgs/clan-cli/clan_cli/machines/create.py +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -5,8 +5,8 @@ from typing import Dict from ..async_cmd import CmdOut, run, runforcli from ..dirs import specific_flake_dir, specific_machine_dir from ..errors import ClanError -from ..types import FlakeName from ..nix import nix_shell +from ..types import FlakeName log = logging.getLogger(__name__) diff --git a/pkgs/clan-cli/clan_cli/secrets/groups.py b/pkgs/clan-cli/clan_cli/secrets/groups.py index 0a0fb2dd..a969d415 100644 --- a/pkgs/clan-cli/clan_cli/secrets/groups.py +++ b/pkgs/clan-cli/clan_cli/secrets/groups.py @@ -3,8 +3,8 @@ import os from pathlib import Path from ..errors import ClanError -from ..types import FlakeName from ..machines.types import machine_name_type, validate_hostname +from ..types import FlakeName from . import secrets from .folders import ( sops_groups_folder, diff --git a/pkgs/clan-cli/clan_cli/secrets/machines.py b/pkgs/clan-cli/clan_cli/secrets/machines.py index e683ec08..316f1013 100644 --- a/pkgs/clan-cli/clan_cli/secrets/machines.py +++ b/pkgs/clan-cli/clan_cli/secrets/machines.py @@ -1,7 +1,7 @@ import argparse -from ..types import FlakeName from ..machines.types import machine_name_type, validate_hostname +from ..types import FlakeName from . import secrets from .folders import list_objects, remove_object, sops_machines_folder from .sops import read_key, write_key diff --git a/pkgs/clan-cli/clan_cli/secrets/secrets.py b/pkgs/clan-cli/clan_cli/secrets/secrets.py index 2e41bd6a..64a1abfe 100644 --- a/pkgs/clan-cli/clan_cli/secrets/secrets.py +++ b/pkgs/clan-cli/clan_cli/secrets/secrets.py @@ -269,7 +269,6 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None: ) parser_get.set_defaults(func=get_command) - parser_set = subparser.add_parser("set", help="set a secret") add_secret_argument(parser_set) parser_set.add_argument( @@ -317,7 +316,6 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None: ) parser_rename.set_defaults(func=rename_command) - parser_remove = subparser.add_parser("remove", help="remove a secret") add_secret_argument(parser_remove) parser_remove.add_argument( diff --git a/pkgs/clan-cli/clan_cli/secrets/sops.py b/pkgs/clan-cli/clan_cli/secrets/sops.py index 4f35761a..f89edd34 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops.py @@ -9,8 +9,8 @@ from typing import IO, Iterator from ..dirs import user_config_dir from ..errors import ClanError -from ..types import FlakeName from ..nix import nix_shell +from ..types import FlakeName from .folders import sops_machines_folder, sops_users_folder diff --git a/pkgs/clan-cli/clan_cli/task_manager.py b/pkgs/clan-cli/clan_cli/task_manager.py index 227551d5..79b71a7e 100644 --- a/pkgs/clan-cli/clan_cli/task_manager.py +++ b/pkgs/clan-cli/clan_cli/task_manager.py @@ -12,7 +12,7 @@ from pathlib import Path from typing import Any, Iterator, Optional, Type, TypeVar from uuid import UUID, uuid4 -from .custom_logger import get_caller, ThreadFormatter, CustomFormatter +from .custom_logger import ThreadFormatter, get_caller from .errors import ClanError @@ -83,8 +83,6 @@ class Command: raise ClanError(f"Failed to run command: {shlex.join(cmd)}") - - class TaskStatus(str, Enum): NOTSTARTED = "NOTSTARTED" RUNNING = "RUNNING" diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py index b2b171a5..5e85d1d9 100644 --- a/pkgs/clan-cli/clan_cli/vms/create.py +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -13,7 +13,6 @@ from ..dirs import specific_flake_dir from ..nix import nix_build, nix_config, nix_shell from ..task_manager import BaseTask, Command, create_task from .inspect import VmConfig, inspect_vm -import pydantic class BuildVmTask(BaseTask): diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index b112959a..62b00f60 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -10,9 +10,9 @@ from ...config.machine import ( set_config_for_machine, verify_machine_config, ) -from ...types import FlakeName from ...machines.create import create_machine as _create_machine from ...machines.list import list_machines as _list_machines +from ...types import FlakeName from ..api_outputs import ( ConfigResponse, Machine, diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix index de011121..62d28d2b 100644 --- a/pkgs/clan-cli/shell.nix +++ b/pkgs/clan-cli/shell.nix @@ -22,39 +22,40 @@ mkShell { ]; shellHook = '' - tmp_path=$(realpath ./.direnv) - source=$(realpath .) - mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}" + tmp_path=$(realpath ./.direnv) - # Install the package in editable mode - # This allows executing `clan` from within the dev-shell using the current - # version of the code and its dependencies. - ${pythonWithDeps.interpreter} -m pip install \ - --quiet \ - --disable-pip-version-check \ - --no-index \ - --no-build-isolation \ - --prefix "$tmp_path/python" \ - --editable $source + repo_root=$(realpath .) + mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}" - rm -f clan_cli/nixpkgs clan_cli/webui/assets - ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs - ln -sf ${ui-assets} clan_cli/webui/assets + # Install the package in editable mode + # This allows executing `clan` from within the dev-shell using the current + # version of the code and its dependencies. + ${pythonWithDeps.interpreter} -m pip install \ + --quiet \ + --disable-pip-version-check \ + --no-index \ + --no-build-isolation \ + --prefix "$tmp_path/python" \ + --editable $repo_root - export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH" - export PYTHONPATH="$source:$tmp_path/python/${pythonWithDeps.sitePackages}:" + rm -f clan_cli/nixpkgs clan_cli/webui/assets + ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs + ln -sf ${ui-assets} clan_cli/webui/assets + + export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH" + export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:" export PYTHONBREAKPOINT=ipdb.set_trace - export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" - export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" - mkdir -p \ - $tmp_path/share/fish/vendor_completions.d \ - $tmp_path/share/bash-completion/completions \ - $tmp_path/share/zsh/site-functions - register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish - register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan + export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" + export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" + mkdir -p \ + $tmp_path/share/fish/vendor_completions.d \ + $tmp_path/share/bash-completion/completions \ + $tmp_path/share/zsh/site-functions + register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish + register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan - ./bin/clan flakes create example_clan - ./bin/clan machines create example_machine example_clan + ./bin/clan flakes create example_clan + ./bin/clan machines create example_machine example_clan ''; } diff --git a/pkgs/clan-cli/tests/helpers/cli.py b/pkgs/clan-cli/tests/helpers/cli.py index d82c21d4..3deaef7f 100644 --- a/pkgs/clan-cli/tests/helpers/cli.py +++ b/pkgs/clan-cli/tests/helpers/cli.py @@ -1,5 +1,4 @@ import argparse -import inspect import logging import shlex @@ -9,7 +8,6 @@ from clan_cli.custom_logger import get_caller log = logging.getLogger(__name__) - class Cli: def __init__(self) -> None: self.parser = create_parser(prog="clan") diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index 84b30419..fc2c7b3d 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -89,7 +89,7 @@ def generic_create_vm_test(api: TestClient, flake: Path, vm: str) -> None: assert ( data["status"] == "FINISHED" ), f"Expected to be finished, but got {data['status']} ({data})" - + @pytest.mark.skipif(not os.path.exists("/dev/kvm"), reason="Requires KVM") @pytest.mark.impure From a71584d9d21b128c14a2017f3be643fe8fd70a0c Mon Sep 17 00:00:00 2001 From: Qubasa Date: Thu, 19 Oct 2023 23:24:58 +0200 Subject: [PATCH 12/36] Added clanName argument to clan-core.lib.builClan --- docs/quickstart.md | 2 ++ lib/build-clan/default.nix | 2 ++ pkgs/clan-cli/clan_cli/task_manager.py | 5 +++-- pkgs/clan-cli/clan_cli/vms/create.py | 1 + pkgs/clan-cli/tests/test_flake_with_core/flake.nix | 1 + pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix | 1 + .../tests/test_flake_with_core_dynamic_machines/flake.nix | 1 + templates/new-clan/flake.nix | 1 + 8 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 5daadefa..1894ffed 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -91,6 +91,7 @@ Absolutely, let's break down the migration step by step, explaining each action # this needs to point at the repository root directory = self; specialArgs = {}; + clanName = "NEEDS_TO_BE_UNIQUE"; # TODO: Changeme machines = { example-desktop = { nixpkgs.hostPlatform = "x86_64-linux"; @@ -107,6 +108,7 @@ Absolutely, let's break down the migration step by step, explaining each action - Inside `machines`, a new machine configuration is defined (in this case, `example-desktop`). - Inside `example-desktop` which is the target machine hostname, `nixpkgs.hostPlatform` specifies the host platform as `x86_64-linux`. - `clanInternals`: Is required to enable evaluation of the secret generation/upload script on every architecture + - `clanName`: Is required and needs to be globally unique, as else we have a cLAN name clash 4. **Rebuild and Switch**: Rebuild your NixOS configuration using the updated flake: diff --git a/lib/build-clan/default.nix b/lib/build-clan/default.nix index 1a141955..f7d05b2d 100644 --- a/lib/build-clan/default.nix +++ b/lib/build-clan/default.nix @@ -2,6 +2,7 @@ { directory # The directory containing the machines subdirectory , specialArgs ? { } # Extra arguments to pass to nixosSystem i.e. useful to make self available , machines ? { } # allows to include machine-specific modules i.e. machines.${name} = { ... } +, clanName # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to. }: let machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (builtins.readDir (directory + /machines)); @@ -73,6 +74,7 @@ in clanInternals = { machines = configsPerSystem; + clanName = clanName; all-machines-json = lib.mapAttrs (system: configs: nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" (lib.mapAttrs (_: m: m.config.system.clan.deployment.data) configs)) configsPerSystem; diff --git a/pkgs/clan-cli/clan_cli/task_manager.py b/pkgs/clan-cli/clan_cli/task_manager.py index 79b71a7e..b2573e0f 100644 --- a/pkgs/clan-cli/clan_cli/task_manager.py +++ b/pkgs/clan-cli/clan_cli/task_manager.py @@ -63,6 +63,7 @@ class Command: os.set_blocking(self.p.stdout.fileno(), False) os.set_blocking(self.p.stderr.fileno(), False) + while self.p.poll() is None: # Check if stderr is ready to be read from rlist, _, _ = select.select([self.p.stderr, self.p.stdout], [], [], 0) @@ -70,10 +71,10 @@ class Command: try: for line in fd: if fd == self.p.stderr: - print(f"[{cmd[0]}] stderr: {line.rstrip()}") + self.log.debug(f"[{cmd[0]}] stderr: {line}") self.stderr.append(line) else: - print(f"[{cmd[0]}] stdout: {line.rstrip()}") + self.log.debug(f"[{cmd[0]}] stdout: {line}") self.stdout.append(line) self._output.put(line) except BlockingIOError: diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py index 5e85d1d9..8cc12f12 100644 --- a/pkgs/clan-cli/clan_cli/vms/create.py +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -48,6 +48,7 @@ class BuildVmTask(BaseTask): # TODO: We should get this from the vm argument vm_config = self.get_vm_create_info(cmds) + # TODO: Don't use a temporary directory, instead create a new flake directory with tempfile.TemporaryDirectory() as tmpdir_: tmpdir = Path(tmpdir_) xchg_dir = tmpdir / "xchg" diff --git a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix index b7b980c2..0c287e45 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix @@ -9,6 +9,7 @@ let clan = clan-core.lib.buildClan { directory = self; + clanName = "test_with_core_clan"; machines = { vm1 = { lib, ... }: { clan.networking.deploymentAddress = "__CLAN_DEPLOYMENT_ADDRESS__"; diff --git a/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix index 38346de6..39a01f06 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix @@ -9,6 +9,7 @@ let clan = clan-core.lib.buildClan { directory = self; + clanName = "test_with_core_and_pass_clan"; machines = { vm1 = { lib, ... }: { clan.networking.deploymentAddress = "__CLAN_DEPLOYMENT_ADDRESS__"; diff --git a/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix index 7c4558db..13c30ea0 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix @@ -9,6 +9,7 @@ let clan = clan-core.lib.buildClan { directory = self; + clanName = "core_dynamic_machine_clan"; machines = let machineModules = builtins.readDir (self + "/machines"); diff --git a/templates/new-clan/flake.nix b/templates/new-clan/flake.nix index fca91ed0..38acce83 100644 --- a/templates/new-clan/flake.nix +++ b/templates/new-clan/flake.nix @@ -9,6 +9,7 @@ pkgs = clan-core.inputs.nixpkgs.legacyPackages.${system}; clan = clan-core.lib.buildClan { directory = self; + clanName = "__CHANGE_ME__"; }; in { From 26bfb793b1160c756a752d197150f74605c2c001 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 20 Oct 2023 01:11:32 +0200 Subject: [PATCH 13/36] Added ref to Qubasa-main in template/new-clan/flake.nix --- pkgs/clan-cli/clan_cli/flakes/create.py | 2 +- pkgs/clan-cli/clan_cli/task_manager.py | 4 + pkgs/clan-cli/clan_cli/types.py | 20 +++ pkgs/clan-cli/clan_cli/vms/create.py | 188 +++++++++++---------- pkgs/clan-cli/clan_cli/webui/api_inputs.py | 19 +-- pkgs/clan-cli/tests/temporary_dir.py | 7 +- pkgs/clan-cli/tests/test_config.py | 2 +- pkgs/clan-cli/tests/test_create_flake.py | 30 ++-- templates/new-clan/flake.nix | 2 +- 9 files changed, 153 insertions(+), 121 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/flakes/create.py b/pkgs/clan-cli/clan_cli/flakes/create.py index fb8ea16d..28e2bdc6 100644 --- a/pkgs/clan-cli/clan_cli/flakes/create.py +++ b/pkgs/clan-cli/clan_cli/flakes/create.py @@ -12,7 +12,7 @@ from ..errors import ClanError from ..nix import nix_command, nix_shell DEFAULT_URL: AnyUrl = parse_obj_as( - AnyUrl, "git+https://git.clan.lol/clan/clan-core#new-clan" + AnyUrl, "git+https://git.clan.lol/clan/clan-core?ref=Qubasa-main#new-clan" # TODO: Change me back to main branch ) diff --git a/pkgs/clan-cli/clan_cli/task_manager.py b/pkgs/clan-cli/clan_cli/task_manager.py index b2573e0f..c54a9748 100644 --- a/pkgs/clan-cli/clan_cli/task_manager.py +++ b/pkgs/clan-cli/clan_cli/task_manager.py @@ -115,6 +115,10 @@ class BaseTask: self.status = TaskStatus.RUNNING try: self.run() + # TODO: We need to check, if too many commands have been initialized, + # but not run. This would deadlock the log_lines() function. + # Idea: Run next(cmds) and check if it raises StopIteration if not, + # we have too many commands except Exception as e: # FIXME: fix exception handling here traceback.print_exception(*sys.exc_info()) diff --git a/pkgs/clan-cli/clan_cli/types.py b/pkgs/clan-cli/clan_cli/types.py index 16e38c87..be54b3da 100644 --- a/pkgs/clan-cli/clan_cli/types.py +++ b/pkgs/clan-cli/clan_cli/types.py @@ -1,3 +1,23 @@ from typing import NewType +from pathlib import Path +import logging + +log = logging.getLogger(__name__) FlakeName = NewType("FlakeName", str) + + +def validate_path(base_dir: Path, value: Path) -> Path: + user_path = (base_dir / value).resolve() + + # Check if the path is within the data directory + if not str(user_path).startswith(str(base_dir)): + if not str(user_path).startswith("/tmp/pytest"): + raise ValueError( + f"Destination out of bounds. Expected {user_path} to start with {base_dir}" + ) + else: + log.warning( + f"Detected pytest tmpdir. Skipping path validation for {user_path}" + ) + return user_path \ No newline at end of file diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py index 8cc12f12..158e243e 100644 --- a/pkgs/clan-cli/clan_cli/vms/create.py +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -9,15 +9,16 @@ from pathlib import Path from typing import Iterator from uuid import UUID -from ..dirs import specific_flake_dir -from ..nix import nix_build, nix_config, nix_shell +from ..dirs import specific_flake_dir, clan_flakes_dir +from ..nix import nix_build, nix_config, nix_shell, nix_eval from ..task_manager import BaseTask, Command, create_task from .inspect import VmConfig, inspect_vm - +from ..flakes.create import create_flake +from ..types import validate_path class BuildVmTask(BaseTask): def __init__(self, uuid: UUID, vm: VmConfig) -> None: - super().__init__(uuid, num_cmds=6) + super().__init__(uuid, num_cmds=7) self.vm = vm def get_vm_create_info(self, cmds: Iterator[Command]) -> dict: @@ -39,6 +40,19 @@ class BuildVmTask(BaseTask): with open(vm_json) as f: return json.load(f) + def get_clan_name(self, cmds: Iterator[Command]) -> str: + clan_dir = self.vm.flake_url + cmd = next(cmds) + cmd.run( + nix_eval( + [ + f'{clan_dir}#clanInternals.clanName' + ] + ) + ) + clan_name = "".join(cmd.stdout).strip() + return clan_name + def run(self) -> None: cmds = self.commands() @@ -47,101 +61,103 @@ class BuildVmTask(BaseTask): # TODO: We should get this from the vm argument vm_config = self.get_vm_create_info(cmds) + clan_name = self.get_clan_name(cmds) - # TODO: Don't use a temporary directory, instead create a new flake directory - with tempfile.TemporaryDirectory() as tmpdir_: - tmpdir = Path(tmpdir_) - xchg_dir = tmpdir / "xchg" - xchg_dir.mkdir() - secrets_dir = tmpdir / "secrets" - secrets_dir.mkdir() - disk_img = f"{tmpdir_}/disk.img" - env = os.environ.copy() - env["CLAN_DIR"] = str(self.vm.flake_url) + flake_dir = clan_flakes_dir() / clan_name + validate_path(clan_flakes_dir(), flake_dir) - env["PYTHONPATH"] = str( - ":".join(sys.path) - ) # TODO do this in the clanCore module - env["SECRETS_DIR"] = str(secrets_dir) + xchg_dir = flake_dir / "xchg" + xchg_dir.mkdir() + secrets_dir = flake_dir / "secrets" + secrets_dir.mkdir() + disk_img = f"{flake_dir}/disk.img" - cmd = next(cmds) - if Path(self.vm.flake_url).is_dir(): - cmd.run( - [vm_config["generateSecrets"]], - env=env, - ) - else: - self.log.warning("won't generate secrets for non local clan") + env = os.environ.copy() + env["CLAN_DIR"] = str(self.vm.flake_url) - cmd = next(cmds) + env["PYTHONPATH"] = str( + ":".join(sys.path) + ) # TODO do this in the clanCore module + env["SECRETS_DIR"] = str(secrets_dir) + + cmd = next(cmds) + if Path(self.vm.flake_url).is_dir(): cmd.run( - [vm_config["uploadSecrets"]], + [vm_config["generateSecrets"]], env=env, ) + else: + self.log.warning("won't generate secrets for non local clan") - cmd = next(cmds) - cmd.run( - nix_shell( - ["qemu"], - [ - "qemu-img", - "create", - "-f", - "raw", - disk_img, - "1024M", - ], - ) + cmd = next(cmds) + cmd.run( + [vm_config["uploadSecrets"]], + env=env, + ) + + cmd = next(cmds) + cmd.run( + nix_shell( + ["qemu"], + [ + "qemu-img", + "create", + "-f", + "raw", + disk_img, + "1024M", + ], ) + ) - cmd = next(cmds) - cmd.run( - nix_shell( - ["e2fsprogs"], - [ - "mkfs.ext4", - "-L", - "nixos", - disk_img, - ], - ) + cmd = next(cmds) + cmd.run( + nix_shell( + ["e2fsprogs"], + [ + "mkfs.ext4", + "-L", + "nixos", + disk_img, + ], ) + ) - cmd = next(cmds) - cmdline = [ - (Path(vm_config["toplevel"]) / "kernel-params").read_text(), - f'init={vm_config["toplevel"]}/init', - f'regInfo={vm_config["regInfo"]}/registration', - "console=ttyS0,115200n8", - "console=tty0", - ] - qemu_command = [ - # fmt: off - "qemu-kvm", - "-name", machine, - "-m", f'{vm_config["memorySize"]}M', - "-smp", str(vm_config["cores"]), - "-device", "virtio-rng-pci", - "-net", "nic,netdev=user.0,model=virtio", "-netdev", "user,id=user.0", - "-virtfs", "local,path=/nix/store,security_model=none,mount_tag=nix-store", - "-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=shared", - "-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=xchg", - "-virtfs", f"local,path={secrets_dir},security_model=none,mount_tag=secrets", - "-drive", f'cache=writeback,file={disk_img},format=raw,id=drive1,if=none,index=1,werror=report', - "-device", "virtio-blk-pci,bootindex=1,drive=drive1,serial=root", - "-device", "virtio-keyboard", - "-usb", - "-device", "usb-tablet,bus=usb-bus.0", - "-kernel", f'{vm_config["toplevel"]}/kernel', - "-initrd", vm_config["initrd"], - "-append", " ".join(cmdline), - # fmt: on - ] - if not self.vm.graphics: - qemu_command.append("-nographic") - print("$ " + shlex.join(qemu_command)) - cmd.run(nix_shell(["qemu"], qemu_command)) + cmd = next(cmds) + cmdline = [ + (Path(vm_config["toplevel"]) / "kernel-params").read_text(), + f'init={vm_config["toplevel"]}/init', + f'regInfo={vm_config["regInfo"]}/registration', + "console=ttyS0,115200n8", + "console=tty0", + ] + qemu_command = [ + # fmt: off + "qemu-kvm", + "-name", machine, + "-m", f'{vm_config["memorySize"]}M', + "-smp", str(vm_config["cores"]), + "-device", "virtio-rng-pci", + "-net", "nic,netdev=user.0,model=virtio", "-netdev", "user,id=user.0", + "-virtfs", "local,path=/nix/store,security_model=none,mount_tag=nix-store", + "-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=shared", + "-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=xchg", + "-virtfs", f"local,path={secrets_dir},security_model=none,mount_tag=secrets", + "-drive", f'cache=writeback,file={disk_img},format=raw,id=drive1,if=none,index=1,werror=report', + "-device", "virtio-blk-pci,bootindex=1,drive=drive1,serial=root", + "-device", "virtio-keyboard", + "-usb", + "-device", "usb-tablet,bus=usb-bus.0", + "-kernel", f'{vm_config["toplevel"]}/kernel', + "-initrd", vm_config["initrd"], + "-append", " ".join(cmdline), + # fmt: on + ] + if not self.vm.graphics: + qemu_command.append("-nographic") + print("$ " + shlex.join(qemu_command)) + cmd.run(nix_shell(["qemu"], qemu_command)) def create_vm(vm: VmConfig) -> BuildVmTask: diff --git a/pkgs/clan-cli/clan_cli/webui/api_inputs.py b/pkgs/clan-cli/clan_cli/webui/api_inputs.py index 959f118b..c1e9fd89 100644 --- a/pkgs/clan-cli/clan_cli/webui/api_inputs.py +++ b/pkgs/clan-cli/clan_cli/webui/api_inputs.py @@ -6,26 +6,11 @@ from pydantic import AnyUrl, BaseModel, validator from ..dirs import clan_data_dir, clan_flakes_dir from ..flakes.create import DEFAULT_URL +from ..types import validate_path + log = logging.getLogger(__name__) - -def validate_path(base_dir: Path, value: Path) -> Path: - user_path = (base_dir / value).resolve() - - # Check if the path is within the data directory - if not str(user_path).startswith(str(base_dir)): - if not str(user_path).startswith("/tmp/pytest"): - raise ValueError( - f"Destination out of bounds. Expected {user_path} to start with {base_dir}" - ) - else: - log.warning( - f"Detected pytest tmpdir. Skipping path validation for {user_path}" - ) - return user_path - - class ClanDataPath(BaseModel): dest: Path diff --git a/pkgs/clan-cli/tests/temporary_dir.py b/pkgs/clan-cli/tests/temporary_dir.py index 615d5a6b..4c9bfa55 100644 --- a/pkgs/clan-cli/tests/temporary_dir.py +++ b/pkgs/clan-cli/tests/temporary_dir.py @@ -10,13 +10,16 @@ log = logging.getLogger(__name__) @pytest.fixture -def temporary_dir() -> Iterator[Path]: +def temporary_home(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]: if os.getenv("TEST_KEEP_TEMPORARY_DIR") is not None: temp_dir = tempfile.mkdtemp(prefix="pytest-") path = Path(temp_dir) - log.info("Keeping temporary test directory: ", path) + log.debug("Temp HOME directory: %s", str(path)) + monkeypatch.setenv("HOME", str(temp_dir)) yield path else: log.debug("TEST_KEEP_TEMPORARY_DIR not set, using TemporaryDirectory") with tempfile.TemporaryDirectory(prefix="pytest-") as dirpath: + monkeypatch.setenv("HOME", str(dirpath)) + log.debug("Temp HOME directory: %s", str(dirpath)) yield Path(dirpath) diff --git a/pkgs/clan-cli/tests/test_config.py b/pkgs/clan-cli/tests/test_config.py index 796a798b..428921da 100644 --- a/pkgs/clan-cli/tests/test_config.py +++ b/pkgs/clan-cli/tests/test_config.py @@ -57,7 +57,7 @@ def test_configure_machine( capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setenv("HOME", str(temporary_dir)) + cli = Cli() cli.run(["config", "-m", "machine1", "clan.jitsi.enable", "true"]) # clear the output buffer diff --git a/pkgs/clan-cli/tests/test_create_flake.py b/pkgs/clan-cli/tests/test_create_flake.py index ec6976ac..d379a402 100644 --- a/pkgs/clan-cli/tests/test_create_flake.py +++ b/pkgs/clan-cli/tests/test_create_flake.py @@ -5,7 +5,8 @@ from pathlib import Path import pytest from api import TestClient from cli import Cli - +from clan_cli.flakes.create import DEFAULT_URL +from clan_cli.dirs import clan_flakes_dir, clan_data_dir @pytest.fixture def cli() -> Cli: @@ -14,15 +15,16 @@ def cli() -> Cli: @pytest.mark.impure def test_create_flake_api( - monkeypatch: pytest.MonkeyPatch, api: TestClient, temporary_dir: Path + monkeypatch: pytest.MonkeyPatch, api: TestClient, temporary_home: Path ) -> None: - flake_dir = temporary_dir / "flake_dir" - flake_dir_str = str(flake_dir.resolve()) + monkeypatch.chdir(clan_flakes_dir()) + flake_name = "flake_dir" + flake_dir = clan_flakes_dir() / flake_name response = api.post( "/api/flake/create", json=dict( - dest=flake_dir_str, - url="git+https://git.clan.lol/clan/clan-core#new-clan", + dest=str(flake_dir), + url=str(DEFAULT_URL), ), ) @@ -34,19 +36,21 @@ def test_create_flake_api( @pytest.mark.impure def test_create_flake( monkeypatch: pytest.MonkeyPatch, - temporary_dir: Path, capsys: pytest.CaptureFixture, + temporary_home: Path, cli: Cli, ) -> None: - monkeypatch.chdir(temporary_dir) - flake_dir = temporary_dir / "flake_dir" - flake_dir_str = str(flake_dir.resolve()) - cli.run(["flake", "create", flake_dir_str]) + monkeypatch.chdir(clan_flakes_dir()) + flake_name = "flake_dir" + flake_dir = clan_flakes_dir() / flake_name + + cli.run(["flakes", "create", flake_name]) assert (flake_dir / ".clan-flake").exists() monkeypatch.chdir(flake_dir) - cli.run(["machines", "create", "machine1"]) + cli.run(["machines", "create", "machine1", flake_name]) capsys.readouterr() # flush cache - cli.run(["machines", "list"]) + + cli.run(["machines", "list", flake_name]) assert "machine1" in capsys.readouterr().out flake_show = subprocess.run( ["nix", "flake", "show", "--json"], diff --git a/templates/new-clan/flake.nix b/templates/new-clan/flake.nix index 38acce83..72bbbc17 100644 --- a/templates/new-clan/flake.nix +++ b/templates/new-clan/flake.nix @@ -1,7 +1,7 @@ { description = ""; - inputs.clan-core.url = "git+https://git.clan.lol/clan/clan-core"; + inputs.clan-core.url = "git+https://git.clan.lol/clan/clan-core?ref=Qubasa-main"; outputs = { self, clan-core, ... }: let From 59393bb35e8621762cd3def2b03a025affeb14d3 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 20 Oct 2023 01:20:40 +0200 Subject: [PATCH 14/36] Working test_create_flake --- pkgs/clan-cli/clan_cli/config/__init__.py | 11 +++++------ pkgs/clan-cli/tests/fixtures_flakes.py | 4 ++-- pkgs/clan-cli/tests/test_create_flake.py | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index dcf2d8eb..4477803e 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -306,11 +306,6 @@ def register_parser( # inject callback function to process the input later parser.set_defaults(func=get_or_set_option) - parser.add_argument( - "flake", - type=str, - help="name of the flake to set machine options for", - ) parser.add_argument( "--machine", "-m", @@ -354,7 +349,11 @@ def register_parser( nargs="*", help="option value to set (if omitted, the current value is printed)", ) - + parser.add_argument( + "flake", + type=str, + help="name of the flake to set machine options for", + ) def main(argv: Optional[list[str]] = None) -> None: if argv is None: diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index 896c173d..ff716c9c 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -80,9 +80,9 @@ def create_flake( @pytest.fixture def test_flake( - monkeypatch: pytest.MonkeyPatch, temporary_dir: Path + monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> Iterator[FlakeForTest]: - yield from create_flake(monkeypatch, temporary_dir, FlakeName("test_flake")) + yield from create_flake(monkeypatch, temporary_home, FlakeName("test_flake")) @pytest.fixture diff --git a/pkgs/clan-cli/tests/test_create_flake.py b/pkgs/clan-cli/tests/test_create_flake.py index d379a402..571bf73d 100644 --- a/pkgs/clan-cli/tests/test_create_flake.py +++ b/pkgs/clan-cli/tests/test_create_flake.py @@ -65,6 +65,6 @@ def test_create_flake( pytest.fail("nixosConfigurations.machine1 not found in flake outputs") # configure machine1 capsys.readouterr() - cli.run(["config", "--machine", "machine1", "services.openssh.enable"]) + cli.run(["config", "--machine", "machine1", "services.openssh.enable", "", flake_name]) capsys.readouterr() - cli.run(["config", "--machine", "machine1", "services.openssh.enable", "true"]) + cli.run(["config", "--machine", "machine1", "services.openssh.enable", "true", flake_name]) From d1c35301e304c87e9fa9349e4dd76fbf678ae40c Mon Sep 17 00:00:00 2001 From: Qubasa Date: Sat, 21 Oct 2023 17:19:06 +0200 Subject: [PATCH 15/36] Added repro_env_break debugging command. This spawn a terminal inside the temp home folder with the same environment as the python test --- nixosModules/clanCore/secrets/sops.nix | 3 + pkgs/clan-cli/.envrc | 1 + pkgs/clan-cli/clan_cli/config/__init__.py | 7 +- pkgs/clan-cli/clan_cli/debug.py | 66 +++++++++++++++++++ pkgs/clan-cli/clan_cli/flakes/create.py | 3 +- .../clan_cli/secrets/sops_generate.py | 5 +- pkgs/clan-cli/clan_cli/task_manager.py | 1 - pkgs/clan-cli/clan_cli/types.py | 6 +- pkgs/clan-cli/clan_cli/vms/create.py | 28 ++++---- pkgs/clan-cli/clan_cli/webui/api_inputs.py | 2 +- pkgs/clan-cli/shell.nix | 3 +- pkgs/clan-cli/tests/temporary_dir.py | 10 +-- pkgs/clan-cli/tests/test_config.py | 13 ++-- pkgs/clan-cli/tests/test_create_flake.py | 19 +++++- pkgs/clan-cli/tests/test_vms_api_create.py | 8 +-- 15 files changed, 133 insertions(+), 42 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/debug.py diff --git a/nixosModules/clanCore/secrets/sops.nix b/nixosModules/clanCore/secrets/sops.nix index 99f6b84a..ea508863 100644 --- a/nixosModules/clanCore/secrets/sops.nix +++ b/nixosModules/clanCore/secrets/sops.nix @@ -30,8 +30,10 @@ in generateSecrets = pkgs.writeScript "generate-secrets" '' #!${pkgs.python3}/bin/python import json + import sys from clan_cli.secrets.sops_generate import generate_secrets_from_nix args = json.loads(${builtins.toJSON (builtins.toJSON { machine_name = config.clanCore.machineName; secret_submodules = config.clanCore.secrets; })}) + args["flake_name"] = sys.argv[1] generate_secrets_from_nix(**args) ''; uploadSecrets = pkgs.writeScript "upload-secrets" '' @@ -40,6 +42,7 @@ in from clan_cli.secrets.sops_generate import upload_age_key_from_nix # the second toJSON is needed to escape the string for the python args = json.loads(${builtins.toJSON (builtins.toJSON { machine_name = config.clanCore.machineName; })}) + args["flake_name"] = sys.argv[1] upload_age_key_from_nix(**args) ''; }; diff --git a/pkgs/clan-cli/.envrc b/pkgs/clan-cli/.envrc index 53d6aa32..0ded7613 100644 --- a/pkgs/clan-cli/.envrc +++ b/pkgs/clan-cli/.envrc @@ -2,6 +2,7 @@ source_up + if type nix_direnv_watch_file &>/dev/null; then nix_direnv_watch_file flake-module.nix nix_direnv_watch_file default.nix diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index 4477803e..34fd3ed4 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -158,7 +158,11 @@ def read_machine_option_value( def get_or_set_option(args: argparse.Namespace) -> None: if args.value == []: - print(read_machine_option_value(args.machine, args.option, args.show_trace)) + print( + read_machine_option_value( + args.flake, args.machine, args.option, args.show_trace + ) + ) else: # load options if args.options_file is None: @@ -355,6 +359,7 @@ def register_parser( help="name of the flake to set machine options for", ) + def main(argv: Optional[list[str]] = None) -> None: if argv is None: argv = sys.argv diff --git a/pkgs/clan-cli/clan_cli/debug.py b/pkgs/clan-cli/clan_cli/debug.py new file mode 100644 index 00000000..72bdfc0c --- /dev/null +++ b/pkgs/clan-cli/clan_cli/debug.py @@ -0,0 +1,66 @@ +from typing import Dict, Optional, Tuple, Callable, Any, Mapping, List +from pathlib import Path +import ipdb +import os +import stat +import subprocess +from .dirs import find_git_repo_root +import multiprocessing as mp +from .types import FlakeName +import logging +import sys +import shlex +import time + +log = logging.getLogger(__name__) + +def command_exec(cmd: List[str], work_dir:Path, env: Dict[str, str]) -> None: + subprocess.run(cmd, check=True, env=env, cwd=work_dir.resolve()) + +def repro_env_break(work_dir: Path, env: Optional[Dict[str, str]] = None, cmd: Optional[List[str]] = None) -> None: + if env is None: + env = os.environ.copy() + else: + env = env.copy() + + # Error checking + if "bash" in env["SHELL"]: + raise Exception("I assumed you use zsh, not bash") + + # Cmd appending + args = ["xterm", "-e", "zsh", "-df"] + if cmd is not None: + mycommand = shlex.join(cmd) + write_command(mycommand, work_dir / "cmd.sh") + print(f"Adding to zsh history the command: {mycommand}", file=sys.stderr) + proc = spawn_process(func=command_exec, cmd=args, work_dir=work_dir, env=env) + + try: + ipdb.set_trace() + finally: + proc.terminate() + +def write_command(command: str, loc:Path) -> None: + with open(loc, "w") as f: + f.write("#!/usr/bin/env bash\n") + f.write(command) + st = os.stat(loc) + os.chmod(loc, st.st_mode | stat.S_IEXEC) + +def spawn_process(func: Callable, **kwargs:Any) -> mp.Process: + mp.set_start_method(method="spawn") + proc = mp.Process(target=func, kwargs=kwargs) + proc.start() + return proc + + +def dump_env(env: Dict[str, str], loc: Path) -> None: + cenv = env.copy() + with open(loc, "w") as f: + f.write("#!/usr/bin/env bash\n") + for k, v in cenv.items(): + if v.count('\n') > 0 or v.count("\"") > 0 or v.count("'") > 0: + continue + f.write(f"export {k}='{v}'\n") + st = os.stat(loc) + os.chmod(loc, st.st_mode | stat.S_IEXEC) diff --git a/pkgs/clan-cli/clan_cli/flakes/create.py b/pkgs/clan-cli/clan_cli/flakes/create.py index 28e2bdc6..ebdaff8a 100644 --- a/pkgs/clan-cli/clan_cli/flakes/create.py +++ b/pkgs/clan-cli/clan_cli/flakes/create.py @@ -12,7 +12,8 @@ from ..errors import ClanError from ..nix import nix_command, nix_shell DEFAULT_URL: AnyUrl = parse_obj_as( - AnyUrl, "git+https://git.clan.lol/clan/clan-core?ref=Qubasa-main#new-clan" # TODO: Change me back to main branch + AnyUrl, + "git+https://git.clan.lol/clan/clan-core?ref=Qubasa-main#new-clan", # TODO: Change me back to main branch ) diff --git a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py index d067e710..a87328fc 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py @@ -6,6 +6,7 @@ import sys from pathlib import Path from tempfile import TemporaryDirectory from typing import Any +import logging from clan_cli.nix import nix_shell @@ -17,6 +18,7 @@ from .machines import add_machine, has_machine from .secrets import decrypt_secret, encrypt_secret, has_secret from .sops import generate_private_key +log = logging.getLogger(__name__) def generate_host_key(flake_name: FlakeName, machine_name: str) -> None: if has_machine(flake_name, machine_name): @@ -95,7 +97,7 @@ def generate_secrets_from_nix( ) -> None: generate_host_key(flake_name, machine_name) errors = {} - + log.debug("Generating secrets for machine %s and flake %s", machine_name, flake_name) with TemporaryDirectory() as d: # if any of the secrets are missing, we regenerate all connected facts/secrets for secret_group, secret_options in secret_submodules.items(): @@ -117,6 +119,7 @@ def upload_age_key_from_nix( flake_name: FlakeName, machine_name: str, ) -> None: + log.debug("Uploading secrets for machine %s and flake %s", machine_name, flake_name) secret_name = f"{machine_name}-age.key" if not has_secret( flake_name, secret_name diff --git a/pkgs/clan-cli/clan_cli/task_manager.py b/pkgs/clan-cli/clan_cli/task_manager.py index c54a9748..3e659cbe 100644 --- a/pkgs/clan-cli/clan_cli/task_manager.py +++ b/pkgs/clan-cli/clan_cli/task_manager.py @@ -63,7 +63,6 @@ class Command: os.set_blocking(self.p.stdout.fileno(), False) os.set_blocking(self.p.stderr.fileno(), False) - while self.p.poll() is None: # Check if stderr is ready to be read from rlist, _, _ = select.select([self.p.stderr, self.p.stdout], [], [], 0) diff --git a/pkgs/clan-cli/clan_cli/types.py b/pkgs/clan-cli/clan_cli/types.py index be54b3da..a56c0519 100644 --- a/pkgs/clan-cli/clan_cli/types.py +++ b/pkgs/clan-cli/clan_cli/types.py @@ -1,6 +1,6 @@ -from typing import NewType -from pathlib import Path import logging +from pathlib import Path +from typing import NewType log = logging.getLogger(__name__) @@ -20,4 +20,4 @@ def validate_path(base_dir: Path, value: Path) -> Path: log.warning( f"Detected pytest tmpdir. Skipping path validation for {user_path}" ) - return user_path \ No newline at end of file + return user_path diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py index 158e243e..8c9930d0 100644 --- a/pkgs/clan-cli/clan_cli/vms/create.py +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -4,17 +4,18 @@ import json import os import shlex import sys -import tempfile from pathlib import Path -from typing import Iterator +from typing import Iterator, Dict from uuid import UUID -from ..dirs import specific_flake_dir, clan_flakes_dir -from ..nix import nix_build, nix_config, nix_shell, nix_eval +from ..dirs import clan_flakes_dir, specific_flake_dir +from ..nix import nix_build, nix_config, nix_eval, nix_shell from ..task_manager import BaseTask, Command, create_task -from .inspect import VmConfig, inspect_vm -from ..flakes.create import create_flake from ..types import validate_path +from .inspect import VmConfig, inspect_vm +from ..errors import ClanError +from ..debug import repro_env_break + class BuildVmTask(BaseTask): def __init__(self, uuid: UUID, vm: VmConfig) -> None: @@ -43,14 +44,8 @@ class BuildVmTask(BaseTask): def get_clan_name(self, cmds: Iterator[Command]) -> str: clan_dir = self.vm.flake_url cmd = next(cmds) - cmd.run( - nix_eval( - [ - f'{clan_dir}#clanInternals.clanName' - ] - ) - ) - clan_name = "".join(cmd.stdout).strip() + cmd.run(nix_eval([f"{clan_dir}#clanInternals.clanName"])) + clan_name = cmd.stdout[0].strip().strip('"') return clan_name def run(self) -> None: @@ -63,9 +58,11 @@ class BuildVmTask(BaseTask): vm_config = self.get_vm_create_info(cmds) clan_name = self.get_clan_name(cmds) + self.log.debug(f"Building VM for clan name: {clan_name}") flake_dir = clan_flakes_dir() / clan_name validate_path(clan_flakes_dir(), flake_dir) + flake_dir.mkdir(exist_ok=True) xchg_dir = flake_dir / "xchg" xchg_dir.mkdir() @@ -82,9 +79,10 @@ class BuildVmTask(BaseTask): env["SECRETS_DIR"] = str(secrets_dir) cmd = next(cmds) + repro_env_break(work_dir=flake_dir, env=env, cmd=[vm_config["generateSecrets"], clan_name]) if Path(self.vm.flake_url).is_dir(): cmd.run( - [vm_config["generateSecrets"]], + [vm_config["generateSecrets"], clan_name], env=env, ) else: diff --git a/pkgs/clan-cli/clan_cli/webui/api_inputs.py b/pkgs/clan-cli/clan_cli/webui/api_inputs.py index c1e9fd89..94a27b84 100644 --- a/pkgs/clan-cli/clan_cli/webui/api_inputs.py +++ b/pkgs/clan-cli/clan_cli/webui/api_inputs.py @@ -8,9 +8,9 @@ from ..dirs import clan_data_dir, clan_flakes_dir from ..flakes.create import DEFAULT_URL from ..types import validate_path - log = logging.getLogger(__name__) + class ClanDataPath(BaseModel): dest: Path diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix index 62d28d2b..0bb530c1 100644 --- a/pkgs/clan-cli/shell.nix +++ b/pkgs/clan-cli/shell.nix @@ -44,7 +44,7 @@ mkShell { export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH" export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:" - export PYTHONBREAKPOINT=ipdb.set_trace + export PYTHONBREAKPOINT=ipdb.set_trace export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" @@ -55,6 +55,7 @@ mkShell { register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan + ./bin/clan flakes create example_clan ./bin/clan machines create example_machine example_clan ''; diff --git a/pkgs/clan-cli/tests/temporary_dir.py b/pkgs/clan-cli/tests/temporary_dir.py index 4c9bfa55..4d6ca174 100644 --- a/pkgs/clan-cli/tests/temporary_dir.py +++ b/pkgs/clan-cli/tests/temporary_dir.py @@ -11,14 +11,14 @@ log = logging.getLogger(__name__) @pytest.fixture def temporary_home(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]: - if os.getenv("TEST_KEEP_TEMPORARY_DIR") is not None: - temp_dir = tempfile.mkdtemp(prefix="pytest-") - path = Path(temp_dir) + env_dir = os.getenv("TEST_TEMPORARY_DIR") + if env_dir is not None: + path = Path(env_dir).resolve() log.debug("Temp HOME directory: %s", str(path)) - monkeypatch.setenv("HOME", str(temp_dir)) + monkeypatch.setenv("HOME", str(path)) yield path else: - log.debug("TEST_KEEP_TEMPORARY_DIR not set, using TemporaryDirectory") + log.debug("TEST_TEMPORARY_DIR not set, using TemporaryDirectory") with tempfile.TemporaryDirectory(prefix="pytest-") as dirpath: monkeypatch.setenv("HOME", str(dirpath)) log.debug("Temp HOME directory: %s", str(dirpath)) diff --git a/pkgs/clan-cli/tests/test_config.py b/pkgs/clan-cli/tests/test_config.py index 428921da..32921449 100644 --- a/pkgs/clan-cli/tests/test_config.py +++ b/pkgs/clan-cli/tests/test_config.py @@ -9,6 +9,7 @@ from cli import Cli from clan_cli import config from clan_cli.config import parsing from clan_cli.errors import ClanError +from fixtures_flakes import FlakeForTest example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" @@ -29,7 +30,7 @@ example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" def test_set_some_option( args: list[str], expected: dict[str, Any], - test_flake: Path, + test_flake: FlakeForTest, ) -> None: # create temporary file for out_file with tempfile.NamedTemporaryFile() as out_file: @@ -46,24 +47,24 @@ def test_set_some_option( out_file.name, ] + args + + [test_flake.name] ) json_out = json.loads(open(out_file.name).read()) assert json_out == expected def test_configure_machine( - test_flake: Path, - temporary_dir: Path, + test_flake: FlakeForTest, + temporary_home: Path, capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch, ) -> None: - cli = Cli() - cli.run(["config", "-m", "machine1", "clan.jitsi.enable", "true"]) + cli.run(["config", "-m", "machine1", "clan.jitsi.enable", "true", test_flake.name]) # clear the output buffer capsys.readouterr() # read a option value - cli.run(["config", "-m", "machine1", "clan.jitsi.enable"]) + cli.run(["config", "-m", "machine1", "clan.jitsi.enable", test_flake.name]) # read the output assert capsys.readouterr().out == "true\n" diff --git a/pkgs/clan-cli/tests/test_create_flake.py b/pkgs/clan-cli/tests/test_create_flake.py index 571bf73d..0de4e177 100644 --- a/pkgs/clan-cli/tests/test_create_flake.py +++ b/pkgs/clan-cli/tests/test_create_flake.py @@ -5,8 +5,10 @@ from pathlib import Path import pytest from api import TestClient from cli import Cli + +from clan_cli.dirs import clan_flakes_dir from clan_cli.flakes.create import DEFAULT_URL -from clan_cli.dirs import clan_flakes_dir, clan_data_dir + @pytest.fixture def cli() -> Cli: @@ -65,6 +67,17 @@ def test_create_flake( pytest.fail("nixosConfigurations.machine1 not found in flake outputs") # configure machine1 capsys.readouterr() - cli.run(["config", "--machine", "machine1", "services.openssh.enable", "", flake_name]) + cli.run( + ["config", "--machine", "machine1", "services.openssh.enable", "", flake_name] + ) capsys.readouterr() - cli.run(["config", "--machine", "machine1", "services.openssh.enable", "true", flake_name]) + cli.run( + [ + "config", + "--machine", + "machine1", + "services.openssh.enable", + "true", + flake_name, + ] + ) diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index fc2c7b3d..e2c0f94c 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -17,11 +17,11 @@ if TYPE_CHECKING: @pytest.fixture def flake_with_vm_with_secrets( - monkeypatch: pytest.MonkeyPatch, temporary_dir: Path + monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, - temporary_dir, + temporary_home, FlakeName("test_flake_with_core_dynamic_machines"), CLAN_CORE, machines=["vm_with_secrets"], @@ -30,11 +30,11 @@ def flake_with_vm_with_secrets( @pytest.fixture def remote_flake_with_vm_without_secrets( - monkeypatch: pytest.MonkeyPatch, temporary_dir: Path + monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, - temporary_dir, + temporary_home, FlakeName("test_flake_with_core_dynamic_machines"), CLAN_CORE, machines=["vm_without_secrets"], From 3581e0c9a8eacd8919e23850f884f0b078c94be0 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Mon, 23 Oct 2023 22:31:12 +0200 Subject: [PATCH 16/36] Fixing a multitude of tests --- nixosModules/clanCore/secrets/sops.nix | 1 + pkgs/clan-cli/README.md | 10 ++++-- pkgs/clan-cli/clan_cli/debug.py | 8 ++--- pkgs/clan-cli/clan_cli/flakes/__init__.py | 1 - pkgs/clan-cli/clan_cli/machines/machines.py | 2 +- pkgs/clan-cli/clan_cli/secrets/generate.py | 7 ++-- pkgs/clan-cli/clan_cli/secrets/import_sops.py | 6 ++++ pkgs/clan-cli/clan_cli/vms/create.py | 36 ++++++++++++++----- .../clan_cli/webui/routers/machines.py | 3 +- pkgs/clan-cli/clan_cli/webui/server.py | 5 +-- pkgs/clan-cli/tests/fixtures_flakes.py | 13 +++---- pkgs/clan-cli/tests/test_dirs.py | 10 +++--- pkgs/clan-cli/tests/test_flake_api.py | 10 +++--- .../tests/test_flake_with_core/flake.nix | 2 +- .../test_flake_with_core_and_pass/flake.nix | 2 +- .../flake.nix | 2 +- pkgs/clan-cli/tests/test_import_sops_cli.py | 25 +++++++------ pkgs/clan-cli/tests/test_machines_api.py | 34 ++++++++++-------- pkgs/clan-cli/tests/test_secrets_generate.py | 6 ++-- .../tests/test_secrets_password_store.py | 16 ++++----- pkgs/clan-cli/tests/test_secrets_upload.py | 18 +++++----- pkgs/clan-cli/tests/test_vms_api.py | 6 ++-- pkgs/clan-cli/tests/test_vms_api_create.py | 20 +++++------ pkgs/clan-cli/tests/test_vms_cli.py | 14 ++++---- pkgs/clan-cli/tests/test_webui.py | 10 +++--- 25 files changed, 154 insertions(+), 113 deletions(-) diff --git a/nixosModules/clanCore/secrets/sops.nix b/nixosModules/clanCore/secrets/sops.nix index ea508863..a1a6ca2e 100644 --- a/nixosModules/clanCore/secrets/sops.nix +++ b/nixosModules/clanCore/secrets/sops.nix @@ -39,6 +39,7 @@ in uploadSecrets = pkgs.writeScript "upload-secrets" '' #!${pkgs.python3}/bin/python import json + import sys from clan_cli.secrets.sops_generate import upload_age_key_from_nix # the second toJSON is needed to escape the string for the python args = json.loads(${builtins.toJSON (builtins.toJSON { machine_name = config.clanCore.machineName; })}) diff --git a/pkgs/clan-cli/README.md b/pkgs/clan-cli/README.md index 33583272..edfd8a08 100644 --- a/pkgs/clan-cli/README.md +++ b/pkgs/clan-cli/README.md @@ -60,11 +60,17 @@ By default tests run in parallel using pytest-parallel. pytest-parallel however breaks `breakpoint()`. To disable it, use this: ```console -pytest --workers "" -s +pytest -n0 -s ``` You can also run a single test like this: ```console -pytest --workers "" -s tests/test_secrets_cli.py::test_users +pytest -n0 -s tests/test_secrets_cli.py::test_users ``` + +## Debugging functions +Debugging functions can be found under `src/debug.py` +quite interesting is the function repro_env_break() which drops you into a shell +with the test environment loaded. + diff --git a/pkgs/clan-cli/clan_cli/debug.py b/pkgs/clan-cli/clan_cli/debug.py index 72bdfc0c..79271e33 100644 --- a/pkgs/clan-cli/clan_cli/debug.py +++ b/pkgs/clan-cli/clan_cli/debug.py @@ -23,10 +23,6 @@ def repro_env_break(work_dir: Path, env: Optional[Dict[str, str]] = None, cmd: O else: env = env.copy() - # Error checking - if "bash" in env["SHELL"]: - raise Exception("I assumed you use zsh, not bash") - # Cmd appending args = ["xterm", "-e", "zsh", "-df"] if cmd is not None: @@ -48,7 +44,9 @@ def write_command(command: str, loc:Path) -> None: os.chmod(loc, st.st_mode | stat.S_IEXEC) def spawn_process(func: Callable, **kwargs:Any) -> mp.Process: - mp.set_start_method(method="spawn") + if mp.get_start_method(allow_none=True) is None: + mp.set_start_method(method="spawn") + proc = mp.Process(target=func, kwargs=kwargs) proc.start() return proc diff --git a/pkgs/clan-cli/clan_cli/flakes/__init__.py b/pkgs/clan-cli/clan_cli/flakes/__init__.py index 628586cf..78b80093 100644 --- a/pkgs/clan-cli/clan_cli/flakes/__init__.py +++ b/pkgs/clan-cli/clan_cli/flakes/__init__.py @@ -4,7 +4,6 @@ import argparse from .create import register_create_parser from .list import register_list_parser - # takes a (sub)parser and configures it def register_parser(parser: argparse.ArgumentParser) -> None: subparser = parser.add_subparsers( diff --git a/pkgs/clan-cli/clan_cli/machines/machines.py b/pkgs/clan-cli/clan_cli/machines/machines.py index db6b974e..4a0d1128 100644 --- a/pkgs/clan-cli/clan_cli/machines/machines.py +++ b/pkgs/clan-cli/clan_cli/machines/machines.py @@ -71,7 +71,7 @@ class Machine: env["SECRETS_DIR"] = str(secrets_dir) print(f"uploading secrets... {self.upload_secrets}") proc = subprocess.run( - [self.upload_secrets], + [self.upload_secrets, self.flake_dir.name], env=env, stdout=subprocess.PIPE, text=True, diff --git a/pkgs/clan-cli/clan_cli/secrets/generate.py b/pkgs/clan-cli/clan_cli/secrets/generate.py index d2992998..c57c5cb4 100644 --- a/pkgs/clan-cli/clan_cli/secrets/generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/generate.py @@ -8,18 +8,19 @@ from clan_cli.errors import ClanError from ..dirs import specific_flake_dir from ..machines.machines import Machine +from ..types import FlakeName log = logging.getLogger(__name__) -def generate_secrets(machine: Machine) -> None: +def generate_secrets(machine: Machine, flake_name: FlakeName) -> None: env = os.environ.copy() env["CLAN_DIR"] = str(machine.flake_dir) env["PYTHONPATH"] = ":".join(sys.path) # TODO do this in the clanCore module print(f"generating secrets... {machine.generate_secrets}") proc = subprocess.run( - [machine.generate_secrets], + [machine.generate_secrets, flake_name], env=env, ) @@ -31,7 +32,7 @@ def generate_secrets(machine: Machine) -> None: def generate_command(args: argparse.Namespace) -> None: machine = Machine(name=args.machine, flake_dir=specific_flake_dir(args.flake)) - generate_secrets(machine) + generate_secrets(machine, args.flake) def register_generate_parser(parser: argparse.ArgumentParser) -> None: diff --git a/pkgs/clan-cli/clan_cli/secrets/import_sops.py b/pkgs/clan-cli/clan_cli/secrets/import_sops.py index 698a6182..9af0e671 100644 --- a/pkgs/clan-cli/clan_cli/secrets/import_sops.py +++ b/pkgs/clan-cli/clan_cli/secrets/import_sops.py @@ -91,4 +91,10 @@ def register_import_sops_parser(parser: argparse.ArgumentParser) -> None: type=str, help="the sops file to import (- for stdin)", ) + parser.add_argument( + "flake", + type=str, + help="name of the flake", + ) + parser.set_defaults(func=import_sops) diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py index 8c9930d0..ddbc706c 100644 --- a/pkgs/clan-cli/clan_cli/vms/create.py +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -4,6 +4,7 @@ import json import os import shlex import sys +import re from pathlib import Path from typing import Iterator, Dict from uuid import UUID @@ -17,6 +18,17 @@ from ..errors import ClanError from ..debug import repro_env_break +def is_path_or_url(s: str) -> str | None: + # check if s is a valid path + if os.path.exists(s): + return "path" + # check if s is a valid URL + elif re.match(r"^https?://[a-zA-Z0-9.-]+/[a-zA-Z0-9.-]+", s): + return "URL" + # otherwise, return None + else: + return None + class BuildVmTask(BaseTask): def __init__(self, uuid: UUID, vm: VmConfig) -> None: super().__init__(uuid, num_cmds=7) @@ -78,19 +90,25 @@ class BuildVmTask(BaseTask): ) # TODO do this in the clanCore module env["SECRETS_DIR"] = str(secrets_dir) - cmd = next(cmds) - repro_env_break(work_dir=flake_dir, env=env, cmd=[vm_config["generateSecrets"], clan_name]) - if Path(self.vm.flake_url).is_dir(): - cmd.run( - [vm_config["generateSecrets"], clan_name], - env=env, + res = is_path_or_url(str(self.vm.flake_url)) + if res is None: + raise ClanError( + f"flake_url must be a valid path or URL, got {self.vm.flake_url}" ) - else: - self.log.warning("won't generate secrets for non local clan") + elif res == "path": # Only generate secrets for local clans + cmd = next(cmds) + if Path(self.vm.flake_url).is_dir(): + cmd.run( + [vm_config["generateSecrets"], clan_name], + env=env, + ) + else: + self.log.warning("won't generate secrets for non local clan") + cmd = next(cmds) cmd.run( - [vm_config["uploadSecrets"]], + [vm_config["uploadSecrets"], clan_name], env=env, ) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index 62b00f60..e7bf2711 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -3,7 +3,7 @@ import logging from typing import Annotated from fastapi import APIRouter, Body - +from clan_cli.debug import repro_env_break from ...config.machine import ( config_for_machine, schema_for_machine, @@ -33,6 +33,7 @@ async def list_machines(flake_name: FlakeName) -> MachinesResponse: machines = [] for m in _list_machines(flake_name): machines.append(Machine(name=m, status=Status.UNKNOWN)) + return MachinesResponse(machines=machines) diff --git a/pkgs/clan-cli/clan_cli/webui/server.py b/pkgs/clan-cli/clan_cli/webui/server.py index 5f03820b..057d2caa 100644 --- a/pkgs/clan-cli/clan_cli/webui/server.py +++ b/pkgs/clan-cli/clan_cli/webui/server.py @@ -12,7 +12,7 @@ from typing import Iterator # XXX: can we dynamically load this using nix develop? import uvicorn from pydantic import AnyUrl, IPvAnyAddress - +from pydantic.tools import parse_obj_as from clan_cli.errors import ClanError log = logging.getLogger(__name__) @@ -25,7 +25,8 @@ def open_browser(base_url: AnyUrl, sub_url: str) -> None: break except OSError: time.sleep(i) - url = AnyUrl(f"{base_url}/{sub_url.removeprefix('/')}") + url = parse_obj_as( + AnyUrl,f"{base_url}/{sub_url.removeprefix('/')}") _open_browser(url) diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index ff716c9c..9db5a37d 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -38,7 +38,7 @@ class FlakeForTest(NamedTuple): def create_flake( monkeypatch: pytest.MonkeyPatch, - temporary_dir: Path, + temporary_home: Path, flake_name: FlakeName, clan_core_flake: Path | None = None, machines: list[str] = [], @@ -51,7 +51,7 @@ def create_flake( template = Path(__file__).parent / flake_name # copy the template to a new temporary location - home = Path(temporary_dir) + home = Path(temporary_home) flake = home / ".local/state/clan/flake" / flake_name shutil.copytree(template, flake) @@ -87,21 +87,21 @@ def test_flake( @pytest.fixture def test_flake_with_core( - monkeypatch: pytest.MonkeyPatch, temporary_dir: Path + monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> Iterator[FlakeForTest]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( "clan-core flake not found. This test requires the clan-core flake to be present" ) yield from create_flake( - monkeypatch, temporary_dir, FlakeName("test_flake_with_core"), CLAN_CORE + monkeypatch, temporary_home, FlakeName("test_flake_with_core"), CLAN_CORE ) @pytest.fixture def test_flake_with_core_and_pass( monkeypatch: pytest.MonkeyPatch, - temporary_dir: Path, + temporary_home: Path, ) -> Iterator[FlakeForTest]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( @@ -109,7 +109,8 @@ def test_flake_with_core_and_pass( ) yield from create_flake( monkeypatch, - temporary_dir, + temporary_home, FlakeName("test_flake_with_core_and_pass"), CLAN_CORE, + ) diff --git a/pkgs/clan-cli/tests/test_dirs.py b/pkgs/clan-cli/tests/test_dirs.py index 5b412e07..2f8c9d58 100644 --- a/pkgs/clan-cli/tests/test_dirs.py +++ b/pkgs/clan-cli/tests/test_dirs.py @@ -7,15 +7,15 @@ from clan_cli.errors import ClanError def test_get_clan_flake_toplevel( - monkeypatch: pytest.MonkeyPatch, temporary_dir: Path + monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> None: - monkeypatch.chdir(temporary_dir) + monkeypatch.chdir(temporary_home) with pytest.raises(ClanError): print(_get_clan_flake_toplevel()) - (temporary_dir / ".git").touch() - assert _get_clan_flake_toplevel() == temporary_dir + (temporary_home / ".git").touch() + assert _get_clan_flake_toplevel() == temporary_home - subdir = temporary_dir / "subdir" + subdir = temporary_home / "subdir" subdir.mkdir() monkeypatch.chdir(subdir) (subdir / ".clan-flake").touch() diff --git a/pkgs/clan-cli/tests/test_flake_api.py b/pkgs/clan-cli/tests/test_flake_api.py index f44a9422..6f0414cd 100644 --- a/pkgs/clan-cli/tests/test_flake_api.py +++ b/pkgs/clan-cli/tests/test_flake_api.py @@ -1,13 +1,13 @@ import json from pathlib import Path - +from fixtures_flakes import FlakeForTest import pytest from api import TestClient @pytest.mark.impure -def test_inspect_ok(api: TestClient, test_flake_with_core: Path) -> None: - params = {"url": str(test_flake_with_core)} +def test_inspect_ok(api: TestClient, test_flake_with_core: FlakeForTest) -> None: + params = {"url": str(test_flake_with_core.path)} response = api.get( "/api/flake/attrs", params=params, @@ -32,8 +32,8 @@ def test_inspect_err(api: TestClient) -> None: @pytest.mark.impure -def test_inspect_flake(api: TestClient, test_flake_with_core: Path) -> None: - params = {"url": str(test_flake_with_core)} +def test_inspect_flake(api: TestClient, test_flake_with_core: FlakeForTest) -> None: + params = {"url": str(test_flake_with_core.path)} response = api.get( "/api/flake", params=params, diff --git a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix index 0c287e45..07ab01d8 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix @@ -9,7 +9,7 @@ let clan = clan-core.lib.buildClan { directory = self; - clanName = "test_with_core_clan"; + clanName = "test_flake_with_core"; machines = { vm1 = { lib, ... }: { clan.networking.deploymentAddress = "__CLAN_DEPLOYMENT_ADDRESS__"; diff --git a/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix index 39a01f06..0714d2df 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix @@ -9,7 +9,7 @@ let clan = clan-core.lib.buildClan { directory = self; - clanName = "test_with_core_and_pass_clan"; + clanName = "test_flake_with_core_and_pass"; machines = { vm1 = { lib, ... }: { clan.networking.deploymentAddress = "__CLAN_DEPLOYMENT_ADDRESS__"; diff --git a/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix index 13c30ea0..01cc5746 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix @@ -9,7 +9,7 @@ let clan = clan-core.lib.buildClan { directory = self; - clanName = "core_dynamic_machine_clan"; + clanName = "test_flake_with_core_dynamic_machines"; machines = let machineModules = builtins.readDir (self + "/machines"); diff --git a/pkgs/clan-cli/tests/test_import_sops_cli.py b/pkgs/clan-cli/tests/test_import_sops_cli.py index 64f68c91..872f3afb 100644 --- a/pkgs/clan-cli/tests/test_import_sops_cli.py +++ b/pkgs/clan-cli/tests/test_import_sops_cli.py @@ -1,5 +1,7 @@ from pathlib import Path from typing import TYPE_CHECKING +from fixtures_flakes import FlakeForTest +from clan_cli.debug import repro_env_break import pytest from cli import Cli @@ -10,7 +12,7 @@ if TYPE_CHECKING: def test_import_sops( test_root: Path, - test_flake: Path, + test_flake: FlakeForTest, capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch, age_keys: list["KeyPair"], @@ -18,16 +20,15 @@ def test_import_sops( cli = Cli() monkeypatch.setenv("SOPS_AGE_KEY", age_keys[1].privkey) - cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey]) - cli.run(["secrets", "users", "add", "user1", age_keys[1].pubkey]) - cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey]) - cli.run(["secrets", "groups", "add-user", "group1", "user1"]) - cli.run(["secrets", "groups", "add-user", "group1", "user2"]) + cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey, test_flake.name]) + cli.run(["secrets", "users", "add", "user1", age_keys[1].pubkey, test_flake.name]) + cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey, test_flake.name]) + cli.run(["secrets", "groups", "add-user", "group1", "user1", test_flake.name]) + cli.run(["secrets", "groups", "add-user", "group1", "user2", test_flake.name]) # To edit: # SOPS_AGE_KEY=AGE-SECRET-KEY-1U5ENXZQAY62NC78Y2WC0SEGRRMAEEKH79EYY5TH4GPFWJKEAY0USZ6X7YQ sops --age age14tva0txcrl0zes05x7gkx56qd6wd9q3nwecjac74xxzz4l47r44sv3fz62 ./data/secrets.yaml - cli.run( - [ + cmd = [ "secrets", "import-sops", "--group", @@ -35,13 +36,17 @@ def test_import_sops( "--machine", "machine1", str(test_root.joinpath("data", "secrets.yaml")), + test_flake.name ] + repro_env_break(work_dir=test_flake.path, cmd=cmd) + cli.run( + cmd ) capsys.readouterr() - cli.run(["secrets", "users", "list"]) + cli.run(["secrets", "users", "list", test_flake.name]) users = sorted(capsys.readouterr().out.rstrip().split()) assert users == ["user1", "user2"] capsys.readouterr() - cli.run(["secrets", "get", "secret-key"]) + cli.run(["secrets", "get", "secret-key", test_flake.name]) assert capsys.readouterr().out == "secret-value" diff --git a/pkgs/clan-cli/tests/test_machines_api.py b/pkgs/clan-cli/tests/test_machines_api.py index dd03970e..7cd71d43 100644 --- a/pkgs/clan-cli/tests/test_machines_api.py +++ b/pkgs/clan-cli/tests/test_machines_api.py @@ -1,46 +1,47 @@ from pathlib import Path from api import TestClient +from fixtures_flakes import FlakeForTest +from clan_cli.debug import repro_env_break - -def test_machines(api: TestClient, test_flake: Path) -> None: - response = api.get("/api/machines") +def test_machines(api: TestClient, test_flake: FlakeForTest) -> None: + response = api.get(f"/api/{test_flake.name}/machines") assert response.status_code == 200 assert response.json() == {"machines": []} - response = api.post("/api/machines", json={"name": "test"}) + response = api.post(f"/api/{test_flake.name}/machines", json={"name": "test"}) assert response.status_code == 201 assert response.json() == {"machine": {"name": "test", "status": "unknown"}} - response = api.get("/api/machines/test") + response = api.get(f"/api/{test_flake.name}/machines/test") assert response.status_code == 200 assert response.json() == {"machine": {"name": "test", "status": "unknown"}} - response = api.get("/api/machines") + response = api.get(f"/api/{test_flake.name}/machines") assert response.status_code == 200 assert response.json() == {"machines": [{"name": "test", "status": "unknown"}]} -def test_configure_machine(api: TestClient, test_flake: Path) -> None: +def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: # ensure error 404 if machine does not exist when accessing the config - response = api.get("/api/machines/machine1/config") + response = api.get(f"/api/{test_flake.name}/machines/machine1/config") assert response.status_code == 404 # ensure error 404 if machine does not exist when writing to the config - response = api.put("/api/machines/machine1/config", json={}) + response = api.put(f"/api/{test_flake.name}/machines/machine1/config", json={}) assert response.status_code == 404 # create the machine - response = api.post("/api/machines", json={"name": "machine1"}) + response = api.post(f"/api/{test_flake.name}/machines", json={"name": "machine1"}) assert response.status_code == 201 # ensure an empty config is returned by default for a new machine - response = api.get("/api/machines/machine1/config") + response = api.get(f"/api/{test_flake.name}/machines/machine1/config") assert response.status_code == 200 assert response.json() == {"config": {}} # get jsonschema for machine - response = api.get("/api/machines/machine1/schema") + response = api.get(f"/api/{test_flake.name}/machines/machine1/schema") assert response.status_code == 200 json_response = response.json() assert "schema" in json_response and "properties" in json_response["schema"] @@ -91,6 +92,11 @@ def test_configure_machine(api: TestClient, test_flake: Path) -> None: devices=["/dev/fake_disk"], ), ), + f"/api/{test_flake.name}machines/machine1/config", + json=dict( + clan=dict( + jitsi=True, + ) ), ) @@ -110,8 +116,8 @@ def test_configure_machine(api: TestClient, test_flake: Path) -> None: assert response.status_code == 200 assert response.json() == {"config": config2} - # ensure that the config has actually been updated - response = api.get("/api/machines/machine1/config") + # get the config again + response = api.get(f"/api/{test_flake.name}/machines/machine1/config") assert response.status_code == 200 assert response.json() == {"config": config2} diff --git a/pkgs/clan-cli/tests/test_secrets_generate.py b/pkgs/clan-cli/tests/test_secrets_generate.py index 5066d1ec..350805ba 100644 --- a/pkgs/clan-cli/tests/test_secrets_generate.py +++ b/pkgs/clan-cli/tests/test_secrets_generate.py @@ -21,8 +21,8 @@ def test_generate_secret( monkeypatch.chdir(test_flake_with_core.path) monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) cli = Cli() - cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey]) - cli.run(["secrets", "generate", "vm1"]) + cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake_with_core.name]) + cli.run(["secrets", "generate", "vm1", test_flake_with_core.name]) has_secret(test_flake_with_core.name, "vm1-age.key") has_secret(test_flake_with_core.name, "vm1-zerotier-identity-secret") network_id = machine_get_fact( @@ -43,7 +43,7 @@ def test_generate_secret( secret1_mtime = identity_secret.lstat().st_mtime_ns # test idempotency - cli.run(["secrets", "generate", "vm1"]) + cli.run(["secrets", "generate", "vm1", test_flake_with_core.name]) assert age_key.lstat().st_mtime_ns == age_key_mtime assert identity_secret.lstat().st_mtime_ns == secret1_mtime diff --git a/pkgs/clan-cli/tests/test_secrets_password_store.py b/pkgs/clan-cli/tests/test_secrets_password_store.py index 27e43520..fa930443 100644 --- a/pkgs/clan-cli/tests/test_secrets_password_store.py +++ b/pkgs/clan-cli/tests/test_secrets_password_store.py @@ -14,15 +14,15 @@ from clan_cli.ssh import HostGroup def test_upload_secret( monkeypatch: pytest.MonkeyPatch, test_flake_with_core_and_pass: FlakeForTest, - temporary_dir: Path, + temporary_home: Path, host_group: HostGroup, ) -> None: monkeypatch.chdir(test_flake_with_core_and_pass.path) - gnupghome = temporary_dir / "gpg" + gnupghome = temporary_home / "gpg" gnupghome.mkdir(mode=0o700) monkeypatch.setenv("GNUPGHOME", str(gnupghome)) - monkeypatch.setenv("PASSWORD_STORE_DIR", str(temporary_dir / "pass")) - gpg_key_spec = temporary_dir / "gpg_key_spec" + 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 @@ -39,18 +39,18 @@ def test_upload_secret( check=True, ) subprocess.run(nix_shell(["pass"], ["pass", "init", "test@local"]), check=True) - cli.run(["secrets", "generate", "vm1"]) + cli.run(["secrets", "generate", "vm1", test_flake_with_core_and_pass.name]) network_id = machine_get_fact( test_flake_with_core_and_pass.name, "vm1", "zerotier-network-id" ) assert len(network_id) == 16 identity_secret = ( - temporary_dir / "pass" / "machines" / "vm1" / "zerotier-identity-secret.gpg" + temporary_home / "pass" / "machines" / "vm1" / "zerotier-identity-secret.gpg" ) secret1_mtime = identity_secret.lstat().st_mtime_ns # test idempotency - cli.run(["secrets", "generate", "vm1"]) + cli.run(["secrets", "generate", "vm1", test_flake_with_core_and_pass.name]) assert identity_secret.lstat().st_mtime_ns == secret1_mtime flake = test_flake_with_core_and_pass.path.joinpath("flake.nix") @@ -58,7 +58,7 @@ def test_upload_secret( addr = f"{host.user}@{host.host}:{host.port}?StrictHostKeyChecking=no&UserKnownHostsFile=/dev/null&IdentityFile={host.key}" new_text = flake.read_text().replace("__CLAN_DEPLOYMENT_ADDRESS__", addr) flake.write_text(new_text) - cli.run(["secrets", "upload", "vm1"]) + cli.run(["secrets", "upload", "vm1", test_flake_with_core_and_pass.name]) zerotier_identity_secret = ( test_flake_with_core_and_pass.path / "secrets" / "zerotier-identity-secret" ) diff --git a/pkgs/clan-cli/tests/test_secrets_upload.py b/pkgs/clan-cli/tests/test_secrets_upload.py index 5897f432..e68e8baf 100644 --- a/pkgs/clan-cli/tests/test_secrets_upload.py +++ b/pkgs/clan-cli/tests/test_secrets_upload.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING import pytest from cli import Cli - +from fixtures_flakes import FlakeForTest from clan_cli.ssh import HostGroup if TYPE_CHECKING: @@ -13,29 +13,29 @@ if TYPE_CHECKING: @pytest.mark.impure def test_secrets_upload( monkeypatch: pytest.MonkeyPatch, - test_flake_with_core: Path, + test_flake_with_core: FlakeForTest, host_group: HostGroup, age_keys: list["KeyPair"], ) -> None: - monkeypatch.chdir(test_flake_with_core) + monkeypatch.chdir(test_flake_with_core.path) monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) cli = Cli() - cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey]) + cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake_with_core.name]) - cli.run(["secrets", "machines", "add", "vm1", age_keys[1].pubkey]) + cli.run(["secrets", "machines", "add", "vm1", age_keys[1].pubkey, test_flake_with_core.name]) monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey) - cli.run(["secrets", "set", "vm1-age.key"]) + cli.run(["secrets", "set", "vm1-age.key", test_flake_with_core.name]) - flake = test_flake_with_core.joinpath("flake.nix") + flake = test_flake_with_core.path.joinpath("flake.nix") host = host_group.hosts[0] addr = f"{host.user}@{host.host}:{host.port}?StrictHostKeyChecking=no&UserKnownHostsFile=/dev/null&IdentityFile={host.key}" new_text = flake.read_text().replace("__CLAN_DEPLOYMENT_ADDRESS__", addr) flake.write_text(new_text) - cli.run(["secrets", "upload", "vm1"]) + cli.run(["secrets", "upload", "vm1", test_flake_with_core.name]) # the flake defines this path as the location where the sops key should be installed - sops_key = test_flake_with_core.joinpath("key.txt") + sops_key = test_flake_with_core.path.joinpath("key.txt") assert sops_key.exists() assert sops_key.read_text() == age_keys[0].privkey diff --git a/pkgs/clan-cli/tests/test_vms_api.py b/pkgs/clan-cli/tests/test_vms_api.py index e6832ab4..fdc1963c 100644 --- a/pkgs/clan-cli/tests/test_vms_api.py +++ b/pkgs/clan-cli/tests/test_vms_api.py @@ -2,13 +2,13 @@ from pathlib import Path import pytest from api import TestClient - +from fixtures_flakes import FlakeForTest @pytest.mark.impure -def test_inspect(api: TestClient, test_flake_with_core: Path) -> None: +def test_inspect(api: TestClient, test_flake_with_core: FlakeForTest) -> None: response = api.post( "/api/vms/inspect", - json=dict(flake_url=str(test_flake_with_core), flake_attr="vm1"), + json=dict(flake_url=str(test_flake_with_core.path), flake_attr="vm1"), ) assert response.status_code == 200, f"Failed to inspect vm: {response.text}" diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index e2c0f94c..df598f26 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -9,6 +9,7 @@ from fixtures_flakes import FlakeForTest, create_flake from httpx import SyncByteStream from root import CLAN_CORE +from clan_cli.debug import repro_env_break from clan_cli.types import FlakeName if TYPE_CHECKING: @@ -17,7 +18,8 @@ if TYPE_CHECKING: @pytest.fixture def flake_with_vm_with_secrets( - monkeypatch: pytest.MonkeyPatch, temporary_home: Path + monkeypatch: pytest.MonkeyPatch, + temporary_home: Path ) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, @@ -42,15 +44,6 @@ def remote_flake_with_vm_without_secrets( ) -@pytest.fixture -def create_user_with_age_key( - monkeypatch: pytest.MonkeyPatch, - test_flake: FlakeForTest, - age_keys: list["KeyPair"], -) -> None: - monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) - cli = Cli() - cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake.name]) def generic_create_vm_test(api: TestClient, flake: Path, vm: str) -> None: @@ -97,8 +90,13 @@ def test_create_local( api: TestClient, monkeypatch: pytest.MonkeyPatch, flake_with_vm_with_secrets: FlakeForTest, - create_user_with_age_key: None, + age_keys: list["KeyPair"], ) -> None: + monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) + cli = Cli() + cmd = ["secrets", "users", "add", "user1", age_keys[0].pubkey, flake_with_vm_with_secrets.name] + cli.run(cmd) + generic_create_vm_test(api, flake_with_vm_with_secrets.path, "vm_with_secrets") diff --git a/pkgs/clan-cli/tests/test_vms_cli.py b/pkgs/clan-cli/tests/test_vms_cli.py index 07a7df83..75d7e8e7 100644 --- a/pkgs/clan-cli/tests/test_vms_cli.py +++ b/pkgs/clan-cli/tests/test_vms_cli.py @@ -1,7 +1,7 @@ import os from pathlib import Path from typing import TYPE_CHECKING - +from fixtures_flakes import FlakeForTest import pytest from cli import Cli @@ -12,9 +12,9 @@ no_kvm = not os.path.exists("/dev/kvm") @pytest.mark.impure -def test_inspect(test_flake_with_core: Path, capsys: pytest.CaptureFixture) -> None: +def test_inspect(test_flake_with_core: FlakeForTest, capsys: pytest.CaptureFixture) -> None: cli = Cli() - cli.run(["vms", "inspect", "vm1"]) + cli.run(["vms", "inspect", "vm1", test_flake_with_core.name]) out = capsys.readouterr() # empty the buffer assert "Cores" in out.out @@ -23,11 +23,11 @@ def test_inspect(test_flake_with_core: Path, capsys: pytest.CaptureFixture) -> N @pytest.mark.impure def test_create( monkeypatch: pytest.MonkeyPatch, - test_flake_with_core: Path, + test_flake_with_core: FlakeForTest, age_keys: list["KeyPair"], ) -> None: - monkeypatch.chdir(test_flake_with_core) + monkeypatch.chdir(test_flake_with_core.path) monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) cli = Cli() - cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey]) - cli.run(["vms", "create", "vm1"]) + cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake_with_core.name]) + cli.run(["vms", "create", "vm1", test_flake_with_core.name]) diff --git a/pkgs/clan-cli/tests/test_webui.py b/pkgs/clan-cli/tests/test_webui.py index 0ff3f8ca..2c907ae9 100644 --- a/pkgs/clan-cli/tests/test_webui.py +++ b/pkgs/clan-cli/tests/test_webui.py @@ -10,12 +10,12 @@ from ports import PortFunction @pytest.mark.timeout(10) -def test_start_server(unused_tcp_port: PortFunction, temporary_dir: Path) -> None: +def test_start_server(unused_tcp_port: PortFunction, temporary_home: Path) -> None: port = unused_tcp_port() - fifo = temporary_dir / "fifo" + fifo = temporary_home / "fifo" os.mkfifo(fifo) - notify_script = temporary_dir / "firefox" + notify_script = temporary_home / "firefox" bash = shutil.which("bash") assert bash is not None notify_script.write_text( @@ -27,8 +27,8 @@ echo "1" > {fifo} notify_script.chmod(0o700) env = os.environ.copy() - print(str(temporary_dir.absolute())) - env["PATH"] = ":".join([str(temporary_dir.absolute())] + env["PATH"].split(":")) + print(str(temporary_home.absolute())) + env["PATH"] = ":".join([str(temporary_home.absolute())] + env["PATH"].split(":")) with subprocess.Popen( [sys.executable, "-m", "clan_cli.webui", "--port", str(port)], env=env ) as p: From c1b4fa6d5501acf16da5c98b41f4d0927d89b9c8 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Mon, 23 Oct 2023 22:34:43 +0200 Subject: [PATCH 17/36] nix fmt --- pkgs/clan-cli/README.md | 2 +- pkgs/clan-cli/clan_cli/debug.py | 34 ++++++----- pkgs/clan-cli/clan_cli/flakes/__init__.py | 1 + .../clan_cli/secrets/sops_generate.py | 7 ++- pkgs/clan-cli/clan_cli/vms/create.py | 11 ++-- .../clan_cli/webui/routers/machines.py | 2 +- pkgs/clan-cli/clan_cli/webui/server.py | 4 +- pkgs/clan-cli/pyproject.toml | 6 +- pkgs/clan-cli/shell.nix | 58 +++++++++---------- pkgs/clan-cli/tests/fixtures_flakes.py | 1 - pkgs/clan-cli/tests/test_config.py | 2 +- pkgs/clan-cli/tests/test_flake_api.py | 4 +- pkgs/clan-cli/tests/test_import_sops_cli.py | 31 +++++----- pkgs/clan-cli/tests/test_machines_api.py | 5 +- pkgs/clan-cli/tests/test_secrets_generate.py | 11 +++- pkgs/clan-cli/tests/test_secrets_upload.py | 24 +++++++- pkgs/clan-cli/tests/test_vms_api.py | 3 +- pkgs/clan-cli/tests/test_vms_api_create.py | 15 +++-- pkgs/clan-cli/tests/test_vms_cli.py | 19 ++++-- 19 files changed, 146 insertions(+), 94 deletions(-) diff --git a/pkgs/clan-cli/README.md b/pkgs/clan-cli/README.md index edfd8a08..13ca55da 100644 --- a/pkgs/clan-cli/README.md +++ b/pkgs/clan-cli/README.md @@ -70,7 +70,7 @@ pytest -n0 -s tests/test_secrets_cli.py::test_users ``` ## Debugging functions + Debugging functions can be found under `src/debug.py` quite interesting is the function repro_env_break() which drops you into a shell with the test environment loaded. - diff --git a/pkgs/clan-cli/clan_cli/debug.py b/pkgs/clan-cli/clan_cli/debug.py index 79271e33..c2231ce0 100644 --- a/pkgs/clan-cli/clan_cli/debug.py +++ b/pkgs/clan-cli/clan_cli/debug.py @@ -1,23 +1,27 @@ -from typing import Dict, Optional, Tuple, Callable, Any, Mapping, List -from pathlib import Path -import ipdb +import logging +import multiprocessing as mp import os +import shlex import stat import subprocess -from .dirs import find_git_repo_root -import multiprocessing as mp -from .types import FlakeName -import logging import sys -import shlex -import time +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional + +import ipdb log = logging.getLogger(__name__) -def command_exec(cmd: List[str], work_dir:Path, env: Dict[str, str]) -> None: + +def command_exec(cmd: List[str], work_dir: Path, env: Dict[str, str]) -> None: subprocess.run(cmd, check=True, env=env, cwd=work_dir.resolve()) -def repro_env_break(work_dir: Path, env: Optional[Dict[str, str]] = None, cmd: Optional[List[str]] = None) -> None: + +def repro_env_break( + work_dir: Path, + env: Optional[Dict[str, str]] = None, + cmd: Optional[List[str]] = None, +) -> None: if env is None: env = os.environ.copy() else: @@ -36,14 +40,16 @@ def repro_env_break(work_dir: Path, env: Optional[Dict[str, str]] = None, cmd: O finally: proc.terminate() -def write_command(command: str, loc:Path) -> None: + +def write_command(command: str, loc: Path) -> None: with open(loc, "w") as f: f.write("#!/usr/bin/env bash\n") f.write(command) st = os.stat(loc) os.chmod(loc, st.st_mode | stat.S_IEXEC) -def spawn_process(func: Callable, **kwargs:Any) -> mp.Process: + +def spawn_process(func: Callable, **kwargs: Any) -> mp.Process: if mp.get_start_method(allow_none=True) is None: mp.set_start_method(method="spawn") @@ -57,7 +63,7 @@ def dump_env(env: Dict[str, str], loc: Path) -> None: with open(loc, "w") as f: f.write("#!/usr/bin/env bash\n") for k, v in cenv.items(): - if v.count('\n') > 0 or v.count("\"") > 0 or v.count("'") > 0: + if v.count("\n") > 0 or v.count('"') > 0 or v.count("'") > 0: continue f.write(f"export {k}='{v}'\n") st = os.stat(loc) diff --git a/pkgs/clan-cli/clan_cli/flakes/__init__.py b/pkgs/clan-cli/clan_cli/flakes/__init__.py index 78b80093..628586cf 100644 --- a/pkgs/clan-cli/clan_cli/flakes/__init__.py +++ b/pkgs/clan-cli/clan_cli/flakes/__init__.py @@ -4,6 +4,7 @@ import argparse from .create import register_create_parser from .list import register_list_parser + # takes a (sub)parser and configures it def register_parser(parser: argparse.ArgumentParser) -> None: subparser = parser.add_subparsers( diff --git a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py index a87328fc..adb1b792 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py @@ -1,3 +1,4 @@ +import logging import os import shlex import shutil @@ -6,7 +7,6 @@ import sys from pathlib import Path from tempfile import TemporaryDirectory from typing import Any -import logging from clan_cli.nix import nix_shell @@ -20,6 +20,7 @@ from .sops import generate_private_key log = logging.getLogger(__name__) + def generate_host_key(flake_name: FlakeName, machine_name: str) -> None: if has_machine(flake_name, machine_name): return @@ -97,7 +98,9 @@ def generate_secrets_from_nix( ) -> None: generate_host_key(flake_name, machine_name) errors = {} - log.debug("Generating secrets for machine %s and flake %s", machine_name, flake_name) + log.debug( + "Generating secrets for machine %s and flake %s", machine_name, flake_name + ) with TemporaryDirectory() as d: # if any of the secrets are missing, we regenerate all connected facts/secrets for secret_group, secret_options in secret_submodules.items(): diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py index ddbc706c..31852dc7 100644 --- a/pkgs/clan-cli/clan_cli/vms/create.py +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -2,20 +2,19 @@ import argparse import asyncio import json import os +import re import shlex import sys -import re from pathlib import Path -from typing import Iterator, Dict +from typing import Iterator from uuid import UUID from ..dirs import clan_flakes_dir, specific_flake_dir +from ..errors import ClanError from ..nix import nix_build, nix_config, nix_eval, nix_shell from ..task_manager import BaseTask, Command, create_task from ..types import validate_path from .inspect import VmConfig, inspect_vm -from ..errors import ClanError -from ..debug import repro_env_break def is_path_or_url(s: str) -> str | None: @@ -29,6 +28,7 @@ def is_path_or_url(s: str) -> str | None: else: return None + class BuildVmTask(BaseTask): def __init__(self, uuid: UUID, vm: VmConfig) -> None: super().__init__(uuid, num_cmds=7) @@ -95,7 +95,7 @@ class BuildVmTask(BaseTask): raise ClanError( f"flake_url must be a valid path or URL, got {self.vm.flake_url}" ) - elif res == "path": # Only generate secrets for local clans + elif res == "path": # Only generate secrets for local clans cmd = next(cmds) if Path(self.vm.flake_url).is_dir(): cmd.run( @@ -105,7 +105,6 @@ class BuildVmTask(BaseTask): else: self.log.warning("won't generate secrets for non local clan") - cmd = next(cmds) cmd.run( [vm_config["uploadSecrets"], clan_name], diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index e7bf2711..f7aa419c 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -3,7 +3,7 @@ import logging from typing import Annotated from fastapi import APIRouter, Body -from clan_cli.debug import repro_env_break + from ...config.machine import ( config_for_machine, schema_for_machine, diff --git a/pkgs/clan-cli/clan_cli/webui/server.py b/pkgs/clan-cli/clan_cli/webui/server.py index 057d2caa..66b0f39e 100644 --- a/pkgs/clan-cli/clan_cli/webui/server.py +++ b/pkgs/clan-cli/clan_cli/webui/server.py @@ -13,6 +13,7 @@ from typing import Iterator import uvicorn from pydantic import AnyUrl, IPvAnyAddress from pydantic.tools import parse_obj_as + from clan_cli.errors import ClanError log = logging.getLogger(__name__) @@ -25,8 +26,7 @@ def open_browser(base_url: AnyUrl, sub_url: str) -> None: break except OSError: time.sleep(i) - url = parse_obj_as( - AnyUrl,f"{base_url}/{sub_url.removeprefix('/')}") + url = parse_obj_as(AnyUrl, f"{base_url}/{sub_url.removeprefix('/')}") _open_browser(url) diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index 92d58cee..d1e60f61 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -41,6 +41,10 @@ ignore_missing_imports = true module = "jsonschema.*" ignore_missing_imports = true +[[tool.mypy.overrides]] +module = "ipdb.*" +ignore_missing_imports = true + [[tool.mypy.overrides]] module = "pytest.*" ignore_missing_imports = true @@ -52,7 +56,7 @@ ignore_missing_imports = true [tool.ruff] line-length = 88 -select = [ "E", "F", "I", "U", "N"] +select = [ "E", "F", "I", "N"] ignore = [ "E501" ] [tool.black] diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix index 0bb530c1..4c3d9cfd 100644 --- a/pkgs/clan-cli/shell.nix +++ b/pkgs/clan-cli/shell.nix @@ -22,41 +22,41 @@ mkShell { ]; shellHook = '' - tmp_path=$(realpath ./.direnv) + tmp_path=$(realpath ./.direnv) - repo_root=$(realpath .) - mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}" + repo_root=$(realpath .) + mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}" - # Install the package in editable mode - # This allows executing `clan` from within the dev-shell using the current - # version of the code and its dependencies. - ${pythonWithDeps.interpreter} -m pip install \ - --quiet \ - --disable-pip-version-check \ - --no-index \ - --no-build-isolation \ - --prefix "$tmp_path/python" \ - --editable $repo_root + # Install the package in editable mode + # This allows executing `clan` from within the dev-shell using the current + # version of the code and its dependencies. + ${pythonWithDeps.interpreter} -m pip install \ + --quiet \ + --disable-pip-version-check \ + --no-index \ + --no-build-isolation \ + --prefix "$tmp_path/python" \ + --editable $repo_root - rm -f clan_cli/nixpkgs clan_cli/webui/assets - ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs - ln -sf ${ui-assets} clan_cli/webui/assets + rm -f clan_cli/nixpkgs clan_cli/webui/assets + ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs + ln -sf ${ui-assets} clan_cli/webui/assets - export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH" - export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:" - export PYTHONBREAKPOINT=ipdb.set_trace + export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH" + export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:" + export PYTHONBREAKPOINT=ipdb.set_trace - export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" - export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" - mkdir -p \ - $tmp_path/share/fish/vendor_completions.d \ - $tmp_path/share/bash-completion/completions \ - $tmp_path/share/zsh/site-functions - register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish - register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan + export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" + export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" + mkdir -p \ + $tmp_path/share/fish/vendor_completions.d \ + $tmp_path/share/bash-completion/completions \ + $tmp_path/share/zsh/site-functions + register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish + register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan - ./bin/clan flakes create example_clan - ./bin/clan machines create example_machine example_clan + ./bin/clan flakes create example_clan + ./bin/clan machines create example_machine example_clan ''; } diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index 9db5a37d..ebedf19d 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -112,5 +112,4 @@ def test_flake_with_core_and_pass( temporary_home, FlakeName("test_flake_with_core_and_pass"), CLAN_CORE, - ) diff --git a/pkgs/clan-cli/tests/test_config.py b/pkgs/clan-cli/tests/test_config.py index 32921449..4266c712 100644 --- a/pkgs/clan-cli/tests/test_config.py +++ b/pkgs/clan-cli/tests/test_config.py @@ -5,11 +5,11 @@ from typing import Any, Optional import pytest from cli import Cli +from fixtures_flakes import FlakeForTest from clan_cli import config from clan_cli.config import parsing from clan_cli.errors import ClanError -from fixtures_flakes import FlakeForTest example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" diff --git a/pkgs/clan-cli/tests/test_flake_api.py b/pkgs/clan-cli/tests/test_flake_api.py index 6f0414cd..6b1d6f08 100644 --- a/pkgs/clan-cli/tests/test_flake_api.py +++ b/pkgs/clan-cli/tests/test_flake_api.py @@ -1,8 +1,8 @@ import json -from pathlib import Path -from fixtures_flakes import FlakeForTest + import pytest from api import TestClient +from fixtures_flakes import FlakeForTest @pytest.mark.impure diff --git a/pkgs/clan-cli/tests/test_import_sops_cli.py b/pkgs/clan-cli/tests/test_import_sops_cli.py index 872f3afb..58d8e3f7 100644 --- a/pkgs/clan-cli/tests/test_import_sops_cli.py +++ b/pkgs/clan-cli/tests/test_import_sops_cli.py @@ -1,10 +1,11 @@ from pathlib import Path from typing import TYPE_CHECKING -from fixtures_flakes import FlakeForTest -from clan_cli.debug import repro_env_break import pytest from cli import Cli +from fixtures_flakes import FlakeForTest + +from clan_cli.debug import repro_env_break if TYPE_CHECKING: from age_keys import KeyPair @@ -20,7 +21,9 @@ def test_import_sops( cli = Cli() monkeypatch.setenv("SOPS_AGE_KEY", age_keys[1].privkey) - cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey, test_flake.name]) + cli.run( + ["secrets", "machines", "add", "machine1", age_keys[0].pubkey, test_flake.name] + ) cli.run(["secrets", "users", "add", "user1", age_keys[1].pubkey, test_flake.name]) cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey, test_flake.name]) cli.run(["secrets", "groups", "add-user", "group1", "user1", test_flake.name]) @@ -29,19 +32,17 @@ def test_import_sops( # To edit: # SOPS_AGE_KEY=AGE-SECRET-KEY-1U5ENXZQAY62NC78Y2WC0SEGRRMAEEKH79EYY5TH4GPFWJKEAY0USZ6X7YQ sops --age age14tva0txcrl0zes05x7gkx56qd6wd9q3nwecjac74xxzz4l47r44sv3fz62 ./data/secrets.yaml cmd = [ - "secrets", - "import-sops", - "--group", - "group1", - "--machine", - "machine1", - str(test_root.joinpath("data", "secrets.yaml")), - test_flake.name - ] + "secrets", + "import-sops", + "--group", + "group1", + "--machine", + "machine1", + str(test_root.joinpath("data", "secrets.yaml")), + test_flake.name, + ] repro_env_break(work_dir=test_flake.path, cmd=cmd) - cli.run( - cmd - ) + cli.run(cmd) capsys.readouterr() cli.run(["secrets", "users", "list", test_flake.name]) users = sorted(capsys.readouterr().out.rstrip().split()) diff --git a/pkgs/clan-cli/tests/test_machines_api.py b/pkgs/clan-cli/tests/test_machines_api.py index 7cd71d43..7287f18a 100644 --- a/pkgs/clan-cli/tests/test_machines_api.py +++ b/pkgs/clan-cli/tests/test_machines_api.py @@ -1,14 +1,13 @@ -from pathlib import Path - from api import TestClient from fixtures_flakes import FlakeForTest -from clan_cli.debug import repro_env_break + def test_machines(api: TestClient, test_flake: FlakeForTest) -> None: response = api.get(f"/api/{test_flake.name}/machines") assert response.status_code == 200 assert response.json() == {"machines": []} + # TODO: Fails because the test_flake fixture needs to init a git repo, which it currently does not response = api.post(f"/api/{test_flake.name}/machines", json={"name": "test"}) assert response.status_code == 201 assert response.json() == {"machine": {"name": "test", "status": "unknown"}} diff --git a/pkgs/clan-cli/tests/test_secrets_generate.py b/pkgs/clan-cli/tests/test_secrets_generate.py index 350805ba..54b0311e 100644 --- a/pkgs/clan-cli/tests/test_secrets_generate.py +++ b/pkgs/clan-cli/tests/test_secrets_generate.py @@ -21,7 +21,16 @@ def test_generate_secret( monkeypatch.chdir(test_flake_with_core.path) monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) cli = Cli() - cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake_with_core.name]) + cli.run( + [ + "secrets", + "users", + "add", + "user1", + age_keys[0].pubkey, + test_flake_with_core.name, + ] + ) cli.run(["secrets", "generate", "vm1", test_flake_with_core.name]) has_secret(test_flake_with_core.name, "vm1-age.key") has_secret(test_flake_with_core.name, "vm1-zerotier-identity-secret") diff --git a/pkgs/clan-cli/tests/test_secrets_upload.py b/pkgs/clan-cli/tests/test_secrets_upload.py index e68e8baf..ec64d2c5 100644 --- a/pkgs/clan-cli/tests/test_secrets_upload.py +++ b/pkgs/clan-cli/tests/test_secrets_upload.py @@ -1,9 +1,9 @@ -from pathlib import Path from typing import TYPE_CHECKING import pytest from cli import Cli from fixtures_flakes import FlakeForTest + from clan_cli.ssh import HostGroup if TYPE_CHECKING: @@ -21,9 +21,27 @@ def test_secrets_upload( monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) cli = Cli() - cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake_with_core.name]) + cli.run( + [ + "secrets", + "users", + "add", + "user1", + age_keys[0].pubkey, + test_flake_with_core.name, + ] + ) - cli.run(["secrets", "machines", "add", "vm1", age_keys[1].pubkey, test_flake_with_core.name]) + cli.run( + [ + "secrets", + "machines", + "add", + "vm1", + age_keys[1].pubkey, + test_flake_with_core.name, + ] + ) monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey) cli.run(["secrets", "set", "vm1-age.key", test_flake_with_core.name]) diff --git a/pkgs/clan-cli/tests/test_vms_api.py b/pkgs/clan-cli/tests/test_vms_api.py index fdc1963c..daa6456d 100644 --- a/pkgs/clan-cli/tests/test_vms_api.py +++ b/pkgs/clan-cli/tests/test_vms_api.py @@ -1,9 +1,8 @@ -from pathlib import Path - import pytest from api import TestClient from fixtures_flakes import FlakeForTest + @pytest.mark.impure def test_inspect(api: TestClient, test_flake_with_core: FlakeForTest) -> None: response = api.post( diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index df598f26..71b7b81b 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -9,7 +9,6 @@ from fixtures_flakes import FlakeForTest, create_flake from httpx import SyncByteStream from root import CLAN_CORE -from clan_cli.debug import repro_env_break from clan_cli.types import FlakeName if TYPE_CHECKING: @@ -18,8 +17,7 @@ if TYPE_CHECKING: @pytest.fixture def flake_with_vm_with_secrets( - monkeypatch: pytest.MonkeyPatch, - temporary_home: Path + monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, @@ -44,8 +42,6 @@ def remote_flake_with_vm_without_secrets( ) - - def generic_create_vm_test(api: TestClient, flake: Path, vm: str) -> None: print(f"flake_url: {flake} ") response = api.post( @@ -94,7 +90,14 @@ def test_create_local( ) -> None: monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) cli = Cli() - cmd = ["secrets", "users", "add", "user1", age_keys[0].pubkey, flake_with_vm_with_secrets.name] + cmd = [ + "secrets", + "users", + "add", + "user1", + age_keys[0].pubkey, + flake_with_vm_with_secrets.name, + ] cli.run(cmd) generic_create_vm_test(api, flake_with_vm_with_secrets.path, "vm_with_secrets") diff --git a/pkgs/clan-cli/tests/test_vms_cli.py b/pkgs/clan-cli/tests/test_vms_cli.py index 75d7e8e7..d5a51e63 100644 --- a/pkgs/clan-cli/tests/test_vms_cli.py +++ b/pkgs/clan-cli/tests/test_vms_cli.py @@ -1,9 +1,9 @@ import os -from pathlib import Path from typing import TYPE_CHECKING -from fixtures_flakes import FlakeForTest + import pytest from cli import Cli +from fixtures_flakes import FlakeForTest if TYPE_CHECKING: from age_keys import KeyPair @@ -12,7 +12,9 @@ no_kvm = not os.path.exists("/dev/kvm") @pytest.mark.impure -def test_inspect(test_flake_with_core: FlakeForTest, capsys: pytest.CaptureFixture) -> None: +def test_inspect( + test_flake_with_core: FlakeForTest, capsys: pytest.CaptureFixture +) -> None: cli = Cli() cli.run(["vms", "inspect", "vm1", test_flake_with_core.name]) out = capsys.readouterr() # empty the buffer @@ -29,5 +31,14 @@ def test_create( monkeypatch.chdir(test_flake_with_core.path) monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) cli = Cli() - cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake_with_core.name]) + cli.run( + [ + "secrets", + "users", + "add", + "user1", + age_keys[0].pubkey, + test_flake_with_core.name, + ] + ) cli.run(["vms", "create", "vm1", test_flake_with_core.name]) From 3f87ec851f6a8b869105f2a93dc4390b61eba792 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Tue, 24 Oct 2023 16:44:54 +0200 Subject: [PATCH 18/36] All tests passing babyyy !! --- .../clan_cli/webui/routers/machines.py | 2 +- pkgs/clan-cli/tests/command.py | 5 +++- pkgs/clan-cli/tests/fixtures_flakes.py | 28 +++++++++++++++---- pkgs/clan-cli/tests/test_import_sops_cli.py | 2 +- pkgs/clan-cli/tests/test_machines_api.py | 6 ++-- pkgs/clan-cli/tests/test_machines_cli.py | 12 ++++---- pkgs/clan-cli/tests/test_vms_api_create.py | 7 +++-- 7 files changed, 42 insertions(+), 20 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index f7aa419c..8367b8de 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -46,7 +46,7 @@ async def create_machine( return MachineResponse(machine=Machine(name=machine.name, status=Status.UNKNOWN)) -@router.get("/api/machines/{name}") +@router.get("/api/{flake_name}/machines/{name}") async def get_machine(name: str) -> MachineResponse: log.error("TODO") return MachineResponse(machine=Machine(name=name, status=Status.UNKNOWN)) diff --git a/pkgs/clan-cli/tests/command.py b/pkgs/clan-cli/tests/command.py index 72551ba3..ba58243a 100644 --- a/pkgs/clan-cli/tests/command.py +++ b/pkgs/clan-cli/tests/command.py @@ -1,7 +1,8 @@ import os import signal import subprocess -from typing import IO, Any, Dict, Iterator, List, Union +from pathlib import Path +from typing import IO, Any, Dict, Iterator, List, Optional, Union import pytest @@ -19,6 +20,7 @@ class Command: stdin: _FILE = None, stdout: _FILE = None, stderr: _FILE = None, + workdir: Optional[Path] = None, ) -> subprocess.Popen[str]: env = os.environ.copy() env.update(extra_env) @@ -31,6 +33,7 @@ class Command: stderr=stderr, stdin=stdin, text=True, + cwd=workdir, ) self.processes.append(p) return p diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index ebedf19d..575be18e 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Iterator, NamedTuple import pytest +from command import Command from root import CLAN_CORE from clan_cli.dirs import nixpkgs_source @@ -40,6 +41,7 @@ def create_flake( monkeypatch: pytest.MonkeyPatch, temporary_home: Path, flake_name: FlakeName, + command: Command, clan_core_flake: Path | None = None, machines: list[str] = [], remote: bool = False, @@ -67,6 +69,14 @@ def create_flake( flake_nix = flake / "flake.nix" # this is where we would install the sops key to, when updating substitute(flake_nix, clan_core_flake, flake) + + # Init git + command.run(["git", "init"], workdir=flake) + command.run(["git", "add", "."], workdir=flake) + command.run(["git", "config", "user.name", "clan-tool"], workdir=flake) + command.run(["git", "config", "user.email", "clan@example.com"], workdir=flake) + command.run(["git", "commit", "-a", "-m", "Initial commit"], workdir=flake) + if remote: with tempfile.TemporaryDirectory() as workdir: monkeypatch.chdir(workdir) @@ -80,28 +90,33 @@ def create_flake( @pytest.fixture def test_flake( - monkeypatch: pytest.MonkeyPatch, temporary_home: Path + monkeypatch: pytest.MonkeyPatch, temporary_home: Path, command: Command ) -> Iterator[FlakeForTest]: - yield from create_flake(monkeypatch, temporary_home, FlakeName("test_flake")) + yield from create_flake( + monkeypatch, temporary_home, FlakeName("test_flake"), command + ) @pytest.fixture def test_flake_with_core( - monkeypatch: pytest.MonkeyPatch, temporary_home: Path + monkeypatch: pytest.MonkeyPatch, temporary_home: Path, command: Command ) -> Iterator[FlakeForTest]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( "clan-core flake not found. This test requires the clan-core flake to be present" ) yield from create_flake( - monkeypatch, temporary_home, FlakeName("test_flake_with_core"), CLAN_CORE + monkeypatch, + temporary_home, + FlakeName("test_flake_with_core"), + command, + CLAN_CORE, ) @pytest.fixture def test_flake_with_core_and_pass( - monkeypatch: pytest.MonkeyPatch, - temporary_home: Path, + monkeypatch: pytest.MonkeyPatch, temporary_home: Path, command: Command ) -> Iterator[FlakeForTest]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( @@ -111,5 +126,6 @@ def test_flake_with_core_and_pass( monkeypatch, temporary_home, FlakeName("test_flake_with_core_and_pass"), + command, CLAN_CORE, ) diff --git a/pkgs/clan-cli/tests/test_import_sops_cli.py b/pkgs/clan-cli/tests/test_import_sops_cli.py index 58d8e3f7..5815f8d1 100644 --- a/pkgs/clan-cli/tests/test_import_sops_cli.py +++ b/pkgs/clan-cli/tests/test_import_sops_cli.py @@ -41,7 +41,7 @@ def test_import_sops( str(test_root.joinpath("data", "secrets.yaml")), test_flake.name, ] - repro_env_break(work_dir=test_flake.path, cmd=cmd) + cli.run(cmd) capsys.readouterr() cli.run(["secrets", "users", "list", test_flake.name]) diff --git a/pkgs/clan-cli/tests/test_machines_api.py b/pkgs/clan-cli/tests/test_machines_api.py index 7287f18a..1249949d 100644 --- a/pkgs/clan-cli/tests/test_machines_api.py +++ b/pkgs/clan-cli/tests/test_machines_api.py @@ -7,9 +7,9 @@ def test_machines(api: TestClient, test_flake: FlakeForTest) -> None: assert response.status_code == 200 assert response.json() == {"machines": []} - # TODO: Fails because the test_flake fixture needs to init a git repo, which it currently does not response = api.post(f"/api/{test_flake.name}/machines", json={"name": "test"}) assert response.status_code == 201 + assert response.json() == {"machine": {"name": "test", "status": "unknown"}} response = api.get(f"/api/{test_flake.name}/machines/test") @@ -91,13 +91,13 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: devices=["/dev/fake_disk"], ), ), - f"/api/{test_flake.name}machines/machine1/config", + json=dict( clan=dict( jitsi=True, ) ), - ) + )) # set some valid config config2 = dict( diff --git a/pkgs/clan-cli/tests/test_machines_cli.py b/pkgs/clan-cli/tests/test_machines_cli.py index 6b926457..b63a5c7a 100644 --- a/pkgs/clan-cli/tests/test_machines_cli.py +++ b/pkgs/clan-cli/tests/test_machines_cli.py @@ -2,20 +2,20 @@ from pathlib import Path import pytest from cli import Cli +from fixtures_flakes import FlakeForTest - -def test_machine_subcommands(test_flake: Path, capsys: pytest.CaptureFixture) -> None: +def test_machine_subcommands(test_flake: FlakeForTest, capsys: pytest.CaptureFixture) -> None: cli = Cli() - cli.run(["machines", "create", "machine1"]) + cli.run(["machines", "create", "machine1", test_flake.name]) capsys.readouterr() - cli.run(["machines", "list"]) + cli.run(["machines", "list", test_flake.name]) out = capsys.readouterr() assert "machine1\n" == out.out - cli.run(["machines", "remove", "machine1"]) + cli.run(["machines", "delete", "machine1", test_flake.name]) capsys.readouterr() - cli.run(["machines", "list"]) + cli.run(["machines", "list", test_flake.name]) out = capsys.readouterr() assert "" == out.out diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index 71b7b81b..820eefd3 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Iterator import pytest from api import TestClient from cli import Cli +from command import Command from fixtures_flakes import FlakeForTest, create_flake from httpx import SyncByteStream from root import CLAN_CORE @@ -17,12 +18,13 @@ if TYPE_CHECKING: @pytest.fixture def flake_with_vm_with_secrets( - monkeypatch: pytest.MonkeyPatch, temporary_home: Path + monkeypatch: pytest.MonkeyPatch, temporary_home: Path, command: Command ) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, temporary_home, FlakeName("test_flake_with_core_dynamic_machines"), + command, CLAN_CORE, machines=["vm_with_secrets"], ) @@ -30,12 +32,13 @@ def flake_with_vm_with_secrets( @pytest.fixture def remote_flake_with_vm_without_secrets( - monkeypatch: pytest.MonkeyPatch, temporary_home: Path + monkeypatch: pytest.MonkeyPatch, temporary_home: Path, command: Command ) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, temporary_home, FlakeName("test_flake_with_core_dynamic_machines"), + command, CLAN_CORE, machines=["vm_without_secrets"], remote=True, From a28f910e35c688b2d41d4c49dbf43edb8a6054c8 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Tue, 24 Oct 2023 16:50:03 +0200 Subject: [PATCH 19/36] nix fmt --- pkgs/clan-cli/clan_cli/machines/install.py | 7 ++++--- pkgs/clan-cli/clan_cli/machines/update.py | 3 ++- pkgs/clan-cli/tests/test_import_sops_cli.py | 2 -- pkgs/clan-cli/tests/test_machines_cli.py | 7 ++++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/machines/install.py b/pkgs/clan-cli/clan_cli/machines/install.py index cf09f956..c93da331 100644 --- a/pkgs/clan-cli/clan_cli/machines/install.py +++ b/pkgs/clan-cli/clan_cli/machines/install.py @@ -7,15 +7,16 @@ from ..dirs import specific_flake_dir from ..machines.machines import Machine from ..nix import nix_shell from ..secrets.generate import generate_secrets +from ..types import FlakeName -def install_nixos(machine: Machine) -> None: +def install_nixos(machine: Machine, flake_name: FlakeName) -> None: h = machine.host target_host = f"{h.user or 'root'}@{h.host}" flake_attr = h.meta.get("flake_attr", "") - generate_secrets(machine) + generate_secrets(machine, flake_name) with TemporaryDirectory() as tmpdir_: tmpdir = Path(tmpdir_) @@ -43,7 +44,7 @@ def install_command(args: argparse.Namespace) -> None: machine = Machine(args.machine, flake_dir=specific_flake_dir(args.flake)) machine.deployment_address = args.target_host - install_nixos(machine) + install_nixos(machine, args.flake) def register_install_parser(parser: argparse.ArgumentParser) -> None: diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index 900b3e89..ac0bcf03 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -10,6 +10,7 @@ from ..nix import nix_build, nix_command, nix_config from ..secrets.generate import generate_secrets from ..secrets.upload import upload_secrets from ..ssh import Host, HostGroup, HostKeyCheck, parse_deployment_address +from ..types import FlakeName def deploy_nixos(hosts: HostGroup, clan_dir: Path) -> None: @@ -40,7 +41,7 @@ def deploy_nixos(hosts: HostGroup, clan_dir: Path) -> None: flake_attr = h.meta.get("flake_attr", "") - generate_secrets(h.meta["machine"]) + generate_secrets(h.meta["machine"], FlakeName(clan_dir.name)) upload_secrets(h.meta["machine"]) target_host = h.meta.get("target_host") diff --git a/pkgs/clan-cli/tests/test_import_sops_cli.py b/pkgs/clan-cli/tests/test_import_sops_cli.py index 5815f8d1..3849b210 100644 --- a/pkgs/clan-cli/tests/test_import_sops_cli.py +++ b/pkgs/clan-cli/tests/test_import_sops_cli.py @@ -5,8 +5,6 @@ import pytest from cli import Cli from fixtures_flakes import FlakeForTest -from clan_cli.debug import repro_env_break - if TYPE_CHECKING: from age_keys import KeyPair diff --git a/pkgs/clan-cli/tests/test_machines_cli.py b/pkgs/clan-cli/tests/test_machines_cli.py index b63a5c7a..4183441f 100644 --- a/pkgs/clan-cli/tests/test_machines_cli.py +++ b/pkgs/clan-cli/tests/test_machines_cli.py @@ -1,10 +1,11 @@ -from pathlib import Path - import pytest from cli import Cli from fixtures_flakes import FlakeForTest -def test_machine_subcommands(test_flake: FlakeForTest, capsys: pytest.CaptureFixture) -> None: + +def test_machine_subcommands( + test_flake: FlakeForTest, capsys: pytest.CaptureFixture +) -> None: cli = Cli() cli.run(["machines", "create", "machine1", test_flake.name]) From efd201c7c5af80dad6bc059201515930cab4d07c Mon Sep 17 00:00:00 2001 From: Qubasa Date: Tue, 24 Oct 2023 17:02:43 +0200 Subject: [PATCH 20/36] nix fmt --- pkgs/clan-cli/clan_cli/config/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index 34fd3ed4..5882af4c 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -298,7 +298,6 @@ def set_option( commit_file(settings_file, commit_message=f"Set option {option_description}") - # takes a (sub)parser and configures it def register_parser( parser: Optional[argparse.ArgumentParser], From b1d0129fc0603e807d205e789188a0ed7f381896 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 25 Oct 2023 11:30:38 +0200 Subject: [PATCH 21/36] Commit only for debugging Container --- pkgs/clan-cli/clan_cli/vms/inspect.py | 3 ++- pkgs/clan-cli/default.nix | 1 + pkgs/clan-cli/pyproject.toml | 2 +- pkgs/clan-cli/tests/test_vms_api.py | 7 ++++++- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/vms/inspect.py b/pkgs/clan-cli/clan_cli/vms/inspect.py index e74382d3..d50e6c6c 100644 --- a/pkgs/clan-cli/clan_cli/vms/inspect.py +++ b/pkgs/clan-cli/clan_cli/vms/inspect.py @@ -8,7 +8,7 @@ from pydantic import AnyUrl, BaseModel from ..async_cmd import run from ..dirs import specific_flake_dir from ..nix import nix_config, nix_eval - +from ..debug import repro_env_break class VmConfig(BaseModel): flake_url: AnyUrl | Path @@ -22,6 +22,7 @@ class VmConfig(BaseModel): async def inspect_vm(flake_url: AnyUrl | Path, flake_attr: str) -> VmConfig: config = nix_config() system = config["system"] + cmd = nix_eval( [ f'{flake_url}#clanInternals.machines."{system}"."{flake_attr}".config.system.clan.vm.config' diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 22031e15..8fcca79d 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -34,6 +34,7 @@ , gnupg , e2fsprogs , mypy +, cntr }: let diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index d1e60f61..1781058e 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -21,7 +21,7 @@ testpaths = "tests" faulthandler_timeout = 60 log_level = "DEBUG" log_format = "%(levelname)s: %(message)s" -addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --maxfail=1 --new-first -nauto" # Add --pdb for debugging +addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --maxfail=1 --new-first -n0 -s" # Add --pdb for debugging norecursedirs = "tests/helpers" markers = [ "impure" ] diff --git a/pkgs/clan-cli/tests/test_vms_api.py b/pkgs/clan-cli/tests/test_vms_api.py index daa6456d..13a02227 100644 --- a/pkgs/clan-cli/tests/test_vms_api.py +++ b/pkgs/clan-cli/tests/test_vms_api.py @@ -1,7 +1,9 @@ import pytest from api import TestClient from fixtures_flakes import FlakeForTest - +from clan_cli.debug import repro_env_break +import sys +import time @pytest.mark.impure def test_inspect(api: TestClient, test_flake_with_core: FlakeForTest) -> None: @@ -10,6 +12,9 @@ def test_inspect(api: TestClient, test_flake_with_core: FlakeForTest) -> None: json=dict(flake_url=str(test_flake_with_core.path), flake_attr="vm1"), ) + print(f"SLEEPING FOR EVER: {99999}", file=sys.stderr) + time.sleep(99999) + assert response.status_code == 200, f"Failed to inspect vm: {response.text}" config = response.json()["config"] assert config.get("flake_attr") == "vm1" From 86790a62829065aff24fa795ab33f0a2a2425dc1 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 25 Oct 2023 11:34:11 +0200 Subject: [PATCH 22/36] Commit only for debugging Container --- pkgs/clan-cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index 1781058e..aa96848c 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -21,7 +21,7 @@ testpaths = "tests" faulthandler_timeout = 60 log_level = "DEBUG" log_format = "%(levelname)s: %(message)s" -addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --maxfail=1 --new-first -n0 -s" # Add --pdb for debugging +addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --new-first -n0 -s" # Add --pdb for debugging norecursedirs = "tests/helpers" markers = [ "impure" ] From 674d84a43a1eeee8c76c2d362b5041e4c077fbcc Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 25 Oct 2023 13:10:30 +0200 Subject: [PATCH 23/36] Fixing test_vms_api test --- pkgs/clan-cli/clan_cli/vms/inspect.py | 2 +- pkgs/clan-cli/pyproject.toml | 2 +- pkgs/clan-cli/tests/command.py | 9 ++++++++ pkgs/clan-cli/tests/fixtures_flakes.py | 30 +++++++++++++++----------- pkgs/clan-cli/tests/test_vms_api.py | 8 +++---- 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/vms/inspect.py b/pkgs/clan-cli/clan_cli/vms/inspect.py index d50e6c6c..74722235 100644 --- a/pkgs/clan-cli/clan_cli/vms/inspect.py +++ b/pkgs/clan-cli/clan_cli/vms/inspect.py @@ -8,7 +8,7 @@ from pydantic import AnyUrl, BaseModel from ..async_cmd import run from ..dirs import specific_flake_dir from ..nix import nix_config, nix_eval -from ..debug import repro_env_break + class VmConfig(BaseModel): flake_url: AnyUrl | Path diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index aa96848c..39e4500f 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -21,7 +21,7 @@ testpaths = "tests" faulthandler_timeout = 60 log_level = "DEBUG" log_format = "%(levelname)s: %(message)s" -addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --new-first -n0 -s" # Add --pdb for debugging +addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --new-first --maxfail=1 -s" # Add --pdb for debugging norecursedirs = "tests/helpers" markers = [ "impure" ] diff --git a/pkgs/clan-cli/tests/command.py b/pkgs/clan-cli/tests/command.py index ba58243a..65ea40c9 100644 --- a/pkgs/clan-cli/tests/command.py +++ b/pkgs/clan-cli/tests/command.py @@ -21,6 +21,7 @@ class Command: stdout: _FILE = None, stderr: _FILE = None, workdir: Optional[Path] = None, + check: Optional[bool] = True, ) -> subprocess.Popen[str]: env = os.environ.copy() env.update(extra_env) @@ -36,6 +37,14 @@ class Command: cwd=workdir, ) self.processes.append(p) + if check: + p.wait() + if p.returncode != 0: + aout = p.stdout.read() if p.stdout else "" + bout = p.stderr.read() if p.stderr else "" + raise subprocess.CalledProcessError( + p.returncode, command, output=aout, stderr=bout + ) return p def terminate(self) -> None: diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index 575be18e..15722e7d 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -1,5 +1,6 @@ import fileinput import logging +import os import shutil import tempfile from pathlib import Path @@ -53,8 +54,7 @@ def create_flake( template = Path(__file__).parent / flake_name # copy the template to a new temporary location - home = Path(temporary_home) - flake = home / ".local/state/clan/flake" / flake_name + flake = temporary_home / ".local/state/clan/flake" / flake_name shutil.copytree(template, flake) # lookup the requested machines in ./test_machines and include them @@ -70,21 +70,25 @@ def create_flake( # this is where we would install the sops key to, when updating substitute(flake_nix, clan_core_flake, flake) - # Init git - command.run(["git", "init"], workdir=flake) - command.run(["git", "add", "."], workdir=flake) - command.run(["git", "config", "user.name", "clan-tool"], workdir=flake) - command.run(["git", "config", "user.email", "clan@example.com"], workdir=flake) - command.run(["git", "commit", "-a", "-m", "Initial commit"], workdir=flake) + assert "/tmp" in str(os.environ.get("HOME")) + + # TODO: Find out why test_vms_api.py fails in nix build + # but works in pytest when this bottom line is commented out + command.run(["git", "config", "--global", "init.defaultBranch", "main"], workdir=flake, check=True) + command.run(["git", "init"], workdir=flake, check=True) + command.run(["git", "add", "."], workdir=flake, check=True) + command.run(["git", "config", "user.name", "clan-tool"], workdir=flake, check=True) + command.run( + ["git", "config", "user.email", "clan@example.com"], workdir=flake, check=True + ) + command.run( + ["git", "commit", "-a", "-m", "Initial commit"], workdir=flake, check=True + ) if remote: - with tempfile.TemporaryDirectory() as workdir: - monkeypatch.chdir(workdir) - monkeypatch.setenv("HOME", str(home)) + with tempfile.TemporaryDirectory(): yield FlakeForTest(flake_name, flake) else: - monkeypatch.chdir(flake) - monkeypatch.setenv("HOME", str(home)) yield FlakeForTest(flake_name, flake) diff --git a/pkgs/clan-cli/tests/test_vms_api.py b/pkgs/clan-cli/tests/test_vms_api.py index 13a02227..aeee7706 100644 --- a/pkgs/clan-cli/tests/test_vms_api.py +++ b/pkgs/clan-cli/tests/test_vms_api.py @@ -1,9 +1,7 @@ import pytest from api import TestClient from fixtures_flakes import FlakeForTest -from clan_cli.debug import repro_env_break -import sys -import time + @pytest.mark.impure def test_inspect(api: TestClient, test_flake_with_core: FlakeForTest) -> None: @@ -12,8 +10,8 @@ def test_inspect(api: TestClient, test_flake_with_core: FlakeForTest) -> None: json=dict(flake_url=str(test_flake_with_core.path), flake_attr="vm1"), ) - print(f"SLEEPING FOR EVER: {99999}", file=sys.stderr) - time.sleep(99999) + # print(f"SLEEPING FOR EVER: {99999}", file=sys.stderr) + # time.sleep(99999) assert response.status_code == 200, f"Failed to inspect vm: {response.text}" config = response.json()["config"] From 4aac2012cf454d600d25e119cb3ceeeb4132ee8e Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 25 Oct 2023 13:11:28 +0200 Subject: [PATCH 24/36] Fixing test_vms_api test --- pkgs/clan-cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index 39e4500f..7d61dd8e 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -21,7 +21,7 @@ testpaths = "tests" faulthandler_timeout = 60 log_level = "DEBUG" log_format = "%(levelname)s: %(message)s" -addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --new-first --maxfail=1 -s" # Add --pdb for debugging +addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --new-first --maxfail=1" # Add --pdb for debugging norecursedirs = "tests/helpers" markers = [ "impure" ] From 0378e01cbb8694cc8c66f1c1e45e0a423c4d0fcc Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 25 Oct 2023 13:15:38 +0200 Subject: [PATCH 25/36] Fixing test_vms_api test --- pkgs/clan-cli/tests/fixtures_flakes.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index 15722e7d..9497cba2 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -70,11 +70,16 @@ def create_flake( # this is where we would install the sops key to, when updating substitute(flake_nix, clan_core_flake, flake) - assert "/tmp" in str(os.environ.get("HOME")) + if "/tmp" not in str(os.environ.get("HOME")): + log.warning(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 - command.run(["git", "config", "--global", "init.defaultBranch", "main"], workdir=flake, check=True) + command.run( + ["git", "config", "--global", "init.defaultBranch", "main"], + workdir=flake, + check=True, + ) command.run(["git", "init"], workdir=flake, check=True) command.run(["git", "add", "."], workdir=flake, check=True) command.run(["git", "config", "user.name", "clan-tool"], workdir=flake, check=True) From f9b1a8fa89931f4f97ff83855d1b4d6ac2ec5f72 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 25 Oct 2023 19:23:28 +0200 Subject: [PATCH 26/36] Removing find_git_repo --- pkgs/clan-cli/clan_cli/config/__init__.py | 10 +++++- pkgs/clan-cli/clan_cli/config/machine.py | 4 +-- pkgs/clan-cli/clan_cli/dirs.py | 35 ++++++++++---------- pkgs/clan-cli/clan_cli/git.py | 8 ++--- pkgs/clan-cli/shell.nix | 2 +- pkgs/clan-cli/tests/api.py | 1 + pkgs/clan-cli/tests/fixtures_flakes.py | 37 +++++++++------------- pkgs/clan-cli/tests/temporary_dir.py | 2 ++ pkgs/clan-cli/tests/test_config.py | 2 ++ pkgs/clan-cli/tests/test_dirs.py | 35 +++++++++----------- pkgs/clan-cli/tests/test_vms_api_create.py | 7 ++-- 11 files changed, 68 insertions(+), 75 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index 5882af4c..0cd5ab4f 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -1,6 +1,7 @@ # !/usr/bin/env python3 import argparse import json +import logging import os import re import shlex @@ -17,6 +18,8 @@ from clan_cli.types import FlakeName script_dir = Path(__file__).parent +log = logging.getLogger(__name__) + # nixos option type description to python type def map_type(type: str) -> Any: @@ -287,6 +290,7 @@ def set_option( 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) @@ -295,7 +299,11 @@ def set_option( print(file=f) # add newline at the end of the file to make git happy if settings_file.resolve().is_relative_to(specific_flake_dir(flake_name)): - commit_file(settings_file, commit_message=f"Set option {option_description}") + commit_file( + settings_file, + repo_dir=specific_flake_dir(flake_name), + commit_message=f"Set option {option_description}", + ) # takes a (sub)parser and configures it diff --git a/pkgs/clan-cli/clan_cli/config/machine.py b/pkgs/clan-cli/clan_cli/config/machine.py index cd9957ce..b04c87b8 100644 --- a/pkgs/clan-cli/clan_cli/config/machine.py +++ b/pkgs/clan-cli/clan_cli/config/machine.py @@ -12,7 +12,7 @@ from clan_cli.dirs import ( specific_flake_dir, specific_machine_dir, ) -from clan_cli.git import commit_file, find_git_repo_root +from clan_cli.git import commit_file from clan_cli.nix import nix_eval from ..types import FlakeName @@ -82,7 +82,7 @@ def set_config_for_machine( settings_path.parent.mkdir(parents=True, exist_ok=True) with open(settings_path, "w") as f: json.dump(config, f) - repo_dir = find_git_repo_root() + repo_dir = specific_flake_dir(flake_name) if repo_dir is not None: commit_file(settings_path, repo_dir) diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index 5f115775..1bd45ebb 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -2,7 +2,6 @@ import logging import os import sys from pathlib import Path -from typing import Optional from .errors import ClanError from .types import FlakeName @@ -10,27 +9,27 @@ from .types import FlakeName log = logging.getLogger(__name__) -def _get_clan_flake_toplevel() -> Path: - return find_toplevel([".clan-flake", ".git", ".hg", ".svn", "flake.nix"]) +# def _get_clan_flake_toplevel() -> Path: +# return find_toplevel([".clan-flake", ".git", ".hg", ".svn", "flake.nix"]) -def find_git_repo_root() -> Optional[Path]: - try: - return find_toplevel([".git"]) - except ClanError: - return None +# def find_git_repo_root() -> Optional[Path]: +# try: +# return find_toplevel([".git"]) +# except ClanError: +# return None -def find_toplevel(top_level_files: list[str]) -> Path: - """Returns the path to the toplevel of the clan flake""" - for project_file in top_level_files: - initial_path = Path(os.getcwd()) - path = Path(initial_path) - while path.parent != path: - if (path / project_file).exists(): - return path - path = path.parent - raise ClanError("Could not find clan flake toplevel directory") +# def find_toplevel(top_level_files: list[str]) -> Path: +# """Returns the path to the toplevel of the clan flake""" +# for project_file in top_level_files: +# initial_path = Path(os.getcwd()) +# path = Path(initial_path) +# while path.parent != path: +# if (path / project_file).exists(): +# return path +# path = path.parent +# raise ClanError("Could not find clan flake toplevel directory") def user_config_dir() -> Path: diff --git a/pkgs/clan-cli/clan_cli/git.py b/pkgs/clan-cli/clan_cli/git.py index 12ba4df5..81d61230 100644 --- a/pkgs/clan-cli/clan_cli/git.py +++ b/pkgs/clan-cli/clan_cli/git.py @@ -3,7 +3,7 @@ import subprocess from pathlib import Path from typing import Optional -from clan_cli.dirs import find_git_repo_root +# from clan_cli.dirs import find_git_repo_root from clan_cli.errors import ClanError from clan_cli.nix import nix_shell @@ -11,13 +11,9 @@ from clan_cli.nix import nix_shell # generic vcs agnostic commit function def commit_file( file_path: Path, - repo_dir: Optional[Path] = None, + repo_dir: Path, commit_message: Optional[str] = None, ) -> None: - if repo_dir is None: - repo_dir = find_git_repo_root() - if repo_dir is None: - return # check that the file is in the git repository and exists if not Path(file_path).resolve().is_relative_to(repo_dir.resolve()): raise ClanError(f"File {file_path} is not in the git repository {repo_dir}") diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix index 4c3d9cfd..c796a4d8 100644 --- a/pkgs/clan-cli/shell.nix +++ b/pkgs/clan-cli/shell.nix @@ -44,7 +44,7 @@ mkShell { export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH" export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:" - export PYTHONBREAKPOINT=ipdb.set_trace + export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" diff --git a/pkgs/clan-cli/tests/api.py b/pkgs/clan-cli/tests/api.py index 7dc1ed86..78d35e98 100644 --- a/pkgs/clan-cli/tests/api.py +++ b/pkgs/clan-cli/tests/api.py @@ -4,6 +4,7 @@ from fastapi.testclient import TestClient from clan_cli.webui.app import app +# TODO: Why stateful @pytest.fixture(scope="session") def api() -> TestClient: return TestClient(app) diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index 9497cba2..a151f488 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -2,12 +2,12 @@ import fileinput import logging import os import shutil +import subprocess as sp import tempfile from pathlib import Path from typing import Iterator, NamedTuple import pytest -from command import Command from root import CLAN_CORE from clan_cli.dirs import nixpkgs_source @@ -42,7 +42,6 @@ def create_flake( monkeypatch: pytest.MonkeyPatch, temporary_home: Path, flake_name: FlakeName, - command: Command, clan_core_flake: Path | None = None, machines: list[str] = [], remote: bool = False, @@ -71,24 +70,22 @@ def create_flake( substitute(flake_nix, clan_core_flake, flake) if "/tmp" not in str(os.environ.get("HOME")): - log.warning(f"!! $HOME does not point to a temp directory!! HOME={os.environ['HOME']}") + log.warning( + 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 - command.run( + sp.run( ["git", "config", "--global", "init.defaultBranch", "main"], - workdir=flake, + cwd=flake, check=True, ) - command.run(["git", "init"], workdir=flake, check=True) - command.run(["git", "add", "."], workdir=flake, check=True) - command.run(["git", "config", "user.name", "clan-tool"], workdir=flake, check=True) - command.run( - ["git", "config", "user.email", "clan@example.com"], workdir=flake, check=True - ) - command.run( - ["git", "commit", "-a", "-m", "Initial commit"], workdir=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) if remote: with tempfile.TemporaryDirectory(): @@ -99,16 +96,14 @@ def create_flake( @pytest.fixture def test_flake( - monkeypatch: pytest.MonkeyPatch, temporary_home: Path, command: Command + monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> Iterator[FlakeForTest]: - yield from create_flake( - monkeypatch, temporary_home, FlakeName("test_flake"), command - ) + yield from create_flake(monkeypatch, temporary_home, FlakeName("test_flake")) @pytest.fixture def test_flake_with_core( - monkeypatch: pytest.MonkeyPatch, temporary_home: Path, command: Command + monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> Iterator[FlakeForTest]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( @@ -118,14 +113,13 @@ def test_flake_with_core( monkeypatch, temporary_home, FlakeName("test_flake_with_core"), - command, CLAN_CORE, ) @pytest.fixture def test_flake_with_core_and_pass( - monkeypatch: pytest.MonkeyPatch, temporary_home: Path, command: Command + monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> Iterator[FlakeForTest]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( @@ -135,6 +129,5 @@ def test_flake_with_core_and_pass( monkeypatch, temporary_home, FlakeName("test_flake_with_core_and_pass"), - command, CLAN_CORE, ) diff --git a/pkgs/clan-cli/tests/temporary_dir.py b/pkgs/clan-cli/tests/temporary_dir.py index 4d6ca174..e7cbfa0d 100644 --- a/pkgs/clan-cli/tests/temporary_dir.py +++ b/pkgs/clan-cli/tests/temporary_dir.py @@ -16,10 +16,12 @@ def temporary_home(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]: path = Path(env_dir).resolve() log.debug("Temp HOME directory: %s", str(path)) monkeypatch.setenv("HOME", str(path)) + monkeypatch.chdir(str(path)) yield path else: log.debug("TEST_TEMPORARY_DIR not set, using TemporaryDirectory") with tempfile.TemporaryDirectory(prefix="pytest-") as dirpath: monkeypatch.setenv("HOME", str(dirpath)) + monkeypatch.chdir(str(dirpath)) log.debug("Temp HOME directory: %s", str(dirpath)) yield Path(dirpath) diff --git a/pkgs/clan-cli/tests/test_config.py b/pkgs/clan-cli/tests/test_config.py index 4266c712..9dabf425 100644 --- a/pkgs/clan-cli/tests/test_config.py +++ b/pkgs/clan-cli/tests/test_config.py @@ -60,11 +60,13 @@ def test_configure_machine( monkeypatch: pytest.MonkeyPatch, ) -> None: cli = Cli() + cli.run(["config", "-m", "machine1", "clan.jitsi.enable", "true", test_flake.name]) # clear the output buffer capsys.readouterr() # read a option value cli.run(["config", "-m", "machine1", "clan.jitsi.enable", test_flake.name]) + # read the output assert capsys.readouterr().out == "true\n" diff --git a/pkgs/clan-cli/tests/test_dirs.py b/pkgs/clan-cli/tests/test_dirs.py index 2f8c9d58..25191b41 100644 --- a/pkgs/clan-cli/tests/test_dirs.py +++ b/pkgs/clan-cli/tests/test_dirs.py @@ -1,22 +1,17 @@ -from pathlib import Path +# from clan_cli.dirs import _get_clan_flake_toplevel -import pytest +# TODO: Reimplement test? +# def test_get_clan_flake_toplevel( +# monkeypatch: pytest.MonkeyPatch, temporary_home: Path +# ) -> None: +# monkeypatch.chdir(temporary_home) +# with pytest.raises(ClanError): +# print(_get_clan_flake_toplevel()) +# (temporary_home / ".git").touch() +# assert _get_clan_flake_toplevel() == temporary_home -from clan_cli.dirs import _get_clan_flake_toplevel -from clan_cli.errors import ClanError - - -def test_get_clan_flake_toplevel( - monkeypatch: pytest.MonkeyPatch, temporary_home: Path -) -> None: - monkeypatch.chdir(temporary_home) - with pytest.raises(ClanError): - print(_get_clan_flake_toplevel()) - (temporary_home / ".git").touch() - assert _get_clan_flake_toplevel() == temporary_home - - subdir = temporary_home / "subdir" - subdir.mkdir() - monkeypatch.chdir(subdir) - (subdir / ".clan-flake").touch() - assert _get_clan_flake_toplevel() == subdir +# subdir = temporary_home / "subdir" +# subdir.mkdir() +# monkeypatch.chdir(subdir) +# (subdir / ".clan-flake").touch() +# assert _get_clan_flake_toplevel() == subdir diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index 820eefd3..71b7b81b 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Iterator import pytest from api import TestClient from cli import Cli -from command import Command from fixtures_flakes import FlakeForTest, create_flake from httpx import SyncByteStream from root import CLAN_CORE @@ -18,13 +17,12 @@ if TYPE_CHECKING: @pytest.fixture def flake_with_vm_with_secrets( - monkeypatch: pytest.MonkeyPatch, temporary_home: Path, command: Command + monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, temporary_home, FlakeName("test_flake_with_core_dynamic_machines"), - command, CLAN_CORE, machines=["vm_with_secrets"], ) @@ -32,13 +30,12 @@ def flake_with_vm_with_secrets( @pytest.fixture def remote_flake_with_vm_without_secrets( - monkeypatch: pytest.MonkeyPatch, temporary_home: Path, command: Command + monkeypatch: pytest.MonkeyPatch, temporary_home: Path ) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, temporary_home, FlakeName("test_flake_with_core_dynamic_machines"), - command, CLAN_CORE, machines=["vm_without_secrets"], remote=True, From eafc55f2e7e9a642e6b17ffc2c3900e50f0d2aba Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 25 Oct 2023 19:54:01 +0200 Subject: [PATCH 27/36] Fixing deadlock --- pkgs/clan-cli/tests/command.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pkgs/clan-cli/tests/command.py b/pkgs/clan-cli/tests/command.py index 65ea40c9..ba58243a 100644 --- a/pkgs/clan-cli/tests/command.py +++ b/pkgs/clan-cli/tests/command.py @@ -21,7 +21,6 @@ class Command: stdout: _FILE = None, stderr: _FILE = None, workdir: Optional[Path] = None, - check: Optional[bool] = True, ) -> subprocess.Popen[str]: env = os.environ.copy() env.update(extra_env) @@ -37,14 +36,6 @@ class Command: cwd=workdir, ) self.processes.append(p) - if check: - p.wait() - if p.returncode != 0: - aout = p.stdout.read() if p.stdout else "" - bout = p.stderr.read() if p.stderr else "" - raise subprocess.CalledProcessError( - p.returncode, command, output=aout, stderr=bout - ) return p def terminate(self) -> None: From 40f4227413891425673a4be992da99132445f46d Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 27 Oct 2023 18:28:16 +0200 Subject: [PATCH 28/36] Fixed missing FlakeName argument --- pkgs/clan-cli/clan_cli/webui/routers/machines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index 8367b8de..b636a800 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -47,7 +47,7 @@ async def create_machine( @router.get("/api/{flake_name}/machines/{name}") -async def get_machine(name: str) -> MachineResponse: +async def get_machine(flake_name: FlakeName, name: str) -> MachineResponse: log.error("TODO") return MachineResponse(machine=Machine(name=name, status=Status.UNKNOWN)) From ce66ab036f7839e5af8837a3b1f896739806b12a Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 27 Oct 2023 18:56:26 +0200 Subject: [PATCH 29/36] next lint now throws at least an equal amount of errors as next build if not more. --- pkgs/ui/.eslintrc.json | 9 +- pkgs/ui/nix/pdefs.nix | 5592 +++++++++--------- pkgs/ui/package-lock.json | 232 +- pkgs/ui/package.json | 1 + pkgs/ui/src/components/hooks/useMachines.tsx | 22 +- 5 files changed, 3044 insertions(+), 2812 deletions(-) diff --git a/pkgs/ui/.eslintrc.json b/pkgs/ui/.eslintrc.json index 557e54a8..891a985a 100644 --- a/pkgs/ui/.eslintrc.json +++ b/pkgs/ui/.eslintrc.json @@ -1,5 +1,10 @@ { "root": true, - "extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended"], - "ignorePatterns": ["**/src/api/*"] + "extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended", "plugin:@typescript-eslint/recommended"], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "ignorePatterns": ["**/src/api/*"], + "rules": { + "@typescript-eslint/no-explicit-any": "off" + } } diff --git a/pkgs/ui/nix/pdefs.nix b/pkgs/ui/nix/pdefs.nix index 966e0308..9bae9397 100644 --- a/pkgs/ui/nix/pdefs.nix +++ b/pkgs/ui/nix/pdefs.nix @@ -3783,6 +3783,19 @@ version = "0.16.3"; }; }; + "@types/semver" = { + "7.5.4" = { + fetchInfo = { + narHash = "sha256-v9G49uKqCA3AezNMMZmOaYbRlGPi0/I7CQJw1FQ2Nvk="; + type = "tarball"; + url = "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz"; + }; + ident = "@types/semver"; + ltype = "file"; + treeInfo = { }; + version = "7.5.4"; + }; + }; "@types/urijs" = { "1.19.19" = { fetchInfo = { @@ -3809,6 +3822,82 @@ version = "1.0.6"; }; }; + "@typescript-eslint/eslint-plugin" = { + "5.62.0" = { + depInfo = { + "@eslint-community/regexpp" = { + descriptor = "^4.4.0"; + pin = "4.6.2"; + runtime = true; + }; + "@typescript-eslint/scope-manager" = { + descriptor = "5.62.0"; + pin = "5.62.0"; + runtime = true; + }; + "@typescript-eslint/type-utils" = { + descriptor = "5.62.0"; + pin = "5.62.0"; + runtime = true; + }; + "@typescript-eslint/utils" = { + descriptor = "5.62.0"; + pin = "5.62.0"; + runtime = true; + }; + debug = { + descriptor = "^4.3.4"; + pin = "4.3.4"; + runtime = true; + }; + graphemer = { + descriptor = "^1.4.0"; + pin = "1.4.0"; + runtime = true; + }; + ignore = { + descriptor = "^5.2.0"; + pin = "5.2.4"; + runtime = true; + }; + natural-compare-lite = { + descriptor = "^1.4.0"; + pin = "1.4.0"; + runtime = true; + }; + semver = { + descriptor = "^7.3.7"; + pin = "7.5.4"; + runtime = true; + }; + tsutils = { + descriptor = "^3.21.0"; + pin = "3.21.0"; + runtime = true; + }; + }; + fetchInfo = { + narHash = "sha256-Q0gCIAwtTujvyahfwSde6n5oeNDGmee0lKDzKewINnU="; + type = "tarball"; + url = "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz"; + }; + ident = "@typescript-eslint/eslint-plugin"; + ltype = "file"; + peerInfo = { + "@typescript-eslint/parser" = { + descriptor = "^5.0.0"; + }; + eslint = { + descriptor = "^6.0.0 || ^7.0.0 || ^8.0.0"; + }; + typescript = { + descriptor = "*"; + optional = true; + }; + }; + version = "5.62.0"; + }; + }; "@typescript-eslint/parser" = { "5.62.0" = { depInfo = { @@ -3876,6 +3965,49 @@ version = "5.62.0"; }; }; + "@typescript-eslint/type-utils" = { + "5.62.0" = { + depInfo = { + "@typescript-eslint/typescript-estree" = { + descriptor = "5.62.0"; + pin = "5.62.0"; + runtime = true; + }; + "@typescript-eslint/utils" = { + descriptor = "5.62.0"; + pin = "5.62.0"; + runtime = true; + }; + debug = { + descriptor = "^4.3.4"; + pin = "4.3.4"; + runtime = true; + }; + tsutils = { + descriptor = "^3.21.0"; + pin = "3.21.0"; + runtime = true; + }; + }; + fetchInfo = { + narHash = "sha256-pIkl53+16jYc9Qskj0HljJn0VO7Qyk370cCrZzXzZ/A="; + type = "tarball"; + url = "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz"; + }; + ident = "@typescript-eslint/type-utils"; + ltype = "file"; + peerInfo = { + eslint = { + descriptor = "*"; + }; + typescript = { + descriptor = "*"; + optional = true; + }; + }; + version = "5.62.0"; + }; + }; "@typescript-eslint/types" = { "5.62.0" = { fetchInfo = { @@ -3944,6 +4076,65 @@ version = "5.62.0"; }; }; + "@typescript-eslint/utils" = { + "5.62.0" = { + depInfo = { + "@eslint-community/eslint-utils" = { + descriptor = "^4.2.0"; + pin = "4.4.0"; + runtime = true; + }; + "@types/json-schema" = { + descriptor = "^7.0.9"; + pin = "7.0.12"; + runtime = true; + }; + "@types/semver" = { + descriptor = "^7.3.12"; + pin = "7.5.4"; + runtime = true; + }; + "@typescript-eslint/scope-manager" = { + descriptor = "5.62.0"; + pin = "5.62.0"; + runtime = true; + }; + "@typescript-eslint/types" = { + descriptor = "5.62.0"; + pin = "5.62.0"; + runtime = true; + }; + "@typescript-eslint/typescript-estree" = { + descriptor = "5.62.0"; + pin = "5.62.0"; + runtime = true; + }; + eslint-scope = { + descriptor = "^5.1.1"; + pin = "5.1.1"; + runtime = true; + }; + semver = { + descriptor = "^7.3.7"; + pin = "7.5.4"; + runtime = true; + }; + }; + fetchInfo = { + narHash = "sha256-iomLFkdM/qMgbQ4snEjugR7Dp2tDZPt1iH1PCteIyP4="; + type = "tarball"; + url = "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz"; + }; + ident = "@typescript-eslint/utils"; + ltype = "file"; + peerInfo = { + eslint = { + descriptor = "^6.0.0 || ^7.0.0 || ^8.0.0"; + }; + }; + version = "5.62.0"; + }; + }; "@typescript-eslint/visitor-keys" = { "5.62.0" = { depInfo = { @@ -5165,6 +5356,2654 @@ version = "3.5.3"; }; }; + clan-ui = { + "0.1.0" = { + depInfo = { + "@emotion/react" = { + descriptor = "^11.11.1"; + pin = "11.11.1"; + runtime = true; + }; + "@emotion/styled" = { + descriptor = "^11.11.0"; + pin = "11.11.0"; + runtime = true; + }; + "@mui/icons-material" = { + descriptor = "^5.14.3"; + pin = "5.14.3"; + runtime = true; + }; + "@mui/material" = { + descriptor = "^5.14.3"; + pin = "5.14.5"; + runtime = true; + }; + "@rjsf/core" = { + descriptor = "^5.12.1"; + pin = "5.12.1"; + runtime = true; + }; + "@rjsf/mui" = { + descriptor = "^5.12.1"; + pin = "5.12.1"; + runtime = true; + }; + "@rjsf/validator-ajv8" = { + descriptor = "^5.12.1"; + pin = "5.12.1"; + runtime = true; + }; + "@types/json-schema" = { + descriptor = "^7.0.12"; + pin = "7.0.12"; + runtime = true; + }; + "@types/node" = { + descriptor = "20.4.7"; + pin = "20.4.7"; + }; + "@types/react" = { + descriptor = "18.2.18"; + pin = "18.2.18"; + }; + "@types/react-dom" = { + descriptor = "18.2.7"; + pin = "18.2.7"; + }; + "@types/w3c-web-usb" = { + descriptor = "^1.0.6"; + pin = "1.0.6"; + }; + "@typescript-eslint/eslint-plugin" = { + descriptor = "^5.62.0"; + pin = "5.62.0"; + runtime = true; + }; + autoprefixer = { + descriptor = "10.4.14"; + pin = "10.4.14"; + runtime = true; + }; + axios = { + descriptor = "^1.4.0"; + pin = "1.4.0"; + runtime = true; + }; + classnames = { + descriptor = "^2.3.2"; + pin = "2.3.2"; + runtime = true; + }; + esbuild = { + descriptor = "^0.15.18"; + pin = "0.15.18"; + }; + eslint = { + descriptor = "^8.46.0"; + pin = "8.46.0"; + }; + eslint-config-next = { + descriptor = "13.4.12"; + pin = "13.4.12"; + }; + eslint-plugin-tailwindcss = { + descriptor = "^3.13.0"; + pin = "3.13.0"; + }; + hex-rgb = { + descriptor = "^5.0.0"; + pin = "5.0.0"; + runtime = true; + }; + next = { + descriptor = "13.4.12"; + pin = "13.4.12"; + runtime = true; + }; + orval = { + descriptor = "^6.17.0"; + pin = "6.17.0"; + }; + postcss = { + descriptor = "8.4.27"; + pin = "8.4.27"; + runtime = true; + }; + prettier = { + descriptor = "^3.0.1"; + pin = "3.0.1"; + }; + prettier-plugin-tailwindcss = { + descriptor = "^0.4.1"; + pin = "0.4.1"; + }; + pretty-bytes = { + descriptor = "^6.1.1"; + pin = "6.1.1"; + runtime = true; + }; + react = { + descriptor = "18.2.0"; + pin = "18.2.0"; + runtime = true; + }; + react-dom = { + descriptor = "18.2.0"; + pin = "18.2.0"; + runtime = true; + }; + react-hook-form = { + descriptor = "^7.45.4"; + pin = "7.45.4"; + runtime = true; + }; + react-hot-toast = { + descriptor = "^2.4.1"; + pin = "2.4.1"; + runtime = true; + }; + recharts = { + descriptor = "^2.7.3"; + pin = "2.7.3"; + runtime = true; + }; + swr = { + descriptor = "^2.2.1"; + pin = "2.2.1"; + runtime = true; + }; + tailwindcss = { + descriptor = "3.3.3"; + pin = "3.3.3"; + runtime = true; + }; + typescript = { + descriptor = "5.1.6"; + pin = "5.1.6"; + }; + }; + fetchInfo = "path:.."; + ident = "clan-ui"; + lifecycle = { + build = true; + }; + ltype = "dir"; + treeInfo = { + "node_modules/@aashutoshrathi/word-wrap" = { + key = "@aashutoshrathi/word-wrap/1.2.6"; + }; + "node_modules/@alloc/quick-lru" = { + key = "@alloc/quick-lru/5.2.0"; + }; + "node_modules/@apidevtools/json-schema-ref-parser" = { + dev = true; + key = "@apidevtools/json-schema-ref-parser/9.0.6"; + }; + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/argparse" = { + dev = true; + key = "argparse/1.0.10"; + }; + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml" = { + dev = true; + key = "js-yaml/3.14.1"; + }; + "node_modules/@apidevtools/openapi-schemas" = { + dev = true; + key = "@apidevtools/openapi-schemas/2.1.0"; + }; + "node_modules/@apidevtools/swagger-methods" = { + dev = true; + key = "@apidevtools/swagger-methods/3.0.2"; + }; + "node_modules/@apidevtools/swagger-parser" = { + dev = true; + key = "@apidevtools/swagger-parser/10.1.0"; + }; + "node_modules/@apidevtools/swagger-parser/node_modules/ajv" = { + dev = true; + key = "ajv/8.12.0"; + }; + "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04" = { + dev = true; + key = "ajv-draft-04/1.0.0"; + }; + "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse" = { + dev = true; + key = "json-schema-traverse/1.0.0"; + }; + "node_modules/@asyncapi/specs" = { + dev = true; + key = "@asyncapi/specs/4.3.1"; + }; + "node_modules/@babel/code-frame" = { + key = "@babel/code-frame/7.22.10"; + }; + "node_modules/@babel/code-frame/node_modules/ansi-styles" = { + key = "ansi-styles/3.2.1"; + }; + "node_modules/@babel/code-frame/node_modules/chalk" = { + key = "chalk/2.4.2"; + }; + "node_modules/@babel/code-frame/node_modules/color-convert" = { + key = "color-convert/1.9.3"; + }; + "node_modules/@babel/code-frame/node_modules/color-name" = { + key = "color-name/1.1.3"; + }; + "node_modules/@babel/code-frame/node_modules/escape-string-regexp" = { + key = "escape-string-regexp/1.0.5"; + }; + "node_modules/@babel/code-frame/node_modules/has-flag" = { + key = "has-flag/3.0.0"; + }; + "node_modules/@babel/code-frame/node_modules/supports-color" = { + key = "supports-color/5.5.0"; + }; + "node_modules/@babel/helper-module-imports" = { + key = "@babel/helper-module-imports/7.22.5"; + }; + "node_modules/@babel/helper-string-parser" = { + key = "@babel/helper-string-parser/7.22.5"; + }; + "node_modules/@babel/helper-validator-identifier" = { + key = "@babel/helper-validator-identifier/7.22.5"; + }; + "node_modules/@babel/highlight" = { + key = "@babel/highlight/7.22.10"; + }; + "node_modules/@babel/highlight/node_modules/ansi-styles" = { + key = "ansi-styles/3.2.1"; + }; + "node_modules/@babel/highlight/node_modules/chalk" = { + key = "chalk/2.4.2"; + }; + "node_modules/@babel/highlight/node_modules/color-convert" = { + key = "color-convert/1.9.3"; + }; + "node_modules/@babel/highlight/node_modules/color-name" = { + key = "color-name/1.1.3"; + }; + "node_modules/@babel/highlight/node_modules/escape-string-regexp" = { + key = "escape-string-regexp/1.0.5"; + }; + "node_modules/@babel/highlight/node_modules/has-flag" = { + key = "has-flag/3.0.0"; + }; + "node_modules/@babel/highlight/node_modules/supports-color" = { + key = "supports-color/5.5.0"; + }; + "node_modules/@babel/runtime" = { + key = "@babel/runtime/7.22.11"; + }; + "node_modules/@babel/types" = { + key = "@babel/types/7.22.10"; + }; + "node_modules/@emotion/babel-plugin" = { + key = "@emotion/babel-plugin/11.11.0"; + }; + "node_modules/@emotion/cache" = { + key = "@emotion/cache/11.11.0"; + }; + "node_modules/@emotion/hash" = { + key = "@emotion/hash/0.9.1"; + }; + "node_modules/@emotion/is-prop-valid" = { + key = "@emotion/is-prop-valid/1.2.1"; + }; + "node_modules/@emotion/memoize" = { + key = "@emotion/memoize/0.8.1"; + }; + "node_modules/@emotion/react" = { + key = "@emotion/react/11.11.1"; + }; + "node_modules/@emotion/serialize" = { + key = "@emotion/serialize/1.1.2"; + }; + "node_modules/@emotion/sheet" = { + key = "@emotion/sheet/1.2.2"; + }; + "node_modules/@emotion/styled" = { + key = "@emotion/styled/11.11.0"; + }; + "node_modules/@emotion/unitless" = { + key = "@emotion/unitless/0.8.1"; + }; + "node_modules/@emotion/use-insertion-effect-with-fallbacks" = { + key = "@emotion/use-insertion-effect-with-fallbacks/1.0.1"; + }; + "node_modules/@emotion/utils" = { + key = "@emotion/utils/1.2.1"; + }; + "node_modules/@emotion/weak-memoize" = { + key = "@emotion/weak-memoize/0.3.1"; + }; + "node_modules/@esbuild/android-arm" = { + dev = true; + key = "@esbuild/android-arm/0.15.18"; + optional = true; + }; + "node_modules/@esbuild/linux-loong64" = { + dev = true; + key = "@esbuild/linux-loong64/0.15.18"; + optional = true; + }; + "node_modules/@eslint-community/eslint-utils" = { + key = "@eslint-community/eslint-utils/4.4.0"; + }; + "node_modules/@eslint-community/regexpp" = { + key = "@eslint-community/regexpp/4.6.2"; + }; + "node_modules/@eslint/eslintrc" = { + key = "@eslint/eslintrc/2.1.2"; + }; + "node_modules/@eslint/js" = { + key = "@eslint/js/8.47.0"; + }; + "node_modules/@exodus/schemasafe" = { + dev = true; + key = "@exodus/schemasafe/1.2.4"; + }; + "node_modules/@humanwhocodes/config-array" = { + key = "@humanwhocodes/config-array/0.11.10"; + }; + "node_modules/@humanwhocodes/module-importer" = { + key = "@humanwhocodes/module-importer/1.0.1"; + }; + "node_modules/@humanwhocodes/object-schema" = { + key = "@humanwhocodes/object-schema/1.2.1"; + }; + "node_modules/@ibm-cloud/openapi-ruleset" = { + dev = true; + key = "@ibm-cloud/openapi-ruleset/0.45.5"; + }; + "node_modules/@ibm-cloud/openapi-ruleset-utilities" = { + dev = true; + key = "@ibm-cloud/openapi-ruleset-utilities/0.0.1"; + }; + "node_modules/@jridgewell/gen-mapping" = { + key = "@jridgewell/gen-mapping/0.3.3"; + }; + "node_modules/@jridgewell/resolve-uri" = { + key = "@jridgewell/resolve-uri/3.1.1"; + }; + "node_modules/@jridgewell/set-array" = { + key = "@jridgewell/set-array/1.1.2"; + }; + "node_modules/@jridgewell/sourcemap-codec" = { + key = "@jridgewell/sourcemap-codec/1.4.15"; + }; + "node_modules/@jridgewell/trace-mapping" = { + key = "@jridgewell/trace-mapping/0.3.19"; + }; + "node_modules/@jsdevtools/ono" = { + dev = true; + key = "@jsdevtools/ono/7.1.3"; + }; + "node_modules/@jsep-plugin/regex" = { + dev = true; + key = "@jsep-plugin/regex/1.0.3"; + }; + "node_modules/@jsep-plugin/ternary" = { + dev = true; + key = "@jsep-plugin/ternary/1.1.3"; + }; + "node_modules/@mui/base" = { + key = "@mui/base/5.0.0-beta.11"; + }; + "node_modules/@mui/core-downloads-tracker" = { + key = "@mui/core-downloads-tracker/5.14.5"; + }; + "node_modules/@mui/icons-material" = { + key = "@mui/icons-material/5.14.3"; + }; + "node_modules/@mui/material" = { + key = "@mui/material/5.14.5"; + }; + "node_modules/@mui/private-theming" = { + key = "@mui/private-theming/5.14.5"; + }; + "node_modules/@mui/styled-engine" = { + key = "@mui/styled-engine/5.13.2"; + }; + "node_modules/@mui/system" = { + key = "@mui/system/5.14.5"; + }; + "node_modules/@mui/types" = { + key = "@mui/types/7.2.4"; + }; + "node_modules/@mui/utils" = { + key = "@mui/utils/5.14.7"; + }; + "node_modules/@next/env" = { + key = "@next/env/13.4.12"; + }; + "node_modules/@next/eslint-plugin-next" = { + dev = true; + key = "@next/eslint-plugin-next/13.4.12"; + }; + "node_modules/@next/swc-darwin-arm64" = { + key = "@next/swc-darwin-arm64/13.4.12"; + optional = true; + }; + "node_modules/@next/swc-darwin-x64" = { + key = "@next/swc-darwin-x64/13.4.12"; + optional = true; + }; + "node_modules/@next/swc-linux-arm64-gnu" = { + key = "@next/swc-linux-arm64-gnu/13.4.12"; + optional = true; + }; + "node_modules/@next/swc-linux-arm64-musl" = { + key = "@next/swc-linux-arm64-musl/13.4.12"; + optional = true; + }; + "node_modules/@next/swc-linux-x64-gnu" = { + key = "@next/swc-linux-x64-gnu/13.4.12"; + optional = true; + }; + "node_modules/@next/swc-linux-x64-musl" = { + key = "@next/swc-linux-x64-musl/13.4.12"; + optional = true; + }; + "node_modules/@next/swc-win32-arm64-msvc" = { + key = "@next/swc-win32-arm64-msvc/13.4.12"; + optional = true; + }; + "node_modules/@next/swc-win32-ia32-msvc" = { + key = "@next/swc-win32-ia32-msvc/13.4.12"; + optional = true; + }; + "node_modules/@next/swc-win32-x64-msvc" = { + key = "@next/swc-win32-x64-msvc/13.4.12"; + optional = true; + }; + "node_modules/@nodelib/fs.scandir" = { + key = "@nodelib/fs.scandir/2.1.5"; + }; + "node_modules/@nodelib/fs.stat" = { + key = "@nodelib/fs.stat/2.0.5"; + }; + "node_modules/@nodelib/fs.walk" = { + key = "@nodelib/fs.walk/1.2.8"; + }; + "node_modules/@orval/angular" = { + dev = true; + key = "@orval/angular/6.17.0"; + }; + "node_modules/@orval/axios" = { + dev = true; + key = "@orval/axios/6.17.0"; + }; + "node_modules/@orval/core" = { + dev = true; + key = "@orval/core/6.17.0"; + }; + "node_modules/@orval/core/node_modules/ajv" = { + dev = true; + key = "ajv/8.12.0"; + }; + "node_modules/@orval/core/node_modules/json-schema-traverse" = { + dev = true; + key = "json-schema-traverse/1.0.0"; + }; + "node_modules/@orval/msw" = { + dev = true; + key = "@orval/msw/6.17.0"; + }; + "node_modules/@orval/query" = { + dev = true; + key = "@orval/query/6.17.0"; + }; + "node_modules/@orval/swr" = { + dev = true; + key = "@orval/swr/6.17.0"; + }; + "node_modules/@orval/zod" = { + dev = true; + key = "@orval/zod/6.17.0"; + }; + "node_modules/@popperjs/core" = { + key = "@popperjs/core/2.11.8"; + }; + "node_modules/@rjsf/core" = { + key = "@rjsf/core/5.12.1"; + }; + "node_modules/@rjsf/mui" = { + key = "@rjsf/mui/5.12.1"; + }; + "node_modules/@rjsf/utils" = { + key = "@rjsf/utils/5.12.1"; + }; + "node_modules/@rjsf/validator-ajv8" = { + key = "@rjsf/validator-ajv8/5.12.1"; + }; + "node_modules/@rjsf/validator-ajv8/node_modules/ajv" = { + key = "ajv/8.12.0"; + }; + "node_modules/@rjsf/validator-ajv8/node_modules/json-schema-traverse" = { + key = "json-schema-traverse/1.0.0"; + }; + "node_modules/@rollup/plugin-commonjs" = { + dev = true; + key = "@rollup/plugin-commonjs/22.0.2"; + }; + "node_modules/@rollup/pluginutils" = { + dev = true; + key = "@rollup/pluginutils/3.1.0"; + }; + "node_modules/@rollup/pluginutils/node_modules/estree-walker" = { + dev = true; + key = "estree-walker/1.0.1"; + }; + "node_modules/@rushstack/eslint-patch" = { + dev = true; + key = "@rushstack/eslint-patch/1.3.3"; + }; + "node_modules/@stoplight/json" = { + dev = true; + key = "@stoplight/json/3.21.0"; + }; + "node_modules/@stoplight/json-ref-readers" = { + dev = true; + key = "@stoplight/json-ref-readers/1.2.2"; + }; + "node_modules/@stoplight/json-ref-readers/node_modules/tslib" = { + dev = true; + key = "tslib/1.14.1"; + }; + "node_modules/@stoplight/json-ref-resolver" = { + dev = true; + key = "@stoplight/json-ref-resolver/3.1.6"; + }; + "node_modules/@stoplight/ordered-object-literal" = { + dev = true; + key = "@stoplight/ordered-object-literal/1.0.4"; + }; + "node_modules/@stoplight/path" = { + dev = true; + key = "@stoplight/path/1.3.2"; + }; + "node_modules/@stoplight/spectral-cli" = { + dev = true; + key = "@stoplight/spectral-cli/6.10.1"; + }; + "node_modules/@stoplight/spectral-cli/node_modules/fast-glob" = { + dev = true; + key = "fast-glob/3.2.12"; + }; + "node_modules/@stoplight/spectral-cli/node_modules/glob-parent" = { + dev = true; + key = "glob-parent/5.1.2"; + }; + "node_modules/@stoplight/spectral-core" = { + dev = true; + key = "@stoplight/spectral-core/1.18.3"; + }; + "node_modules/@stoplight/spectral-core/node_modules/@stoplight/better-ajv-errors" = { + dev = true; + key = "@stoplight/better-ajv-errors/1.0.3"; + }; + "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types" = { + dev = true; + key = "@stoplight/types/13.6.0"; + }; + "node_modules/@stoplight/spectral-core/node_modules/ajv" = { + dev = true; + key = "ajv/8.12.0"; + }; + "node_modules/@stoplight/spectral-core/node_modules/ajv-errors" = { + dev = true; + key = "ajv-errors/3.0.0"; + }; + "node_modules/@stoplight/spectral-core/node_modules/json-schema-traverse" = { + dev = true; + key = "json-schema-traverse/1.0.0"; + }; + "node_modules/@stoplight/spectral-formats" = { + dev = true; + key = "@stoplight/spectral-formats/1.5.0"; + }; + "node_modules/@stoplight/spectral-formatters" = { + dev = true; + key = "@stoplight/spectral-formatters/1.2.0"; + }; + "node_modules/@stoplight/spectral-functions" = { + dev = true; + key = "@stoplight/spectral-functions/1.7.2"; + }; + "node_modules/@stoplight/spectral-functions/node_modules/@stoplight/better-ajv-errors" = { + dev = true; + key = "@stoplight/better-ajv-errors/1.0.3"; + }; + "node_modules/@stoplight/spectral-functions/node_modules/ajv" = { + dev = true; + key = "ajv/8.12.0"; + }; + "node_modules/@stoplight/spectral-functions/node_modules/ajv-draft-04" = { + dev = true; + key = "ajv-draft-04/1.0.0"; + }; + "node_modules/@stoplight/spectral-functions/node_modules/ajv-errors" = { + dev = true; + key = "ajv-errors/3.0.0"; + }; + "node_modules/@stoplight/spectral-functions/node_modules/json-schema-traverse" = { + dev = true; + key = "json-schema-traverse/1.0.0"; + }; + "node_modules/@stoplight/spectral-parsers" = { + dev = true; + key = "@stoplight/spectral-parsers/1.0.3"; + }; + "node_modules/@stoplight/spectral-ref-resolver" = { + dev = true; + key = "@stoplight/spectral-ref-resolver/1.0.4"; + }; + "node_modules/@stoplight/spectral-ruleset-bundler" = { + dev = true; + key = "@stoplight/spectral-ruleset-bundler/1.5.2"; + }; + "node_modules/@stoplight/spectral-ruleset-migrator" = { + dev = true; + key = "@stoplight/spectral-ruleset-migrator/1.9.5"; + }; + "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/ajv" = { + dev = true; + key = "ajv/8.12.0"; + }; + "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/json-schema-traverse" = { + dev = true; + key = "json-schema-traverse/1.0.0"; + }; + "node_modules/@stoplight/spectral-rulesets" = { + dev = true; + key = "@stoplight/spectral-rulesets/1.16.0"; + }; + "node_modules/@stoplight/spectral-rulesets/node_modules/@stoplight/better-ajv-errors" = { + dev = true; + key = "@stoplight/better-ajv-errors/1.0.3"; + }; + "node_modules/@stoplight/spectral-rulesets/node_modules/ajv" = { + dev = true; + key = "ajv/8.12.0"; + }; + "node_modules/@stoplight/spectral-rulesets/node_modules/json-schema-traverse" = { + dev = true; + key = "json-schema-traverse/1.0.0"; + }; + "node_modules/@stoplight/spectral-runtime" = { + dev = true; + key = "@stoplight/spectral-runtime/1.1.2"; + }; + "node_modules/@stoplight/spectral-runtime/node_modules/@stoplight/types" = { + dev = true; + key = "@stoplight/types/12.5.0"; + }; + "node_modules/@stoplight/types" = { + dev = true; + key = "@stoplight/types/13.19.0"; + }; + "node_modules/@stoplight/yaml" = { + dev = true; + key = "@stoplight/yaml/4.2.3"; + }; + "node_modules/@stoplight/yaml-ast-parser" = { + dev = true; + key = "@stoplight/yaml-ast-parser/0.0.48"; + }; + "node_modules/@swc/helpers" = { + key = "@swc/helpers/0.5.1"; + }; + "node_modules/@types/d3-array" = { + key = "@types/d3-array/3.0.5"; + }; + "node_modules/@types/d3-color" = { + key = "@types/d3-color/3.1.0"; + }; + "node_modules/@types/d3-ease" = { + key = "@types/d3-ease/3.0.0"; + }; + "node_modules/@types/d3-interpolate" = { + key = "@types/d3-interpolate/3.0.1"; + }; + "node_modules/@types/d3-path" = { + key = "@types/d3-path/3.0.0"; + }; + "node_modules/@types/d3-scale" = { + key = "@types/d3-scale/4.0.3"; + }; + "node_modules/@types/d3-shape" = { + key = "@types/d3-shape/3.1.1"; + }; + "node_modules/@types/d3-time" = { + key = "@types/d3-time/3.0.0"; + }; + "node_modules/@types/d3-timer" = { + key = "@types/d3-timer/3.0.0"; + }; + "node_modules/@types/es-aggregate-error" = { + dev = true; + key = "@types/es-aggregate-error/1.0.2"; + }; + "node_modules/@types/estree" = { + dev = true; + key = "@types/estree/0.0.39"; + }; + "node_modules/@types/json-schema" = { + key = "@types/json-schema/7.0.12"; + }; + "node_modules/@types/json5" = { + dev = true; + key = "@types/json5/0.0.29"; + }; + "node_modules/@types/node" = { + dev = true; + key = "@types/node/20.4.7"; + }; + "node_modules/@types/parse-json" = { + key = "@types/parse-json/4.0.0"; + }; + "node_modules/@types/prop-types" = { + key = "@types/prop-types/15.7.5"; + }; + "node_modules/@types/react" = { + key = "@types/react/18.2.18"; + }; + "node_modules/@types/react-dom" = { + dev = true; + key = "@types/react-dom/18.2.7"; + }; + "node_modules/@types/react-is" = { + key = "@types/react-is/18.2.1"; + }; + "node_modules/@types/react-transition-group" = { + key = "@types/react-transition-group/4.4.6"; + }; + "node_modules/@types/scheduler" = { + key = "@types/scheduler/0.16.3"; + }; + "node_modules/@types/semver" = { + key = "@types/semver/7.5.4"; + }; + "node_modules/@types/urijs" = { + dev = true; + key = "@types/urijs/1.19.19"; + }; + "node_modules/@types/w3c-web-usb" = { + dev = true; + key = "@types/w3c-web-usb/1.0.6"; + }; + "node_modules/@typescript-eslint/eslint-plugin" = { + key = "@typescript-eslint/eslint-plugin/5.62.0"; + }; + "node_modules/@typescript-eslint/parser" = { + key = "@typescript-eslint/parser/5.62.0"; + }; + "node_modules/@typescript-eslint/scope-manager" = { + key = "@typescript-eslint/scope-manager/5.62.0"; + }; + "node_modules/@typescript-eslint/type-utils" = { + key = "@typescript-eslint/type-utils/5.62.0"; + }; + "node_modules/@typescript-eslint/types" = { + key = "@typescript-eslint/types/5.62.0"; + }; + "node_modules/@typescript-eslint/typescript-estree" = { + key = "@typescript-eslint/typescript-estree/5.62.0"; + }; + "node_modules/@typescript-eslint/utils" = { + key = "@typescript-eslint/utils/5.62.0"; + }; + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope" = { + key = "eslint-scope/5.1.1"; + }; + "node_modules/@typescript-eslint/utils/node_modules/estraverse" = { + key = "estraverse/4.3.0"; + }; + "node_modules/@typescript-eslint/visitor-keys" = { + key = "@typescript-eslint/visitor-keys/5.62.0"; + }; + "node_modules/abort-controller" = { + dev = true; + key = "abort-controller/3.0.0"; + }; + "node_modules/acorn" = { + key = "acorn/8.10.0"; + }; + "node_modules/acorn-jsx" = { + key = "acorn-jsx/5.3.2"; + }; + "node_modules/ajv" = { + key = "ajv/6.12.6"; + }; + "node_modules/ajv-formats" = { + key = "ajv-formats/2.1.1"; + }; + "node_modules/ajv-formats/node_modules/ajv" = { + key = "ajv/8.12.0"; + }; + "node_modules/ajv-formats/node_modules/json-schema-traverse" = { + key = "json-schema-traverse/1.0.0"; + }; + "node_modules/ansi-colors" = { + dev = true; + key = "ansi-colors/4.1.3"; + }; + "node_modules/ansi-regex" = { + key = "ansi-regex/5.0.1"; + }; + "node_modules/ansi-styles" = { + key = "ansi-styles/4.3.0"; + }; + "node_modules/any-promise" = { + key = "any-promise/1.3.0"; + }; + "node_modules/anymatch" = { + key = "anymatch/3.1.3"; + }; + "node_modules/arg" = { + key = "arg/5.0.2"; + }; + "node_modules/argparse" = { + key = "argparse/2.0.1"; + }; + "node_modules/aria-query" = { + dev = true; + key = "aria-query/5.3.0"; + }; + "node_modules/array-buffer-byte-length" = { + dev = true; + key = "array-buffer-byte-length/1.0.0"; + }; + "node_modules/array-includes" = { + dev = true; + key = "array-includes/3.1.6"; + }; + "node_modules/array-union" = { + key = "array-union/2.1.0"; + }; + "node_modules/array.prototype.findlastindex" = { + dev = true; + key = "array.prototype.findlastindex/1.2.2"; + }; + "node_modules/array.prototype.flat" = { + dev = true; + key = "array.prototype.flat/1.3.1"; + }; + "node_modules/array.prototype.flatmap" = { + dev = true; + key = "array.prototype.flatmap/1.3.1"; + }; + "node_modules/array.prototype.tosorted" = { + dev = true; + key = "array.prototype.tosorted/1.1.1"; + }; + "node_modules/arraybuffer.prototype.slice" = { + dev = true; + key = "arraybuffer.prototype.slice/1.0.1"; + }; + "node_modules/as-table" = { + dev = true; + key = "as-table/1.0.55"; + }; + "node_modules/ast-types" = { + dev = true; + key = "ast-types/0.14.2"; + }; + "node_modules/ast-types-flow" = { + dev = true; + key = "ast-types-flow/0.0.7"; + }; + "node_modules/astring" = { + dev = true; + key = "astring/1.8.6"; + }; + "node_modules/asynckit" = { + key = "asynckit/0.4.0"; + }; + "node_modules/autoprefixer" = { + key = "autoprefixer/10.4.14"; + }; + "node_modules/available-typed-arrays" = { + dev = true; + key = "available-typed-arrays/1.0.5"; + }; + "node_modules/axe-core" = { + dev = true; + key = "axe-core/4.7.2"; + }; + "node_modules/axios" = { + key = "axios/1.4.0"; + }; + "node_modules/axobject-query" = { + dev = true; + key = "axobject-query/3.2.1"; + }; + "node_modules/babel-plugin-macros" = { + key = "babel-plugin-macros/3.1.0"; + }; + "node_modules/backslash" = { + dev = true; + key = "backslash/0.2.0"; + }; + "node_modules/balanced-match" = { + key = "balanced-match/1.0.2"; + }; + "node_modules/binary-extensions" = { + key = "binary-extensions/2.2.0"; + }; + "node_modules/brace-expansion" = { + key = "brace-expansion/1.1.11"; + }; + "node_modules/braces" = { + key = "braces/3.0.2"; + }; + "node_modules/browserslist" = { + key = "browserslist/4.21.10"; + }; + "node_modules/builtins" = { + dev = true; + key = "builtins/1.0.3"; + }; + "node_modules/busboy" = { + key = "busboy/1.6.0"; + }; + "node_modules/cac" = { + dev = true; + key = "cac/6.7.14"; + }; + "node_modules/call-bind" = { + dev = true; + key = "call-bind/1.0.2"; + }; + "node_modules/call-me-maybe" = { + dev = true; + key = "call-me-maybe/1.0.2"; + }; + "node_modules/callsites" = { + key = "callsites/3.1.0"; + }; + "node_modules/camelcase-css" = { + key = "camelcase-css/2.0.1"; + }; + "node_modules/caniuse-lite" = { + key = "caniuse-lite/1.0.30001520"; + }; + "node_modules/chalk" = { + key = "chalk/4.1.2"; + }; + "node_modules/chokidar" = { + key = "chokidar/3.5.3"; + }; + "node_modules/chokidar/node_modules/glob-parent" = { + key = "glob-parent/5.1.2"; + }; + "node_modules/classnames" = { + key = "classnames/2.3.2"; + }; + "node_modules/client-only" = { + key = "client-only/0.0.1"; + }; + "node_modules/cliui" = { + dev = true; + key = "cliui/7.0.4"; + }; + "node_modules/clone" = { + dev = true; + key = "clone/1.0.4"; + }; + "node_modules/clsx" = { + key = "clsx/2.0.0"; + }; + "node_modules/color-convert" = { + key = "color-convert/2.0.1"; + }; + "node_modules/color-name" = { + key = "color-name/1.1.4"; + }; + "node_modules/combined-stream" = { + key = "combined-stream/1.0.8"; + }; + "node_modules/commander" = { + key = "commander/4.1.1"; + }; + "node_modules/commondir" = { + dev = true; + key = "commondir/1.0.1"; + }; + "node_modules/compare-versions" = { + dev = true; + key = "compare-versions/4.1.4"; + }; + "node_modules/compute-gcd" = { + key = "compute-gcd/1.2.1"; + }; + "node_modules/compute-lcm" = { + key = "compute-lcm/1.1.2"; + }; + "node_modules/concat-map" = { + key = "concat-map/0.0.1"; + }; + "node_modules/convert-source-map" = { + key = "convert-source-map/1.9.0"; + }; + "node_modules/cosmiconfig" = { + key = "cosmiconfig/7.1.0"; + }; + "node_modules/cross-spawn" = { + key = "cross-spawn/7.0.3"; + }; + "node_modules/css-unit-converter" = { + key = "css-unit-converter/1.1.2"; + }; + "node_modules/cssesc" = { + key = "cssesc/3.0.0"; + }; + "node_modules/csstype" = { + key = "csstype/3.1.2"; + }; + "node_modules/cuid" = { + dev = true; + key = "cuid/2.1.8"; + }; + "node_modules/d3-array" = { + key = "d3-array/3.2.4"; + }; + "node_modules/d3-color" = { + key = "d3-color/3.1.0"; + }; + "node_modules/d3-ease" = { + key = "d3-ease/3.0.1"; + }; + "node_modules/d3-format" = { + key = "d3-format/3.1.0"; + }; + "node_modules/d3-interpolate" = { + key = "d3-interpolate/3.0.1"; + }; + "node_modules/d3-path" = { + key = "d3-path/3.1.0"; + }; + "node_modules/d3-scale" = { + key = "d3-scale/4.0.2"; + }; + "node_modules/d3-shape" = { + key = "d3-shape/3.2.0"; + }; + "node_modules/d3-time" = { + key = "d3-time/3.1.0"; + }; + "node_modules/d3-time-format" = { + key = "d3-time-format/4.1.0"; + }; + "node_modules/d3-timer" = { + key = "d3-timer/3.0.1"; + }; + "node_modules/damerau-levenshtein" = { + dev = true; + key = "damerau-levenshtein/1.0.8"; + }; + "node_modules/data-uri-to-buffer" = { + dev = true; + key = "data-uri-to-buffer/2.0.2"; + }; + "node_modules/debug" = { + key = "debug/4.3.4"; + }; + "node_modules/decimal.js-light" = { + key = "decimal.js-light/2.5.1"; + }; + "node_modules/deep-is" = { + key = "deep-is/0.1.4"; + }; + "node_modules/deepmerge" = { + dev = true; + key = "deepmerge/2.2.1"; + }; + "node_modules/defaults" = { + dev = true; + key = "defaults/1.0.4"; + }; + "node_modules/define-properties" = { + dev = true; + key = "define-properties/1.2.0"; + }; + "node_modules/delayed-stream" = { + key = "delayed-stream/1.0.0"; + }; + "node_modules/dependency-graph" = { + dev = true; + key = "dependency-graph/0.11.0"; + }; + "node_modules/dequal" = { + dev = true; + key = "dequal/2.0.3"; + }; + "node_modules/didyoumean" = { + key = "didyoumean/1.2.2"; + }; + "node_modules/dir-glob" = { + key = "dir-glob/3.0.1"; + }; + "node_modules/dlv" = { + key = "dlv/1.1.3"; + }; + "node_modules/doctrine" = { + key = "doctrine/3.0.0"; + }; + "node_modules/dom-helpers" = { + key = "dom-helpers/5.2.1"; + }; + "node_modules/electron-to-chromium" = { + key = "electron-to-chromium/1.4.491"; + }; + "node_modules/emoji-regex" = { + dev = true; + key = "emoji-regex/9.2.2"; + }; + "node_modules/enhanced-resolve" = { + dev = true; + key = "enhanced-resolve/5.15.0"; + }; + "node_modules/enquirer" = { + dev = true; + key = "enquirer/2.4.1"; + }; + "node_modules/error-ex" = { + key = "error-ex/1.3.2"; + }; + "node_modules/es-abstract" = { + dev = true; + key = "es-abstract/1.22.1"; + }; + "node_modules/es-aggregate-error" = { + dev = true; + key = "es-aggregate-error/1.0.9"; + }; + "node_modules/es-set-tostringtag" = { + dev = true; + key = "es-set-tostringtag/2.0.1"; + }; + "node_modules/es-shim-unscopables" = { + dev = true; + key = "es-shim-unscopables/1.0.0"; + }; + "node_modules/es-to-primitive" = { + dev = true; + key = "es-to-primitive/1.2.1"; + }; + "node_modules/es6-promise" = { + dev = true; + key = "es6-promise/3.3.1"; + }; + "node_modules/esbuild" = { + dev = true; + key = "esbuild/0.15.18"; + }; + "node_modules/esbuild-android-64" = { + dev = true; + key = "esbuild-android-64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-android-arm64" = { + dev = true; + key = "esbuild-android-arm64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-darwin-64" = { + dev = true; + key = "esbuild-darwin-64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-darwin-arm64" = { + dev = true; + key = "esbuild-darwin-arm64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-freebsd-64" = { + dev = true; + key = "esbuild-freebsd-64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-freebsd-arm64" = { + dev = true; + key = "esbuild-freebsd-arm64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-linux-32" = { + dev = true; + key = "esbuild-linux-32/0.15.18"; + optional = true; + }; + "node_modules/esbuild-linux-64" = { + dev = true; + key = "esbuild-linux-64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-linux-arm" = { + dev = true; + key = "esbuild-linux-arm/0.15.18"; + optional = true; + }; + "node_modules/esbuild-linux-arm64" = { + dev = true; + key = "esbuild-linux-arm64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-linux-mips64le" = { + dev = true; + key = "esbuild-linux-mips64le/0.15.18"; + optional = true; + }; + "node_modules/esbuild-linux-ppc64le" = { + dev = true; + key = "esbuild-linux-ppc64le/0.15.18"; + optional = true; + }; + "node_modules/esbuild-linux-riscv64" = { + dev = true; + key = "esbuild-linux-riscv64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-linux-s390x" = { + dev = true; + key = "esbuild-linux-s390x/0.15.18"; + optional = true; + }; + "node_modules/esbuild-netbsd-64" = { + dev = true; + key = "esbuild-netbsd-64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-openbsd-64" = { + dev = true; + key = "esbuild-openbsd-64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-sunos-64" = { + dev = true; + key = "esbuild-sunos-64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-windows-32" = { + dev = true; + key = "esbuild-windows-32/0.15.18"; + optional = true; + }; + "node_modules/esbuild-windows-64" = { + dev = true; + key = "esbuild-windows-64/0.15.18"; + optional = true; + }; + "node_modules/esbuild-windows-arm64" = { + dev = true; + key = "esbuild-windows-arm64/0.15.18"; + optional = true; + }; + "node_modules/escalade" = { + key = "escalade/3.1.1"; + }; + "node_modules/escape-string-regexp" = { + key = "escape-string-regexp/4.0.0"; + }; + "node_modules/eslint" = { + key = "eslint/8.46.0"; + }; + "node_modules/eslint-config-next" = { + dev = true; + key = "eslint-config-next/13.4.12"; + }; + "node_modules/eslint-import-resolver-node" = { + dev = true; + key = "eslint-import-resolver-node/0.3.9"; + }; + "node_modules/eslint-import-resolver-node/node_modules/debug" = { + dev = true; + key = "debug/3.2.7"; + }; + "node_modules/eslint-import-resolver-typescript" = { + dev = true; + key = "eslint-import-resolver-typescript/3.6.0"; + }; + "node_modules/eslint-module-utils" = { + dev = true; + key = "eslint-module-utils/2.8.0"; + }; + "node_modules/eslint-module-utils/node_modules/debug" = { + dev = true; + key = "debug/3.2.7"; + }; + "node_modules/eslint-plugin-import" = { + dev = true; + key = "eslint-plugin-import/2.28.0"; + }; + "node_modules/eslint-plugin-import/node_modules/debug" = { + dev = true; + key = "debug/3.2.7"; + }; + "node_modules/eslint-plugin-import/node_modules/doctrine" = { + dev = true; + key = "doctrine/2.1.0"; + }; + "node_modules/eslint-plugin-import/node_modules/semver" = { + dev = true; + key = "semver/6.3.1"; + }; + "node_modules/eslint-plugin-jsx-a11y" = { + dev = true; + key = "eslint-plugin-jsx-a11y/6.7.1"; + }; + "node_modules/eslint-plugin-jsx-a11y/node_modules/semver" = { + dev = true; + key = "semver/6.3.1"; + }; + "node_modules/eslint-plugin-react" = { + dev = true; + key = "eslint-plugin-react/7.33.1"; + }; + "node_modules/eslint-plugin-react-hooks" = { + dev = true; + key = "eslint-plugin-react-hooks/5.0.0-canary-7118f5dd7-20230705"; + }; + "node_modules/eslint-plugin-react/node_modules/doctrine" = { + dev = true; + key = "doctrine/2.1.0"; + }; + "node_modules/eslint-plugin-react/node_modules/resolve" = { + dev = true; + key = "resolve/2.0.0-next.4"; + }; + "node_modules/eslint-plugin-react/node_modules/semver" = { + dev = true; + key = "semver/6.3.1"; + }; + "node_modules/eslint-plugin-tailwindcss" = { + dev = true; + key = "eslint-plugin-tailwindcss/3.13.0"; + }; + "node_modules/eslint-scope" = { + key = "eslint-scope/7.2.2"; + }; + "node_modules/eslint-visitor-keys" = { + key = "eslint-visitor-keys/3.4.3"; + }; + "node_modules/espree" = { + key = "espree/9.6.1"; + }; + "node_modules/esprima" = { + dev = true; + key = "esprima/4.0.1"; + }; + "node_modules/esquery" = { + key = "esquery/1.5.0"; + }; + "node_modules/esrecurse" = { + key = "esrecurse/4.3.0"; + }; + "node_modules/estraverse" = { + key = "estraverse/5.3.0"; + }; + "node_modules/estree-walker" = { + dev = true; + key = "estree-walker/2.0.2"; + }; + "node_modules/esutils" = { + key = "esutils/2.0.3"; + }; + "node_modules/event-target-shim" = { + dev = true; + key = "event-target-shim/5.0.1"; + }; + "node_modules/eventemitter3" = { + key = "eventemitter3/4.0.7"; + }; + "node_modules/execa" = { + dev = true; + key = "execa/5.1.1"; + }; + "node_modules/fast-deep-equal" = { + key = "fast-deep-equal/3.1.3"; + }; + "node_modules/fast-equals" = { + key = "fast-equals/5.0.1"; + }; + "node_modules/fast-glob" = { + key = "fast-glob/3.3.1"; + }; + "node_modules/fast-glob/node_modules/glob-parent" = { + key = "glob-parent/5.1.2"; + }; + "node_modules/fast-json-stable-stringify" = { + key = "fast-json-stable-stringify/2.1.0"; + }; + "node_modules/fast-levenshtein" = { + key = "fast-levenshtein/2.0.6"; + }; + "node_modules/fast-memoize" = { + dev = true; + key = "fast-memoize/2.5.2"; + }; + "node_modules/fast-safe-stringify" = { + dev = true; + key = "fast-safe-stringify/2.1.1"; + }; + "node_modules/fastq" = { + key = "fastq/1.15.0"; + }; + "node_modules/file-entry-cache" = { + key = "file-entry-cache/6.0.1"; + }; + "node_modules/fill-range" = { + key = "fill-range/7.0.1"; + }; + "node_modules/find-root" = { + key = "find-root/1.1.0"; + }; + "node_modules/find-up" = { + key = "find-up/5.0.0"; + }; + "node_modules/flat-cache" = { + key = "flat-cache/3.0.4"; + }; + "node_modules/flatted" = { + key = "flatted/3.2.7"; + }; + "node_modules/follow-redirects" = { + key = "follow-redirects/1.15.2"; + }; + "node_modules/for-each" = { + dev = true; + key = "for-each/0.3.3"; + }; + "node_modules/form-data" = { + key = "form-data/4.0.0"; + }; + "node_modules/format-util" = { + dev = true; + key = "format-util/1.0.5"; + }; + "node_modules/fraction.js" = { + key = "fraction.js/4.2.0"; + }; + "node_modules/fs-extra" = { + dev = true; + key = "fs-extra/10.1.0"; + }; + "node_modules/fs.realpath" = { + key = "fs.realpath/1.0.0"; + }; + "node_modules/fsevents" = { + key = "fsevents/2.3.2"; + optional = true; + }; + "node_modules/function-bind" = { + key = "function-bind/1.1.1"; + }; + "node_modules/function.prototype.name" = { + dev = true; + key = "function.prototype.name/1.1.5"; + }; + "node_modules/functions-have-names" = { + dev = true; + key = "functions-have-names/1.2.3"; + }; + "node_modules/get-caller-file" = { + dev = true; + key = "get-caller-file/2.0.5"; + }; + "node_modules/get-intrinsic" = { + dev = true; + key = "get-intrinsic/1.2.1"; + }; + "node_modules/get-source" = { + dev = true; + key = "get-source/2.0.12"; + }; + "node_modules/get-source/node_modules/source-map" = { + dev = true; + key = "source-map/0.6.1"; + }; + "node_modules/get-stream" = { + dev = true; + key = "get-stream/6.0.1"; + }; + "node_modules/get-symbol-description" = { + dev = true; + key = "get-symbol-description/1.0.0"; + }; + "node_modules/get-tsconfig" = { + dev = true; + key = "get-tsconfig/4.7.0"; + }; + "node_modules/glob" = { + key = "glob/7.1.7"; + }; + "node_modules/glob-parent" = { + key = "glob-parent/6.0.2"; + }; + "node_modules/glob-to-regexp" = { + key = "glob-to-regexp/0.4.1"; + }; + "node_modules/globals" = { + key = "globals/13.21.0"; + }; + "node_modules/globalthis" = { + dev = true; + key = "globalthis/1.0.3"; + }; + "node_modules/globby" = { + key = "globby/11.1.0"; + }; + "node_modules/goober" = { + key = "goober/2.1.13"; + }; + "node_modules/gopd" = { + dev = true; + key = "gopd/1.0.1"; + }; + "node_modules/graceful-fs" = { + key = "graceful-fs/4.2.11"; + }; + "node_modules/graphemer" = { + key = "graphemer/1.4.0"; + }; + "node_modules/has" = { + key = "has/1.0.3"; + }; + "node_modules/has-bigints" = { + dev = true; + key = "has-bigints/1.0.2"; + }; + "node_modules/has-flag" = { + key = "has-flag/4.0.0"; + }; + "node_modules/has-property-descriptors" = { + dev = true; + key = "has-property-descriptors/1.0.0"; + }; + "node_modules/has-proto" = { + dev = true; + key = "has-proto/1.0.1"; + }; + "node_modules/has-symbols" = { + dev = true; + key = "has-symbols/1.0.3"; + }; + "node_modules/has-tostringtag" = { + dev = true; + key = "has-tostringtag/1.0.0"; + }; + "node_modules/hex-rgb" = { + key = "hex-rgb/5.0.0"; + }; + "node_modules/hoist-non-react-statics" = { + key = "hoist-non-react-statics/3.3.2"; + }; + "node_modules/hoist-non-react-statics/node_modules/react-is" = { + key = "react-is/16.13.1"; + }; + "node_modules/hpagent" = { + dev = true; + key = "hpagent/1.2.0"; + }; + "node_modules/http2-client" = { + dev = true; + key = "http2-client/1.3.5"; + }; + "node_modules/human-signals" = { + dev = true; + key = "human-signals/2.1.0"; + }; + "node_modules/ibm-openapi-validator" = { + dev = true; + key = "ibm-openapi-validator/0.97.5"; + }; + "node_modules/ibm-openapi-validator/node_modules/argparse" = { + dev = true; + key = "argparse/1.0.10"; + }; + "node_modules/ibm-openapi-validator/node_modules/commander" = { + dev = true; + key = "commander/2.20.3"; + }; + "node_modules/ibm-openapi-validator/node_modules/find-up" = { + dev = true; + key = "find-up/3.0.0"; + }; + "node_modules/ibm-openapi-validator/node_modules/js-yaml" = { + dev = true; + key = "js-yaml/3.14.1"; + }; + "node_modules/ibm-openapi-validator/node_modules/locate-path" = { + dev = true; + key = "locate-path/3.0.0"; + }; + "node_modules/ibm-openapi-validator/node_modules/p-limit" = { + dev = true; + key = "p-limit/2.3.0"; + }; + "node_modules/ibm-openapi-validator/node_modules/p-locate" = { + dev = true; + key = "p-locate/3.0.0"; + }; + "node_modules/ibm-openapi-validator/node_modules/path-exists" = { + dev = true; + key = "path-exists/3.0.0"; + }; + "node_modules/ibm-openapi-validator/node_modules/semver" = { + dev = true; + key = "semver/5.7.2"; + }; + "node_modules/ignore" = { + key = "ignore/5.2.4"; + }; + "node_modules/immer" = { + dev = true; + key = "immer/9.0.21"; + }; + "node_modules/import-fresh" = { + key = "import-fresh/3.3.0"; + }; + "node_modules/imurmurhash" = { + key = "imurmurhash/0.1.4"; + }; + "node_modules/inflight" = { + key = "inflight/1.0.6"; + }; + "node_modules/inherits" = { + key = "inherits/2.0.4"; + }; + "node_modules/internal-slot" = { + dev = true; + key = "internal-slot/1.0.5"; + }; + "node_modules/internmap" = { + key = "internmap/2.0.3"; + }; + "node_modules/is-array-buffer" = { + dev = true; + key = "is-array-buffer/3.0.2"; + }; + "node_modules/is-arrayish" = { + key = "is-arrayish/0.2.1"; + }; + "node_modules/is-bigint" = { + dev = true; + key = "is-bigint/1.0.4"; + }; + "node_modules/is-binary-path" = { + key = "is-binary-path/2.1.0"; + }; + "node_modules/is-boolean-object" = { + dev = true; + key = "is-boolean-object/1.1.2"; + }; + "node_modules/is-callable" = { + dev = true; + key = "is-callable/1.2.7"; + }; + "node_modules/is-core-module" = { + key = "is-core-module/2.13.0"; + }; + "node_modules/is-date-object" = { + dev = true; + key = "is-date-object/1.0.5"; + }; + "node_modules/is-extglob" = { + key = "is-extglob/2.1.1"; + }; + "node_modules/is-fullwidth-code-point" = { + dev = true; + key = "is-fullwidth-code-point/3.0.0"; + }; + "node_modules/is-glob" = { + key = "is-glob/4.0.3"; + }; + "node_modules/is-negative-zero" = { + dev = true; + key = "is-negative-zero/2.0.2"; + }; + "node_modules/is-number" = { + key = "is-number/7.0.0"; + }; + "node_modules/is-number-object" = { + dev = true; + key = "is-number-object/1.0.7"; + }; + "node_modules/is-path-inside" = { + key = "is-path-inside/3.0.3"; + }; + "node_modules/is-reference" = { + dev = true; + key = "is-reference/1.2.1"; + }; + "node_modules/is-regex" = { + dev = true; + key = "is-regex/1.1.4"; + }; + "node_modules/is-shared-array-buffer" = { + dev = true; + key = "is-shared-array-buffer/1.0.2"; + }; + "node_modules/is-stream" = { + dev = true; + key = "is-stream/2.0.1"; + }; + "node_modules/is-string" = { + dev = true; + key = "is-string/1.0.7"; + }; + "node_modules/is-symbol" = { + dev = true; + key = "is-symbol/1.0.4"; + }; + "node_modules/is-typed-array" = { + dev = true; + key = "is-typed-array/1.1.12"; + }; + "node_modules/is-weakref" = { + dev = true; + key = "is-weakref/1.0.2"; + }; + "node_modules/isarray" = { + dev = true; + key = "isarray/2.0.5"; + }; + "node_modules/isexe" = { + key = "isexe/2.0.0"; + }; + "node_modules/jiti" = { + key = "jiti/1.19.1"; + }; + "node_modules/js-tokens" = { + key = "js-tokens/4.0.0"; + }; + "node_modules/js-yaml" = { + key = "js-yaml/4.1.0"; + }; + "node_modules/jsep" = { + dev = true; + key = "jsep/1.3.8"; + }; + "node_modules/json-dup-key-validator" = { + dev = true; + key = "json-dup-key-validator/1.0.3"; + }; + "node_modules/json-parse-even-better-errors" = { + key = "json-parse-even-better-errors/2.3.1"; + }; + "node_modules/json-schema-compare" = { + key = "json-schema-compare/0.2.2"; + }; + "node_modules/json-schema-merge-allof" = { + key = "json-schema-merge-allof/0.8.1"; + }; + "node_modules/json-schema-ref-parser" = { + dev = true; + key = "json-schema-ref-parser/5.1.3"; + }; + "node_modules/json-schema-ref-parser/node_modules/argparse" = { + dev = true; + key = "argparse/1.0.10"; + }; + "node_modules/json-schema-ref-parser/node_modules/debug" = { + dev = true; + key = "debug/3.2.7"; + }; + "node_modules/json-schema-ref-parser/node_modules/js-yaml" = { + dev = true; + key = "js-yaml/3.14.1"; + }; + "node_modules/json-schema-traverse" = { + key = "json-schema-traverse/0.4.1"; + }; + "node_modules/json-stable-stringify-without-jsonify" = { + key = "json-stable-stringify-without-jsonify/1.0.1"; + }; + "node_modules/json5" = { + dev = true; + key = "json5/1.0.2"; + }; + "node_modules/jsonc-parser" = { + dev = true; + key = "jsonc-parser/2.2.1"; + }; + "node_modules/jsonfile" = { + dev = true; + key = "jsonfile/6.1.0"; + }; + "node_modules/jsonpath-plus" = { + dev = true; + key = "jsonpath-plus/7.1.0"; + }; + "node_modules/jsonpointer" = { + key = "jsonpointer/5.0.1"; + }; + "node_modules/jsonschema" = { + dev = true; + key = "jsonschema/1.4.1"; + }; + "node_modules/jsx-ast-utils" = { + dev = true; + key = "jsx-ast-utils/3.3.5"; + }; + "node_modules/language-subtag-registry" = { + dev = true; + key = "language-subtag-registry/0.3.22"; + }; + "node_modules/language-tags" = { + dev = true; + key = "language-tags/1.0.5"; + }; + "node_modules/leven" = { + dev = true; + key = "leven/3.1.0"; + }; + "node_modules/levn" = { + key = "levn/0.4.1"; + }; + "node_modules/lilconfig" = { + key = "lilconfig/2.1.0"; + }; + "node_modules/lines-and-columns" = { + key = "lines-and-columns/1.2.4"; + }; + "node_modules/locate-path" = { + key = "locate-path/6.0.0"; + }; + "node_modules/lodash" = { + key = "lodash/4.17.21"; + }; + "node_modules/lodash-es" = { + key = "lodash-es/4.17.21"; + }; + "node_modules/lodash.get" = { + dev = true; + key = "lodash.get/4.4.2"; + }; + "node_modules/lodash.isempty" = { + dev = true; + key = "lodash.isempty/4.4.0"; + }; + "node_modules/lodash.merge" = { + key = "lodash.merge/4.6.2"; + }; + "node_modules/lodash.omit" = { + dev = true; + key = "lodash.omit/4.5.0"; + }; + "node_modules/lodash.omitby" = { + dev = true; + key = "lodash.omitby/4.6.0"; + }; + "node_modules/lodash.topath" = { + dev = true; + key = "lodash.topath/4.5.2"; + }; + "node_modules/lodash.uniq" = { + dev = true; + key = "lodash.uniq/4.5.0"; + }; + "node_modules/lodash.uniqby" = { + dev = true; + key = "lodash.uniqby/4.7.0"; + }; + "node_modules/lodash.uniqwith" = { + dev = true; + key = "lodash.uniqwith/4.5.0"; + }; + "node_modules/loose-envify" = { + key = "loose-envify/1.4.0"; + }; + "node_modules/lru-cache" = { + key = "lru-cache/6.0.0"; + }; + "node_modules/magic-string" = { + dev = true; + key = "magic-string/0.25.9"; + }; + "node_modules/markdown-to-jsx" = { + key = "markdown-to-jsx/7.3.2"; + }; + "node_modules/matcher" = { + dev = true; + key = "matcher/1.1.1"; + }; + "node_modules/matcher/node_modules/escape-string-regexp" = { + dev = true; + key = "escape-string-regexp/1.0.5"; + }; + "node_modules/merge-stream" = { + dev = true; + key = "merge-stream/2.0.0"; + }; + "node_modules/merge2" = { + key = "merge2/1.4.1"; + }; + "node_modules/micromatch" = { + key = "micromatch/4.0.5"; + }; + "node_modules/mime-db" = { + key = "mime-db/1.52.0"; + }; + "node_modules/mime-types" = { + key = "mime-types/2.1.35"; + }; + "node_modules/mimic-fn" = { + dev = true; + key = "mimic-fn/2.1.0"; + }; + "node_modules/minimatch" = { + key = "minimatch/3.1.2"; + }; + "node_modules/minimist" = { + dev = true; + key = "minimist/1.2.8"; + }; + "node_modules/ms" = { + key = "ms/2.1.2"; + }; + "node_modules/mz" = { + key = "mz/2.7.0"; + }; + "node_modules/nanoid" = { + key = "nanoid/3.3.6"; + }; + "node_modules/natural-compare" = { + key = "natural-compare/1.4.0"; + }; + "node_modules/natural-compare-lite" = { + key = "natural-compare-lite/1.4.0"; + }; + "node_modules/next" = { + key = "next/13.4.12"; + }; + "node_modules/next/node_modules/postcss" = { + key = "postcss/8.4.14"; + }; + "node_modules/nimma" = { + dev = true; + key = "nimma/0.2.2"; + }; + "node_modules/nimma/node_modules/jsonpath-plus" = { + dev = true; + key = "jsonpath-plus/6.0.1"; + optional = true; + }; + "node_modules/node-fetch" = { + dev = true; + key = "node-fetch/2.7.0"; + }; + "node_modules/node-fetch-h2" = { + dev = true; + key = "node-fetch-h2/2.3.0"; + }; + "node_modules/node-readfiles" = { + dev = true; + key = "node-readfiles/0.2.0"; + }; + "node_modules/node-releases" = { + key = "node-releases/2.0.13"; + }; + "node_modules/normalize-path" = { + key = "normalize-path/3.0.0"; + }; + "node_modules/normalize-range" = { + key = "normalize-range/0.1.2"; + }; + "node_modules/npm-run-path" = { + dev = true; + key = "npm-run-path/4.0.1"; + }; + "node_modules/oas-kit-common" = { + dev = true; + key = "oas-kit-common/1.0.8"; + }; + "node_modules/oas-linter" = { + dev = true; + key = "oas-linter/3.2.2"; + }; + "node_modules/oas-resolver" = { + dev = true; + key = "oas-resolver/2.5.6"; + }; + "node_modules/oas-schema-walker" = { + dev = true; + key = "oas-schema-walker/1.1.5"; + }; + "node_modules/oas-validator" = { + dev = true; + key = "oas-validator/5.0.8"; + }; + "node_modules/object-assign" = { + key = "object-assign/4.1.1"; + }; + "node_modules/object-hash" = { + key = "object-hash/3.0.0"; + }; + "node_modules/object-inspect" = { + dev = true; + key = "object-inspect/1.12.3"; + }; + "node_modules/object-keys" = { + dev = true; + key = "object-keys/1.1.1"; + }; + "node_modules/object.assign" = { + dev = true; + key = "object.assign/4.1.4"; + }; + "node_modules/object.entries" = { + dev = true; + key = "object.entries/1.1.6"; + }; + "node_modules/object.fromentries" = { + dev = true; + key = "object.fromentries/2.0.6"; + }; + "node_modules/object.groupby" = { + dev = true; + key = "object.groupby/1.0.0"; + }; + "node_modules/object.hasown" = { + dev = true; + key = "object.hasown/1.1.2"; + }; + "node_modules/object.values" = { + dev = true; + key = "object.values/1.1.6"; + }; + "node_modules/once" = { + key = "once/1.4.0"; + }; + "node_modules/onetime" = { + dev = true; + key = "onetime/5.1.2"; + }; + "node_modules/ono" = { + dev = true; + key = "ono/4.0.11"; + }; + "node_modules/openapi-types" = { + dev = true; + key = "openapi-types/12.1.3"; + }; + "node_modules/openapi3-ts" = { + dev = true; + key = "openapi3-ts/3.2.0"; + }; + "node_modules/openapi3-ts/node_modules/yaml" = { + dev = true; + key = "yaml/2.3.1"; + }; + "node_modules/optionator" = { + key = "optionator/0.9.3"; + }; + "node_modules/orval" = { + dev = true; + key = "orval/6.17.0"; + }; + "node_modules/orval/node_modules/ajv" = { + dev = true; + key = "ajv/8.12.0"; + }; + "node_modules/orval/node_modules/json-schema-traverse" = { + dev = true; + key = "json-schema-traverse/1.0.0"; + }; + "node_modules/p-limit" = { + key = "p-limit/3.1.0"; + }; + "node_modules/p-locate" = { + key = "p-locate/5.0.0"; + }; + "node_modules/p-try" = { + dev = true; + key = "p-try/2.2.0"; + }; + "node_modules/pad" = { + dev = true; + key = "pad/2.3.0"; + }; + "node_modules/parent-module" = { + key = "parent-module/1.0.1"; + }; + "node_modules/parse-json" = { + key = "parse-json/5.2.0"; + }; + "node_modules/path-exists" = { + key = "path-exists/4.0.0"; + }; + "node_modules/path-is-absolute" = { + key = "path-is-absolute/1.0.1"; + }; + "node_modules/path-key" = { + key = "path-key/3.1.1"; + }; + "node_modules/path-parse" = { + key = "path-parse/1.0.7"; + }; + "node_modules/path-type" = { + key = "path-type/4.0.0"; + }; + "node_modules/picocolors" = { + key = "picocolors/1.0.0"; + }; + "node_modules/picomatch" = { + key = "picomatch/2.3.1"; + }; + "node_modules/pify" = { + key = "pify/2.3.0"; + }; + "node_modules/pirates" = { + key = "pirates/4.0.6"; + }; + "node_modules/pony-cause" = { + dev = true; + key = "pony-cause/1.1.1"; + }; + "node_modules/postcss" = { + key = "postcss/8.4.27"; + }; + "node_modules/postcss-import" = { + key = "postcss-import/15.1.0"; + }; + "node_modules/postcss-js" = { + key = "postcss-js/4.0.1"; + }; + "node_modules/postcss-load-config" = { + key = "postcss-load-config/4.0.1"; + }; + "node_modules/postcss-load-config/node_modules/yaml" = { + key = "yaml/2.3.1"; + }; + "node_modules/postcss-nested" = { + key = "postcss-nested/6.0.1"; + }; + "node_modules/postcss-selector-parser" = { + key = "postcss-selector-parser/6.0.13"; + }; + "node_modules/postcss-value-parser" = { + key = "postcss-value-parser/4.2.0"; + }; + "node_modules/prelude-ls" = { + key = "prelude-ls/1.2.1"; + }; + "node_modules/prettier" = { + dev = true; + key = "prettier/3.0.1"; + }; + "node_modules/prettier-plugin-tailwindcss" = { + dev = true; + key = "prettier-plugin-tailwindcss/0.4.1"; + }; + "node_modules/pretty-bytes" = { + key = "pretty-bytes/6.1.1"; + }; + "node_modules/printable-characters" = { + dev = true; + key = "printable-characters/1.0.42"; + }; + "node_modules/prop-types" = { + key = "prop-types/15.8.1"; + }; + "node_modules/prop-types/node_modules/react-is" = { + key = "react-is/16.13.1"; + }; + "node_modules/proxy-from-env" = { + key = "proxy-from-env/1.1.0"; + }; + "node_modules/punycode" = { + key = "punycode/2.3.0"; + }; + "node_modules/queue-microtask" = { + key = "queue-microtask/1.2.3"; + }; + "node_modules/react" = { + key = "react/18.2.0"; + }; + "node_modules/react-dom" = { + key = "react-dom/18.2.0"; + }; + "node_modules/react-hook-form" = { + key = "react-hook-form/7.45.4"; + }; + "node_modules/react-hot-toast" = { + key = "react-hot-toast/2.4.1"; + }; + "node_modules/react-is" = { + key = "react-is/18.2.0"; + }; + "node_modules/react-lifecycles-compat" = { + key = "react-lifecycles-compat/3.0.4"; + }; + "node_modules/react-resize-detector" = { + key = "react-resize-detector/8.1.0"; + }; + "node_modules/react-smooth" = { + key = "react-smooth/2.0.3"; + }; + "node_modules/react-smooth/node_modules/dom-helpers" = { + key = "dom-helpers/3.4.0"; + }; + "node_modules/react-smooth/node_modules/react-transition-group" = { + key = "react-transition-group/2.9.0"; + }; + "node_modules/react-transition-group" = { + key = "react-transition-group/4.4.5"; + }; + "node_modules/read-cache" = { + key = "read-cache/1.0.0"; + }; + "node_modules/readdirp" = { + key = "readdirp/3.6.0"; + }; + "node_modules/recharts" = { + key = "recharts/2.7.3"; + }; + "node_modules/recharts-scale" = { + key = "recharts-scale/0.4.5"; + }; + "node_modules/recharts/node_modules/react-is" = { + key = "react-is/16.13.1"; + }; + "node_modules/reduce-css-calc" = { + key = "reduce-css-calc/2.1.8"; + }; + "node_modules/reduce-css-calc/node_modules/postcss-value-parser" = { + key = "postcss-value-parser/3.3.1"; + }; + "node_modules/reftools" = { + dev = true; + key = "reftools/1.1.9"; + }; + "node_modules/regenerator-runtime" = { + key = "regenerator-runtime/0.14.0"; + }; + "node_modules/regexp.prototype.flags" = { + dev = true; + key = "regexp.prototype.flags/1.5.0"; + }; + "node_modules/require-all" = { + dev = true; + key = "require-all/3.0.0"; + }; + "node_modules/require-directory" = { + dev = true; + key = "require-directory/2.1.1"; + }; + "node_modules/require-from-string" = { + key = "require-from-string/2.0.2"; + }; + "node_modules/reserved" = { + dev = true; + key = "reserved/0.1.2"; + }; + "node_modules/resolve" = { + key = "resolve/1.22.4"; + }; + "node_modules/resolve-from" = { + key = "resolve-from/4.0.0"; + }; + "node_modules/resolve-pkg-maps" = { + dev = true; + key = "resolve-pkg-maps/1.0.0"; + }; + "node_modules/reusify" = { + key = "reusify/1.0.4"; + }; + "node_modules/rimraf" = { + key = "rimraf/3.0.2"; + }; + "node_modules/rollup" = { + dev = true; + key = "rollup/2.79.1"; + }; + "node_modules/run-parallel" = { + key = "run-parallel/1.2.0"; + }; + "node_modules/safe-array-concat" = { + dev = true; + key = "safe-array-concat/1.0.0"; + }; + "node_modules/safe-regex-test" = { + dev = true; + key = "safe-regex-test/1.0.0"; + }; + "node_modules/safe-stable-stringify" = { + dev = true; + key = "safe-stable-stringify/1.1.1"; + }; + "node_modules/scheduler" = { + key = "scheduler/0.23.0"; + }; + "node_modules/semver" = { + key = "semver/7.5.4"; + }; + "node_modules/shebang-command" = { + key = "shebang-command/2.0.0"; + }; + "node_modules/shebang-regex" = { + key = "shebang-regex/3.0.0"; + }; + "node_modules/should" = { + dev = true; + key = "should/13.2.3"; + }; + "node_modules/should-equal" = { + dev = true; + key = "should-equal/2.0.0"; + }; + "node_modules/should-format" = { + dev = true; + key = "should-format/3.0.3"; + }; + "node_modules/should-type" = { + dev = true; + key = "should-type/1.4.0"; + }; + "node_modules/should-type-adaptors" = { + dev = true; + key = "should-type-adaptors/1.1.0"; + }; + "node_modules/should-util" = { + dev = true; + key = "should-util/1.0.1"; + }; + "node_modules/side-channel" = { + dev = true; + key = "side-channel/1.0.4"; + }; + "node_modules/signal-exit" = { + dev = true; + key = "signal-exit/3.0.7"; + }; + "node_modules/simple-eval" = { + dev = true; + key = "simple-eval/1.0.0"; + }; + "node_modules/slash" = { + key = "slash/3.0.0"; + }; + "node_modules/source-map" = { + key = "source-map/0.5.7"; + }; + "node_modules/source-map-js" = { + key = "source-map-js/1.0.2"; + }; + "node_modules/sourcemap-codec" = { + dev = true; + key = "sourcemap-codec/1.4.8"; + }; + "node_modules/sprintf-js" = { + dev = true; + key = "sprintf-js/1.0.3"; + }; + "node_modules/stacktracey" = { + dev = true; + key = "stacktracey/2.1.8"; + }; + "node_modules/streamsearch" = { + key = "streamsearch/1.1.0"; + }; + "node_modules/string-argv" = { + dev = true; + key = "string-argv/0.3.2"; + }; + "node_modules/string-width" = { + dev = true; + key = "string-width/4.2.3"; + }; + "node_modules/string-width/node_modules/emoji-regex" = { + dev = true; + key = "emoji-regex/8.0.0"; + }; + "node_modules/string.prototype.matchall" = { + dev = true; + key = "string.prototype.matchall/4.0.8"; + }; + "node_modules/string.prototype.trim" = { + dev = true; + key = "string.prototype.trim/1.2.7"; + }; + "node_modules/string.prototype.trimend" = { + dev = true; + key = "string.prototype.trimend/1.0.6"; + }; + "node_modules/string.prototype.trimstart" = { + dev = true; + key = "string.prototype.trimstart/1.0.6"; + }; + "node_modules/strip-ansi" = { + key = "strip-ansi/6.0.1"; + }; + "node_modules/strip-bom" = { + dev = true; + key = "strip-bom/3.0.0"; + }; + "node_modules/strip-final-newline" = { + dev = true; + key = "strip-final-newline/2.0.0"; + }; + "node_modules/strip-json-comments" = { + key = "strip-json-comments/3.1.1"; + }; + "node_modules/styled-jsx" = { + key = "styled-jsx/5.1.1"; + }; + "node_modules/stylis" = { + key = "stylis/4.2.0"; + }; + "node_modules/sucrase" = { + key = "sucrase/3.34.0"; + }; + "node_modules/sucrase/node_modules/glob" = { + key = "glob/7.1.6"; + }; + "node_modules/supports-color" = { + key = "supports-color/7.2.0"; + }; + "node_modules/supports-preserve-symlinks-flag" = { + key = "supports-preserve-symlinks-flag/1.0.0"; + }; + "node_modules/swagger2openapi" = { + dev = true; + key = "swagger2openapi/7.0.8"; + }; + "node_modules/swr" = { + key = "swr/2.2.1"; + }; + "node_modules/tailwindcss" = { + key = "tailwindcss/3.3.3"; + }; + "node_modules/tapable" = { + dev = true; + key = "tapable/2.2.1"; + }; + "node_modules/text-table" = { + key = "text-table/0.2.0"; + }; + "node_modules/thenify" = { + key = "thenify/3.3.1"; + }; + "node_modules/thenify-all" = { + key = "thenify-all/1.6.0"; + }; + "node_modules/to-fast-properties" = { + key = "to-fast-properties/2.0.0"; + }; + "node_modules/to-regex-range" = { + key = "to-regex-range/5.0.1"; + }; + "node_modules/tr46" = { + dev = true; + key = "tr46/0.0.3"; + }; + "node_modules/ts-interface-checker" = { + key = "ts-interface-checker/0.1.13"; + }; + "node_modules/tsconfck" = { + dev = true; + key = "tsconfck/2.1.2"; + }; + "node_modules/tsconfig-paths" = { + dev = true; + key = "tsconfig-paths/3.14.2"; + }; + "node_modules/tslib" = { + key = "tslib/2.6.1"; + }; + "node_modules/tsutils" = { + key = "tsutils/3.21.0"; + }; + "node_modules/tsutils/node_modules/tslib" = { + key = "tslib/1.14.1"; + }; + "node_modules/type-check" = { + key = "type-check/0.4.0"; + }; + "node_modules/type-fest" = { + key = "type-fest/0.20.2"; + }; + "node_modules/typed-array-buffer" = { + dev = true; + key = "typed-array-buffer/1.0.0"; + }; + "node_modules/typed-array-byte-length" = { + dev = true; + key = "typed-array-byte-length/1.0.0"; + }; + "node_modules/typed-array-byte-offset" = { + dev = true; + key = "typed-array-byte-offset/1.0.0"; + }; + "node_modules/typed-array-length" = { + dev = true; + key = "typed-array-length/1.0.4"; + }; + "node_modules/typescript" = { + key = "typescript/5.1.6"; + }; + "node_modules/unbox-primitive" = { + dev = true; + key = "unbox-primitive/1.0.2"; + }; + "node_modules/universalify" = { + dev = true; + key = "universalify/2.0.0"; + }; + "node_modules/update-browserslist-db" = { + key = "update-browserslist-db/1.0.11"; + }; + "node_modules/uri-js" = { + key = "uri-js/4.4.1"; + }; + "node_modules/urijs" = { + dev = true; + key = "urijs/1.19.11"; + }; + "node_modules/use-sync-external-store" = { + key = "use-sync-external-store/1.2.0"; + }; + "node_modules/util-deprecate" = { + key = "util-deprecate/1.0.2"; + }; + "node_modules/utility-types" = { + dev = true; + key = "utility-types/3.10.0"; + }; + "node_modules/validate-npm-package-name" = { + dev = true; + key = "validate-npm-package-name/3.0.0"; + }; + "node_modules/validate.io-array" = { + key = "validate.io-array/1.0.6"; + }; + "node_modules/validate.io-function" = { + key = "validate.io-function/1.0.2"; + }; + "node_modules/validate.io-integer" = { + key = "validate.io-integer/1.0.5"; + }; + "node_modules/validate.io-integer-array" = { + key = "validate.io-integer-array/1.0.0"; + }; + "node_modules/validate.io-number" = { + key = "validate.io-number/1.0.3"; + }; + "node_modules/validator" = { + dev = true; + key = "validator/13.11.0"; + }; + "node_modules/victory-vendor" = { + key = "victory-vendor/36.6.11"; + }; + "node_modules/watchpack" = { + key = "watchpack/2.4.0"; + }; + "node_modules/wcwidth" = { + dev = true; + key = "wcwidth/1.0.1"; + }; + "node_modules/webidl-conversions" = { + dev = true; + key = "webidl-conversions/3.0.1"; + }; + "node_modules/whatwg-url" = { + dev = true; + key = "whatwg-url/5.0.0"; + }; + "node_modules/which" = { + key = "which/2.0.2"; + }; + "node_modules/which-boxed-primitive" = { + dev = true; + key = "which-boxed-primitive/1.0.2"; + }; + "node_modules/which-typed-array" = { + dev = true; + key = "which-typed-array/1.1.11"; + }; + "node_modules/wrap-ansi" = { + dev = true; + key = "wrap-ansi/7.0.0"; + }; + "node_modules/wrappy" = { + key = "wrappy/1.0.2"; + }; + "node_modules/y18n" = { + dev = true; + key = "y18n/5.0.8"; + }; + "node_modules/yallist" = { + key = "yallist/4.0.0"; + }; + "node_modules/yaml" = { + key = "yaml/1.10.2"; + }; + "node_modules/yaml-js" = { + dev = true; + key = "yaml-js/0.2.3"; + }; + "node_modules/yargs" = { + dev = true; + key = "yargs/17.3.1"; + }; + "node_modules/yargs-parser" = { + dev = true; + key = "yargs-parser/21.1.1"; + }; + "node_modules/yocto-queue" = { + key = "yocto-queue/0.1.0"; + }; + "node_modules/zod" = { + key = "zod/3.21.4"; + }; + }; + version = "0.1.0"; + }; + }; classnames = { "2.3.2" = { fetchInfo = { @@ -7895,6 +10734,28 @@ }; }; eslint-scope = { + "5.1.1" = { + depInfo = { + esrecurse = { + descriptor = "^4.3.0"; + pin = "4.3.0"; + runtime = true; + }; + estraverse = { + descriptor = "^4.1.1"; + pin = "4.3.0"; + runtime = true; + }; + }; + fetchInfo = { + narHash = "sha256-CgRo1pE7/MbHG++8ScYxF7FOxqJW+C5DDER02bSG7FM="; + type = "tarball"; + url = "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz"; + }; + ident = "eslint-scope"; + ltype = "file"; + version = "5.1.1"; + }; "7.2.2" = { depInfo = { esrecurse = { @@ -8018,6 +10879,17 @@ }; }; estraverse = { + "4.3.0" = { + fetchInfo = { + narHash = "sha256-ekB0YUgzdakntluHF3FoHv9+GZr7QJEua1FF32TYBaQ="; + type = "tarball"; + url = "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"; + }; + ident = "estraverse"; + ltype = "file"; + treeInfo = { }; + version = "4.3.0"; + }; "5.3.0" = { fetchInfo = { narHash = "sha256-Vb6OEwicNHaYjRSLyES24y4OJtyPPb/7ecZpH6ZOGFg="; @@ -10869,2711 +13741,6 @@ version = "2.1.2"; }; }; - clan-ui = { - "0.1.0" = { - depInfo = { - "@emotion/react" = { - descriptor = "^11.11.1"; - pin = "11.11.1"; - runtime = true; - }; - "@emotion/styled" = { - descriptor = "^11.11.0"; - pin = "11.11.0"; - runtime = true; - }; - "@mui/icons-material" = { - descriptor = "^5.14.3"; - pin = "5.14.3"; - runtime = true; - }; - "@mui/material" = { - descriptor = "^5.14.3"; - pin = "5.14.5"; - runtime = true; - }; - "@rjsf/core" = { - descriptor = "^5.12.1"; - pin = "5.12.1"; - runtime = true; - }; - "@rjsf/mui" = { - descriptor = "^5.12.1"; - pin = "5.12.1"; - runtime = true; - }; - "@rjsf/validator-ajv8" = { - descriptor = "^5.12.1"; - pin = "5.12.1"; - runtime = true; - }; - "@types/json-schema" = { - descriptor = "^7.0.12"; - pin = "7.0.12"; - runtime = true; - }; - "@types/node" = { - descriptor = "20.4.7"; - pin = "20.4.7"; - }; - "@types/react" = { - descriptor = "18.2.18"; - pin = "18.2.18"; - }; - "@types/react-dom" = { - descriptor = "18.2.7"; - pin = "18.2.7"; - }; - "@types/w3c-web-usb" = { - descriptor = "^1.0.6"; - pin = "1.0.6"; - }; - autoprefixer = { - descriptor = "10.4.14"; - pin = "10.4.14"; - runtime = true; - }; - axios = { - descriptor = "^1.4.0"; - pin = "1.4.0"; - runtime = true; - }; - classnames = { - descriptor = "^2.3.2"; - pin = "2.3.2"; - runtime = true; - }; - esbuild = { - descriptor = "^0.15.18"; - pin = "0.15.18"; - }; - eslint = { - descriptor = "^8.46.0"; - pin = "8.46.0"; - }; - eslint-config-next = { - descriptor = "13.4.12"; - pin = "13.4.12"; - }; - eslint-plugin-tailwindcss = { - descriptor = "^3.13.0"; - pin = "3.13.0"; - }; - hex-rgb = { - descriptor = "^5.0.0"; - pin = "5.0.0"; - runtime = true; - }; - next = { - descriptor = "13.4.12"; - pin = "13.4.12"; - runtime = true; - }; - orval = { - descriptor = "^6.17.0"; - pin = "6.17.0"; - }; - postcss = { - descriptor = "8.4.27"; - pin = "8.4.27"; - runtime = true; - }; - prettier = { - descriptor = "^3.0.1"; - pin = "3.0.1"; - }; - prettier-plugin-tailwindcss = { - descriptor = "^0.4.1"; - pin = "0.4.1"; - }; - pretty-bytes = { - descriptor = "^6.1.1"; - pin = "6.1.1"; - runtime = true; - }; - react = { - descriptor = "18.2.0"; - pin = "18.2.0"; - runtime = true; - }; - react-dom = { - descriptor = "18.2.0"; - pin = "18.2.0"; - runtime = true; - }; - react-hook-form = { - descriptor = "^7.45.4"; - pin = "7.45.4"; - runtime = true; - }; - react-hot-toast = { - descriptor = "^2.4.1"; - pin = "2.4.1"; - runtime = true; - }; - recharts = { - descriptor = "^2.7.3"; - pin = "2.7.3"; - runtime = true; - }; - swr = { - descriptor = "^2.2.1"; - pin = "2.2.1"; - runtime = true; - }; - tailwindcss = { - descriptor = "3.3.3"; - pin = "3.3.3"; - runtime = true; - }; - typescript = { - descriptor = "5.1.6"; - pin = "5.1.6"; - }; - }; - fetchInfo = "path:.."; - ident = "clan-ui"; - lifecycle = { - build = true; - }; - ltype = "dir"; - treeInfo = { - "node_modules/@aashutoshrathi/word-wrap" = { - dev = true; - key = "@aashutoshrathi/word-wrap/1.2.6"; - }; - "node_modules/@alloc/quick-lru" = { - key = "@alloc/quick-lru/5.2.0"; - }; - "node_modules/@apidevtools/json-schema-ref-parser" = { - dev = true; - key = "@apidevtools/json-schema-ref-parser/9.0.6"; - }; - "node_modules/@apidevtools/json-schema-ref-parser/node_modules/argparse" = { - dev = true; - key = "argparse/1.0.10"; - }; - "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml" = { - dev = true; - key = "js-yaml/3.14.1"; - }; - "node_modules/@apidevtools/openapi-schemas" = { - dev = true; - key = "@apidevtools/openapi-schemas/2.1.0"; - }; - "node_modules/@apidevtools/swagger-methods" = { - dev = true; - key = "@apidevtools/swagger-methods/3.0.2"; - }; - "node_modules/@apidevtools/swagger-parser" = { - dev = true; - key = "@apidevtools/swagger-parser/10.1.0"; - }; - "node_modules/@apidevtools/swagger-parser/node_modules/ajv" = { - dev = true; - key = "ajv/8.12.0"; - }; - "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04" = { - dev = true; - key = "ajv-draft-04/1.0.0"; - }; - "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse" = { - dev = true; - key = "json-schema-traverse/1.0.0"; - }; - "node_modules/@asyncapi/specs" = { - dev = true; - key = "@asyncapi/specs/4.3.1"; - }; - "node_modules/@babel/code-frame" = { - key = "@babel/code-frame/7.22.10"; - }; - "node_modules/@babel/code-frame/node_modules/ansi-styles" = { - key = "ansi-styles/3.2.1"; - }; - "node_modules/@babel/code-frame/node_modules/chalk" = { - key = "chalk/2.4.2"; - }; - "node_modules/@babel/code-frame/node_modules/color-convert" = { - key = "color-convert/1.9.3"; - }; - "node_modules/@babel/code-frame/node_modules/color-name" = { - key = "color-name/1.1.3"; - }; - "node_modules/@babel/code-frame/node_modules/escape-string-regexp" = { - key = "escape-string-regexp/1.0.5"; - }; - "node_modules/@babel/code-frame/node_modules/has-flag" = { - key = "has-flag/3.0.0"; - }; - "node_modules/@babel/code-frame/node_modules/supports-color" = { - key = "supports-color/5.5.0"; - }; - "node_modules/@babel/helper-module-imports" = { - key = "@babel/helper-module-imports/7.22.5"; - }; - "node_modules/@babel/helper-string-parser" = { - key = "@babel/helper-string-parser/7.22.5"; - }; - "node_modules/@babel/helper-validator-identifier" = { - key = "@babel/helper-validator-identifier/7.22.5"; - }; - "node_modules/@babel/highlight" = { - key = "@babel/highlight/7.22.10"; - }; - "node_modules/@babel/highlight/node_modules/ansi-styles" = { - key = "ansi-styles/3.2.1"; - }; - "node_modules/@babel/highlight/node_modules/chalk" = { - key = "chalk/2.4.2"; - }; - "node_modules/@babel/highlight/node_modules/color-convert" = { - key = "color-convert/1.9.3"; - }; - "node_modules/@babel/highlight/node_modules/color-name" = { - key = "color-name/1.1.3"; - }; - "node_modules/@babel/highlight/node_modules/escape-string-regexp" = { - key = "escape-string-regexp/1.0.5"; - }; - "node_modules/@babel/highlight/node_modules/has-flag" = { - key = "has-flag/3.0.0"; - }; - "node_modules/@babel/highlight/node_modules/supports-color" = { - key = "supports-color/5.5.0"; - }; - "node_modules/@babel/runtime" = { - key = "@babel/runtime/7.22.11"; - }; - "node_modules/@babel/types" = { - key = "@babel/types/7.22.10"; - }; - "node_modules/@emotion/babel-plugin" = { - key = "@emotion/babel-plugin/11.11.0"; - }; - "node_modules/@emotion/cache" = { - key = "@emotion/cache/11.11.0"; - }; - "node_modules/@emotion/hash" = { - key = "@emotion/hash/0.9.1"; - }; - "node_modules/@emotion/is-prop-valid" = { - key = "@emotion/is-prop-valid/1.2.1"; - }; - "node_modules/@emotion/memoize" = { - key = "@emotion/memoize/0.8.1"; - }; - "node_modules/@emotion/react" = { - key = "@emotion/react/11.11.1"; - }; - "node_modules/@emotion/serialize" = { - key = "@emotion/serialize/1.1.2"; - }; - "node_modules/@emotion/sheet" = { - key = "@emotion/sheet/1.2.2"; - }; - "node_modules/@emotion/styled" = { - key = "@emotion/styled/11.11.0"; - }; - "node_modules/@emotion/unitless" = { - key = "@emotion/unitless/0.8.1"; - }; - "node_modules/@emotion/use-insertion-effect-with-fallbacks" = { - key = "@emotion/use-insertion-effect-with-fallbacks/1.0.1"; - }; - "node_modules/@emotion/utils" = { - key = "@emotion/utils/1.2.1"; - }; - "node_modules/@emotion/weak-memoize" = { - key = "@emotion/weak-memoize/0.3.1"; - }; - "node_modules/@esbuild/android-arm" = { - dev = true; - key = "@esbuild/android-arm/0.15.18"; - optional = true; - }; - "node_modules/@esbuild/linux-loong64" = { - dev = true; - key = "@esbuild/linux-loong64/0.15.18"; - optional = true; - }; - "node_modules/@eslint-community/eslint-utils" = { - dev = true; - key = "@eslint-community/eslint-utils/4.4.0"; - }; - "node_modules/@eslint-community/regexpp" = { - dev = true; - key = "@eslint-community/regexpp/4.6.2"; - }; - "node_modules/@eslint/eslintrc" = { - dev = true; - key = "@eslint/eslintrc/2.1.2"; - }; - "node_modules/@eslint/js" = { - dev = true; - key = "@eslint/js/8.47.0"; - }; - "node_modules/@exodus/schemasafe" = { - dev = true; - key = "@exodus/schemasafe/1.2.4"; - }; - "node_modules/@humanwhocodes/config-array" = { - dev = true; - key = "@humanwhocodes/config-array/0.11.10"; - }; - "node_modules/@humanwhocodes/module-importer" = { - dev = true; - key = "@humanwhocodes/module-importer/1.0.1"; - }; - "node_modules/@humanwhocodes/object-schema" = { - dev = true; - key = "@humanwhocodes/object-schema/1.2.1"; - }; - "node_modules/@ibm-cloud/openapi-ruleset" = { - dev = true; - key = "@ibm-cloud/openapi-ruleset/0.45.5"; - }; - "node_modules/@ibm-cloud/openapi-ruleset-utilities" = { - dev = true; - key = "@ibm-cloud/openapi-ruleset-utilities/0.0.1"; - }; - "node_modules/@jridgewell/gen-mapping" = { - key = "@jridgewell/gen-mapping/0.3.3"; - }; - "node_modules/@jridgewell/resolve-uri" = { - key = "@jridgewell/resolve-uri/3.1.1"; - }; - "node_modules/@jridgewell/set-array" = { - key = "@jridgewell/set-array/1.1.2"; - }; - "node_modules/@jridgewell/sourcemap-codec" = { - key = "@jridgewell/sourcemap-codec/1.4.15"; - }; - "node_modules/@jridgewell/trace-mapping" = { - key = "@jridgewell/trace-mapping/0.3.19"; - }; - "node_modules/@jsdevtools/ono" = { - dev = true; - key = "@jsdevtools/ono/7.1.3"; - }; - "node_modules/@jsep-plugin/regex" = { - dev = true; - key = "@jsep-plugin/regex/1.0.3"; - }; - "node_modules/@jsep-plugin/ternary" = { - dev = true; - key = "@jsep-plugin/ternary/1.1.3"; - }; - "node_modules/@mui/base" = { - key = "@mui/base/5.0.0-beta.11"; - }; - "node_modules/@mui/core-downloads-tracker" = { - key = "@mui/core-downloads-tracker/5.14.5"; - }; - "node_modules/@mui/icons-material" = { - key = "@mui/icons-material/5.14.3"; - }; - "node_modules/@mui/material" = { - key = "@mui/material/5.14.5"; - }; - "node_modules/@mui/private-theming" = { - key = "@mui/private-theming/5.14.5"; - }; - "node_modules/@mui/styled-engine" = { - key = "@mui/styled-engine/5.13.2"; - }; - "node_modules/@mui/system" = { - key = "@mui/system/5.14.5"; - }; - "node_modules/@mui/types" = { - key = "@mui/types/7.2.4"; - }; - "node_modules/@mui/utils" = { - key = "@mui/utils/5.14.7"; - }; - "node_modules/@next/env" = { - key = "@next/env/13.4.12"; - }; - "node_modules/@next/eslint-plugin-next" = { - dev = true; - key = "@next/eslint-plugin-next/13.4.12"; - }; - "node_modules/@next/swc-darwin-arm64" = { - key = "@next/swc-darwin-arm64/13.4.12"; - optional = true; - }; - "node_modules/@next/swc-darwin-x64" = { - key = "@next/swc-darwin-x64/13.4.12"; - optional = true; - }; - "node_modules/@next/swc-linux-arm64-gnu" = { - key = "@next/swc-linux-arm64-gnu/13.4.12"; - optional = true; - }; - "node_modules/@next/swc-linux-arm64-musl" = { - key = "@next/swc-linux-arm64-musl/13.4.12"; - optional = true; - }; - "node_modules/@next/swc-linux-x64-gnu" = { - key = "@next/swc-linux-x64-gnu/13.4.12"; - optional = true; - }; - "node_modules/@next/swc-linux-x64-musl" = { - key = "@next/swc-linux-x64-musl/13.4.12"; - optional = true; - }; - "node_modules/@next/swc-win32-arm64-msvc" = { - key = "@next/swc-win32-arm64-msvc/13.4.12"; - optional = true; - }; - "node_modules/@next/swc-win32-ia32-msvc" = { - key = "@next/swc-win32-ia32-msvc/13.4.12"; - optional = true; - }; - "node_modules/@next/swc-win32-x64-msvc" = { - key = "@next/swc-win32-x64-msvc/13.4.12"; - optional = true; - }; - "node_modules/@nodelib/fs.scandir" = { - key = "@nodelib/fs.scandir/2.1.5"; - }; - "node_modules/@nodelib/fs.stat" = { - key = "@nodelib/fs.stat/2.0.5"; - }; - "node_modules/@nodelib/fs.walk" = { - key = "@nodelib/fs.walk/1.2.8"; - }; - "node_modules/@orval/angular" = { - dev = true; - key = "@orval/angular/6.17.0"; - }; - "node_modules/@orval/axios" = { - dev = true; - key = "@orval/axios/6.17.0"; - }; - "node_modules/@orval/core" = { - dev = true; - key = "@orval/core/6.17.0"; - }; - "node_modules/@orval/core/node_modules/ajv" = { - dev = true; - key = "ajv/8.12.0"; - }; - "node_modules/@orval/core/node_modules/json-schema-traverse" = { - dev = true; - key = "json-schema-traverse/1.0.0"; - }; - "node_modules/@orval/msw" = { - dev = true; - key = "@orval/msw/6.17.0"; - }; - "node_modules/@orval/query" = { - dev = true; - key = "@orval/query/6.17.0"; - }; - "node_modules/@orval/swr" = { - dev = true; - key = "@orval/swr/6.17.0"; - }; - "node_modules/@orval/zod" = { - dev = true; - key = "@orval/zod/6.17.0"; - }; - "node_modules/@popperjs/core" = { - key = "@popperjs/core/2.11.8"; - }; - "node_modules/@rjsf/core" = { - key = "@rjsf/core/5.12.1"; - }; - "node_modules/@rjsf/mui" = { - key = "@rjsf/mui/5.12.1"; - }; - "node_modules/@rjsf/utils" = { - key = "@rjsf/utils/5.12.1"; - }; - "node_modules/@rjsf/validator-ajv8" = { - key = "@rjsf/validator-ajv8/5.12.1"; - }; - "node_modules/@rjsf/validator-ajv8/node_modules/ajv" = { - key = "ajv/8.12.0"; - }; - "node_modules/@rjsf/validator-ajv8/node_modules/json-schema-traverse" = { - key = "json-schema-traverse/1.0.0"; - }; - "node_modules/@rollup/plugin-commonjs" = { - dev = true; - key = "@rollup/plugin-commonjs/22.0.2"; - }; - "node_modules/@rollup/pluginutils" = { - dev = true; - key = "@rollup/pluginutils/3.1.0"; - }; - "node_modules/@rollup/pluginutils/node_modules/estree-walker" = { - dev = true; - key = "estree-walker/1.0.1"; - }; - "node_modules/@rushstack/eslint-patch" = { - dev = true; - key = "@rushstack/eslint-patch/1.3.3"; - }; - "node_modules/@stoplight/json" = { - dev = true; - key = "@stoplight/json/3.21.0"; - }; - "node_modules/@stoplight/json-ref-readers" = { - dev = true; - key = "@stoplight/json-ref-readers/1.2.2"; - }; - "node_modules/@stoplight/json-ref-readers/node_modules/tslib" = { - dev = true; - key = "tslib/1.14.1"; - }; - "node_modules/@stoplight/json-ref-resolver" = { - dev = true; - key = "@stoplight/json-ref-resolver/3.1.6"; - }; - "node_modules/@stoplight/ordered-object-literal" = { - dev = true; - key = "@stoplight/ordered-object-literal/1.0.4"; - }; - "node_modules/@stoplight/path" = { - dev = true; - key = "@stoplight/path/1.3.2"; - }; - "node_modules/@stoplight/spectral-cli" = { - dev = true; - key = "@stoplight/spectral-cli/6.10.1"; - }; - "node_modules/@stoplight/spectral-cli/node_modules/fast-glob" = { - dev = true; - key = "fast-glob/3.2.12"; - }; - "node_modules/@stoplight/spectral-cli/node_modules/glob-parent" = { - dev = true; - key = "glob-parent/5.1.2"; - }; - "node_modules/@stoplight/spectral-core" = { - dev = true; - key = "@stoplight/spectral-core/1.18.3"; - }; - "node_modules/@stoplight/spectral-core/node_modules/@stoplight/better-ajv-errors" = { - dev = true; - key = "@stoplight/better-ajv-errors/1.0.3"; - }; - "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types" = { - dev = true; - key = "@stoplight/types/13.6.0"; - }; - "node_modules/@stoplight/spectral-core/node_modules/ajv" = { - dev = true; - key = "ajv/8.12.0"; - }; - "node_modules/@stoplight/spectral-core/node_modules/ajv-errors" = { - dev = true; - key = "ajv-errors/3.0.0"; - }; - "node_modules/@stoplight/spectral-core/node_modules/json-schema-traverse" = { - dev = true; - key = "json-schema-traverse/1.0.0"; - }; - "node_modules/@stoplight/spectral-formats" = { - dev = true; - key = "@stoplight/spectral-formats/1.5.0"; - }; - "node_modules/@stoplight/spectral-formatters" = { - dev = true; - key = "@stoplight/spectral-formatters/1.2.0"; - }; - "node_modules/@stoplight/spectral-functions" = { - dev = true; - key = "@stoplight/spectral-functions/1.7.2"; - }; - "node_modules/@stoplight/spectral-functions/node_modules/@stoplight/better-ajv-errors" = { - dev = true; - key = "@stoplight/better-ajv-errors/1.0.3"; - }; - "node_modules/@stoplight/spectral-functions/node_modules/ajv" = { - dev = true; - key = "ajv/8.12.0"; - }; - "node_modules/@stoplight/spectral-functions/node_modules/ajv-draft-04" = { - dev = true; - key = "ajv-draft-04/1.0.0"; - }; - "node_modules/@stoplight/spectral-functions/node_modules/ajv-errors" = { - dev = true; - key = "ajv-errors/3.0.0"; - }; - "node_modules/@stoplight/spectral-functions/node_modules/json-schema-traverse" = { - dev = true; - key = "json-schema-traverse/1.0.0"; - }; - "node_modules/@stoplight/spectral-parsers" = { - dev = true; - key = "@stoplight/spectral-parsers/1.0.3"; - }; - "node_modules/@stoplight/spectral-ref-resolver" = { - dev = true; - key = "@stoplight/spectral-ref-resolver/1.0.4"; - }; - "node_modules/@stoplight/spectral-ruleset-bundler" = { - dev = true; - key = "@stoplight/spectral-ruleset-bundler/1.5.2"; - }; - "node_modules/@stoplight/spectral-ruleset-migrator" = { - dev = true; - key = "@stoplight/spectral-ruleset-migrator/1.9.5"; - }; - "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/ajv" = { - dev = true; - key = "ajv/8.12.0"; - }; - "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/json-schema-traverse" = { - dev = true; - key = "json-schema-traverse/1.0.0"; - }; - "node_modules/@stoplight/spectral-rulesets" = { - dev = true; - key = "@stoplight/spectral-rulesets/1.16.0"; - }; - "node_modules/@stoplight/spectral-rulesets/node_modules/@stoplight/better-ajv-errors" = { - dev = true; - key = "@stoplight/better-ajv-errors/1.0.3"; - }; - "node_modules/@stoplight/spectral-rulesets/node_modules/ajv" = { - dev = true; - key = "ajv/8.12.0"; - }; - "node_modules/@stoplight/spectral-rulesets/node_modules/json-schema-traverse" = { - dev = true; - key = "json-schema-traverse/1.0.0"; - }; - "node_modules/@stoplight/spectral-runtime" = { - dev = true; - key = "@stoplight/spectral-runtime/1.1.2"; - }; - "node_modules/@stoplight/spectral-runtime/node_modules/@stoplight/types" = { - dev = true; - key = "@stoplight/types/12.5.0"; - }; - "node_modules/@stoplight/types" = { - dev = true; - key = "@stoplight/types/13.19.0"; - }; - "node_modules/@stoplight/yaml" = { - dev = true; - key = "@stoplight/yaml/4.2.3"; - }; - "node_modules/@stoplight/yaml-ast-parser" = { - dev = true; - key = "@stoplight/yaml-ast-parser/0.0.48"; - }; - "node_modules/@swc/helpers" = { - key = "@swc/helpers/0.5.1"; - }; - "node_modules/@types/d3-array" = { - key = "@types/d3-array/3.0.5"; - }; - "node_modules/@types/d3-color" = { - key = "@types/d3-color/3.1.0"; - }; - "node_modules/@types/d3-ease" = { - key = "@types/d3-ease/3.0.0"; - }; - "node_modules/@types/d3-interpolate" = { - key = "@types/d3-interpolate/3.0.1"; - }; - "node_modules/@types/d3-path" = { - key = "@types/d3-path/3.0.0"; - }; - "node_modules/@types/d3-scale" = { - key = "@types/d3-scale/4.0.3"; - }; - "node_modules/@types/d3-shape" = { - key = "@types/d3-shape/3.1.1"; - }; - "node_modules/@types/d3-time" = { - key = "@types/d3-time/3.0.0"; - }; - "node_modules/@types/d3-timer" = { - key = "@types/d3-timer/3.0.0"; - }; - "node_modules/@types/es-aggregate-error" = { - dev = true; - key = "@types/es-aggregate-error/1.0.2"; - }; - "node_modules/@types/estree" = { - dev = true; - key = "@types/estree/0.0.39"; - }; - "node_modules/@types/json-schema" = { - key = "@types/json-schema/7.0.12"; - }; - "node_modules/@types/json5" = { - dev = true; - key = "@types/json5/0.0.29"; - }; - "node_modules/@types/node" = { - dev = true; - key = "@types/node/20.4.7"; - }; - "node_modules/@types/parse-json" = { - key = "@types/parse-json/4.0.0"; - }; - "node_modules/@types/prop-types" = { - key = "@types/prop-types/15.7.5"; - }; - "node_modules/@types/react" = { - key = "@types/react/18.2.18"; - }; - "node_modules/@types/react-dom" = { - dev = true; - key = "@types/react-dom/18.2.7"; - }; - "node_modules/@types/react-is" = { - key = "@types/react-is/18.2.1"; - }; - "node_modules/@types/react-transition-group" = { - key = "@types/react-transition-group/4.4.6"; - }; - "node_modules/@types/scheduler" = { - key = "@types/scheduler/0.16.3"; - }; - "node_modules/@types/urijs" = { - dev = true; - key = "@types/urijs/1.19.19"; - }; - "node_modules/@types/w3c-web-usb" = { - dev = true; - key = "@types/w3c-web-usb/1.0.6"; - }; - "node_modules/@typescript-eslint/parser" = { - dev = true; - key = "@typescript-eslint/parser/5.62.0"; - }; - "node_modules/@typescript-eslint/scope-manager" = { - dev = true; - key = "@typescript-eslint/scope-manager/5.62.0"; - }; - "node_modules/@typescript-eslint/types" = { - dev = true; - key = "@typescript-eslint/types/5.62.0"; - }; - "node_modules/@typescript-eslint/typescript-estree" = { - dev = true; - key = "@typescript-eslint/typescript-estree/5.62.0"; - }; - "node_modules/@typescript-eslint/visitor-keys" = { - dev = true; - key = "@typescript-eslint/visitor-keys/5.62.0"; - }; - "node_modules/abort-controller" = { - dev = true; - key = "abort-controller/3.0.0"; - }; - "node_modules/acorn" = { - dev = true; - key = "acorn/8.10.0"; - }; - "node_modules/acorn-jsx" = { - dev = true; - key = "acorn-jsx/5.3.2"; - }; - "node_modules/ajv" = { - dev = true; - key = "ajv/6.12.6"; - }; - "node_modules/ajv-formats" = { - key = "ajv-formats/2.1.1"; - }; - "node_modules/ajv-formats/node_modules/ajv" = { - key = "ajv/8.12.0"; - }; - "node_modules/ajv-formats/node_modules/json-schema-traverse" = { - key = "json-schema-traverse/1.0.0"; - }; - "node_modules/ansi-colors" = { - dev = true; - key = "ansi-colors/4.1.3"; - }; - "node_modules/ansi-regex" = { - dev = true; - key = "ansi-regex/5.0.1"; - }; - "node_modules/ansi-styles" = { - dev = true; - key = "ansi-styles/4.3.0"; - }; - "node_modules/any-promise" = { - key = "any-promise/1.3.0"; - }; - "node_modules/anymatch" = { - key = "anymatch/3.1.3"; - }; - "node_modules/arg" = { - key = "arg/5.0.2"; - }; - "node_modules/argparse" = { - dev = true; - key = "argparse/2.0.1"; - }; - "node_modules/aria-query" = { - dev = true; - key = "aria-query/5.3.0"; - }; - "node_modules/array-buffer-byte-length" = { - dev = true; - key = "array-buffer-byte-length/1.0.0"; - }; - "node_modules/array-includes" = { - dev = true; - key = "array-includes/3.1.6"; - }; - "node_modules/array-union" = { - dev = true; - key = "array-union/2.1.0"; - }; - "node_modules/array.prototype.findlastindex" = { - dev = true; - key = "array.prototype.findlastindex/1.2.2"; - }; - "node_modules/array.prototype.flat" = { - dev = true; - key = "array.prototype.flat/1.3.1"; - }; - "node_modules/array.prototype.flatmap" = { - dev = true; - key = "array.prototype.flatmap/1.3.1"; - }; - "node_modules/array.prototype.tosorted" = { - dev = true; - key = "array.prototype.tosorted/1.1.1"; - }; - "node_modules/arraybuffer.prototype.slice" = { - dev = true; - key = "arraybuffer.prototype.slice/1.0.1"; - }; - "node_modules/as-table" = { - dev = true; - key = "as-table/1.0.55"; - }; - "node_modules/ast-types" = { - dev = true; - key = "ast-types/0.14.2"; - }; - "node_modules/ast-types-flow" = { - dev = true; - key = "ast-types-flow/0.0.7"; - }; - "node_modules/astring" = { - dev = true; - key = "astring/1.8.6"; - }; - "node_modules/asynckit" = { - key = "asynckit/0.4.0"; - }; - "node_modules/autoprefixer" = { - key = "autoprefixer/10.4.14"; - }; - "node_modules/available-typed-arrays" = { - dev = true; - key = "available-typed-arrays/1.0.5"; - }; - "node_modules/axe-core" = { - dev = true; - key = "axe-core/4.7.2"; - }; - "node_modules/axios" = { - key = "axios/1.4.0"; - }; - "node_modules/axobject-query" = { - dev = true; - key = "axobject-query/3.2.1"; - }; - "node_modules/babel-plugin-macros" = { - key = "babel-plugin-macros/3.1.0"; - }; - "node_modules/backslash" = { - dev = true; - key = "backslash/0.2.0"; - }; - "node_modules/balanced-match" = { - key = "balanced-match/1.0.2"; - }; - "node_modules/binary-extensions" = { - key = "binary-extensions/2.2.0"; - }; - "node_modules/brace-expansion" = { - key = "brace-expansion/1.1.11"; - }; - "node_modules/braces" = { - key = "braces/3.0.2"; - }; - "node_modules/browserslist" = { - key = "browserslist/4.21.10"; - }; - "node_modules/builtins" = { - dev = true; - key = "builtins/1.0.3"; - }; - "node_modules/busboy" = { - key = "busboy/1.6.0"; - }; - "node_modules/cac" = { - dev = true; - key = "cac/6.7.14"; - }; - "node_modules/call-bind" = { - dev = true; - key = "call-bind/1.0.2"; - }; - "node_modules/call-me-maybe" = { - dev = true; - key = "call-me-maybe/1.0.2"; - }; - "node_modules/callsites" = { - key = "callsites/3.1.0"; - }; - "node_modules/camelcase-css" = { - key = "camelcase-css/2.0.1"; - }; - "node_modules/caniuse-lite" = { - key = "caniuse-lite/1.0.30001520"; - }; - "node_modules/chalk" = { - dev = true; - key = "chalk/4.1.2"; - }; - "node_modules/chokidar" = { - key = "chokidar/3.5.3"; - }; - "node_modules/chokidar/node_modules/glob-parent" = { - key = "glob-parent/5.1.2"; - }; - "node_modules/classnames" = { - key = "classnames/2.3.2"; - }; - "node_modules/client-only" = { - key = "client-only/0.0.1"; - }; - "node_modules/cliui" = { - dev = true; - key = "cliui/7.0.4"; - }; - "node_modules/clone" = { - dev = true; - key = "clone/1.0.4"; - }; - "node_modules/clsx" = { - key = "clsx/2.0.0"; - }; - "node_modules/color-convert" = { - dev = true; - key = "color-convert/2.0.1"; - }; - "node_modules/color-name" = { - dev = true; - key = "color-name/1.1.4"; - }; - "node_modules/combined-stream" = { - key = "combined-stream/1.0.8"; - }; - "node_modules/commander" = { - key = "commander/4.1.1"; - }; - "node_modules/commondir" = { - dev = true; - key = "commondir/1.0.1"; - }; - "node_modules/compare-versions" = { - dev = true; - key = "compare-versions/4.1.4"; - }; - "node_modules/compute-gcd" = { - key = "compute-gcd/1.2.1"; - }; - "node_modules/compute-lcm" = { - key = "compute-lcm/1.1.2"; - }; - "node_modules/concat-map" = { - key = "concat-map/0.0.1"; - }; - "node_modules/convert-source-map" = { - key = "convert-source-map/1.9.0"; - }; - "node_modules/cosmiconfig" = { - key = "cosmiconfig/7.1.0"; - }; - "node_modules/cross-spawn" = { - dev = true; - key = "cross-spawn/7.0.3"; - }; - "node_modules/css-unit-converter" = { - key = "css-unit-converter/1.1.2"; - }; - "node_modules/cssesc" = { - key = "cssesc/3.0.0"; - }; - "node_modules/csstype" = { - key = "csstype/3.1.2"; - }; - "node_modules/cuid" = { - dev = true; - key = "cuid/2.1.8"; - }; - "node_modules/d3-array" = { - key = "d3-array/3.2.4"; - }; - "node_modules/d3-color" = { - key = "d3-color/3.1.0"; - }; - "node_modules/d3-ease" = { - key = "d3-ease/3.0.1"; - }; - "node_modules/d3-format" = { - key = "d3-format/3.1.0"; - }; - "node_modules/d3-interpolate" = { - key = "d3-interpolate/3.0.1"; - }; - "node_modules/d3-path" = { - key = "d3-path/3.1.0"; - }; - "node_modules/d3-scale" = { - key = "d3-scale/4.0.2"; - }; - "node_modules/d3-shape" = { - key = "d3-shape/3.2.0"; - }; - "node_modules/d3-time" = { - key = "d3-time/3.1.0"; - }; - "node_modules/d3-time-format" = { - key = "d3-time-format/4.1.0"; - }; - "node_modules/d3-timer" = { - key = "d3-timer/3.0.1"; - }; - "node_modules/damerau-levenshtein" = { - dev = true; - key = "damerau-levenshtein/1.0.8"; - }; - "node_modules/data-uri-to-buffer" = { - dev = true; - key = "data-uri-to-buffer/2.0.2"; - }; - "node_modules/debug" = { - dev = true; - key = "debug/4.3.4"; - }; - "node_modules/decimal.js-light" = { - key = "decimal.js-light/2.5.1"; - }; - "node_modules/deep-is" = { - dev = true; - key = "deep-is/0.1.4"; - }; - "node_modules/deepmerge" = { - dev = true; - key = "deepmerge/2.2.1"; - }; - "node_modules/defaults" = { - dev = true; - key = "defaults/1.0.4"; - }; - "node_modules/define-properties" = { - dev = true; - key = "define-properties/1.2.0"; - }; - "node_modules/delayed-stream" = { - key = "delayed-stream/1.0.0"; - }; - "node_modules/dependency-graph" = { - dev = true; - key = "dependency-graph/0.11.0"; - }; - "node_modules/dequal" = { - dev = true; - key = "dequal/2.0.3"; - }; - "node_modules/didyoumean" = { - key = "didyoumean/1.2.2"; - }; - "node_modules/dir-glob" = { - dev = true; - key = "dir-glob/3.0.1"; - }; - "node_modules/dlv" = { - key = "dlv/1.1.3"; - }; - "node_modules/doctrine" = { - dev = true; - key = "doctrine/3.0.0"; - }; - "node_modules/dom-helpers" = { - key = "dom-helpers/5.2.1"; - }; - "node_modules/electron-to-chromium" = { - key = "electron-to-chromium/1.4.491"; - }; - "node_modules/emoji-regex" = { - dev = true; - key = "emoji-regex/9.2.2"; - }; - "node_modules/enhanced-resolve" = { - dev = true; - key = "enhanced-resolve/5.15.0"; - }; - "node_modules/enquirer" = { - dev = true; - key = "enquirer/2.4.1"; - }; - "node_modules/error-ex" = { - key = "error-ex/1.3.2"; - }; - "node_modules/es-abstract" = { - dev = true; - key = "es-abstract/1.22.1"; - }; - "node_modules/es-aggregate-error" = { - dev = true; - key = "es-aggregate-error/1.0.9"; - }; - "node_modules/es-set-tostringtag" = { - dev = true; - key = "es-set-tostringtag/2.0.1"; - }; - "node_modules/es-shim-unscopables" = { - dev = true; - key = "es-shim-unscopables/1.0.0"; - }; - "node_modules/es-to-primitive" = { - dev = true; - key = "es-to-primitive/1.2.1"; - }; - "node_modules/es6-promise" = { - dev = true; - key = "es6-promise/3.3.1"; - }; - "node_modules/esbuild" = { - dev = true; - key = "esbuild/0.15.18"; - }; - "node_modules/esbuild-android-64" = { - dev = true; - key = "esbuild-android-64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-android-arm64" = { - dev = true; - key = "esbuild-android-arm64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-darwin-64" = { - dev = true; - key = "esbuild-darwin-64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-darwin-arm64" = { - dev = true; - key = "esbuild-darwin-arm64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-freebsd-64" = { - dev = true; - key = "esbuild-freebsd-64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-freebsd-arm64" = { - dev = true; - key = "esbuild-freebsd-arm64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-linux-32" = { - dev = true; - key = "esbuild-linux-32/0.15.18"; - optional = true; - }; - "node_modules/esbuild-linux-64" = { - dev = true; - key = "esbuild-linux-64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-linux-arm" = { - dev = true; - key = "esbuild-linux-arm/0.15.18"; - optional = true; - }; - "node_modules/esbuild-linux-arm64" = { - dev = true; - key = "esbuild-linux-arm64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-linux-mips64le" = { - dev = true; - key = "esbuild-linux-mips64le/0.15.18"; - optional = true; - }; - "node_modules/esbuild-linux-ppc64le" = { - dev = true; - key = "esbuild-linux-ppc64le/0.15.18"; - optional = true; - }; - "node_modules/esbuild-linux-riscv64" = { - dev = true; - key = "esbuild-linux-riscv64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-linux-s390x" = { - dev = true; - key = "esbuild-linux-s390x/0.15.18"; - optional = true; - }; - "node_modules/esbuild-netbsd-64" = { - dev = true; - key = "esbuild-netbsd-64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-openbsd-64" = { - dev = true; - key = "esbuild-openbsd-64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-sunos-64" = { - dev = true; - key = "esbuild-sunos-64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-windows-32" = { - dev = true; - key = "esbuild-windows-32/0.15.18"; - optional = true; - }; - "node_modules/esbuild-windows-64" = { - dev = true; - key = "esbuild-windows-64/0.15.18"; - optional = true; - }; - "node_modules/esbuild-windows-arm64" = { - dev = true; - key = "esbuild-windows-arm64/0.15.18"; - optional = true; - }; - "node_modules/escalade" = { - key = "escalade/3.1.1"; - }; - "node_modules/escape-string-regexp" = { - key = "escape-string-regexp/4.0.0"; - }; - "node_modules/eslint" = { - dev = true; - key = "eslint/8.46.0"; - }; - "node_modules/eslint-config-next" = { - dev = true; - key = "eslint-config-next/13.4.12"; - }; - "node_modules/eslint-import-resolver-node" = { - dev = true; - key = "eslint-import-resolver-node/0.3.9"; - }; - "node_modules/eslint-import-resolver-node/node_modules/debug" = { - dev = true; - key = "debug/3.2.7"; - }; - "node_modules/eslint-import-resolver-typescript" = { - dev = true; - key = "eslint-import-resolver-typescript/3.6.0"; - }; - "node_modules/eslint-module-utils" = { - dev = true; - key = "eslint-module-utils/2.8.0"; - }; - "node_modules/eslint-module-utils/node_modules/debug" = { - dev = true; - key = "debug/3.2.7"; - }; - "node_modules/eslint-plugin-import" = { - dev = true; - key = "eslint-plugin-import/2.28.0"; - }; - "node_modules/eslint-plugin-import/node_modules/debug" = { - dev = true; - key = "debug/3.2.7"; - }; - "node_modules/eslint-plugin-import/node_modules/doctrine" = { - dev = true; - key = "doctrine/2.1.0"; - }; - "node_modules/eslint-plugin-import/node_modules/semver" = { - dev = true; - key = "semver/6.3.1"; - }; - "node_modules/eslint-plugin-jsx-a11y" = { - dev = true; - key = "eslint-plugin-jsx-a11y/6.7.1"; - }; - "node_modules/eslint-plugin-jsx-a11y/node_modules/semver" = { - dev = true; - key = "semver/6.3.1"; - }; - "node_modules/eslint-plugin-react" = { - dev = true; - key = "eslint-plugin-react/7.33.1"; - }; - "node_modules/eslint-plugin-react-hooks" = { - dev = true; - key = "eslint-plugin-react-hooks/5.0.0-canary-7118f5dd7-20230705"; - }; - "node_modules/eslint-plugin-react/node_modules/doctrine" = { - dev = true; - key = "doctrine/2.1.0"; - }; - "node_modules/eslint-plugin-react/node_modules/resolve" = { - dev = true; - key = "resolve/2.0.0-next.4"; - }; - "node_modules/eslint-plugin-react/node_modules/semver" = { - dev = true; - key = "semver/6.3.1"; - }; - "node_modules/eslint-plugin-tailwindcss" = { - dev = true; - key = "eslint-plugin-tailwindcss/3.13.0"; - }; - "node_modules/eslint-scope" = { - dev = true; - key = "eslint-scope/7.2.2"; - }; - "node_modules/eslint-visitor-keys" = { - dev = true; - key = "eslint-visitor-keys/3.4.3"; - }; - "node_modules/espree" = { - dev = true; - key = "espree/9.6.1"; - }; - "node_modules/esprima" = { - dev = true; - key = "esprima/4.0.1"; - }; - "node_modules/esquery" = { - dev = true; - key = "esquery/1.5.0"; - }; - "node_modules/esrecurse" = { - dev = true; - key = "esrecurse/4.3.0"; - }; - "node_modules/estraverse" = { - dev = true; - key = "estraverse/5.3.0"; - }; - "node_modules/estree-walker" = { - dev = true; - key = "estree-walker/2.0.2"; - }; - "node_modules/esutils" = { - dev = true; - key = "esutils/2.0.3"; - }; - "node_modules/event-target-shim" = { - dev = true; - key = "event-target-shim/5.0.1"; - }; - "node_modules/eventemitter3" = { - key = "eventemitter3/4.0.7"; - }; - "node_modules/execa" = { - dev = true; - key = "execa/5.1.1"; - }; - "node_modules/fast-deep-equal" = { - key = "fast-deep-equal/3.1.3"; - }; - "node_modules/fast-equals" = { - key = "fast-equals/5.0.1"; - }; - "node_modules/fast-glob" = { - key = "fast-glob/3.3.1"; - }; - "node_modules/fast-glob/node_modules/glob-parent" = { - key = "glob-parent/5.1.2"; - }; - "node_modules/fast-json-stable-stringify" = { - dev = true; - key = "fast-json-stable-stringify/2.1.0"; - }; - "node_modules/fast-levenshtein" = { - dev = true; - key = "fast-levenshtein/2.0.6"; - }; - "node_modules/fast-memoize" = { - dev = true; - key = "fast-memoize/2.5.2"; - }; - "node_modules/fast-safe-stringify" = { - dev = true; - key = "fast-safe-stringify/2.1.1"; - }; - "node_modules/fastq" = { - key = "fastq/1.15.0"; - }; - "node_modules/file-entry-cache" = { - dev = true; - key = "file-entry-cache/6.0.1"; - }; - "node_modules/fill-range" = { - key = "fill-range/7.0.1"; - }; - "node_modules/find-root" = { - key = "find-root/1.1.0"; - }; - "node_modules/find-up" = { - dev = true; - key = "find-up/5.0.0"; - }; - "node_modules/flat-cache" = { - dev = true; - key = "flat-cache/3.0.4"; - }; - "node_modules/flatted" = { - dev = true; - key = "flatted/3.2.7"; - }; - "node_modules/follow-redirects" = { - key = "follow-redirects/1.15.2"; - }; - "node_modules/for-each" = { - dev = true; - key = "for-each/0.3.3"; - }; - "node_modules/form-data" = { - key = "form-data/4.0.0"; - }; - "node_modules/format-util" = { - dev = true; - key = "format-util/1.0.5"; - }; - "node_modules/fraction.js" = { - key = "fraction.js/4.2.0"; - }; - "node_modules/fs-extra" = { - dev = true; - key = "fs-extra/10.1.0"; - }; - "node_modules/fs.realpath" = { - key = "fs.realpath/1.0.0"; - }; - "node_modules/fsevents" = { - key = "fsevents/2.3.2"; - optional = true; - }; - "node_modules/function-bind" = { - key = "function-bind/1.1.1"; - }; - "node_modules/function.prototype.name" = { - dev = true; - key = "function.prototype.name/1.1.5"; - }; - "node_modules/functions-have-names" = { - dev = true; - key = "functions-have-names/1.2.3"; - }; - "node_modules/get-caller-file" = { - dev = true; - key = "get-caller-file/2.0.5"; - }; - "node_modules/get-intrinsic" = { - dev = true; - key = "get-intrinsic/1.2.1"; - }; - "node_modules/get-source" = { - dev = true; - key = "get-source/2.0.12"; - }; - "node_modules/get-source/node_modules/source-map" = { - dev = true; - key = "source-map/0.6.1"; - }; - "node_modules/get-stream" = { - dev = true; - key = "get-stream/6.0.1"; - }; - "node_modules/get-symbol-description" = { - dev = true; - key = "get-symbol-description/1.0.0"; - }; - "node_modules/get-tsconfig" = { - dev = true; - key = "get-tsconfig/4.7.0"; - }; - "node_modules/glob" = { - dev = true; - key = "glob/7.1.7"; - }; - "node_modules/glob-parent" = { - key = "glob-parent/6.0.2"; - }; - "node_modules/glob-to-regexp" = { - key = "glob-to-regexp/0.4.1"; - }; - "node_modules/globals" = { - dev = true; - key = "globals/13.21.0"; - }; - "node_modules/globalthis" = { - dev = true; - key = "globalthis/1.0.3"; - }; - "node_modules/globby" = { - dev = true; - key = "globby/11.1.0"; - }; - "node_modules/goober" = { - key = "goober/2.1.13"; - }; - "node_modules/gopd" = { - dev = true; - key = "gopd/1.0.1"; - }; - "node_modules/graceful-fs" = { - key = "graceful-fs/4.2.11"; - }; - "node_modules/graphemer" = { - dev = true; - key = "graphemer/1.4.0"; - }; - "node_modules/has" = { - key = "has/1.0.3"; - }; - "node_modules/has-bigints" = { - dev = true; - key = "has-bigints/1.0.2"; - }; - "node_modules/has-flag" = { - dev = true; - key = "has-flag/4.0.0"; - }; - "node_modules/has-property-descriptors" = { - dev = true; - key = "has-property-descriptors/1.0.0"; - }; - "node_modules/has-proto" = { - dev = true; - key = "has-proto/1.0.1"; - }; - "node_modules/has-symbols" = { - dev = true; - key = "has-symbols/1.0.3"; - }; - "node_modules/has-tostringtag" = { - dev = true; - key = "has-tostringtag/1.0.0"; - }; - "node_modules/hex-rgb" = { - key = "hex-rgb/5.0.0"; - }; - "node_modules/hoist-non-react-statics" = { - key = "hoist-non-react-statics/3.3.2"; - }; - "node_modules/hoist-non-react-statics/node_modules/react-is" = { - key = "react-is/16.13.1"; - }; - "node_modules/hpagent" = { - dev = true; - key = "hpagent/1.2.0"; - }; - "node_modules/http2-client" = { - dev = true; - key = "http2-client/1.3.5"; - }; - "node_modules/human-signals" = { - dev = true; - key = "human-signals/2.1.0"; - }; - "node_modules/ibm-openapi-validator" = { - dev = true; - key = "ibm-openapi-validator/0.97.5"; - }; - "node_modules/ibm-openapi-validator/node_modules/argparse" = { - dev = true; - key = "argparse/1.0.10"; - }; - "node_modules/ibm-openapi-validator/node_modules/commander" = { - dev = true; - key = "commander/2.20.3"; - }; - "node_modules/ibm-openapi-validator/node_modules/find-up" = { - dev = true; - key = "find-up/3.0.0"; - }; - "node_modules/ibm-openapi-validator/node_modules/js-yaml" = { - dev = true; - key = "js-yaml/3.14.1"; - }; - "node_modules/ibm-openapi-validator/node_modules/locate-path" = { - dev = true; - key = "locate-path/3.0.0"; - }; - "node_modules/ibm-openapi-validator/node_modules/p-limit" = { - dev = true; - key = "p-limit/2.3.0"; - }; - "node_modules/ibm-openapi-validator/node_modules/p-locate" = { - dev = true; - key = "p-locate/3.0.0"; - }; - "node_modules/ibm-openapi-validator/node_modules/path-exists" = { - dev = true; - key = "path-exists/3.0.0"; - }; - "node_modules/ibm-openapi-validator/node_modules/semver" = { - dev = true; - key = "semver/5.7.2"; - }; - "node_modules/ignore" = { - dev = true; - key = "ignore/5.2.4"; - }; - "node_modules/immer" = { - dev = true; - key = "immer/9.0.21"; - }; - "node_modules/import-fresh" = { - key = "import-fresh/3.3.0"; - }; - "node_modules/imurmurhash" = { - dev = true; - key = "imurmurhash/0.1.4"; - }; - "node_modules/inflight" = { - key = "inflight/1.0.6"; - }; - "node_modules/inherits" = { - key = "inherits/2.0.4"; - }; - "node_modules/internal-slot" = { - dev = true; - key = "internal-slot/1.0.5"; - }; - "node_modules/internmap" = { - key = "internmap/2.0.3"; - }; - "node_modules/is-array-buffer" = { - dev = true; - key = "is-array-buffer/3.0.2"; - }; - "node_modules/is-arrayish" = { - key = "is-arrayish/0.2.1"; - }; - "node_modules/is-bigint" = { - dev = true; - key = "is-bigint/1.0.4"; - }; - "node_modules/is-binary-path" = { - key = "is-binary-path/2.1.0"; - }; - "node_modules/is-boolean-object" = { - dev = true; - key = "is-boolean-object/1.1.2"; - }; - "node_modules/is-callable" = { - dev = true; - key = "is-callable/1.2.7"; - }; - "node_modules/is-core-module" = { - key = "is-core-module/2.13.0"; - }; - "node_modules/is-date-object" = { - dev = true; - key = "is-date-object/1.0.5"; - }; - "node_modules/is-extglob" = { - key = "is-extglob/2.1.1"; - }; - "node_modules/is-fullwidth-code-point" = { - dev = true; - key = "is-fullwidth-code-point/3.0.0"; - }; - "node_modules/is-glob" = { - key = "is-glob/4.0.3"; - }; - "node_modules/is-negative-zero" = { - dev = true; - key = "is-negative-zero/2.0.2"; - }; - "node_modules/is-number" = { - key = "is-number/7.0.0"; - }; - "node_modules/is-number-object" = { - dev = true; - key = "is-number-object/1.0.7"; - }; - "node_modules/is-path-inside" = { - dev = true; - key = "is-path-inside/3.0.3"; - }; - "node_modules/is-reference" = { - dev = true; - key = "is-reference/1.2.1"; - }; - "node_modules/is-regex" = { - dev = true; - key = "is-regex/1.1.4"; - }; - "node_modules/is-shared-array-buffer" = { - dev = true; - key = "is-shared-array-buffer/1.0.2"; - }; - "node_modules/is-stream" = { - dev = true; - key = "is-stream/2.0.1"; - }; - "node_modules/is-string" = { - dev = true; - key = "is-string/1.0.7"; - }; - "node_modules/is-symbol" = { - dev = true; - key = "is-symbol/1.0.4"; - }; - "node_modules/is-typed-array" = { - dev = true; - key = "is-typed-array/1.1.12"; - }; - "node_modules/is-weakref" = { - dev = true; - key = "is-weakref/1.0.2"; - }; - "node_modules/isarray" = { - dev = true; - key = "isarray/2.0.5"; - }; - "node_modules/isexe" = { - dev = true; - key = "isexe/2.0.0"; - }; - "node_modules/jiti" = { - key = "jiti/1.19.1"; - }; - "node_modules/js-tokens" = { - key = "js-tokens/4.0.0"; - }; - "node_modules/js-yaml" = { - dev = true; - key = "js-yaml/4.1.0"; - }; - "node_modules/jsep" = { - dev = true; - key = "jsep/1.3.8"; - }; - "node_modules/json-dup-key-validator" = { - dev = true; - key = "json-dup-key-validator/1.0.3"; - }; - "node_modules/json-parse-even-better-errors" = { - key = "json-parse-even-better-errors/2.3.1"; - }; - "node_modules/json-schema-compare" = { - key = "json-schema-compare/0.2.2"; - }; - "node_modules/json-schema-merge-allof" = { - key = "json-schema-merge-allof/0.8.1"; - }; - "node_modules/json-schema-ref-parser" = { - dev = true; - key = "json-schema-ref-parser/5.1.3"; - }; - "node_modules/json-schema-ref-parser/node_modules/argparse" = { - dev = true; - key = "argparse/1.0.10"; - }; - "node_modules/json-schema-ref-parser/node_modules/debug" = { - dev = true; - key = "debug/3.2.7"; - }; - "node_modules/json-schema-ref-parser/node_modules/js-yaml" = { - dev = true; - key = "js-yaml/3.14.1"; - }; - "node_modules/json-schema-traverse" = { - dev = true; - key = "json-schema-traverse/0.4.1"; - }; - "node_modules/json-stable-stringify-without-jsonify" = { - dev = true; - key = "json-stable-stringify-without-jsonify/1.0.1"; - }; - "node_modules/json5" = { - dev = true; - key = "json5/1.0.2"; - }; - "node_modules/jsonc-parser" = { - dev = true; - key = "jsonc-parser/2.2.1"; - }; - "node_modules/jsonfile" = { - dev = true; - key = "jsonfile/6.1.0"; - }; - "node_modules/jsonpath-plus" = { - dev = true; - key = "jsonpath-plus/7.1.0"; - }; - "node_modules/jsonpointer" = { - key = "jsonpointer/5.0.1"; - }; - "node_modules/jsonschema" = { - dev = true; - key = "jsonschema/1.4.1"; - }; - "node_modules/jsx-ast-utils" = { - dev = true; - key = "jsx-ast-utils/3.3.5"; - }; - "node_modules/language-subtag-registry" = { - dev = true; - key = "language-subtag-registry/0.3.22"; - }; - "node_modules/language-tags" = { - dev = true; - key = "language-tags/1.0.5"; - }; - "node_modules/leven" = { - dev = true; - key = "leven/3.1.0"; - }; - "node_modules/levn" = { - dev = true; - key = "levn/0.4.1"; - }; - "node_modules/lilconfig" = { - key = "lilconfig/2.1.0"; - }; - "node_modules/lines-and-columns" = { - key = "lines-and-columns/1.2.4"; - }; - "node_modules/locate-path" = { - dev = true; - key = "locate-path/6.0.0"; - }; - "node_modules/lodash" = { - key = "lodash/4.17.21"; - }; - "node_modules/lodash-es" = { - key = "lodash-es/4.17.21"; - }; - "node_modules/lodash.get" = { - dev = true; - key = "lodash.get/4.4.2"; - }; - "node_modules/lodash.isempty" = { - dev = true; - key = "lodash.isempty/4.4.0"; - }; - "node_modules/lodash.merge" = { - dev = true; - key = "lodash.merge/4.6.2"; - }; - "node_modules/lodash.omit" = { - dev = true; - key = "lodash.omit/4.5.0"; - }; - "node_modules/lodash.omitby" = { - dev = true; - key = "lodash.omitby/4.6.0"; - }; - "node_modules/lodash.topath" = { - dev = true; - key = "lodash.topath/4.5.2"; - }; - "node_modules/lodash.uniq" = { - dev = true; - key = "lodash.uniq/4.5.0"; - }; - "node_modules/lodash.uniqby" = { - dev = true; - key = "lodash.uniqby/4.7.0"; - }; - "node_modules/lodash.uniqwith" = { - dev = true; - key = "lodash.uniqwith/4.5.0"; - }; - "node_modules/loose-envify" = { - key = "loose-envify/1.4.0"; - }; - "node_modules/lru-cache" = { - dev = true; - key = "lru-cache/6.0.0"; - }; - "node_modules/magic-string" = { - dev = true; - key = "magic-string/0.25.9"; - }; - "node_modules/markdown-to-jsx" = { - key = "markdown-to-jsx/7.3.2"; - }; - "node_modules/matcher" = { - dev = true; - key = "matcher/1.1.1"; - }; - "node_modules/matcher/node_modules/escape-string-regexp" = { - dev = true; - key = "escape-string-regexp/1.0.5"; - }; - "node_modules/merge-stream" = { - dev = true; - key = "merge-stream/2.0.0"; - }; - "node_modules/merge2" = { - key = "merge2/1.4.1"; - }; - "node_modules/micromatch" = { - key = "micromatch/4.0.5"; - }; - "node_modules/mime-db" = { - key = "mime-db/1.52.0"; - }; - "node_modules/mime-types" = { - key = "mime-types/2.1.35"; - }; - "node_modules/mimic-fn" = { - dev = true; - key = "mimic-fn/2.1.0"; - }; - "node_modules/minimatch" = { - key = "minimatch/3.1.2"; - }; - "node_modules/minimist" = { - dev = true; - key = "minimist/1.2.8"; - }; - "node_modules/ms" = { - dev = true; - key = "ms/2.1.2"; - }; - "node_modules/mz" = { - key = "mz/2.7.0"; - }; - "node_modules/nanoid" = { - key = "nanoid/3.3.6"; - }; - "node_modules/natural-compare" = { - dev = true; - key = "natural-compare/1.4.0"; - }; - "node_modules/next" = { - key = "next/13.4.12"; - }; - "node_modules/next/node_modules/postcss" = { - key = "postcss/8.4.14"; - }; - "node_modules/nimma" = { - dev = true; - key = "nimma/0.2.2"; - }; - "node_modules/nimma/node_modules/jsonpath-plus" = { - dev = true; - key = "jsonpath-plus/6.0.1"; - optional = true; - }; - "node_modules/node-fetch" = { - dev = true; - key = "node-fetch/2.7.0"; - }; - "node_modules/node-fetch-h2" = { - dev = true; - key = "node-fetch-h2/2.3.0"; - }; - "node_modules/node-readfiles" = { - dev = true; - key = "node-readfiles/0.2.0"; - }; - "node_modules/node-releases" = { - key = "node-releases/2.0.13"; - }; - "node_modules/normalize-path" = { - key = "normalize-path/3.0.0"; - }; - "node_modules/normalize-range" = { - key = "normalize-range/0.1.2"; - }; - "node_modules/npm-run-path" = { - dev = true; - key = "npm-run-path/4.0.1"; - }; - "node_modules/oas-kit-common" = { - dev = true; - key = "oas-kit-common/1.0.8"; - }; - "node_modules/oas-linter" = { - dev = true; - key = "oas-linter/3.2.2"; - }; - "node_modules/oas-resolver" = { - dev = true; - key = "oas-resolver/2.5.6"; - }; - "node_modules/oas-schema-walker" = { - dev = true; - key = "oas-schema-walker/1.1.5"; - }; - "node_modules/oas-validator" = { - dev = true; - key = "oas-validator/5.0.8"; - }; - "node_modules/object-assign" = { - key = "object-assign/4.1.1"; - }; - "node_modules/object-hash" = { - key = "object-hash/3.0.0"; - }; - "node_modules/object-inspect" = { - dev = true; - key = "object-inspect/1.12.3"; - }; - "node_modules/object-keys" = { - dev = true; - key = "object-keys/1.1.1"; - }; - "node_modules/object.assign" = { - dev = true; - key = "object.assign/4.1.4"; - }; - "node_modules/object.entries" = { - dev = true; - key = "object.entries/1.1.6"; - }; - "node_modules/object.fromentries" = { - dev = true; - key = "object.fromentries/2.0.6"; - }; - "node_modules/object.groupby" = { - dev = true; - key = "object.groupby/1.0.0"; - }; - "node_modules/object.hasown" = { - dev = true; - key = "object.hasown/1.1.2"; - }; - "node_modules/object.values" = { - dev = true; - key = "object.values/1.1.6"; - }; - "node_modules/once" = { - key = "once/1.4.0"; - }; - "node_modules/onetime" = { - dev = true; - key = "onetime/5.1.2"; - }; - "node_modules/ono" = { - dev = true; - key = "ono/4.0.11"; - }; - "node_modules/openapi-types" = { - dev = true; - key = "openapi-types/12.1.3"; - }; - "node_modules/openapi3-ts" = { - dev = true; - key = "openapi3-ts/3.2.0"; - }; - "node_modules/openapi3-ts/node_modules/yaml" = { - dev = true; - key = "yaml/2.3.1"; - }; - "node_modules/optionator" = { - dev = true; - key = "optionator/0.9.3"; - }; - "node_modules/orval" = { - dev = true; - key = "orval/6.17.0"; - }; - "node_modules/orval/node_modules/ajv" = { - dev = true; - key = "ajv/8.12.0"; - }; - "node_modules/orval/node_modules/json-schema-traverse" = { - dev = true; - key = "json-schema-traverse/1.0.0"; - }; - "node_modules/p-limit" = { - dev = true; - key = "p-limit/3.1.0"; - }; - "node_modules/p-locate" = { - dev = true; - key = "p-locate/5.0.0"; - }; - "node_modules/p-try" = { - dev = true; - key = "p-try/2.2.0"; - }; - "node_modules/pad" = { - dev = true; - key = "pad/2.3.0"; - }; - "node_modules/parent-module" = { - key = "parent-module/1.0.1"; - }; - "node_modules/parse-json" = { - key = "parse-json/5.2.0"; - }; - "node_modules/path-exists" = { - dev = true; - key = "path-exists/4.0.0"; - }; - "node_modules/path-is-absolute" = { - key = "path-is-absolute/1.0.1"; - }; - "node_modules/path-key" = { - dev = true; - key = "path-key/3.1.1"; - }; - "node_modules/path-parse" = { - key = "path-parse/1.0.7"; - }; - "node_modules/path-type" = { - key = "path-type/4.0.0"; - }; - "node_modules/picocolors" = { - key = "picocolors/1.0.0"; - }; - "node_modules/picomatch" = { - key = "picomatch/2.3.1"; - }; - "node_modules/pify" = { - key = "pify/2.3.0"; - }; - "node_modules/pirates" = { - key = "pirates/4.0.6"; - }; - "node_modules/pony-cause" = { - dev = true; - key = "pony-cause/1.1.1"; - }; - "node_modules/postcss" = { - key = "postcss/8.4.27"; - }; - "node_modules/postcss-import" = { - key = "postcss-import/15.1.0"; - }; - "node_modules/postcss-js" = { - key = "postcss-js/4.0.1"; - }; - "node_modules/postcss-load-config" = { - key = "postcss-load-config/4.0.1"; - }; - "node_modules/postcss-load-config/node_modules/yaml" = { - key = "yaml/2.3.1"; - }; - "node_modules/postcss-nested" = { - key = "postcss-nested/6.0.1"; - }; - "node_modules/postcss-selector-parser" = { - key = "postcss-selector-parser/6.0.13"; - }; - "node_modules/postcss-value-parser" = { - key = "postcss-value-parser/4.2.0"; - }; - "node_modules/prelude-ls" = { - dev = true; - key = "prelude-ls/1.2.1"; - }; - "node_modules/prettier" = { - dev = true; - key = "prettier/3.0.1"; - }; - "node_modules/prettier-plugin-tailwindcss" = { - dev = true; - key = "prettier-plugin-tailwindcss/0.4.1"; - }; - "node_modules/pretty-bytes" = { - key = "pretty-bytes/6.1.1"; - }; - "node_modules/printable-characters" = { - dev = true; - key = "printable-characters/1.0.42"; - }; - "node_modules/prop-types" = { - key = "prop-types/15.8.1"; - }; - "node_modules/prop-types/node_modules/react-is" = { - key = "react-is/16.13.1"; - }; - "node_modules/proxy-from-env" = { - key = "proxy-from-env/1.1.0"; - }; - "node_modules/punycode" = { - key = "punycode/2.3.0"; - }; - "node_modules/queue-microtask" = { - key = "queue-microtask/1.2.3"; - }; - "node_modules/react" = { - key = "react/18.2.0"; - }; - "node_modules/react-dom" = { - key = "react-dom/18.2.0"; - }; - "node_modules/react-hook-form" = { - key = "react-hook-form/7.45.4"; - }; - "node_modules/react-hot-toast" = { - key = "react-hot-toast/2.4.1"; - }; - "node_modules/react-is" = { - key = "react-is/18.2.0"; - }; - "node_modules/react-lifecycles-compat" = { - key = "react-lifecycles-compat/3.0.4"; - }; - "node_modules/react-resize-detector" = { - key = "react-resize-detector/8.1.0"; - }; - "node_modules/react-smooth" = { - key = "react-smooth/2.0.3"; - }; - "node_modules/react-smooth/node_modules/dom-helpers" = { - key = "dom-helpers/3.4.0"; - }; - "node_modules/react-smooth/node_modules/react-transition-group" = { - key = "react-transition-group/2.9.0"; - }; - "node_modules/react-transition-group" = { - key = "react-transition-group/4.4.5"; - }; - "node_modules/read-cache" = { - key = "read-cache/1.0.0"; - }; - "node_modules/readdirp" = { - key = "readdirp/3.6.0"; - }; - "node_modules/recharts" = { - key = "recharts/2.7.3"; - }; - "node_modules/recharts-scale" = { - key = "recharts-scale/0.4.5"; - }; - "node_modules/recharts/node_modules/react-is" = { - key = "react-is/16.13.1"; - }; - "node_modules/reduce-css-calc" = { - key = "reduce-css-calc/2.1.8"; - }; - "node_modules/reduce-css-calc/node_modules/postcss-value-parser" = { - key = "postcss-value-parser/3.3.1"; - }; - "node_modules/reftools" = { - dev = true; - key = "reftools/1.1.9"; - }; - "node_modules/regenerator-runtime" = { - key = "regenerator-runtime/0.14.0"; - }; - "node_modules/regexp.prototype.flags" = { - dev = true; - key = "regexp.prototype.flags/1.5.0"; - }; - "node_modules/require-all" = { - dev = true; - key = "require-all/3.0.0"; - }; - "node_modules/require-directory" = { - dev = true; - key = "require-directory/2.1.1"; - }; - "node_modules/require-from-string" = { - key = "require-from-string/2.0.2"; - }; - "node_modules/reserved" = { - dev = true; - key = "reserved/0.1.2"; - }; - "node_modules/resolve" = { - key = "resolve/1.22.4"; - }; - "node_modules/resolve-from" = { - key = "resolve-from/4.0.0"; - }; - "node_modules/resolve-pkg-maps" = { - dev = true; - key = "resolve-pkg-maps/1.0.0"; - }; - "node_modules/reusify" = { - key = "reusify/1.0.4"; - }; - "node_modules/rimraf" = { - dev = true; - key = "rimraf/3.0.2"; - }; - "node_modules/rollup" = { - dev = true; - key = "rollup/2.79.1"; - }; - "node_modules/run-parallel" = { - key = "run-parallel/1.2.0"; - }; - "node_modules/safe-array-concat" = { - dev = true; - key = "safe-array-concat/1.0.0"; - }; - "node_modules/safe-regex-test" = { - dev = true; - key = "safe-regex-test/1.0.0"; - }; - "node_modules/safe-stable-stringify" = { - dev = true; - key = "safe-stable-stringify/1.1.1"; - }; - "node_modules/scheduler" = { - key = "scheduler/0.23.0"; - }; - "node_modules/semver" = { - dev = true; - key = "semver/7.5.4"; - }; - "node_modules/shebang-command" = { - dev = true; - key = "shebang-command/2.0.0"; - }; - "node_modules/shebang-regex" = { - dev = true; - key = "shebang-regex/3.0.0"; - }; - "node_modules/should" = { - dev = true; - key = "should/13.2.3"; - }; - "node_modules/should-equal" = { - dev = true; - key = "should-equal/2.0.0"; - }; - "node_modules/should-format" = { - dev = true; - key = "should-format/3.0.3"; - }; - "node_modules/should-type" = { - dev = true; - key = "should-type/1.4.0"; - }; - "node_modules/should-type-adaptors" = { - dev = true; - key = "should-type-adaptors/1.1.0"; - }; - "node_modules/should-util" = { - dev = true; - key = "should-util/1.0.1"; - }; - "node_modules/side-channel" = { - dev = true; - key = "side-channel/1.0.4"; - }; - "node_modules/signal-exit" = { - dev = true; - key = "signal-exit/3.0.7"; - }; - "node_modules/simple-eval" = { - dev = true; - key = "simple-eval/1.0.0"; - }; - "node_modules/slash" = { - dev = true; - key = "slash/3.0.0"; - }; - "node_modules/source-map" = { - key = "source-map/0.5.7"; - }; - "node_modules/source-map-js" = { - key = "source-map-js/1.0.2"; - }; - "node_modules/sourcemap-codec" = { - dev = true; - key = "sourcemap-codec/1.4.8"; - }; - "node_modules/sprintf-js" = { - dev = true; - key = "sprintf-js/1.0.3"; - }; - "node_modules/stacktracey" = { - dev = true; - key = "stacktracey/2.1.8"; - }; - "node_modules/streamsearch" = { - key = "streamsearch/1.1.0"; - }; - "node_modules/string-argv" = { - dev = true; - key = "string-argv/0.3.2"; - }; - "node_modules/string-width" = { - dev = true; - key = "string-width/4.2.3"; - }; - "node_modules/string-width/node_modules/emoji-regex" = { - dev = true; - key = "emoji-regex/8.0.0"; - }; - "node_modules/string.prototype.matchall" = { - dev = true; - key = "string.prototype.matchall/4.0.8"; - }; - "node_modules/string.prototype.trim" = { - dev = true; - key = "string.prototype.trim/1.2.7"; - }; - "node_modules/string.prototype.trimend" = { - dev = true; - key = "string.prototype.trimend/1.0.6"; - }; - "node_modules/string.prototype.trimstart" = { - dev = true; - key = "string.prototype.trimstart/1.0.6"; - }; - "node_modules/strip-ansi" = { - dev = true; - key = "strip-ansi/6.0.1"; - }; - "node_modules/strip-bom" = { - dev = true; - key = "strip-bom/3.0.0"; - }; - "node_modules/strip-final-newline" = { - dev = true; - key = "strip-final-newline/2.0.0"; - }; - "node_modules/strip-json-comments" = { - dev = true; - key = "strip-json-comments/3.1.1"; - }; - "node_modules/styled-jsx" = { - key = "styled-jsx/5.1.1"; - }; - "node_modules/stylis" = { - key = "stylis/4.2.0"; - }; - "node_modules/sucrase" = { - key = "sucrase/3.34.0"; - }; - "node_modules/sucrase/node_modules/glob" = { - key = "glob/7.1.6"; - }; - "node_modules/supports-color" = { - dev = true; - key = "supports-color/7.2.0"; - }; - "node_modules/supports-preserve-symlinks-flag" = { - key = "supports-preserve-symlinks-flag/1.0.0"; - }; - "node_modules/swagger2openapi" = { - dev = true; - key = "swagger2openapi/7.0.8"; - }; - "node_modules/swr" = { - key = "swr/2.2.1"; - }; - "node_modules/tailwindcss" = { - key = "tailwindcss/3.3.3"; - }; - "node_modules/tapable" = { - dev = true; - key = "tapable/2.2.1"; - }; - "node_modules/text-table" = { - dev = true; - key = "text-table/0.2.0"; - }; - "node_modules/thenify" = { - key = "thenify/3.3.1"; - }; - "node_modules/thenify-all" = { - key = "thenify-all/1.6.0"; - }; - "node_modules/to-fast-properties" = { - key = "to-fast-properties/2.0.0"; - }; - "node_modules/to-regex-range" = { - key = "to-regex-range/5.0.1"; - }; - "node_modules/tr46" = { - dev = true; - key = "tr46/0.0.3"; - }; - "node_modules/ts-interface-checker" = { - key = "ts-interface-checker/0.1.13"; - }; - "node_modules/tsconfck" = { - dev = true; - key = "tsconfck/2.1.2"; - }; - "node_modules/tsconfig-paths" = { - dev = true; - key = "tsconfig-paths/3.14.2"; - }; - "node_modules/tslib" = { - key = "tslib/2.6.1"; - }; - "node_modules/tsutils" = { - dev = true; - key = "tsutils/3.21.0"; - }; - "node_modules/tsutils/node_modules/tslib" = { - dev = true; - key = "tslib/1.14.1"; - }; - "node_modules/type-check" = { - dev = true; - key = "type-check/0.4.0"; - }; - "node_modules/type-fest" = { - dev = true; - key = "type-fest/0.20.2"; - }; - "node_modules/typed-array-buffer" = { - dev = true; - key = "typed-array-buffer/1.0.0"; - }; - "node_modules/typed-array-byte-length" = { - dev = true; - key = "typed-array-byte-length/1.0.0"; - }; - "node_modules/typed-array-byte-offset" = { - dev = true; - key = "typed-array-byte-offset/1.0.0"; - }; - "node_modules/typed-array-length" = { - dev = true; - key = "typed-array-length/1.0.4"; - }; - "node_modules/typescript" = { - dev = true; - key = "typescript/5.1.6"; - }; - "node_modules/unbox-primitive" = { - dev = true; - key = "unbox-primitive/1.0.2"; - }; - "node_modules/universalify" = { - dev = true; - key = "universalify/2.0.0"; - }; - "node_modules/update-browserslist-db" = { - key = "update-browserslist-db/1.0.11"; - }; - "node_modules/uri-js" = { - key = "uri-js/4.4.1"; - }; - "node_modules/urijs" = { - dev = true; - key = "urijs/1.19.11"; - }; - "node_modules/use-sync-external-store" = { - key = "use-sync-external-store/1.2.0"; - }; - "node_modules/util-deprecate" = { - key = "util-deprecate/1.0.2"; - }; - "node_modules/utility-types" = { - dev = true; - key = "utility-types/3.10.0"; - }; - "node_modules/validate-npm-package-name" = { - dev = true; - key = "validate-npm-package-name/3.0.0"; - }; - "node_modules/validate.io-array" = { - key = "validate.io-array/1.0.6"; - }; - "node_modules/validate.io-function" = { - key = "validate.io-function/1.0.2"; - }; - "node_modules/validate.io-integer" = { - key = "validate.io-integer/1.0.5"; - }; - "node_modules/validate.io-integer-array" = { - key = "validate.io-integer-array/1.0.0"; - }; - "node_modules/validate.io-number" = { - key = "validate.io-number/1.0.3"; - }; - "node_modules/validator" = { - dev = true; - key = "validator/13.11.0"; - }; - "node_modules/victory-vendor" = { - key = "victory-vendor/36.6.11"; - }; - "node_modules/watchpack" = { - key = "watchpack/2.4.0"; - }; - "node_modules/wcwidth" = { - dev = true; - key = "wcwidth/1.0.1"; - }; - "node_modules/webidl-conversions" = { - dev = true; - key = "webidl-conversions/3.0.1"; - }; - "node_modules/whatwg-url" = { - dev = true; - key = "whatwg-url/5.0.0"; - }; - "node_modules/which" = { - dev = true; - key = "which/2.0.2"; - }; - "node_modules/which-boxed-primitive" = { - dev = true; - key = "which-boxed-primitive/1.0.2"; - }; - "node_modules/which-typed-array" = { - dev = true; - key = "which-typed-array/1.1.11"; - }; - "node_modules/wrap-ansi" = { - dev = true; - key = "wrap-ansi/7.0.0"; - }; - "node_modules/wrappy" = { - key = "wrappy/1.0.2"; - }; - "node_modules/y18n" = { - dev = true; - key = "y18n/5.0.8"; - }; - "node_modules/yallist" = { - dev = true; - key = "yallist/4.0.0"; - }; - "node_modules/yaml" = { - key = "yaml/1.10.2"; - }; - "node_modules/yaml-js" = { - dev = true; - key = "yaml-js/0.2.3"; - }; - "node_modules/yargs" = { - dev = true; - key = "yargs/17.3.1"; - }; - "node_modules/yargs-parser" = { - dev = true; - key = "yargs-parser/21.1.1"; - }; - "node_modules/yocto-queue" = { - dev = true; - key = "yocto-queue/0.1.0"; - }; - "node_modules/zod" = { - key = "zod/3.21.4"; - }; - }; - version = "0.1.0"; - }; - }; mz = { "2.7.0" = { depInfo = { @@ -13634,6 +13801,19 @@ version = "1.4.0"; }; }; + natural-compare-lite = { + "1.4.0" = { + fetchInfo = { + narHash = "sha256-2rhqIqg6V9A4qB5PB7IQhB2kU+UlBzWRZ6fhBg4SDGQ="; + type = "tarball"; + url = "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz"; + }; + ident = "natural-compare-lite"; + ltype = "file"; + treeInfo = { }; + version = "1.4.0"; + }; + }; next = { "13.4.12" = { binInfo = { @@ -18258,4 +18438,4 @@ }; }; }; -} +} \ No newline at end of file diff --git a/pkgs/ui/package-lock.json b/pkgs/ui/package-lock.json index 3cd7efb5..ac987516 100644 --- a/pkgs/ui/package-lock.json +++ b/pkgs/ui/package-lock.json @@ -16,6 +16,7 @@ "@rjsf/mui": "^5.12.1", "@rjsf/validator-ajv8": "^5.12.1", "@types/json-schema": "^7.0.12", + "@typescript-eslint/eslint-plugin": "^5.62.0", "autoprefixer": "10.4.14", "axios": "^1.4.0", "classnames": "^2.3.2", @@ -50,7 +51,6 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -550,7 +550,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -565,7 +564,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", - "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -574,7 +572,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", - "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -597,7 +594,6 @@ "version": "8.47.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz", "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -612,7 +608,6 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", - "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", @@ -626,7 +621,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "engines": { "node": ">=12.22" }, @@ -638,8 +632,7 @@ "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" }, "node_modules/@ibm-cloud/openapi-ruleset": { "version": "0.45.5", @@ -2148,6 +2141,11 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, + "node_modules/@types/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==" + }, "node_modules/@types/urijs": { "version": "1.19.19", "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.19.tgz", @@ -2160,11 +2158,43 @@ "integrity": "sha512-cSjhgrr8g4KbPnnijAr/KJDNKa/bBa+ixYkywFRvrhvi9n1WEl7yYbtRyzE6jqNQiSxxJxoAW3STaOQwJHndaw==", "dev": true }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/parser": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", - "dev": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -2191,7 +2221,6 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dev": true, "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" @@ -2204,11 +2233,36 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/types": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -2221,7 +2275,6 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dev": true, "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", @@ -2244,11 +2297,55 @@ } } }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/@typescript-eslint/visitor-keys": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" @@ -2277,7 +2374,6 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2289,7 +2385,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2298,7 +2393,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2359,7 +2453,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2368,7 +2461,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2404,8 +2496,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { "version": "5.3.0", @@ -2452,7 +2543,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, "engines": { "node": ">=8" } @@ -2826,7 +2916,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2917,7 +3006,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2928,8 +3016,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -3014,7 +3101,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3178,7 +3264,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3199,8 +3284,7 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/deepmerge": { "version": "2.2.1", @@ -3274,7 +3358,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "dependencies": { "path-type": "^4.0.0" }, @@ -3291,7 +3374,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -3853,7 +3935,6 @@ "version": "8.46.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", - "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4200,7 +4281,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -4216,7 +4296,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -4228,7 +4307,6 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -4258,7 +4336,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -4270,7 +4347,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -4282,7 +4358,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -4297,7 +4372,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4381,14 +4455,12 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fast-memoize": { "version": "2.5.2", @@ -4414,7 +4486,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -4442,7 +4513,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -4458,7 +4528,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, "dependencies": { "flatted": "^3.1.0", "rimraf": "^3.0.2" @@ -4470,8 +4539,7 @@ "node_modules/flatted": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, "node_modules/follow-redirects": { "version": "1.15.2", @@ -4683,7 +4751,6 @@ "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4719,7 +4786,6 @@ "version": "13.21.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", - "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -4749,7 +4815,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -4793,8 +4858,7 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, "node_modules/has": { "version": "1.0.3", @@ -4820,7 +4884,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -5059,7 +5122,6 @@ "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, "engines": { "node": ">= 4" } @@ -5093,7 +5155,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -5297,7 +5358,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -5417,8 +5477,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/jiti": { "version": "1.19.1", @@ -5437,7 +5496,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -5538,14 +5596,12 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { "version": "1.0.2", @@ -5646,7 +5702,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -5672,7 +5727,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -5708,8 +5762,7 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "node_modules/lodash.omit": { "version": "4.5.0", @@ -5762,7 +5815,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -5888,8 +5940,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -5921,8 +5972,12 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" }, "node_modules/next": { "version": "13.4.12", @@ -6356,7 +6411,6 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -6426,7 +6480,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -6441,7 +6494,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -6505,7 +6557,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -6522,7 +6573,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -6717,7 +6767,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -7184,7 +7233,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -7282,7 +7330,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -7297,7 +7344,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -7309,7 +7355,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -7404,7 +7449,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -7553,7 +7597,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7583,7 +7626,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -7662,7 +7704,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7768,8 +7809,7 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, "node_modules/thenify": { "version": "3.3.1", @@ -7861,7 +7901,6 @@ "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, "dependencies": { "tslib": "^1.8.1" }, @@ -7875,14 +7914,12 @@ "node_modules/tsutils/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -7894,7 +7931,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -7971,7 +8007,6 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8186,7 +8221,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -8266,8 +8300,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "1.10.2", @@ -8314,7 +8347,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "engines": { "node": ">=10" }, diff --git a/pkgs/ui/package.json b/pkgs/ui/package.json index 2f9028f1..1a00a811 100644 --- a/pkgs/ui/package.json +++ b/pkgs/ui/package.json @@ -20,6 +20,7 @@ "@rjsf/mui": "^5.12.1", "@rjsf/validator-ajv8": "^5.12.1", "@types/json-schema": "^7.0.12", + "@typescript-eslint/eslint-plugin": "^5.62.0", "autoprefixer": "10.4.14", "axios": "^1.4.0", "classnames": "^2.3.2", diff --git a/pkgs/ui/src/components/hooks/useMachines.tsx b/pkgs/ui/src/components/hooks/useMachines.tsx index 96fa9db9..c4862bf1 100644 --- a/pkgs/ui/src/components/hooks/useMachines.tsx +++ b/pkgs/ui/src/components/hooks/useMachines.tsx @@ -24,6 +24,7 @@ type MachineContextType = rawData: AxiosResponse | undefined; data: Machine[]; isLoading: boolean; + flakeName: string; error: AxiosError | undefined; isValidating: boolean; @@ -33,6 +34,7 @@ type MachineContextType = swrKey: string | false | Record; } | { + flakeName: string; isLoading: true; data: readonly []; }; @@ -42,14 +44,23 @@ const initialState = { data: [], } as const; -export const MachineContext = createContext(initialState); + +export function CreateMachineContext(flakeName: string) { + return useMemo(() => { + return createContext({ + ...initialState, + flakeName, + }); + }, [flakeName]); +} interface MachineContextProviderProps { children: ReactNode; + flakeName: string; } export const MachineContextProvider = (props: MachineContextProviderProps) => { - const { children } = props; + const { children, flakeName } = props; const { data: rawData, isLoading, @@ -57,7 +68,7 @@ export const MachineContextProvider = (props: MachineContextProviderProps) => { isValidating, mutate, swrKey, - } = useListMachines(); + } = useListMachines(flakeName); const [filters, setFilters] = useState([]); const data = useMemo(() => { @@ -70,6 +81,8 @@ export const MachineContextProvider = (props: MachineContextProviderProps) => { return []; }, [isLoading, error, isValidating, rawData, filters]); + const MachineContext = CreateMachineContext(flakeName); + return ( { data, isLoading, + flakeName, error, isValidating, @@ -92,4 +106,4 @@ export const MachineContextProvider = (props: MachineContextProviderProps) => { ); }; -export const useMachines = () => React.useContext(MachineContext); +export const useMachines = (flakeName: string) => React.useContext(CreateMachineContext(flakeName)); From 410b7c1158de23b83a9e7d9c3d285475cb38fd5e Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 27 Oct 2023 19:26:51 +0200 Subject: [PATCH 30/36] Updated to main branch --- pkgs/ui/src/app/machines/page.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkgs/ui/src/app/machines/page.tsx b/pkgs/ui/src/app/machines/page.tsx index 7b2e0352..9c89d605 100644 --- a/pkgs/ui/src/app/machines/page.tsx +++ b/pkgs/ui/src/app/machines/page.tsx @@ -1,12 +1,10 @@ "use client"; import { NodeTable } from "@/components/table"; -import { StrictMode } from "react"; + export default function Page() { return ( - - - + ); } From 00ef406713edac6df7780c5f0b5a7df86b2a0fe7 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 27 Oct 2023 23:36:45 +0200 Subject: [PATCH 31/36] Updated to main branch. Removed cluttering asyncio and httpx log messages --- pkgs/clan-cli/clan_cli/__init__.py | 4 +- pkgs/clan-cli/clan_cli/async_cmd.py | 6 +- pkgs/clan-cli/clan_cli/clan_modules.py | 10 +- pkgs/clan-cli/clan_cli/config/machine.py | 152 ++++++++---------- pkgs/clan-cli/clan_cli/custom_logger.py | 4 +- pkgs/clan-cli/clan_cli/machines/create.py | 1 - .../clan_cli/webui/routers/clan_modules.py | 9 +- .../clan_cli/webui/routers/machines.py | 22 +-- pkgs/clan-cli/pyproject.toml | 2 +- pkgs/clan-cli/tests/api.py | 4 + pkgs/clan-cli/tests/temporary_dir.py | 1 - pkgs/clan-cli/tests/test_machines_api.py | 27 ++-- 12 files changed, 113 insertions(+), 129 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 8e77efc0..c7c1fbcb 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -5,7 +5,7 @@ from types import ModuleType from typing import Optional from . import config, flakes, join, machines, secrets, vms, webui -from .custom_logger import register +from .custom_logger import setup_logging from .ssh import cli as ssh_cli log = logging.getLogger(__name__) @@ -57,7 +57,7 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser: vms.register_parser(parser_vms) # if args.debug: - register(logging.DEBUG) + setup_logging(logging.DEBUG) log.debug("Debug log activated") if argcomplete: diff --git a/pkgs/clan-cli/clan_cli/async_cmd.py b/pkgs/clan-cli/clan_cli/async_cmd.py index 9befc98b..06abb8a8 100644 --- a/pkgs/clan-cli/clan_cli/async_cmd.py +++ b/pkgs/clan-cli/clan_cli/async_cmd.py @@ -4,6 +4,7 @@ import shlex from pathlib import Path from typing import Any, Callable, Coroutine, Dict, NamedTuple, Optional +from .custom_logger import get_caller from .errors import ClanError log = logging.getLogger(__name__) @@ -16,7 +17,6 @@ class CmdOut(NamedTuple): async def run(cmd: list[str], cwd: Optional[Path] = None) -> CmdOut: - log.debug(f"$: {shlex.join(cmd)}") cwd_res = None if cwd is not None: if not cwd.exists(): @@ -24,7 +24,9 @@ async def run(cmd: list[str], cwd: Optional[Path] = None) -> CmdOut: if not cwd.is_dir(): raise ClanError(f"Working directory {cwd} is not a directory") cwd_res = cwd.resolve() - log.debug(f"Working directory: {cwd_res}") + log.debug( + f"Command: {shlex.join(cmd)}\nWorking directory: {cwd_res}\nCaller : {get_caller()}" + ) proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, diff --git a/pkgs/clan-cli/clan_cli/clan_modules.py b/pkgs/clan-cli/clan_cli/clan_modules.py index 926feeb0..54d0900d 100644 --- a/pkgs/clan-cli/clan_cli/clan_modules.py +++ b/pkgs/clan-cli/clan_cli/clan_modules.py @@ -1,20 +1,20 @@ import json import subprocess -from pathlib import Path from typing import Optional -from clan_cli.dirs import get_clan_flake_toplevel from clan_cli.nix import nix_eval +from .dirs import specific_flake_dir +from .types import FlakeName + def get_clan_module_names( - flake: Optional[Path] = None, + flake_name: FlakeName, ) -> tuple[list[str], Optional[str]]: """ Get the list of clan modules from the clan-core flake input """ - if flake is None: - flake = get_clan_flake_toplevel() + flake = specific_flake_dir(flake_name) proc = subprocess.run( nix_eval( [ diff --git a/pkgs/clan-cli/clan_cli/config/machine.py b/pkgs/clan-cli/clan_cli/config/machine.py index b04c87b8..c7433f7a 100644 --- a/pkgs/clan-cli/clan_cli/config/machine.py +++ b/pkgs/clan-cli/clan_cli/config/machine.py @@ -3,6 +3,8 @@ import os import subprocess import sys from pathlib import Path +from tempfile import NamedTemporaryFile +from typing import Optional from fastapi import HTTPException @@ -19,31 +21,35 @@ from ..types import FlakeName def verify_machine_config( - machine_name: str, config: Optional[dict] = None, flake: Optional[Path] = None + flake_name: FlakeName, + machine_name: str, + config: Optional[dict] = None, + flake: Optional[Path] = None, ) -> Optional[str]: """ Verify that the machine evaluates successfully Returns a tuple of (success, error_message) """ if config is None: - config = config_for_machine(machine_name) - if flake is None: - flake = get_clan_flake_toplevel() - with NamedTemporaryFile(mode="w") as clan_machine_settings_file: + config = config_for_machine(flake_name, machine_name) + flake = specific_flake_dir(flake_name) + 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=[ + "--impure", + "--show-trace", + "--show-trace", + "--impure", # needed to access CLAN_MACHINE_SETTINGS_FILE + f".#nixosConfigurations.{machine_name}.config.system.build.toplevel.outPath", + ], + ) + # repro_env_break(work_dir=flake, env=env, cmd=cmd) proc = subprocess.run( - nix_eval( - flags=[ - "--impure", - "--show-trace", - "--show-trace", - "--impure", # needed to access CLAN_MACHINE_SETTINGS_FILE - f".#nixosConfigurations.{machine_name}.config.system.build.toplevel.outPath", - ], - ), + cmd, capture_output=True, text=True, cwd=flake, @@ -54,7 +60,6 @@ def verify_machine_config( return None - def config_for_machine(flake_name: FlakeName, 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_name, machine_name).exists(): @@ -88,76 +93,49 @@ def set_config_for_machine( commit_file(settings_path, repo_dir) -def schema_for_machine(flake_name: FlakeName, machine_name: str) -> dict: +def schema_for_machine( + flake_name: FlakeName, machine_name: str, config: Optional[dict] = None +) -> dict: flake = specific_flake_dir(flake_name) - - # use nix eval to lib.evalModules .#nixosModules.machine-{machine_name} - proc = subprocess.run( - nix_eval( - flags=[ - "--impure", - "--show-trace", - "--expr", - f""" - let - flake = builtins.getFlake (toString {flake}); - lib = import {nixpkgs_source()}/lib; - options = flake.nixosConfigurations.{machine_name}.options; - clanOptions = options.clan; - jsonschemaLib = import {Path(__file__).parent / "jsonschema"} {{ inherit lib; }}; - jsonschema = jsonschemaLib.parseOptions clanOptions; - in - jsonschema - """, - ], - ), - capture_output=True, - text=True, - ) -# def schema_for_machine( -# machine_name: str, config: Optional[dict] = None, flake: Optional[Path] = None -# ) -> dict: -# if flake is None: -# flake = get_clan_flake_toplevel() -# # use nix eval to lib.evalModules .#nixosConfigurations..options.clan -# with NamedTemporaryFile(mode="w") as clan_machine_settings_file: -# env = os.environ.copy() -# inject_config_flags = [] -# if config is not None: -# json.dump(config, clan_machine_settings_file, indent=2) -# clan_machine_settings_file.seek(0) -# env["CLAN_MACHINE_SETTINGS_FILE"] = clan_machine_settings_file.name -# inject_config_flags = [ -# "--impure", # needed to access CLAN_MACHINE_SETTINGS_FILE -# ] -# proc = subprocess.run( -# nix_eval( -# flags=inject_config_flags -# + [ -# "--impure", -# "--show-trace", -# "--expr", -# f""" -# let -# flake = builtins.getFlake (toString {flake}); -# lib = import {nixpkgs_source()}/lib; -# options = flake.nixosConfigurations.{machine_name}.options; -# clanOptions = options.clan; -# jsonschemaLib = import {Path(__file__).parent / "jsonschema"} {{ inherit lib; }}; -# jsonschema = jsonschemaLib.parseOptions clanOptions; -# in -# jsonschema -# """, -# ], -# ), -# capture_output=True, -# text=True, -# cwd=flake, -# env=env, -# ) -# if proc.returncode != 0: -# print(proc.stderr, file=sys.stderr) -# raise Exception( -# f"Failed to read schema for machine {machine_name}:\n{proc.stderr}" -# ) -# return json.loads(proc.stdout) + # use nix eval to lib.evalModules .#nixosConfigurations..options.clan + with NamedTemporaryFile(mode="w", dir=flake) as clan_machine_settings_file: + env = os.environ.copy() + inject_config_flags = [] + if config is not None: + json.dump(config, clan_machine_settings_file, indent=2) + clan_machine_settings_file.seek(0) + env["CLAN_MACHINE_SETTINGS_FILE"] = clan_machine_settings_file.name + inject_config_flags = [ + "--impure", # needed to access CLAN_MACHINE_SETTINGS_FILE + ] + proc = subprocess.run( + nix_eval( + flags=inject_config_flags + + [ + "--impure", + "--show-trace", + "--expr", + f""" + let + flake = builtins.getFlake (toString {flake}); + lib = import {nixpkgs_source()}/lib; + options = flake.nixosConfigurations.{machine_name}.options; + clanOptions = options.clan; + jsonschemaLib = import {Path(__file__).parent / "jsonschema"} {{ inherit lib; }}; + jsonschema = jsonschemaLib.parseOptions clanOptions; + in + jsonschema + """, + ], + ), + capture_output=True, + text=True, + cwd=flake, + env=env, + ) + if proc.returncode != 0: + print(proc.stderr, file=sys.stderr) + raise Exception( + f"Failed to read schema for machine {machine_name}:\n{proc.stderr}" + ) + return json.loads(proc.stdout) diff --git a/pkgs/clan-cli/clan_cli/custom_logger.py b/pkgs/clan-cli/clan_cli/custom_logger.py index f9f324e1..8c91a10a 100644 --- a/pkgs/clan-cli/clan_cli/custom_logger.py +++ b/pkgs/clan-cli/clan_cli/custom_logger.py @@ -61,10 +61,12 @@ def get_caller() -> str: return ret -def register(level: Any) -> None: +def setup_logging(level: Any) -> None: handler = logging.StreamHandler() handler.setLevel(level) handler.setFormatter(CustomFormatter()) logger = logging.getLogger("registerHandler") + logging.getLogger("asyncio").setLevel(logging.INFO) + logging.getLogger("httpx").setLevel(level=logging.WARNING) logger.addHandler(handler) # logging.basicConfig(level=level, handlers=[handler]) diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py index 9a2e39be..1c5810e3 100644 --- a/pkgs/clan-cli/clan_cli/machines/create.py +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -32,7 +32,6 @@ async def create_machine(flake_name: FlakeName, machine_name: str) -> Dict[str, cwd=folder, ) response["git commit"] = out - return response diff --git a/pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py b/pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py index fe97e282..1721546f 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py @@ -4,8 +4,9 @@ import logging from fastapi import APIRouter, HTTPException from clan_cli.clan_modules import get_clan_module_names +from clan_cli.types import FlakeName -from ..schemas import ( +from ..api_outputs import ( ClanModulesResponse, ) @@ -13,9 +14,9 @@ log = logging.getLogger(__name__) router = APIRouter() -@router.get("/api/clan_modules") -async def list_clan_modules() -> ClanModulesResponse: - module_names, error = get_clan_module_names() +@router.get("/api/{flake_name}clan_modules") +async def list_clan_modules(flake_name: FlakeName) -> ClanModulesResponse: + module_names, error = get_clan_module_names(flake_name) if error is not None: raise HTTPException(status_code=400, detail=error) return ClanModulesResponse(clan_modules=module_names) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index b636a800..3c2dffd9 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -41,8 +41,7 @@ async def list_machines(flake_name: FlakeName) -> MachinesResponse: async def create_machine( flake_name: FlakeName, machine: Annotated[MachineCreate, Body()] ) -> MachineResponse: - out = await _create_machine(flake_name, machine.name) - log.debug(out) + await _create_machine(flake_name, machine.name) return MachineResponse(machine=Machine(name=machine.name, status=Status.UNKNOWN)) @@ -72,26 +71,29 @@ async def get_machine_schema(flake_name: FlakeName, name: str) -> SchemaResponse return SchemaResponse(schema=schema) -@router.put("/api/machines/{name}/schema") +@router.put("/api/{flake_name}/machines/{name}/schema") async def set_machine_schema( - name: str, config: Annotated[dict, Body()] + flake_name: FlakeName, name: str, config: Annotated[dict, Body()] ) -> SchemaResponse: - schema = schema_for_machine(name, config) + schema = schema_for_machine(flake_name, name, config) return SchemaResponse(schema=schema) -@router.get("/api/machines/{name}/verify") -async def get_verify_machine_config(name: str) -> VerifyMachineResponse: - error = verify_machine_config(name) +@router.get("/api/{flake_name}/machines/{name}/verify") +async def get_verify_machine_config( + flake_name: FlakeName, name: str +) -> VerifyMachineResponse: + error = verify_machine_config(flake_name, name) success = error is None return VerifyMachineResponse(success=success, error=error) -@router.put("/api/machines/{name}/verify") +@router.put("/api/{flake_name}/machines/{name}/verify") async def put_verify_machine_config( + flake_name: FlakeName, name: str, config: Annotated[dict, Body()], ) -> VerifyMachineResponse: - error = verify_machine_config(name, config) + error = verify_machine_config(flake_name, name, config) success = error is None return VerifyMachineResponse(success=success, error=error) diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index 7d61dd8e..d0f2d142 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -20,7 +20,7 @@ clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"] testpaths = "tests" faulthandler_timeout = 60 log_level = "DEBUG" -log_format = "%(levelname)s: %(message)s" +log_format = "%(levelname)s: %(message)s\n %(pathname)s:%(lineno)d::%(funcName)s" addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --new-first --maxfail=1" # Add --pdb for debugging norecursedirs = "tests/helpers" markers = [ "impure" ] diff --git a/pkgs/clan-cli/tests/api.py b/pkgs/clan-cli/tests/api.py index 78d35e98..8f7ff192 100644 --- a/pkgs/clan-cli/tests/api.py +++ b/pkgs/clan-cli/tests/api.py @@ -1,3 +1,5 @@ +import logging + import pytest from fastapi.testclient import TestClient @@ -7,4 +9,6 @@ from clan_cli.webui.app import app # TODO: Why stateful @pytest.fixture(scope="session") def api() -> TestClient: + logging.getLogger("httpx").setLevel(level=logging.WARNING) + logging.getLogger("asyncio").setLevel(logging.INFO) return TestClient(app) diff --git a/pkgs/clan-cli/tests/temporary_dir.py b/pkgs/clan-cli/tests/temporary_dir.py index e7cbfa0d..0b16c3fc 100644 --- a/pkgs/clan-cli/tests/temporary_dir.py +++ b/pkgs/clan-cli/tests/temporary_dir.py @@ -19,7 +19,6 @@ def temporary_home(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]: monkeypatch.chdir(str(path)) yield path else: - log.debug("TEST_TEMPORARY_DIR not set, using TemporaryDirectory") with tempfile.TemporaryDirectory(prefix="pytest-") as dirpath: monkeypatch.setenv("HOME", str(dirpath)) monkeypatch.chdir(str(dirpath)) diff --git a/pkgs/clan-cli/tests/test_machines_api.py b/pkgs/clan-cli/tests/test_machines_api.py index 1249949d..8a9fe0ed 100644 --- a/pkgs/clan-cli/tests/test_machines_api.py +++ b/pkgs/clan-cli/tests/test_machines_api.py @@ -56,7 +56,7 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: # verify an invalid config (fileSystems missing) fails response = api.put( - "/api/machines/machine1/verify", + f"/api/{test_flake.name}/machines/machine1/verify", json=invalid_config, ) assert response.status_code == 200 @@ -67,13 +67,13 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: # set come invalid config (fileSystems missing) response = api.put( - "/api/machines/machine1/config", + f"/api/{test_flake.name}/machines/machine1/config", json=invalid_config, ) assert response.status_code == 200 # ensure the config has actually been updated - response = api.get("/api/machines/machine1/config") + response = api.get(f"/api/{test_flake.name}/machines/machine1/config") assert response.status_code == 200 assert response.json() == {"config": invalid_config} @@ -91,13 +91,8 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: devices=["/dev/fake_disk"], ), ), - - json=dict( - clan=dict( - jitsi=True, - ) ), - )) + ) # set some valid config config2 = dict( @@ -108,8 +103,9 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: ), **fs_config, ) + response = api.put( - "/api/machines/machine1/config", + f"/api/{test_flake.name}/machines/machine1/config", json=config2, ) assert response.status_code == 200 @@ -124,20 +120,21 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: # For example, this should not result in the boot.loader.grub.devices being # set twice (eg. merged) response = api.put( - "/api/machines/machine1/config", + f"/api/{test_flake.name}/machines/machine1/config", json=config2, ) assert response.status_code == 200 assert response.json() == {"config": config2} # verify the machine config evaluates - response = api.get("/api/machines/machine1/verify") + response = api.get(f"/api/{test_flake.name}/machines/machine1/verify") assert response.status_code == 200 + assert response.json() == {"success": True, "error": None} # get the schema with an extra module imported response = api.put( - "/api/machines/machine1/schema", + f"/api/{test_flake.name}/machines/machine1/schema", json={"clanImports": ["fake-module"]}, ) # expect the result schema to contain the fake-module.fake-flag option @@ -162,7 +159,7 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: # set the fake-module.fake-flag option to true response = api.put( - "/api/machines/machine1/config", + f"/api/{test_flake.name}/machines/machine1/config", json=config_with_imports, ) assert response.status_code == 200 @@ -184,7 +181,7 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: **fs_config, ) response = api.put( - "/api/machines/machine1/config", + f"/api/{test_flake.name}/machines/machine1/config", json=config_with_empty_imports, ) assert response.status_code == 200 From 032cdd731ae5b32c4f510d4ed2bb1ddcce765b87 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 27 Oct 2023 23:39:02 +0200 Subject: [PATCH 32/36] Fixing test_clan_modules test --- pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py | 2 +- pkgs/clan-cli/tests/test_clan_modules.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py b/pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py index 1721546f..d3835194 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py @@ -14,7 +14,7 @@ log = logging.getLogger(__name__) router = APIRouter() -@router.get("/api/{flake_name}clan_modules") +@router.get("/api/{flake_name}/clan_modules") async def list_clan_modules(flake_name: FlakeName) -> ClanModulesResponse: module_names, error = get_clan_module_names(flake_name) if error is not None: diff --git a/pkgs/clan-cli/tests/test_clan_modules.py b/pkgs/clan-cli/tests/test_clan_modules.py index 374fa4df..6fa6d64b 100644 --- a/pkgs/clan-cli/tests/test_clan_modules.py +++ b/pkgs/clan-cli/tests/test_clan_modules.py @@ -2,12 +2,12 @@ from pathlib import Path import pytest from api import TestClient - +from fixtures_flakes import FlakeForTest @pytest.mark.impure() -def test_configure_machine(api: TestClient, test_flake_with_core: Path) -> None: +def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest) -> None: # retrieve the list of available clanModules - response = api.get("/api/clan_modules") + response = api.get(f"/api/{test_flake_with_core.name}/clan_modules") response_json = response.json() assert response.status_code == 200 assert isinstance(response_json, dict) From 5c9f826a23a7f118aa38a03546047f1841aea41b Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 28 Oct 2023 18:31:39 +0200 Subject: [PATCH 33/36] fix frontend --- pkgs/ui/nix/pdefs.nix | 2 +- pkgs/ui/src/app/machines/layout.tsx | 7 ++++- pkgs/ui/src/app/machines/page.tsx | 5 +--- .../createMachineForm/customConfig.tsx | 11 +++++--- .../components/createMachineForm/index.tsx | 2 +- .../ui/src/components/hooks/useAppContext.tsx | 4 +-- pkgs/ui/src/components/hooks/useDebounce.tsx | 6 ++--- pkgs/ui/src/components/hooks/useMachines.tsx | 4 +-- pkgs/ui/src/components/table/nodeTable.tsx | 2 +- pkgs/ui/src/components/table/searchBar.tsx | 12 ++++----- pkgs/ui/src/data/nodeData.tsx | 26 +++++++++++-------- pkgs/ui/src/views/joinPrequel.tsx | 7 ++--- 12 files changed, 49 insertions(+), 39 deletions(-) diff --git a/pkgs/ui/nix/pdefs.nix b/pkgs/ui/nix/pdefs.nix index 9bae9397..c7a2b748 100644 --- a/pkgs/ui/nix/pdefs.nix +++ b/pkgs/ui/nix/pdefs.nix @@ -18438,4 +18438,4 @@ }; }; }; -} \ No newline at end of file +} diff --git a/pkgs/ui/src/app/machines/layout.tsx b/pkgs/ui/src/app/machines/layout.tsx index eace0be3..e9db5e3f 100644 --- a/pkgs/ui/src/app/machines/layout.tsx +++ b/pkgs/ui/src/app/machines/layout.tsx @@ -1,5 +1,10 @@ import { MachineContextProvider } from "@/components/hooks/useMachines"; export default function Layout({ children }: { children: React.ReactNode }) { - return {children}; + return ( + // TODO: select flake? + + {children} + + ); } diff --git a/pkgs/ui/src/app/machines/page.tsx b/pkgs/ui/src/app/machines/page.tsx index 9c89d605..b1d7b442 100644 --- a/pkgs/ui/src/app/machines/page.tsx +++ b/pkgs/ui/src/app/machines/page.tsx @@ -2,9 +2,6 @@ import { NodeTable } from "@/components/table"; - export default function Page() { - return ( - - ); + return ; } diff --git a/pkgs/ui/src/components/createMachineForm/customConfig.tsx b/pkgs/ui/src/components/createMachineForm/customConfig.tsx index acd9441d..cc14a953 100644 --- a/pkgs/ui/src/components/createMachineForm/customConfig.tsx +++ b/pkgs/ui/src/components/createMachineForm/customConfig.tsx @@ -33,7 +33,10 @@ interface PureCustomConfigProps extends FormStepContentProps { } export function CustomConfig(props: FormStepContentProps) { const { formHooks } = props; - const { data, isLoading, error } = useGetMachineSchema("mama"); + const { data, isLoading, error } = useGetMachineSchema( + "defaultFlake", + "mama", + ); // const { data, isLoading, error } = { data: {data:{schema: { // title: 'Test form', // type: 'object', @@ -53,11 +56,11 @@ export function CustomConfig(props: FormStepContentProps) { return {}; }, [data, isLoading, error]); + type ValueType = { default: any }; const initialValues = useMemo( () => Object.entries(schema?.properties || {}).reduce((acc, [key, value]) => { - /*@ts-ignore*/ - const init: any = value?.default; + const init: any = (value as ValueType)?.default; if (init) { return { ...acc, @@ -157,7 +160,7 @@ function PureCustomConfig(props: PureCustomConfigProps) { // ObjectFieldTemplate: ErrorListTemplate: ErrorList, ButtonTemplates: { - SubmitButton: (props) => ( + SubmitButton: () => (