milestone 20: declarative configuration for iac

This commit is contained in:
2026-08-11 23:31:40 +02:00
parent 2f29121e27
commit d76afc147a
74 changed files with 6722 additions and 1949 deletions
+49 -22
View File
@@ -579,10 +579,14 @@ No `std.log.err` in any new code, per the standing spec rule for new code
- **Adopt file mode** on a UI-configured box: **stop the service first**, then
`nxdns export --out /etc/nxdns/config.zon`
`nxdns check --config=/etc/nxdns/config.zon` → add the flag → start.
Stop-first is load-bearing twice: `export` opens the DB immutable and
refuses with `WalPending` against a live instance's steady-state WAL
(db.zig:222-226, cli.md:121), and any UI edit landing between a live export
and the restart would be silently reverted by the first reconcile. Stopped,
Stop-first is load-bearing twice: any UI edit landing between a live
export and the restart would be silently reverted by the first reconcile,
and `check` refuses to grade a live database (immutable open, `WalPending`
against the steady-state WAL, db.zig:222-226) so the pre-flight gate only
works stopped. (Sync note, found in R4: the spec originally claimed
`export` itself refuses against a live instance — it does not; `export`
opens read/write and succeeds. The claim was corrected to name `check`.)
Stopped,
the first reconcile's summary is all-zero and writes nothing — blocklist
state, compiled files, and client history all survive. Every unchanged boot
after it writes nothing either.
@@ -766,24 +770,29 @@ api.md/cli.md rows), and the `-Dlive` acceptance run.
## Acceptance (design complete when implemented)
- [ ] `nxdns run --config=<file>` on a DB with fetched blocklists (any boot
- [x] `nxdns run --config=<file>` on a DB with fetched blocklists (any boot
after adoption): restart performs zero downloads **and zero DB writes**;
source ids, checksums, `last_updated`, and compiled
`<id>.list`/`<id>.wild` files are identical before and after. Proven
once with `-Dlive` against a real source.
- [ ] Reconciling an unchanged exported config twice yields a byte-identical
once with `-Dlive` against a real source. *(Closed by
filter_integration test 10e: a hermetic HTTP fixture counts accepted
connections across a full Manager restart — 1 download total; inode,
mtime and source ids identical. The `-Dlive` gate passed 1559/1559
with 0 skips, which includes this test against the fixture and the
live-network suites against real upstreams.)*
- [x] Reconciling an unchanged exported config twice yields a byte-identical
`dump()` and an all-zero summary — including with a plaintext
`password` in the file, and with non-canonical addresses.
- [ ] Removing (and renaming) a group that observed clients were assigned to
- [x] Removing (and renaming) a group that observed clients were assigned to
converges: clients land in the default group, no FK error, counts
reported. Declaring an observed client's IP promotes the row in place:
`first_seen`/`last_seen` and row id survive, counted as `updated`.
- [ ] A file with neither `password` nor `password_hash` leaves the stored
- [x] A file with neither `password` nor `password_hash` leaves the stored
hash — and auth — intact; `password_hash = ""` disables auth and the
startup summary says so; a present-but-empty `password` is refused at
validate with a diagnostic naming `password_hash = ""` as the disable
path.
- [ ] `run --config=<file>` with a missing file exits 2 with the path in
- [x] `run --config=<file>` with a missing file exits 2 with the path in
the message; with an invalid file exits 2 with diagnostics; never serves
from the DB. An open failure outside the path class (fd exhaustion, I/O
error) exits 1, not 2. `check --config=<file>` agrees with `run`
@@ -791,38 +800,56 @@ api.md/cli.md rows), and the `-Dlive` acceptance run.
claim of ruling 2).
- [ ] A config file present at `/etc/nxdns/config.zon` with no `--config`
flag changes nothing: bare `run` serves the DB and never reads the
file.
- [ ] `nxdns import` whose diff would delete rows fails exit 2 without
file. *(Design claim, verified structurally: no code path opens
`/etc/nxdns` — the deletion gate below proves the seed-by-presence
path is gone, and tests cover bare `run` with a config file present
in a lab directory. Not executed against the literal path
`/etc/nxdns/config.zon` on a host that has one; this machine does
not run nxdns from /etc.)*
- [x] `nxdns import` whose diff would delete rows fails exit 2 without
`--allow-delete`, printing per-table delete counts, and rolls back;
with the flag it applies; an additive import needs no flag.
- [ ] Fresh empty DB in db mode exits 2 (`NoUsableUpstreams`) with the hint
line; bare `check` with no `config.db` exits 2 with the
same hint; neither restart-loops under the shipped unit
(`RestartPreventExitStatus=2 64`).
- [ ] The shipped compose file boots a fresh container (empty volume, mounted
*(Exit codes and hint lines are test-covered and closed. The
no-restart-loop half is a design claim: the unit line parses under
`systemd-analyze verify`, but no root systemd host was available to
observe systemd actually holding the unit down. Close it on the Pi 5
deployment.)*
- [x] The shipped compose file boots a fresh container (empty volume, mounted
config.zon) into file mode successfully; the db-mode import recovery
one-liner is documented and works.
- [ ] In file mode: every `config_write` route answers 403 with the
one-liner is documented and works. *(Run against a locally built
image: first boot reconciled `upstreams +1 ~0 -0; settings +45` with
auth enabled; second boot logged no changes.)*
- [x] In file mode: every `config_write` route answers 403 with the
single-field error envelope — `application/json` even when the managed
path is long; every `runtime_action` and `read` route behaves as in db
mode; DELETE of an observed client succeeds, of a declared client
answers 403; unauthenticated requests to protected routes still answer
401, not 403.
- [ ] `GET /api/settings` reports `authority` with `reconciled_at` (null in
- [x] `GET /api/settings` reports `authority` with `reconciled_at` (null in
db mode); the UI shows the read-only banner and disables mutation
controls in file mode.
- [ ] `rg -n 'import\.isEmpty|content_tables|config/bootstrap|seedFromFile|config_explicit' src/`
- [x] `rg -n 'import\.isEmpty|content_tables|config/bootstrap|seedFromFile|config_explicit' src/`
returns nothing (historical specs exempt; pattern chosen so
fetcher.zig's `host.isEmpty()` and validate.zig's "bootstrap problem"
prose cannot false-positive).
- [ ] Adopt-file-mode walkthrough (stop → `export``check --config` → add
- [x] Adopt-file-mode walkthrough (stop → `export``check --config` → add
the flag → start) run end to end on a UI-configured instance: the first
reconcile summary is all-zero and writes nothing, as does every
unchanged boot after it; leave-file-mode (drop the flag, restart)
serves identically; `export` against the *running* instance refuses
with the WalPending message, as documented.
- [ ] All existing gates pass; tripped drift guards are regenerated, not
suppressed.
serves identically; `check` against the *running* instance's database
refuses with the uncheckpointed-WAL message, as documented (corrected
from `export`, which opens read/write and succeeds live — see ruling 9's
sync note).
- [x] All existing gates pass; tripped drift guards are regenerated, not
suppressed. *(Final numbers: `zig build test` 1429/1559 pass, 130
skipped, 0 failed; `-Dintegration` 1555/1559, 4 skipped, 0 failed;
`-Dintegration -Dlive` 1559/1559, 0 skipped, 0 failed. The live gate
also caught and fixed storage S7 case 22, whose expectation had been
stale since milestone 13 because nothing ran `-Dlive` in between.)*
## Anti-requirements