From 912d6428a38ff43d142cef79f515df075f6358a5 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Sun, 1 Oct 2023 12:45:01 +0200 Subject: [PATCH] API: Added endpoint & test for /api/flake/attrs --- pkgs/clan-cli/clan_cli/nix.py | 12 ++++ pkgs/clan-cli/clan_cli/webui/app.py | 6 +- pkgs/clan-cli/clan_cli/webui/routers/flake.py | 30 +++++----- pkgs/clan-cli/clan_cli/webui/routers/utils.py | 54 ++++++++++++++++++ pkgs/clan-cli/clan_cli/webui/routers/vms.py | 55 ++----------------- pkgs/clan-cli/clan_cli/webui/schemas.py | 4 ++ pkgs/clan-cli/tests/test_flake_api.py | 17 ++++++ 7 files changed, 110 insertions(+), 68 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/webui/routers/utils.py create mode 100644 pkgs/clan-cli/tests/test_flake_api.py diff --git a/pkgs/clan-cli/clan_cli/nix.py b/pkgs/clan-cli/clan_cli/nix.py index 247183348..4ebf59b6a 100644 --- a/pkgs/clan-cli/clan_cli/nix.py +++ b/pkgs/clan-cli/clan_cli/nix.py @@ -11,6 +11,18 @@ 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]: + return nix_command( + [ + "flake", + "show", + "--json", + "--show-trace", + f"{flake_url}", + ] + ) + + def nix_build( flags: list[str], ) -> list[str]: diff --git a/pkgs/clan-cli/clan_cli/webui/app.py b/pkgs/clan-cli/clan_cli/webui/app.py index ca1a4d4fd..b3efaa603 100644 --- a/pkgs/clan-cli/clan_cli/webui/app.py +++ b/pkgs/clan-cli/clan_cli/webui/app.py @@ -7,7 +7,7 @@ from fastapi.staticfiles import StaticFiles from .. import custom_logger from .assets import asset_path -from .routers import flake, health, machines, root, vms +from .routers import flake, health, machines, root, utils, vms origins = [ "http://localhost:3000", @@ -33,7 +33,9 @@ def setup_app() -> FastAPI: # Needs to be last in register. Because of wildcard route app.include_router(root.router) - app.add_exception_handler(vms.NixBuildException, vms.nix_build_exception_handler) + app.add_exception_handler( + utils.NixBuildException, utils.nix_build_exception_handler + ) app.mount("/static", StaticFiles(directory=asset_path()), name="static") diff --git a/pkgs/clan-cli/clan_cli/webui/routers/flake.py b/pkgs/clan-cli/clan_cli/webui/routers/flake.py index bbb2158d9..09541803f 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/flake.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/flake.py @@ -1,16 +1,26 @@ -import asyncio import json from pathlib import Path -from fastapi import APIRouter, HTTPException, status +from fastapi import APIRouter, HTTPException -from clan_cli.webui.schemas import FlakeAction, FlakeResponse +from clan_cli.webui.schemas import FlakeAction, FlakeAttrResponse, FlakeResponse -from ...nix import nix_command +from ...nix import nix_command, nix_flake_show +from .utils import run_cmd router = APIRouter() +@router.get("/api/flake/attrs") +async def inspect_flake_attrs(url: str) -> FlakeAttrResponse: + cmd = nix_flake_show(url) + stdout = await run_cmd(cmd) + data = json.loads(stdout) + nixos_configs = data["nixosConfigurations"] + flake_attrs = list(nixos_configs.keys()) + return FlakeAttrResponse(flake_attrs=flake_attrs) + + @router.get("/api/flake") async def inspect_flake( url: str, @@ -19,17 +29,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"]) - proc = await asyncio.create_subprocess_exec( - cmd[0], - *cmd[1:], - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await proc.communicate() - - if proc.returncode != 0: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(stderr)) - + stdout = await run_cmd(cmd) data: dict[str, str] = json.loads(stdout) if data.get("storePath") is None: diff --git a/pkgs/clan-cli/clan_cli/webui/routers/utils.py b/pkgs/clan-cli/clan_cli/webui/routers/utils.py new file mode 100644 index 000000000..dff71d245 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/webui/routers/utils.py @@ -0,0 +1,54 @@ +import asyncio +import logging +import shlex + +from fastapi import HTTPException, Request, status +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse + +log = logging.getLogger(__name__) + + +class NixBuildException(HTTPException): + def __init__(self, msg: str, loc: list = ["body", "flake_attr"]): + detail = [ + { + "loc": loc, + "msg": msg, + "type": "value_error", + } + ] + super().__init__( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail + ) + + +def nix_build_exception_handler( + request: Request, exc: NixBuildException +) -> JSONResponse: + log.error("NixBuildException: %s", exc) + return JSONResponse( + status_code=exc.status_code, + content=jsonable_encoder(dict(detail=exc.detail)), + ) + + +async def run_cmd(cmd: list[str]) -> bytes: + log.debug(f"Running command: {shlex.join(cmd)}") + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate() + + if proc.returncode != 0: + raise NixBuildException( + f""" +command: {shlex.join(cmd)} +exit code: {proc.returncode} +command output: +{stderr.decode("utf-8")} +""" + ) + return stdout diff --git a/pkgs/clan-cli/clan_cli/webui/routers/vms.py b/pkgs/clan-cli/clan_cli/webui/routers/vms.py index 0e77d5b18..3011c32c5 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/vms.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/vms.py @@ -1,17 +1,15 @@ -import asyncio import json import logging -import shlex from typing import Annotated, Iterator from uuid import UUID -from fastapi import APIRouter, BackgroundTasks, Body, HTTPException, Request, status -from fastapi.encoders import jsonable_encoder -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi import APIRouter, BackgroundTasks, Body +from fastapi.responses import StreamingResponse from ...nix import nix_build, nix_eval from ..schemas import VmConfig, VmCreateResponse, VmInspectResponse, VmStatusResponse from ..task_manager import BaseTask, get_task, register_task +from .utils import run_cmd log = logging.getLogger(__name__) router = APIRouter() @@ -33,20 +31,6 @@ def nix_build_vm_cmd(machine: str, flake_url: str) -> list[str]: ) -class NixBuildException(HTTPException): - def __init__(self, msg: str, loc: list = ["body", "flake_attr"]): - detail = [ - { - "loc": loc, - "msg": msg, - "type": "value_error", - } - ] - super().__init__( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail - ) - - class BuildVmTask(BaseTask): def __init__(self, uuid: UUID, vm: VmConfig) -> None: super().__init__(uuid) @@ -71,43 +55,12 @@ class BuildVmTask(BaseTask): log.exception(e) -def nix_build_exception_handler( - request: Request, exc: NixBuildException -) -> JSONResponse: - log.error("NixBuildException: %s", exc) - return JSONResponse( - status_code=exc.status_code, - content=jsonable_encoder(dict(detail=exc.detail)), - ) - - -################################## -# # -# ======== VM ROUTES ======== # -# # -################################## @router.post("/api/vms/inspect") async def inspect_vm( flake_url: Annotated[str, Body()], flake_attr: Annotated[str, Body()] ) -> VmInspectResponse: cmd = nix_inspect_vm_cmd(flake_attr, flake_url=flake_url) - proc = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await proc.communicate() - - if proc.returncode != 0: - raise NixBuildException( - f""" -Failed to evaluate vm from '{flake_url}#{flake_attr}'. -command: {shlex.join(cmd)} -exit code: {proc.returncode} -command output: -{stderr.decode("utf-8")} -""" - ) + stdout = await run_cmd(cmd) data = json.loads(stdout) return VmInspectResponse( config=VmConfig(flake_url=flake_url, flake_attr=flake_attr, **data) diff --git a/pkgs/clan-cli/clan_cli/webui/schemas.py b/pkgs/clan-cli/clan_cli/webui/schemas.py index 5cb7c9d4a..c87931a04 100644 --- a/pkgs/clan-cli/clan_cli/webui/schemas.py +++ b/pkgs/clan-cli/clan_cli/webui/schemas.py @@ -53,6 +53,10 @@ class VmCreateResponse(BaseModel): uuid: str +class FlakeAttrResponse(BaseModel): + flake_attrs: list[str] + + class VmInspectResponse(BaseModel): config: VmConfig diff --git a/pkgs/clan-cli/tests/test_flake_api.py b/pkgs/clan-cli/tests/test_flake_api.py new file mode 100644 index 000000000..767af4f7b --- /dev/null +++ b/pkgs/clan-cli/tests/test_flake_api.py @@ -0,0 +1,17 @@ +from pathlib import Path + +import pytest +from api import TestClient + + +@pytest.mark.impure +def test_inspect(api: TestClient, test_flake_with_core: Path) -> None: + params = {"url": str(test_flake_with_core)} + response = api.get( + "/api/flake/attrs", + params=params, + ) + assert response.status_code == 200, "Failed to inspect vm" + data = response.json() + print("Data: ", data) + assert data.get("flake_attrs") == ["vm1"]