milestone 32: task-shaped configuration, file mode as a rendering, config status api
Gates / frontend (push) Successful in 1m22s
Gates / test (push) Successful in 1m54s
Gates / test-aarch64 (push) Successful in 7m57s
Gates / package (push) Successful in 5m29s
Gates / container (push) Successful in 10s
CI / gates (push) Successful in 32m51s
Gates / frontend (push) Successful in 1m22s
Gates / test (push) Successful in 1m54s
Gates / test-aarch64 (push) Successful in 7m57s
Gates / package (push) Successful in 5m29s
Gates / container (push) Successful in 10s
CI / gates (push) Successful in 32m51s
This commit is contained in:
+14
-8
@@ -4,7 +4,7 @@ nxdns serves its admin API itself, on `web.bind:web.port` (default port 8080), a
|
||||
|
||||
The machine-readable contract is `src/web/openapi.yaml`, which the running server hands out unauthenticated at `GET /api/openapi.yaml`. Request and response schemas for every operation live there. When this page and the YAML disagree, the YAML wins.
|
||||
|
||||
The route table is `src/web/routes.zig`; the [Operations](#operations) table below carries all 63 of its entries.
|
||||
The route table is `src/web/routes.zig`; the [Operations](#operations) table below carries all 64 of its entries.
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -15,7 +15,7 @@ The route table is `src/web/routes.zig`; the [Operations](#operations) table bel
|
||||
413.
|
||||
- A request whose path matches but whose method does not answers 405 with an `Allow` header. An unknown `/api` path is a JSON 404; unknown non-`/api` paths fall through to the embedded SPA (`index.html`), so client-side routing works.
|
||||
- Item routes (`{id}`) match a positive integer id only.
|
||||
- Mutations to groups, blocklists, rules, local records, forward zones, clients and client prefixes take effect live. Upstreams and `/api/settings` are restart-required.
|
||||
- Mutations to groups, blocklists, rules, local records, forward zones, clients and client prefixes take effect live. Upstreams and `/api/settings` are restart-required, and once one of them is written `GET /api/config/status` reports `restart_pending: true`.
|
||||
- Every route has a policy class — `read`, `config_write` or `runtime_action` — and in file mode the `config_write` routes are refused. See [Configuration authority](#configuration-authority).
|
||||
|
||||
## Authentication
|
||||
@@ -62,7 +62,7 @@ A `config write` route in file mode answers **403** with the ordinary error enve
|
||||
{"error":"configuration is managed by /etc/nxdns/config.zon; edit the file and restart"}
|
||||
```
|
||||
|
||||
There is no `code` field and no richer body. 403 is used for nothing else in this API, so the status alone is the machine-readable part, and a client that wants to know the mode in advance reads it from `GET /api/settings` rather than probing for errors.
|
||||
There is no `code` field and no richer body. 403 is used for nothing else in this API, so the status alone is the machine-readable part, and a client that wants to know the mode in advance reads it from `GET /api/config/status` rather than probing for errors.
|
||||
|
||||
**401 comes first.** The router matches the path, spends a rate-limit token, checks the session, and only then checks the policy. So an unauthenticated request to a `config write` route in file mode is a 401, not a 403 — answering 403 first would tell an anonymous caller which routes exist.
|
||||
|
||||
@@ -72,23 +72,26 @@ There is no `code` field and no richer body. 403 is used for nothing else in thi
|
||||
|
||||
### Discovering the authority
|
||||
|
||||
`GET /api/settings` carries an `authority` object:
|
||||
`GET /api/config/status` answers with the authority and the process's restart state:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `mode` | `"database"` or `"managed_file"`. |
|
||||
| `authority` | `"database"` or `"managed_file"`. |
|
||||
| `path` | The managed file's path, or `null` in database mode. |
|
||||
| `reconciled_at` | Unix seconds when this process loaded the file, or `null` in database mode. |
|
||||
| `restart_pending` | True once this process has committed a configuration change that only a restart applies. |
|
||||
|
||||
All three keys are always present; the two nullable ones carry `null` rather than being omitted, so a client can read `authority.mode` without probing.
|
||||
All four keys are always present; the two nullable ones carry `null` rather than being omitted, so a client can read `authority` without probing.
|
||||
|
||||
```json
|
||||
{"mode": "database", "path": null, "reconciled_at": null}
|
||||
{"mode": "managed_file", "path": "/etc/nxdns/config.zon", "reconciled_at": 1786474016}
|
||||
{"authority": "database", "path": null, "reconciled_at": null, "restart_pending": false}
|
||||
{"authority": "managed_file", "path": "/etc/nxdns/config.zon", "reconciled_at": 1786474016, "restart_pending": false}
|
||||
```
|
||||
|
||||
The route requires a session, which is why the filesystem path is here rather than on the open `/api/version` and `/api/health`.
|
||||
|
||||
`restart_pending` is per-process state and nothing but process exit clears it. It rises when this server writes an upstream or a settings key — the changes the running process cannot apply — and it is never persisted, so a `false` read after a restart means the restart happened, not that the flag was cleared. In file mode it stays false: those writes are refused before any handler runs.
|
||||
|
||||
`reconciled_at` answers exactly one question: **when did this process last read the file?** Compare it against the file's mtime to spot a restart that has not happened yet. It is a hint and not a verdict, in both directions — a clock that stepped, or a copy that preserved mtimes (`git checkout`, `rsync -a`), can make a newer file look older, and the database can change without either timestamp moving. It does not tell you whether the file and the running configuration agree; answering that would take content hashing, which nxdns deliberately does not do.
|
||||
|
||||
## Operations
|
||||
@@ -159,6 +162,7 @@ Auth `open` means no session is required; `session` means a valid session cookie
|
||||
| POST | `/api/pause` | session | counted | runtime action | Pause or resume blocking |
|
||||
| GET | `/api/settings` | session | counted | read | Read the scalar settings |
|
||||
| PUT | `/api/settings` | session | counted | config write | Update settings |
|
||||
| GET | `/api/config/status` | session | counted | read | Read the configuration authority and restart state |
|
||||
| POST | `/api/certs/reload` | session | counted | runtime action | Reload the TLS certificates from disk |
|
||||
|
||||
There is no `POST /api/clients`: client rows come from DNS activity or import, never from the API.
|
||||
@@ -169,6 +173,8 @@ Static assets are not routes. The router sends unmatched non-`/api` paths to the
|
||||
|
||||
## Settings keys
|
||||
|
||||
The envelope both operations answer with is `{settings, restart_required}`: the stored values, and the list of keys a restart applies. It says nothing about the live authority or a restart already owed — those are per-process facts, and `GET /api/config/status` is their one home.
|
||||
|
||||
`GET /api/settings` and `PUT /api/settings` speak the `section.field` keys of [the configuration reference](configuration.md), with the values in their database spelling — notably `logging.level` is `"error"`, not `"err"`. Two keys behave differently over the API than in the file: `web.password` is write-only (accepted on a `PUT`, never returned, hashed before storage), and `web.password_hash` is neither readable nor directly writable, because a client that could install a hash could install one whose password it already knows.
|
||||
|
||||
In file mode `PUT /api/settings` is refused with the 403 above, password changes included. The password then lives where the rest of the configuration lives: set `web.password` in the file and restart. See [Password and hash](configuration.md#password-and-hash).
|
||||
|
||||
Reference in New Issue
Block a user