Files
felhom-controller/controller/internal/report/config_refresh.go
T
admin 2487681396 style: gofmt normalization — no logic changes
gofmt -w across the controller tree (46 files) so gofmt -l is empty — disarms the
formatting landmine where a targeted edit + accidental gofmt -w swept ~46 unrelated
files. Pure formatting: whitespace + gofmt's optional-semicolon removal in reflowed
inline closures. One doc comment reworded ('' -> 'the empty string') to avoid gofmt's
Go-1.19 doc-comment typographic substitition ('' -> curly quote) muddying its meaning.
No build/vet/test behavior change.
2026-07-25 07:37:02 +02:00

76 lines
3.5 KiB
Go

package report
import "log"
// ConfigRefresher reconciles the hub-advertised config_version (from the report ACK) against the
// controller's last-applied version and, on a change, re-pulls controller.yaml and self-restarts —
// the pull-based config-delivery path (the hub never connects into the box; this rides the existing
// report cycle exactly like the Phase 2 version floor).
//
// All side effects are injected so the reconcile is unit-testable without a real hub / filesystem /
// process exit:
// - Applied reads the persisted last-applied config_version (0 = none recorded yet).
// - Record persists a newly-applied config_version.
// - Refresh re-pulls controller.yaml from the hub and writes it (re-merging local_api). It must
// NOT touch settings.json. A hub-unreachable / write failure returns an error.
// - Restart triggers the graceful self-restart (process exit → Docker restart → fresh config).
type ConfigRefresher struct {
Applied func() int
Record func(int) error
Refresh func() error
Restart func()
Logger *log.Logger
}
// Reconcile applies the config-refresh decision for one report ACK. Rules (acceptance B + safety §5):
// - ackVersion == 0 → no-op (hub didn't advertise; old hub / no config row).
// - no version recorded yet → record the baseline WITHOUT restarting (first-ever ACK; the
// first-boot pull already fetched the current config).
// - ackVersion == applied → no-op (no change; this is what prevents a restart storm — after a
// refresh, applied == ackVersion so the next report is a no-op).
// - ackVersion != applied → Refresh; on success Record THEN Restart (record-before-restart so
// the post-restart process sees it applied); on a failed Refresh keep the current config, do NOT
// record, do NOT restart — retried on the next report cycle.
func (cr *ConfigRefresher) Reconcile(ackVersion int) {
if ackVersion == 0 {
return // hub didn't advertise a config_version
}
applied := cr.Applied()
if applied == 0 {
// First-ever ACK carrying a config_version: record the baseline, do NOT restart (the box came
// up on the first-boot pull, which already has the current config). Mirrors the floor's
// first-run-records-baseline.
if err := cr.Record(ackVersion); err != nil {
cr.logf("[WARN] config-refresh: failed to record baseline config_version=%d: %v", ackVersion, err)
return
}
cr.logf("[INFO] config-refresh: baseline config_version=%d recorded (no restart)", ackVersion)
return
}
if ackVersion == applied {
return // no change
}
cr.logf("[INFO] config-refresh: hub config_version=%d != applied=%d — re-pulling controller.yaml", ackVersion, applied)
if err := cr.Refresh(); err != nil {
// Fail-safe: keep the current config, do NOT record, do NOT restart — retry next cycle.
cr.logf("[WARN] config-refresh: re-pull failed: %v — keeping current config, will retry next report", err)
return
}
// Record BEFORE restarting so the freshly-started process sees the version as applied and does not
// loop. (The restart is delayed, so the record persists first.)
if err := cr.Record(ackVersion); err != nil {
cr.logf("[WARN] config-refresh: applied config but failed to record version=%d: %v — skipping restart to avoid a loop", ackVersion, err)
return
}
cr.logf("[INFO] config-refresh: applied config_version=%d — self-restarting to load it", ackVersion)
if cr.Restart != nil {
cr.Restart()
}
}
func (cr *ConfigRefresher) logf(format string, args ...interface{}) {
if cr.Logger != nil {
cr.Logger.Printf(format, args...)
}
}