fix(image-refresh): gate the fresh-install re-pin roll on a digest-applied marker - #1033
Conversation
…plied marker (backend#3556) On `recorded == latest` with the workload on `:tag`, the loop could not tell a genuine fresh install (first-observation recorded the digest but applied nothing) from a helm re-render that reverted an already-applied pin. It re-pinned both, so every fresh install paid the shared #563 flap path (resource-monitor DaemonSet under `tolerations: Exists`) and a `strategy: Recreate` downtime on jobs-manager for byte-identical content the install had just pulled. Add a durable per-image `tracebloc.io/digest-applied-<image>` marker, written alongside the digest record only after a re-image rollout succeeds (never on the first-observation record). Its ABSENCE gates the off-digest re-pin out: a fresh install is left on `:tag` (IfNotPresent-safe) until the first genuine upstream digest change pins it. The client-runtime#199 helm-revert repair is unaffected -- that path has the marker set, so it still rolls. Marker and digest record share one annotate, so a later `recorded == latest` tick sees both or neither. Item 2 of backend#3556 (split from client#1008). Item 1 (the latched-flap Pass-0 annotation drop) is a separate fix, not included here. Tests: - image-refresh-repin-on-revert.bats: new "fresh install does NOT roll" case; the existing revert case now carries the applied marker (established edge). - image_refresh_test.yaml: render guard locking the marker derive/read/write. Header comments updated to describe the gate (the old "fresh edge reproducible ~one interval post-install" and "a follow-up may gate this" notes are now false). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka
left a comment
There was a problem hiding this comment.
Read at 36c6d0c. The intent is right and the mechanics of the marker are clean: derived once per image, read only in the off-digest arm, written in the same annotate as the digest record and only after a successful rollout, with the first-observation arm writing neither. The bats pair reddens on an inverted or deleted gate, which is the proof that matters. Three things before this can go in, one of them a design gap.
1. The gate has no evidence for the fleet that already exists. The marker is written in exactly one place (:781, the re-image path), so every edge pinned before this chart version has no marker. The header's own premise (:634-637) is that the auto-upgrade re-renders the Deployment back to repo:tag and discards the pin. So the upgrade that ships this chart reverts every established edge to :tag, and on the next tick each one has recorded == latest, a workload on :tag, and no marker, which is byte-for-byte the fresh-install shape. They all take the continue at :762 and sit on :tag until the next upstream digest change. On :dev that is minutes; on :prod it can be weeks, and on a node whose :tag layer is stale it is the exact exposure the client-runtime#199 repair exists to close, fleet-wide, for one hop. "Established edges (any edge that has ever applied a digest) are unaffected" in the body and the header is not true for any edge that applied its digest before this version. A backfill in the no-op arm does not help either, because those edges are already reverted before their first post-upgrade tick.
The fail-closed shape is to skip only on positive evidence. Have the first-observation arm (:628) also write a tracebloc.io/first-observed-<image>=1 marker, and gate the skip at :762 on first-observed present and digest-applied absent. Then: a workload with neither marker is a pre-marker edge and keeps the repair roll; a new install has first-observed only and is skipped; an established edge has digest-applied and rolls. Three bats cases fall out of that (legacy neither, fresh first-observed only, established applied), and the "unaffected" claim becomes true rather than asserted.
2. Public repo, private tracker. The Related section names the private backend tracker twice (owner-qualified and bare), and the same reference appears in the new shipped header, the in-script comments, the log line at :763, the helm unittest comment and the bats comments. This repo is public. Please strip it from the title and body (a #1008 split note carries the context on its own) and, since this PR introduces every one of those comment occurrences, from the shipped comments too.
3. Chart version gate. chart content ⇒ Chart.yaml version bump is red: the template changed and client/Chart.yaml is still 1.9.110, while develop is already at 1.9.111. Bump to 1.9.112.
Happy to approve once those land and mutation-check, Unit tests and Bugbot are green.
…refs; bump chart Addresses review on #1033. 1. Migration gap (High, Bugbot + LukasWodka): the single digest-applied marker is written only by this version, so every edge pinned by an earlier version has none. The upgrade shipping this chart re-renders the Deployment back to :tag (the same client-runtime#199 premise), so on the next tick each such edge is byte-for-byte the fresh-install shape and, under the old gate, took the skip -- stranding the whole existing fleet on a possibly-stale :tag until the next upstream digest change. Fix (Lukas's shape): skip only on POSITIVE evidence. Add first-observed-<image>, stamped by the first-observation arm; skip the off-digest roll iff first_observed present AND applied absent. Three shapes now fall out: fresh (first_observed only) -> skip; established (applied) -> roll; pre-marker/legacy (neither) -> roll (repair), which then stamps applied. Reads are fail-closed: a marker read ERROR skips the tick and retries. New bats case for the legacy shape; existing fresh/established cases updated; render guard updated to the two-marker code shapes. 2. Public repo: stripped the private tracker ref from the shipped header, the in-script comments, the log line, the helm-unittest and bats comments, and the PR title/body -- the #1008 split note carries the context. 3. Chart version guard: bump client/Chart.yaml 1.9.111 -> 1.9.112 (merged develop first, which had advanced to 1.9.111). Verified: image-refresh-repin-on-revert.bats 13/13; helm unittest 729/729; make check green; shellcheck unchanged (only the pre-existing intentional SC2086). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chart-version guard requires version and appVersion to move together (_helpers.tpl feeds appVersion into app.kubernetes.io/version); the earlier bump moved version only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f0856ce. Configure here.
|
Thanks @LukasWodka — all three addressed, pushed to 1. Pre-marker fleet (the design gap). Fixed exactly as you prescribed: skip only on positive evidence. Added
So the upgrade hop no longer strands the existing fleet, and "unaffected" is now true rather than asserted. Marker reads are fail-closed: a read error (as opposed to genuine absence) skips the tick and retries, matching the unreadable- 2. Public repo / private tracker. Stripped from the title, body, shipped header, in-script comments, the log line, and the helm-unittest + bats comments — the 3. Chart version. Merged current Green on |
…n't mis-stamp first-observed Bugbot Medium on #1008. The first-observation arm now stamps first_observed, which suppresses the client-runtime#199 repair on later ticks. But the digest record was still read with `recorded="$(get_annotation "$key" || true)"`, which collapses a transient kubectl/jq ERROR into "" -- so on a legacy edge (which HAS a recorded digest) a read error would fall into the first-observation arm, stamp first_observed, and strand that edge on :tag until the next upstream digest change. Read fail-closed instead: get_annotation returns non-zero on a read error and zero+empty only on genuine absence, so `if ! recorded=...; then skip; fi` makes the first-observation arm reachable only on a real absence. Render guard asserts the guarded read is present and the fail-open `|| true` form is gone. Verified: image_refresh helm-unittest 39/39; repin bats 13/13; shellcheck unchanged (pre-existing SC2086 only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
LukasWodka
left a comment
There was a problem hiding this comment.
Re-read at 5962509. All three asks from my first pass are in, and the fail-open read Bugbot caught after my second pass is now closed too.
- The gate skips only on positive evidence. The first-observation arm stamps
tracebloc.io/first-observed-<image>beside the digest record; the off-digest arm skips the roll only when first-observed is present AND digest-applied is absent, rolls an established edge (applied present), and rolls a pre-marker edge (neither) so the fleet upgrading into this chart is repaired once rather than stranded. The batsPRE-MARKER / legacy edgecase asserts the roll, the repair log line, the pin and the applied stamp, and that the fresh-install log line is absent. - The digest-record read is fail-closed.
recorded="$(get_annotation "$key" || true)"was the one read that could still collapse a kubectl or jq error into "absent" and walk a legacy edge into the first-observation arm, where the new marker would then suppress the repair for good. It is nowif ! recorded="$(get_annotation "$key")"; then … continue; fi, skipping the image for this tick and retrying, the same stance the two marker reads and the settled guard already take. I checkedget_annotation's contract on this head: success with empty output on genuine absence, non-zero only on a read failure, so the guard cannot mistake absence for an error. The Helm unit test pins both the guarded form present and the|| trueform absent, so a revert reddens. The remaining|| trueannotation reads (pending_attemptand the three stale markers) are best-effort by design, with the fail-closed authority documented at the restart block, so I did not ask for them to change. - Private tracker refs are gone from the title, body, shipped header, in-script comments, log lines and test comments; the
#1008split note carries the context. client/Chart.yamlis1.9.112overdevelop's1.9.111.
Approving once the suite, mutation-check, the version-bump gate and Bugbot are green on this head.
… same 1.9.112 that #1032 already shipped The image-refresh CronJob fix (#1033) bumped Chart.yaml 1.9.111 -> 1.9.112 on its own branch, and so did the control-plane trim (#1032). Both merged; the second bump was a no-op, so develop's chart version now equals staging's while the templates differ, and the chart-version guard rightly refuses the staging promotion. This bump carries #1033 to 1.9.113. No template change.
… same 1.9.112 that #1032 already shipped (#1040) The image-refresh CronJob fix (#1033) bumped Chart.yaml 1.9.111 -> 1.9.112 on its own branch, and so did the control-plane trim (#1032). Both merged; the second bump was a no-op, so develop's chart version now equals staging's while the templates differ, and the chart-version guard rightly refuses the staging promotion. This bump carries #1033 to 1.9.113. No template change.

Summary
On
recorded == latestwith the workload on:tag, the image-refresh loop could not distinguish a fresh install (first-observation recorded a digest but applied nothing) from a helm re-render that reverted an already-applied pin — so every fresh install paid the shared #563 flap path (arollout statuson the resource-monitor DaemonSet, whosedesiredNumberScheduledcounts every node undertolerations: Exists) plus astrategy: Recreatedowntime on jobs-manager, for byte-identical content the install had just pulled.The gate skips the fresh-install roll only on positive evidence, via two durable per-image markers:
tracebloc.io/first-observed-<image>— stamped by the first-observation arm (we watched this workload born on:taghere).tracebloc.io/digest-applied-<image>— stamped alongside the digest record only after a re-image rollout succeeds.Decision in the off-digest arm:
first_observedpresent andappliedabsent → fresh install → leave on:tag(IfNotPresent-safe); the first genuine upstream digest change pins it.appliedpresent → established edge → roll (the client-runtime#199 helm-revert repair).applied. This is the key correction from review: the upgrade shipping this chart reverts the whole existing fleet to:tag, so skipping on marker absence alone would strand every pre-marker edge on a possibly-stale:taguntil the next upstream digest change. A marker read error (vs genuine absence) skips the tick and retries — fail-closed.Related
Split from #1008 (item 2 — the fresh-install digest gate). Item 1 from that split (the latched-flap Pass-0 annotation drop on an off-digest tick) is a separate fix and is not in this PR.
Type of change
Test plan
bats scripts/tests/image-refresh-repin-on-revert.bats— 13/13. Cases for all three shapes: fresh (first-observed only → no roll), established (applied → roll), legacy (neither → repair roll). Harness gained a 6th arg + aget_annotationstub modelling absent-vs-error.helm unittest ./client— 729/729 (image_refresh guard updated to the two-marker code shapes).make check— green.shellcheck -s shon the rendered script — unchanged (only the pre-existing intentional SC2086 word-split notes).develop(which had advanced to chart1.9.111); bumpedclient/Chart.yaml→ 1.9.112 for the chart-version guard.Deployment notes
Behavioural change scoped to fresh installs only: a freshly-installed edge stays on the floating
:tag(IfNotPresent) instead of being pinned to a digest ~one interval post-install; it is pinned on the first genuine upstream digest change. Established and pre-marker/legacy edges are unaffected — the client-runtime#199 helm-revert repair still runs for both.Checklist
Note
Medium Risk
Changes control-plane image reconciliation and rollout gating fleet-wide; logic is heavily tested but a mistake could skip needed re-pins or trigger extra DaemonSet/jobs-manager rollouts on upgrade.
Overview
Fixes image-refresh so fresh installs no longer pay an expensive digest re-pin rollout when the registry digest is unchanged but the workload still runs the floating
:tag.The CronJob script now uses two per-image deployment annotations (
first-observedon first observation,digest-appliedafter a successful re-image). Whenrecorded == latestand the live image is off-digest, it skips rolling only if both markers show a fresh install (first_observedset,appliedabsent). Established edges (helm reverted an applied pin) and legacy edges (neither marker, e.g. fleet upgrading into this chart) still get the client-runtime#199 repair roll so the fleet is not left on a possibly stale tag.Fail-closed behavior: digest and marker annotation reads no longer use
|| true; API/jq errors skip the image for that tick instead of mis-stampingfirst_observed. Chart version 1.9.112; helm-unittest and bats tests lock the gate and three edge shapes (fresh / established / legacy).Reviewed by Cursor Bugbot for commit 5962509. Bugbot is set up for automated code reviews on this repo. Configure here.