Add typed settings areas to the REST API

/api/v1/settings/router/firewall, .../router/lans/<name>, .../system, down to
a single option: GET reads, PUT replaces, PATCH merges, DELETE removes, with
the settings' ETag/If-Match and the same limits as the whole-document edit
(editSettings is now shared). Values are checked against a JSON Schema made
from the option documentation (schema.go reads the types, which nixosOptionsDoc
describes in words); a wrong type, a value out of range or an unknown key is
refused with its place. The OpenAPI document has the area paths with their
schemas; the API reference page lists every area's options.

Tests: unit tests for the type reader, the validator and the paths; the real
option file and a router's own settings validate (ROUTER_OPTIONS_JSON,
LIVE_SETTINGS_JSON); a subtest in apply.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-11-01 20:09:20 +01:00
co-authored by Claude Sonnet 5.5
parent 42ceb78d2b
commit 64e08fb485
11 changed files with 1064 additions and 10 deletions
+13
View File
@@ -296,6 +296,19 @@ pkgs.testers.runNixOSTest {
assert client.succeed(f"curl -sk -o /dev/null -w '%{{http_code}}' {ui}/api/v1/status").strip() == "401"
spec = json.loads(client.succeed(f"curl -sk {ui}/api/openapi.json"))
assert spec["openapi"].startswith("3.") and "/api/v1/logs" in spec["paths"]
# Areas of the settings, typed by the real option documentation.
area = f"{rest} -H 'Content-Type: application/json'"
etag = client.succeed(f"{rest} -D - -o /dev/null {ui}/api/v1/settings/router/lans/main | tr -d '\\r' | sed -n 's/^[Ee][Tt]ag: *//p'").strip()
assert etag, "no ETag from an area"
assert json.loads(client.succeed(f"{rest} {ui}/api/v1/settings/router/lans/main"))["interface"]
code = client.succeed(f"{area} -o /tmp/area.json -w '%{{http_code}}' -X PUT -H 'If-Match: {etag}' -d '{{\"domian\": \"x\"}}' {ui}/api/v1/settings/router/dns").strip()
assert code == "400" and "unknown option" in client.succeed("cat /tmp/area.json"), code
code = client.succeed(f"{area} -o /tmp/area.json -w '%{{http_code}}' -X PUT -H 'If-Match: {etag}' -d '{{\"domain\": \"area.test\"}}' {ui}/api/v1/settings/router/dns").strip()
assert code == "200", (code, client.succeed("cat /tmp/area.json"))
assert "area.test" in mcp("get_pending_changes")[0]
assert not mcp("discard_changes")[1]
spec = json.loads(client.succeed(f"curl -sk {ui}/api/openapi.json"))
assert "/api/v1/settings/router/lans/{name}" in spec["paths"]
# The log shows what was done, by whom.
assert "update_settings" in curl(f"{ui}/tokens") and "assistant" in curl(f"{ui}/tokens")
'';