Merge pull request 'api/machines: better input/output validation' (#472) from DavHau-dave into main
All checks were successful
assets1 / test (push) Successful in 22s
checks / test (push) Successful in 43s
checks-impure / test (push) Successful in 1m24s

This commit is contained in:
clan-bot 2023-11-06 10:59:14 +00:00
commit 231f1fe322
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,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,
}