hub v0.82.0 (R-120): the vouch path REFUSES a golden the fleet has already outrun
The golden's version IS the controller it bakes (build-golden.sh:345 defaults GOLDEN_VERSION to the controller tag), so a golden behind the newest deployed controller means every FRESH install lands on stale application code. On the R-120 occurrence that stale code shipped a customer-facing falsehood: a box from the 0.185.1 golden told a customer whose backup drive had fallen out that the backup was on the same disk as the system -- false, the drive was gone -- and offered a different drive as the remedy. WHY A GATE, NOT A REMINDER. The gap has opened three times: R-111 (golden's agent 17 releases behind), R-115 (agent built and deployed, never published), R-120 (this). The first two were closed by re-baking and remembering; remembering then failed again. R-29 is the standing proof that a check nobody runs is worse than none because it reads as coverage -- hostinstall_gates.py sat RED and uninvoked across three version bumps and hub_confirm_gate.py has never run at all. So the property that matters is not whether a check exists but whether it BLOCKS. - Wired into handleSetArtifacts (internal/web/configs.go), immediately before the only write, on the sole UI path to SetArtifactManifest -- it runs on every vouch without anyone choosing to. A script in scripts/ would have been a fourth orphan. - It REFUSES (operator ruling, 2026-07-30), with a flash naming the remedy. - Signal: store.NewestReportedControllerVersion() over reports.controller_version, SEMVER-compared in Go -- MAX() in SQL ranks 0.99.0 above 0.186.0, a pair this fleet has shipped. No outbound call, no new credential. - Fail-open in exactly two deliberate cases: an empty golden field (clearing the manifest is legitimate) and an unknown fleet version (a new hub must vouch its first golden). NEAR-MISS RECORDED: the first draft read guests.controller_version, a column that exists in the schema and that NOTHING writes -- it would always have seen "" and failed open, i.e. inert, this gate's own failure shape. Caught by grepping for a writer before trusting the column. Blind spot stated rather than papered over: a controller no box has ever run is invisible to this signal. Not the failure that has bitten -- all three instances were deployed-newer-than-baked. 4 tests through the PRODUCTION handler over httptest, never an injected seam. The refusal asserts both the flash and that the manifest was NOT written, because a gate that redirects and saves anyway reads as enforcement while providing none. Red-proof: deleting the block makes the stale golden vouchable and both assertions fail. ROADMAP R-29's audit list now records this as the FIRST enforced gate, so the contrast with its three orphans is kept rather than lost. The orphans are unchanged. Suite rc=0 read separately from this commit.
This commit is contained in:
@@ -1616,6 +1616,48 @@ func (s *Store) GetGlobalMinControllerVersion() string {
|
||||
return s.defaultMinControllerVersion
|
||||
}
|
||||
|
||||
// NewestReportedControllerVersion returns the highest controller version ANY customer has reported, or
|
||||
// "" when none has. Semver-ordered in Go, not in SQL: `MAX(controller_version)` would compare lexically
|
||||
// and rank 0.99.0 above 0.186.0 — which is the exact pair this gate has to get right.
|
||||
//
|
||||
// It reads `reports.controller_version`, the column SaveReport denormalises out of every report
|
||||
// (store.go:903). It deliberately does NOT read `guests.controller_version`: that column exists in the
|
||||
// schema (:294) and **nothing writes it**, so a gate keyed on it would always see "" and fail open —
|
||||
// an inert gate, which is the exact class R-29 is about. Verified by grep before writing this.
|
||||
//
|
||||
// R-120's gate signal. The hub cannot ask "what is the newest controller that exists" — it has no
|
||||
// registry credential and makes no outbound call at vouch time — but it does know what the FLEET is
|
||||
// running, and that is the signal that matters: the failure this exists to catch is a golden left
|
||||
// behind a controller **already deployed**. It has happened three times (R-111, R-115, R-120) and on
|
||||
// the R-120 occurrence felhom-pve was reporting 0.186.0 while the manifest vouched a 0.185.1 golden —
|
||||
// exactly the comparison below.
|
||||
//
|
||||
// KNOWN BLIND SPOT, stated rather than papered over: a controller no box has ever run is invisible
|
||||
// here, so a golden baked behind an unreleased controller still passes. That is a real limit and it is
|
||||
// not the failure mode that has bitten — the three instances were all "deployed newer than baked".
|
||||
func (s *Store) NewestReportedControllerVersion() string {
|
||||
rows, err := s.db.Query(`SELECT DISTINCT controller_version FROM reports WHERE controller_version IS NOT NULL AND controller_version != ''`)
|
||||
if err != nil {
|
||||
return "" // unreadable → the gate degrades to "cannot compare", never to a false refusal
|
||||
}
|
||||
defer rows.Close()
|
||||
newest := ""
|
||||
for rows.Next() {
|
||||
var v string
|
||||
if rows.Scan(&v) != nil {
|
||||
continue
|
||||
}
|
||||
v = strings.TrimPrefix(strings.TrimSpace(v), "v")
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if newest == "" || semver.Compare(v, newest) > 0 {
|
||||
newest = v
|
||||
}
|
||||
}
|
||||
return newest
|
||||
}
|
||||
|
||||
// GlobalFloorResolution is the full picture of the effective global floor for the operator UI: the
|
||||
// resolved value + WHICH source won + both raw inputs. It makes the "a manifest save silently armed
|
||||
// a live floor" incident (publish-train 0.81/0.113) permanently visible — the operator can see the
|
||||
|
||||
Reference in New Issue
Block a user