Merge pull request 'api/machines: better input/output validation' (#472) from DavHau-dave into main

This commit is contained in:
clan-bot
2023-11-06 10:59:14 +00:00
4 changed files with 67 additions and 22 deletions

View File

@@ -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

View File

@@ -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):

View File

@@ -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])

View File

@@ -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,8 +178,13 @@ 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": {
@@ -173,7 +193,6 @@ def test_configure_machine(api: TestClient, test_flake: FlakeForTest) -> None:
},
**fs_config,
}
}
# remove the import from the config
config_with_empty_imports = dict(
@@ -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,
}