Merge pull request 'api/machines: init put_machine replacing create_machine and set_machine_config' (#494) from DavHau-dave into main

This commit is contained in:
clan-bot
2023-11-13 13:30:53 +00:00
10 changed files with 49 additions and 82 deletions

2
flake.lock generated
View File

@@ -143,7 +143,7 @@
"sops-nix": {
"inputs": {
"nixpkgs": [
"sops-nix"
"nixpkgs"
],
"nixpkgs-stable": []
},

View File

@@ -1,5 +1,6 @@
import json
import os
import re
import subprocess
from pathlib import Path
from tempfile import NamedTemporaryFile
@@ -12,6 +13,7 @@ from clan_cli.dirs import (
specific_flake_dir,
specific_machine_dir,
)
from clan_cli.errors import ClanError
from clan_cli.git import commit_file
from clan_cli.nix import nix_eval
@@ -75,16 +77,22 @@ def config_for_machine(flake_name: FlakeName, machine_name: str) -> dict:
def set_config_for_machine(
flake_name: FlakeName, machine_name: str, config: dict
) -> None:
hostname_regex = r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)$"
if not re.match(hostname_regex, machine_name):
raise ClanError("Machine name must be a valid hostname")
if "networking" in config and "hostName" in config["networking"]:
if machine_name != config["networking"]["hostName"]:
raise HTTPException(
status_code=400,
detail="Machine name does not match the 'networking.hostName' setting in the config",
)
config["networking"]["hostName"] = machine_name
# create machine folder if it doesn't exist
# write the config to a json file located at {flake}/machines/{machine_name}/settings.json
if not specific_machine_dir(flake_name, machine_name).exists():
raise HTTPException(
status_code=404,
detail=f"Machine {machine_name} not found. Create the machine first`",
)
settings_path = machine_settings_file(flake_name, machine_name)
settings_path.parent.mkdir(parents=True, exist_ok=True)
with open(settings_path, "w") as f:
json.dump(config, f)
json.dump(config, f, indent=2)
repo_dir = specific_flake_dir(flake_name)
if repo_dir is not None:

View File

@@ -53,7 +53,7 @@ def _commit_file_to_git(repo_dir: Path, file_path: Path, commit_message: str) ->
# check if there is a diff
cmd = nix_shell(
["git"],
["git", "-C", str(repo_dir), "diff", "--cached", "--exit-code"],
["git", "-C", str(repo_dir), "diff", "--cached", "--exit-code", str(file_path)],
)
result = subprocess.run(cmd, cwd=repo_dir)
# if there is no diff, return

View File

@@ -1,46 +1,13 @@
import argparse
import logging
from typing import Dict
from ..async_cmd import CmdOut, run, runforcli
from ..dirs import specific_flake_dir, specific_machine_dir
from ..errors import ClanError
from ..nix import nix_shell
from ..types import FlakeName
from clan_cli.config.machine import set_config_for_machine
log = logging.getLogger(__name__)
async def create_machine(flake_name: FlakeName, machine_name: str) -> Dict[str, CmdOut]:
folder = specific_machine_dir(flake_name, machine_name)
if folder.exists():
raise ClanError(f"Machine '{machine_name}' already exists")
folder.mkdir(parents=True, exist_ok=True)
# create empty settings.json file inside the folder
with open(folder / "settings.json", "w") as f:
f.write("{}")
response = {}
out = await run(nix_shell(["git"], ["git", "add", str(folder)]), cwd=folder)
response["git add"] = out
out = await run(
nix_shell(
["git"],
["git", "commit", "-m", f"Added machine {machine_name}", str(folder)],
),
cwd=folder,
)
response["git commit"] = out
return response
def create_command(args: argparse.Namespace) -> None:
try:
flake_dir = specific_flake_dir(args.flake)
runforcli(create_machine, flake_dir, args.machine)
except ClanError as e:
print(e)
set_config_for_machine(args.flake, args.machine, dict())
def register_create_parser(parser: argparse.ArgumentParser) -> None:

View File

@@ -71,10 +71,10 @@ class Command:
try:
for line in fd:
if fd == self.p.stderr:
self.log.debug(f"[{cmd[0]}] stderr: {line}")
self.log.debug(f"[{cmd}] stderr: {line}")
self.stderr.append(line)
else:
self.log.debug(f"[{cmd[0]}] stdout: {line}")
self.log.debug(f"[{cmd}] stdout: {line}")
self.stdout.append(line)
self._output.put(line)
except BlockingIOError:

View File

@@ -23,10 +23,6 @@ class Machine(BaseModel):
status: Status
class MachineCreate(BaseModel):
name: str
class MachinesResponse(BaseModel):
machines: list[Machine]

View File

@@ -3,6 +3,7 @@ import logging
from typing import Annotated
from fastapi import APIRouter, Body
from fastapi.encoders import jsonable_encoder
from clan_cli.webui.api_errors import MissingClanImports
from clan_cli.webui.api_inputs import MachineConfig
@@ -13,13 +14,11 @@ from ...config.machine import (
verify_machine_config,
)
from ...config.schema import machine_schema
from ...machines.create import create_machine as _create_machine
from ...machines.list import list_machines as _list_machines
from ...types import FlakeName
from ..api_outputs import (
ConfigResponse,
Machine,
MachineCreate,
MachineResponse,
MachinesResponse,
SchemaResponse,
@@ -41,14 +40,6 @@ async def list_machines(flake_name: FlakeName) -> MachinesResponse:
return MachinesResponse(machines=machines)
@router.post("/api/{flake_name}/machines", tags=[Tags.machine], status_code=201)
async def create_machine(
flake_name: FlakeName, machine: Annotated[MachineCreate, Body()]
) -> MachineResponse:
await _create_machine(flake_name, machine.name)
return MachineResponse(machine=Machine(name=machine.name, status=Status.UNKNOWN))
@router.get("/api/{flake_name}/machines/{name}", tags=[Tags.machine])
async def get_machine(flake_name: FlakeName, name: str) -> MachineResponse:
log.error("TODO")
@@ -62,10 +53,14 @@ async def get_machine_config(flake_name: FlakeName, name: str) -> ConfigResponse
@router.put("/api/{flake_name}/machines/{name}/config", tags=[Tags.machine])
async def set_machine_config(
async def put_machine(
flake_name: FlakeName, name: str, config: Annotated[MachineConfig, Body()]
) -> None:
conf = dict(config)
"""
Set the config for a machine.
Creates the machine if it doesn't yet exist.
"""
conf = jsonable_encoder(config)
set_config_for_machine(flake_name, name, conf)
@@ -75,9 +70,9 @@ async def set_machine_config(
responses={400: {"model": MissingClanImports}},
)
async def get_machine_schema(
flake_name: FlakeName, config: Annotated[dict, Body()]
flake_name: FlakeName, config: Annotated[MachineConfig, Body()]
) -> SchemaResponse:
schema = machine_schema(flake_name, config=config)
schema = machine_schema(flake_name, config=dict(config))
return SchemaResponse(schema=schema)

View File

@@ -57,6 +57,6 @@ mkShell {
./bin/clan flakes create example_clan
./bin/clan machines create example_machine example_clan
./bin/clan machines create example-machine example_clan
'';
}

View File

@@ -3,15 +3,13 @@ from api import TestClient
from fixtures_flakes import FlakeForTest
def test_machines(api: TestClient, test_flake: FlakeForTest) -> None:
def test_create_and_list(api: TestClient, test_flake: FlakeForTest) -> None:
response = api.get(f"/api/{test_flake.name}/machines")
assert response.status_code == 200
assert response.json() == {"machines": []}
response = api.post(f"/api/{test_flake.name}/machines", json={"name": "test"})
assert response.status_code == 201
assert response.json() == {"machine": {"name": "test", "status": "unknown"}}
response = api.put(f"/api/{test_flake.name}/machines/test/config", json=dict())
assert response.status_code == 200
response = api.get(f"/api/{test_flake.name}/machines/test")
assert response.status_code == 200
@@ -52,23 +50,29 @@ def test_schema_invalid_clan_imports(
assert "non-existing-clan-module" in response.json()["detail"]["modules_not_found"]
def test_create_machine_invalid_hostname(
api: TestClient, test_flake: FlakeForTest
) -> None:
response = api.put(
f"/api/{test_flake.name}/machines/-invalid-hostname/config", json=dict()
)
assert response.status_code == 422
assert (
"Machine name must be a valid hostname" in response.json()["detail"][0]["msg"]
)
@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
response = api.get(f"/api/{test_flake_with_core.name}/machines/machine1/config")
assert response.status_code == 404
# ensure error 404 if machine does not exist when writing to the config
# create the machine
response = api.put(
f"/api/{test_flake_with_core.name}/machines/machine1/config", json={}
)
assert response.status_code == 404
# create the machine
response = api.post(
f"/api/{test_flake_with_core.name}/machines", json={"name": "machine1"}
)
assert response.status_code == 201
assert response.status_code == 200
# ensure an empty config is returned by default for a new machine
response = api.get(f"/api/{test_flake_with_core.name}/machines/machine1/config")

View File

@@ -1,4 +1,4 @@
import { createMachine, setMachineConfig } from "@/api/machine/machine";
import { putMachine } from "@/api/machine/machine";
import {
Box,
Button,
@@ -79,10 +79,7 @@ export function CreateMachineForm() {
toast.error("Machine name should not be empty");
return;
}
await createMachine(clanName, {
name: data.name,
});
await setMachineConfig(clanName, data.name, {
await putMachine(clanName, data.name, {
clan: data.config.formData,
clanImports: data.modules,
});