Skip to content

Show the Pod/template comparison in resource drawers - #2198

Open
nadaverell wants to merge 10 commits into
mainfrom
feat/drawer-template-divergence
Open

nadaverell wants to merge 10 commits into
mainfrom
feat/drawer-template-divergence

Conversation

@nadaverell

@nadaverell nadaverell commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Why

#2154 added templateDivergence: how a Pod's live spec differs from the template that created it, plus the objects that could have changed it. Until now only MCP agents saw it. Someone opening a Pod or Deployment in Radar saw the 16Mi limit and nothing saying the template asked for 256Mi.

What changes

  • Resource API: /api/resources/{kind}/{namespace}/{name} returns an optional templateDivergence for Pods, ReplicaSets, Deployments and Jobs. It is the same comparison, computed as the caller, that MCP get_resource returns. It is omitted when nothing differs.
  • Drawers: Pod and workload drawers show a Live spec differs from template card, in both the drawer and the expanded view. It starts collapsed to one line: the first difference, how many more, and how many objects could have caused it, e.g. memory limit 16Mi (template 256Mi) · +1 more · 4 possible sources. The header is a button with aria-expanded. Opened, the card has:
    • one plain sentence (for a workload, "2 of 2 Pods don't match their template", with the example Pod linked);
    • the differences;
    • Could have changed it, with the candidate objects as links, each with when it was created ("created 8m ago" beside "created 160d ago"), in the same order as the issue fact. The list is deliberately not sorted newest first; the times are shown as facts;
    • any sources Radar could not inspect;
    • a footnote that a listed source only matches the Pod.
      An opened card stays open for that resource for the rest of the browser session. The agent-facing sentences stay out of it.
  • TemplateDivergence gains structured sourceRefs (with each candidate's creation time), sourceGaps and sourcesNotNamed so the card can link objects instead of parsing prose.
  • Version skew: Hub bundles radar-app against older Radars. The field is optional and the card renders nothing without it. There is no new endpoint, so no feature flag.

Screenshots

Captured on an EKS test cluster, with a webhook rewriting memory to 16Mi and a namespace LimitRange.

Deployment drawer, collapsed (light and dark):

Collapsed: Deployment drawer
Collapsed: Deployment drawer, dark

Opened: the Deployment card, and the LimitRange case (dark):

Expanded: Deployment card
Expanded: LimitRange case, dark

Clicking a candidate opens it; for example, the webhook opens the MutatingWebhookConfiguration drawer.

Tests

  • Server: the resource API includes the comparison for a differing Pod and its ReplicaSet (2 compared, 1 differing). It omits it for a matching Pod.
  • k8s-ui:
    • the card renders nothing without the field or with no differences;
    • it starts collapsed; the one-line summary for one difference, several, with and without sources; the button toggles aria-expanded and the open state persists only for the same resource;
    • the plain workload and Pod sentences, links, +N more and gaps all render;
    • the agent sentences are excluded;
    • the drawer shows the card only when the response carries it.
  • issues: structured sourceRefs and sourceGaps.

Known limits

Possible follow-up

  • Open the card automatically when an OOMKill or similar failure hits a field that differs from the template. Not done here: the card is always collapsed by default.

@nadaverell
nadaverell requested a review from hisco as a code owner October 18, 2026 11:24
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Show Pod/template differences in resource drawers

âœĻ Enhancement 🧊 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

â€Ē Exposes Pod/template differences through the resource API, using the caller-aware comparison
 already available to MCP.
â€Ē Shows differences, linked candidate sources, and inspection gaps in Pod and workload views.
â€Ē Keeps the field optional for older Radars and adds server and UI coverage.
Diagram

graph TD
  Provider["Comparison provider"] --> Helper["Shared server helper"] --> API["Resource API"] --> Query["Resource query"] --> View["Workload view"] --> Card["Divergence card"] --> Links["Resource links"]
  Helper --> MCP["MCP context"]
Loading
High-Level Assessment

Reusing the existing caller-aware comparison and optional resource-response field is appropriate. A dedicated endpoint would add a request and another access-control path without providing a clear benefit for the drawer.

Files changed (15) +345 / -17

Enhancement (10) +185 / -12
provider_pod_template.goRetain structured candidate coverage and overflow +7/-1

Retain structured candidate coverage and overflow

â€Ē Keeps uninspectable-source messages and the count of unnamed candidates alongside the existing source sentences and references.

internal/issues/provider_pod_template.go

template_divergence_context.goPopulate structured divergence sources +22/-8

Populate structured divergence sources

â€Ē Adds candidate object references, inspection gaps, and the unnamed-candidate count to TemplateDivergence for UI consumption.

internal/issues/template_divergence_context.go

server.goInclude comparison in resource details +1/-1

Include comparison in resource details

â€Ē Wraps the existing resource response with an optional, caller-aware templateDivergence field.

internal/server/server.go

template_divergence.goShare caller-aware comparison across endpoints +30/-0

Share caller-aware comparison across endpoints

â€Ē Defines the optional resource-detail field and a helper that computes divergence for supported runtime objects using the caller's access checks.

internal/server/template_divergence.go

TemplateDivergenceCard.tsxAdd human-readable template divergence card +76/-0

Add human-readable template divergence card

â€Ē Displays comparison counts and differences, linked example Pods and candidate objects, inspection gaps, and a caution that matching sources are not proven causes. Renders nothing when no differences are supplied.

packages/k8s-ui/src/components/workload/TemplateDivergenceCard.tsx

WorkloadView.tsxPlace comparison card in workload views +21/-2

Place comparison card in workload views

â€Ē Accepts the optional comparison and adds its card to drawer content and the expanded overview, with resource navigation callbacks.

packages/k8s-ui/src/components/workload/WorkloadView.tsx

core.tsType the optional comparison response +18/-0

Type the optional comparison response

â€Ē Defines TemplateDivergence for the UI and adds it as an optional field on resource-detail responses, including structured candidate sources.

packages/k8s-ui/src/types/core.ts

types.goExtend shared divergence JSON with source details +7/-0

Extend shared divergence JSON with source details

â€Ē Adds optional candidate references, inspection gaps, and unnamed-candidate count while retaining the existing source prose.

pkg/resourcecontext/types.go

client.tsExpose divergence from the resource query +1/-0

Expose divergence from the resource query

â€Ē Passes the optional templateDivergence field from the resource response to consumers of useResource.

web/src/api/client.ts

WorkloadView.tsxForward divergence to the shared workload view +2/-0

Forward divergence to the shared workload view

â€Ē Reads the comparison from the resource response and supplies it to the k8s-ui workload view.

web/src/components/workload/WorkloadView.tsx

Refactor (1) +2 / -4
ai_handlers.goReuse server comparison helper for MCP +2/-4

Reuse server comparison helper for MCP

â€Ē Routes AI resource-context comparison through the same caller-aware helper used by the resource API.

internal/server/ai_handlers.go

Tests (3) +155 / -0
template_divergence_context_test.goAssert structured inspection gaps +3/-0

Assert structured inspection gaps

â€Ē Verifies that a divergence reports an unavailable webhook-configuration source as a structured gap, with no candidate references.

internal/issues/template_divergence_context_test.go

template_divergence_test.goTest resource API comparison behavior +83/-0

Test resource API comparison behavior

â€Ē Checks divergence for a differing Pod and its ReplicaSet, omission for a matching Pod, and preservation of the existing resource payload.

internal/server/template_divergence_test.go

TemplateDivergenceCard.test.tsxTest divergence card and drawer visibility +69/-0

Test divergence card and drawer visibility

â€Ē Covers absent or empty comparisons, Pod and workload wording, candidate information and gaps, exclusion of agent-facing prose, and conditional drawer rendering.

packages/k8s-ui/src/components/workload/TemplateDivergenceCard.test.tsx

Documentation (1) +3 / -1
mcp.mdDocument resource API and drawer availability +3/-1

Document resource API and drawer availability

â€Ē Notes that the Pod/template comparison is also returned by the resource API and shown in Radar resource drawers.

docs/mcp.md

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

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Remediation recommended

1. Early resource views omit comparisons 🐞 Bug ≡ Correctness
Description
templateDivergenceFor creates a provider from the promoted global cache instead of using the cache
that supplied the resource. When a typed resource is readable during initial synchronization but the
cache has not been promoted, its detail response omits the comparison; a cache replacement between
the two reads can instead pair a resource with comparison data from another cache generation.
Code

internal/server/template_divergence.go[R27-29]

+	return issues.NewCacheProvider().TemplateDivergence(o, s.issueClusterScopedAccess(r), s.issueRelatedResourceAccess(r), func(verb, group, resource, namespace string) bool {
+		return s.canRead(r, group, resource, namespace, verb)
+	})
Evidence
gateResourceRead can select a ready mid-sync cache, whereas NewCacheProvider returns nil until a
promoted cache exists. Cache promotion and reset can also replace that global handle after the
handler has selected and read its resource.

internal/server/server.go[2807-2846]
internal/server/server.go[2979-2989]
internal/issues/provider.go[46-62]
internal/k8s/cache.go[645-658]
internal/k8s/cache.go[667-690]

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 resource GET can read from a syncing or previously selected cache, while divergence independently reads the current promoted cache.
## Fix Focus Areas
- internal/server/server.go[3314-3314]
- internal/server/template_divergence.go[20-29]
## Recommended Fix
Pass the cache selected for the resource read into divergence computation so both parts of the response use the same cache handle, including during progressive startup.

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


2. Large namespaces slow workload details 🐞 Bug âžđ Performance
Description
handleGetResource computes divergence synchronously for each applicable detail response, including
requests that will return no difference. A Deployment read scans namespace-wide ReplicaSets and Pods
before applying the 200-Pod comparison cap, so detail refetches following resource-watch
invalidations repeat that work in large namespaces.
Code

internal/server/server.go[3314]

+	s.writeJSON(w, resourceDetailResponse{ResourceWithRelationships: response, TemplateDivergence: s.templateDivergenceFor(r, resource)})
Evidence
The newly unconditional response enrichment invokes workload comparison. That comparison lists all
namespace ReplicaSets for Deployments and all namespace Pods for supported workloads, then filters
and truncates; the resource hook is invalidated by watch updates.

internal/server/template_divergence.go[20-29]
internal/issues/template_divergence_context.go[46-73]
internal/issues/template_divergence_context.go[76-106]
web/src/api/client.ts[2732-2750]
web/src/App.tsx[1165-1168]

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

## Issue description
Ordinary workload detail requests now repeat namespace-wide scans before responding, and the comparison cap does not bound those scans.
## Fix Focus Areas
- internal/server/server.go[3314-3314]
- internal/issues/template_divergence_context.go[53-115]
## Recommended Fix
Use an owner-indexed lookup or another bounded, reusable comparison path for resource details; ensure frequent detail refetches do not rescan every ReplicaSet and Pod in the namespace.

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


3. Job full view never shows the template comparison 🐞 Bug ≡ Correctness
Description
WorkloadView passes divergenceCard to the expanded view only through overviewIntro →
introContent, which is skipped when expandedOverview is present. For core batch Jobs, the web
host supplies BatchExecutionFullscreen as expandedOverview, so a server-provided
templateDivergence appears in the drawer but not when the user opens the full view.
Code

packages/k8s-ui/src/components/workload/WorkloadView.tsx[R947-952]

+  const overviewIntro = divergenceCard || hostOverviewIntro ? (
+    <div className="space-y-4">
+      {hostOverviewIntro}
+      {divergenceCard}
+    </div>
+  ) : undefined
Evidence
expandedOverview comes from renderExpandedOverview at line 938. The `effectiveTab === 'overview'
&& expandedOverview branch at line 1323 renders {expandedOverview}` instead of the overview
renderer that receives introContent={overviewIntro} at line 1367. The web host returns
BatchExecutionFullscreen for core batch Jobs because they support batch execution, while the
server computes divergence for Jobs.

packages/k8s-ui/src/components/workload/WorkloadView.tsx[1323-1331]
packages/k8s-ui/src/components/workload/WorkloadView.tsx[1366-1368]
web/src/components/workload/WorkloadView.tsx[1188-1204]
web/src/components/workload/WorkloadView.tsx[143-150]
packages/k8s-ui/src/components/workload/WorkloadView.tsx[1319-1330]
packages/k8s-ui/src/components/workload/WorkloadView.tsx[1365-1368]
web/src/components/workload/WorkloadView.tsx[132-148]
web/src/components/workload/WorkloadView.tsx[1187-1205]
internal/issues/template_divergence_context.go[46-56]

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

## Issue description
Expanded Jobs receive a template divergence comparison, but their custom batch overview bypasses the `introContent` containing its card.
## Fix Focus Areas
- packages/k8s-ui/src/components/workload/WorkloadView.tsx[939-952]
- packages/k8s-ui/src/components/workload/WorkloadView.tsx[1319-1331]
## Recommended Fix
Render `divergenceCard` with suitable padding before `{expandedOverview}` in the `effectiveTab === 'overview' && expandedOverview` branch. Keep it in the normal overview without rendering it twice in either branch, and add a test for an expanded Job with `renderExpandedOverview` set.

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


View medium (1)
4. Cloud fleet drawers omit comparisons ⊘ Outdated
Description
The new WorkloadView card requires a templateDivergence prop, but radar-hub-web’s
EmbeddedWorkloadView fetches the resource-detail response without forwarding that field. When
users open a Pod or workload in a Hub Web fleet application or related-resource drawer, the
comparison returned by Radar is ignored and the card does not appear.
Code

packages/k8s-ui/src/components/workload/WorkloadView.tsx[194]

+  templateDivergence?: TemplateDivergence
Evidence
Radar adds the comparison to the resource-detail response and the shared view renders it only when
given the new prop. Hub Web uses that response and view for both fleet workloads and
related-resource drawers but does not pass the prop.

radar -> radar-hub-web
internal/server/template_divergence.go[12-18]
packages/k8s-ui/src/components/workload/WorkloadView.tsx[192-194]
packages/k8s-ui/src/components/workload/WorkloadView.tsx[939-945]
External repo: skyhook-dev/radar-hub-web, src/pages/fleet/EmbeddedWorkloadView.tsx [39-52]
External repo: skyhook-dev/radar-hub-web, src/pages/fleet/EmbeddedWorkloadView.tsx [215-238]
External repo: skyhook-dev/radar-hub-web, src/pages/fleet/SingleAppView.tsx [225-255]

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

## Issue description
Hub Web's fleet workload view does not pass the new resource-detail comparison to the shared WorkloadView, so its fleet drawers cannot show the card.
## Fix Focus Areas
- /cross_repos/radar-hub-web/src/pages/fleet/EmbeddedWorkloadView.tsx[225-238]
- packages/k8s-ui/src/components/workload/WorkloadView.tsx[192-194]
## Recommended Fix
Coordinate a radar-hub-web update when adopting the new k8s-ui release: pass `resourceQuery.data?.templateDivergence` to `WorkloadView`, and verify it renders in both the fleet workload view and related-resource drawer.

ⓘ 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 move or hide resolved findings to focus on what still needs action

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/server/template_divergence.go
Comment thread internal/server/server.go
Comment thread packages/k8s-ui/src/components/workload/WorkloadView.tsx
Comment thread packages/k8s-ui/src/components/workload/WorkloadView.tsx
@nadaverell
nadaverell force-pushed the feat/drawer-template-divergence branch from 5ccc6c3 to f457b11 Compare October 18, 2026 11:32
@cursor

cursor Bot commented Oct 18, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

1 similar comment
@cursor

cursor Bot commented Oct 18, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@nadaverell
nadaverell force-pushed the feat/drawer-template-divergence branch 3 times, most recently from 723b3c1 to ee87e94 Compare October 18, 2026 14:47
@cursor

cursor Bot commented Oct 18, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@nadaverell
nadaverell force-pushed the feat/drawer-template-divergence branch from ee87e94 to e64b66e Compare October 18, 2026 15:04
Comment thread packages/k8s-ui/src/components/workload/TemplateDivergenceCard.tsx Fixed
Comment thread packages/k8s-ui/src/components/workload/TemplateDivergenceCard.tsx Fixed
@cursor

cursor Bot commented Oct 18, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

nadaverell added a commit that referenced this pull request Oct 18, 2026
# Fix data race in the helm target-namespace test server

`targetNamespaceActionConfig` (added in #2080) starts an `httptest`
server whose handler reads and writes a shared `objects` map and appends
to a `writes` slice. The kube client sends requests concurrently, so `go
test` sometimes died with `fatal error: concurrent map read and map
write`, as in the Backend job on #2198.

The handler now holds a `sync.Mutex` while it touches that state. Only
the test changes.

**Verification** (`go test -race -count=20 ./internal/helm -run
TargetNamespace`):
- Before: FAIL, with 3 `WARNING: DATA RACE` reports.
- After: `ok` (20.2s), no race reports.
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.

2 participants