From 170d29a15a6fe59d5e629e7c88a2f6872a18ba59 Mon Sep 17 00:00:00 2001 From: DavHau Date: Fri, 10 Nov 2023 15:06:59 +0700 Subject: [PATCH] api/schema: return list of missing modules --- pkgs/clan-cli/clan_cli/config/schema.py | 54 ++++++++++++++++--- pkgs/clan-cli/clan_cli/webui/api_errors.py | 10 ++++ .../clan_cli/webui/routers/machines.py | 7 ++- pkgs/clan-cli/tests/test_machines_api.py | 17 +++++- 4 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/webui/api_errors.py diff --git a/pkgs/clan-cli/clan_cli/config/schema.py b/pkgs/clan-cli/clan_cli/config/schema.py index c3a5712b0..a7f535668 100644 --- a/pkgs/clan-cli/clan_cli/config/schema.py +++ b/pkgs/clan-cli/clan_cli/config/schema.py @@ -6,6 +6,8 @@ from pathlib import Path from tempfile import NamedTemporaryFile from typing import Optional +from fastapi import HTTPException + from clan_cli.dirs import ( nixpkgs_source, specific_flake_dir, @@ -25,19 +27,59 @@ def machine_schema( # 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 clan_imports is not None: config["clanImports"] = clan_imports + # dump config to file 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 - ] + # ensure that the requested clanImports exist proc = subprocess.run( nix_eval( - flags=inject_config_flags - + [ + flags=[ + "--impure", + "--show-trace", + "--expr", + f""" + let + b = builtins; + system = b.currentSystem; + flake = b.getFlake (toString {flake}); + clan-core = flake.inputs.clan-core; + config = b.fromJSON (b.readFile (b.getEnv "CLAN_MACHINE_SETTINGS_FILE")); + modules_not_found = + b.filter + (modName: ! clan-core.clanModules ? ${{modName}}) + config.clanImports or []; + in + modules_not_found + """, + ] + ), + capture_output=True, + text=True, + cwd=flake, + env=env, + ) + if proc.returncode != 0: + print(proc.stderr, file=sys.stderr) + raise ClanError( + f"Failed to check clanImports for existence:\n{proc.stderr}" + ) + modules_not_found = json.loads(proc.stdout) + if len(modules_not_found) > 0: + raise HTTPException( + status_code=400, + detail={ + "msg": "Some requested clan modules could not be found", + "modules_not_found": modules_not_found, + }, + ) + + # get the schema + proc = subprocess.run( + nix_eval( + flags=[ "--impure", "--show-trace", "--expr", diff --git a/pkgs/clan-cli/clan_cli/webui/api_errors.py b/pkgs/clan-cli/clan_cli/webui/api_errors.py new file mode 100644 index 000000000..947f2da7f --- /dev/null +++ b/pkgs/clan-cli/clan_cli/webui/api_errors.py @@ -0,0 +1,10 @@ +import logging + +from pydantic import BaseModel + +log = logging.getLogger(__name__) + + +class MissingClanImports(BaseModel): + missing_clan_imports: list[str] = [] + msg: str = "Some requested clan modules could not be found" diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index d8a5b8e41..c40871e6f 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -4,6 +4,7 @@ from typing import Annotated from fastapi import APIRouter, Body +from clan_cli.webui.api_errors import MissingClanImports from clan_cli.webui.api_inputs import MachineConfig from ...config.machine import ( @@ -68,7 +69,11 @@ async def set_machine_config( set_config_for_machine(flake_name, name, conf) -@router.put("/api/{flake_name}/schema", tags=[Tags.machine]) +@router.put( + "/api/{flake_name}/schema", + tags=[Tags.machine], + responses={400: {"model": MissingClanImports}}, +) async def get_machine_schema( flake_name: FlakeName, config: Annotated[dict, Body()] ) -> SchemaResponse: diff --git a/pkgs/clan-cli/tests/test_machines_api.py b/pkgs/clan-cli/tests/test_machines_api.py index 8b0cbddc9..33576bdd3 100644 --- a/pkgs/clan-cli/tests/test_machines_api.py +++ b/pkgs/clan-cli/tests/test_machines_api.py @@ -30,13 +30,28 @@ def test_schema_errors(api: TestClient, test_flake_with_core: FlakeForTest) -> N json={"imports": ["some-inavlid-import"]}, ) assert response.status_code == 422 - # expect error to contain "error: string 'some-inavlid-import' doesn't represent an absolute path" assert ( "error: string 'some-inavlid-import' doesn't represent an absolute path" in response.json()["detail"][0]["msg"] ) +@pytest.mark.with_core +def test_schema_invalid_clan_imports( + api: TestClient, test_flake_with_core: FlakeForTest +) -> None: + response = api.put( + f"/api/{test_flake_with_core.name}/schema", + json={"clanImports": ["non-existing-clan-module"]}, + ) + assert response.status_code == 400 + assert ( + "Some requested clan modules could not be found" + in response.json()["detail"]["msg"] + ) + assert "non-existing-clan-module" in response.json()["detail"]["modules_not_found"] + + @pytest.mark.with_core def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest) -> None: # ensure error 404 if machine does not exist when accessing the config