diff --git a/pkgs/clan-cli/clan_cli/webui/api_inputs.py b/pkgs/clan-cli/clan_cli/webui/api_inputs.py index f9d7c541..105c8227 100644 --- a/pkgs/clan-cli/clan_cli/webui/api_inputs.py +++ b/pkgs/clan-cli/clan_cli/webui/api_inputs.py @@ -2,7 +2,7 @@ import logging from pathlib import Path from typing import Any -from pydantic import AnyUrl, BaseModel, validator +from pydantic import AnyUrl, BaseModel, Extra, validator from ..dirs import clan_data_dir, clan_flakes_dir from ..flakes.create import DEFAULT_URL @@ -29,3 +29,12 @@ class ClanFlakePath(BaseModel): class FlakeCreateInput(ClanFlakePath): url: AnyUrl = DEFAULT_URL + + +class MachineConfig(BaseModel): + clanImports: list[str] = [] # noqa: N815 + clan: dict = {} + + # allow extra fields to cover the full spectrum of a nixos config + class Config: + extra = Extra.allow diff --git a/pkgs/clan-cli/clan_cli/webui/api_outputs.py b/pkgs/clan-cli/clan_cli/webui/api_outputs.py index ba69c032..b0d4f26a 100644 --- a/pkgs/clan-cli/clan_cli/webui/api_outputs.py +++ b/pkgs/clan-cli/clan_cli/webui/api_outputs.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Dict, List -from pydantic import BaseModel, Field +from pydantic import BaseModel, Extra, Field from ..async_cmd import CmdOut from ..task_manager import TaskStatus @@ -36,7 +36,12 @@ class MachineResponse(BaseModel): class ConfigResponse(BaseModel): - config: dict + clanImports: list[str] = [] # noqa: N815 + clan: dict = {} + + # allow extra fields to cover the full spectrum of a nixos config + class Config: + extra = Extra.allow class SchemaResponse(BaseModel): diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index 43f9c6a7..c2d6b364 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -4,6 +4,8 @@ from typing import Annotated from fastapi import APIRouter, Body +from clan_cli.webui.api_inputs import MachineConfig + from ...config.machine import ( config_for_machine, schema_for_machine, @@ -55,15 +57,15 @@ async def get_machine(flake_name: FlakeName, name: str) -> MachineResponse: @router.get("/api/{flake_name}/machines/{name}/config", tags=[Tags.machine]) async def get_machine_config(flake_name: FlakeName, name: str) -> ConfigResponse: config = config_for_machine(flake_name, name) - return ConfigResponse(config=config) + return ConfigResponse(**config) @router.put("/api/{flake_name}/machines/{name}/config", tags=[Tags.machine]) async def set_machine_config( - flake_name: FlakeName, name: str, config: Annotated[dict, Body()] -) -> ConfigResponse: - set_config_for_machine(flake_name, name, config) - return ConfigResponse(config=config) + flake_name: FlakeName, name: str, config: Annotated[MachineConfig, Body()] +) -> None: + conf = dict(config) + set_config_for_machine(flake_name, name, conf) @router.get("/api/{flake_name}/machines/{name}/schema", tags=[Tags.machine]) diff --git a/pkgs/clan-cli/tests/test_machines_api.py b/pkgs/clan-cli/tests/test_machines_api.py index 8a9fe0ed..dde4ae86 100644 --- a/pkgs/clan-cli/tests/test_machines_api.py +++ b/pkgs/clan-cli/tests/test_machines_api.py @@ -37,7 +37,10 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: # ensure an empty config is returned by default for a new machine response = api.get(f"/api/{test_flake.name}/machines/machine1/config") assert response.status_code == 200 - assert response.json() == {"config": {}} + assert response.json() == { + "clanImports": [], + "clan": {}, + } # get jsonschema for machine response = api.get(f"/api/{test_flake.name}/machines/machine1/schema") @@ -75,7 +78,7 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: # ensure the config has actually been updated response = api.get(f"/api/{test_flake.name}/machines/machine1/config") assert response.status_code == 200 - assert response.json() == {"config": invalid_config} + assert response.json() == dict(clanImports=[], **invalid_config) # the part of the config that makes the evaluation pass fs_config = dict( @@ -109,12 +112,18 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: json=config2, ) assert response.status_code == 200 - assert response.json() == {"config": config2} + + # ensure the config has been applied + response = api.get( + f"/api/{test_flake.name}/machines/machine1/config", + ) + assert response.status_code == 200 + assert response.json() == dict(clanImports=[], **config2) # 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} + assert response.json() == {"clanImports": [], **config2} # ensure PUT on the config is idempotent by passing the config again # For example, this should not result in the boot.loader.grub.devices being @@ -124,7 +133,13 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: json=config2, ) assert response.status_code == 200 - assert response.json() == {"config": config2} + + # ensure the config has been applied + response = api.get( + f"/api/{test_flake.name}/machines/machine1/config", + ) + assert response.status_code == 200 + assert response.json() == dict(clanImports=[], **config2) # verify the machine config evaluates response = api.get(f"/api/{test_flake.name}/machines/machine1/verify") @@ -163,16 +178,20 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: json=config_with_imports, ) assert response.status_code == 200 + + # ensure the config has been applied + response = api.get( + f"/api/{test_flake.name}/machines/machine1/config", + ) + assert response.status_code == 200 assert response.json() == { - "config": { - "clanImports": ["fake-module"], - "clan": { - "fake-module": { - "fake-flag": True, - }, + "clanImports": ["fake-module"], + "clan": { + "fake-module": { + "fake-flag": True, }, - **fs_config, - } + }, + **fs_config, } # remove the import from the config @@ -185,4 +204,14 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None: json=config_with_empty_imports, ) assert response.status_code == 200 - assert response.json() == {"config": config_with_empty_imports} + + # ensure the config has been applied + response = api.get( + f"/api/{test_flake.name}/machines/machine1/config", + ) + assert response.status_code == 200 + assert response.json() == { + "clanImports": ["fake-module"], + "clan": {}, + **config_with_empty_imports, + }