hub v0.15.0: Phase 2 managed updates — per-customer controller-version floor

Operator sets a minimum controller version (FLOOR), per-customer defaulting to a
global floor; the report ACK returns the effective floor + latest_version so the
controller auto-updates to the floor when below it (latest stays the opt-in button).

- store: min_controller_version column + hub_settings global floor + Effective/
  Get/SetGlobal/SetMin resolution + config/env DEFAULT_MIN_CONTROLLER_VERSION
- handler: report ACK {min_controller_version, latest_version}; LatestVersionProvider
- web: global floor editor + per-customer override form + Floor column (English)
- tests: floor resolution + ACK + render; override-precedence red-proof verified

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSZmmSFVzGwEzhYmxbkgBK
This commit is contained in:
2026-06-27 11:59:30 +02:00
parent ea09ead806
commit 30380a59f4
12 changed files with 614 additions and 111 deletions
+36 -91
View File
@@ -2,100 +2,45 @@
> **Overwrite** this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in [hub/CHANGELOG.md](hub/CHANGELOG.md).
---
## hub v0.15.0 — Phase 2 managed updates: per-customer controller-version floor (2026-06-27)
## Day-0 host-bootstrap script — built + live wipe-and-provision test — 2026-06-26
**Why.** Phase 1 gave enthusiasts an opt-in "update to latest" button. Phase 2 adds the managed
default: the operator sets a per-customer **minimum controller version** (defaulting to a global floor),
and any box below it **auto-updates** on its next report — so the household that never clicks stays
current and patched. Floor = the operator's enforced minimum and the auto-target; latest stays the
customer's manual opt-in.
**Task:** Deliver `scripts/felhom-host-install.sh` (operator-run Day-0 bootstrap) and validate it with
an **operator-authorized destructive live test**: wipe demo guest 9201 and re-provision it from scratch
via the script. Single repo (`felhom.eu`, new `scripts/`).
### Files changed
- `internal/store/store.go``customer_configs.min_controller_version` column; `hub_settings` KV table;
`defaultMinControllerVersion` + `SetDefaultMinControllerVersion`; `Get/SetGlobalMinControllerVersion`,
`SetMinControllerVersion`, `EffectiveMinControllerVersion`.
- `internal/api/handler.go``LatestVersionProvider` interface + `SetLatestVersionProvider`; report ACK
returns `{min_controller_version, latest_version}` (both omitted when empty).
- `internal/web/configs.go`, `server.go`, `templates/configs.html`, `templates/customer_unified.html`
operator UI: global floor editor + per-customer override form + Floor column / below-floor markers;
routes `POST /configs/global-floor`, `POST /customers/{id}/floor`.
- `cmd/hub/main.go``Config.ControllerUpdates.DefaultMinVersion` (yaml `default_min_version`, env
`DEFAULT_MIN_CONTROLLER_VERSION`); seeds the store default; wires the version checker as latest provider.
### Confirmed baselines
- hub `1539746` v0.14.0 (`/host-enroll` option C, just shipped) · felhom-agent `4725396` v0.41.0
(`--selftest=provision`) · felhom-controller `e0cf78b` v0.84.0.
- Live host `felhom-pve` (192.168.0.162): PVE 9.2.2, node `demo-felhom`, agent v0.41.0 at
`/root/.config/felhom-agent/agent.json` (systemd unit `-config` path — **not** `/etc/felhom-agent/`),
golden `local:backup/vzdump-lxc-9100-2026_06_23-16_38_49.tar.zst` (rootfs 32 / docker 16 / sysdata 8 GiB base).
### Design
- Effective floor = per-customer override if non-empty, else global (hub_settings → config/env default),
else "" (Phase 2 inert for that customer).
- Rides the **existing report cycle** — no new floor-polling endpoint. The actual swap is the
controller's Phase 1 flow (in-guest pull → agent `POST /controller/swap` → rollback); the hub only
advertises the floor. **No agent change.**
### Deliverable
- `scripts/felhom-host-install.sh` **v1.0.0** — 7 idempotent/resumable steps (pre-flight → pveum token →
grows → host-enroll → agent config → provision → verify), single-secret (passphrase no-echo/0600 file),
`--dry-run`/`--resume`/`--force`/`--preserve-from`, `--mode dr` documented stub.
- `scripts/README.md` (operator runbook), `scripts/CHANGELOG.md` (v1.0.0).
- `documentation/audits/SPIKE-…-2026-06-26.md` — pointer added (script ships the gap inventory).
- `bash -n` passes (Windows + host). **shellcheck unavailable** on dev box, build box, and felhom-pve — not run.
### Tests (`go test ./...` green)
- `internal/store/floor_test.go` — empty-when-unset; global-default fallback; DB-overrides-default;
**override-beats-global (companion red-proof)**; set-and-preserve-across-save.
- `internal/api/floor_ack_test.go` — ACK carries effective floor (override beats global) + latest; omits
both when unset.
- `internal/web/render_test.go` — template render smoke (catches `template.Must` panics + floor fields).
- **Companion red-proof (verified):** making `EffectiveMinControllerVersion` ignore the override →
`TestEffectiveFloor_OverrideBeatsGlobal` AND `TestReportACK_FloorAndLatest` FAIL → restored → green.
### Live supervised test (felhom-pve, customer `demo-felhom`, authorized destructive)
Passphrase sourced operator-side from the hub DB into a 0600 file on the host (never printed); deleted
(shredded) after. Methods noted per check.
### Green gate
`go build ./... && go vet ./... && go test ./...` → all pass.
**A. Pre-wipe snapshot:** saved `pct config 9201`, controller `0.84.0 Up (healthy)`, `agent.json.bak`
(1574 B), token list. Confirmed `demo-felhom` host exists in hub (`demo-felhom-01`, plus two stale
`dr-drill-host*` cruft rows — `demo-felhom-01` has today's `updated_at`, so `GetHostByCustomer` returns it).
**B. Wipe (authorized):** `pct stop 9201` + `pct destroy 9201 --purge` (all 3 volumes removed); moved
`agent.json` + cleared state to simulate a fresh host.
**C. Dry-run:** every mutating command printed correctly — pveum role-modify (16 privs), **both** ACL
lines, token rotate, enroll curl, agent.json write, provision call. Pre-flight green (golden auto-found,
passphrase valid, vmid free).
**D. Real run — found + fixed a bug, then SUCCESS:**
- **Bug found live:** first run failed the post-config agent selftest with `HTTP 403 … missing privilege
Sys.Audit`. Root cause: the script applied the **token ACL before rotating the token**, and
`pveum user token remove` **purges the token's ACL** — so the new token had no ACL. **Fix:** apply both
ACL grants **after** the token is (re)created. (`scripts/felhom-host-install.sh` step 2.)
- **Re-run green:** token rotated (old config token failed selftest → rotate → ACL → selftest **passed**);
`host-enroll` → **200 REUSE** of `demo-felhom-01` (idempotency confirmed); agent.json written 0600 +
read-only selftest passed + daemon restarted; provision front-half bring-up (1m11s) + back-half bootstrap
mount `mp9` → `/etc/felhom-bootstrap` + local-api token minted; guest **9201 running, onboot:1**, layout
rootfs 32G / mp0 200G / mp1 50G / mp8 bind — exactly the standard appliance.
**E. Verify (method per check):**
| Check | Method | Result |
|-------|--------|--------|
| guest running + onboot:1 + volumes | `pct status/config` | ✅ running, onboot 1, 32/200/50G + mp8 bind |
| controller healthy | `pct exec docker ps` | ✅ `Up (healthy)`, image `felhom-controller:0.77.0` (from golden) |
| **controller pulled config from hub** | controller logs (`pct exec`) | ✅ `bootstrap: pulled config from hub for demo-felhom, merged local_api … coming up configured`; `Hub connectivity … HTTP 200` |
| serving | `pct exec curl :80` | ✅ HTTP 301 (traefik) |
| **Cloudflare tunnel connected** | cloudflared logs | ✅ 4 `Registered tunnel connection` (vie06, bud01) |
| **public URL** | curl from build box (public DNS) | ✅ `felhom.demo-felhom.eu → HTTP 200` via Cloudflare `188.114.97.11` (ingress `*.demo-felhom.eu → traefik`) |
| real dashboard | curl public | ✅ `<title>Vezérlőpult — Felhom.eu</title>` |
| **host-report includes guest 9201** | hub sqlite | ✅ `demo-felhom-01/9201` status `running`, last_seen current |
| storage plane (functional touch) | `pct exec ls` | ✅ `/mnt/felhom-drives` bound, `felhom-flash` + `felhom-usb` visible (data intact); local-api drive plane reachable |
> Public URL returned `000` from felhom-pve and the Windows dev box — split-horizon (the agent's LAN
> resolver points `*.demo-felhom.eu` at the guest's LAN IP, which serves `:80` not `:443`). Through the
> Cloudflare edge (build box) it is **200**. Not a provision fault.
**F. Idempotency:** plain re-run **refused** to clobber the live 9201 (exit 1, "pass `--force`");
`--resume` re-run **skipped** token/grows/enroll/agent_config/provision → verify-only **no-op**, token
**not** rotated (selftest still passes), guest **not** re-provisioned.
**G. Cleanup:** passphrase file shredded; `agent.json.bak` + snapshot retained; final state — 9201
running, host_id `demo-felhom-01`, agent active.
### pveum / token automation result
Role `FelhomAgent` normalised to the authoritative **16 privileges** (incl. `Sys.Audit`, `SDN.Use`); user
`felhom-agent@pve`; privsep token rotated; **both** ACL grants (user **and** token) applied **after** token
creation. Read-only `felhom-agent --selftest` passed against the live API with the minted token.
### Observations (out-of-scope — not acted on)
1. **`-hub-password` on argv:** the agent's only input for the retrieval passphrase during provision is the
`-hub-password` flag → briefly visible in `ps` during `--selftest=provision`. **Candidate agent
follow-up:** accept it via env/stdin. (Not fixed here — would modify the agent.)
2. **Golden freshness:** the golden bakes controller **0.77.0**; the freshly provisioned demo runs 0.77.0
(pre-wipe was 0.84.0, and the v0.78.0 drive-register stable-path fix is newer than the golden). Rebuild
the golden at the current controller for parity; full drive **re-enroll** via the UI needs controller
≥0.78.0. The Day-0 script itself is version-agnostic.
3. **Multi-host cruft:** `demo-felhom` carries two stale `dr-drill-host*` rows. `GetHostByCustomer`
(most-recent-updated) correctly returned `demo-felhom-01` here, but the Day-0 invariant is one host per
customer — drill artifacts should be cleaned up (left untouched; out of scope).
4. **Serving + binary/golden delivery:** the script lives in `scripts/`; serving it at
`https://felhom.eu/scripts/…`, plus agent-binary and golden central delivery, are documented
productionization hooks (the test used the local binary + golden).
### Commits
- `<this commit>` — script v1.0.0 + scripts/README + scripts/CHANGELOG + spike pointer + REPORT.
No secrets in any committed file. PVE-token secret + host api_key live only in the host's 0600 agent config.
### Deploy
Built + pushed `gitea.dooplex.hu/admin/felhom-hub:0.15.0`, manifest bumped + synced (see hub/CHANGELOG
and the live-test summary in felhom-controller/REPORT.md for the end-to-end floor auto-update result).