Files
felhom-controller/REPORT.md
T

4.1 KiB

REPORT — pull-based config-refresh (controller v0.94.0)

Date: 2026-06-30 Task: retire the hub's inbound controls + add pull-based config delivery. Controller half (companion: hub v0.26.0). The controller re-pulls controller.yaml + self-restarts when the report ACK's config_version changes — the box-pulls-config replacement for the retired "Push Config".

Baselines

  • felhom-controller 464b14f (v0.93.0) → 419d3d0 (v0.94.0, live on guest 9201 / felhom-pve).
  • Companion hub v0.26.0 live on k3s (advertises config_version in the report ACK).

What changed

  • internal/report/pusher.go: PushResponse.ConfigVersion (json:"config_version").
  • internal/report/config_refresh.go (NEW): ConfigRefresher.Reconcile — testable reconcile with injected side effects. change → Refresh→Record→Restart; first-run records baseline (no restart); unchanged = no-op; failed pull keeps config (no record/restart, retries); record-before-restart.
  • internal/bootstrap/bootstrap.go: RefreshConfig — re-pull + re-write controller.yaml, re-merging local_api from bootstrap.json (reuses pullWithRetry/mergeLocalAPI/writeFileAtomic). Overwrites controller.yaml (hub = source of truth); never touches settings.json; fail-safe.
  • internal/settings/settings.go: applied_config_version + Get/Set.
  • internal/api/selfrestart.go: exported GracefulSelfRestart (reuse; the unexported one calls it).
  • cmd/controller/main.go: wired ConfigRefresher into OnPushResponse beside the floor reconcile.

Tests & green gate

  • go build ./... && go vet ./... && go test ./...all green.
  • Reconcile: change→refresh+record+restart; same-version NO-OP red-proof; baseline-no-restart; failed-pull no-record/no-restart; zero-version no-op; record-fail skips restart.
  • RefreshConfig: re-pull overwrites + re-merges local_api; failed pull leaves config unchanged; absent bootstrap errors without writing.
  • Red-proof (no-restart-storm): removing the ackVersion == applied guard makes TestReconcile_SameVersion_NoOp FAIL (refreshes=1/restarts=1, want 0) — proving the guard prevents a restart-every-report loop. Restored → green.

Live validation (guest 9201 on felhom-pve, hub.felhom.eu)

Deployed 0.94.0 via the golden/bootstrap mechanism; push_interval temporarily 1m for fast cycles (restored to 15m after).

  1. First-run baseline (no restart): first report after deploy recorded applied_config_version=1 with RestartCount=0 — baseline recorded, no restart.
  2. Config edit converges: bumped demo-felhom config_version 1→2 in the hub DB (equivalent to a config save). Next report ACK carried 2; controller logged: config-refresh: hub config_version=2 != applied=1 — re-pulling controller.yamlre-pulled … merged local_api (192.168.0.162:8443)applied config_version=2 — self-restartingGraceful self-restart: exiting (0). Container restarted (RestartCount 0→1, healthy); applied_config_version=2; the re-pull is proven by web.session_secret changing (e1ed2dd3…f456197e…, regenerated by the hub each pull); local_api block preserved.
  3. No restart storm: 4 further report cycles at config_version=2 → zero config-refresh activity, container stayed up (no further restart).
  4. Apps unaffected: during the config-apply restart only felhom-controller restarted — every customer app (nextcloud, gitea, calcom, rallly, sparkyfitness, …) stayed Up (healthy).
  5. No inbound: the whole flow ran with the hub never connecting into the box (it only answers the box's report); hub grep client.Do internal/web/ is clean of ControllerURL targets.

Notes

  • A config change rotates web.session_secret (configgen regenerates it per pull) → controller dashboard sessions are invalidated on a config apply. This is identical to the retired Push Config behaviour (same configgen path) — no regression; only on actual config changes, not every report.
  • The first-boot bootstrap.MaybeIngest never-clobber is untouched; the refresh is a separate explicit re-pull keyed on a version change.