Skip to content

fix(vertical-pod-autoscaler): migrate admission-controller TLS to cert-manager to stop reconcile-driven ReplicaSet churn - #3178

Merged
IvanHunters merged 15 commits into
mainfrom
fix/vpa-admission-cert-manager
Jul 4, 2026
Merged

IvanHunters merged 15 commits into
mainfrom
fix/vpa-admission-cert-manager

Conversation

@IvanHunters

@IvanHunters IvanHunters commented Jul 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The VPA admission-controller webhook TLS Secret is currently rendered by
an in-chart genCA + genSignedCert template. Those functions are not
idempotent across helm template invocations — every Flux HelmRelease
reconcile produces a fresh CA + cert. A checksum/tls-secret
podAnnotation 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 the
Secret when the certificate is close to expiry (renewBefore), so
reconcile-driven cert churn stops. Pod rotation is delegated to a
runtime-lookup-based checksum/tls-cert podAnnotation that only
changes when the Secret's tls.crt content actually changes — not on
every 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 genSignedCert shape (single DNS SAN
    vpa-webhook.<ns>.svc, 365d validity, matching Secret name), so
    cert-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=true
    AND no existingSecret is supplied.
  • charts/vertical-pod-autoscaler/templates/admission-controller/tls-secret.yaml
    (modified): wrapped in an extra certManagerEnabled=false guard so
    the legacy genCA path remains available as an explicit opt-out.
  • charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml
    (modified): the legacy checksum/tls-secret annotation is gated
    behind certManagerEnabled=false. Under the cert-manager path the
    Deployment carries a checksum/tls-cert annotation computed from
    lookup-ing the runtime Secret's tls.crt — see the template
    comment for the full timing analysis and the day-350 renewal
    trade-off it documents.
  • charts/vertical-pod-autoscaler/values.yaml (modified): new
    admissionController.tls.certManagerEnabled value defaulting to
    true. Values-block header documents the switch as effectively
    one-way (cert-manager writes type: kubernetes.io/tls;
    Secret.type is immutable, so falling back requires kubectl delete first).
  • patches/cert-manager-admission-tls.diff (new) +
    Makefile update extended: the four subchart edits above live as a
    single reviewable diff that Makefile re-applies over the freshly
    vendored chart on every make update, matching the fluxcd-operator
    package 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},
    existingSecret bypass, disabled admission-controller, and
    the offline-render behaviour of the checksum/tls-cert lookup.

Why (evidence)

Snapshot from CI run 28587656430 (this branch, pre-workaround-review):

admission-controller ReplicaSets after first-install churn window:
  vertical-pod-autoscaler-admission-controller-55d888d6d4
  vertical-pod-autoscaler-admission-controller-56f8d4c747
  vertical-pod-autoscaler-admission-controller-5775dbc9b7
  vertical-pod-autoscaler-admission-controller-585b7c56c9
  vertical-pod-autoscaler-admission-controller-6fc44944fb
  vertical-pod-autoscaler-admission-controller-74bccf4849
  vertical-pod-autoscaler-admission-controller-84df98f98b
  vertical-pod-autoscaler-admission-controller-8666dffb85
  vertical-pod-autoscaler-admission-controller-86b469457c
  vertical-pod-autoscaler-admission-controller-9b979f9d6
  → 10 ReplicaSets, one per Flux reconcile, each with a different
    genCA-rooted TLS Secret sha in checksum/tls-secret.

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/tls Secret of the same name via the
new Certificate. Secret.type is immutable, so in-place adoption is
impossible — the delete/create pass is the correct path, and the
seconds-long window it produces is safe: the vpa-webhook-config
MutatingWebhookConfiguration 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-cert annotation differs from anything the pod had
before, so the admission-controller re-registers vpa-webhook-config
with the fresh caBundle on the same pass.

Test plan

  • helm unittest packages/system/vertical-pod-autoscaler — 15/15
    pass.
  • helm template diff between main and this branch is scoped
    exactly to the cert-manager path (new Certificate + Issuer,
    new checksum/tls-cert annotation, guarded legacy Secret, new
    values 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.
  • Live migration on cozy-dev3: helm upgrade VPA release from
    pre-fix version → observe cert-manager write the new tls Secret
    → verify checksum/tls-cert on the new pod's podTemplate →
    verify MutatingWebhookConfiguration vpa-webhook-config
    caBundle updated in place → confirm no ReplicaSet churn on
    subsequent Flux reconciles. Blocking the draft's removal.
  • Full E2E on this branch — verify only 1 admission-controller
    ReplicaSet after install, VPA HR reaches Ready inside
    cozystack-platform's 15m install timeout, and
    kubernetes-latest.bats / kubernetes-previous.bats no longer
    time out on VPA-attributable resource churn.

Review pass — kvaps' CHANGES_REQUESTED addressed

  • ✅ Vendored chart mods now carried as patches/*.diff re-applied by
    Makefile update (matches fluxcd-operator convention).
  • ✅ Retired the tls-migrator hook — kvaps correctly showed it was
    a no-op: Helm's delete decision is driven by the stored release
    manifest not by live labels (ResourcePolicyAnno is the only opt-
    out), and Secret.type is immutable so cert-manager could not have
    adopted the legacy Opaque Secret in place anyway. ~200 lines
    removed.
  • ✅ PR description rewritten to match the actual diff (no more
    reference to the removed tls-rollout.yaml post-upgrade hook, real
    test count, dev3 verification honestly listed as pending).
  • ✅ checksum/tls-cert timing comment corrected: lookup runs on
    Helm actions only, day-350 renewal trade-off spelled out.
  • ✅ certManagerEnabled=false documented as effectively one-way
    (Secret.type immutable).
  • ✅ Dropped the secretTemplate labels — app.kubernetes.io/managed- by: Helm on a cert-manager-owned Secret was misleading.
  • ✅ Banner comment indentation restored in vpa_test.yaml.

Summary by CodeRabbit

  • New Features
    • VPA admission-controller webhook TLS now uses cert-manager by default, with options to disable cert-manager or supply an existing TLS secret.
    • Webhook pod restarts are now tied to cert-manager Secret contents via a content-based checksum.
  • Bug Fixes
    • Improved webhook TLS provisioning reliability when TLS readiness is delayed.
    • Corrected conditional rendering/rollout behavior to avoid mismatched TLS ownership.
  • Tests
    • Added rendering-invariant tests covering cert-manager, legacy TLS, existing-secret bypass, and admission-controller disabled behavior.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bd9c1920-3b2c-4457-9732-b1d933ba382e

📥 Commits

Reviewing files that changed from the base of the PR and between 79ce69a and 2ac269a.

📒 Files selected for processing (2)
  • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml
  • packages/system/vertical-pod-autoscaler/patches/cert-manager-admission-tls.diff
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml
  • packages/system/vertical-pod-autoscaler/patches/cert-manager-admission-tls.diff

📝 Walkthrough

Walkthrough

This 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.

Changes

VPA admission-controller cert-manager TLS

Layer / File(s) Summary
Platform dependency and values flag
packages/core/platform/sources/vertical-pod-autoscaler.yaml, packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/values.yaml
Adds cozystack.cert-manager to the default variant dependsOn list and adds admissionController.tls.certManagerEnabled with default true.
Cert-manager Issuer and Certificate templates
packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/certificate.yaml, packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/tls-secret.yaml
Adds a conditional self-signed Issuer and Certificate for the webhook TLS Secret name, and fully gates the legacy in-chart Secret generation behind certManagerEnabled=false.
Deployment annotation updates for TLS rotation
packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml
Restricts checksum/tls-secret to the non-cert-manager path and adds checksum/tls-cert based on a live Secret lookup of tls.crt.
Upstream patch and Makefile wiring
packages/system/vertical-pod-autoscaler/Makefile, packages/system/vertical-pod-autoscaler/patches/cert-manager-admission-tls.diff
Applies the upstream chart patch after download, carrying the cert-manager template, checksum, legacy Secret gating, and values changes.
Chart rendering test coverage
packages/system/vertical-pod-autoscaler/tests/vpa_test.yaml
Adds render tests for cert-manager on/off, existingSecret bypass, offline lookup behavior, and disabled admission-controller cases.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • cozystack/cozystack#2726: Also adds cozystack.cert-manager to platform dependsOn entries for chart consumers that need cert-manager-issued webhook certificates.

Suggested labels: area/kubernetes

Suggested reviewers: kvaps, lllamnyp, androndo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: migrating admission-controller TLS to cert-manager to prevent ReplicaSet churn.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/vpa-admission-cert-manager

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files labels Jul 2, 2026
IvanHunters added a commit that referenced this pull request Jul 2, 2026
…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]>
IvanHunters added a commit that referenced this pull request Jul 2, 2026
…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]>
@IvanHunters
IvanHunters force-pushed the fix/vpa-admission-cert-manager branch from a0418c7 to 1a80527 Compare July 2, 2026 11:48
@IvanHunters
IvanHunters marked this pull request as ready for review July 2, 2026 14:48
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • TLS Management Migration: Replaced the non-idempotent in-chart TLS generation with cert-manager Issuer and Certificate resources to prevent unnecessary ReplicaSet churn.
  • Upgrade Safety: Introduced pre-upgrade and post-upgrade hooks to strip existing Helm/Flux ownership from the TLS Secret and perform a controlled rollout of the admission-controller.
  • Dependency Updates: Added a mandatory dependency on cozystack.cert-manager to the VPA PackageSource to ensure TLS provisioning readiness.
  • Test Coverage: Added 18 new helm-unittest cases to validate cert-manager integration, legacy escape hatches, and RBAC scoping.
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
  • Ignored by pattern: **/charts/** (5)
    • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/certificate.yaml
    • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml
    • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/tls-migrator.yaml
    • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/tls-secret.yaml
    • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/values.yaml
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@dosubot dosubot Bot added area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) priority/critical-urgent Highest priority. Must be actively worked on as someone's top priority right now labels Jul 2, 2026

@gemini-code-assist gemini-code-assist Bot 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.

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.

@coderabbitai coderabbitai Bot 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.

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 win

Hash all mounted TLS material, not only tls.crt.

The pod reads ca.crt, tls.crt, and tls.key; if cert-manager updates ca.crt or tls.key without changing tls.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

📥 Commits

Reviewing files that changed from the base of the PR and between 9aa045f and 1a80527.

📒 Files selected for processing (7)
  • packages/core/platform/sources/vertical-pod-autoscaler.yaml
  • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/certificate.yaml
  • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml
  • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/tls-migrator.yaml
  • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/tls-secret.yaml
  • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/values.yaml
  • packages/system/vertical-pod-autoscaler/tests/vpa_test.yaml

@kvaps Andrei Kvapil (kvaps) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 — see pkg/kube/client.go in Helm (the original.Difference(target) loop 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. Likewise, helm-controller has no independent label-based prune for Helm-managed resources — the helm.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 manages type: kubernetes.io/tls, and Secret.type is 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 the vpa-webhook Service selector and becomes a bogus endpoint. If the Job survives this review, give it dedicated labels. (moot if the migrator is dropped)
  • The lookup comment says the checksum "resolves at every reconcile" — it doesn't; lookup only 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-cert alone 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=false after 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.
  • secretTemplate re-applies chart labels including app.kubernetes.io/managed-by: Helm to 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.

IvanHunters added a commit that referenced this pull request Jul 2, 2026
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]>
IvanHunters added a commit that referenced this pull request Jul 2, 2026
…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]>
IvanHunters added a commit that referenced this pull request Jul 2, 2026
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]>
@IvanHunters
IvanHunters force-pushed the fix/vpa-admission-cert-manager branch from 944d455 to 5e65a3e Compare July 2, 2026 15:57
…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]>

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
packages/system/vertical-pod-autoscaler/tests/vpa_test.yaml (1)

162-199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the existingSecret deployment annotation bypass.

Lines 162-166 state that existingSecret means 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1a80527 and 73c73df.

📒 Files selected for processing (8)
  • packages/core/platform/sources/vertical-pod-autoscaler.yaml
  • packages/system/vertical-pod-autoscaler/Makefile
  • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/certificate.yaml
  • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/deployment.yaml
  • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/templates/admission-controller/tls-secret.yaml
  • packages/system/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/values.yaml
  • packages/system/vertical-pod-autoscaler/patches/cert-manager-admission-tls.diff
  • packages/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

@kvaps Andrei Kvapil (kvaps) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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
myasnikovdaniil previously approved these changes Jul 3, 2026

@myasnikovdaniil myasnikovdaniil 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.

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-secret flip → ReplicaSet churn) is accurate.
  • The three TLS modes — cert-manager (default), legacy genCA (certManagerEnabled=false), and existingSecret — 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-manager is non-circular (cert-manager → networking + gateway-api-crds; neither reaches VPA).
  • patches/cert-manager-admission-tls.diff is byte-consistent with the vendored tree, so make update re-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

  1. 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/tls Secret delete/recreate plus single pod roll is reasoned, not yet observed on a live upgrade.
  2. Renewal safety window — see inline note on certificate.yaml.
  3. checksum/tls-cert scope — see inline note on deployment.yaml.
  4. Optional: add an existingSecret assertion 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

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.

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 }}

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.

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]>
@github-actions github-actions Bot added size/XL This PR changes 500-999 lines, ignoring generated files and removed size/L This PR changes 100-499 lines, ignoring generated files labels Jul 3, 2026
…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]>
IvanHunters added a commit that referenced this pull request Jul 4, 2026
…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]>
@IvanHunters
IvanHunters merged commit a1c7798 into main Jul 4, 2026
17 checks passed
@IvanHunters
IvanHunters deleted the fix/vpa-admission-cert-manager branch July 4, 2026 19:48
IvanHunters added a commit that referenced this pull request Jul 4, 2026
…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]>
IvanHunters added a commit that referenced this pull request Jul 5, 2026
…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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug priority/critical-urgent Highest priority. Must be actively worked on as someone's top priority right now size/XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants