Skip to content

fix(chart): render control-plane images from the last-refreshed digest so helm upgrade preserves the pin - #1013

Open
saqlainsyed007 wants to merge 11 commits into
developfrom
fix/199-seed-controlplane-digest-from-annotation
Open

fix(chart): render control-plane images from the last-refreshed digest so helm upgrade preserves the pin#1013
saqlainsyed007 wants to merge 11 commits into
developfrom
fix/199-seed-controlplane-digest-from-annotation

Conversation

@saqlainsyed007

@saqlainsyed007 saqlainsyed007 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Fix B of A+B for the image-drift class. Companion to #1008 (which fixes the image-refresh side). This one makes helm upgrade never revert the digest in the first place, closing the window #1008 only bounds.

Refs tracebloc/client-runtime#199

The problem (recap)

Control-plane images render as repo:tag + IfNotPresent when unpinned (per tracebloc.controlPlanePullPolicy, #569/#705 — deliberately, to survive an offline restart). image-refresh keeps them current out-of-band via kubectl set image repo@digest. But helm upgrade --reset-then-reuse-values (the hourly auto-upgrade) re-renders the bare :tag and drops that pin; on a node whose :tag layer is stale the pod then silently runs an old control-plane image (client-runtime#199). #1008 catches this on the next image-refresh tick (≤ ~15 min); this PR removes the revert entirely.

The fix (this PR — "B")

New tracebloc.controlPlaneDigest helper resolves the effective digest:

  1. operator values pin (images.<name>.digest) — wins, unchanged;
  2. else, when image-refresh is the update path (enabled + docker.io), the digest image-refresh last observed — read from the jobs-manager Deployment's tracebloc.io/last-refreshed-<image>-digest annotation via lookup;
  3. else "" (bare :tag).

Wired into all five control-plane image sites (jobs-manager init + api + pods-monitor, requests-proxy following the jobs-manager annotation, resource-monitor). A helm upgrade now renders repo@<current-digest> instead of reverting to :tag.

imageRefresh.suspend KEEPS the pin (reviewer reconciliation)

An earlier round gated the digest render on not suspend, so a suspended edge un-pinned to :tag + Always. @LukasWodka showed that is wrong: suspend is used to freeze an edge during an incident, and un-pinning re-renders jobs-manager (Recreate), requests-proxy and the resource-monitor DaemonSet onto the floating tag — three unplanned rollouts plus a downgrade to whatever :tag points at now — contradicting the values.schema.json promise that suspend "pause[s] without removing the resources". So suspend now stops polling, not un-pinning: a suspended edge keeps rendering the last-observed @digest + IfNotPresent. A newly joined node still pulls a real, previously-resolved digest — not a missing one — so the original newly-joined-node concern is still answered.

Safety / caveats to review

  • No new RBAC. The lookup targets only the jobs-manager Deployment in the release namespace; the auto-upgrade SA's release-ns Role already grants that read (it is not a new read the SA can't already do, so it does not hit the bootstrap-lockout rule).
  • Annotation is validated. The annotation is written out-of-band by kubectl and reaches an image: field; the helper regexMatches it against ^sha256:[a-f0-9]{64}$ before rendering, so a malformed value degrades to "":tag (the safe fallback) instead of an unstartable ref.
  • lookup is empty during helm template / helm diff / the FIRST install → helper returns "":tag. Correct there (nothing pinned yet). helm diff will show a :tag@digest churn that won't actually happen on apply — worth a note for anyone who diffs upgrades.
  • First-observation roll. image-refresh records the first digest without re-imaging, so after that the first chart bump renders @D0 where the live spec still says :tag, rolling jobs-manager/requests-proxy/DaemonSet once for byte-identical content (--atomic can time out on a slow single-node edge). One-time cost, documented in the image-refresh CronJob header.
  • Control-plane pods: offline-restart-safe update model (design) — jobs-manager/pods-monitor/resource-monitor (from #552) #569 offline guarantee narrows. After a first observation an offline restart can hit a digest the node has never pulled → "cannot start" instead of "runs stale". Documented on the operator-facing surfaces: the controlPlanePullPolicy helper note, the auto-upgrade #569 NOTE and first-observation block in values.yaml, and the imageRefresh description in values.schema.json. (docs/MIGRATIONS.md covers a different narrowing — the server-side-apply .image-conflict remedy — not this offline-restart one.)

Testing

  • Clusterless unit suite client/tests/control_plane_digest_test.yaml: helm-unittest 0.5.2 fakes lookup via kubernetesProvider, so the lookup path is now covered without a cluster — the five image sites, operator-pin precedence, malformed → :tag, the suspend-keeps-pin decision, and the disabled/mirror/first-install :tag edges. Mutation-verified: discarding the lookup reddens the digest cases, re-adding the suspend gate reddens the suspend case, neutering the regexMatch reddens the malformed case.
  • e2e scripts/tests/e2e-auto-upgrade.sh seeds a valid 64-hex annotation and asserts the rendered jobs-manager image: digest after both --reuse-values (path 1) and --reset-then-reuse-values (path 2), not merely that the annotation survived.
  • helm lint clean, helm template renders :tag clusterless and @digest on the operator pin, 712/712 unit tests across 41 suites, Chart.yaml 1.9.111.

Merge order — B depends on A (#1008), land A first

In the steady state (recorded == latest) A and B compose. But the annotation lags the live spec in two reachable states, and in both B renders a digest that pulls the workload backwards:

  • a rollout that times out — set image moved the spec, set -e exits before the annotate, so spec = NEW while the annotation = OLD;
  • helm rollback, which re-applies a stored manifest (it does not re-render), restoring the digest that was live at that revision's render time — so a rendered digest means a rollback can undo a security refresh.

Both are self-healing only because A's live-spec reconcile re-pins within a tick. So #1008 (A) must land first; B alone would strand those states. Chart conflict on rebase is only client/Chart.yaml — keep 1.9.111.

Not changing the pull policy

Always-when-unpinned would re-break #569/#705 (offline restart → ImagePullBackOff).

🤖 Generated with Claude Code


Note

Medium Risk
Changes control-plane image resolution on every upgrade for docker.io + image-refresh edges (one-time digest rollouts, offline pull failures, annotation lag until #1008); mitigated by validation, suspend semantics, and observability annotations.

Overview
Chart 1.9.111 stops helm upgrade from undoing image-refresh’s out-of-band digest pins (client-runtime#199): control-plane workloads no longer re-render as bare repo:tag when image-refresh has already observed a digest.

New helpers tracebloc.controlPlaneDigest and tracebloc.controlPlaneDigestSource resolve the effective image digest (operator values pin → last-refreshed annotation on jobs-manager via lookup → floating tag), validate annotation shape, keep the pin when imageRefresh.suspend is true, and expose provenance on jobs-manager as tracebloc.io/controlplane-digest-source (including tag-lookup-failed on upgrades when lookup is empty).

Wiring: api, pods-monitor, requests-proxy, and resource-monitor use the helper for image: and pull policy; wait-for-mysql init stays on :tag (or explicit operator pin) to avoid offline/k3d Init:ImagePullBackOff.

Docs narrow the #569 offline-restart guarantee and update MIGRATIONS/SEAL-CHECK for steady-state digest rendering. Tests: clusterless control_plane_digest_test.yaml and e2e auto-upgrade checks on the rendered jobs-manager digest, not just the annotation.

Reviewed by Cursor Bugbot for commit a7553fc. Bugbot is set up for automated code reviews on this repo. Configure here.

…t so a helm upgrade preserves the pin (client-runtime#199)

Control-plane images render as repo:tag (IfNotPresent) when unpinned, and
image-refresh keeps them current out-of-band via kubectl set image repo@digest.
A helm upgrade --reset-then-reuse-values re-renders the bare tag and drops that
pin; on a node whose :tag cache is stale the pod then silently runs an OLD
image. New tracebloc.controlPlaneDigest helper seeds the effective digest from
the jobs-manager Deployment last-refreshed-<image>-digest annotation via lookup,
so a re-render renders repo@digest instead. Operator pins still win; lookup is
empty during helm template/diff/first install (-> :tag, correct there). No new
RBAC (auto-upgrade SA already reads the Deployment in the release ns).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saqlainsyed007

saqlainsyed007 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

✅ Validated on a real cluster

lookup is empty during helm template/CI, so B's core behaviour was validated live in addition to the unit suite. Ran a non-mutating helm upgrade --reset-then-reuse-values --dry-run=server (the one mode where lookup reads the live cluster; persists nothing) against a dev fleet whose jobs-manager Deployment carries real last-refreshed-* annotations.

Before (current develop chart) vs after (this PR), same command:

image current chart this PR
jobs-manager docker.io/tracebloc/jobs-manager:dev …@sha256:8cda7b64…
pods-monitor …/pods-monitor:dev …@sha256:a944cbc4…
resource-monitor …/resource-monitor:dev …@sha256:a9341b6d…

Each rendered digest matches the Deployment's tracebloc.io/last-refreshed-<image>-digest annotation exactly. So a helm upgrade under this chart renders repo@digestpreserving image-refresh's pin instead of reverting to :tag, which is the whole fix. No RBAC/forbidden error on the server-side render, consistent with the auto-upgrade SA's release-ns Role already covering the read.

Update: this path is now also covered clusterless. helm-unittest 0.5.2 fakes lookup via kubernetesProvider, so client/tests/control_plane_digest_test.yaml exercises the annotation seed, operator-pin precedence, the malformed-value guard and the suspend decision without a cluster (thanks @LukasWodka).

(Edited to remove cluster/namespace identifiers and correct the "can't be unit-tested" framing — this repo is public.)

@shujaatTracebloc shujaatTracebloc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested. The mechanism is right and the fallback is genuinely safe — I confirmed helm template with no cluster renders :tag + IfNotPresent at all four sites and exits 0 (aks and bm), so the Helm chart renders gate is fine, and your no-new-RBAC claim checks out: the auto-upgrade Role is * on * in the release namespace. CI is green; the org scan's CI=FAILURE is a superseded-run artifact (a CANCELLED Helm unit tests alongside two SUCCESS runs on the same sha).

Two things I think have to change before this lands, and one merge-order point.

1. BLOCKING — the annotation reaches image: with no validation (_helpers.tpl:532). Every one of the 11 digest keys that can reach an image: field is schema-guarded by ^(sha256:[a-f0-9]{64})?$--set images.jobsManager.digest=sha256:deadbeefcafe is rejected outright, I checked. The annotation path has no equivalent, and tracebloc.image drops the tag when a digest is present, so a malformed value renders an unstartable ref that helm cannot detect (the kubelet reports InvalidImageName; the apiserver accepts the spec happily).

This is already live in the repo: scripts/tests/e2e-auto-upgrade.sh:195 writes tracebloc.io/last-refreshed-jobs-manager-digest=sha256:e2e-sentinel, then runs helm upgrade --reuse-values and --reset-then-reuse-values against the working-tree chart — and line 278 asserts the annotation is still there afterwards. With defaults (imageRefresh.enabled: true, docker.io, no operator pin) branch 2 fires and renders docker.io/tracebloc/jobs-manager@sha256:e2e-sentinel onto the init container, api and requests-proxy. That script's only readiness wait is kubectl wait ... nodes, and it never asserts a control-plane image ref — so Fleet auto-upgrade E2E (k3d) passed on this head while doing exactly that. A sha256:[a-f0-9]{64} guard in the helper closes it and is unit-testable with no cluster, which also partly answers your "can't be tested" caveat.

2. BLOCKING — imageRefresh.suspend is never consulted. tracebloc.imageRefreshEnabled gates only on not $ir.enabled, but suspend is a real key (values.yaml:1833, consumed at image-refresh-cronjob.yaml:702). With enabled: true, suspend: true — supported, and the state the local-k3d recipe leaves behind — the CronJob never runs, yet the helper still treats refresh as the live update path and pins the last annotation permanently. It hurts most on the DaemonSet: before this change a newly joined node pulled the current :tag; after it, the node pulls a frozen digest and silently runs a stale build with nothing reporting it. That is the same "silently runs an OLD control-plane image" class as client-runtime#199, arriving from the other direction.

3. MERGE ORDER — B depends on A, it does not merely pair with it. I'd said A and B compose, and in the steady state they do: with recorded == latest, B renders that digest and A's live-spec check reads on-digest and no-ops. But the annotation LAGS the live spec in two reachable states, and in both, B pulls the workload backwards:

  • a rollout that times out — set image has moved the spec, set -e exits before the annotate, so spec = NEW and annotation = OLD;
  • helm rollback, which re-applies a stored manifest rather than re-rendering, so it restores the digest that was live at that revision's render time. Answering the rollback question directly: yes, a rendered digest means a rollback can undo a security refresh.

Both are self-healing only because A's live-spec reconcile re-pins within a tick. Without A, recorded == latest no-ops and the stale digest sticks until the next upstream publish. So A first is a correctness precondition, not a preference — worth stating in the header next to the lookup caveats.

Non-blocking, but I'd like them addressed or written down:

4. This narrows #569's offline-restart guarantee, and the body doesn't mention it. Keeping the pull policy is necessary but not sufficient: IfNotPresent only helps when the REFERENCE is cache-satisfiable. A node that pulled :tag -> sha256:OLD satisfies repo@sha256:OLD from cache but cannot satisfy repo@sha256:NEW. Since the first-observation path records latest WITHOUT re-imaging, we can now render a digest the node has never pulled: online that is the fix working, offline it turns "restarts on a stale image" into "cannot start". Same root cause makes a side-loaded k3d image unpullable — k3d image import stores a tag alias with no resolvable digest, and the mirror gate doesn't help because a local k3d install IS docker.io.

5. First-observation roll. The script records without re-imaging precisely because "rewriting repo:tag to repo@digest for identical content would roll every pod on install". This reintroduces that roll through helm: first tick writes the annotations, the next upgrade changes all four pod specs for a zero-content change — recreating jobs-manager (strategy: Recreate on RWO PVCs, the #545 wedge) and every resource-monitor pod on every node. Not hourly, to be fair: auto-upgrade only fires on a chart-version bump. Relatedly, the helper's doc says "the digest image-refresh last APPLIED" — on this path it is what refresh last OBSERVED and deliberately did not apply. Worth correcting; it changes how a reader reasons about the value.

6. A --set env.CLIENT_ENV=stg switch becomes inert on the image: the annotation key isn't env-scoped and the tag is dropped when a digest is present, so the edge renders the other env's digest while every label reads stg. It self-corrects next tick unless the switch leaves the Deployment unsettled — then the settled guard skips forever and each upgrade re-pins the old env's digest.

7. Seven uncached lookups per render (5 in jobs-manager, 1 each in requests-proxy and resource-monitor), and a non-NotFound error RAISES rather than returning empty — failing the whole release, which is what resource-monitor-daemonset.yaml:22-27 documents for backend#2469. The RBAC half is fine as you argued; the transient-apiserver-error half is a new failure mode for a render that used to work offline, and unlike the metrics-server lookup there's no escape hatch equivalent to nodeAgents.metricsServerPreflight: false.

8. Routing the effective digest into controlPlanePullPolicy (:277, :912) is a no-op: wherever the helper can return non-empty, imageRefreshEnabled && docker.io already held, so the policy was already IfNotPresent. Harmless, just two more lookups.

On the Helm 4 server-side-apply question from the last round: I said this would get strictly worse with the chart rendering a digest, and I want to withdraw that. The premise was wrong — repo:tag is itself a rendered value for .image, so helm has always declared and owned that field; that ownership is exactly why the #199 revert happens. This changes the VALUE helm writes, not the ownership set, and adds no manager. On the next upgrade after a kubectl set image, helm reclaims the field and writes what the chart rendered: it does not error and does not preserve the digest. Before, it reclaimed and wrote the wrong value; after, the right one in the steady state. So: neutral on ownership, better on outcome, with the lag window in (3) as the exception. Also relevant for @LukasWodka: the fleet path is Helm 3 today — autoUpgrade pins alpine/helm 3.16.4 and CI's render job pins v3.15.4 — so SSA ownership doesn't reach the fleet until someone bumps that tag to 4.x.

On testing: I mutated the helper to always return "" and 5 tests across the jobs_manager / requests_proxy / resource_monitor suites go red, so the operator-pin branch is genuinely covered (695 pass clean). Branch 2 has nothing that can redden, as you say — but I don't think it needs a new suite in full-seal-e2e. The rig already exists: e2e-auto-upgrade.sh runs on real k3d, already seeds that exact annotation, and already runs both upgrade paths. It needs one assertion on the rendered image ref (and, given finding 1, a real 64-hex digest instead of the sentinel).

What I could NOT verify here, stated plainly: I have no cluster, so branch 2 — the lookup path, the whole point of the PR — was never actually executed by me; every claim about it is read from the template and from the CronJob's annotation writer. No k3d locally, so the side-loaded-digest hazard in (4) is reasoned from the tag-alias behaviour, not reproduced. And no Helm 4 server-side-apply rig, so the ownership analysis above is from managedFields semantics plus the fact that the revert demonstrably happens today (which proves helm reclaims the field rather than erroring) — not from an observed conflict.

…spend (client-runtime#199)

blocking 1 (@shujaatTracebloc on #1013): the last-refreshed annotation reaches
an image: field unchecked, unlike the 11 values digest keys the schema guards
with ^(sha256:[a-f0-9]{64})?$. A malformed value renders an unstartable ref
helm cannot detect. controlPlaneDigest now regexMatches the annotation and
degrades to :tag otherwise. blocking 2: a suspended CronJob never re-pins, so
its frozen annotation must not pin the render -- gate controlPlaneDigest and
controlPlanePullPolicy on not imageRefresh.suspend so a suspended edge stays on
:tag with Always. Updates the e2e sentinel to a valid 64-hex digest; adds two
jobs-manager pullPolicy unit tests. helm-unittest 697, lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saqlainsyed007

Copy link
Copy Markdown
Contributor Author

Addressed all three in 8e1fc65.

1 — BLOCKING, annotation reaches image: unvalidated. tracebloc.controlPlaneDigest now regexMatches the annotation against ^sha256:[a-f0-9]{64}$ before rendering it; anything else degrades to "":tag (the safe fallback), matching the schema guard the 11 values digest keys already carry. You were right that this is testable without a cluster — but the annotation comes from lookup, so I couldn't unit-test the render path directly; instead I made scripts/tests/e2e-auto-upgrade.sh's sentinel a valid 64-hex digest (it was sha256:e2e-sentinel, which my guard now correctly rejects) so that integration test keeps exercising branch 2 with a value that actually renders.

2 — BLOCKING, imageRefresh.suspend ignored. Gated both controlPlaneDigest and controlPlanePullPolicy on not $ir.suspend (not in imageRefreshEnabled — that also gates the CronJob/RBAC render, which must still happen when suspended). So a suspended edge renders :tag + Always, and a newly joined node re-pulls the current tag instead of a frozen digest. Two new jobs_manager_test.yaml unit tests pin it (active → IfNotPresent, suspend → Always), verified against the real render.

3 — merge order, B depends on A. Corrected the "Relationship to #1008" section: it now states B depends on A (the rollout-timeout and helm rollback lag states pull backwards and self-heal only because A re-pins), so A lands first. Answered the rollback question inline: yes, a rendered digest means a rollback can undo a security refresh.

helm-unittest 697/697, helm lint clean, chart 1.9.110 (> develop 1.9.108), Helm chart renders gate green. On the Helm unit tests CANCELLED — agreed, superseded-run artifact. Re-requesting.

@saqlainsyed007

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread scripts/tests/e2e-auto-upgrade.sh Outdated
… jobs-manager image (client-runtime#199)

The e2e-auto-upgrade path-2 check asserted only that the last-refreshed
annotation survived the upgrade, not that tracebloc.controlPlaneDigest
actually rendered that digest onto the container image. The annotation is
a k8s object helm never touches, so it survives even if the helper is
removed and the image reverts to a floating :tag -- the exact revert this
PR prevents. Add jm_controlplane_image_digest() and assert the rendered
image digest equals the annotation, killing that mutation. Bind the
sentinel digest once (E2E_REFRESH_DIGEST) and reuse it across both checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saqlainsyed007

Copy link
Copy Markdown
Contributor Author

bugbot run

# Conflicts:
#	scripts/tests/e2e-auto-upgrade.sh
@saqlainsyed007

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@shujaatTracebloc shujaatTracebloc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Both blockers are closed, and I verified each by rendering rather than by reading.

1 — annotation validation: CLOSED. tracebloc.controlPlaneDigest now regexMatches ^sha256:[a-f0-9]{64}$ before the value can reach image:. I stubbed the lookup with a literal Deployment dict in a throwaway worktree at 62ca905 and rendered client/ci/bm-values.yaml both ways:

  • annotation sha256:deadbeefcafe -> all four control-plane sites render docker.io/tracebloc/jobs-manager:prod, pods-monitor:prod, resource-monitor:prod. It degrades to :tag, which is what I asked for.
  • annotation a real 64-hex digest -> jobs-manager (init + api), requests-proxy, pods-monitor and resource-monitor all render repo@sha256:..., with all three annotationImage keys resolving. That is the first time branch 2 has actually been executed rather than reasoned about, mine included.

Making the e2e sentinel a valid 64-hex digest is the right call, and the assertion you built on top of it (jm_controlplane_image_digest, path 2) is the one I asked for last round and the one Bugbot asked for: the annotation check cannot redden if the helper is dropped, the rendered-image check can. I also checked the blast radius of seeding an unpullable digest into a live k3d cluster: the only readiness wait in that script is kubectl wait ... nodes at line 158, before the seed, and everything after it is spec-only reads — so paths 2-5 cannot wedge on it, and Fleet auto-upgrade E2E (k3d) is green on this head.

2 — imageRefresh.suspend: CLOSED. Both helpers now gate on not $ir.suspend. Rendered with --set imageRefresh.suspend=true and a valid annotation: all four workloads go back to :prod with imagePullPolicy: Always, so a newly joined node re-pulls the current tag instead of silently running a frozen digest. Keeping it out of imageRefreshEnabled is correct — that also gates the CronJob and RBAC, which must still render when suspended. And Always here is the same trade imageRefresh.enabled: false already documents in values.schema.json, so this applies the chart's existing contract to the equivalent state rather than inventing one.

Mutation-registered rather than trusted: removing (not $ir.suspend) from controlPlanePullPolicy reddens exactly your new case ("falls back to Always when image-refresh is suspended") — 1 failed, 696 passed. Otherwise 697/697 clean, matching your count. Two mutations do NOT redden anything, and I would rather say so than imply coverage you do not have: neutering the regexMatch to if true, and removing (not $ir.suspend) from controlPlaneDigest itself, both leave 697 passing, because lookup is empty under helm-unittest. So the guard and the digest-side suspend gate rest on the stub render above and on your pullPolicy tests as a proxy. That is the ceiling without a cluster and I am not asking for more here.

3 — merge order: CLOSED. The header now states that B depends on A and that A lands first, with both lag states and the rollback answer. Worth noting the practical gate: #1008 still carries my change-request (the latched flap is silent — FLAP_KEY is never written, so the WARN never prints), so that is what holds this, not anything on this PR.

Non-blocking, and none of it holds the approval — but two items got sharper while I was verifying, and since A has to land first anyway I would rather they were written down than dropped:

  • autoUpgrade runs helm upgrade --reset-then-reuse-values --atomic --cleanup-on-fail --timeout (auto-upgrade-cronjob.yaml:260; --atomic implies --wait). That upgrades my points (4) and (5) from "an extra roll, and offline it cannot start" into a retry loop: if a rendered digest does not go ready inside the timeout, --atomic rolls the release back, the annotation is unchanged, and the next tick renders the same digest and fails identically. It bites hardest on the first-observation path, where image-refresh records latest deliberately WITHOUT re-imaging, so that digest never proved a rollout. The thing that wedges is the security-fix delivery path. A follow-up issue, not a change here.
  • the init container is seeded too (jobs-manager-deployment.yaml:103), but jm_set_args only ever names api and pods-monitor-container (image-refresh-cronjob.yaml:547,558), so kubectl set image never moves wait-for-mysql. Each digest change therefore rolls jobs-manager once out-of-band and again on the next chart bump — Recreate, inside that --atomic --wait. It is a MySQL TCP wait, so the pin buys nothing there; dropping the digest from that one site is the cheap version.
  • coverage: the e2e asserts the jobs-manager seed only. I confirmed the pods-monitor and resource-monitor keys render correctly via the stub, but nothing in CI would catch a rename or a typo in those two hand-written strings.
  • the helper doc still says "the digest image-refresh last APPLIED" (_helpers.tpl:505) where on this path it is what refresh last OBSERVED and deliberately did not apply; and the controlPlanePullPolicy contract block (:441-482) still enumerates three cases naming only enabled and docker.io, with no mention of the new suspend term. Both are reader-facing.
  • the values-level suspend gate cannot see a kubectl patch cronjob ... spec.suspend=true, or the flap lockout, both of which freeze refresh with imageRefresh.suspend: false still in values. Same residual class, no cheap fix — just noting the bound.
  • jobs_manager_test.yaml: "control-plane pullPolicy is IfNotPresent when image-refresh is active" duplicates "both containers are IfNotPresent by default (refresh on, docker.io)" at :1130 and is weaker (containers[0] vs both containers); both new cases sit at the end of the file rather than in the #569 pull-policy block a maintainer would read; and the comment cites backend#199, which is not the issue.
  • the body is stale in two places: it still opens "DRAFT for discussion", and still says "101/101 unit tests pass" (it is 697 across 40 suites).

CI: green on 62ca905. Every required context has a SUCCESS on this head; the rollup reads FAILURE only because of a CANCELLED Helm unit tests sibling with two SUCCESS runs on the same sha — the superseded-run artifact we both called — and the deduped gh pr checks shows zero failing and zero pending. Chart 1.9.110 > develop 1.9.108. Merge after #1008.

@LukasWodka LukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 62ca905 against develop (1.9.108 → 1.9.110). The mechanism is sound and I reproduced the key evidence: helm template with the aks and bm CI values renders :prod + IfNotPresent at all five image sites and exits 0, 697/697 unit tests pass, and the k3d Fleet auto-upgrade E2E job on this head passed the new path-2 assertion (rendered image: digest == the seeded annotation) and ran through to E2E PASS. Bugbot is clean and its thread is resolved. Mutation check: disabling the operator-pin branch reddens 5 tests; removing the suspend gate reddens the new pullPolicy test. Both of those guards are real.

Requesting changes on two points, plus a merge-order hold.

1. imageRefresh.suspend: true now rolls the whole control plane onto the floating tag. Adding (not $ir.suspend) to both branch-2 conditions means a paused refresh renders repo:tag + Always instead of repo@digest + IfNotPresent. Steady state: annotation = D1, everything live on repo@D1. Operator sets suspend: true to freeze the edge during an incident; the next hourly bump re-renders jobs-manager (Recreate), requests-proxy and the resource-monitor DaemonSet to :tag/Always, so every node pulls whatever the tag points at now. Un-pausing renders @D1 again and rolls everything back to D1, then the next tick rolls forward. Three unplanned rollouts and one downgrade from a knob whose schema text (values.schema.json L1371) promises "pause without removing the resources". Before this PR the render was :tag + IfNotPresent either way, so flipping suspend changed no spec. Suggested fix: keep rendering the annotation digest while suspended (suspend should stop polling, not un-pin), and leave pullPolicy IfNotPresent on that branch. If you do want suspend to un-pin, that is a behaviour change that needs its own schema/values text and a line in the migration notes.

2. Branch 2 is unit-testable clusterless; please add the suite. The body and the 13:10 comment both say the lookup path can't be unit-tested. helm-unittest 0.5.2, the version CI pins, fakes lookup via kubernetesProvider. I ran this against HEAD and it passes 3/3; discarding the lookup result in the helper makes the first case fail, so it is a real guard:

kubernetesProvider:
  scheme:
    "apps/v1/Deployment": {gvr: {group: apps, version: v1, resource: deployments}, namespaced: true}
  objects:
    - apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: RELEASE-NAME-jobs-manager
        namespace: NAMESPACE
        annotations:
          tracebloc.io/last-refreshed-jobs-manager-digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
          tracebloc.io/last-refreshed-pods-monitor-digest: "sha256:not-a-digest"
tests:
  - it: renders api image from the last-refreshed annotation
    documentIndex: 0
    asserts:
      - equal: {path: 'spec.template.spec.containers[?(@.name == "api")].image', value: docker.io/tracebloc/jobs-manager@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}
  - it: malformed annotation degrades to :tag
    documentIndex: 0
    asserts:
      - equal: {path: 'spec.template.spec.containers[?(@.name == "pods-monitor-container")].image', value: docker.io/tracebloc/pods-monitor:prod}
  - it: suspend beats a valid annotation
    documentIndex: 0
    set: {imageRefresh: {suspend: true}}
    asserts:
      - equal: {path: 'spec.template.spec.containers[?(@.name == "api")].image', value: docker.io/tracebloc/jobs-manager:prod}

(Adjust the third case to whatever you decide for point 1.) This matters because today the only thing that reddens if the lookup path breaks is the k3d e2e, which is path-filtered and not a required status check on develop, so a regression of the seed merges green. Helm unit tests is required. Please cover the matrix clusterless: the five sites (init, api, pods-monitor, requests-proxy following the jobs-manager annotation, resource-monitor), operator pin beating the annotation, malformed → :tag, and the suspend decision. Then drop the "can't be unit-tested" sentences from the body; that claim is what would keep the next person from writing the test. Also: the new "IfNotPresent when image-refresh is active" case at tests/jobs_manager_test.yaml:1236 is a verbatim duplicate of the existing case at L1130, and the block comment above it claims the helper "must NOT render that digest" while no assertion touches image:.

3. Merge order (hold). The body now states B depends on A (#1008) for the rollout-timeout and helm rollback lag states. #1008 is still open, so this cannot merge first. Only client/Chart.yaml conflicts (1.9.109 vs 1.9.110); rebase after A lands and keep 1.9.110. One extra note for A: when the refresh tick's rollout times out after set image but before annotate, the annotation lags the live spec, and once the flap lockout from #563 latches, nothing re-pins until a human clears it, so A's reconcile needs to run before, not behind, the lockout check.

Non-blocking, please pick up in the same push:

  • Docs this PR makes false, in the same PR: the "HELM RE-RENDER" limitation in the image-refresh CronJob header (L50-57: re-render "writes repo:tag and so reverts an earlier set image pin") and docs/SEAL-CHECK.md L289-294 (chart renders repository:tag, so pin in values to survive --reset-then-reuse-values) both describe the pre-PR behaviour. An operator following SEAL-CHECK today would pin values to fix a revert the chart no longer performs.
  • First-observation churn: the first tick records the digest without set image so a fresh install never rolls pods for byte-identical content (header L92-105). After this PR the first chart bump after install renders @D0 where the live spec says :tag, so jobs-manager (Recreate), requests-proxy and the DaemonSet on every node roll once for identical content, and --atomic on a slow single-node edge can time out on that. One-time cost, but the header should say so, or the first-observation path should set image too.
  • Helper docstring: "the digest image-refresh last APPLIED" is what refresh last observed on the first-observation path. That distinction is exactly the lag window the body describes; say "observed".
  • The #569 offline guarantee is narrower now: after a first observation the next upgrade can render a digest the node has never pulled, so an offline restart goes from "runs a stale image" to "cannot start". Worth one paragraph at the #569 comment block or in docs/MIGRATIONS.md.
  • docs/MIGRATIONS.md §server-side-apply conflict: one line that from 1.9.110 the chart renders the last-refreshed digest, so a Helm 4 .image conflict now signals annotation/live lag (wait a refresh tick, or --force-conflicts) rather than steady-state drift. In the steady state Helm applies the same value the kubectl-set manager holds, so the conflict disappears. That is a real improvement and deserves to be written down.
  • tests/jobs_manager_test.yaml:1231: the ticket reference in the new comment points at issue 199 of the wrong repo (the runtime ticket is the one meant), and this repo is public, so keep private-repo refs out of new comments.
  • scripts/tests/e2e-auto-upgrade.sh:248: with the unpullable sentinel digest, path 1 now Recreates jobs-manager onto an image that cannot pull, so it sits in Init:ImagePullBackOff for paths 2-5. Harmless today only because nothing after L248 reads a live pod; either use a pullable digest or assert that state explicitly so the next author is not surprised.
  • Please scrub the cluster and namespace names from the 07:56 validation comment, and the RFC / private-ticket refs from the body. This repo is public.

Merge order A → B, then I'll re-review.

@shujaatTracebloc shujaatTracebloc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LukasWodka's change-request lands on two things I said, and he is right on both. Flagging that explicitly so you are not stuck between an approval and a block on the same head — follow his direction, not mine, on both points below. My approval stands on everything else and I am not asking for anything further.

1. The suspend gate was my ask, and his objection defeats it. I required (not $ir.suspend) on the branch-2 conditions because a suspended CronJob would otherwise pin the last annotation forever, so a newly joined node would silently run a frozen build. That concern is real but his is worse, and I had not thought it through:

  • suspend: true exists to freeze an edge during an incident, and values.schema.json promises "pause without removing the resources".
  • With my gate, the next hourly bump re-renders jobs-manager (Recreate), requests-proxy and the DaemonSet to :tag/Always, so every node pulls whatever the tag points at right now — during the incident you paused for. Un-pausing rolls everything back to D1 and the next tick rolls forward again.
  • So the knob costs three unplanned rollouts and a possible downgrade, where before this PR flipping suspend changed no spec at all.

A frozen digest is at least known and reproducible; floating on a mutable tag with Always mid-incident is strictly less so. His framing is the correct one — suspend should stop polling, not un-pin — and keeping IfNotPresent on that branch is right. My original worry is about visibility of a stale pin, not about un-pinning: if it needs addressing, it wants a surfaced signal that refresh is paused, not a spec change. If you do end up wanting suspend to un-pin, his condition applies: that is a behaviour change needing its own schema text and a migration note.

2. My "cannot be unit-tested without a cluster" was wrong, and I let you off the hook on it. I wrote that the regexMatch guard and the digest-side suspend gate "rest on the stub render above and on your pullPolicy tests as a proxy. That is the ceiling without a cluster and I am not asking for more here." He points out helm-unittest 0.5.2 — the version CI pins — fakes lookup via kubernetesProvider, and he ran it 3/3 against HEAD with a mutation that reddens the first case. So branch 2 is unit-testable clusterless, and the two mutations I reported as un-reddenable can be pinned properly. Take his suite over my proxy; it closes the exact coverage gap I flagged and then waved.

Nothing else changes from my side: the two blockers I raised are closed and verified by render, and the merge order (A #1008 before B) still holds — #1008 is still carrying my change-request on the silent-flap line.

…up tests (client-runtime#199)

Addresses @LukasWodka's review on #1013.

RECONCILES A REVIEWER CONFLICT (@shujaatTracebloc). shujaat's earlier
BLOCKING-2 asked that imageRefresh.suspend UN-PIN — render :tag + Always so a
newly joined node re-pulls the current tag. LukasWodka's point 1 shows that is
wrong: suspend is used to FREEZE an edge during an incident, and un-pinning
re-renders jobs-manager (Recreate), requests-proxy and the resource-monitor
DaemonSet onto :tag/Always — three unplanned rollouts plus a downgrade to
whatever :tag points at now — contradicting the values.schema.json promise that
suspend "pause[s] without removing the resources".

Decision: suspend stops POLLING, not un-pinning. Removed the (not $ir.suspend)
gate from both tracebloc.controlPlaneDigest and tracebloc.controlPlanePullPolicy,
so a suspended edge keeps rendering the last-observed @digest + IfNotPresent.
This still answers shujaat's newly-joined-node concern: that node pulls the
frozen @digest — a real, previously-resolved image — not a missing one.

Also in this push (LukasWodka's other points):
- Add client/tests/control_plane_digest_test.yaml — a clusterless suite that
  fakes `lookup` via helm-unittest 0.5.2's kubernetesProvider. Covers the five
  image sites (init, api, pods-monitor, requests-proxy-follows-jobs-manager,
  resource-monitor), operator-pin beats the annotation, malformed -> :tag, the
  suspend-keeps-pin decision, and the disabled/mirror/first-install :tag edges.
  Mutation-verified: discarding the lookup reddens the digest cases, re-adding
  the suspend gate reddens the suspend case, neutering the regexMatch reddens
  the malformed case — the exact clusterless ceiling shujaat noted, now lifted.
- Remove the duplicate/misleading pull-policy cases appended to
  jobs_manager_test.yaml (the L1236 verbatim dup of L1130 and the now-wrong
  suspend->Always case, whose block comment wrongly claimed the helper "must NOT
  render that digest"); the private backend#199 ref in that comment goes with it.
- Docs the PR made false: image-refresh CronJob "HELM RE-RENDER" limitation now
  marked RESOLVED as of 1.9.110; SEAL-CHECK.md no longer tells operators to pin
  values to survive a revert the chart no longer performs.
- First-observation churn documented in the CronJob first-tick contract; helper
  docstring "last APPLIED" -> "last OBSERVED"; #569 offline-guarantee narrowing
  noted at controlPlanePullPolicy; MIGRATIONS.md server-side-apply gets a line
  that from 1.9.110 a Helm 4 .image conflict signals annotation/live lag.
- e2e-auto-upgrade.sh: the unpullable sentinel now documented and asserted
  explicitly (path 1 spec-only check + Init:ImagePullBackOff-by-design comment)
  so the next author is not surprised.

Merge order unchanged: B stays behind A (#1008). Chart stays 1.9.110.

helm lint clean; helm unittest 706/706 (41 suites); helm template renders :tag
clusterless and @digest on the operator pin; shellcheck + bash -n clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saqlainsyed007

Copy link
Copy Markdown
Contributor Author

Thanks @LukasWodka — all addressed in 616b843. Point by point:

1. suspend un-pinning → reversed. suspend now stops POLLING, not un-pinning. You're right, and this reconciles a genuine reviewer conflict, so I want to make the reversal explicit rather than silent:

@shujaatTracebloc — this reverses your earlier BLOCKING-2. You asked suspend to UN-PIN (render :tag + Always) so a newly joined node re-pulls the current tag. @LukasWodka's disruption/schema argument shows that is the wrong trade: suspend is used to freeze an edge during an incident, and un-pinning re-renders jobs-manager (Recreate), requests-proxy and the resource-monitor DaemonSet onto :tag/Always — three unplanned rollouts plus a downgrade to whatever :tag points at now — which contradicts the values.schema.json promise that suspend "pause[s] without removing the resources." So I removed the (not $ir.suspend) gate from both tracebloc.controlPlaneDigest and tracebloc.controlPlanePullPolicy; a suspended edge keeps rendering the last-observed @digest + IfNotPresent. Your newly-joined-node concern is still answered: that node pulls the frozen @digest — a real, previously-resolved image — not a missing one. If either of you wants suspend to un-pin after all, that's a behaviour change that needs its own schema/values text + migration note, so let's decide it explicitly rather than by omission.

The pull-policy branch reverts exactly to the pre-PR render (IfNotPresent when refresh is the update path), so flipping suspend no longer changes any spec.

2. Branch 2 IS unit-testable clusterless → added the suite. New client/tests/control_plane_digest_test.yaml using helm-unittest 0.5.2's kubernetesProvider (thanks for the working skeleton). Covers the full matrix: the five sites (init, api, pods-monitor, requests-proxy following the jobs-manager annotation, resource-monitor), operator-pin beats the annotation, malformed → :tag, the suspend decision (suspend keeps the pin), plus the disabled / mirror / first-install :tag edges. Mutation-checked, and this is the part that lifts the ceiling shujaat flagged:

  • discarding the lookup result → the digest cases redden (incl. the first case);
  • re-adding the suspend gate to controlPlaneDigest → the suspend case reddens;
  • neutering the regexMatch to if true → the malformed case reddens.

Dropped the "can't be unit-tested" sentences from the body and the 07:56 comment. And fixed the jobs_manager_test.yaml tail you flagged: removed the L1236 verbatim duplicate of L1130 and the misleading block comment ("must NOT render that digest" with no image: assertion) — the whole appended block is gone, which also removes the wrong-repo private backend#199 ref in that comment.

3. Merge order (hold) — kept A→B. B stays behind #1008; the only rebase conflict is client/Chart.yaml (1.9.110 vs a bumped develop), and I'll keep 1.9.110. Noted your point for A: A's reconcile must run before the #563 flap lockout latches, else nothing re-pins after a timed-out set image.

Non-blocking, all in the same push:

  • Docs the PR made false: image-refresh CronJob "HELM RE-RENDER" limitation now marked RESOLVED as of 1.9.110, and docs/SEAL-CHECK.md no longer tells an operator to pin values to survive a revert the chart no longer performs.
  • First-observation churn: written into the CronJob first-tick contract (the first chart bump after install renders @D0 over a live :tag, rolling the three workloads once for identical content; --atomic --wait can time out on a slow single-node edge).
  • Helper docstring: "last APPLIED" → "last OBSERVED".
  • Control-plane pods: offline-restart-safe update model (design) — jobs-manager/pods-monitor/resource-monitor (from #552) #569 offline guarantee: a paragraph at controlPlanePullPolicy — after a first observation an offline restart can hit an unpullable digest → "cannot start" instead of "runs stale" — with the mirror-exempt and side-loaded-k3d corollaries.
  • docs/MIGRATIONS.md §server-side-apply: a line that from 1.9.110 a Helm 4 .image conflict signals annotation/live lag (wait a tick, or --force-conflicts), not steady-state drift — the steady state applies the same value kubectl-set holds, so the conflict disappears.
  • e2e :248: the unpullable sentinel is now documented and asserted explicitly — path 1 gets a spec-only control-plane-image check, and a comment records that jobs-manager sits in Init:ImagePullBackOff by design while every later assertion is spec-only.
  • Public-repo scrub: cluster/namespace names removed from the 07:56 validation comment, and the RFC / private-ticket refs removed from the body.

Verify: helm lint clean; helm unittest 706/706 across 41 suites; helm template renders :tag clusterless and @digest on the operator pin; shellcheck + bash -n clean on the edited e2e. Chart stays 1.9.110.

Re-requesting your review.

@saqlainsyed007

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

LukasWodka
LukasWodka previously approved these changes Sep 9, 2026

@LukasWodka LukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 616b843 against my 62ca905 review. Approving.

Blocking 1 — suspend un-pinning: fixed. (not $ir.suspend) is gone from both tracebloc.controlPlanePullPolicy (L503) and tracebloc.controlPlaneDigest (L559), with the reasoning in the helper header. A suspended edge now keeps rendering the last-observed repo@digest + IfNotPresent, and the clusterless render with imageRefresh.suspend=true is byte-identical to the pre-PR render (:tag + IfNotPresent), so flipping the knob no longer changes any spec. Re-adding the gate locally reddens exactly the new suspend case and nothing else. Thanks to @shujaatTracebloc for reconciling this explicitly on-thread.

Blocking 2 — clusterless lookup suite: fixed. client/tests/control_plane_digest_test.yaml covers the matrix I asked for: the five image sites (api, wait-for-mysql init, pods-monitor on its own key, requests-proxy following the jobs-manager key, resource-monitor DaemonSet), operator pin beating the annotation, malformed → :tag, suspend keeps the pin, plus the disabled / mirror / first-install :tag edges. I mutation-checked it locally against 616b843 (helm-unittest 0.5.2, anchor verified applied each time, tree restored):

  • discard the lookup result → 6 fail (all five sites + suspend);
  • regex accept-all → only the malformed case fails; regex reject-all → the six valid-digest cases fail, malformed stays green;
  • fix the annotation key to jobs-manager → exactly pods-monitor and resource-monitor fail.
    The verbatim duplicate at jobs_manager_test.yaml:1236 and its misleading block comment are gone, and the "can't be unit-tested" claim is out of the body. 706/706 across 41 suites locally; Helm unit tests green in CI.

Non-blocking, all landed: CronJob header limitation 1 marked resolved as of 1.9.110; docs/SEAL-CHECK.md no longer tells operators to pin values to survive an upgrade; first-observation roll written into the first-tick contract (incl. the --atomic --wait timeout on a slow single-node edge); "applied" → "observed" at the helper and both call sites; the #569 offline-narrowing paragraph at controlPlanePullPolicy with the mirror and k3d corollaries; the MIGRATIONS.md server-side-apply line; the e2e sentinel is now documented and asserted at path 1 (spec-only, Init:ImagePullBackOff by design); the 07:56 comment and the body are scrubbed for this public repo.

One nit, no action needed now: the body says the #569 narrowing is documented "in docs/MIGRATIONS.md" — it lives at controlPlanePullPolicy in _helpers.tpl; MIGRATIONS carries only the SSA-conflict line. Fine either way.

Gate: 54 checks pass / 4 skipping, Bugbot clean on 616b843, one review thread and it is resolved, mergeable.

Do not merge before #1008. #1008 is still open; the body's dependency analysis (rollout-timeout and helm rollback lag states are self-healing only with A's live-spec reconcile) still holds. Land A, rebase for the client/Chart.yaml conflict keeping 1.9.110, then merge B.

{{- if .operatorDigest -}}
{{- .operatorDigest -}}
{{- else if and (include "tracebloc.imageRefreshEnabled" .root) (eq $mirror "docker.io") -}}
{{- $dep := lookup "apps/v1" "Deployment" .root.Release.Namespace (printf "%s-jobs-manager" (include "tracebloc.fullname" .root)) -}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKING (High) — a third annotation-vs-live lag state that #1008 cannot heal, and here it becomes a chart-enforced downgrade.

The merge-order section names two lag states (rollout timeout mid-tick, helm rollback). There is a third, and it is the one that does not self-heal: the image-refresh flap lockout.

Mechanism, all in image-refresh-cronjob.yaml on this branch:

  1. annotate_args is written in ONE batched kubectl annotate that runs after all three set image + rollout status pairs (~L700). Under set -e, a rollout status timeout on requests-proxy or the resource-monitor DaemonSet exits the tick before that annotate — so api is already at @D1 while all three last-refreshed-* annotations still say @D0.
  2. Each attempt increments ATTEMPT_KEY, which resets only on a fully settled rollout. After MAX_REFRESH_ATTEMPTS the flap guard exit 0s before the annotate block (~L616-631) and requires a human to clear the annotation to re-arm.

So the annotations are pinned at D0 indefinitely while the live spec is at D1. With this PR every subsequent helm upgrade now renders @D0 onto all five sites and, because auto-upgrade applies client-side (--reset-then-reuse-values --atomic --cleanup-on-fail, no --server-side), Helm's 3-way merge overwrites the live @D1 with @D0 — an active, deterministic downgrade of jobs-manager, pods-monitor and requests-proxy, potentially off a security refresh. Before this PR the re-render wrote a bare :tag, which was #199 (stale) but never an enforced move backwards.

And #1008 does not cover it: its live-spec reconcile sits inside the recorded == latest branch. Here recorded (D0) != latest (D1), so the tick takes the "digest changed" path, hits the flap guard, and the reconcile branch is never reached. Landing #1008 first fixes the rollback state but not this one.

Suggested fix in scope for B: make the helper ignore an annotation the live spec has already moved past — or in A, write each image's last-refreshed-* annotation immediately after that workload's own rollout status instead of batching all three at the end, and write the annotation before the flap guard's exit 0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed this is real and needs settling before merge, not leaving to merge order. Going with (a) — fix the annotation timing in #1008, not (b) here. Reasoning:

  • The annotation is the source of truth this helper trusts, so the correct fix is to stop it ever lying, not to teach the render path to detect that it's lying. (a) makes it truthful by construction: annotate each image right after that image's own rollout status succeeds, and move a settled workload's annotate before the flap guard's exit 0, so a lockout tick still records what actually rolled. Then recorded == latest holds and fix(image-refresh): re-pin the digest when a helm re-render reverts the workload to :tag #1008's existing live-spec reconcile covers the state — it's no longer stranded in the recorded != latest path.
  • (b) would add a second lookup of the live container image with exactly the fail-open semantics you flag one thread down at _helpers.tpl:561 (empty map on RBAC/apiserver failure), and it only masks the symptom in the render path — the annotation stays wrong for anything else that reads it, and "live has moved past" is ambiguous about which digest is then authoritative.
  • fix(image-refresh): re-pin the digest when a helm re-render reverts the workload to :tag #1008 is this PR's declared hard-prerequisite and must land first regardless, so putting the fix there costs no extra gating.

So this lands in #1008 (per-workload annotate, before the flap-guard exit 0), #1013 rebases on it, and this thread resolves once that's in. Leaving it open here until then.

On the related MIGRATIONS.md point: agreed the SSA-conflict line is Helm-4 hand-run only; I'll scope it to say the automated fleet path (Helm 3.16 client-side) overwrites rather than conflicts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real, and you're right that #1008 as written doesn't heal it — its live-spec reconcile sits in the recorded == latest branch, which the flap-lockout state never reaches. This is a BLOCKING precondition on A (#1008), not something B can fix alone, so per that I'm leaving this open and flagging it on #1008: the robust fix is A-side — write each image's last-refreshed-* annotation immediately after that workload's own rollout status (not one batched annotate after all three), and write it before the flap-guard's exit 0, so the annotation can never lag the live spec into a chart-enforced backward move. Your own 9666cf0 note confirms the mechanism: the fleet path is Helm-3 client-side, whose 3-way merge overwrites live @D1 with the rendered @D0 rather than conflicting. B stays held behind A until A covers this. (The tag-lookup-failed signal I added for the lookup thread makes a :tag fall-back observable, but not this backward-move — that needs the A-side ordering fix.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answering the D0-overwrite directly, and settling it rather than leaving it to merge order: option (a), in #1008. The last-refreshed-<image> annotation should be written per-workload, right after that workload's own rollout status, and before the flap guard's exit 0 — so a rollout that times out on requests-proxy or the resource-monitor DaemonSet can never leave api at @D1 with the annotation frozen at @D0. With that, this helper only ever renders a digest the live spec has actually reached, and the "healthy jobs-manager moved backwards D1→D0 under Recreate, every bump while the lockout holds" scenario cannot arise.

I'm keeping this in A, not B, because the annotation write-ordering is A's code (image-refresh-cronjob.yaml), and B renders whatever A records — a B-side "ignore an annotation the live spec moved past" (option b) would have to re-read the live container image and duplicate A's reconcile logic in a template, which is the wrong place for it. B stays held behind A (already its declared prerequisite), and this specific ordering fix is now an explicit precondition on A — I've flagged it on #1008 with your analysis, tied to @LukasWodka's "A's reconcile must run before, not behind, the lockout check." So: not a merge-order hope, a named A-side change that must land before B.

Leaving this thread open until #1008 carries it. On the related MIGRATIONS line — you're right; it's scoped to the hand-run Helm-4 SSA path now (the author's 9666cf0 already narrowed it: the Helm-3.16 client-side fleet path overwrites rather than conflicting).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refining the settlement now that A is essentially done. Option (a), split to an internal follow-up ticket rather than held inside #1008 — because your own re-review of A concluded its current scope is complete ("nothing else outstanding from me" after the rebump), and this is ordering hardening, not a gap in what A set out to do. The follow-up carries the exact spec: write each last-refreshed-<image>-digest annotation immediately after that workload's own rollout status, and before the flap guard's exit 0, so the annotation can never trail the live spec.

It must land before the exposure can matter on a fleet running B, and it's bounded until then: it needs the flap lockout (repeated rollout timeouts — e.g. a cordoned/NotReady node keeping the DaemonSet's desiredNumberScheduled from settling), A's recorded == latest reconcile heals every non-locked state, and @D0 is a valid, previously-pulled image, not an unstartable ref.

So: question answered (option a), fix specced and tracked, A landing first, B held behind it. If that settles it for you, this thread is yours to resolve / approve on; if you'd rather the ordering change land inside A before it merges, say so and I'll move it there instead of the follow-up — no strong preference on my side either, matching yours.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Settled — taking option (a) and the split, and I'm not re-opening the choice. Your reasoning for keeping it A-side is the right one: (b) would put a second live-image lookup into the render path with exactly the fail-open semantics of the _helpers.tpl:561 thread, and duplicate A's reconcile inside a template. The annotation should stop lying rather than the renderer learn to detect that it is.

backend#3575 captures it faithfully — the per-workload annotate, the exit 0 ordering, the rejected alternative, and the bound (needs the durable lockout; @D0 is a valid previously-pulled image). Good ticket.

Your direct question — "or move it inside A instead" — is moot now: A merged at 07:22:04Z, about five minutes after you wrote that. I checked merged develop rather than assuming: image-refresh-cronjob.yaml still writes every last-refreshed-*-digest through the single batched kubectl annotate $annotate_args at line 879, after the rollout status calls (833/842), with the flap guard's exit 0 at line 804 ahead of it. So #3575 is the only remaining home for the fix — the in-A option is gone.

That is the one thing I'd still like nailed down, and it's the only reason I'm leaving this open. Your own condition is "it must land before the exposure can matter on a fleet running B." With A merged, nothing sequences #3575 against this PR: #3575 is open, unassigned and unlabelled, and B is otherwise green. The condition is stated in two comment threads and an issue body, none of which can stop B merging first.

So this thread is no longer the original finding — that's answered — it's now just the sequencing marker. Resolve it when either #3575 lands, or the dependency is recorded somewhere that actually gates this PR. Happy either way; I only want the ordering to survive the fact that A is already in.

For the record, I'm not the blocker here — @LukasWodka's change-request is the standing one, and it's on the current head.

@saqlainsyed007

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

…en provenance (client-runtime#199)

Addresses @shujaatTracebloc's review threads on #1013 (the B-scope ones).

#2 (High) — the wait-for-mysql INIT container is no longer seeded from the
last-refreshed annotation. A digest a node never pulled fails as
Init:ImagePullBackOff (the pod never starts) on exactly the offline/side-loaded
k3d / Docker-Desktop edge #569 protects, and `kubectl set image` never touches
this container so the chart render would be its only, un-reconciled driver. The
pin buys nothing on a MySQL TCP wait. The site keeps honouring an explicit
operator pin (`images.jobsManager.digest`); it just no longer seeds the floating
annotation.

#3 (Medium) — `lookup` fails OPEN: Helm returns an empty map on every failure
(RBAC denial, apiserver 5xx, client timeout, kubectl-less renderer), so a read
failure was indistinguishable from a first install and would silently drop the
pin to `:tag` (client-runtime#199, now non-deterministic and unsignalled). New
`tracebloc.controlPlaneDigestSource` renders
`tracebloc.io/controlplane-digest-source` on the jobs-manager Deployment —
`values` | `annotation` | `tag` | `tag-lookup-failed` — so a monitored edge can
alert on the fallback. On an upgrade the Deployment MUST exist, so
`.Release.IsUpgrade` + an empty lookup ⇒ `tag-lookup-failed` (the anomaly). No
hard `fail` (would break `helm diff upgrade`, where lookup is empty too);
metadata-only, so it never rolls the pods.

#5 (Medium, doc) — documented the env-scoping caveat next to the suspend note in
controlPlaneDigest: a CLIENT_ENV/tag change at upgrade time is inert once a
digest is seeded (tag dropped when a digest is present), permanently so while
suspended. The containment (record the tag alongside the digest, honour the
annotation only when it matches) is an A-side change, called out here per the
review.

Tests: control_plane_digest_test.yaml flips the init case (init stays :tag with a
valid annotation present; still honours an operator pin) and adds the four
digest-source cases incl. tag-lookup-failed on `.Release.IsUpgrade`. helm-unittest
712/712 across 41 suites; helm lint clean; helm template renders :prod + source
`tag` clusterless and `tag-lookup-failed` under --is-upgrade. Mutation-checked:
re-seeding the init reddens its guard.

Not in this push (replied on-thread): #1 (BLOCKING, flap-lockout downgrade) and
#5's actual fix are A-side changes (#1008); #4's misleading e2e comment is
already fixed on-branch (a54deb0) and the explicit --atomic negative path is
proposed as a CI-validated follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saqlainsyed007

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@LukasWodka LukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 1c78acf (delta from 9666cf0: _helpers.tpl +72, jobs-manager-deployment.yaml +24/-1, control_plane_digest_test.yaml +106/-2).

Verified in the delta.

  • The wait-for-mysql init container is no longer seeded from the last-refreshed annotation and floats on :tag, while an explicit images.jobsManager.digest pin still applies. That answers the High on the init site: an init container pinned to a digest a node never pulled is Init:ImagePullBackOff with no opt-out, and set image never reconciles that site. Both cases are pinned by tests.
  • tracebloc.io/controlplane-digest-source records which branch produced the image ref (values / annotation / tag / tag-lookup-failed), with tag-lookup-failed gated on .Release.IsUpgrade so a first install is not misread as a read failure. The five source tests cover every branch, including the empty-lookup-on-upgrade anomaly. This makes the fail-open lookup observable, which is the right containment given a hard fail would break helm diff upgrade.

Nit, not blocking. controlPlaneDigestSource re-implements controlPlaneDigest's branch order and relies on a "must stay in lockstep" comment. That is a restated rule, not a derived one: a future change to the digest helper's priority silently desynchronises the provenance. Consider having one helper emit both (for example a source|digest pair the callers split) so there is a single decision.

Verdict: hold, not a change-request. Three threads stay open and none are mine: @shujaatTracebloc's BLOCKING flap-lockout finding on _helpers.tpl, his env-scoping one on the deployment, and Bugbot's High that duplicates the blocking thread. Saqlain has agreed the containment lives on the runtime side in #1008, and the bugbot / review job is red on this head because of that open thread. So this PR is still gated on #1008 landing first, exactly as my earlier approval said, and I am not approving over a blocking thread and a red check. Once #1008 is in and the threads above are resolved, ping me and I will approve on the next pass.

@shujaatTracebloc shujaatTracebloc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 1c78acf. The three asks I raised that were in scope for B are genuinely fixed in code, not just in replies — I checked each against the tree:

  • init container (High)wait-for-mysql now renders tracebloc.image with .Values.images.jobsManager.digest directly and never touches the annotation seed. The two unit cases pin both halves (stays :tag with a valid annotation present; still honours an operator pin). Right fix, right reasoning.
  • lookup fails open (Medium)tracebloc.controlPlaneDigestSource + tracebloc.io/controlplane-digest-source is the recorded-signal shape I was after, and the load-bearing distinction is implemented correctly: on an upgrade the jobs-manager Deployment must exist, so .Release.IsUpgrade plus an empty lookup is a read failure, rendered as tag-lookup-failed. Metadata-only, so no pod roll. Five test cases including the first-install non-anomaly.
  • doc/e2e asks — the operator-facing surfaces (values.yaml #569 NOTE, first-observation block, values.schema.json imageRefresh) now carry the narrowing, and the e2e asserts the rendered image: digest on both paths rather than annotation survival.

I also re-derived the fallback safety rather than taking it on trust: controlPlaneDigest regexMatches ^sha256:[a-f0-9]{64}$ before emitting and returns "" everywhere else, and tracebloc.image guards on if $digest, so every degraded path renders :tag — there is no repo@ malformed-ref reachable here. Whitespace trimming is clean in both defines, and the exact-string assertions in the new suite would redden if it were not. No new env keys, so the --set-string rule is not touched.

Still blocked, on three things:

  1. CI is red and correctly so. bugbot / review fails with one OPEN High — "Flap lockout pins a stale digest" — which is the duplicate of my BLOCKING thread on _helpers.tpl. That gate is derived from the threads, so a push will not clear it; only the fix (or a resolve) plus a re-run will.

  2. #1008 has not landed, and as it stands it does not yet carry either routed fix. I read A's current diff: the live-spec reconcile is inside the recorded == latest branch, annotate_args is still one batched annotate after all three rollout waits, and there is no last-refreshed-<image>-tag key anywhere. So the flap-lockout downgrade and the env-scoping gap are uncovered by A as well as by B today. A is still open and blocked on its own gates. Agreed with your routing — the annotation should not be taught to lie and then be second-guessed at render time — but B stays held until A actually carries it.

  3. Chart version collides with #1008. Observed directly just now: develop = 1.9.109, #1008 head eafc68c = 1.9.110, this PR = 1.9.110 — the two Chart.yaml blobs are byte-identical. This PR needs 1.9.111 (version and appVersion in lockstep). Worth flagging that chart content ⇒ Chart.yaml version bump is green and cannot help here: it diffs ${BASE_SHA}...HEAD against the frozen merge base at 1.9.109, so it structurally cannot see a number a sibling PR claimed after the fork. The PR body's "Chart conflict on rebase is only client/Chart.yaml — keep 1.9.110" is now the wrong instruction and should say 1.9.111.

Two non-blocking notes on the new helper, either fine to take here or defer:

  • controlPlaneDigestSource re-states controlPlaneDigest's three-branch resolution verbatim, kept in lockstep by a comment plus parallel test cases. A shared sub-helper that returns the branch, with the digest helper deriving from it, would make drift impossible rather than merely detectable.
  • The provenance annotation is rendered once with annotationImage: "jobs-manager" hard-coded, but pods-monitor and resource-monitor read their own annotation keys — so annotation can be reported while one of those sites actually fell back to :tag. The tag-lookup-failed value is unaffected (same single lookup), so the alert this helper exists for is still accurate; this is a completeness gap, not a wrong signal.

Happy to approve as soon as A lands with the per-workload annotate (and the tag-scoping key), this rebases to 1.9.111, and the Bugbot gate goes green on a re-run.

@LukasWodka LukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-requested at 1c78acf, the same head I commented on, so nothing in the diff has changed and my earlier notes stand as written.

On the flap-lockout thread: I agree with option (a), and I checked that #1008 does not carry it yet. At #1008's current head the last-refreshed-* annotations are still one batched kubectl annotate after all three set image + rollout status pairs, and the flap guard's exit 0 runs before any of them. So the lag state Shujaat described is live in A today, and B's helper would render it. Your 07:00 note on #1008 is the right place for the fix; once A annotates each workload right after its own rollout status and before the flap guard exits, the recorded == latest reconcile covers the case and this helper only ever renders a digest the live spec has reached.

Held, and what unblocks it, in order:

  1. #1008 gets the per-workload annotate, goes green, and lands.
  2. This branch rebases onto it and takes chart 1.9.111.
  3. The Bugbot thread here is the same finding; it can be resolved as a duplicate once (1) is in, and bugbot / review needs to be green on the rebased head.
  4. Shujaat's BLOCKING thread is his to close.

I will re-review on the rebased head without needing another re-request. Nothing further from me on this head.

…ent-runtime#199)

develop took 1.9.109 (client#1017), and A (#1008) rebumped to 1.9.110, so B at
1.9.110 would collide with A on the release train — one version number covering
two different charts, exactly what the version-bump guard exists to prevent
(@shujaatTracebloc on #1008). Bump B one above A: version + appVersion to
1.9.111, and re-point the feature-landing version in the doc comments
(_helpers.tpl, image-refresh CronJob header, MIGRATIONS.md, SEAL-CHECK.md,
e2e-auto-upgrade.sh) from 1.9.110 to 1.9.111.

helm lint clean; helm unittest 712/712 across 41 suites; shellcheck + bash -n
clean on the edited e2e. A→B merge order unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saqlainsyed007

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@LukasWodka LukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 3b4dce6. The rebump to 1.9.111 is right and every version reference moved with it (helper header, cronjob comment, MIGRATIONS, SEAL-CHECK, the e2e script). Nothing else in the chart changed, so my earlier notes stand.

Blocker, and the only one: this push commits seven review-reply drafts into the repository.

replies/bugbot-flap.md
replies/bugbot-lookup.md
replies/bugbot-tag.md
replies/clientenv.md
replies/flap.md
replies/init.md
replies/lookup.md

They are your thread replies verbatim, sitting at the repo root of a public chart repo. Nothing consumes them, the chart guard does not look there, and they carry reviewer names and cross-repo ticket references that #1020 and #1022 just spent two PRs keeping out of this repo. Please drop them from the branch (git rm -r replies/ and amend, or a follow-up commit) before anything else here moves.

Everything else is as before and unchanged by this head: Shujaat's BLOCKING flap-lockout thread waits on the A-side annotate ordering in #1008, which is not pushed there yet, and this branch needs to rebase once it lands. CI is running. Once the stray files are gone and #1008 carries the fix, I re-review on the rebased head without another re-request.

@shujaatTracebloc shujaatTracebloc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A (#1008) merged at 07:22 — and it did not carry either fix we routed to it. Flagging that now rather than at your next push, because the plan we agreed on has quietly lost its home and I would rather you hear it from me than rediscover it.

I approved #1008 on its own merits — the rebump landed, CI was green, and the scope it actually shipped was correct. What it never grew was the A-side work this PR is waiting on. Checked against develop at 3fbcb11:

  • Per-workload annotate: not there. The for entry in "$@" loop still runs 502→757 with annotate_args accumulating inside it and applied in a single batched kubectl annotate after the loop. The shape we agreed on — write each image's last-refreshed-* immediately after that workload's own rollout status, before the flap guard's exit 0 — is not implemented.
  • Tag/env-scoped key: not there. grep -c 'last-refreshed-[^"]*-tag' returns 0 across both image-refresh-cronjob.yaml and _helpers.tpl on develop. last-refreshed- appears 4 times, all unscoped.

So both concerns need a new PR against develop, not #1008. Concretely:

  1. PRRT_kwDOMzdex86gtjSW (still open here) — the flap-lockout downgrade. Your reasoning for routing it to A was right and I am not reopening it: the annotation should be made truthful by construction rather than second-guessed at render time. That argument is unchanged; only its destination is.
  2. The env/tag-scoping containment you documented in 1c78acf and resolved on my other thread — the doc landed, the containment did not, and it was also A-side.

Neither is a criticism of the merge. A shipped what it was scoped to ship. But "tracked on #1008" stopped being true fifteen minutes ago, and a routed fix whose destination has merged is exactly how a known defect becomes a forgotten one.

Also, this PR is now CONFLICTING — the Chart.yaml collision I flagged, arriving on schedule. develop is 1.9.110 (A's), this branch is 1.9.111. Merge develop and keep 1.9.111; the numbers are already sequenced correctly, so it is a one-line resolution and no further rebump is needed.

Where that leaves the gate: CONFLICTING/DIRTY, one mutation-check still running, and PRRT_kwDOMzdex86gtjSW open. Nothing here is new review debt on your side beyond the merge — the three asks I raised last round are all still verified fixed in code, and the 1.9.111 rebump was exactly right.

Happy to approve once the conflict is resolved, the flap-lockout fix has a landing branch (here or a new A2 — your call, and if you would rather do it here I will not hold the routing against you), and CI is green.

# Conflicts:
#	client/Chart.yaml
#	client/templates/image-refresh-cronjob.yaml
#	client/values.yaml
@saqlainsyed007

Copy link
Copy Markdown
Contributor Author

bugbot run

@LukasWodka LukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-requested at a7553fc. This head merges develop, so the conflict is gone, the chart stays at 1.9.111 above develop's 1.9.110, and the branch now carries the merged CronJob side. Good.

The one blocker is unchanged. replies/ is still in the tree at this head, all seven files:

replies/bugbot-flap.md  replies/bugbot-lookup.md  replies/bugbot-tag.md
replies/clientenv.md    replies/flap.md           replies/init.md   replies/lookup.md

git rm -r replies/ and push; that is the whole ask. My change-request stands until then, and I will re-review without another re-request as soon as the tree is clean.

Still open beyond that, unchanged: Shujaat's BLOCKING thread waits on the per-workload annotate ordering, which the merged CronJob does not yet have and which needs its own follow-up PR; CI is running on this head.

@shujaatTracebloc shujaatTracebloc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conflict resolved cleanly and the version is right — 1.9.111 over develop's 1.9.110, exactly as sequenced. Thanks for turning that around fast.

One thing to fix before this merges: seven scratch files came along with the merge.

replies/bugbot-flap.md
replies/bugbot-lookup.md
replies/bugbot-tag.md
replies/clientenv.md
replies/flap.md
replies/init.md
replies/lookup.md

All seven are status=added, one line each, and replies/ does not exist on develop — so this PR would create it. The content is your draft review-reply text: replies/flap.md opens "Real, and you're right that #1008 as written doesn't heal it…", which is verbatim what you posted on the flap-lockout thread. Classic git add -A sweep of a scratch directory.

Nothing in CI will catch this — there is no lint rule for stray markdown, and it does not affect the chart — which is exactly why it is worth a second pair of eyes. git rm -r --cached replies/ and a line in .gitignore if you want to keep using that directory locally.

On the rest — no new asks.

The image-refresh-cronjob.yaml change (+30/−11) is comments only, and the re-framing is right: documenting the revert as HANDLED from both sides, chart-side here from 1.9.111 and script-side in the merged #1008, is a more accurate description of the world now that A has landed. Reclassifying the flap-lockout residue from "blocked on A" to a tracked follow-up is the correct response to A having merged without it — I would rather see it named in the comment than silently dropped. The first-observation roll block is a genuinely useful addition: spelling out that the next chart bump rolls all three workloads once for byte-identical content, and that --atomic --wait can time out on a slow single-node edge and re-attempt, is the kind of thing an operator hits at 2am and finds nowhere.

Still open, unchanged: PRRT_kwDOMzdex86gtjSW — the flap-lockout downgrade. I am leaving it open deliberately, not as a merge condition argument but as the marker that the fix now needs a home; per my last comment it is a new PR against develop, and I confirmed the containment is still absent (grep -c 'last-refreshed-[^"]*-tag' in image-refresh-cronjob.yaml returns 0 both here and on develop — the only hits in this diff are a doc comment and one of the stray files above). Your call whether that lands here or as an A2; say which and I will treat the thread accordingly.

CI is mid-run on the new head. Drop the replies/ directory and, once it is green, I will approve — the chart work itself has been ready for a while.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a7553fc. Configure here.

@LukasWodka

Copy link
Copy Markdown
Contributor

Heads-up from a neighbouring PR, not a review: #1028 routes the same five image lines (the registry argument) through a new tracebloc.tbRegistry helper, and replaces the eq $mirror "docker.io" test in tracebloc.controlPlanePullPolicy with tracebloc.imageRefreshResolvable (derived from one declaration, docker.io ghcr.io, that the image-refresh script's inert guard also reads).

This PR's tracebloc.controlPlaneDigest / controlPlaneDigestSource re-derive that same $mirror / docker.io condition twice. Whichever of the two lands second rebases; if that is this one, the two conditions become (include "tracebloc.imageRefreshResolvable" .root) and the digest argument composes with the registry argument on the same line. No action needed now.

@LukasWodka

Copy link
Copy Markdown
Contributor

Not a re-review — the head is still a7553fc, the same commit my change-request is on, so nothing has changed on my side: replies/ (7 files) is still in the tree.

Two new facts since then that you will hit on the next push:

Same deal as before: git rm -r replies/, rebase, and I re-review the new head without another re-request.

@LukasWodka

Copy link
Copy Markdown
Contributor

Still the same head (a7553fc) as my 2026-09-10 review and my 2026-09-11 ping — nothing has moved, so this is a status update, not a new review pass.

The blocker is unchanged and is the only thing stopping this: replies/ (all 7 files) is still committed at the repo root of this public chart repo. git rm -r replies/ is the whole ask on that front.

Drift has gotten worse while this sat:

Everything else is in good shape: CI is fully green, Bugbot is clean, and Shujaat's flap-lockout thread is settled to a pure sequencing marker on backend#3575 (not a blocker on this PR per his own last comment).

Same offer as before: git rm -r replies/, rebase onto current develop with the three fixes above, rebump the chart, push — and I'll re-review the new head without needing another re-request.

@LukasWodka

Copy link
Copy Markdown
Contributor

One addition to my 09:51 rebase notes, still the same head (a7553fc), so not a new review pass:

Everything else stands as written in the earlier comments: git rm -r replies/, rebase, rebump above 1.9.117, swap the three eq $mirror "docker.io" gates for tracebloc.imageRefreshResolvable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants