From fb76ad45e8340d19609cf23305744d7a5b20d46b Mon Sep 17 00:00:00 2001 From: DavHau Date: Fri, 25 Aug 2023 21:26:30 +0200 Subject: [PATCH] webui: implement /api/machines/{name}/schema --- lib/jsonschema/default.nix | 2 +- pkgs/clan-cli/clan_cli/config/machine.py | 47 +++++++++++++++++++ .../clan_cli/webui/routers/machines.py | 6 +-- pkgs/clan-cli/clan_cli/webui/schemas.py | 6 +-- pkgs/clan-cli/default.nix | 4 ++ pkgs/clan-cli/shell.nix | 6 ++- .../example_flake/clanModules/machine1.nix | 7 +++ .../tests/config/example_flake/flake.nix | 10 ++++ .../tests/{ => config}/test_config.py | 0 .../tests/config/test_machine_schema.py | 44 +++++++++++++++++ 10 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/config/machine.py create mode 100644 pkgs/clan-cli/tests/config/example_flake/clanModules/machine1.nix create mode 100644 pkgs/clan-cli/tests/config/example_flake/flake.nix rename pkgs/clan-cli/tests/{ => config}/test_config.py (100%) create mode 100644 pkgs/clan-cli/tests/config/test_machine_schema.py diff --git a/lib/jsonschema/default.nix b/lib/jsonschema/default.nix index 7a263f05d..26c399e45 100644 --- a/lib/jsonschema/default.nix +++ b/lib/jsonschema/default.nix @@ -1,4 +1,4 @@ -{ lib ? (import { }).lib }: +{ lib ? import }: let # from nixos type to jsonschema type diff --git a/pkgs/clan-cli/clan_cli/config/machine.py b/pkgs/clan-cli/clan_cli/config/machine.py new file mode 100644 index 000000000..3787bf6cb --- /dev/null +++ b/pkgs/clan-cli/clan_cli/config/machine.py @@ -0,0 +1,47 @@ +import json +import subprocess +import sys +from pathlib import Path +from typing import Optional + +from ..dirs import get_clan_flake_toplevel + + +def schema_for_machine(machine_name: str, flake: Optional[Path] = None) -> dict: + if flake is None: + flake = get_clan_flake_toplevel() + # use nix eval to read from .#clanModules..options + proc = subprocess.run( + [ + "nix", + "eval", + "--json", + "--impure", + "--show-trace", + "--extra-experimental-features", + "nix-command flakes", + "--expr", + f""" + let + flake = builtins.getFlake (toString {flake}); + lib = flake.inputs.nixpkgs.lib; + module = builtins.trace (builtins.attrNames flake) flake.clanModules.machine-{machine_name}; + evaled = lib.evalModules {{ + modules = [module]; + }}; + clanOptions = evaled.options.clan; + jsonschemaLib = import {Path(__file__).parent / "jsonschema"} {{ inherit lib; }}; + jsonschema = jsonschemaLib.parseOptions clanOptions; + in + jsonschema + """, + ], + capture_output=True, + text=True, + ) + if proc.returncode != 0: + print(proc.stderr, file=sys.stderr) + raise Exception( + f"Failed to read schema for machine {machine_name}:\n{proc.stderr}" + ) + return json.loads(proc.stdout) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index 6036b6299..9c7ebacff 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -2,6 +2,7 @@ from typing import Annotated from fastapi import APIRouter, Body +from ...config.machine import schema_for_machine from ...machines.create import create_machine as _create_machine from ...machines.list import list_machines as _list_machines from ..schemas import ( @@ -11,7 +12,6 @@ from ..schemas import ( MachineCreate, MachineResponse, MachinesResponse, - Schema, SchemaResponse, Status, ) @@ -54,5 +54,5 @@ async def set_machine_config( @router.get("/api/machines/{name}/schema") async def get_machine_schema(name: str) -> SchemaResponse: - print("TODO") - return SchemaResponse(schema=Schema()) + schema = schema_for_machine(name) + return SchemaResponse(schema=schema) diff --git a/pkgs/clan-cli/clan_cli/webui/schemas.py b/pkgs/clan-cli/clan_cli/webui/schemas.py index b260cbfcd..00258630b 100644 --- a/pkgs/clan-cli/clan_cli/webui/schemas.py +++ b/pkgs/clan-cli/clan_cli/webui/schemas.py @@ -34,9 +34,5 @@ class ConfigResponse(BaseModel): config: Config -class Schema(BaseModel): - pass - - class SchemaResponse(BaseModel): - schema_: Schema = Field(alias="schema") + schema_: dict = Field(alias="schema") diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index f6e63d75a..c57ed1ce9 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -18,6 +18,7 @@ , wheel , zerotierone , rsync +, pkgs }: let # This provides dummy options for testing clan config and prevents it from @@ -55,6 +56,8 @@ python3.pkgs.buildPythonPackage { format = "pyproject"; inherit CLAN_OPTIONS_FILE; + # This is required for the python tests, where some nix libs depend on nixpkgs + CLAN_NIXPKGS = pkgs.path; nativeBuildInputs = [ setuptools @@ -65,6 +68,7 @@ python3.pkgs.buildPythonPackage { passthru.tests.clan-pytest = runCommand "clan-tests" { nativeBuildInputs = [ age zerotierone bubblewrap sops nix openssh rsync stdenv.cc ]; + CLAN_NIXPKGS = pkgs.path; } '' export CLAN_OPTIONS_FILE="${CLAN_OPTIONS_FILE}" cp -r ${source} ./src diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix index 1a17315b5..965acaea5 100644 --- a/pkgs/clan-cli/shell.nix +++ b/pkgs/clan-cli/shell.nix @@ -9,7 +9,7 @@ let ] ); checkScript = pkgs.writeScriptBin "check" '' - nix build .#checks.${pkgs.system}.{treefmt,clan-mypy,clan-pytest} -L "$@" + nix build .#checks.${pkgs.system}.{treefmt,clan-pytest} -L "$@" ''; in pkgs.mkShell { @@ -18,8 +18,10 @@ pkgs.mkShell { self.packages.${pkgs.system}.nix-unit pythonWithDeps ]; - # sets up an editable install and add enty points to $PATH CLAN_FLAKE = self; + # This is required for the python tests, where some nix libs depend on nixpkgs + CLAN_NIXPKGS = pkgs.path; + # sets up an editable install and add enty points to $PATH # This provides dummy options for testing clan config and prevents it from # evaluating the flake .# CLAN_OPTIONS_FILE = ./clan_cli/config/jsonschema/options.json; diff --git a/pkgs/clan-cli/tests/config/example_flake/clanModules/machine1.nix b/pkgs/clan-cli/tests/config/example_flake/clanModules/machine1.nix new file mode 100644 index 000000000..8bf312e38 --- /dev/null +++ b/pkgs/clan-cli/tests/config/example_flake/clanModules/machine1.nix @@ -0,0 +1,7 @@ +{ lib, ... }: { + options.clan.jitsi.enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable jitsi on this machine"; + }; +} diff --git a/pkgs/clan-cli/tests/config/example_flake/flake.nix b/pkgs/clan-cli/tests/config/example_flake/flake.nix new file mode 100644 index 000000000..f245d04d1 --- /dev/null +++ b/pkgs/clan-cli/tests/config/example_flake/flake.nix @@ -0,0 +1,10 @@ +{ + inputs = { + # this placeholder is replaced by the path to nixpkgs + nixpkgs.url = "__CLAN_NIXPKGS__"; + }; + + outputs = _inputs: { + clanModules.machine-machine1 = ./clanModules/machine1.nix; + }; +} diff --git a/pkgs/clan-cli/tests/test_config.py b/pkgs/clan-cli/tests/config/test_config.py similarity index 100% rename from pkgs/clan-cli/tests/test_config.py rename to pkgs/clan-cli/tests/config/test_config.py diff --git a/pkgs/clan-cli/tests/config/test_machine_schema.py b/pkgs/clan-cli/tests/config/test_machine_schema.py new file mode 100644 index 000000000..799f7f075 --- /dev/null +++ b/pkgs/clan-cli/tests/config/test_machine_schema.py @@ -0,0 +1,44 @@ +import os +import tempfile +from pathlib import Path +from typing import Generator + +import pytest + +from clan_cli.config import machine + +CLAN_NIXPKGS = os.environ.get("CLAN_NIXPKGS", "") +if CLAN_NIXPKGS == "": + raise Exception("CLAN_NIXPKGS not set") + + +# fixture for the example flake located under ./example_flake +# The flake is a template that is copied to a temporary location. +# Variables like __CLAN_NIXPKGS__ are replaced with the value of the +# CLAN_NIXPKGS environment variable. +@pytest.fixture +def flake_dir() -> Generator[Path, None, None]: + template = Path(__file__).parent / "example_flake" + # copy the template to a new temporary location + with tempfile.TemporaryDirectory() as tmpdir_: + tmpdir = Path(tmpdir_) + for path in template.glob("**/*"): + if path.is_dir(): + (tmpdir / path.relative_to(template)).mkdir() + else: + (tmpdir / path.relative_to(template)).write_text(path.read_text()) + # in the flake.nix file replace the string __CLAN_URL__ with the the clan flake + # provided by get_clan_flake_toplevel + flake_nix = tmpdir / "flake.nix" + flake_nix.write_text( + flake_nix.read_text().replace("__CLAN_NIXPKGS__", CLAN_NIXPKGS) + ) + yield tmpdir + + +def test_schema_for_machine( + flake_dir: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.chdir(tmp_path) + schema = machine.schema_for_machine("machine1", flake_dir) + assert "properties" in schema