diff --git a/stacks/kyverno/modules/kyverno/keel-annotations.tf b/stacks/kyverno/modules/kyverno/keel-annotations.tf index 3c17d87a..e1f6cba3 100644 --- a/stacks/kyverno/modules/kyverno/keel-annotations.tf +++ b/stacks/kyverno/modules/kyverno/keel-annotations.tf @@ -26,7 +26,7 @@ resource "kubernetes_manifest" "policy_inject_keel_annotations" { "policies.kyverno.io/title" = "Inject Keel Auto-Update Annotations" "policies.kyverno.io/category" = "Automation" "policies.kyverno.io/severity" = "low" - "policies.kyverno.io/description" = "Adds keel.sh/policy: force + trigger: poll annotations to workloads in namespaces labeled keel.sh/enrolled=true. Phase rollout per docs/plans/2026-05-16-auto-upgrade-apps-{design,plan}.md." + "policies.kyverno.io/description" = "Adds keel.sh/policy: force + match-tag: true + trigger: poll annotations to workloads in namespaces labeled keel.sh/enrolled=true. force+match-tag is the safe pairing: Keel watches the deployment's CURRENT tag for digest changes only, never rewrites the tag string. Phase rollout per docs/plans/2026-05-16-auto-upgrade-apps-{design,plan}.md." } } spec = { @@ -86,33 +86,42 @@ resource "kubernetes_manifest" "policy_inject_keel_annotations" { patchStrategicMerge = { metadata = { annotations = { - # `+(...)` only adds if not present; per-workload overrides win. + # DEFAULT IS `force` + `match-tag: true` — the safe-force + # pairing learned from the 2026-05-16 :17 incident. # - # DEFAULT IS `patch` — Keel auto-updates only PATCH versions - # within the current major.minor. e.g. 0.26.6 → 0.26.7 is OK, - # 0.26.6 → 0.27.0 is NOT, 0.26.6 → :nightly-latest is NOT. + # How safe-force works: + # - `force` alone polls the registry and grabs the NEWEST + # tag (any tag), which is what downgraded claude-memory + # from :71b32438 → :17 (numeric "17" sorted higher than + # hex SHA). UNSAFE on its own. + # - `match-tag: "true"` constrains `force` to watch ONLY + # the deployment's CURRENT tag string for DIGEST changes. + # Keel never rewrites the tag — it just rolls the pod + # when the digest behind that tag changes. This is the + # correct primitive for `:latest` (and `:major`-style + # floating tags). # - # Why not `force`: the 2026-05-16 incident — Keel's `force` - # policy is "always update to the newest tag in the registry," - # not "watch current tag for digest changes." On semver-pinned - # workloads, force triggered tag-rewrites (affine → nightly, - # calico → master). `patch` is semver-parser-bounded and safe. + # Effect per tag type: + # - `:latest` / `:nightly` / `:v1` (mutable): Keel rolls + # whenever upstream pushes a new digest under that tag. + # ⇐ This is the auto-update behaviour the design wants. + # - `:1.2.3` / `:71b32438` (immutable/content-addressed): + # digest never changes ⇒ Keel does nothing ⇒ pinned. + # ⇐ Safe-by-default for SHA-pinned workloads. # - # Caveats of `patch`: - # - Tags that aren't parseable as semver (e.g. `:latest`, - # `:11`, `:nightly`, SHA tags) are ignored by Keel. - # - For services pinned to semver, Keel will REWRITE the - # tag (0.26.6 → 0.26.7). This causes Terraform drift - # until the stack is updated or its lifecycle adds - # `ignore_changes` on the container[].image field. - # For now, accepting periodic drift (drift_detection.yml - # pipeline will surface it). + # `+(...)` is anchor-preserve (add only if missing). We DROP + # `+()` on `policy` and `match-tag` so an apply migrates + # existing workloads from the old `patch` default to the new + # `force + match-tag` pair. Annotation-only changes do NOT + # restart pods; future digest changes do. # - # Per-workload overrides: - # "keel.sh/policy" = "force" — for mutable tags (:latest) - # "keel.sh/policy" = "minor" — wider semver bumps - # "keel.sh/policy" = "never" — opt out (CI-bumped, deliberate pins) - "+(keel.sh/policy)" = "patch" + # Per-workload overrides (set via kubectl/Terraform): + # "keel.sh/policy" = "never" — opt out (set the LABEL too + # to bypass this mutation) + # Per-namespace opt-out: + # Remove the `keel.sh/enrolled=true` namespace label. + "keel.sh/policy" = "force" + "keel.sh/match-tag" = "true" "+(keel.sh/trigger)" = "poll" "+(keel.sh/pollSchedule)" = "@every 1h" }