Skip to content

Keep /api/issues fast on clusters with many Helm releases - #2058

Merged
hisco merged 10 commits into
mainfrom
friction/issues-helm-deadline
Oct 12, 2026
Merged

hisco merged 10 commits into
mainfrom
friction/issues-helm-deadline

Conversation

@hisco

@hisco hisco commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

What is wrong today

/api/issues finds failed and stuck Helm releases by listing every Helm release Secret with its full payload and decoding all of them, on every request. The cost grows with the number of releases and their history. On a kind cluster with real chart payloads (10 revisions per release, about 120 KB per Secret), main takes 8 s at 150 releases and 13 s at 200, and Radar's memory peaks above 1 GB during the read.

Radar Cloud gives each cluster 7 s. Past that, a scoped Issues page shows "Issues query failed for this scope" and Home shows the cluster as errored, even though the cluster is connected and healthy.

The read also runs on context.Background(), so when the caller gives up it keeps pulling data from the API server for nobody. The batch upgrade check and the packages inventory read the same way.

What the change does

Issue detection reads labels first. Helm labels every release Secret with the release name, revision and status. A Helm issue can only come from a release whose newest revision is failed or pending (helmhistory.Analyze raises nothing else), so:

  1. List the release Secrets as metadata only (labels, no payload).
  2. Find each release's newest revision and its status from the labels.
  3. List and decode in full only the releases whose newest revision is failed, pending-install, pending-upgrade, pending-rollback or uninstalling, plus any release with the same name in that read.
  4. Build rows from those with the same code as before.

Both lists run as the caller, with the same verb (list secrets), so permissions and the 403 handling don't change. If any release Secret lacks Helm's labels, that scope falls back to the full read. The Helm page, upgrade check and dashboard still do the full read; they need every release.

Helm reads also take a context and stop with it: the Secret list, the decode loop and row building. A caller leaving is not logged or reported as a Helm failure, and the packages inventory, cached per namespace scope for every caller, is not cached from a read cut short. A read that outlasts the server's 60 s request timeout is still reported as a failure.

Smaller pieces:

  • Issue detection skips per-release resource health, which it never reads.
  • The batch upgrade check builds releases and storage namespaces from one storage snapshot instead of action.NewList plus, cluster-wide, a second full read.
  • Direct release storage reads use the Helm client's own rest.Config when it has one.

Where to look hardest

  • issueStatuses in internal/helm/issue_releases.go. Skipping a release is only safe if no other newest status can raise an issue. TestOnlyIssueStatusesRaiseIssues runs every Helm status through helmhistory.Analyze to hold that.
  • The same-name rule. A full cluster-wide read groups revisions by the release's own namespace and name, so one release stored in two namespaces (Flux storageNamespace) forms one history whose newest revision wins. Reading every release that shares a name with a failed or pending one keeps that grouping intact.
  • Skipped releases still clear their stuck-uninstall start (forgetUninstallStarts), as the full read does for any release that isn't uninstalling.
  • ListReleasesAcrossNamespaces no longer runs Helm's per-namespace reachability probe. An unreachable cluster now reports the Secret list error instead of "kubernetes cluster unreachable".

Evidence

kind cluster, real chart payloads (kube-prometheus-stack, cert-manager, ingress-nginx, redis, podinfo), 10 revisions per release, one in 25 releases failed. Radar's own /api/issues, warm, 5 runs (3 for main at 200). The laptop was heavily loaded during these runs, so absolute numbers are noisy; the ratio held in every run.

Helm releases main median / worst this PR median / worst main peak RSS this PR peak RSS
25 1.89 s / 3.54 s 0.03 s / 0.03 s 261 MB 64 MB
50 2.86 s / 3.25 s 0.15 s / 0.17 s 423 MB 80 MB
100 4.91 s / 5.34 s 0.26 s / 0.28 s 823 MB 224 MB
150 8.21 s / 8.80 s 0.36 s / 0.62 s 1,126 MB 254 MB
200 12.99 s / 13.84 s 0.66 s / 0.89 s 1,409 MB 355 MB
300 not run 0.83 s / 0.94 s 563 MB

Peak RSS is the whole Radar process. Most of what remains on this PR is Radar's Secret informer cache, which holds the same release Secrets (425 MB idle at 300 releases).

The Helm issue rows were identical between main and this PR on the same cluster, including releases added in pending-install, pending-upgrade, pending-rollback and uninstalling states, an install failure, a rolled-back upgrade, one name failed in one namespace and healthy in another, and a release stored in two namespaces.

Through a local Radar Cloud stack at 150 releases (one cluster):

main this PR
Issues scoped to the cluster "Checking", then "Issues query failed for this scope" at 11.8 s 6 Helm issues and the rest at 0.6 s
Home "Checking", then "No clusters reporting" at 9.7 s 9 critical issues listed at 0.6 s

With this PR's Radar behind the hub and web from main, the same pages answer at 0.7 s, so the Radar change alone is enough.

Cancellation, local KWOK cluster, 400 release Secrets (about 210 MB), link to the API server throttled to 20 MB/s, caller gives up after 2 s:

main this PR
Bytes read after the caller gave up 99.6 MB 18.4 MB (already in flight)

Deliberately not in scope

  • Releases stored with Helm's ConfigMap or SQL drivers. Radar only reads the Secret driver, before and after.
  • If the newest revision's Secret can't be decoded, the full read falls back to an older revision and may report it. The label read skips that release.
  • If one stored history holds revisions installed into two different namespaces, the full read splits it in two and can report the older half. The label read follows Helm and treats it as one release.

Tests

  • TestIssueReleasesMatchAFullRead compares the label read with the full read on one fixture for cluster-wide, every-namespace, one-namespace and empty scopes: failed install, failed upgrade, stuck and fresh pending states, stuck and fresh uninstalls, rolled-back and since-fixed failures, uninstalled and unknown releases, one name in two namespaces, a release stored away from its namespace, and one stored in two namespaces. Every returned row must equal the full read's row field for field.
  • TestIssueReleasesReadOnlyFailedOrPendingReleasesInFull serves 62 release Secrets from a fake API server and checks only the failed release's 2 are sent in full. It fails on the full read.
  • TestIssueReleasesFallBackToAFullReadWithoutHelmLabels and TestOnlyIssueStatusesRaiseIssues.
  • TestIssuesStopReadingHelmStorageWhenCallerGivesUp passes here and fails when the Helm reads are put back on context.Background(). Further tests cover the packages read and its cache, the batch upgrade check, list and decode cancellation, and a caller leaving not being logged or recorded as a Helm error.
  • go test -race passes for internal/helm, internal/server, internal/mcp and internal/issues. go vet passes for internal/helm, internal/mcp and internal/issues. internal/server has the same 4 "copies lock" findings in test files as main.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Cancel Helm storage reads when callers disconnect

🐞 Bug fix âœĻ Enhancement 🧊 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

â€Ē Bind Helm Secret reads and release processing to caller contexts, stopping work after
 disconnection.
â€Ē Build batch upgrade results from one storage snapshot instead of reading cluster-wide Secrets
 twice.
â€Ē Skip unused resource-health checks during issue detection and cover cancellation with regression
 tests.
Diagram

graph TD
  R["Request consumers"] --> L["Release listing"] --> S["Storage snapshot"] --> K[("Helm Secrets")]
  R --> U["Upgrade check"] --> S
  K --> D["Decode revisions"] --> H["Release rows"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Context-aware Helm storage driver
  • ➕ Could retain more of Helm's native list-action behavior and error semantics.
  • ➖ Requires deeper integration with Helm's storage interfaces and does not inherently eliminate the second cluster-wide read.

Recommendation: Use the existing snapshot as this PR does: it supports cancellation and supplies upgrade data in one read without a new Helm integration layer. Review the latest-revision and status-mask parity carefully, along with the documented change from reachability-probe errors to Secret-list errors.

Files changed (12) +590 / -131

Bug fix (8) +133 / -118
client.goMake Helm snapshots cancellable and reuse them for upgrade checks +106/-100

Make Helm snapshots cancellable and reuse them for upgrade checks

â€Ē Threads contexts through release listing, Secret reads, decoding, row creation, and batch upgrade checks. Uses one snapshot for upgrade releases and storage namespaces, and adds an issue-list path that omits unused resource-health work. Cross-namespace listing now reads storage directly rather than running Helm's per-namespace reachability probe.

internal/helm/client.go

handlers.goPass HTTP request contexts to Helm list and upgrade handlers +2/-2

Pass HTTP request contexts to Helm list and upgrade handlers

â€Ē Connects release-list and batch-upgrade requests to their Helm operations so request cancellation reaches storage reads.

internal/helm/handlers.go

tools.goBind MCP dashboard and issue reads to tool contexts +2/-2

Bind MCP dashboard and issue reads to tool contexts

â€Ē Passes the tool context into dashboard release listing. Native Helm issue collection uses the context-aware, health-free issue listing path.

internal/mcp/tools.go

tools_helm.goBind MCP Helm listing to its tool context +1/-1

Bind MCP Helm listing to its tool context

â€Ē Passes the tool context to cross-namespace release listing so its Secret read can stop when the tool call ends.

internal/mcp/tools_helm.go

tools_helm_changes.goBind MCP Helm change reads to their tool context +1/-1

Bind MCP Helm change reads to their tool context

â€Ē Passes the tool context into the release listing used to assemble recent Helm changes.

internal/mcp/tools_helm_changes.go

dashboard.goBind dashboard Helm summaries to request contexts +1/-1

Bind dashboard Helm summaries to request contexts

â€Ē Passes the HTTP request context to release listing for the dashboard Helm summary.

internal/server/dashboard.go

issues_handler.goStop native Helm issue reads when requests end +17/-8

Stop native Helm issue reads when requests end

â€Ē Uses request-bound issue release listing without resource-health enrichment. Suppresses error logging when the caller has canceled the request.

internal/server/issues_handler.go

packages.goBind packages inventory Helm reads to its context +3/-3

Bind packages inventory Helm reads to its context

â€Ē Threads the inventory context through Helm release collection and each namespace-scoped list, preventing continued Secret reads after cancellation.

internal/server/packages.go

Tests (3) +455 / -13
client_test.goAdapt release-row tests to context-aware snapshot APIs +22/-13

Adapt release-row tests to context-aware snapshot APIs

â€Ē Passes contexts to snapshot reads and uses a helper for the new row-building signature. Reads storage namespaces from the snapshot after removal of the separate lookup helper.

internal/helm/client_test.go

list_context_test.goTest Helm cancellation, issue summaries, and single-read upgrades +263/-0

Test Helm cancellation, issue summaries, and single-read upgrades

â€Ē Adds API-server-backed cancellation tests for Secret lists and upgrade checks, plus cancellation checks for decoding and row creation. Verifies one Secret list for cluster-wide upgrades, issue rows without resource health, and forbidden-namespace behavior.

internal/helm/list_context_test.go

issues_helm_context_test.goTest issues and packages cancellation against stalled storage +170/-0

Test issues and packages cancellation against stalled storage

â€Ē Adds stalled API-server tests showing that canceled issues and packages reads return and abandon their Helm Secret requests.

internal/server/issues_helm_context_test.go

Documentation (1) +2 / -0
helm.mdDocument issue-list Helm read behavior +2/-0

Document issue-list Helm read behavior

â€Ē Explains that finding native Helm issues requires listing and decoding release Secrets. Clarifies that the response waits for the read while the caller remains connected, but the read stops on disconnection.

docs/helm.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) ðŸŽĻ UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. One abandoned request hides Helm packages from everyone for a minute ✓ Resolved
Description
computePackagesInternal now passes the request context into collectHelmReleases. When that
context is canceled partway through, the Helm read fails, the error is downgraded to a non-fatal
SourceError, and ListPackages caches the rows with no Helm releases in packagesCache for
packagesCacheTTL (60s). The cache key is built only from the namespaces, so if one caller closes
the tab or hits a proxy timeout, every later caller with the same namespace scope (REST and MCP)
gets an inventory with no Helm releases and a 'context canceled' Helm source error until the entry
expires.
Code

internal/server/packages.go[R368-370]

+	helmReleases, helmErrs := collectHelmReleases(ctx, namespaces, "", nil)
src.Helm = helmReleases
errs = append(errs, helmErrs...)
Evidence
collectHelmReleases turns any error that is not a forbidden error, including context.Canceled
and DeadlineExceeded from the new ctx-aware ListReleasesAsUser, into a SourceError and
returns. computePackagesInternal returns err == nil with those partial rows. ListPackages then
stores packagesCacheEntry{rows, errors} under packagesCacheKeyFor(p.Namespaces), a key that
ignores the caller's identity and whether it canceled, and serves that entry for 60s. The new test
TestPackagesHelmReadStopsWithItsRequest confirms a canceled read produces source errors rather
than a fatal error.

internal/server/packages.go[208-234]
internal/server/packages.go[475-526]
internal/server/packages.go[61-61]
internal/server/issues_helm_context_test.go[141-170]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Packages inventory now reads Helm on the request context. When the request is canceled, `collectHelmReleases` returns a non-fatal SourceError and `ListPackages` caches the rows without Helm releases for 60s, keyed only by namespaces. Other callers then get that incomplete result.
## Fix Focus Areas
- internal/server/packages.go[220-234]
- internal/server/packages.go[368-370]
## Recommended Fix
After `computePackagesInternal` returns in `ListPackages`, check `ctx.Err()`. If it is non-nil, return `ctx.Err()` (or return the result without storing it), and skip writing to `packagesCache`. You could also make `computePackagesInternal` return `ctx.Err()` as a fatal error once `collectHelmReleases` comes back while the context is done.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. A test comment recounts old Helm reads ✓ Resolved
Description
The comment above TestIssuesStopReadingHelmStorageWhenCallerGivesUp says `Before Helm reads were
tied to the request` and describes the former handler behavior. A reader of the test encounters
change history rather than a description of the behavior the test currently verifies.
Code

internal/server/issues_helm_context_test.go[R94-96]

+// release Secret read behind it must stop with it. Before Helm reads were
+// tied to the request, the handler held the response until the API server
+// answered and the read kept running after the caller had gone.
Evidence
The added test comment explicitly contrasts the current behavior with how Helm reads behaved before
the change.

Rule 3036538: Disallow references to tickets or PR history in code comments
internal/server/issues_helm_context_test.go[92-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test comment describes how Helm reads behaved before this change, rather than only the behavior being tested.
## Fix Focus Areas
- internal/server/issues_helm_context_test.go[92-96]
## Recommended Fix
Remove the historical sentences and retain only the test's current cancellation expectation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. A package test comment repeats its name ✓ Resolved
Description
The comment above TestPackagesHelmReadStopsWithItsRequest says that the packages inventory reads
Helm releases on the request's context, repeating the test name and its `collectHelmReleases(ctx,
...)` call. It adds no reason for the test or constraint that a later change would need to preserve.
Code

internal/server/issues_helm_context_test.go[140]

+// The packages inventory reads Helm releases on the request's context too.
Evidence
The added comment directly restates the adjacent test name and its call passing ctx to
collectHelmReleases.

Rule 3036542: Avoid explanatory comments that restate obvious code behavior
internal/server/issues_helm_context_test.go[140-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The package test comment restates the behavior already expressed by the test name and context-bearing call.
## Fix Focus Areas
- internal/server/issues_helm_context_test.go[140-147]
## Recommended Fix
Delete the comment, or replace it with a non-obvious reason for the test if one needs documenting.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Upgrade checks target the wrong cluster ✓ Resolved
Description
batchCheckUpgrades now obtains its Secret client through helmStorageClient, which uses the
process-global Kubernetes client instead of the receiver's REST configuration previously used by
Helm's list action. When the Helm client is bound to an explicit cluster, the check can inspect
another cluster's releases or panic if the global client has not been initialized.
Code

internal/helm/client.go[3194]

+	client, err := helmStorageClient(username, groups)
Evidence
The old batch path built an action configuration from the receiver and listed through it; the new
path calls a helper that selects the global Kubernetes client. The repository explicitly supports
Helm clients bound to a separate REST configuration, and documents that assigning an uninitialized
typed Kubernetes client to an interface makes the helper's nil guard ineffective.

internal/helm/client.go[164-170]
internal/helm/client.go[218-243]
internal/helm/client.go[1474-1486]
internal/helm/client.go[3189-3199]
internal/k8s/client.go[729-745]
cmd/explorer/cloud_cmd.go[621-637]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The batch upgrade check replaces a Helm list action configured from its receiver with a Secret client taken from process-global Kubernetes state. Explicitly configured Helm clients can consequently inspect the wrong cluster or panic when that global client is absent.
## Fix Focus Areas
- internal/helm/client.go[3189-3199]
- internal/helm/client.go[1474-1486]
- internal/helm/client.go[218-243]
## Recommended Fix
Construct the cancellable Secret-list client from the Helm receiver's resolved REST configuration, preserving impersonation when requested. Keep the snapshot read on the supplied context and test a receiver whose cluster differs from the process-global Kubernetes client.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

5. MCP issue stream logs every client cancel as a Helm failure 🐞 Bug ◔ Observability
Description
nativeHelmIssuesForContext now passes the MCP context into ListIssueReleasesAcrossNamespaces,
but its error branch still logs every error that is not a forbidden error as 'Failed to list Helm
releases for issue stream'. The REST sibling nativeHelmIssuesForRequest was changed in this PR to
stay quiet when ctx.Err() != nil, so MCP callers that disconnect or time out now produce false
Helm failure logs that look like storage or API problems.
Code

internal/mcp/tools.go[3105]

+	releases, err := helmClient.ListIssueReleasesAcrossNamespaces(ctx, helmNamespaces, username, groups)
Evidence
The server handler adds a case ctx.Err() != nil: branch to suppress logging on caller
cancellation. The MCP sibling, which now also runs on a cancellable ctx, keeps the old `if
!helm.IsForbiddenError(err) { log.Printf(...) }`.

internal/mcp/tools.go[3105-3110]
internal/server/issues_handler.go[222-238]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The MCP Helm issue stream logs context cancellation as a Helm list failure, unlike the REST handler.
## Fix Focus Areas
- internal/mcp/tools.go[3105-3112]
## Recommended Fix
Do the same as `nativeHelmIssuesForRequest`: if `ctx.Err() != nil`, return nil without logging, and only then fall through to the forbidden and other-error branches.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
ðŸ’Ą Did you know, you can commit Qodo's fix in one click with committable suggestions (GitHub & GitLab)

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/server/issues_helm_context_test.go Outdated
Comment thread internal/server/issues_helm_context_test.go Outdated
Comment thread internal/helm/client.go Outdated
Comment thread internal/server/packages.go
Comment thread internal/mcp/tools.go
helmNamespaces = resolveHelmListNamespaces(ctx, "")
}
releases, err := helmClient.ListReleasesAcrossNamespaces(helmNamespaces, username, groups)
releases, err := helmClient.ListIssueReleasesAcrossNamespaces(ctx, helmNamespaces, username, groups)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

5. Mcp issue stream logs every client cancel as a helm failure 🐞 Bug ◔ Observability

nativeHelmIssuesForContext now passes the MCP context into ListIssueReleasesAcrossNamespaces,
but its error branch still logs every error that is not a forbidden error as 'Failed to list Helm
releases for issue stream'. The REST sibling nativeHelmIssuesForRequest was changed in this PR to
stay quiet when ctx.Err() != nil, so MCP callers that disconnect or time out now produce false
Helm failure logs that look like storage or API problems.
Agent Prompt
## Issue description
The MCP Helm issue stream logs context cancellation as a Helm list failure, unlike the REST handler.

## Fix Focus Areas
- internal/mcp/tools.go[3105-3112]

## Recommended Fix
Do the same as `nativeHelmIssuesForRequest`: if `ctx.Err() != nil`, return nil without logging, and only then fall through to the forbidden and other-error branches.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

hisco added 10 commits October 12, 2026 11:41
/api/issues finds failed and stuck Helm releases by listing and decoding
every Helm release Secret. That read ran on context.Background(), so when
the caller gave up (a closed tab, a proxy timeout) the read kept pulling
data from the API server for nobody. The Helm batch upgrade check and the
packages inventory read the same way, and the cluster-wide upgrade check
read every Secret twice.

- Helm release storage reads take a context and stop with it: the Secret
  list, the decode loop, and row building. ListReleasesAcrossNamespaces,
  ListReleasesAsUser, ListReleases and the batch upgrade check take a
  context; the issues handler, Helm page, upgrade check, dashboard summary,
  packages inventory and MCP tools pass theirs.
- The batch upgrade check reads releases and their storage namespaces from
  one storage snapshot instead of Helm's list action plus, for the
  cluster-wide scope, a second full Secret read.
- Issue detection skips per-release resource health, which it never reads.

The issues response still waits for the whole Helm read while the caller
does; this change only stops the read when the caller leaves.

ListReleasesAcrossNamespaces no longer runs Helm's per-namespace
reachability probe, so an unreachable cluster reports the Secret list error
instead of "kubernetes cluster unreachable".

Nothing here proves that slow Helm reads caused any particular production
report; the change was measured on a local test cluster only.
The packages inventory is cached for 60 seconds per namespace scope and
shared by every caller. Now that the Helm read stops with its request, a
caller that leaves mid-read produced an inventory with no Helm releases and
a "context canceled" Helm error, and that result was cached for everyone
else asking about the same namespaces.

ListPackages now returns the context error instead of caching when the
caller has gone, and the REST handler stays quiet in that case.
Helm reads now stop when their caller leaves, which surfaces as a
"context canceled" or "context deadline exceeded" error. The REST issues
handler already treated that as nobody left to tell. Four other callers did
not:

- the MCP issue stream and the MCP dashboard logged it as a Helm list
  failure
- the REST dashboard did the same
- the Helm release list and the batch upgrade check answered 500, which
  records a Helm error in the diagnostics log and, for a timeout, asks the
  connection check whether the cluster is unreachable

Each now returns quietly when its own context is done.
One recounted how the handler behaved before this change, and one restated
its test's name.
The batch upgrade check now reads release Secrets directly instead of
through Helm's list action, and took its Kubernetes client from Radar's
process-wide client. A Helm client bound to an explicit rest.Config would
then check upgrades against whatever cluster Radar's own client points at.
Today only the process-wide Helm client runs upgrade checks, so the two
always agree, but the standalone client promises every read targets its
own config.

Release storage reads now use the receiver's rest.Config when it has one,
with impersonation when a user is given, and Radar's client otherwise. A
missing Radar client is now an error instead of a nil pointer panic.
Helm, packages and dashboard requests run under the server's 60 second
request timeout. When that runs out the context reports a deadline, not a
cancellation, and the caller is still waiting. The earlier checks treated
both the same, so a Helm read slower than the server's budget answered an
empty 504 with nothing logged, and the packages inventory returned nothing
instead of its other sources plus a Helm error.

Only a canceled context now counts as the caller leaving. A deadline is
reported as a failure again, and its partial packages inventory is cached
as before. The upgrade impact scan gets the same quiet path for a caller
leaving.
Its deadline case answers a Helm 5xx that reads as a connectivity error,
which can mark the cluster disconnected for the rest of the package run.
Stuck uninstall timing records a start only for reads that began on the
context still connected. The multi-namespace Secret read now carries that
context into the history analysis, and no test covered it there.
/api/issues found failed and stuck Helm releases by listing every Helm
release Secret with its encoded payload and decoding all of them. A Helm
issue only comes from a release whose newest revision is failed or
pending, and Helm labels every release Secret with the release name,
revision and status.

Issue detection now lists the Secret labels alone, then lists and decodes
only the releases whose newest revision is failed or pending, plus any
release with the same name in that read, so revisions group exactly as a
full read groups them. If a release Secret lacks Helm's labels, the read
falls back to decoding everything. The same identity makes both lists, so
permissions behave as before. Skipped releases still clear their
uninstall start, as a full read does.

A parity test compares the issue rows with a full read across scopes,
statuses, storage namespaces and name collisions.
Issue detection lists Helm release labels and then the selected release
Secrets with two clients. Each read the cluster config on its own, so a
context switch between the two could list labels on one cluster and
Secrets on the other, and that response would miss Helm issues. Both
clients now come from the same config.
@hisco
hisco force-pushed the friction/issues-helm-deadline branch from 9f09b5c to 2eedbbf Compare October 12, 2026 19:04
@hisco hisco changed the title Stop Helm reads from outliving the request that started them Keep /api/issues fast on clusters with many Helm releases Oct 12, 2026
@hisco
hisco merged commit 072ddd1 into main Oct 12, 2026
9 checks passed
hisco added a commit to StefanSa/radar that referenced this pull request Oct 13, 2026
- Helm list: the batch upgrade check guessed across all repos, so the list
  could offer an upgrade from the wrong repo while the drawer refused it. It
  now uses the recorded source first, like the single-release check.
- Pruned versions: a recorded source whose index no longer lists the
  installed version disabled upgrades. Recorded provenance now only needs
  the version list from that source; manual association still needs the
  exact installed version.
- Credentialed repository URLs: installing from an alias or a direct
  repository URL that carries credentials or a query failed with "failed to
  record chart source". It now installs without provenance, as before, and
  logs why. Link-local hosts are still rejected.
- Concurrent Helm operations: associating a source rewrote the release
  record and could clobber a running upgrade. It now refuses pending or
  uninstalling releases, re-reads the release before writing, and returns
  409 if it changed.
- Private repos: adding a repo that is already configured downloaded its
  index without the configured credentials and got a 401. It now uses the
  configured entry.
- Permission errors: a Kubernetes denial while associating a source (for
  example no permission to update the release Secret) returned 500. It now
  returns 403, like the other release writes.
- Source state in the dialog: while the source was loading, or when reading
  it failed, the dialog said "Not recorded". It now says it is checking, or
  shows the error with a Retry button.
- Repository config writes: adding a repository rewrote repositories.yaml
  in place, so a concurrent reader could see an empty file, or an entry
  whose index was not cached yet. The index is now cached first and the
  config is replaced by rename.
- Link-local check: an IPv6 address with a zone (fe80::1%eth0) skipped the
  check. The zone is now ignored when classifying the address.
- Version picker: changing a release's source did not refresh the cached
  version list. All three source mutations now invalidate it.
- Operator notice: it named helm.repositories, which the chart does not
  have. It now names helm.ociSources only.
- Dialog copy: "Verified exact matches" claimed more than the check does.
  It now says the listed sources publish the chart at the installed version.
- Removed TestCandidateOrderingStable, which sorted its own slice and did
  not exercise production code.
- Moved machine: when the recorded repository is not configured here, the
  dialog showed its URL but made the user retype it into an empty form. It
  now offers "Add <repository> and use it", which adds the recorded URL and
  associates it in one click. It keeps the recorded name when that name is
  free here, and otherwise picks the first free "<name>-N", so a name that
  points at a different repository is never reused.
- Re-associating the recorded repository under a new local name no longer
  needs the installed version in its index. The release already records
  that URL, and repositories prune old versions.

- Upgrade version picker: when the repository no longer publishes the
  installed version, the picker left it out, so nothing showed where the
  release is now. It is now listed in version order as "(current, no
  longer in the repository)" and cannot be selected.
- Helm list on clusters with release Secrets: the faster release read
  added in skyhook-io#2058 decodes the Secret payload only, and Helm keeps release
  labels on the Secret itself, so the batch upgrade check never saw the
  recorded source. The decoded releases now get the Secret's labels, as
  Helm's own List does.
hisco added a commit that referenced this pull request Oct 13, 2026
- Helm list: the batch upgrade check guessed across all repos, so the list
  could offer an upgrade from the wrong repo while the drawer refused it. It
  now uses the recorded source first, like the single-release check.
- Pruned versions: a recorded source whose index no longer lists the
  installed version disabled upgrades. Recorded provenance now only needs
  the version list from that source; manual association still needs the
  exact installed version.
- Credentialed repository URLs: installing from an alias or a direct
  repository URL that carries credentials or a query failed with "failed to
  record chart source". It now installs without provenance, as before, and
  logs why. Link-local hosts are still rejected.
- Concurrent Helm operations: associating a source rewrote the release
  record and could clobber a running upgrade. It now refuses pending or
  uninstalling releases, re-reads the release before writing, and returns
  409 if it changed.
- Private repos: adding a repo that is already configured downloaded its
  index without the configured credentials and got a 401. It now uses the
  configured entry.
- Permission errors: a Kubernetes denial while associating a source (for
  example no permission to update the release Secret) returned 500. It now
  returns 403, like the other release writes.
- Source state in the dialog: while the source was loading, or when reading
  it failed, the dialog said "Not recorded". It now says it is checking, or
  shows the error with a Retry button.
- Repository config writes: adding a repository rewrote repositories.yaml
  in place, so a concurrent reader could see an empty file, or an entry
  whose index was not cached yet. The index is now cached first and the
  config is replaced by rename.
- Link-local check: an IPv6 address with a zone (fe80::1%eth0) skipped the
  check. The zone is now ignored when classifying the address.
- Version picker: changing a release's source did not refresh the cached
  version list. All three source mutations now invalidate it.
- Operator notice: it named helm.repositories, which the chart does not
  have. It now names helm.ociSources only.
- Dialog copy: "Verified exact matches" claimed more than the check does.
  It now says the listed sources publish the chart at the installed version.
- Removed TestCandidateOrderingStable, which sorted its own slice and did
  not exercise production code.
- Moved machine: when the recorded repository is not configured here, the
  dialog showed its URL but made the user retype it into an empty form. It
  now offers "Add <repository> and use it", which adds the recorded URL and
  associates it in one click. It keeps the recorded name when that name is
  free here, and otherwise picks the first free "<name>-N", so a name that
  points at a different repository is never reused.
- Re-associating the recorded repository under a new local name no longer
  needs the installed version in its index. The release already records
  that URL, and repositories prune old versions.

- Upgrade version picker: when the repository no longer publishes the
  installed version, the picker left it out, so nothing showed where the
  release is now. It is now listed in version order as "(current, no
  longer in the repository)" and cannot be selected.
- Helm list on clusters with release Secrets: the faster release read
  added in #2058 decodes the Secret payload only, and Helm keeps release
  labels on the Secret itself, so the batch upgrade check never saw the
  recorded source. The decoded releases now get the Secret's labels, as
  Helm's own List does.
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.

1 participant