Skip to content

fix: refresh Helm client credentials to prevent silent reconcile failures - #24

Merged
williamsena-vtex merged 3 commits into
mainfrom
fix/stale-helm-client-credentials
Sep 2, 2026
Merged

fix: refresh Helm client credentials to prevent silent reconcile failures#24
williamsena-vtex merged 3 commits into
mainfrom
fix/stale-helm-client-credentials

Conversation

@williamsena-vtex

Copy link
Copy Markdown
Contributor

Summary

  • ConditionalTTLReconciler.clientForNamespace built the Helm genericclioptions.ConfigFlags using r.Config.BearerToken — a static string captured once at process startup by ctrl.GetConfigOrDie() in main.go. It never followed r.Config.BearerTokenFile, unlike the controller-runtime client (mgr.GetClient()), whose transport re-reads the projected service-account token file on every request.
  • Once the startup token rotates/expires, every subsequent helm uninstall inside helmReleaseFinalizer fails with:
    error: "Kubernetes cluster unreachable: the server has asked for the client to provide credentials"
    
    for the rest of the pod's lifetime. The pod does not crash or restart, so this fails silently — ConditionalTTL finalizers stop running and Helm releases are never cleaned up, causing unbounded resource/backlog growth.
  • Observed in production on 3 separate EKS clusters within the same week; each time, only a manual kubectl rollout restart of the deployment (which re-runs ctrl.GetConfigOrDie() and gets a fresh token) resolved it — until it recurred after enough uptime.

Fix

clientForNamespace now re-reads the current token from r.Config.BearerTokenFile on each call (mirroring client-go's own bearer-token-refresh transport), falling back to the original static r.Config.BearerToken when BearerTokenFile is unset (e.g. out-of-cluster kubeconfigs). The function signature now returns an error for the file-read case, handled at the one call site in helmReleaseFinalizer via the existing Recorder.Eventf(..., "HelmSetupFailed", ...) pattern.

Verification

  • go build ./... — passes
  • go vet ./... — passes
  • go test ./... — passes (existing controllers envtest suite and custom_cel package). Note: the existing suite pre-sets HelmConfig on the reconciler to bypass clientForNamespace entirely (a documented test-only shortcut), so it confirms no regression but does not directly exercise the changed code path — that's inherently hard to unit test since it depends on BearerTokenFile pointing at a real file, which differs between envtest and real in-cluster environments.
  • Not verified against a live cluster reproduction — this is a code-level fix based on reading the credential-construction path; please review before merging.

Opening for team review, not intended for auto-merge.

… reconcile failures

The Helm client built in clientForNamespace() copied the static
BearerToken captured once at process startup (via ctrl.GetConfigOrDie())
instead of following BearerTokenFile like the controller-runtime client
does. Once the projected service-account token rotates, every Helm
uninstall in helmReleaseFinalizer starts failing with "Kubernetes
cluster unreachable: the server has asked for the client to provide
credentials" for the rest of the pod's lifetime, with no crash/restart
to surface it.

clientForNamespace now re-reads the token from BearerTokenFile on each
call, mirroring client-go's own refresh behavior, falling back to the
static token when BearerTokenFile is unset (e.g. out-of-cluster
kubeconfigs).

Observed in production on 3 separate EKS clusters within the same
week; only fixed by a manual pod restart each time.
@vtex-pr-sentinel

Copy link
Copy Markdown

🛡️ SDD Check — action required

I couldn't detect an SDD in this PR. Please check one option below (requires write access to the repo):

  • SDD lives in another PR — paste the SDD PR URL here:
  • This PR doesn't need an SDD
  • SDD applies, but I'm not adopting it in this PR

Covers: BearerTokenFile takes precedence over the static startup
token; the file is re-read on every call (the core regression for the
original bug — a rotated token on disk is picked up without a
restart); fallback to the static token when BearerTokenFile is unset
(out-of-cluster kubeconfig); and an unreadable token file surfaces as
an error instead of silently falling back.

@AdrianWR AdrianWR left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Status: Low risk, well-tested, ready for review and merge pending approvals.

Observations:

Code quality: Clean, defensive, well-commented. The extended comment block (lines 378–391) clearly documents the root cause and rationale.
Test coverage: Excellent — four unit tests plus the existing envtest suite all pass. The suite confirms no regression, though it doesn't directly exercise the changed code path (by design, since it pre-sets HelmConfig to bypass file I/O).
Error handling: Properly propagated up the call stack using existing Recorder.Eventf(...) pattern.
Production impact: Fixes a known, reproducible production issue on three EKS clusters. No breaking changes to public APIs.
CI/Merge status: Code passes go build, go vet, and go test. PR is mergeable but blocked by status checks and review approvals.

@FelipeNascimento FelipeNascimento left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

@williamsena-vtex
williamsena-vtex merged commit 26ead6c into main Sep 2, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants