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:
2
flake.lock
generated
2
flake.lock
generated
@@ -143,7 +143,7 @@
|
|||||||
"sops-nix": {
|
"sops-nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"sops-nix"
|
"nixpkgs"
|
||||||
],
|
],
|
||||||
"nixpkgs-stable": []
|
"nixpkgs-stable": []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
@@ -12,6 +13,7 @@ from clan_cli.dirs import (
|
|||||||
specific_flake_dir,
|
specific_flake_dir,
|
||||||
specific_machine_dir,
|
specific_machine_dir,
|
||||||
)
|
)
|
||||||
|
from clan_cli.errors import ClanError
|
||||||
from clan_cli.git import commit_file
|
from clan_cli.git import commit_file
|
||||||
from clan_cli.nix import nix_eval
|
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(
|
def set_config_for_machine(
|
||||||
flake_name: FlakeName, machine_name: str, config: dict
|
flake_name: FlakeName, machine_name: str, config: dict
|
||||||
) -> None:
|
) -> None:
|
||||||
# write the config to a json file located at {flake}/machines/{machine_name}/settings.json
|
hostname_regex = r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)$"
|
||||||
if not specific_machine_dir(flake_name, machine_name).exists():
|
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(
|
raise HTTPException(
|
||||||
status_code=404,
|
status_code=400,
|
||||||
detail=f"Machine {machine_name} not found. Create the machine first`",
|
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
|
||||||
settings_path = machine_settings_file(flake_name, machine_name)
|
settings_path = machine_settings_file(flake_name, machine_name)
|
||||||
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(settings_path, "w") as f:
|
with open(settings_path, "w") as f:
|
||||||
json.dump(config, f)
|
json.dump(config, f, indent=2)
|
||||||
repo_dir = specific_flake_dir(flake_name)
|
repo_dir = specific_flake_dir(flake_name)
|
||||||
|
|
||||||
if repo_dir is not None:
|
if repo_dir is not None:
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def _commit_file_to_git(repo_dir: Path, file_path: Path, commit_message: str) ->
|
|||||||
# check if there is a diff
|
# check if there is a diff
|
||||||
cmd = nix_shell(
|
cmd = nix_shell(
|
||||||
["git"],
|
["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)
|
result = subprocess.run(cmd, cwd=repo_dir)
|
||||||
# if there is no diff, return
|
# if there is no diff, return
|
||||||
|
|||||||
@@ -1,46 +1,13 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from ..async_cmd import CmdOut, run, runforcli
|
from clan_cli.config.machine import set_config_for_machine
|
||||||
from ..dirs import specific_flake_dir, specific_machine_dir
|
|
||||||
from ..errors import ClanError
|
|
||||||
from ..nix import nix_shell
|
|
||||||
from ..types import FlakeName
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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:
|
def create_command(args: argparse.Namespace) -> None:
|
||||||
try:
|
set_config_for_machine(args.flake, args.machine, dict())
|
||||||
flake_dir = specific_flake_dir(args.flake)
|
|
||||||
runforcli(create_machine, flake_dir, args.machine)
|
|
||||||
except ClanError as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
|
|
||||||
def register_create_parser(parser: argparse.ArgumentParser) -> None:
|
def register_create_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
|||||||
@@ -71,10 +71,10 @@ class Command:
|
|||||||
try:
|
try:
|
||||||
for line in fd:
|
for line in fd:
|
||||||
if fd == self.p.stderr:
|
if fd == self.p.stderr:
|
||||||
self.log.debug(f"[{cmd[0]}] stderr: {line}")
|
self.log.debug(f"[{cmd}] stderr: {line}")
|
||||||
self.stderr.append(line)
|
self.stderr.append(line)
|
||||||
else:
|
else:
|
||||||
self.log.debug(f"[{cmd[0]}] stdout: {line}")
|
self.log.debug(f"[{cmd}] stdout: {line}")
|
||||||
self.stdout.append(line)
|
self.stdout.append(line)
|
||||||
self._output.put(line)
|
self._output.put(line)
|
||||||
except BlockingIOError:
|
except BlockingIOError:
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ class Machine(BaseModel):
|
|||||||
status: Status
|
status: Status
|
||||||
|
|
||||||
|
|
||||||
class MachineCreate(BaseModel):
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
class MachinesResponse(BaseModel):
|
class MachinesResponse(BaseModel):
|
||||||
machines: list[Machine]
|
machines: list[Machine]
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body
|
from fastapi import APIRouter, Body
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
|
||||||
from clan_cli.webui.api_errors import MissingClanImports
|
from clan_cli.webui.api_errors import MissingClanImports
|
||||||
from clan_cli.webui.api_inputs import MachineConfig
|
from clan_cli.webui.api_inputs import MachineConfig
|
||||||
@@ -13,13 +14,11 @@ from ...config.machine import (
|
|||||||
verify_machine_config,
|
verify_machine_config,
|
||||||
)
|
)
|
||||||
from ...config.schema import machine_schema
|
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 ...machines.list import list_machines as _list_machines
|
||||||
from ...types import FlakeName
|
from ...types import FlakeName
|
||||||
from ..api_outputs import (
|
from ..api_outputs import (
|
||||||
ConfigResponse,
|
ConfigResponse,
|
||||||
Machine,
|
Machine,
|
||||||
MachineCreate,
|
|
||||||
MachineResponse,
|
MachineResponse,
|
||||||
MachinesResponse,
|
MachinesResponse,
|
||||||
SchemaResponse,
|
SchemaResponse,
|
||||||
@@ -41,14 +40,6 @@ async def list_machines(flake_name: FlakeName) -> MachinesResponse:
|
|||||||
return MachinesResponse(machines=machines)
|
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])
|
@router.get("/api/{flake_name}/machines/{name}", tags=[Tags.machine])
|
||||||
async def get_machine(flake_name: FlakeName, name: str) -> MachineResponse:
|
async def get_machine(flake_name: FlakeName, name: str) -> MachineResponse:
|
||||||
log.error("TODO")
|
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])
|
@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()]
|
flake_name: FlakeName, name: str, config: Annotated[MachineConfig, Body()]
|
||||||
) -> None:
|
) -> 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)
|
set_config_for_machine(flake_name, name, conf)
|
||||||
|
|
||||||
|
|
||||||
@@ -75,9 +70,9 @@ async def set_machine_config(
|
|||||||
responses={400: {"model": MissingClanImports}},
|
responses={400: {"model": MissingClanImports}},
|
||||||
)
|
)
|
||||||
async def get_machine_schema(
|
async def get_machine_schema(
|
||||||
flake_name: FlakeName, config: Annotated[dict, Body()]
|
flake_name: FlakeName, config: Annotated[MachineConfig, Body()]
|
||||||
) -> SchemaResponse:
|
) -> SchemaResponse:
|
||||||
schema = machine_schema(flake_name, config=config)
|
schema = machine_schema(flake_name, config=dict(config))
|
||||||
return SchemaResponse(schema=schema)
|
return SchemaResponse(schema=schema)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,6 @@ mkShell {
|
|||||||
|
|
||||||
|
|
||||||
./bin/clan flakes create example_clan
|
./bin/clan flakes create example_clan
|
||||||
./bin/clan machines create example_machine example_clan
|
./bin/clan machines create example-machine example_clan
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ from api import TestClient
|
|||||||
from fixtures_flakes import FlakeForTest
|
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")
|
response = api.get(f"/api/{test_flake.name}/machines")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {"machines": []}
|
assert response.json() == {"machines": []}
|
||||||
|
|
||||||
response = api.post(f"/api/{test_flake.name}/machines", json={"name": "test"})
|
response = api.put(f"/api/{test_flake.name}/machines/test/config", json=dict())
|
||||||
assert response.status_code == 201
|
assert response.status_code == 200
|
||||||
|
|
||||||
assert response.json() == {"machine": {"name": "test", "status": "unknown"}}
|
|
||||||
|
|
||||||
response = api.get(f"/api/{test_flake.name}/machines/test")
|
response = api.get(f"/api/{test_flake.name}/machines/test")
|
||||||
assert response.status_code == 200
|
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"]
|
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
|
@pytest.mark.with_core
|
||||||
def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest) -> None:
|
def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest) -> None:
|
||||||
# ensure error 404 if machine does not exist when accessing the config
|
# 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")
|
response = api.get(f"/api/{test_flake_with_core.name}/machines/machine1/config")
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
# ensure error 404 if machine does not exist when writing to the config
|
# create the machine
|
||||||
response = api.put(
|
response = api.put(
|
||||||
f"/api/{test_flake_with_core.name}/machines/machine1/config", json={}
|
f"/api/{test_flake_with_core.name}/machines/machine1/config", json={}
|
||||||
)
|
)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 200
|
||||||
|
|
||||||
# create the machine
|
|
||||||
response = api.post(
|
|
||||||
f"/api/{test_flake_with_core.name}/machines", json={"name": "machine1"}
|
|
||||||
)
|
|
||||||
assert response.status_code == 201
|
|
||||||
|
|
||||||
# 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_with_core.name}/machines/machine1/config")
|
response = api.get(f"/api/{test_flake_with_core.name}/machines/machine1/config")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createMachine, setMachineConfig } from "@/api/machine/machine";
|
import { putMachine } from "@/api/machine/machine";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -79,10 +79,7 @@ export function CreateMachineForm() {
|
|||||||
toast.error("Machine name should not be empty");
|
toast.error("Machine name should not be empty");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await createMachine(clanName, {
|
await putMachine(clanName, data.name, {
|
||||||
name: data.name,
|
|
||||||
});
|
|
||||||
await setMachineConfig(clanName, data.name, {
|
|
||||||
clan: data.config.formData,
|
clan: data.config.formData,
|
||||||
clanImports: data.modules,
|
clanImports: data.modules,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user