Skip to content

Prevent spurious canary rollouts using a server-side change fence#1942

Open
stefanprodan wants to merge 5 commits into
mainfrom
feat/server-side-change-fence
Open

Prevent spurious canary rollouts using a server-side change fence#1942
stefanprodan wants to merge 5 commits into
mainfrom
feat/server-side-change-fence

Conversation

@stefanprodan

@stefanprodan stefanprodan commented Jul 2, 2026

Copy link
Copy Markdown
Member

Upgrading Flagger triggers canary analyses for unchanged workloads because the spec hash is computed with spew over the decoded Go structs: every field, including unset ones, ends up in the hash, so any k8s.io/api upgrade that adds a field to PodSpec changes the hash of an unchanged deployment.

This PR makes change detection independent of the client schema:

  • The spec hash is now computed from the canonical JSON representation (unset fields omitted, sorted keys, sha256, v2: prefix), so new struct fields no longer move the hash.
  • A change fence is stored in status.lastTrackedRevision (<uid>/<counter>[/<deployment-revision>], counter = metadata.generation for Deployments/DaemonSets, resourceVersion for Services, latest revision name for Knative). When the fence proves the spec did not change, a hash mismatch is an upgrade artifact and is absorbed by re-baselining the status instead of triggering an analysis. When the fence moved, hashes decide between replicas-only changes, manual rollbacks and real changes.
  • Existing canaries migrate once, without triggering: settled canaries adopt the current state as baseline (guarded by a canary-vs-primary image comparison), in-flight and failed canaries restart their analysis once.

Fixes #1673
Closes: #1923

@stefanprodan
stefanprodan requested a review from aryan9600 as a code owner July 2, 2026 09:34
@stefanprodan stefanprodan added the kind/enhancement Improvement request for an existing feature label Jul 2, 2026
@stefanprodan
stefanprodan force-pushed the feat/server-side-change-fence branch from d402007 to 4204312 Compare July 2, 2026 09:35
The field records the server-side change fence of the target workload as
<uid>/<counter>[/<deployment-revision>], where the counter is
metadata.generation for Deployments and DaemonSets, resourceVersion for
Services and the latest created revision name for Knative Services. It
identifies the exact object state that lastAppliedSpec was computed
from, allowing hash drift caused by Flagger or cluster upgrades to be
told apart from real spec changes.

No codegen changes required: the generated deepcopy covers the string
field via the struct value copy.

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
Additive change applied to all three CRD copies (artifacts, Helm chart
and kustomize base).

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
Flagger detected target changes by comparing spew-based hashes of
decoded Go structs. Spew prints every struct field, so the hash encoded
the k8s.io/api schema and changed on every dependency upgrade,
triggering spurious canary analyses for unchanged workloads (#1673).

The spec hash is now computed from the canonical JSON representation
(unset fields omitted, sorted keys, sha256, 'v2:' prefix) and stored
together with the change fence in status.lastTrackedRevision. When the
fence proves the spec did not change, a differing hash is an upgrade
artifact and is absorbed by re-baselining the status; when the fence
moved, hashes decide between replicas-only changes, manual rollbacks
and real changes. For Deployments, a revision annotation component
(recorded only while rollouts are not paused and the deployment
controller has observed the current generation) lets scale-only
generation bumps coincide with hash drift without false triggers.
Volatile counters (Service resourceVersion) are never chased with
status writes, and untrusted fences (generation 0, empty uid) fall back
to hash comparison and never absorb. Flagger's own scale-down writes
refresh the fence from the write response, guarded so a concurrent
change is never absorbed.

Canaries with legacy status migrate once: settled phases adopt the
current state as baseline (guarded by a canary-vs-primary image
comparison for Deployments/DaemonSets and a latest-vs-promoted revision
comparison for Knative), while in-flight and failed canaries restart
their analysis once — loud but safe. All new status writes go through a
DeepCopy and abort when a concurrent writer moved the tracked spec, so
the in-memory canary object is never mutated and the
(lastAppliedSpec, lastTrackedRevision) pair always describes the same
observed object state.

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
The freshly initialized canary status is copied back into the local
object before shouldAdvance runs; without the fence the next evaluation
would take the migration branch spuriously.

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
Covers the decision table per rule (uid gate, drift absorption with
lastPromotedSpec lockstep, replicas-only fence refresh, revision
backfill, manual rollback, secondary revision guards, incapable and
volatile fences), the phase-aware migration incl. the image comparison
heuristic and its failure mode, persistence freshness checks under
conflict retries, persist-failure verdict stability, the DaemonSet
scale-to-zero normalization edge and golden hash values pinning the
canonical JSON representation against future k8s.io/api upgrades. The
Knative change detection test now exercises the v2 path instead of
passing vacuously through the migration branch.

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
@stefanprodan
stefanprodan force-pushed the feat/server-side-change-fence branch from 4204312 to ba973e4 Compare July 2, 2026 10:15
@aryan9600

Copy link
Copy Markdown
Member

is the canary vs primary image comparison guard to detect the "common" way of active rollouts? since rollouts can be triggered by any spec change.

@stefanprodan

Copy link
Copy Markdown
Member Author

is the canary vs primary image comparison guard to detect the "common" way of active rollouts? since rollouts can be triggered by any spec change.

No, the image diff is a one-time migration event from the legacy hash to v2, which happens once per cluster, first time the new Flagger version is rolled out. There is no other stable field that we can use to detect if the primary/canary drifted while Flagger was shutdown (this window is very short).

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

Labels

kind/enhancement Improvement request for an existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upgrading to Flagger built on main (commit id #133fdec) causes canary rollouts when no change in deployments

2 participants