api/machines: init put_machine replacing create_machine and set_machine_config
All checks were successful
checks-impure / test (pull_request) Successful in 1m36s
checks / test (pull_request) Successful in 1m59s

This allows creating and configuring a machine in one single step.
This commit is contained in:
DavHau 2023-11-13 20:25:52 +07:00
parent 2395119d21
commit 1652b5c27b
8 changed files with 35 additions and 88 deletions

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

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

@ -1,5 +1,4 @@
import logging
import re
from pathlib import Path
from typing import Any
@ -31,15 +30,3 @@ class MachineConfig(BaseModel):
# allow extra fields to cover the full spectrum of a nixos config
class Config:
extra = Extra.allow
class MachineCreate(BaseModel):
name: str
@classmethod
@validator("name")
def validate_hostname(cls, v: str) -> str:
hostname_regex = r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)$"
if not re.match(hostname_regex, v):
raise ValueError("Machine name must be a valid hostname")
return v

View File

@ -3,9 +3,10 @@ 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, MachineCreate
from clan_cli.webui.api_inputs import MachineConfig
from ...config.machine import (
config_for_machine,
@ -13,7 +14,6 @@ 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 (
@ -40,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")
@ -61,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)

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
@ -55,8 +53,8 @@ def test_schema_invalid_clan_imports(
def test_create_machine_invalid_hostname(
api: TestClient, test_flake: FlakeForTest
) -> None:
response = api.post(
f"/api/{test_flake.name}/machines", json={"name": "-invalid-hostname"}
response = api.put(
f"/api/{test_flake.name}/machines/-invalid-hostname/config", json=dict()
)
assert response.status_code == 422
assert (
@ -70,17 +68,11 @@ def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest)
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,
});