Merge pull request 'api/machines: better input/output validation' (#472) from DavHau-dave into main
This commit is contained in:
@@ -2,7 +2,7 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
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 ..dirs import clan_data_dir, clan_flakes_dir
|
||||||
from ..flakes.create import DEFAULT_URL
|
from ..flakes.create import DEFAULT_URL
|
||||||
@@ -29,3 +29,12 @@ class ClanFlakePath(BaseModel):
|
|||||||
|
|
||||||
class FlakeCreateInput(ClanFlakePath):
|
class FlakeCreateInput(ClanFlakePath):
|
||||||
url: AnyUrl = DEFAULT_URL
|
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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Extra, Field
|
||||||
|
|
||||||
from ..async_cmd import CmdOut
|
from ..async_cmd import CmdOut
|
||||||
from ..task_manager import TaskStatus
|
from ..task_manager import TaskStatus
|
||||||
@@ -36,7 +36,12 @@ class MachineResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ConfigResponse(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):
|
class SchemaResponse(BaseModel):
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from typing import Annotated
|
|||||||
|
|
||||||
from fastapi import APIRouter, Body
|
from fastapi import APIRouter, Body
|
||||||
|
|
||||||
|
from clan_cli.webui.api_inputs import MachineConfig
|
||||||
|
|
||||||
from ...config.machine import (
|
from ...config.machine import (
|
||||||
config_for_machine,
|
config_for_machine,
|
||||||
schema_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])
|
@router.get("/api/{flake_name}/machines/{name}/config", tags=[Tags.machine])
|
||||||
async def get_machine_config(flake_name: FlakeName, name: str) -> ConfigResponse:
|
async def get_machine_config(flake_name: FlakeName, name: str) -> ConfigResponse:
|
||||||
config = config_for_machine(flake_name, name)
|
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])
|
@router.put("/api/{flake_name}/machines/{name}/config", tags=[Tags.machine])
|
||||||
async def set_machine_config(
|
async def set_machine_config(
|
||||||
flake_name: FlakeName, name: str, config: Annotated[dict, Body()]
|
flake_name: FlakeName, name: str, config: Annotated[MachineConfig, Body()]
|
||||||
) -> ConfigResponse:
|
) -> None:
|
||||||
set_config_for_machine(flake_name, name, config)
|
conf = dict(config)
|
||||||
return ConfigResponse(config=config)
|
set_config_for_machine(flake_name, name, conf)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/{flake_name}/machines/{name}/schema", tags=[Tags.machine])
|
@router.get("/api/{flake_name}/machines/{name}/schema", tags=[Tags.machine])
|
||||||
|
|||||||
@@ -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
|
# ensure an empty config is returned by default for a new machine
|
||||||
response = api.get(f"/api/{test_flake.name}/machines/machine1/config")
|
response = api.get(f"/api/{test_flake.name}/machines/machine1/config")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {"config": {}}
|
assert response.json() == {
|
||||||
|
"clanImports": [],
|
||||||
|
"clan": {},
|
||||||
|
}
|
||||||
|
|
||||||
# get jsonschema for machine
|
# get jsonschema for machine
|
||||||
response = api.get(f"/api/{test_flake.name}/machines/machine1/schema")
|
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
|
# ensure the config has actually been updated
|
||||||
response = api.get(f"/api/{test_flake.name}/machines/machine1/config")
|
response = api.get(f"/api/{test_flake.name}/machines/machine1/config")
|
||||||
assert response.status_code == 200
|
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
|
# the part of the config that makes the evaluation pass
|
||||||
fs_config = dict(
|
fs_config = dict(
|
||||||
@@ -109,12 +112,18 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None:
|
|||||||
json=config2,
|
json=config2,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
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
|
# get the config again
|
||||||
response = api.get(f"/api/{test_flake.name}/machines/machine1/config")
|
response = api.get(f"/api/{test_flake.name}/machines/machine1/config")
|
||||||
assert response.status_code == 200
|
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
|
# 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
|
# 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,
|
json=config2,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
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
|
# verify the machine config evaluates
|
||||||
response = api.get(f"/api/{test_flake.name}/machines/machine1/verify")
|
response = api.get(f"/api/{test_flake.name}/machines/machine1/verify")
|
||||||
@@ -163,8 +178,13 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None:
|
|||||||
json=config_with_imports,
|
json=config_with_imports,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
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() == {
|
assert response.json() == {
|
||||||
"config": {
|
|
||||||
"clanImports": ["fake-module"],
|
"clanImports": ["fake-module"],
|
||||||
"clan": {
|
"clan": {
|
||||||
"fake-module": {
|
"fake-module": {
|
||||||
@@ -173,7 +193,6 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None:
|
|||||||
},
|
},
|
||||||
**fs_config,
|
**fs_config,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# remove the import from the config
|
# remove the import from the config
|
||||||
config_with_empty_imports = dict(
|
config_with_empty_imports = dict(
|
||||||
@@ -185,4 +204,14 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None:
|
|||||||
json=config_with_empty_imports,
|
json=config_with_empty_imports,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
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,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user