REPORT: hub v0.14.0 host-enroll — shipped + live-verified
Per-test results + companion red-proof, deployed v0.14.0 pod verification, live throwaway enroll/reuse/401/404/400 + Bearer-auth check, cleanup confirmed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7
This commit is contained in:
@@ -4,44 +4,83 @@
|
||||
|
||||
---
|
||||
|
||||
## REPORT — hub v0.13.1: DR recipe v1 drive-shape sync (test-data + regression guard only)
|
||||
## hub v0.14.0 — Passphrase-authed host enrollment (Day-0 option C) — 2026-06-26
|
||||
|
||||
**TASK — DR-recipe completion (hub side of a cross-repo change).** The agent's **v0.39.0** dropped two
|
||||
fields from the host-half `drives[]` (`role`, `restic_repo_coord`) and now resolves the `pbs` coord live.
|
||||
The hub change is **test-data + a regression guard only — NO behavior change; redeploy optional**, because
|
||||
the hub already reads drive sections as `json.RawMessage` (verbatim passthrough): dropping fields needs no
|
||||
store/handler/struct change.
|
||||
**Task:** Implement option C from `documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md`:
|
||||
a passphrase-authed, mint-once-reuse host-enrollment endpoint so Day-0 carries **one** secret (the
|
||||
retrieval passphrase) and the global operator key never enters the field deploy path. Single repo
|
||||
(`felhom.eu`, `hub/`).
|
||||
|
||||
**Baseline:** v0.13.0 (`5f5e3c5`) → **v0.13.1** (`149a3b0`, pushed to `main`).
|
||||
### Confirmed baseline
|
||||
- felhom.eu (hub) `main` @ `230980f` (= origin/main, verified by `git fetch`), version `v0.13.1`
|
||||
(ldflags-injected; no source constant) → target `v0.14.0`.
|
||||
- Reconfirmed every relied-on symbol live: `handleConfigRetrieve` (auth pattern), `handleAdminCreateHost`
|
||||
(mint block — left untouched), `UpsertHost` (`ON CONFLICT(host_id)`), `GetHostByAPIKey`/`GetHost`/
|
||||
`scanHost`/`hostSelectCols`, `idx_hosts_customer`, `configgen.RandomHex`.
|
||||
|
||||
### Changes
|
||||
- `internal/store/testdata/dr-recipe.golden.json` + the `drHostHalf` test fixture — dropped the `role`
|
||||
key from `drives[0]` to match the v1 shape the agent emits.
|
||||
- `internal/api/testdata/host-report.golden.json` — re-synced **byte-identical** with the agent's
|
||||
`internal/hub/testdata/host-report.golden.json` (sha256 `57f2a5e7…18b2f2b5`). The hub copy previously
|
||||
lacked the `dr_recipe` section entirely; it is now a verbatim copy, so the cross-repo golden truly
|
||||
matches and POSTing it through `/host-report` exercises the `SaveDRRecipeHostHalf` ingest path.
|
||||
- New `internal/store/dr_recipe_test.go::TestAssembleDRRecipe_V1DriveShape` — the regression guard: a
|
||||
stored host half whose `drives[]` carry NEITHER dropped field but WHICH HAS a `pbs` block assembles
|
||||
cleanly (pbs carried through, drives passed through verbatim, neither `role` nor `restic_repo_coord`
|
||||
present). Demonstrated to FAIL when the fixture re-adds `role`, then reverted.
|
||||
### Files changed
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `hub/internal/store/store.go` | **+** `Store.GetHostByCustomer` (SELECT … WHERE customer_id=? ORDER BY updated_at DESC LIMIT 1; nil-on-not-found; uses `idx_hosts_customer`) |
|
||||
| `hub/internal/store/host_test.go` | **+** `TestGetHostByCustomer` (none→nil / one / two→most-recent / other-customer) |
|
||||
| `hub/internal/api/handler.go` | **+** route `POST /host-enroll` (exact-match, beside `/host-report`); **+** `handleHostEnroll` (passphrase auth → mint-once-reuse). `handleConfigRetrieve` + `handleAdminCreateHost` **unchanged** |
|
||||
| `hub/internal/api/host_enroll_test.go` | **+** 5 scenario tests (mint/reuse/401-no-mint/404/400) |
|
||||
| `hub/CHANGELOG.md` | **+** v0.14.0 entry (newest on top) |
|
||||
| `hub/README.md` | **+** "Host Enrollment (Day-0, option C)" section |
|
||||
| `documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md` | **+** "c-pref SHIPPED in v0.14.0" pointer in §5 |
|
||||
|
||||
### Verification
|
||||
- `go build ./... && go vet ./... && go test ./...` — **green**.
|
||||
- **Live (the real acceptance):** after deploying the daemon (agent v0.39.0) and its startup host-report
|
||||
landing in the hub DB, the **real `AssembleDRRecipe`** over a copy of the live `/data/hub.db` produced
|
||||
the `demo-felhom` recipe — `recipe_version 1`, 3 guests w/ sizing, **`pbs` present** (`felhom-pbs/root/9201`),
|
||||
**2 drives clean** (no `role`/`restic_repo_coord`), 5 pve_storage, **14 apps**. **Secret scan over the
|
||||
full assembled recipe AND both raw stored halves = 0 hits** (nothing to redact — the point of the arc).
|
||||
(Operator HTML panel not browser-rendered: no dashboard password in this env; the server pipeline +
|
||||
exact stored/assembled bytes were verified.)
|
||||
### Commits
|
||||
- `8098237` — `hub v0.14.0: passphrase-authed host enrollment (Day-0 option C)` (code + tests + CHANGELOG + README + spike pointer).
|
||||
- `<this REPORT commit>` — REPORT.md overwrite.
|
||||
|
||||
### Cross-repo golden checksum
|
||||
`hub/internal/api/testdata/host-report.golden.json` ≡ `felhom-agent/internal/hub/testdata/host-report.golden.json`:
|
||||
`57f2a5e7154ece1be94a79fd327f2924762e70c3ee388d6e375df7c918b2f2b5` (both).
|
||||
### Tests (temp-sqlite stores; no network)
|
||||
- **Per-scenario (all PASS):**
|
||||
- A — first enroll mints → `201`, `host_id=c1-<6hex>`, `api_key=64hex`, exactly 1 host row, `GetHostByAPIKey(minted)` resolves.
|
||||
- B — second enroll **reuses** → `200`, creds **byte-for-byte identical**, still exactly 1 host row.
|
||||
- C — wrong passphrase → `401`, host-row count **unchanged at 0** (auth-before-mint proven).
|
||||
- D — unknown customer → `404`, no mint.
|
||||
- E — missing header → `401`; missing `customer_id` → `400`.
|
||||
- store `TestGetHostByCustomer` — none→nil, one→hit, two→most-recently-updated.
|
||||
- **Companion red-proof (§10):** temporarily disabled the reuse branch (always-mint) → Scenario B
|
||||
**FAILED** as required (`second status = 201, want 200` — second call returned a *new* `host_id`
|
||||
`c1-0d44c6`, not the first). Reverted to mint-once-reuse → green. This proves the idempotency test is
|
||||
load-bearing, not hollow.
|
||||
- **Test count:** api package 29 → **34** (+5); store package 11 → **12** (+1).
|
||||
- **Green gate:** `go build ./... && go vet ./... && go test ./...` — all `ok` (api, configgen, monitor,
|
||||
store), no failures.
|
||||
|
||||
### BACKLOG flagged (not acted on)
|
||||
External-drive bulk data has **no offsite / second-failure-domain backup** — cross-drive (Tier-2) rsync
|
||||
lands on the same internal SSD in the same guest. This is why `restic_repo_coord` named nothing real and
|
||||
was dropped from v1. Design a genuine off-host bulk-data tier before re-introducing a drive backup coord.
|
||||
Full detail in `felhom-agent/REPORT.md` §8.
|
||||
### Deploy + pod verification
|
||||
- `./build.sh 0.14.0 --push` on the build box (auto-pulls `8098237`) → image
|
||||
`gitea.dooplex.hu/admin/felhom-hub:0.14.0` pushed (digest `sha256:541cdb54…`, 24M).
|
||||
- `kubectl set image deploy/hub` → rollout succeeded; pod `hub-6c7d8bcc9-454n9` `1/1 Running`.
|
||||
- Startup log: `[INFO] felhom-hub 0.14.0 starting`; deployed image confirmed `…:0.14.0`.
|
||||
|
||||
### Live verification against the real endpoint (throwaway `enroll-spike-2026-06-26`)
|
||||
Curl-against-real-endpoint (the exact call the future host-bootstrap script makes). Secrets stayed
|
||||
resident on the build box; only statuses / non-secret host_id / identical-bytes booleans surfaced.
|
||||
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| 1. first enroll (correct passphrase) | **201**, `host_id=enroll-spike-2026-06-26-c641d7`, api_key 64-hex |
|
||||
| 2. second enroll (idempotent) | **200**, host_id identical YES, api_key identical YES, **exactly 1 host row** |
|
||||
| 3. wrong passphrase | **401** |
|
||||
| 4. unknown customer | **404** |
|
||||
| 5. missing customer_id | **400** |
|
||||
| 6. **Bearer-auth proof** — `GET /hosts/{host_id}/desired-state` with `Authorization: Bearer <enrolled key>` | **200** (the enrolled credential is a real, working host key end-to-end) |
|
||||
|
||||
### Cleanup (throwaway discipline, mirroring the spike)
|
||||
- Deleted throwaway `customer_configs` / `hosts` / `host_reports` rows → counts **0/0/0** (verified).
|
||||
- Real `demo-felhom` customer confirmed **still present / untouched**.
|
||||
- Uninstalled temporary `sqlite` from the hub pod (`apk del`); removed all `/tmp/enroll-spike*` scratch.
|
||||
- The global key was read out-of-band into a shell var only; **no secret** written to disk, any
|
||||
committed file, or this report.
|
||||
|
||||
### Observations / out-of-scope (not acted on)
|
||||
- `POST /admin/hosts` is still **PROVISIONAL** (`handler.go:595`) and is left **as the operator/global
|
||||
escape hatch** — its removal/lock-down is a separate cutover decision, deliberately not done here.
|
||||
- `GET /config/{id}` was **not** modified (no host fields, no JSON, raw-YAML body intact) — the in-guest
|
||||
controller pull is unaffected.
|
||||
- The host-bootstrap script (Day-0 wiring) is the **next** task — not started here. `host-enroll`'s
|
||||
mint-once-reuse is the idempotency primitive that step will depend on.
|
||||
- Under option A, `admin/hosts` re-mint still orphans a prior host (no idempotency there); option C
|
||||
fixes this structurally for the enroll path.
|
||||
|
||||
Reference in New Issue
Block a user