Repository navigation
fix(vertical-pod-autoscaler): migrate admission-controller TLS to cert-manager to stop reconcile-driven ReplicaSet churn - #3178
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThis PR migrates VPA admission-controller webhook TLS provisioning to cert-manager when enabled, adds the supporting chart value and platform dependency, updates rollout annotations, applies the upstream chart patch during build, and expands render tests for the new TLS paths. ChangesVPA admission-controller cert-manager TLS
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…real name certificate.yaml renders the Certificate with name <fullname>-tls (its secretName matches the legacy chart-owned Secret exactly), but the rollout hook was passing plain <fullname> to `kubectl wait` — so every post-install/post-upgrade cycle sat 3 minutes on NotFound, failed, and burned through the Job's backoffLimit. cozystack-platform then aborted its HR reconcile because the VPA release's post-upgrade hook was Failed. Snapshot from the first CI run of #3178 confirms: - Certificate object: vertical-pod-autoscaler-admission-controller-tls (namespace cozy-vertical-pod-autoscaler) - Rollout Job pods: 10+ BackOff / BackoffLimitExceeded - HR condition: post-upgrade hooks failed - Admission-controller Deployment itself: 1 ReplicaSet, 1 Ready pod (the actual chart change works — churn is gone; only the rollout trigger was broken) Fix propagates the -tls suffix through both places that reference the Certificate: * The rollout container's `cert` variable used in `kubectl wait`. * The Role's resourceNames on cert-manager.io/certificates so RBAC still admits the get/watch/list against the actual object. Regression test in vpa_test.yaml asserts rules[0].resourceNames[0] carries the -tls suffix so this exact off-by-suffix bug cannot come back through a future rename. Signed-off-by: Ivan Okhotnikov <[email protected]>
…real name certificate.yaml renders the Certificate with name <fullname>-tls (its secretName matches the legacy chart-owned Secret exactly), but the rollout hook was passing plain <fullname> to `kubectl wait` — so every post-install/post-upgrade cycle sat 3 minutes on NotFound, failed, and burned through the Job's backoffLimit. cozystack-platform then aborted its HR reconcile because the VPA release's post-upgrade hook was Failed. Snapshot from the first CI run of #3178 confirms: - Certificate object: vertical-pod-autoscaler-admission-controller-tls (namespace cozy-vertical-pod-autoscaler) - Rollout Job pods: 10+ BackOff / BackoffLimitExceeded - HR condition: post-upgrade hooks failed - Admission-controller Deployment itself: 1 ReplicaSet, 1 Ready pod (the actual chart change works — churn is gone; only the rollout trigger was broken) Fix propagates the -tls suffix through both places that reference the Certificate: * The rollout container's `cert` variable used in `kubectl wait`. * The Role's resourceNames on cert-manager.io/certificates so RBAC still admits the get/watch/list against the actual object. Regression test in vpa_test.yaml asserts rules[0].resourceNames[0] carries the -tls suffix so this exact off-by-suffix bug cannot come back through a future rename. Signed-off-by: Ivan Okhotnikov <[email protected]>
a0418c7 to
1a80527
Compare
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses a critical issue where the VPA admission-controller webhook TLS Secret was being regenerated on every Helm reconcile, causing constant ReplicaSet churn and deployment livelocks. By migrating to cert-manager, the system now achieves stable TLS management. The changes include robust migration hooks to handle existing Secrets and ensure a smooth transition without cluster-wide disruption. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request updates the vertical-pod-autoscaler platform source to depend on cozystack.cert-manager, ensuring that the VPA admission-controller webhook TLS is provisioned correctly before reconciliation to prevent crash loops. Additionally, a comprehensive suite of Helm unit tests has been added to vpa_test.yaml to verify the behavior of the admission-controller TLS configuration under various scenarios, including when certManagerEnabled is true or false, when an existingSecret is used, and when the admission controller is disabled. There are no review comments, and I have no feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml (1)
81-84: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winHash all mounted TLS material, not only
tls.crt.The pod reads
ca.crt,tls.crt, andtls.key; if cert-manager updatesca.crtortls.keywithout changingtls.crt, this annotation will not roll the admission-controller.Proposed adjustment
- {{- if and $secret $secret.data (index $secret.data "tls.crt") }} - checksum/tls-cert: {{ index $secret.data "tls.crt" | sha256sum }} + {{- if and $secret $secret.data (index $secret.data "ca.crt") (index $secret.data "tls.crt") (index $secret.data "tls.key") }} + checksum/tls-cert: {{ printf "%s:%s:%s" (index $secret.data "ca.crt") (index $secret.data "tls.crt") (index $secret.data "tls.key") | sha256sum }} {{- end }}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml` around lines 81 - 84, Update the admission-controller Deployment template so the checksum annotation covers all mounted TLS Secret data, not just tls.crt. In the existing lookup block that sets checksum/tls-cert, hash the Secret’s relevant entries from $secret.data—ca.crt, tls.crt, and tls.key—so any cert-manager change to the mounted TLS material triggers a rollout. Keep the logic in the same deployment.yaml template around the admission-controller TLS secret lookup and checksum annotation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/tls-migrator.yaml`:
- Around line 193-208: The Secret ownership cleanup in tls-migrator.yaml only
removes Helm and Flux metadata, so Helm can still prune the legacy Secret when
tls-secret.yaml disappears. Update the migration flow around the Secret-handling
logic to preserve it with a helm.sh/resource-policy: keep handoff, or keep
rendering the Secret for one more release until cert-manager takes over. Make
the change in the tls-migrator template where the kubectl annotate/label steps
run, so the Secret survives the upgrade.
---
Nitpick comments:
In
`@packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml`:
- Around line 81-84: Update the admission-controller Deployment template so the
checksum annotation covers all mounted TLS Secret data, not just tls.crt. In the
existing lookup block that sets checksum/tls-cert, hash the Secret’s relevant
entries from $secret.data—ca.crt, tls.crt, and tls.key—so any cert-manager
change to the mounted TLS material triggers a rollout. Keep the logic in the
same deployment.yaml template around the admission-controller TLS secret lookup
and checksum annotation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1886de58-2148-4791-9d7b-36624259e1e1
📒 Files selected for processing (7)
packages/core/platform/sources/vertical-pod-autoscaler.yamlpackages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/certificate.yamlpackages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yamlpackages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/tls-migrator.yamlpackages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/tls-secret.yamlpackages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/values.yamlpackages/system/vertical-pod-autoscaler/tests/vpa_test.yaml
Andrei Kvapil (kvaps)
left a comment
There was a problem hiding this comment.
The root-cause analysis is right (per-render genCA + checksum/tls-secret amplifier), the gating is clean, the existingSecret contract is preserved, and the test coverage is thoughtful. The overall direction — cert-manager-owned TLS — is the correct fix. That said, I found three things that need to be addressed before this can merge, plus a few smaller items.
1. Vendored chart is edited in place — the next make update silently reverts this fix. This package's update target does rm -rf charts && helm pull cowboysysop/vertical-pod-autoscaler --version 11.1.1 with no re-apply step. The repo convention for carrying local modifications on vendored charts is patches/*.diff applied in the update target (see packages/system/fluxcd-operator/Makefile). Please either add patch files, or move the cozystack-specific resources into the umbrella chart's own templates/. Upstream is currently at exactly 11.1.1 with no cert-manager support, so there is no bump path — upstreaming certManagerEnabled to cowboysysop would be a nice follow-up.
2. The tls-migrator doesn't do what its comments claim, for two independent reasons.
- Helm decides what to delete on upgrade from the stored release manifest, and the only live-object opt-out is
helm.sh/resource-policy: keep— seepkg/kube/client.goin Helm (theoriginal.Difference(target)loop only checksResourcePolicyAnno). Strippingmeta.helm.sh/*andapp.kubernetes.io/managed-bydoes not prevent the deletion; those markers only matter for adoption checks on resources entering a manifest. Likewise, helm-controller has no independent label-based prune for Helm-managed resources — thehelm.toolkit.fluxcd.io/*labels are tracking metadata only. - Even if the Secret survived, in-place adoption can't work: the legacy Secret is
type: Opaque, cert-manager managestype: kubernetes.io/tls, andSecret.typeis immutable — cert-manager's apply would fail with "field is immutable" and the Certificate would never become Ready.
In practice the migration converges because the Job is a no-op: Helm deletes the old Opaque Secret, cert-manager mints a fresh one of the right type, and the Deployment rolls anyway (its podTemplate annotations change in the same upgrade). Given failurePolicy: Ignore on the webhook, the seconds-long window is acceptable. I'd suggest deleting the migrator (with its SA/Role/RoleBinding and the hardcoded clastix/kubectl image) and documenting the real flow — it's simpler and it removes ~200 lines of machinery whose rationale doesn't hold.
3. The PR description no longer matches the diff. It describes a tls-rollout.yaml post-upgrade hook (3m Certificate wait + rollout) that isn't in the diff — the checksum/tls-cert lookup annotation replaced it, as the deployment.yaml comment itself explains — and it claims 18 new tests where the diff has 15. Please refresh the description; reviewers and future archaeologists will read it as the design doc. Also, the test plan says the dev3 live migration would be completed "before removing draft" — the PR is out of draft, so please post the results (or re-draft).
A few more, non-blocking:
- The migrator Job pod reuses the admission-controller
selectorLabels, so while it runs it matches thevpa-webhookService selector and becomes a bogus endpoint. If the Job survives this review, give it dedicated labels. (moot if the migrator is dropped) - The
lookupcomment says the checksum "resolves at every reconcile" — it doesn't;lookuponly executes during actual Helm actions (install/upgrade/rollback), and helm-controller doesn't re-render a deployed release on interval ticks. Consequence worth documenting: after cert-manager renews at ~day 350, nothing rolls the pod, so on a cluster with no upgrades or pod restarts for 15+ days after renewal the in-memory cert expires at day 365 and the webhook silently stops mutating until the next restart (--reload-certalone would make this worse — the serving cert would rotate while the self-registered caBundle goes stale). Fine to accept given cozystack's release cadence, but the comment should state the real trade-off. - Flipping the escape hatch back (
certManagerEnabled=falseafter migrating) will fail Helm's ownership adoption check on the now cert-manager-owned Secret — worth a sentence in the values comment that the switch is effectively one-way without manual Secret deletion. secretTemplatere-applies chart labels includingapp.kubernetes.io/managed-by: Helmto a Secret Helm doesn't own — drop or slim it.- Nit: the comment banner above the "admission-controller disabled" test group lost its indentation.
CI: the E2E run is red (platform HR timed out on PackageSource/cozystack.vm-disk-application, and the VPA HR itself only became Ready ~30 min into install). It doesn't look VPA-caused, but it also means this run doesn't demonstrate the e2e stabilization this PR targets — please re-run/investigate before merge.
Kvaps' review on #3178 correctly identified that the pre-upgrade tls-migrator Job cannot do what its comments claim, for two independent reasons: 1. Helm decides what to delete on upgrade from the STORED release manifest, not from live labels on the object. The only live- object opt-out is the \`helm.sh/resource-policy: keep\` annotation — see the \`original.Difference(target)\` loop in helm's pkg/kube/client.go, which only checks \`ResourcePolicyAnno\`. Stripping \`meta.helm.sh/*\` and \`app.kubernetes.io/managed-by\` does not prevent the deletion; those markers only matter for adoption checks on resources entering a manifest, not on resources leaving it. Likewise, helm-controller has no independent label-based prune for Helm-managed resources — the \`helm.toolkit.fluxcd.io/*\` labels are tracking metadata only, not a deletion gate. 2. Even if the Secret survived the Helm/Flux delete pass, cert-manager still could not adopt it in place: the legacy Secret has \`type: Opaque\`, cert-manager writes \`type: kubernetes.io/tls\`, and Secret.type is immutable in Kubernetes. The Certificate reconcile would fail with \"field is immutable\" and never become Ready. The migration converges anyway because the Job is a no-op: Helm deletes the old Opaque Secret, cert-manager mints a fresh one of the right type, and the Deployment rolls in the same upgrade (its podTemplate carries the new \`checksum/tls-cert\` annotation, which is the sha of the runtime Secret and therefore differs the first time cert-manager writes new content). The webhook's \`failurePolicy: Ignore\` makes the seconds-long window benign. Delete the migrator entirely: template file, RBAC (ServiceAccount + Role + RoleBinding), the hardcoded clastix/kubectl image reference, and the four helm-unittest cases pinning its hook wiring / RBAC scope. ~200 lines removed, one less pinned image, and the documented migration flow now matches what actually happens. Existing helm-unittest suite: 15/15 pass (was 19 with the migrator tests; removed 4 that referenced the retired template). Signed-off-by: Ivan Okhotnikov <[email protected]>
…diff on the vendored chart Kvaps' review on #3178 flagged that this package's \`make update\` target does \`rm -rf charts && helm pull cowboysysop/vertical-pod- autoscaler --version 11.1.1\` with no re-apply step. Any local edits to the vendored subchart were therefore one \`make update\` away from vanishing without warning — exactly what the fluxcd-operator package pre-empts via a \`patches/*.diff\` re-apply loop after the helm pull. Adopt the same convention here: * New file \`patches/cert-manager-admission-tls.diff\` capturing all four modifications the cert-manager path makes to the vendored subchart — the new \`certificate.yaml\` template, the checksum/tls-secret + new checksum/tls-cert annotation gating on the admission-controller Deployment, the certManagerEnabled guard on the legacy \`tls-secret.yaml\`, and the new \`certManagerEnabled: true\` default in the subchart values. * Makefile \`update\` target now runs \`patch --no-backup-if-mismatch -p1 < patches/*.diff\` after the helm pull, so a future upstream bump (or a routine \`make update\` today) re-applies the cozystack modifications over the freshly-fetched vendored files and leaves the chart in the same state git tracks. Verified end-to-end: with the on-disk chart intact I ran cp -r charts/vertical-pod-autoscaler /tmp/reference rm -rf charts helm pull cowboysysop/vertical-pod-autoscaler --untar --untardir charts --version 11.1.1 patch --no-backup-if-mismatch -p1 < patches/cert-manager-admission-tls.diff diff -r charts/vertical-pod-autoscaler /tmp/reference The diff is empty — \`make update\` now produces a byte-identical tree to what git has right now, and the 15 helm-unittest cases still pass unchanged. Signed-off-by: Ivan Okhotnikov <[email protected]>
Kvaps' review on #3178 called out three imprecisions in the supporting comments. Fix them without touching runtime behaviour: 1. certificate.yaml previously emitted a secretTemplate whose labels include \`app.kubernetes.io/managed-by: Helm\`. Helm does NOT own the Secret written by cert-manager — the Certificate resource does — and stamping that label on it misleads operators reading the object with \`kubectl get secret -oyaml\`. Drop secretTemplate; cert-manager labels its outputs unambiguously (\`cert-manager.io/*\`, \`controller.cert-manager.io/*\`). 2. deployment.yaml comment on checksum/tls-cert claimed the lookup \"resolves at every reconcile\". Not true. \`lookup\` runs only on Helm actions (install / upgrade / rollback); helm-controller does not re-render a deployed release on interval ticks. Under cozystack's release cadence this is fine — any real chart bump, values change, or manual \`flux reconcile hr\` triggers a re-template within days — but at day ~350 renewal the pod continues serving the certificate it loaded at startup until the next Helm action. If no Helm action lands in the ~15 days between renewal and expiry the webhook silently stops mutating. The comment now spells that trade-off out so a future reader can find it, and points at a small post-renewal Job as the follow-up without re-introducing the ReplicaSet-churn amplifier the whole PR retires. 3. values.yaml comment on certManagerEnabled did not warn that the switch is effectively one-way. Once cert-manager has written the Secret with \`type: kubernetes.io/tls\`, flipping the flag back to false and rolling back to the legacy Opaque path fails — \`Secret.type\` is immutable, so the legacy Secret template cannot overwrite the cert-manager-owned one in place. The doc now says operators must \`kubectl delete\` the Secret first if they need to roll back. Also refreshed patches/cert-manager-admission-tls.diff to reflect these comment-only edits and re-verified end-to-end that \`make update\`-style pull + patch produces a byte-identical tree to what git tracks now. 15/15 helm-unittest cases still pass. Signed-off-by: Ivan Okhotnikov <[email protected]>
…ler webhook
The legacy path used genCA/genSignedCert inside a Helm template. Those
functions are not idempotent across renders — every Flux HelmRelease
reconcile produces a fresh CA + cert. The deployment's
checksum/tls-secret podAnnotation is a sha256 of that ever-changing
template, so each reconcile flips the PodTemplate and materialises a
new ReplicaSet. Under load — e.g. the E2E install phase where
cert-manager / CDI / LINSTOR cascade races cause repeated VPA HR
reconciles — the churn produces 10+ admission-controller ReplicaSets
in minutes and blocks the entire cozystack-platform HR because VPA
never converges to Ready.
Replace the source of the TLS Secret with a cert-manager Certificate
plus a namespace-scoped selfSigned Issuer, gated on a new
`admissionController.tls.certManagerEnabled` value that defaults to
`true`. cert-manager only writes the Secret when the certificate is
close to expiry (renewBefore), so reconcile-driven churn stops.
* New charts/vertical-pod-autoscaler/templates/admission-controller/
certificate.yaml renders the Issuer and the Certificate. The
Certificate spec exactly mirrors the legacy genSignedCert shape —
single DNS SAN `vpa-webhook.<ns>.svc`, 365d validity — so the
admission-controller sees the same certificate topology it always
saw. The Secret name resolves via the existing
`admissionController.tls.secretName` helper, which means
cert-manager writes into the same Secret the legacy template used
to own (`<release>-admission-controller-tls`); a separate
pre-upgrade hook (next commit) strips Helm/Flux ownership from
that existing Secret on upgrade so cert-manager can adopt it
without racing Helm's garbage collector.
* The legacy Secret template stays in the chart, wrapped in an
additional `{{- if not .certManagerEnabled }}` guard, so
operators who explicitly opt out keep the old behaviour.
* The deployment's `checksum/tls-secret` podAnnotation is gated
behind the same flag. Under cert-manager it would either be
misleading (Secret content is not driven by that template
anymore) or trigger a pointless rollout on every unrelated chart
change; both worse than not emitting it.
The `existingSecret` path is untouched — operators supplying an
external TLS Secret continue to bypass both cert-manager and the
legacy Secret template.
Verified by `helm template`:
- certManagerEnabled=true (default): Certificate+Issuer rendered,
no legacy Secret, no checksum/tls-secret annotation.
- certManagerEnabled=false: legacy Secret rendered, no Certificate,
checksum/tls-secret annotation restored.
- existingSecret=<name>: neither path renders, deployment mounts
the external Secret.
Signed-off-by: Ivan Okhotnikov <[email protected]>
… from Helm+Flux to cert-manager
Existing clusters carry an admission-controller TLS Secret that was
rendered by the legacy in-chart genCA template. It is owned by both
Helm (via app.kubernetes.io/managed-by=Helm and meta.helm.sh/*
annotations) and Flux HelmController (via helm.toolkit.fluxcd.io/*
labels). On upgrade to a chart version that no longer renders the
Secret directly — the certManagerEnabled=true path — Helm's release
diff shows the Secret as removed from the manifest and garbage-
collects it; Flux prune does the same, independently. If cert-manager
has not yet reconciled its new Certificate at that instant, the
admission-controller pod loses its TLS mount and CrashLoops until
cert-manager catches up.
Add a pre-upgrade Job with hook-weight -10 that strips both Helm and
Flux ownership from the existing Secret before Helm computes the
release diff. cert-manager then adopts the orphaned Secret via the
new Certificate (see previous commit) and rewrites its content in
place — no delete/create window, no dangling mount.
The Job is idempotent:
* No secret → fresh install, no-op.
* Secret already carries controller.cert-manager.io/fao=true → the
migration already ran, no-op.
* Otherwise → strip ownership labels/annotations. The kubectl
label/annotate -- form is itself idempotent.
RBAC is scoped to the single Secret by resourceName; the Job's
ServiceAccount cannot list or modify any other Secret in the namespace.
kubectl image tracks docker.io/clastix/kubectl:v1.32 to match the hook
Job in packages/system/keycloak-configure/templates/delete.yaml — one
shared kubectl image across cozystack hook Jobs.
If the Job fails (RBAC misconfig, apiserver blip), Helm aborts the
upgrade. The release stays on the previous chart version; no partial
migration state.
Signed-off-by: Ivan Okhotnikov <[email protected]>
…oller after cert-manager writes the Secret
The admission-controller reads TLS material once at process startup
(--tls-cert-file / --tls-private-key) — it does not watch the Secret.
The MutatingWebhookConfiguration vpa-webhook-config is (re-)registered
by the pod itself via --register-webhook=true at that same startup,
so its caBundle lives only as long as the pod that wrote it.
Under the new cert-manager path, nothing in the chart triggers a
Deployment rollout when cert-manager rewrites the Secret — the old
checksum/tls-secret podAnnotation is intentionally gone (it was the
ReplicaSet-churn amplifier we removed in the first commit). Left on
its own, an in-place upgrade leaves the cluster in a mixed-generation
state: cert-manager wrote a new CA + cert (issuer changed from the
ephemeral in-chart CA to the selfSigned Issuer, so the adopted Secret
content gets regenerated), but the running admission-controller pod
still holds the old cert in memory and MWC.caBundle still points at
the old CA. That's internally consistent (old pod ↔ old MWC), but
the tenant apiserver is trusting a caBundle that no longer
corresponds to the Secret on disk — the next node reboot, next
preemption, next manual rollout, or the next renewal-driven Secret
rewrite would flip half of the chain at random.
The post-install,post-upgrade Job resolves the transition
deterministically:
1. wait for cert-manager to make the Certificate Ready
(3-minute budget)
2. rollout restart the admission-controller Deployment
3. wait for rollout to complete (3-minute budget)
When the new pod comes up, it reads the new Secret and re-registers
MWC.caBundle with the new CA — old pod, old cert, and old MWC
caBundle all leave the cluster together in a single rolling update.
Downtime analysis:
* replicaCount=1 (chart default), strategy=RollingUpdate default →
≤60s window with zero admission-controller pods.
* MWC failurePolicy is Ignore (verified on cozy-dev3), so during
that window the apiserver's webhook call times out and pod
admission proceeds without VPA mutation. No cluster-wide
request rejection.
RBAC is scoped by resourceName to the single Certificate and the
single Deployment; the Job's ServiceAccount cannot inspect any other
resource in the namespace. Same kubectl image
(docker.io/clastix/kubectl:v1.32) as the tls-migrator hook so
cozystack ships one image across hook Jobs.
Signed-off-by: Ivan Okhotnikov <[email protected]>
…r TLS migration The VPA admission-controller webhook TLS is now provisioned via a cert-manager Certificate + Issuer instead of the in-chart genCA Secret. On a fresh install the VPA package must not reconcile until cert-manager is Ready; otherwise the Certificate resource sits Pending forever, the admission-controller Secret is never written, and the pod CrashLoops on missing TLS mount — cascading up through the cozystack-platform PackageSource in exactly the way the old genCA path did, only for a different reason. Add cozystack.cert-manager to the variant's dependsOn list. Verified non-circular against packages/core/platform/sources/cert-manager.yaml (cert-manager depends on cozystack.networking + cozystack.gateway-api-crds, neither of which reaches VPA). Signed-off-by: Ivan Okhotnikov <[email protected]>
…r migration paths
Locks in the four configurations the chart must support:
1. certManagerEnabled=true (default): Certificate + Issuer render
with the same DNS shape (`vpa-webhook.<ns>.svc`) and validity
(365d) the legacy genSignedCert produced, so cert-manager
adoption of an existing Secret rotates content once and stays
stable. Legacy Secret is suppressed. The checksum/tls-secret
podAnnotation — the ReplicaSet-churn amplifier of the legacy
path — is absent. A future refactor that reintroduces that
annotation on the cert-manager branch would silently regress
to the same infinite rotation loop from a different angle;
this test blocks that.
2. certManagerEnabled=false: opt-out for operators without cert-
manager. Legacy Secret must be back AND the
checksum/tls-secret annotation must be back — either one alone
leaves admission-controller pods failing to mount.
3. existingSecret set: operator supplies TLS out-of-band. Chart
mints nothing (no Certificate, no Issuer, no legacy Secret, no
migration or rollout hook). Preserves the pre-existing chart
contract.
4. admissionController.enabled=false (nested vpa-for-vpa release):
no admission-controller means no TLS Secret is needed. Every
admission-controller template must render zero documents,
otherwise the nested release would apply stray
Certificate/Issuer/Job manifests into cozy-vpa-for-vpa for no
reason.
Also locks the hook wiring for tls-migrator (pre-upgrade, weight -10)
and tls-rollout (post-install,post-upgrade, weight 10) and asserts
the RBAC scoping-by-resourceName so a future edit cannot accidentally
grant broader Secret/Deployment access than the Job requires.
All 22 tests pass locally under helm-unittest.
Signed-off-by: Ivan Okhotnikov <[email protected]>
…real name certificate.yaml renders the Certificate with name <fullname>-tls (its secretName matches the legacy chart-owned Secret exactly), but the rollout hook was passing plain <fullname> to `kubectl wait` — so every post-install/post-upgrade cycle sat 3 minutes on NotFound, failed, and burned through the Job's backoffLimit. cozystack-platform then aborted its HR reconcile because the VPA release's post-upgrade hook was Failed. Snapshot from the first CI run of #3178 confirms: - Certificate object: vertical-pod-autoscaler-admission-controller-tls (namespace cozy-vertical-pod-autoscaler) - Rollout Job pods: 10+ BackOff / BackoffLimitExceeded - HR condition: post-upgrade hooks failed - Admission-controller Deployment itself: 1 ReplicaSet, 1 Ready pod (the actual chart change works — churn is gone; only the rollout trigger was broken) Fix propagates the -tls suffix through both places that reference the Certificate: * The rollout container's `cert` variable used in `kubectl wait`. * The Role's resourceNames on cert-manager.io/certificates so RBAC still admits the get/watch/list against the actual object. Regression test in vpa_test.yaml asserts rules[0].resourceNames[0] carries the -tls suffix so this exact off-by-suffix bug cannot come back through a future rename. Signed-off-by: Ivan Okhotnikov <[email protected]>
…ecksum/tls-cert on Secret content, not via kubectl rollout hook
Retiring the tls-rollout Job. Every Flux HelmRelease reconcile
re-fires the post-install/post-upgrade hooks, and the hook stamped a
fresh `kubectl.kubernetes.io/restartedAt` timestamp into the
Deployment podTemplate at each firing. That mutation spawns a new
ReplicaSet on every reconcile — the exact ReplicaSet-churn pattern
this whole change was meant to end, resurrected from a different
angle. CI run 28584337959 showed 10 admission-controller
ReplicaSets accumulated in ~20 minutes with no Secret content
change; the cert-manager Certificate was Ready and stable, the pod
churn was entirely driven by the hook itself.
Replace it with a declarative `checksum/tls-cert` podAnnotation on
the admission-controller Deployment. The annotation hashes the
runtime Secret's `tls.crt` via `lookup`, so it mutates only when
the Secret content actually mutates:
* fresh install: Secret not yet present at first render;
annotation absent; pod comes up on whichever
Secret cert-manager writes next.
* steady state: Secret content stable → sha stable → no
rotation on any Flux reconcile.
* cert-manager adopt: cert-manager rewrites Secret once on the
first upgrade (issuer change) → sha flips
→ pod restarts ONCE and picks up the new
CA + cert.
* renewal (~350d): cert-manager rotates the Secret → sha flips
→ pod restarts ONCE.
`lookup` returns nil during offline `helm template --dry-run=client`
(CI, unit tests) — the annotation just does not appear in that mode,
which is fine because the pod does not need to rotate in that mode
either. Flux HelmController runs the Helm SDK against the live
apiserver, so `lookup` resolves at every reconcile there.
The tls-migrator pre-upgrade hook is retained. It performs a
one-shot ownership transfer (strip Helm/Flux labels so cert-manager
can adopt the existing Secret without a delete/create race). It
does not stamp any mutable timestamps into the Deployment, so it
does not amplify. It is idempotent (no-op on fresh install and
after the first successful migration).
helm-unittest updated:
* dropped the four assertions that pinned the tls-rollout Job's
hook wiring and RBAC (template no longer exists);
* added an assertion that `checksum/tls-cert` is absent when the
tests run offline (lookup returns nil, exactly as intended).
Signed-off-by: Ivan Okhotnikov <[email protected]>
Kvaps' review on #3178 correctly identified that the pre-upgrade tls-migrator Job cannot do what its comments claim, for two independent reasons: 1. Helm decides what to delete on upgrade from the STORED release manifest, not from live labels on the object. The only live- object opt-out is the \`helm.sh/resource-policy: keep\` annotation — see the \`original.Difference(target)\` loop in helm's pkg/kube/client.go, which only checks \`ResourcePolicyAnno\`. Stripping \`meta.helm.sh/*\` and \`app.kubernetes.io/managed-by\` does not prevent the deletion; those markers only matter for adoption checks on resources entering a manifest, not on resources leaving it. Likewise, helm-controller has no independent label-based prune for Helm-managed resources — the \`helm.toolkit.fluxcd.io/*\` labels are tracking metadata only, not a deletion gate. 2. Even if the Secret survived the Helm/Flux delete pass, cert-manager still could not adopt it in place: the legacy Secret has \`type: Opaque\`, cert-manager writes \`type: kubernetes.io/tls\`, and Secret.type is immutable in Kubernetes. The Certificate reconcile would fail with \"field is immutable\" and never become Ready. The migration converges anyway because the Job is a no-op: Helm deletes the old Opaque Secret, cert-manager mints a fresh one of the right type, and the Deployment rolls in the same upgrade (its podTemplate carries the new \`checksum/tls-cert\` annotation, which is the sha of the runtime Secret and therefore differs the first time cert-manager writes new content). The webhook's \`failurePolicy: Ignore\` makes the seconds-long window benign. Delete the migrator entirely: template file, RBAC (ServiceAccount + Role + RoleBinding), the hardcoded clastix/kubectl image reference, and the four helm-unittest cases pinning its hook wiring / RBAC scope. ~200 lines removed, one less pinned image, and the documented migration flow now matches what actually happens. Existing helm-unittest suite: 15/15 pass (was 19 with the migrator tests; removed 4 that referenced the retired template). Signed-off-by: Ivan Okhotnikov <[email protected]>
…diff on the vendored chart Kvaps' review on #3178 flagged that this package's \`make update\` target does \`rm -rf charts && helm pull cowboysysop/vertical-pod- autoscaler --version 11.1.1\` with no re-apply step. Any local edits to the vendored subchart were therefore one \`make update\` away from vanishing without warning — exactly what the fluxcd-operator package pre-empts via a \`patches/*.diff\` re-apply loop after the helm pull. Adopt the same convention here: * New file \`patches/cert-manager-admission-tls.diff\` capturing all four modifications the cert-manager path makes to the vendored subchart — the new \`certificate.yaml\` template, the checksum/tls-secret + new checksum/tls-cert annotation gating on the admission-controller Deployment, the certManagerEnabled guard on the legacy \`tls-secret.yaml\`, and the new \`certManagerEnabled: true\` default in the subchart values. * Makefile \`update\` target now runs \`patch --no-backup-if-mismatch -p1 < patches/*.diff\` after the helm pull, so a future upstream bump (or a routine \`make update\` today) re-applies the cozystack modifications over the freshly-fetched vendored files and leaves the chart in the same state git tracks. Verified end-to-end: with the on-disk chart intact I ran cp -r charts/vertical-pod-autoscaler /tmp/reference rm -rf charts helm pull cowboysysop/vertical-pod-autoscaler --untar --untardir charts --version 11.1.1 patch --no-backup-if-mismatch -p1 < patches/cert-manager-admission-tls.diff diff -r charts/vertical-pod-autoscaler /tmp/reference The diff is empty — \`make update\` now produces a byte-identical tree to what git has right now, and the 15 helm-unittest cases still pass unchanged. Signed-off-by: Ivan Okhotnikov <[email protected]>
Kvaps' review on #3178 called out three imprecisions in the supporting comments. Fix them without touching runtime behaviour: 1. certificate.yaml previously emitted a secretTemplate whose labels include \`app.kubernetes.io/managed-by: Helm\`. Helm does NOT own the Secret written by cert-manager — the Certificate resource does — and stamping that label on it misleads operators reading the object with \`kubectl get secret -oyaml\`. Drop secretTemplate; cert-manager labels its outputs unambiguously (\`cert-manager.io/*\`, \`controller.cert-manager.io/*\`). 2. deployment.yaml comment on checksum/tls-cert claimed the lookup \"resolves at every reconcile\". Not true. \`lookup\` runs only on Helm actions (install / upgrade / rollback); helm-controller does not re-render a deployed release on interval ticks. Under cozystack's release cadence this is fine — any real chart bump, values change, or manual \`flux reconcile hr\` triggers a re-template within days — but at day ~350 renewal the pod continues serving the certificate it loaded at startup until the next Helm action. If no Helm action lands in the ~15 days between renewal and expiry the webhook silently stops mutating. The comment now spells that trade-off out so a future reader can find it, and points at a small post-renewal Job as the follow-up without re-introducing the ReplicaSet-churn amplifier the whole PR retires. 3. values.yaml comment on certManagerEnabled did not warn that the switch is effectively one-way. Once cert-manager has written the Secret with \`type: kubernetes.io/tls\`, flipping the flag back to false and rolling back to the legacy Opaque path fails — \`Secret.type\` is immutable, so the legacy Secret template cannot overwrite the cert-manager-owned one in place. The doc now says operators must \`kubectl delete\` the Secret first if they need to roll back. Also refreshed patches/cert-manager-admission-tls.diff to reflect these comment-only edits and re-verified end-to-end that \`make update\`-style pull + patch produces a byte-identical tree to what git tracks now. 15/15 helm-unittest cases still pass. Signed-off-by: Ivan Okhotnikov <[email protected]>
944d455 to
5e65a3e
Compare
…tired migrator/rollout hooks Self-review after the tls-migrator removal caught a stale comment in certificate.yaml still promising that a pre-upgrade hook would strip Helm/Flux ownership so cert-manager could adopt the legacy Secret in place. That hook no longer exists and — as kvaps' review pointed out — could not have done what it claimed anyway: \`Secret.type\` is immutable, so cert-manager cannot rewrite an Opaque Secret in place into a \`kubernetes.io/tls\` one. Rewrite the certificate.yaml secretName comment to describe the actual migration path: Helm garbage-collects the legacy Opaque Secret from the release manifest, cert-manager immediately writes a fresh \`kubernetes.io/tls\` Secret of the same name via this Certificate, and the same-upgrade rollout — driven by the fresh \`checksum/tls-cert\` annotation — brings up a pod that re-registers \`vpa-webhook-config\` with the new caBundle before the delete/ create window on the MWC matters (given failurePolicy: Ignore). Also refresh patches/cert-manager-admission-tls.diff to reflect the comment edit and re-verify end-to-end that \`rm -rf charts && helm pull && patch\` produces a byte-identical tree to what git tracks. 15/15 helm-unittest cases still pass. Signed-off-by: Ivan Okhotnikov <[email protected]>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/system/vertical-pod-autoscaler/tests/vpa_test.yaml (1)
162-199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover the existingSecret deployment annotation bypass.
Lines 162-166 state that
existingSecretmeans no chart-managed TLS or rollout hooks; the tests currently assert only Certificate/Issuer and Secret suppression. Add a Deployment assertion so future checksum changes cannot regress external TLS users.Suggested test coverage
- it: existingSecret bypasses the legacy in-chart Secret set: vertical-pod-autoscaler.admissionController.tls.existingSecret: my-external-tls template: charts/vertical-pod-autoscaler/templates/admission-controller/tls-secret.yaml asserts: - hasDocuments: count: 0 + + - it: existingSecret bypasses admission-controller TLS checksum annotations + set: + vertical-pod-autoscaler.admissionController.tls.existingSecret: my-external-tls + vertical-pod-autoscaler.admissionController.tls.certManagerEnabled: true + template: charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml + asserts: + - notExists: + path: spec.template.metadata.annotations["checksum/tls-secret"] + - notExists: + path: spec.template.metadata.annotations["checksum/tls-cert"] # `checksum/tls-cert` uses a runtime `lookup` on the Secret content —🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/system/vertical-pod-autoscaler/tests/vpa_test.yaml` around lines 162 - 199, The existingSecret path is only asserting that Certificate/Issuer and the in-chart Secret are skipped, but it still needs coverage for the Deployment rollout annotation. In vpa_test.yaml, add an assertion against the admission-controller/deployment.yaml template for the existingSecret scenario using the same existingSecret setting, and verify that checksum/tls-cert is not emitted so external TLS users do not get chart-managed rollout hooks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/system/vertical-pod-autoscaler/tests/vpa_test.yaml`:
- Around line 162-199: The existingSecret path is only asserting that
Certificate/Issuer and the in-chart Secret are skipped, but it still needs
coverage for the Deployment rollout annotation. In vpa_test.yaml, add an
assertion against the admission-controller/deployment.yaml template for the
existingSecret scenario using the same existingSecret setting, and verify that
checksum/tls-cert is not emitted so external TLS users do not get chart-managed
rollout hooks.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7442cdbe-f8e4-4a82-b47a-8491f03abd38
📒 Files selected for processing (8)
packages/core/platform/sources/vertical-pod-autoscaler.yamlpackages/system/vertical-pod-autoscaler/Makefilepackages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/certificate.yamlpackages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yamlpackages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/tls-secret.yamlpackages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/values.yamlpackages/system/vertical-pod-autoscaler/patches/cert-manager-admission-tls.diffpackages/system/vertical-pod-autoscaler/tests/vpa_test.yaml
✅ Files skipped from review due to trivial changes (1)
- packages/core/platform/sources/vertical-pod-autoscaler.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/values.yaml
- packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml
Andrei Kvapil (kvaps)
left a comment
There was a problem hiding this comment.
All three blockers and the smaller items are addressed: the chart modifications are now carried as patches/cert-manager-admission-tls.diff applied in the Makefile update target (I verified the patch is byte-consistent with the in-tree chart — blob hashes match), the tls-migrator is gone with the delete/recreate flow honestly documented right in certificate.yaml, and the PR description matches the diff again. The lookup renewal trade-off and the one-way nature of the certManagerEnabled=false escape hatch are both documented. Nice turnaround.
One cosmetic nit, no action needed: the certificate.yaml comment attributes the migration-upgrade rollout to the checksum/tls-cert flip. Within that upgrade the annotation still carries the pre-migration sha (lookup ran before Helm deleted the old Secret) — the rollout actually comes from the podTemplate change itself, and the annotation only catches up on the next Helm action. The described behaviour (fresh pod re-registers the webhook with the new caBundle in the same upgrade) is still correct.
Approving. Please make sure the E2E run on this revision goes green before merging — the previous run predates these changes, and its failure mode (vm-disk-application PackageSource stall) is the one #3182 targets.
…ion in certificate.yaml Kvaps' approval comment flagged that certificate.yaml attributed the migration-upgrade rollout to the checksum/tls-cert value flipping. That is technically inaccurate: at first-upgrade render time, \`lookup\` runs BEFORE Helm deletes the legacy Opaque Secret, so the annotation's value at that moment is the sha of the old Secret content, not the yet-to-be-written cert-manager one. The rollout still happens on the first upgrade — but because the pod-template itself carries a structural change: the pre-migration chart did not emit \`checksum/tls-cert\` at all, and the new chart does. That new-annotation-present is what Helm sees as a podTemplate diff and rolls the ReplicaSet. The annotation's VALUE only catches up on the NEXT Helm action (when lookup re-fires and sees the cert-manager-written Secret). That is fine on first upgrade because the structural pod-template change already turns the ReplicaSet over, and the fresh pod re- registers the MutatingWebhookConfiguration with the new caBundle after cert-manager has written the new Secret. Subsequent renewal- driven rotations (or manual \`flux reconcile hr\`) are driven by the annotation VALUE flip, as before. Comment rewritten to lay this out explicitly. Patch file regenerated to include the comment change. 15/15 helm-unittest cases still pass. Signed-off-by: Ivan Okhotnikov <[email protected]>
myasnikovdaniil
left a comment
There was a problem hiding this comment.
LGTM — cert-manager-owned TLS correctly removes the reconcile-driven ReplicaSet-churn livelock; the three TLS paths are cleanly gated, tests pass, and the make update patch convention prevents a silent revert. Non-blocking follow-ups below; the failing E2E is unrelated to this change (evidence follows).
Business context: the VPA admission-controller webhook TLS is minted in-chart via non-idempotent genCA/genSignedCert, so every Flux reconcile regenerates the cert, flips the checksum/tls-secret podAnnotation, and spawns a new ReplicaSet — under first-install load this churns 10+ ReplicaSets and livelocks VPA, timing out cozystack-platform. This PR moves TLS provisioning to a cert-manager Issuer + Certificate so the Secret is stable across reconciles.
Verified
- Root cause (genCA-per-render →
checksum/tls-secretflip → ReplicaSet churn) is accurate. - The three TLS modes — cert-manager (default), legacy
genCA(certManagerEnabled=false), andexistingSecret— are mutually exclusive and correctly gated; 15/15 helm-unittest cases pass. - cert-manager writes the same Secret name the legacy path used (
…-admission-controller-tls, via the shared helper), so the deployment volume mount is unchanged. - The Certificate mirrors the legacy cert shape (CN + single SAN
vpa-webhook.<ns>.svc, 365d validity). dependsOn: cozystack.cert-manageris non-circular (cert-manager → networking + gateway-api-crds; neither reaches VPA).patches/cert-manager-admission-tls.diffis byte-consistent with the vendored tree, somake updatere-applies the fix rather than reverting it.
The failing E2E is not caused by this PR
In the latest E2E run the failure is vmclusters/longterm → progress deadline exceeded for Deployment=tenant-root/vminsert-longterm (VictoriaMetrics vminsert CrashLoopBackOff). In that same snapshot all VPA HelmReleases are Ready:
cozy-vertical-pod-autoscaler vertical-pod-autoscaler True
cozy-vertical-pod-autoscaler vertical-pod-autoscaler-crds True
cozy-vpa-for-vpa vpa-for-vpa True
The install progressed past the platform phase into tenant configuration before failing on an unrelated VM-stack component — direct evidence the churn livelock is gone. (The red Require API owner review entry is a cancelled duplicate workflow run; a passing instance exists.)
Non-blocking follow-ups
- Confirm a clean E2E on this revision and complete the live upgrade-migration check before merge. Both are unchecked in the test plan and this is
priority/critical-urgent, touching every existing cluster's upgrade path. The current redness is unrelated (above), but the Opaque →kubernetes.io/tlsSecret delete/recreate plus single pod roll is reasoned, not yet observed on a live upgrade. - Renewal safety window — see inline note on
certificate.yaml. checksum/tls-certscope — see inline note ondeployment.yaml.- Optional: add an
existingSecretassertion that the deployment emits neither checksum annotation (templates are already correctly gated; belt-and-suspenders).
Direction and implementation are sound and already carry a maintainer approval; the above are pre-merge verification and hardening notes, not blockers.
| dnsNames: | ||
| - {{ printf "vpa-webhook.%s.svc" (include "common.names.namespace" .) }} | ||
| duration: 8760h | ||
| renewBefore: 360h |
There was a problem hiding this comment.
duration: 8760h / renewBefore: 360h leaves only a 15-day window in which a Helm action must fire to roll the pod after cert-manager rotates the Secret. If none fires in that window, the pod keeps serving the expired in-memory cert and — with the webhook's failurePolicy: Ignore — VPA silently stops mutating until the next Helm action or pod restart. The trade-off is honestly documented; consider widening renewBefore (e.g. ~2160h / 90d) so the safety window is months rather than 15 days, independent of release cadence.
| {{- if and .Values.admissionController.tls.certManagerEnabled (not .Values.admissionController.tls.existingSecret) }} | ||
| {{- $secret := lookup "v1" "Secret" (include "common.names.namespace" .) (include "vertical-pod-autoscaler.admissionController.tls.secretName" .) }} | ||
| {{- if and $secret $secret.data (index $secret.data "tls.crt") }} | ||
| checksum/tls-cert: {{ index $secret.data "tls.crt" | sha256sum }} |
There was a problem hiding this comment.
Hashing only tls.crt is sufficient here because the Issuer is selfSigned — the leaf is its own CA, so ca.crt == tls.crt, and any rotation always re-issues tls.crt regardless of key-rotation policy. Worth a one-line note that this assumption holds only while the Issuer stays selfSigned; if it ever becomes CA-backed, the CA could rotate independently and the annotation should then also cover ca.crt.
… checksum, add existingSecret belt-and-suspenders assertion Address the three non-blocking follow-ups on myasnikovdaniil's approval: * renewBefore: 360h → 2160h (15d → 90d). With webhook `failurePolicy: Ignore`, an expired in-memory cert silently drops admission traffic until a Helm action rolls the ReplicaSet. helm-controller does not re-render on reconcile ticks after the release converges, only on install/upgrade/rollback — a 15-day window ties admission-webhook liveness to release cadence. 90 days survives realistic release schedules on customer clusters. Rotate-and-restart requirement is unchanged; the window is widened. * Document on deployment.yaml why hashing only `tls.crt` is safe: the Issuer is `selfSigned`, so `ca.crt == tls.crt` and every rotation re-issues `tls.crt`. If a future refactor ever swaps the Issuer to a CA-backed one (external CA, ACME, Vault), the checksum MUST be extended to also cover `ca.crt` — otherwise a CA-only rotation would leave the self-registered MutatingWebhookConfiguration serving a stale caBundle until the next leaf rotation. * Add a belt-and-suspenders helm-unittest assertion pinning that under `existingSecret` the admission-controller pod-template carries NEITHER `checksum/tls-secret` (legacy amplifier) NOR `checksum/tls-cert` (cert-manager runtime lookup). Templates are already structurally gated; this catches a future refactor that accidentally moves either checksum outside its `if` gate. Regenerated patches/cert-manager-admission-tls.diff against upstream via `make update` flow so the vendored chart edits stay in sync with the patch file (avoids silent revert on the next update). Verified: 16/16 helm-unittest cases pass, `helm lint` clean, `helm template ... certManagerEnabled=true` renders `renewBefore: 2160h`, and re-applying the regenerated patch onto a fresh upstream pull produces a byte-identical chart tree. Signed-off-by: Ivan Okhotnikov <[email protected]>
…ve with widened renewBefore
Self-review caught a stale day-math block in deployment.yaml that still
described the previous `renewBefore: 360h` behaviour ("cert-manager
renews at day ~350 and NO Helm action happens in the following ~15
days"). After bumping `renewBefore` to 2160h in the previous commit,
rotation now lands at day ~275 with a ~90-day window before expiry.
Update the numbers in place and regenerate the vendored-chart patch so
`make update` re-applies the corrected text instead of silently
reverting to the stale description on the next chart refresh.
No functional change — cert rotation math already lives in
`certificate.yaml`; this only fixes the parallel rationale block a
future reader would read alongside it.
Signed-off-by: Ivan Okhotnikov <[email protected]>
…scii timeline Second self-review pass caught a remaining reference to the old rotation math in the checksum/tls-cert doc block's ascii timeline (`renewal (~350d)` — from `renewBefore: 360h` when 365 - 15 = 350). With the widened `renewBefore: 2160h` rotation now lands at day ~275, matching the bullet-list narrative directly below. Update the number in place and regenerate the vendored-chart patch. No functional change; one-line doc fix. Signed-off-by: Ivan Okhotnikov <[email protected]>
…tus-patch stalls Symptom cozystack-platform HelmRelease times out at 15m on fresh install with a single PackageSource stuck in InProgress. Snapshot from #3178 CI run 28587656430: PackageSource cozystack.vm-disk-application Ready: Unknown, reason: Progressing lastTransitionTime: 2026-07-02T13:39:26Z (snapshot generated 30+ minutes later — no state change) ArtifactGenerator cozystack.vm-disk-application status.inventory: 2 items with sha256 digests status.observedSourcesDigest: populated status.conditions[Ready]: Unknown, reason: Progressing status.conditions[Reconciling]: True, reason: Progressing ExternalArtifacts are produced. Downstream Flux consumes them and its HelmReleases install fine. But PackageSourceReconciler faithfully copies the Unknown Ready condition through, so cozystack-platform (which waits on PackageSource.Ready=True) never converges. Root cause Traced in fluxcd/source-watcher:v2.1.0 log for the affected ArtifactGenerator: 13:39:28.475 ExternalArtifact vm-disk reconciled ✅ 13:39:28.641 ExternalArtifact vm-disk-rd reconciled ✅ 13:39:45.719 ❌ failed to update status: etcdserver: request timed out 13:39:45.720 ❌ Reconciler error (etcd timeout) 13:39:46.485 ✅ No drift detected, 2 artifact(s) up to date ——— nothing else for this ArtifactGenerator ——— Under first-install etcd pressure the patchStatus call times out. The reconciler returns the etcd error; controller-runtime prints the "returned both a result and non-nil error, RequeueAfter ignored" warning and requeues. The next reconcile hits the "No drift detected" short-circuit (artifacts are already up to date) and early-exits before touching status. Result: the Ready condition is stuck Unknown until either the process restarts or something forces a state change on the ArtifactGenerator. Neither happens in a real install run. This is an upstream bug in source-watcher's reconcile loop. It has been papered over once already at the runtime-tuning level in #3163 (source-watcher concurrency + CPU), but a bad etcd second during install still lands us here. Cozystack cannot afford to wait for an upstream release cycle to unblock every install. Fix PackageSourceReconciler.updateStatus now checks two invariants before falling back to copying the (potentially stuck) upstream Ready condition: 1. ArtifactGenerator.status.Inventory is non-empty 2. ArtifactGenerator.status.ObservedSourcesDigest is set Both fields are only written by source-watcher after the artifacts have actually been produced — status.Inventory carries the ExternalArtifact digests downstream Flux is already consuming. If BOTH are present AND the upstream Ready condition is still Unknown (or absent), the PackageSource Ready condition is synthesised as True with a distinctive reason `ArtifactsGeneratedAwaitingUpstreamStatus` and a message that names the upstream bug and the observable state it relied on. If the upstream Ready ever catches up (True or False) the Owns() watch re-fires the reconciler and the real condition overwrites the synthetic one — True stays True, False surfaces the real failure. Failures are NEVER masked: the synthesis path only triggers when Ready is Unknown or missing. Safety * Never synthesises True over a real False → upstream failure is surfaced unchanged. * Requires BOTH Inventory non-empty AND ObservedSourcesDigest set → a fresh ArtifactGenerator that has literally not started work cannot be prematurely marked Ready. * ObservedGeneration is stamped so a spec change re-invalidates the synthesis until the next observable-artifacts pass. * Reason string is greppable so operators can audit which PackageSources came up via the workaround. Tests Added TestArtifactGeneratorObservablyReady with 7 cases covering every branch of the predicate: - fresh generator: false - inventory only: false - digest only: false - both set + Unknown Ready: true (the actual bug) - both set + no Ready cond: true (same window pre-stamp) - both set + Ready=True: false (pass real condition) - both set + Ready=False: false (never mask failure) All pass; existing operator unit tests are unaffected. Signed-off-by: Ivan Okhotnikov <[email protected]>
…tus-patch stalls Symptom cozystack-platform HelmRelease times out at 15m on fresh install with a single PackageSource stuck in InProgress. Snapshot from #3178 CI run 28587656430: PackageSource cozystack.vm-disk-application Ready: Unknown, reason: Progressing lastTransitionTime: 2026-07-02T13:39:26Z (snapshot generated 30+ minutes later — no state change) ArtifactGenerator cozystack.vm-disk-application status.inventory: 2 items with sha256 digests status.observedSourcesDigest: populated status.conditions[Ready]: Unknown, reason: Progressing status.conditions[Reconciling]: True, reason: Progressing ExternalArtifacts are produced. Downstream Flux consumes them and its HelmReleases install fine. But PackageSourceReconciler faithfully copies the Unknown Ready condition through, so cozystack-platform (which waits on PackageSource.Ready=True) never converges. Root cause Traced in fluxcd/source-watcher:v2.1.0 log for the affected ArtifactGenerator: 13:39:28.475 ExternalArtifact vm-disk reconciled ✅ 13:39:28.641 ExternalArtifact vm-disk-rd reconciled ✅ 13:39:45.719 ❌ failed to update status: etcdserver: request timed out 13:39:45.720 ❌ Reconciler error (etcd timeout) 13:39:46.485 ✅ No drift detected, 2 artifact(s) up to date ——— nothing else for this ArtifactGenerator ——— Under first-install etcd pressure the patchStatus call times out. The reconciler returns the etcd error; controller-runtime prints the "returned both a result and non-nil error, RequeueAfter ignored" warning and requeues. The next reconcile hits the "No drift detected" short-circuit (artifacts are already up to date) and early-exits before touching status. Result: the Ready condition is stuck Unknown until either the process restarts or something forces a state change on the ArtifactGenerator. Neither happens in a real install run. This is an upstream bug in source-watcher's reconcile loop. It has been papered over once already at the runtime-tuning level in #3163 (source-watcher concurrency + CPU), but a bad etcd second during install still lands us here. Cozystack cannot afford to wait for an upstream release cycle to unblock every install. Fix PackageSourceReconciler.updateStatus now checks two invariants before falling back to copying the (potentially stuck) upstream Ready condition: 1. ArtifactGenerator.status.Inventory is non-empty 2. ArtifactGenerator.status.ObservedSourcesDigest is set Both fields are only written by source-watcher after the artifacts have actually been produced — status.Inventory carries the ExternalArtifact digests downstream Flux is already consuming. If BOTH are present AND the upstream Ready condition is still Unknown (or absent), the PackageSource Ready condition is synthesised as True with a distinctive reason `ArtifactsGeneratedAwaitingUpstreamStatus` and a message that names the upstream bug and the observable state it relied on. If the upstream Ready ever catches up (True or False) the Owns() watch re-fires the reconciler and the real condition overwrites the synthetic one — True stays True, False surfaces the real failure. Failures are NEVER masked: the synthesis path only triggers when Ready is Unknown or missing. Safety * Never synthesises True over a real False → upstream failure is surfaced unchanged. * Requires BOTH Inventory non-empty AND ObservedSourcesDigest set → a fresh ArtifactGenerator that has literally not started work cannot be prematurely marked Ready. * ObservedGeneration is stamped so a spec change re-invalidates the synthesis until the next observable-artifacts pass. * Reason string is greppable so operators can audit which PackageSources came up via the workaround. Tests Added TestArtifactGeneratorObservablyReady with 7 cases covering every branch of the predicate: - fresh generator: false - inventory only: false - digest only: false - both set + Unknown Ready: true (the actual bug) - both set + no Ready cond: true (same window pre-stamp) - both set + Ready=True: false (pass real condition) - both set + Ready=False: false (never mask failure) All pass; existing operator unit tests are unaffected. Signed-off-by: Ivan Okhotnikov <[email protected]>
…tus-patch stalls Symptom cozystack-platform HelmRelease times out at 15m on fresh install with a single PackageSource stuck in InProgress. Snapshot from #3178 CI run 28587656430: PackageSource cozystack.vm-disk-application Ready: Unknown, reason: Progressing lastTransitionTime: 2026-07-02T13:39:26Z (snapshot generated 30+ minutes later — no state change) ArtifactGenerator cozystack.vm-disk-application status.inventory: 2 items with sha256 digests status.observedSourcesDigest: populated status.conditions[Ready]: Unknown, reason: Progressing status.conditions[Reconciling]: True, reason: Progressing ExternalArtifacts are produced. Downstream Flux consumes them and its HelmReleases install fine. But PackageSourceReconciler faithfully copies the Unknown Ready condition through, so cozystack-platform (which waits on PackageSource.Ready=True) never converges. Root cause Traced in fluxcd/source-watcher:v2.1.0 log for the affected ArtifactGenerator: 13:39:28.475 ExternalArtifact vm-disk reconciled ✅ 13:39:28.641 ExternalArtifact vm-disk-rd reconciled ✅ 13:39:45.719 ❌ failed to update status: etcdserver: request timed out 13:39:45.720 ❌ Reconciler error (etcd timeout) 13:39:46.485 ✅ No drift detected, 2 artifact(s) up to date ——— nothing else for this ArtifactGenerator ——— Under first-install etcd pressure the patchStatus call times out. The reconciler returns the etcd error; controller-runtime prints the "returned both a result and non-nil error, RequeueAfter ignored" warning and requeues. The next reconcile hits the "No drift detected" short-circuit (artifacts are already up to date) and early-exits before touching status. Result: the Ready condition is stuck Unknown until either the process restarts or something forces a state change on the ArtifactGenerator. Neither happens in a real install run. This is an upstream bug in source-watcher's reconcile loop. It has been papered over once already at the runtime-tuning level in #3163 (source-watcher concurrency + CPU), but a bad etcd second during install still lands us here. Cozystack cannot afford to wait for an upstream release cycle to unblock every install. Fix PackageSourceReconciler.updateStatus now checks two invariants before falling back to copying the (potentially stuck) upstream Ready condition: 1. ArtifactGenerator.status.Inventory is non-empty 2. ArtifactGenerator.status.ObservedSourcesDigest is set Both fields are only written by source-watcher after the artifacts have actually been produced — status.Inventory carries the ExternalArtifact digests downstream Flux is already consuming. If BOTH are present AND the upstream Ready condition is still Unknown (or absent), the PackageSource Ready condition is synthesised as True with a distinctive reason `ArtifactsGeneratedAwaitingUpstreamStatus` and a message that names the upstream bug and the observable state it relied on. If the upstream Ready ever catches up (True or False) the Owns() watch re-fires the reconciler and the real condition overwrites the synthetic one — True stays True, False surfaces the real failure. Failures are NEVER masked: the synthesis path only triggers when Ready is Unknown or missing. Safety * Never synthesises True over a real False → upstream failure is surfaced unchanged. * Requires BOTH Inventory non-empty AND ObservedSourcesDigest set → a fresh ArtifactGenerator that has literally not started work cannot be prematurely marked Ready. * ObservedGeneration is stamped so a spec change re-invalidates the synthesis until the next observable-artifacts pass. * Reason string is greppable so operators can audit which PackageSources came up via the workaround. Tests Added TestArtifactGeneratorObservablyReady with 7 cases covering every branch of the predicate: - fresh generator: false - inventory only: false - digest only: false - both set + Unknown Ready: true (the actual bug) - both set + no Ready cond: true (same window pre-stamp) - both set + Ready=True: false (pass real condition) - both set + Ready=False: false (never mask failure) All pass; existing operator unit tests are unaffected. Signed-off-by: Ivan Okhotnikov <[email protected]>
Summary
The VPA admission-controller webhook TLS Secret is currently rendered by
an in-chart
genCA+genSignedCerttemplate. Those functions are notidempotent across
helm templateinvocations — every Flux HelmReleasereconcile produces a fresh CA + cert. A
checksum/tls-secretpodAnnotation on the admission-controller Deployment then flips its
sha, materialising a new ReplicaSet on every reconcile.
Under load — the E2E install phase where cert-manager / CDI / LINSTOR
cascade races repeatedly refire the VPA HelmRelease — this produces
10+ admission-controller ReplicaSets in minutes and blocks the whole
cozystack-platform HR: the admission-controller pod livelocks in
startup (waits on its own election lease which its previous
ReplicaSet is still holding, hits livenessProbe timeout,
CrashLoopBackOffs), VPA never becomes Ready, PackageSources stay
InProgress, and cozystack-platform aborts at its 15-minute install
timeout.
Fix: replace the source of the TLS Secret with a namespace-scoped
cert-manager
Issuer+Certificate. cert-manager only rewrites theSecret when the certificate is close to expiry (
renewBefore), soreconcile-driven cert churn stops. Pod rotation is delegated to a
runtime-lookup-based
checksum/tls-certpodAnnotation that onlychanges when the Secret's
tls.crtcontent actually changes — not onevery render, not on every reconcile.
What
charts/vertical-pod-autoscaler/templates/admission-controller/certificate.yaml(new): namespace-scoped selfSigned Issuer + Certificate. The
Certificate mirrors the legacy
genSignedCertshape (single DNS SANvpa-webhook.<ns>.svc, 365d validity, matching Secret name), socert-manager on the first upgrade adopts the same Secret name the
legacy path wrote — no rename cascade in the Deployment volume mount.
Rendered only when
admissionController.tls.certManagerEnabled=trueAND no
existingSecretis supplied.charts/vertical-pod-autoscaler/templates/admission-controller/tls-secret.yaml(modified): wrapped in an extra
certManagerEnabled=falseguard sothe legacy genCA path remains available as an explicit opt-out.
charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml(modified): the legacy
checksum/tls-secretannotation is gatedbehind
certManagerEnabled=false. Under the cert-manager path theDeployment carries a
checksum/tls-certannotation computed fromlookup-ing the runtime Secret'stls.crt— see the templatecomment for the full timing analysis and the day-350 renewal
trade-off it documents.
charts/vertical-pod-autoscaler/values.yaml(modified): newadmissionController.tls.certManagerEnabledvalue defaulting totrue. Values-block header documents the switch as effectivelyone-way (cert-manager writes
type: kubernetes.io/tls;Secret.typeis immutable, so falling back requireskubectl deletefirst).patches/cert-manager-admission-tls.diff(new) +Makefile updateextended: the four subchart edits above live as asingle reviewable diff that Makefile re-applies over the freshly
vendored chart on every
make update, matching the fluxcd-operatorpackage convention. Prevents a bump from silently reverting the
fix.
packages/core/platform/sources/vertical-pod-autoscaler.yaml(modified): VPA PackageSource now depends on
cozystack.cert-manager.Non-circular: cert-manager depends on
cozystack.networking+cozystack.gateway-api-crds, neither reaches VPA.tests/vpa_test.yaml(modified): +11 helm-unittest cases (15 total,4 pre-existing) covering
certManagerEnabled={true,false},existingSecretbypass, disabled admission-controller, andthe offline-render behaviour of the
checksum/tls-certlookup.Why (evidence)
Snapshot from CI run 28587656430 (this branch, pre-workaround-review):
Migration for existing clusters
On upgrade the legacy Opaque Secret disappears from the release
manifest, Helm garbage-collects it, and cert-manager immediately
writes a fresh
kubernetes.io/tlsSecret of the same name via thenew Certificate.
Secret.typeis immutable, so in-place adoption isimpossible — the delete/create pass is the correct path, and the
seconds-long window it produces is safe: the
vpa-webhook-configMutatingWebhookConfiguration ships with
failurePolicy: Ignore(verified on cozy-dev3), so Pod admissions in that window skip VPA
mutation but never get rejected — no cluster-wide disruption.
The Deployment rolls in the same upgrade because the new
checksum/tls-certannotation differs from anything the pod hadbefore, so the admission-controller re-registers
vpa-webhook-configwith the fresh caBundle on the same pass.
Test plan
helm unittest packages/system/vertical-pod-autoscaler— 15/15pass.
helm templatediff between main and this branch is scopedexactly to the cert-manager path (new Certificate + Issuer,
new
checksum/tls-certannotation, guarded legacy Secret, newvalues key, patch file + Makefile step, PackageSource
dependsOn). No unrelated lines change.
make update-style simulation (rm -rf charts && helm pull &&patch) produces a byte-identical subchart to what git tracks —
the patch stays clean across future upstream bumps until it
genuinely conflicts.
helm upgradeVPA release frompre-fix version → observe cert-manager write the new tls Secret
→ verify
checksum/tls-certon the new pod's podTemplate →verify MutatingWebhookConfiguration
vpa-webhook-configcaBundle updated in place → confirm no ReplicaSet churn on
subsequent Flux reconciles. Blocking the draft's removal.
ReplicaSet after install, VPA HR reaches Ready inside
cozystack-platform's 15m install timeout, andkubernetes-latest.bats/kubernetes-previous.batsno longertime out on VPA-attributable resource churn.
Review pass — kvaps' CHANGES_REQUESTED addressed
patches/*.diffre-applied byMakefile update(matches fluxcd-operator convention).a no-op: Helm's delete decision is driven by the stored release
manifest not by live labels (
ResourcePolicyAnnois the only opt-out), and
Secret.typeis immutable so cert-manager could not haveadopted the legacy Opaque Secret in place anyway. ~200 lines
removed.
reference to the removed
tls-rollout.yamlpost-upgrade hook, realtest count, dev3 verification honestly listed as pending).
checksum/tls-certtiming comment corrected:lookupruns onHelm actions only, day-350 renewal trade-off spelled out.
certManagerEnabled=falsedocumented as effectively one-way(Secret.type immutable).
secretTemplatelabels —app.kubernetes.io/managed- by: Helmon a cert-manager-owned Secret was misleading.vpa_test.yaml.Summary by CodeRabbit