milestone 20: declarative configuration for iac
This commit is contained in:
+49
-22
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user