Skip to content

Per-resource health with provenance when Argo CD doesn't persist it - #1743

Merged
nadaverell merged 17 commits into
mainfrom
feat/argo3-resource-health
Sep 13, 2026
Merged

nadaverell merged 17 commits into
mainfrom
feat/argo3-resource-health

Conversation

@nadaverell

@nadaverell nadaverell commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Argo CD 3.0 stopped persisting per-resource health into the Application object by default (status.resourceHealthSource: appTree). Every Radar reader of status.resources[].health went blind on such installs: no per-resource Issues, no cause bridge to the issues engine, Unknown tree nodes for non-topology kinds, no Degraded/Missing categories in the Resources table. The app-level badge still said Degraded with nothing under it. #1624 surfaced this.

What changes

One resolved health per managed resource, with provenance. A tree node's health now carries healthSource (controller for the GitOps CR's own verdict, radar for Radar's read) plus healthReason / healthMessage / healthSeverity. Every surface reads that one value: node colour (previously from Radar's topology status even when Argo said Degraded), the Resources-table category, per-resource Issues, and the summary counts. argoResourceChanges takes health from the tree instead of re-reading the CR, and the tree and insights endpoints share one resolveGitOpsTree path so a node can't differ between the two responses. The wire field health stays a string, so @skyhook-io/k8s-ui consumers are unaffected.

Radar's diagnostics fill the gap, labelled as Radar's. In appTree mode, for an app that is not Healthy and deploys to this cluster, the host overlays the issues engine's existing classification (crash loops, image pulls, failed Ready conditions; already transient-filtered and owner-collapsed) onto declared nodes. Problems only, never Healthy; controller health is final; a node whose health was only Radar's topology read gives way to the engine's more specific finding. The composition is memoized per user and namespace set for 5s (gitopsIssuesMemo) so a page load's two requests and the 2s polls share one pass.

Severity follows the evidence. An engine finding keeps its own reason and severity as a per-resource Issue. A Radar topology read with no classified reason is a warning-tier observation, not a verdict, and neither it nor the "N managed resources are degraded" count appears on an app Argo calls Healthy. The count and the Warning-event lead from #1624 only show when no per-resource finding already names a resource.

The UI says so. A notice above the Issues band: "Argo CD 3 no longer records health for each resource on the application. Rows marked "Found by Radar" are problems Radar detected by looking at the resources itself." with a "Why?" link to the docs. When Radar's own look found nothing to mark, the notice says that and names the way out (controller.resource.health.persist: "true" in argocd-cmd-params-cm). Rows and declared nodes carry a "Found by Radar" marker with the engine's message in the tooltip.

Remote-destination apps read nothing local. An Application deploying to another cluster used to be matched against same-named local objects (topology, cache, drift, events, cause bridge). It now carries only what the CR says, with a notice: "This application deploys to a different cluster, so its resources aren't visible from here." Standalone Radar, when the Cloud funnel is available, adds "Connect both clusters to Radar Cloud to see this app's resources together.", where "Radar Cloud" opens the existing funnel dialog.

Fixture. gitops-demo installs Argo CD v3.5.2 by default (server-side apply; ARGOCD_VERSION=v2.13.2 selects the older inline shape). New argo3-health app sourced from this repo (ARGO3_FIXTURE_REVISION to test a branch): a CRD Argo evaluates via a Lua health check installed into argocd-cm, one it ignores, and a crashlooping Deployment. guestbook-remote covers the remote-destination case.

Docs. docs/gitops.md "Per-resource health"; one-line rule in CLAUDE.md; demo README matrix. The in-product "Why?" link targets /features/gitops#per-resource-health on radar-docs, added by skyhook-dev/radar-docs#94 — merge that before the release that carries this.

Not in this PR: sourcing Argo's own verdicts from the argocd-server API when the integration is connected (fast-follow), and Missing derivation for declared-but-absent resources.

Testing

  • go test ./... in both modules, make tsc, k8s-ui vitest (108).
  • Visual, on a fresh Argo CD 3.5.2 kind cluster: argo3-health (notice, engine-sourced headline, "Found by Radar" on the three declared nodes and in the Resources table, none on generated ReplicaSet/Pod; light and dark) and guestbook-remote (notice; the Cloud sentence opening the funnel dialog with RADAR_CLOUD_FUNNEL=on).
  • Argo CD 2.x parity (controller health wins, no marker, no notice) is pinned by unit tests; not re-run live.

Note

Medium Risk
Changes how GitOps detail surfaces health and issues for Argo CD 3 and remote apps—operator-facing diagnosis paths—but rules limit false positives (no synthetic Healthy, skip remote/healthy-app overlay) and coverage is heavy on unit tests.

Overview
Adds per-resource health with provenance for Argo CD 3’s default (appTree), where the Application CR no longer carries status.resources[].health. Tree and insights now share resolveGitOpsTree, which can overlayRadarHealth from the issues engine (problems only, memoized ~5s per user/namespaces) so tree nodes, Resources table, and Issues stay aligned via healthSource (controller | radar) plus reason/message/severity.

Insights and tree behavior change accordingly: argoResourceChanges resolves health from the tree (not a second CR read); per-resource Issues respect engine severity and suppress contradictions when Argo calls the app Healthy; remote-destination Applications skip local topology/drift/events/cause overlay. The tree builder tags healthMode / remoteDestination and stops attributing local objects to spoke destinations.

UI adds a banner (GitOpsHealthSourceNotice), “Found by Radar” markers on tree/Changes rows, docs links, and standalone Radar Cloud hint for remote apps (openCloudFunnel). Demo/docs default to Argo v3.5.2 with argo3-health and guestbook-remote fixtures.

Reviewed by Cursor Bugbot for commit 7c58e43. Bugbot is set up for automated code reviews on this repo. Configure here.

…t persist it

Argo CD 3.0 stops writing per-resource health into the Application
(status.resourceHealthSource: appTree), so every reader of
status.resources[].health went blind: no per-resource Issues, no cause
bridge, Unknown tree nodes for non-topology kinds.

Health on a tree node now carries where it came from (controller vs
Radar) plus the reason/message/severity behind it, and every surface
(node colour, Changes category, per-resource Issue, summary counts) reads
that one resolved value. In appTree mode the host overlays Radar's issues
engine finding on declared nodes that have none — problems only, never
Healthy, in-cluster destinations only — and the UI marks those rows as
Radar's with a notice above the Issues band. Remote-destination apps
derive nothing and say so; standalone Radar adds a Radar Cloud pointer.

The gitops-demo installs Argo CD 3.x by default with a fixture app that
shows every branch: a CRD Argo evaluates, one it ignores, and a
crashlooping Deployment.
…Radar's topology read

A remote-destination Application declares resources that live on another
cluster; matching them against this cluster's topology or cache
attributed a same-named local object's health, metadata and ownership to
them. Declared nodes of a remote app now carry only what the CR says.

In appTree mode the issues-engine overlay also applies to nodes whose
health is Radar's own topology read (Progressing at 1/2 replicas): the
classified finding is the more specific answer. Controller health stays
final. The demo script patches argocd-cm with a YAML patch file instead of
JSON-escaping the Lua.
…alls Argo CD server-side

Changes rows for a remote-destination app no longer read this cluster's
objects for drift diffs or events, and the insights handler skips the
live prefetch for them. Argo CD 3.x CRDs exceed the client-side
last-applied annotation limit, so the demo applies install.yaml
server-side.
Reworded to say what the user sees ('its resources aren't visible from
here') and what to do about it. 'Radar Cloud' now opens the same funnel
dialog as the top-right button instead of linking out, and the hint only
renders when that button does (cloudConnect capability present, not
embedded).
@nadaverell
nadaverell requested a review from hisco as a code owner September 13, 2026 19:51
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Restore Argo CD 3 resource health with source provenance

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Restores Argo CD 3 resource diagnostics using provenance-aware Radar findings.
• Unifies tree, issues, table, and summary health behind one resolved value.
• Blocks local enrichment for remote destinations and explains health limitations in the UI.
Diagram

graph TD
  A["Argo Application"] --> B["Tree Builder"] --> C{"Local destination?"}
  C -->|Yes| D["Health Resolver"] --> E["Issues Engine"] --> F["Resolved Tree"] --> G["Insights API"] --> H["GitOps UI"]
  C -->|No, CR only| F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fetch health from Argo API
  • ➕ Preserves Argo's authoritative custom and built-in health evaluations
  • ➕ Can expose Healthy and Progressing states unavailable to problem-only diagnostics
  • ➖ Requires optional Argo credentials and integration configuration
  • ➖ Needs caching, identity verification, failure handling, and version-specific API validation
2. Require persisted Argo health
  • ➕ Keeps Argo as the sole health authority
  • ➕ Requires little additional Radar runtime logic
  • ➖ Forces operators to reverse an upstream performance-oriented default
  • ➖ Leaves default Argo CD 3 installations without useful resource diagnostics
3. Evaluate conditions directly
  • ➕ Works without an Argo API integration
  • ➕ Could cover arbitrary resources with Ready conditions
  • ➖ Duplicates controller-specific health semantics and Lua customizations
  • ➖ Risks false degradation during valid transient states

Recommendation: The PR's issues-engine overlay is the best immediate OSS-compatible fix because it reuses filtered, owner-collapsed diagnostics and labels them explicitly as Radar findings. Keep the problem-only and local-destination gates; pursue authenticated Argo API retrieval as a later authoritative tier rather than replacing this fallback.

Files changed (37) +1729 / -150

Enhancement (13) +301 / -13
GitOpsDetailLayout.tsxRender health-source notices above GitOps issues +10/-0

Render health-source notices above GitOps issues

• Adds host-provided documentation and remote-destination hint inputs, then renders the new notice before the Issues band.

packages/k8s-ui/src/components/gitops/GitOpsDetailLayout.tsx

GitOpsHealthSourceNotice.tsxAdd appTree and remote-destination notices +76/-0

Add appTree and remote-destination notices

• Introduces plain-language notices selected from insight summary metadata, with an optional documentation link and host-supplied remote-cluster guidance.

packages/k8s-ui/src/components/gitops/GitOpsHealthSourceNotice.tsx

health-provenance.tsCentralize Radar health marker behavior +24/-0

Centralize Radar health marker behavior

• Adds shared copy and a predicate that identifies Radar-sourced Degraded or Missing values and includes the diagnostic message in tooltips.

packages/k8s-ui/src/components/gitops/health-provenance.ts

index.tsExport health provenance UI helpers +2/-0

Export health provenance UI helpers

• Exports the notice component, notice predicates and copy, and Radar health marker utilities from the GitOps package.

packages/k8s-ui/src/components/gitops/index.ts

GitOpsInsightViews.tsxMark Radar findings in resource rows +15/-1

Mark Radar findings in resource rows

• Adds “Found by Radar” markers and diagnostic tooltips to declared resource health badges while leaving generated resource rows unmarked.

packages/k8s-ui/src/components/gitops/insights/GitOpsInsightViews.tsx

GitOpsTreeGraph.tsxMark Radar-derived health on tree nodes +13/-0

Mark Radar-derived health on tree nodes

• Displays provenance markers and tooltips on declared nodes whose unhealthy status came from Radar rather than the GitOps controller.

packages/k8s-ui/src/components/gitops/tree/GitOpsTreeGraph.tsx

gitops-insights.tsType health provenance in GitOps insights +17/-0

Type health provenance in GitOps insights

• Adds health mode and remote-destination summary fields, issue source metadata, and source, reason, and severity fields for resource changes.

packages/k8s-ui/src/types/gitops-insights.ts

gitops-tree.tsType tree health sources and storage modes +21/-0

Type tree health sources and storage modes

• Extends tree nodes and responses with provenance details, Argo health-storage mode, and remote-destination metadata while keeping health itself a string.

packages/k8s-ui/src/types/gitops-tree.ts

inventory.goParse controller health source and Argo storage mode +19/-2

Parse controller health source and Argo storage mode

• Marks persisted resource health as controller-sourced and detects appTree mode exclusively from Argo's explicit resourceHealthSource status field.

pkg/gitops/tree/inventory.go

types.goModel health provenance and destination locality +58/-10

Model health provenance and destination locality

• Adds health source, reason, message, severity, storage mode, and remote-destination fields to the Go tree model without changing the health wire type.

pkg/gitops/tree/types.go

CloudFunnelButton.tsxExpose the existing Cloud connection dialog +15/-0

Expose the existing Cloud connection dialog

• Adds a window event helper that lets other standalone UI surfaces open the existing Radar Cloud funnel instead of duplicating its flow.

web/src/components/CloudFunnelButton.tsx

GitOpsView.tsxWire health documentation and remote Cloud guidance +5/-0

Wire health documentation and remote Cloud guidance

• Provides the shared detail layout with the per-resource health documentation URL and standalone remote-destination Cloud hint.

web/src/components/gitops/GitOpsView.tsx

RemoteDestinationCloudHint.tsxAdd contextual Cloud guidance for remote applications +26/-0

Add contextual Cloud guidance for remote applications

• Offers eligible standalone users a link that opens the existing Cloud funnel, while suppressing the prompt for embedded deployments or unavailable capabilities.

web/src/components/gitops/RemoteDestinationCloudHint.tsx

Bug fix (5) +307 / -108
gitops_handlers.goResolve provenance-aware health through one server path +77/-30

Resolve provenance-aware health through one server path

• Introduces a shared tree resolution pipeline for tree and insights endpoints. It overlays RBAC-aware issue-engine findings only for non-healthy, in-cluster appTree applications, recomputes summaries, and suppresses local prefetching for remote destinations.

internal/server/gitops_handlers.go

merge.tsMerge health values with their provenance +7/-0

Merge health values with their provenance

• Updates multi-cluster tree merging so health metadata is copied from the same source node as the selected health value.

packages/k8s-ui/src/components/gitops/tree/merge.ts

insights.goBuild all insights from resolved tree health +160/-64

Build all insights from resolved tree health

• Uses tree health as the shared authority for changes, categories, issues, and summaries. Radar-sourced issues retain engine reason and severity, while remote applications skip local causes, events, and drift enrichment.

pkg/gitops/insights/insights.go

builder.goDetect health mode and isolate remote application trees +30/-10

Detect health mode and isolate remote application trees

• Records appTree versus inline mode and remote destinations on Argo trees. Remote applications now produce synthetic CR-only nodes without reading local topology, objects, metadata, or ownership.

pkg/gitops/tree/builder.go

graph.goResolve node tone from provenance-aware health +33/-4

Resolve node tone from provenance-aware health

• Makes controller health override topology status, labels topology fallbacks as Radar-sourced, and exports the common tree summarizer for post-overlay and post-RBAC use.

pkg/gitops/tree/graph.go

Refactor (1) +1 / -1
managed.goAdopt the shared exported tree summarizer +1/-1

Adopt the shared exported tree summarizer

• Routes managed-tree summary calculation through the same summarizer used by built, filtered, and overlaid trees.

pkg/gitops/tree/managed.go

Tests (6) +479 / -18
gitops_health_overlay_test.goTest Radar health overlay precedence and gates +97/-0

Test Radar health overlay precedence and gates

• Covers problem metadata propagation, replacement of topology-derived health, controller precedence, declared-node filtering, and skips for inline, remote, and healthy applications.

internal/server/gitops_health_overlay_test.go

health-provenance.test.tsTest provenance markers and notice selection +35/-0

Test provenance markers and notice selection

• Verifies markers appear only for Radar-sourced problems, remote notices take precedence, healthy and Flux views remain silent, and user-facing copy avoids internal terminology.

packages/k8s-ui/src/components/gitops/health-provenance.test.ts

merge.test.tsTest provenance-preserving tree merges +34/-0

Test provenance-preserving tree merges

• Ensures source, reason, message, and severity follow whichever merged tree contributes the winning health value.

packages/k8s-ui/src/components/gitops/tree/merge.test.ts

insights_test.goCover resolved health, issue provenance, and remote isolation +176/-17

Cover resolved health, issue provenance, and remote isolation

• Adds tests for controller precedence, Radar issue semantics, fallback suppression, summary metadata, and preventing remote applications from borrowing local causes or enrichment.

pkg/gitops/insights/insights_test.go

builder_test.goTest tree provenance and remote-cluster safety +136/-0

Test tree provenance and remote-cluster safety

• Verifies controller health controls node tone, appTree topology health is Radar-sourced, unassessed resources stay unknown, and remote resources receive no local data.

pkg/gitops/tree/builder_test.go

graph_test.goUse the exported tree summarizer in tests +1/-1

Use the exported tree summarizer in tests

• Updates summary coverage to call the shared exported implementation.

pkg/gitops/tree/graph_test.go

Documentation (4) +380 / -7
CLAUDE.mdDocument the resolved-health provenance contract +1/-0

Document the resolved-health provenance contract

• Adds contributor guidance for controller- versus Radar-sourced health, overlay constraints, and the requirement that all consumers use the tree's resolved value.

CLAUDE.md

gitops.mdExplain Argo CD per-resource health sources +12/-1

Explain Argo CD per-resource health sources

• Documents inline Argo health, Argo CD 3 appTree behavior, Radar's problem-only fallback, remote-destination limits, and the persistence escape hatch.

docs/gitops.md

argo3-resource-health.mdAdd the signed-off Argo 3 health implementation plan +331/-0

Add the signed-off Argo 3 health implementation plan

• Records the problem analysis, design rationale, implementation sequence, UI copy requirements, alternatives, risks, and follow-up Argo API tier.

docs/plans/argo3-resource-health.md

README.mdDocument Argo 3 health and remote fixtures +36/-6

Document Argo 3 health and remote fixtures

• Describes expected Widget, Gadget, crash-loop, and remote-application behavior, branch testing, version selection, and remaining multi-cluster limitations.

scripts/gitops-demo/README.md

Other (8) +261 / -3
gitops-demo.shMake Argo CD 3 the default demo environment +47/-3

Make Argo CD 3 the default demo environment

• Upgrades the default to Argo CD 3.5.2, applies large CRDs server-side, supports fixture branch selection, and installs the demo Widget health customization.

scripts/gitops-demo.sh

02-argo-appproject.yamlPermit new health and remote demo scenarios +10/-0

Permit new health and remote demo scenarios

• Allows Radar repository sources, the synthetic spoke destination, and radar.demo resources in the demo AppProject.

scripts/gitops-demo/02-argo-appproject.yaml

11-argo3-health-crds.yamlAdd evaluated and unevaluated health fixture kinds +93/-0

Add evaluated and unevaluated health fixture kinds

• Defines Widget and Gadget CRDs plus a Widget Lua health customization, allowing tests of Argo-evaluated and Radar-only condition failures.

scripts/gitops-demo/11-argo3-health-crds.yaml

12-argo3-health-app.yamlAdd the Argo CD 3 health fixture application +31/-0

Add the Argo CD 3 health fixture application

• Creates an automated in-cluster Application sourcing the new health scenario from the Radar repository.

scripts/gitops-demo/12-argo3-health-app.yaml

13-argo-remote-destination.yamlAdd a remote-destination fixture application +20/-0

Add a remote-destination fixture application

• Defines an Application targeting an unavailable spoke cluster to exercise CR-only rendering and remote-destination notices.

scripts/gitops-demo/13-argo-remote-destination.yaml

crashloop.yamlAdd a crash-loop health fixture +27/-0

Add a crash-loop health fixture

• Defines a Deployment that repeatedly exits so Radar can classify CrashLoopBackOff and collapse the problem onto the managed workload.

scripts/gitops-demo/argo3-health/crashloop.yaml

gadget.yamlAdd a Radar-only condition fixture +17/-0

Add a Radar-only condition fixture

• Defines an unhealthy custom resource without an Argo health check to verify that Radar findings remain explicitly identified as Radar-sourced.

scripts/gitops-demo/argo3-health/gadget.yaml

widget.yamlAdd an Argo-evaluated condition fixture +16/-0

Add an Argo-evaluated condition fixture

• Defines an unhealthy Widget whose Ready condition is evaluated by the demo Argo Lua customization and independently classified by Radar.

scripts/gitops-demo/argo3-health/widget.yaml

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

Stale Bugbot comment from a previous run.

Comment thread pkg/gitops/insights/insights.go
@qodo-code-review

qodo-code-review Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Health help opens no explanation ✓ Resolved 🔗 Cross-repo conflict ≡ Correctness
Description
GITOPS_HEALTH_DOCS_URL targets /docs/gitops/#per-resource-health, but radar-docs publishes
GitOps under features/gitops and that page has no per-resource-health section. The sync pipeline
excludes unmapped files such as docs/gitops.md, so merging this PR cannot publish the newly
written explanation at the linked destination.
Code

web/src/components/gitops/GitOpsView.tsx[77]

+const GITOPS_HEALTH_DOCS_URL = 'https://radarhq.io/docs/gitops/#per-resource-health'
Relevance

●● Moderate

Cross-repository documentation routing is plausible, but no close historical acceptance or rejection
precedent was found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR points its new Why link at a path and anchor that the publishing repository does not define.
Radar-docs identifies features/gitops as the navigated page, its current diagnosis section lacks
the new heading, and its synchronizer processes only explicitly mapped files plus
docs/integrations.md.

web/src/components/gitops/GitOpsView.tsx[75-77]
docs/gitops.md[72-81]
External repo: skyhook-dev/radar-docs, docs.json [34-45]
External repo: skyhook-dev/radar-docs, features/gitops.mdx [53-70]
External repo: skyhook-dev/radar-docs, scripts/sync-oss-docs.mjs [43-50]
External repo: skyhook-dev/radar-docs, scripts/sync-oss-docs.mjs [693-720]

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 new health-provenance help URL does not match the GitOps page published by radar-docs, and the promised anchor is absent because `docs/gitops.md` is not synchronized.

## Fix Focus Areas
- web/src/components/gitops/GitOpsView.tsx[77-77]
- docs/gitops.md[72-81]
- /cross_repos/radar-docs/features/gitops.mdx[53-63]
- /cross_repos/radar-docs/scripts/sync-oss-docs.mjs[43-50]

## Recommended Fix
Add the per-resource-health section to the curated radar-docs GitOps page, either directly or through a deliberate sync mapping, then change `GITOPS_HEALTH_DOCS_URL` to the resulting published `features/gitops#per-resource-health` URL.

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



Remediation recommended

2. One namespace hides another's failure ✗ Dismissed 🐞 Bug ≡ Correctness
Description
The new tree-backed argoResourceChanges call makes app-tree health findings enter the existing
operation-failure suppression path, which keys affected resources by kind and name without
namespace. When a failed operation names one resource and the application manages another same-kind,
same-name resource in a different namespace, the second resource's newly derived Degraded or Missing
issue is suppressed too.
Code

pkg/gitops/insights/insights.go[R599-602]

+		for _, change := range argoResourceChanges(root, resourceTree, nil) {
			// Suppress a resource issue when its kind/name match a resource
			// already named in the operation failure — same root cause, no
			// value in showing it twice. Also suppress every resource in a
Relevance

●●● Strong

Recent GitOps history accepts identity-key collisions; namespace omission can suppress distinct
resources in this suppression path.

PR-#1624
PR-#719

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
This PR changes the resource-issue pass to use tree-resolved health, so Argo 3 findings now reach
the suppression lookup. That lookup intentionally discards namespace even though each
managed-resource change retains it, causing a single operation message to match all same-kind/name
rows.

pkg/gitops/insights/insights.go[541-550]
pkg/gitops/insights/insights.go[599-619]
pkg/gitops/insights/insights.go[1120-1163]
pkg/gitops/insights/insights.go[1181-1217]
pkg/gitops/insights/insights.go[1741-1753]

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 operation failure parser has no namespace for an affected resource, but the suppression key matches every managed resource with that kind and name. Tree-derived health now makes this suppress unrelated Argo 3 resource issues in applications that deploy identical names to multiple namespaces.

### Fix Focus Areas
- pkg/gitops/insights/insights.go[541-550]
- pkg/gitops/insights/insights.go[599-619]
- pkg/gitops/insights/insights.go[1741-1753]

### Recommended Fix
Before suppressing a kind/name affected by an operation failure, count matching managed resource rows. Suppress the resource-level issue only when exactly one managed resource has that kind and name; otherwise retain the per-resource issues because the operation message cannot safely identify their namespace. Add a regression test with two same-named resources in distinct namespaces and tree-derived health on the non-failed resource.

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


3. Radar-only problems become critical ✓ Resolved 🐞 Bug ≡ Correctness
Description
resourceHealthIssue recognizes Radar health only when HealthReason is non-empty, so
topology-derived Radar health falls through to the controller branch and receives
SeverityCritical. This occurs for degraded topology nodes without an issues-engine overlay,
including inline-mode applications and healthy app-tree applications, and promotes Radar's generic
live-state observation into a critical issue.
Code

pkg/gitops/insights/insights.go[R753-756]

+	if change.HealthSource == string(gitopstree.HealthSourceRadar) && change.HealthReason != "" {
+		severity := SeverityWarning
+		if change.HealthSeverity == "critical" {
+			severity = SeverityCritical
Relevance

●●● Strong

Recent insights review accepted preserving warning-tier Radar severity and provenance instead of
promoting findings to critical.

PR-#1624

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Topology fallback assigns HealthSourceRadar and a health value but does not populate reason or
severity. The changed issue path consumes that tree health, while the new resourceHealthIssue
predicate requires a reason before preserving Radar semantics; otherwise it assigns the controller
path's critical severity.

pkg/gitops/tree/graph.go[27-43]
pkg/gitops/insights/insights.go[599-620]
pkg/gitops/insights/insights.go[752-780]

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

## Issue description
Radar-sourced topology health has no `HealthReason` or `HealthSeverity`, so `resourceHealthIssue` mistakes it for controller health and emits a critical issue. This elevates a generic Radar observation beyond its prior warning-level treatment.

## Fix Focus Areas
- pkg/gitops/insights/insights.go[752-780]
- pkg/gitops/tree/graph.go[27-43]

## Recommended Fix
Handle every `HealthSourceRadar` change in the Radar branch, not only changes with a non-empty reason. Use the supplied Radar reason/message/severity when present, and use warning severity plus suitable fallback reason/message for topology-only health; reserve the controller branch's critical default for controller-sourced values.

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


4. Opening a resource runs two full scans ✗ Dismissed 🐞 Bug ➹ Performance
Description
resolveGitOpsTree unconditionally constructs an insights resolver and calls overlayRadarHealth,
whose first ResourceProblems lookup invokes uncapped issues.Compose across every detector. The
detail view fetches the tree and insights endpoints independently, so a non-Healthy app-tree
application performs this cluster-wide composition once for the tree response and again for insights
whenever the page opens.
Code

internal/server/gitops_handlers.go[R173-175]

+	resolver := newInsightsResolver(r.Context(), req.Cache, req.AllowedNamespaces, canAccess)
+	overlayRadarHealth(tree, root, resolver.ResourceProblems)
+	tree = s.filterGitOpsTreeForUser(r, req, tree)
Relevance

●●● Strong

Recent history accepts concrete duplicate-work findings; separate endpoints independently repeat
expensive issue composition.

PR-#1187
PR-#775

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR routes the tree endpoint through the new resolver path, and the detail view mounts
independent tree and insights queries. ResourceProblems lazily composes all issue sources with
NoLimit, so the two HTTP requests cannot share its request-local sync.Once result.

internal/server/gitops_handlers.go[133-177]
internal/server/gitops_handlers.go[894-928]
internal/issues/compose.go[105-112]
internal/issues/compose.go[155-170]
web/src/components/gitops/GitOpsView.tsx[380-383]
web/src/api/client.ts[2207-2215]
web/src/api/client.ts[2237-2250]

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

### Issue description
`resolveGitOpsTree` invokes the full issues-engine composition for the tree endpoint, then the detail page invokes it again through the separate insights endpoint. Avoid recomputing the same per-resource findings for the same application view.

### Fix Focus Areas
- internal/server/gitops_handlers.go[157-177]
- internal/server/gitops_handlers.go[894-928]
- web/src/components/gitops/GitOpsView.tsx[380-383]

### Recommended Fix
Make the resolved-health result reusable across the tree and insights requests, for example by returning or caching a short-lived, RBAC-scoped per-resource problem index keyed to the application and cache generation. Alternatively, consolidate the client responses so the view does not request two independently resolved trees. Preserve the current per-user access checks when reusing any result.

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


View medium (2)
5. A test helper repeats its behavior ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
findNode is preceded by a comment saying it returns the matching node and fails when absent,
exactly what the short body expresses. A later reader gains no rationale or hidden constraint from
the comment and must keep redundant prose synchronized with the helper.
Code

pkg/gitops/tree/builder_test.go[296]

+// findNode returns the tree node for ref, failing the test when absent.
Relevance

●●● Strong

An exact recent precedent accepts removing redundant comments that merely restate adjacent test
behavior.

PR-#1624

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3036542 disallows comments that only translate adjacent code into prose. The cited comment
describes the return and failure behavior directly implemented by the following short helper without
adding intent or constraints.

Rule 3036542: Avoid explanatory comments that restate obvious code behavior
pkg/gitops/tree/builder_test.go[296-305]

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 comment above `findNode` merely restates the helper's immediately visible control flow and adds no rationale, constraint, or other non-obvious information.

## Fix Focus Areas
- pkg/gitops/tree/builder_test.go[296-305]

## Recommended Fix
Delete the comment above `findNode`. Keep the clearly named helper and its concise implementation unchanged.

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


6. Health views receive raw states ✗ Dismissed 📘 Rule violation ≡ Correctness
Description
overlayRadarHealth writes the noncanonical value Degraded into Node.Health, which remains an
unconstrained string rather than a standard health tone. When Radar finds a resource problem, that
value flows through argoResourceChanges, category and issue comparisons, and frontend rendering
before the separate TopologyStatus conversion normalizes only the graph tone.
Code

internal/server/gitops_handlers.go[217]

+		n.Health = "Degraded"
Relevance

●● Moderate

Health vocabulary normalization is valued, but this repository already uses Degraded as a standard
health value.

PR-#1146

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3036741 permits only the canonical health tones and requires external values to be normalized
before storage or downstream use. The changed overlay stores Degraded, and the changed insights
path propagates that value into comparisons and API output while the node model exposes it as an
unconstrained string.

Rule 3036741: Use only the standard HealthLevel tones and normalize external values
internal/server/gitops_handlers.go[217-222]
pkg/gitops/tree/types.go[37-46]
pkg/gitops/insights/insights.go[1153-1165]

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

## Issue description
GitOps resource health is stored and exposed using values such as `Degraded` and `Healthy` instead of the canonical health tones, allowing downstream consumers to depend on a parallel vocabulary.

## Fix Focus Areas
- internal/server/gitops_handlers.go[217-222]
- pkg/gitops/tree/types.go[37-46]
- pkg/gitops/insights/insights.go[1153-1165]

## Recommended Fix
Normalize every controller and Radar health value to the standard tone vocabulary before assigning it to the public health field. If the original controller verdict must remain available, preserve it in a distinctly named raw-status field and update summaries, issue classification, and frontend rendering to consume only the normalized tone.

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



Informational

7. Cloud users miss health help ✓ Resolved 🔗 Cross-repo conflict ≡ Correctness
Description
GitOpsHealthSourceNotice renders its Why link only when the host supplies healthDocsUrl, but
radar-hub-web's GitOpsDetailLayout invocation supplies neither that prop nor an equivalent
explanation. Embedded Radar Hub users therefore receive the new Argo health notice without the
promised route to its technical details.
Code

packages/k8s-ui/src/components/gitops/GitOpsHealthSourceNotice.tsx[R63-66]

+            {docsUrl && (
+              <>
+                {' '}
+                <a href={docsUrl} target="_blank" rel="noreferrer" className="underline decoration-theme-border underline-offset-2 hover:text-theme-text-primary">
Relevance

● Weak

The PR explicitly designs embedded hosts to omit host-specific documentation links; this is
intentional integration behavior.

PR-#719

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new shared component conditionally omits Why when docsUrl is absent. Radar Hub Web consumes
GitOpsDetailLayout directly but its complete prop list around that invocation does not provide
healthDocsUrl, while the PR's standalone host does.

packages/k8s-ui/src/components/gitops/GitOpsHealthSourceNotice.tsx[60-70]
packages/k8s-ui/src/components/gitops/GitOpsDetailLayout.tsx[142-146]
web/src/components/gitops/GitOpsView.tsx[634-641]
External repo: skyhook-dev/radar-hub-web, src/pages/fleet/GitOpsDetailPage.tsx [463-495]

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 shared GitOps layout requires its host to provide `healthDocsUrl`, but Radar Hub Web has not been coordinated to do so, leaving its new notice without a Why link.

## Fix Focus Areas
- packages/k8s-ui/src/components/gitops/GitOpsHealthSourceNotice.tsx[63-68]
- packages/k8s-ui/src/components/gitops/GitOpsDetailLayout.tsx[142-146]
- /cross_repos/radar-hub-web/src/pages/fleet/GitOpsDetailPage.tsx[463-495]

## Recommended Fix
After publishing the health section, pass its canonical URL as `healthDocsUrl` from Radar Hub Web's `GitOpsDetailLayout` invocation and add embedded-view coverage for the link.

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


Grey Divider

Context sources
✅ Compliance rules (platform): 44 rules
✅ Cross-repo context — repo relationships
  Explored: repo: skyhook-dev/radar-docs (sha: 82731c0c)
  Explored: repo: skyhook-dev/radar-e2e (sha: e4918cc2)
  Explored: repo: skyhook-dev/radar-hub-web (sha: 2b5189f1)
Review mode: 🧠 Deep: This is a broad, behavior-changing cross-stack PR spanning backend health resolution, RBAC and remote-cluster isolation, shared tree/insight semantics, UI provenance, and demo infrastructure, with many independent logic paths where redundant review can catch subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/server/gitops_handlers.go
Comment thread pkg/gitops/tree/builder_test.go Outdated
Comment thread pkg/gitops/insights/insights.go
Comment thread internal/server/gitops_handlers.go
Comment thread pkg/gitops/insights/insights.go
Comment thread web/src/components/gitops/GitOpsView.tsx Outdated
…nings; docs link that exists

The degraded-resources count and the Warning-event lead no longer stack on
a per-resource finding of any severity. A Radar topology read with no
engine reason is an observation, not a verdict: warning tier, and no Issue
at all when Argo calls the app Healthy. The notice's Why link points at
Argo's own 3.0 upgrade note until radar-docs carries the section.

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

Stale Bugbot comment from a previous run.

Comment thread pkg/gitops/insights/insights.go Outdated
… page's requests

The tree and insights requests of one page load (and their 2s polls while
a sync runs) each composed the cluster's issues. One 5s memo per user and
namespace set now serves both; cleared on context switch with the other
post-switch caches.

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a28e1e6. Configure here.

Comment thread packages/k8s-ui/src/components/gitops/GitOpsHealthSourceNotice.tsx
@nadaverell
nadaverell merged commit 9dffdfb into main Sep 13, 2026
9 checks passed
@nadaverell
nadaverell deleted the feat/argo3-resource-health branch September 13, 2026 22:28
nadaverell added a commit that referenced this pull request Sep 14, 2026
…, with or without a token (#1751)

Fast-follow to #1743. On Argo CD 3 the Application object no longer
carries per-resource health, so Radar showed its own read of each
resource, labelled as Radar's. argocd-server still has Argo's verdicts
in its own cache. When Radar can reach it — with the integration
connected, or with no setup at all on installs that serve reads
anonymously — it now shows those instead, and the page looks exactly
like it does on Argo CD 2.x: Argo's per-resource health, no notice, no
"Found by Radar" markers. When it can't, the page says why and how to
get there.

## What changes

**Argo's verdicts from argocd-server.** For an Application in `appTree`
mode that deploys to this cluster, Radar asks the Argo CD API server for
the Application (`GET /api/v1/applications/{name}`, no refresh) to
establish identity and mode, then reads per-resource health from its
`resource-tree`. The tree is the source rather than the GET's own
inlined health because Argo CD 3.0's GET-side inference keys the tree
cache by bare app name, which can hand an Application outside Argo's
namespace a same-named app's verdicts; the tree is scoped correctly on
every 3.x. The answer is accepted only when its UID matches the local
Application, so a different Argo install serving a same-named app is
ignored.

**Argo's answer is the whole answer.** When it arrives, declared nodes
take Argo's health with `healthSource: controllerApi`, Radar's topology
fill is cleared for kinds Argo has no check for, the issues-engine
overlay doesn't run, and the notice doesn't show.
`resourceHealthFromApi` on the insights summary and `healthFromApi` on
the tree tell consumers so.

**With a token, or without one.** With the Argo CD integration connected
(Settings → Argo CD), the existing client and token are used. With
nothing configured, this read path runs the existing argocd-server
discovery and asks without credentials. A client that carries no token
skips TLS verification, so the probe and the credential-free read reach
the default in-cluster argocd-server with its self-signed certificate;
every client that sends the token still honours `argoCdInsecureTls`, and
a certificate the token-bearing client can't verify is reported with
that setting named. Radar never sends credentials it wasn't given;
refusals are cached per app for 15s, and discovery or a failed connect
is throttled to once per retry interval, so a polling detail page never
hammers an install that says no.

**A tokenless connection counts only once it answers.** A reachable
argocd-server that refuses reads is not an integration: it stays "Not
connected" in Settings and never lights the diff. Once an anonymous read
has succeeded (`users.anonymous.enabled` with a default role that can
read applications), that connection *is* the integration — Settings
shows "Connected · no token needed" with a line explaining when a token
or URL still matters, and the Git-rendered diff and revision metadata
work over it too.

**A configured connection that doesn't deliver says so.** With the
integration set up and no usable answer — token rejected, requires a
token, server unreachable, request timed out, a different install
answering, unknown app, or anything else — the insights summary carries
the reason (`resourceHealthApiError`) and the notice leads with it:
"Radar couldn't read Argo's per-resource health from your Argo CD
server: the server couldn't be reached. Check Argo CD in Settings." The
Argo CD status carries the same failure in plain words, so the Settings
section the notice links to shows what's wrong.

**The way out, from the page.** When Radar found nothing to mark on a
Degraded app, the notice offers connecting Radar to argocd-server first
and the `persist` knob second. The Settings clause is host-injected
through the layout's `onOpenSettings`: standalone Radar renders it as a
link that opens the Argo CD section (and focuses the token field when
nothing is set up); an embedded host without that page gets only the
Argo-side knob. Connecting in Settings refreshes the GitOps queries, so
the page the user came from shows Argo's verdicts when they get back.
The Argo CD Settings section names per-resource health alongside the
diff.

**Docs.** `docs/gitops.md` describes the three health sources and the
connection semantics; radar-docs counterpart is
skyhook-dev/radar-docs#95, to merge together.

## Testing
- `go test` in both modules (client; manager: cache, negative cache,
throttle for unconfigured and configured failures, anonymous-connection
gating, TLS modes, probe-failure reporting; overlay: identity, mode,
remote, clearing Radar fill, failure carrying, reason phrasing; status
endpoint), `make tsc`, k8s-ui vitest (3684).
- Live on a kind cluster with Argo CD v3.5.2:
- nothing configured, anonymous disabled → 401 → Radar's read with
markers, Settings "Not connected", no managed-resources calls; first
open 0.22s from a laptop (port-forward + probe), repeats ~1ms.
- nothing configured, `users.anonymous.enabled` + `policy.default:
role:readonly` → Argo's verdicts with `controllerApi` (Widget Degraded
via its Lua check with Argo's message, Deployment Progressing, Gadget no
verdict), no notice, Settings connected + anonymous, diff capability
lit.
- integration connected with a token → same verdicts, no notice, no
markers.
- configured URL unreachable → notice with "the server couldn't be
reached" and the Settings link landing on the Argo CD section; first
open 0.05s, repeats throttled.
- Anonymous negative case against a real Argo CD 3.2.6 install with
defaults: 401, not connected.
- Plain-GET vs resource-tree behaviour verified from Argo CD source
(v3.0.0 vs v3.2.6 `inferResourcesStatusHealth`).
- Visual: notice (error and findings states) and the Argo CD Settings
section rendered and checked on a dev build.

## Notes
- The anonymous connection is discovered by the Argo CD 3 health read,
so on an Argo CD 2.x install (inline health) nothing triggers it and the
diff still needs a token there — by design for now; making the
capabilities probe try anonymously would open a port-forward on every
app open for every unconfigured user.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds outbound calls to argocd-server on GitOps detail (including
unconfigured anonymous discovery) and changes when the integration
counts as connected; core auth paths are extended but bounded by
caching, UID checks, and existing probe guards.
> 
> **Overview**
> On **Argo CD 3** (`appTree` mode), Radar can now show **Argo’s
per-resource health from argocd-server** instead of defaulting to
Radar’s own cluster read. The GitOps detail path calls
**`ApplicationHealthCached`**: GET the Application for UID/mode, then
**`resource-tree`** when health isn’t persisted on the CR (tree is
authoritative in appTree; mis-scoped GET inference is not used as
fallback). Successful answers overlay the tree with **`controllerApi`**
health, skip the Radar issues overlay, and hide the “Found by Radar”
notice.
> 
> The Argo CD manager also supports **credential-free reads** when
nothing is configured: discover argocd-server, probe/read without a
token (tokenless clients skip TLS verify for self-signed in-cluster
certs). A reachable server that **401s is not “connected”**; after a
successful anonymous read, Settings shows **“Connected · no token
needed”** and diff/revision metadata work like a normal integration.
Health fetches are **15s cached** (including failures), with **throttled
retries** so 2s polling doesn’t hammer refusals or failed discovery.
> 
> When integration is configured but health can’t be fetched, the UI
surfaces **`resourceHealthApiError`** and a notice with **Settings**
remediation; anonymous/unconfigured failures fall through to Radar’s
read quietly. Settings gains probe **failure reasons**, anonymous
status, token focus on connect, and **GitOps query invalidation** after
saving Argo CD config.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
94f71ca. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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