diff --git a/pkgs/clan-cli/clan_cli/clan_modules.py b/pkgs/clan-cli/clan_cli/clan_modules.py new file mode 100644 index 000000000..926feeb0d --- /dev/null +++ b/pkgs/clan-cli/clan_cli/clan_modules.py @@ -0,0 +1,39 @@ +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 + + +def get_clan_module_names( + flake: Optional[Path] = None, +) -> 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() + proc = subprocess.run( + nix_eval( + [ + "--impure", + "--show-trace", + "--expr", + f""" + let + flake = builtins.getFlake (toString {flake}); + in + builtins.attrNames flake.inputs.clan-core.clanModules + """, + ], + ), + capture_output=True, + text=True, + cwd=flake, + ) + if proc.returncode != 0: + return [], proc.stderr + module_names = json.loads(proc.stdout) + return module_names, None diff --git a/pkgs/clan-cli/clan_cli/webui/app.py b/pkgs/clan-cli/clan_cli/webui/app.py index d399577e1..6083b0140 100644 --- a/pkgs/clan-cli/clan_cli/webui/app.py +++ b/pkgs/clan-cli/clan_cli/webui/app.py @@ -8,7 +8,7 @@ from fastapi.staticfiles import StaticFiles from ..errors import ClanError from .assets import asset_path from .error_handlers import clan_error_handler -from .routers import flake, health, machines, root, vms +from .routers import clan_modules, flake, health, machines, root, vms origins = [ "http://localhost:3000", @@ -26,6 +26,7 @@ def setup_app() -> FastAPI: allow_methods=["*"], allow_headers=["*"], ) + app.include_router(clan_modules.router) app.include_router(flake.router) app.include_router(health.router) app.include_router(machines.router) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py b/pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py new file mode 100644 index 000000000..fe97e2825 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/webui/routers/clan_modules.py @@ -0,0 +1,21 @@ +# Logging setup +import logging + +from fastapi import APIRouter, HTTPException + +from clan_cli.clan_modules import get_clan_module_names + +from ..schemas import ( + ClanModulesResponse, +) + +log = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/api/clan_modules") +async def list_clan_modules() -> ClanModulesResponse: + module_names, error = get_clan_module_names() + 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/schemas.py b/pkgs/clan-cli/clan_cli/webui/schemas.py index 43ac47a10..97341ed2f 100644 --- a/pkgs/clan-cli/clan_cli/webui/schemas.py +++ b/pkgs/clan-cli/clan_cli/webui/schemas.py @@ -13,6 +13,10 @@ class Status(Enum): UNKNOWN = "unknown" +class ClanModulesResponse(BaseModel): + clan_modules: list[str] + + class Machine(BaseModel): name: str status: Status diff --git a/pkgs/clan-cli/tests/test_clan_modules.py b/pkgs/clan-cli/tests/test_clan_modules.py new file mode 100644 index 000000000..374fa4dff --- /dev/null +++ b/pkgs/clan-cli/tests/test_clan_modules.py @@ -0,0 +1,17 @@ +from pathlib import Path + +import pytest +from api import TestClient + + +@pytest.mark.impure() +def test_configure_machine(api: TestClient, test_flake_with_core: Path) -> None: + # retrieve the list of available clanModules + response = api.get("/api/clan_modules") + response_json = response.json() + assert response.status_code == 200 + assert isinstance(response_json, dict) + assert "clan_modules" in response_json + assert len(response_json["clan_modules"]) > 0 + # ensure all entries are a string + assert all(isinstance(x, str) for x in response_json["clan_modules"])