From b49433958b1f8101510c8068b54e22afd4769dd8 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Mon, 9 Oct 2023 14:01:34 +0200 Subject: [PATCH] API: Added /api/flake/create. Fixed vscode search settings. Moved clan create to clan flake create --- .gitignore | 1 + pkgs/clan-cli/.vscode/settings.json | 3 ++ pkgs/clan-cli/clan_cli/__init__.py | 8 ++-- pkgs/clan-cli/clan_cli/async_cmd.py | 15 +++++- pkgs/clan-cli/clan_cli/create.py | 25 ---------- pkgs/clan-cli/clan_cli/flake/__init__.py | 16 +++++++ pkgs/clan-cli/clan_cli/flake/create.py | 47 +++++++++++++++++++ pkgs/clan-cli/clan_cli/task_manager.py | 21 ++++++++- pkgs/clan-cli/clan_cli/vms/inspect.py | 2 +- pkgs/clan-cli/clan_cli/webui/routers/flake.py | 26 ++++++++-- pkgs/clan-cli/clan_cli/webui/schemas.py | 4 ++ pkgs/clan-cli/clan_cli/webui/server.py | 2 + pkgs/clan-cli/tests/test_clan_template.py | 12 ----- pkgs/clan-cli/tests/test_create_flake.py | 27 ++++++++++- 14 files changed, 157 insertions(+), 52 deletions(-) delete mode 100644 pkgs/clan-cli/clan_cli/create.py create mode 100644 pkgs/clan-cli/clan_cli/flake/__init__.py create mode 100644 pkgs/clan-cli/clan_cli/flake/create.py delete mode 100644 pkgs/clan-cli/tests/test_clan_template.py diff --git a/.gitignore b/.gitignore index e40f14cc..9d0c58f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .direnv +**/testdir democlan result* /pkgs/clan-cli/clan_cli/nixpkgs diff --git a/pkgs/clan-cli/.vscode/settings.json b/pkgs/clan-cli/.vscode/settings.json index 66a301ca..f1d83ff3 100644 --- a/pkgs/clan-cli/.vscode/settings.json +++ b/pkgs/clan-cli/.vscode/settings.json @@ -12,4 +12,7 @@ ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, + "search.exclude": { + "**/.direnv": true + }, } \ No newline at end of file diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 5e26886d..6f205736 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, create, join, machines, secrets, vms, webui +from . import config, flake, join, machines, secrets, vms, webui from .ssh import cli as ssh_cli argcomplete: Optional[ModuleType] = None @@ -24,10 +24,10 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser: subparsers = parser.add_subparsers() - parser_create = subparsers.add_parser( - "create", help="create a clan flake inside the current directory" + parser_flake = subparsers.add_parser( + "flake", help="create a clan flake inside the current directory" ) - create.register_parser(parser_create) + flake.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 e9e2dba5..d23e61a9 100644 --- a/pkgs/clan-cli/clan_cli/async_cmd.py +++ b/pkgs/clan-cli/clan_cli/async_cmd.py @@ -1,18 +1,29 @@ import asyncio import logging import shlex +from pathlib import Path +from typing import Optional, Tuple from .errors import ClanError log = logging.getLogger(__name__) -async def run(cmd: list[str]) -> bytes: +async def run(cmd: list[str], cwd: Optional[Path] = None) -> Tuple[bytes, bytes]: log.debug(f"$: {shlex.join(cmd)}") + cwd_res = None + if cwd is not None: + if not cwd.exists(): + raise ClanError(f"Working directory {cwd} does not exist") + 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}") proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + cwd=cwd_res, ) stdout, stderr = await proc.communicate() @@ -25,4 +36,4 @@ command output: {stderr.decode("utf-8")} """ ) - return stdout + return stdout, stderr diff --git a/pkgs/clan-cli/clan_cli/create.py b/pkgs/clan-cli/clan_cli/create.py deleted file mode 100644 index 2a6b3ce8..00000000 --- a/pkgs/clan-cli/clan_cli/create.py +++ /dev/null @@ -1,25 +0,0 @@ -# !/usr/bin/env python3 -import argparse -import subprocess - -from .nix import nix_command - - -def create(args: argparse.Namespace) -> None: - # TODO create clan template in flake - subprocess.run( - nix_command( - [ - "flake", - "init", - "-t", - "git+https://git.clan.lol/clan/clan-core#new-clan", - ] - ), - check=True, - ) - - -# takes a (sub)parser and configures it -def register_parser(parser: argparse.ArgumentParser) -> None: - parser.set_defaults(func=create) diff --git a/pkgs/clan-cli/clan_cli/flake/__init__.py b/pkgs/clan-cli/clan_cli/flake/__init__.py new file mode 100644 index 00000000..8756e3c8 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/flake/__init__.py @@ -0,0 +1,16 @@ +# !/usr/bin/env python3 +import argparse + +from .create import register_create_parser + + +# takes a (sub)parser and configures it +def register_parser(parser: argparse.ArgumentParser) -> None: + subparser = parser.add_subparsers( + title="command", + description="the command to run", + help="the command to run", + required=True, + ) + update_parser = subparser.add_parser("create", help="Create a clan flake") + register_create_parser(update_parser) diff --git a/pkgs/clan-cli/clan_cli/flake/create.py b/pkgs/clan-cli/clan_cli/flake/create.py new file mode 100644 index 00000000..45970422 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/flake/create.py @@ -0,0 +1,47 @@ +# !/usr/bin/env python3 +import argparse +import asyncio +from pathlib import Path +from typing import Tuple + +from ..async_cmd import run +from ..errors import ClanError +from ..nix import nix_command + +DEFAULT_URL = "git+https://git.clan.lol/clan/clan-core#new-clan" + + +async def create_flake(directory: Path, url: str) -> Tuple[bytes, bytes]: + if not directory.exists(): + directory.mkdir() + flake_command = nix_command( + [ + "flake", + "init", + "-t", + url, + ] + ) + stdout, stderr = await run(flake_command, directory) + return stdout, stderr + + +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="") + except ClanError as e: + print(e) + exit(1) + + +# 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", + ) + # 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/task_manager.py b/pkgs/clan-cli/clan_cli/task_manager.py index 65c03dba..67fdabc5 100644 --- a/pkgs/clan-cli/clan_cli/task_manager.py +++ b/pkgs/clan-cli/clan_cli/task_manager.py @@ -8,6 +8,7 @@ import sys import threading import traceback from enum import Enum +from pathlib import Path from typing import Any, Iterator, Optional, Type, TypeVar from uuid import UUID, uuid4 @@ -30,14 +31,30 @@ class Command: self._output.put(None) self.done = True - def run(self, cmd: list[str], env: Optional[dict[str, str]] = None) -> None: + def run( + self, + cmd: list[str], + env: Optional[dict[str, str]] = None, + cwd: Optional[Path] = None, + ) -> None: self.running = True self.log.debug(f"Running command: {shlex.join(cmd)}") + + cwd_res = None + if cwd is not None: + if not cwd.exists(): + raise ClanError(f"Working directory {cwd} does not exist") + if not cwd.is_dir(): + raise ClanError(f"Working directory {cwd} is not a directory") + cwd_res = cwd.resolve() + self.log.debug(f"Working directory: {cwd_res}") + self.p = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", + cwd=cwd_res, env=env, ) assert self.p.stdout is not None and self.p.stderr is not None @@ -106,7 +123,7 @@ class BaseTask: def run(self) -> None: raise NotImplementedError - ## TODO: If two clients are connected to the same task, + ## TODO: Test when two clients are connected to the same task def log_lines(self) -> Iterator[str]: with self.logs_lock: for proc in self.procs: diff --git a/pkgs/clan-cli/clan_cli/vms/inspect.py b/pkgs/clan-cli/clan_cli/vms/inspect.py index 83230add..a68ded23 100644 --- a/pkgs/clan-cli/clan_cli/vms/inspect.py +++ b/pkgs/clan-cli/clan_cli/vms/inspect.py @@ -26,7 +26,7 @@ async def inspect_vm(flake_url: str, flake_attr: str) -> VmConfig: f'{flake_url}#clanInternals.machines."{system}"."{flake_attr}".config.system.clan.vm.config' ] ) - stdout = await run(cmd) + stdout, stderr = await run(cmd) data = json.loads(stdout) return VmConfig(flake_url=flake_url, flake_attr=flake_attr, **data) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/flake.py b/pkgs/clan-cli/clan_cli/webui/routers/flake.py index e1361284..68ebffe5 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/flake.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/flake.py @@ -1,12 +1,18 @@ import json from json.decoder import JSONDecodeError from pathlib import Path +from typing import Annotated -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, Body, HTTPException, Response, status -from clan_cli.webui.schemas import FlakeAction, FlakeAttrResponse, FlakeResponse +from clan_cli.webui.schemas import ( + FlakeAction, + FlakeAttrResponse, + FlakeResponse, +) from ...async_cmd import run +from ...flake import create from ...nix import nix_command, nix_flake_show router = APIRouter() @@ -14,7 +20,7 @@ router = APIRouter() async def get_attrs(url: str) -> list[str]: cmd = nix_flake_show(url) - stdout = await run(cmd) + stdout, stderr = await run(cmd) data: dict[str, dict] = {} try: @@ -45,7 +51,7 @@ 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", url, "--json", "--refresh"]) - stdout = await run(cmd) + stdout, stderr = await run(cmd) data: dict[str, str] = json.loads(stdout) if data.get("storePath") is None: @@ -60,3 +66,15 @@ async def inspect_flake( actions.append(FlakeAction(id="vms/create", uri="api/vms/create")) return FlakeResponse(content=content, actions=actions) + + +@router.post("/api/flake/create") +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 diff --git a/pkgs/clan-cli/clan_cli/webui/schemas.py b/pkgs/clan-cli/clan_cli/webui/schemas.py index 85750e58..c53636c5 100644 --- a/pkgs/clan-cli/clan_cli/webui/schemas.py +++ b/pkgs/clan-cli/clan_cli/webui/schemas.py @@ -60,6 +60,10 @@ class FlakeAction(BaseModel): uri: str +class FlakeCreateResponse(BaseModel): + uuid: str + + class FlakeResponse(BaseModel): content: str actions: List[FlakeAction] diff --git a/pkgs/clan-cli/clan_cli/webui/server.py b/pkgs/clan-cli/clan_cli/webui/server.py index 3b82698c..fb9fbf8a 100644 --- a/pkgs/clan-cli/clan_cli/webui/server.py +++ b/pkgs/clan-cli/clan_cli/webui/server.py @@ -31,6 +31,8 @@ def open_browser(base_url: str, sub_url: str) -> None: def _open_browser(url: str) -> 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 + # the -kiosk flag. cmd = [ browser, "-kiosk", diff --git a/pkgs/clan-cli/tests/test_clan_template.py b/pkgs/clan-cli/tests/test_clan_template.py deleted file mode 100644 index 10b4a9b3..00000000 --- a/pkgs/clan-cli/tests/test_clan_template.py +++ /dev/null @@ -1,12 +0,0 @@ -from pathlib import Path - -import pytest -from cli import Cli - - -@pytest.mark.impure -def test_template(monkeypatch: pytest.MonkeyPatch, temporary_dir: Path) -> None: - monkeypatch.chdir(temporary_dir) - cli = Cli() - cli.run(["create"]) - assert (temporary_dir / ".clan-flake").exists() diff --git a/pkgs/clan-cli/tests/test_create_flake.py b/pkgs/clan-cli/tests/test_create_flake.py index 8eece684..07ced09d 100644 --- a/pkgs/clan-cli/tests/test_create_flake.py +++ b/pkgs/clan-cli/tests/test_create_flake.py @@ -3,6 +3,7 @@ import subprocess from pathlib import Path import pytest +from api import TestClient from cli import Cli @@ -11,6 +12,25 @@ def cli() -> Cli: return Cli() +@pytest.mark.impure +def test_create_flake_api( + monkeypatch: pytest.MonkeyPatch, api: TestClient, temporary_dir: Path +) -> None: + flake_dir = temporary_dir / "flake_dir" + flake_dir_str = str(flake_dir.resolve()) + response = api.post( + "/api/flake/create", + json=dict( + destination=flake_dir_str, + url="git+https://git.clan.lol/clan/clan-core#new-clan", + ), + ) + + assert response.status_code == 201, "Failed to create flake" + assert (flake_dir / ".clan-flake").exists() + assert (flake_dir / "flake.nix").exists() + + @pytest.mark.impure def test_create_flake( monkeypatch: pytest.MonkeyPatch, @@ -19,8 +39,11 @@ def test_create_flake( cli: Cli, ) -> None: monkeypatch.chdir(temporary_dir) - cli.run(["create"]) - assert (temporary_dir / ".clan-flake").exists() + flake_dir = temporary_dir / "flake_dir" + flake_dir_str = str(flake_dir.resolve()) + cli.run(["flake", "create", flake_dir_str]) + assert (flake_dir / ".clan-flake").exists() + monkeypatch.chdir(flake_dir) cli.run(["machines", "create", "machine1"]) capsys.readouterr() # flush cache cli.run(["machines", "list"])