@skyhook-io/k8s-ui 1.10.4 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/package.json +5 -5
  2. package/src/components/applications/ApplicationDetail.test.tsx +46 -0
  3. package/src/components/applications/ApplicationDetail.tsx +14 -15
  4. package/src/components/issues/IssuesView.tsx +9 -0
  5. package/src/components/issues/diagnostic.ts +3 -0
  6. package/src/components/issues/issues.test.ts +11 -0
  7. package/src/components/issues/severity.ts +3 -0
  8. package/src/components/issues/types.ts +3 -0
  9. package/src/components/resources/ResourcesSidebar.tsx +1 -1
  10. package/src/components/resources/ResourcesView.tsx +259 -30
  11. package/src/components/resources/index.ts +2 -0
  12. package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
  13. package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
  14. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
  15. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +109 -9
  16. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +8 -4
  17. package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
  18. package/src/components/resources/renderers/EventRenderer.tsx +10 -18
  19. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
  20. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
  21. package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
  22. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
  23. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
  24. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
  25. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
  26. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
  27. package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
  28. package/src/components/resources/renderers/index.ts +4 -0
  29. package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
  30. package/src/components/resources/renderers/velero-cells.tsx +128 -9
  31. package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
  32. package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
  33. package/src/components/resources/resource-utils-cnpg.test.ts +527 -1
  34. package/src/components/resources/resource-utils-cnpg.ts +373 -56
  35. package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
  36. package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
  37. package/src/components/resources/resource-utils-velero.test.ts +237 -0
  38. package/src/components/resources/resource-utils-velero.ts +214 -22
  39. package/src/components/resources/resource-utils.ts +70 -3
  40. package/src/components/shared/ResourceActionsBar.tsx +3 -1
  41. package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
  42. package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
  43. package/src/components/topology/K8sResourceNode.tsx +11 -0
  44. package/src/components/topology/TopologyGraph.tsx +85 -5
  45. package/src/components/trace/ReachabilityGraph.tsx +517 -0
  46. package/src/components/trace/ReachabilityView.tsx +1173 -0
  47. package/src/components/trace/TracePanel.test.ts +172 -0
  48. package/src/components/trace/TracePanel.tsx +558 -0
  49. package/src/components/trace/TraceSummary.test.ts +213 -0
  50. package/src/components/trace/TraceSummary.tsx +168 -0
  51. package/src/components/trace/inClusterSummary.test.ts +98 -0
  52. package/src/components/trace/inClusterSummary.ts +81 -0
  53. package/src/components/trace/index.ts +19 -0
  54. package/src/components/trace/podReach.test.ts +71 -0
  55. package/src/components/trace/podReach.ts +37 -0
  56. package/src/components/trace/probe-display.test.ts +107 -0
  57. package/src/components/trace/probe-display.ts +37 -0
  58. package/src/components/trace/problemRows.test.ts +107 -0
  59. package/src/components/trace/reachFixtures.ts +261 -0
  60. package/src/components/trace/reachGraphModel.test.ts +1423 -0
  61. package/src/components/trace/reachGraphModel.ts +1521 -0
  62. package/src/components/trace/reachInspector.test.ts +892 -0
  63. package/src/components/trace/reachInspector.ts +797 -0
  64. package/src/components/trace/reachMarks.test.ts +698 -0
  65. package/src/components/trace/reachMarks.ts +623 -0
  66. package/src/components/trace/reachOrigins.test.ts +392 -0
  67. package/src/components/trace/reachOrigins.ts +449 -0
  68. package/src/components/trace/reachVerdict.test.ts +537 -0
  69. package/src/components/trace/reachVerdict.ts +601 -0
  70. package/src/components/trace/traceFingerprint.test.ts +214 -0
  71. package/src/components/trace/traceFingerprint.ts +122 -0
  72. package/src/components/trace/traceToSubgraph.test.ts +623 -0
  73. package/src/components/trace/traceToSubgraph.ts +463 -0
  74. package/src/components/trace/types.ts +414 -0
  75. package/src/components/ui/Badge.tsx +36 -0
  76. package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
  77. package/src/components/ui/InClusterConsentDialog.test.tsx +83 -0
  78. package/src/components/ui/InClusterConsentDialog.tsx +141 -0
  79. package/src/components/ui/index.ts +1 -0
  80. package/src/components/workload/WorkloadView.tsx +58 -5
  81. package/src/components/workload/index.ts +1 -1
  82. package/src/components/workload/reachabilityTab.test.ts +47 -0
  83. package/src/index.ts +5 -0
  84. package/src/theme/components.css +64 -0
  85. package/src/theme/variables.css +5 -0
  86. package/src/types/core.ts +8 -0
  87. package/src/utils/api-resources.ts +2 -0
  88. package/src/utils/application-history.test.ts +1 -1
  89. package/src/utils/applications.test.ts +63 -1
  90. package/src/utils/applications.ts +27 -0
  91. package/src/utils/inClusterConsent.test.ts +109 -0
  92. package/src/utils/inClusterConsent.ts +72 -0
  93. package/src/utils/index.ts +1 -0
  94. package/src/utils/navigation.test.ts +61 -0
  95. package/src/utils/navigation.ts +11 -9
  96. package/src/utils/pluralize.test.ts +80 -1
  97. package/src/utils/pluralize.ts +34 -0
  98. package/src/utils/resource-icons.test.ts +33 -0
  99. package/src/utils/resource-icons.ts +65 -2
@@ -0,0 +1,414 @@
1
+ // Mirrors internal/trace types.go on the Go side. Keep field names in sync
2
+ // when extending - JSON tags on Go must match these property names exactly.
3
+
4
+ export type Verdict = 'healthy' | 'degraded' | 'broken' | 'unknown'
5
+ export type FindingSeverity = 'critical' | 'warning' | 'info'
6
+
7
+ export interface ResourceRef {
8
+ group?: string
9
+ kind: string
10
+ namespace?: string
11
+ name: string
12
+ }
13
+
14
+ export interface Finding {
15
+ code: string
16
+ severity: FindingSeverity
17
+ message: string
18
+ /** Parsed root cause (plain English) when the detector classified the
19
+ * failure - e.g. "Image not found in registry", "Exit code 137 (OOMKilled)".
20
+ * Empty for generic detections; UI renders message in that case. */
21
+ cause?: string
22
+ /** Next-step guidance paired with cause - e.g. "Push the image or fix the
23
+ * reference", "Increase the container memory limit". Empty when not parsed. */
24
+ action?: string
25
+ remediation?: string
26
+ command?: string
27
+ /** Renderable chip strip carrying structured detail a sentence can't hold -
28
+ * today the egress-note's allowed destinations. On-chip text is jargon-free;
29
+ * the jargon lives in each chip's title. */
30
+ chips?: FindingChip[]
31
+ /** Tiny field label printed before the chip strip ("Allowed out"). Empty when
32
+ * the chips read on their own (deny-all, anywhere). */
33
+ chipsLabel?: string
34
+ /** Quiet tertiary caveats printed under the chips (declared-vs-enforced, the
35
+ * gated DNS observation). Kept subordinate to the inbound verdict. */
36
+ chipNotes?: string[]
37
+ /** The specific object this finding is about when it differs from the hop it
38
+ * hangs on - a pod-fanout hop carries one finding per code but the culprit is
39
+ * a named Pod. Empty = the hop's own resource is the subject. */
40
+ resource?: ResourceRef
41
+ }
42
+
43
+ /** One tag in a Finding's chip strip. Tone 'accent' renders the info-blue
44
+ * variant (anywhere / deny-all); omitted renders the muted variant. */
45
+ export interface FindingChip {
46
+ text: string
47
+ title?: string
48
+ tone?: 'accent' | ''
49
+ }
50
+
51
+ /** Known hop.meta keys stamped by the Go tracer (buildPodsHop et al. in
52
+ * internal/trace). The wire is an open map - property names must match the Go
53
+ * meta keys exactly; unknown keys pass through the index signature. */
54
+ export interface HopMeta {
55
+ /** Count of endpoints the dataplane actually routes to. For a
56
+ * publishNotReadyAddresses Service this is the PUBLISHED endpoint count
57
+ * (every selected pod, readiness not required) - NOT the kubelet-ready
58
+ * count, so it must never render as "N/M ready" there. */
59
+ ready?: number
60
+ /** Pods matched by the Service selector. */
61
+ selected?: number
62
+ /** Set when the Service publishes endpoints regardless of pod readiness
63
+ * (spec.publishNotReadyAddresses) - the signal that meta.ready is a
64
+ * published count, not a readiness count. */
65
+ publishNotReadyAddresses?: boolean
66
+ /** 'pod-readiness' when endpoints were judged from pod state; 'unknown' when
67
+ * the pod lister failed and the count can't be trusted. */
68
+ endpointSource?: string
69
+ selectorless?: boolean
70
+ [key: string]: unknown
71
+ }
72
+
73
+ export interface Hop {
74
+ resource: ResourceRef
75
+ edge: string
76
+ findings: Finding[]
77
+ meta?: HopMeta
78
+ config?: HopConfig
79
+ probes?: ProbeResult[]
80
+ }
81
+
82
+ export type ProbeLayer = 'dns' | 'tcp' | 'tls' | 'http'
83
+ export type ProbeVantage = 'in-cluster' | 'local'
84
+ /**
85
+ * Which route a Service/Pod probe took. "data" = straight to the resource
86
+ * over the network (kube-proxy for ClusterIP, direct dial for PodIP).
87
+ * "apiserver" = through the K8s API server's proxy subresource. Empty when
88
+ * the question doesn't apply (DNS, HTTP to an Ingress hostname, etc.).
89
+ */
90
+ export type ProbePath = 'data' | 'apiserver'
91
+
92
+ /**
93
+ * Tone classifies a Result for the UI when ok alone is too coarse. It encodes
94
+ * the honest distinction between reaching a target, verifying what we asked
95
+ * for, and the server erroring:
96
+ * healthy - reached AND verified (HTTP 2xx; clean DNS/TCP/TLS)
97
+ * reached - reached an HTTP server, but the thing we asked for is unproven
98
+ * (3xx redirect not followed, or 4xx route/auth not what we hit).
99
+ * NOT a reachability failure; never renders as "verified" or
100
+ * "degraded".
101
+ * degraded - reached, but the server answered 5xx. Traffic passed.
102
+ * unhealthy - could not reach (transport failure).
103
+ * Empty falls back to (skipped → unknown, !ok → unhealthy, ok → healthy).
104
+ */
105
+ export type ProbeTone = 'healthy' | 'reached' | 'degraded' | 'unhealthy'
106
+
107
+ export interface ProbeResult {
108
+ layer: ProbeLayer
109
+ target: string
110
+ vantage: ProbeVantage
111
+ path?: ProbePath
112
+ ok: boolean
113
+ tone?: ProbeTone
114
+ skipped?: boolean
115
+ /** Structured skip class from the producer. 'informational' means the probe
116
+ * RAN and was deliberately kept out of the verdict - a disposition, never a
117
+ * coverage gap. */
118
+ skipClass?: string
119
+ reason?: string
120
+ latencyNs?: number
121
+ /** The Service/target port this result is for, when port-scoped (lets the UI group
122
+ * per-port results into one row per path across directions). */
123
+ port?: number
124
+ /** Declared L4 protocol when not TCP - two ServicePorts may share a number. */
125
+ protocol?: string
126
+ detail?: string
127
+ error?: string
128
+ /** What a DNS lookup resolved to. */
129
+ addresses?: string[]
130
+ /** Scope of those addresses. Describes the ADDRESS, not the journey: a public
131
+ * address does not prove a packet crossed the public internet. */
132
+ addressScope?: 'public' | 'private' | 'mixed'
133
+ /** WHO issued this probe. Radar's own process and a throwaway probe Job are
134
+ * both in-cluster over the data path but are different observers with
135
+ * different identities. Absent means 'radar'. */
136
+ source?: 'radar' | 'probe-job'
137
+ /** Copyable command to verify what this probe couldn't (non-HTTP port, HTTPS
138
+ * backend, an address only reachable in-cluster). Set at the skip site. */
139
+ command?: string
140
+ }
141
+
142
+ export interface PortMap {
143
+ name?: string
144
+ port: number
145
+ targetPort?: string
146
+ protocol?: string
147
+ appProtocol?: string
148
+ }
149
+
150
+ export interface ContainerPortRef {
151
+ container: string
152
+ name?: string
153
+ port: number
154
+ protocol?: string
155
+ }
156
+
157
+ /** One row of the Pods hop's per-pod reachability grid. Mirrors Go trace.PodStatus.
158
+ * A ready pod joins its probe result by name (apiserver path) or ip (data path);
159
+ * a not-ready pod has no probe and reads its cause from `reason`. */
160
+ export interface PodStatus {
161
+ name: string
162
+ ip?: string
163
+ ready: boolean
164
+ reason?: string
165
+ }
166
+
167
+ export interface ProbeRef {
168
+ container: string
169
+ type: string
170
+ port?: string
171
+ path?: string
172
+ scheme?: string
173
+ }
174
+
175
+ export interface BackendRef {
176
+ kind: string
177
+ name: string
178
+ namespace?: string
179
+ port?: string
180
+ }
181
+
182
+ export interface RouteRule {
183
+ hosts?: string[]
184
+ paths?: string[]
185
+ backends?: BackendRef[]
186
+ }
187
+
188
+ export interface GatewayListener {
189
+ name?: string
190
+ port: number
191
+ protocol?: string
192
+ hostname?: string
193
+ }
194
+
195
+ export interface HopConfig {
196
+ ports?: PortMap[]
197
+ serviceType?: string
198
+ clusterIP?: string
199
+ selector?: Record<string, string>
200
+ containerPorts?: ContainerPortRef[]
201
+ probes?: ProbeRef[]
202
+ hostnames?: string[]
203
+ rules?: RouteRule[]
204
+ tlsHosts?: string[]
205
+ listeners?: GatewayListener[]
206
+ addresses?: string[]
207
+ servedBy?: string
208
+ servedByTitle?: string
209
+ podIPs?: string[]
210
+ podNames?: string[]
211
+ pods?: PodStatus[]
212
+ podTotal?: number
213
+ /** What RUNS these Pods, resolved through their owner chain by the producer.
214
+ * Absent when the selected Pods have no single owner - a bare Pod, or two
215
+ * workloads behind one Service, where naming one would be a guess. */
216
+ workload?: ResourceRef
217
+ }
218
+
219
+ export type UnknownClass = 'by-design' | 'investigate'
220
+
221
+ export interface Trace {
222
+ subject: ResourceRef
223
+ upstreams: Hop[]
224
+ downstream: Hop[]
225
+ verdict: Verdict
226
+ brokenAt: number
227
+ reason?: string
228
+ /** Where RADAR ITSELF ran the inline probes from: 'in-cluster' when Radar is
229
+ * a Pod (its direct dials are real pod-to-pod traffic, issued with Radar's
230
+ * own identity) or 'local' when it runs on a workstation. Without this an
231
+ * in-cluster probe with no `source` is ambiguous between Radar's own dial and
232
+ * the throwaway Job's. */
233
+ runVantage?: 'in-cluster' | 'local'
234
+ /** Distinguishes the two flavors of the unknown verdict so the UI can
235
+ * pick the right visual register. 'by-design' covers configurations
236
+ * where auto-verification doesn't apply (e.g. selectorless Service);
237
+ * the banner renders as informational. 'investigate' covers cases
238
+ * where the trace tried and couldn't read state (RBAC, cache cold,
239
+ * lookup failure); the banner renders as warning. Absent on
240
+ * non-unknown verdicts. */
241
+ unknownClass?: UnknownClass
242
+ truncated?: boolean
243
+ // Coverage-honest projection (server-authoritative, mirrors internal/trace/coverage.go).
244
+ // These describe ONLY what was actually tested. Localization on a RouteResult is the
245
+ // behind-the-gate evidence, kept separate so it can never feed the headline/outcome.
246
+ coverage?: Coverage
247
+ routes?: RouteResult[]
248
+ notTested?: RouteSkip[]
249
+ brokenRoute?: ResourceRef
250
+ /** The single coverage-honest banner sentence - same string the MCP renders. */
251
+ headline?: string
252
+ /** The one finding that matters - cause, the named culprit, the next action -
253
+ * promoted from the path so a consumer reads the "why + what next" without
254
+ * walking the hop chain. Absent when there is nothing to diagnose (every
255
+ * route verified over real traffic). */
256
+ diagnosis?: Diagnosis
257
+ /** Faults on the DECLARED ENTRY POINTS (an Ingress or route that will never
258
+ * receive traffic). Deliberately separate from `diagnosis`: entries are
259
+ * parallel, so a dead front door must not condemn a Service that another
260
+ * entry still serves - but a verdict scoped to the tested path cannot see it
261
+ * either. Vantage-invariant; deduped against `diagnosis` by the producer. */
262
+ entryProblems?: EntryProblem[]
263
+ }
264
+
265
+ /** One declared entry point that cannot carry traffic, mirrors
266
+ * internal/trace/trace.go EntryProblem. */
267
+ export interface EntryProblem {
268
+ resource: ResourceRef
269
+ /** The human line; `detail` carries the raw controller cause for the hover. */
270
+ summary: string
271
+ detail?: string
272
+ severity: string
273
+ code?: string
274
+ action?: string
275
+ command?: string
276
+ }
277
+
278
+ /** Hoisted lead diagnosis, mirrors internal/trace/coverage.go Diagnosis. Every
279
+ * field is PROMOTED from a real finding (or coverage state), never synthesized.
280
+ * causeCode is the structured code, present ONLY for trustworthy structural
281
+ * fingerprints (missing-ref / svc:*); a pod-state code is omitted and summary
282
+ * carries the honest prose instead. */
283
+ export interface Diagnosis {
284
+ /** "fault" (something wrong, promoted from a finding) or "coverage" (a
285
+ * statement about what could be tested). The problem list renders faults
286
+ * only - a coverage sentence there restates the headline. */
287
+ class?: string
288
+ /** Severity of the finding this was promoted from. */
289
+ severity?: string
290
+ causeCode?: string
291
+ /** Which route this diagnosis explains, when attributable to exactly one.
292
+ * Absent means it describes the resource as a whole. The selected-path panel
293
+ * must not present another path's cause as this path's. */
294
+ route?: string
295
+ summary: string
296
+ culpritResource?: ResourceRef
297
+ nextAction?: string
298
+ command?: string
299
+ }
300
+
301
+ /** Per-route outcome (pairs with a confidence). Mirrors the Go Outcome* constants. */
302
+ export type RouteOutcome = 'verified' | 'reached' | 'server-error' | 'unreachable' | 'not-tested'
303
+ /** How an outcome was learned: 'real' = real-traffic path; 'indirect' = apiserver proxy
304
+ * (annotates, never sets the headline). '' for a static-known break (missing backend). */
305
+ export type RouteConfidence = 'real' | 'indirect' | ''
306
+ /** Skip impact: 'coverage' + 'vantage' are real gaps (cap green); 'benign' loses no coverage. */
307
+ export type SkipReasonClass = 'coverage' | 'benign' | 'vantage'
308
+
309
+ export interface Coverage {
310
+ tested: number
311
+ passed: number
312
+ failed: number
313
+ skipped: number
314
+ /** Routes known broken WITHOUT being dialled - from what is declared, or from
315
+ * current cluster state. Real breaks, but not test results, so they are
316
+ * reported apart from tested/passed/failed rather than as requests that
317
+ * failed. */
318
+ derived?: number
319
+ }
320
+
321
+ export interface ProbeFact {
322
+ layer: string
323
+ path?: string
324
+ target?: string
325
+ ok: boolean
326
+ tone?: string
327
+ detail?: string
328
+ }
329
+
330
+ export interface RouteResult {
331
+ route: string
332
+ target?: string
333
+ /** Backend Service namespace - set when it differs from the subject namespace
334
+ * (cross-namespace Gateway API backendRef) so the in-cluster probe dials an FQDN. */
335
+ targetNamespace?: string
336
+ outcome: RouteOutcome
337
+ /** Which layer failed on a non-reachable route, so the UI can say exactly what
338
+ * broke: 'tcp' | 'tls' | 'http' (unreachable), or 'upstream' for a 502/504 where
339
+ * HTTP was reached but the gateway couldn't reach its backend. */
340
+ failedLayer?: 'tcp' | 'tls' | 'http' | 'upstream'
341
+ confidence?: RouteConfidence
342
+ evidence?: string
343
+ command?: string
344
+ /** Unreachable by DESIGN (backing workload intentionally scaled to 0) - read
345
+ * amber-benign, not red. */
346
+ benign?: boolean
347
+ /** Behind-the-gate facts (apiserver/direct-pod). Render as plain "checked behind it"
348
+ * sub-lines - never label them "localization" in the UI. */
349
+ localization?: ProbeFact[]
350
+ /** Best-guess concrete request to test this route from inside the cluster.
351
+ * A STARTING POINT the user edits before running; pathGuessed marks a path
352
+ * derived from a pattern (no single faithful request exists). */
353
+ inClusterRequest?: ProbeRequest
354
+ /** Where this route's `outcome` came from. Absent (the normal case) means it
355
+ * was OBSERVED and `byVantage` says by whom. Otherwise it was DERIVED and no
356
+ * vantage dialled it, so it must never be rendered as the selected origin's
357
+ * observation nor in the language of a request:
358
+ *
359
+ * - 'declared-config' read off what is declared (a backendRef naming a
360
+ * Service that does not exist) - broken regardless of
361
+ * what the cluster is currently doing.
362
+ * - 'cluster-state' read off current state (no ready endpoints) - true
363
+ * now, and changes when the workload does. */
364
+ basis?: 'declared-config' | 'cluster-state'
365
+ /** Each vantage's OWN view of this route.
366
+ *
367
+ * `outcome` / `confidence` / `evidence` above are a documented lossy rollup:
368
+ * the producer buckets by mechanism and takes worst-wins, so a route that
369
+ * works in-cluster and fails from a laptop collapses to "unreachable" with no
370
+ * field left to say where it DID work. Anything answering "did this path work
371
+ * from THIS vantage" must read this instead of the rollup. */
372
+ byVantage?: VantageResult[]
373
+ }
374
+
375
+ /** One vantage's unmerged view of a route. Keyed by (vantage, path): the same
376
+ * vantage relayed through the API server is a different claim from one that
377
+ * used the real network path. */
378
+ export interface VantageResult {
379
+ vantage: ProbeVantage
380
+ path: ProbePath
381
+ /** Who dialled: 'radar' (Radar's own process) or 'probe-job' (a throwaway Pod
382
+ * Radar created). Absent means 'radar'. Kept apart from `vantage` because the
383
+ * two share (in-cluster, data) but are different observers. */
384
+ source?: 'radar' | 'probe-job'
385
+ outcome: RouteOutcome
386
+ confidence?: RouteConfidence
387
+ evidence?: string
388
+ failedLayer?: 'tcp' | 'tls' | 'http' | 'upstream'
389
+ /** Which hop-to-hop boundary broke, when two observations either side of it
390
+ * establish that. Empty means we could not tell - the common case, and never
391
+ * to be guessed at in the UI. */
392
+ failedBoundary?: 'service-routing'
393
+ /** Which part of the DECLARED path this row's dial exercised. 'backend' means
394
+ * the dial bypassed the route's front door, so this row can never vouch for
395
+ * the entry path. Empty when there is no separate entry segment. */
396
+ segment?: 'backend'
397
+ }
398
+
399
+ /** A concrete request the in-cluster runner can send to the Service. HTTP
400
+ * fields are absent for transport-only TCP probes. */
401
+ export interface ProbeRequest {
402
+ protocol: 'http' | 'https' | 'tcp'
403
+ scheme?: 'http' | 'https'
404
+ host?: string
405
+ path?: string
406
+ pathGuessed?: boolean
407
+ }
408
+
409
+ export interface RouteSkip {
410
+ route?: string
411
+ reason: string
412
+ reasonClass?: SkipReasonClass
413
+ command?: string
414
+ }
@@ -106,6 +106,25 @@ const KIND: Record<string, string> = {
106
106
  CiliumClusterwideNetworkPolicy: 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-700/40',
107
107
  ClusterNetworkPolicy: 'bg-violet-100 text-violet-800 border-violet-300 dark:bg-violet-950/50 dark:text-violet-300 dark:border-violet-700/40',
108
108
 
109
+ // Kyverno. Policy kinds share the indigo family used by the other
110
+ // policy-shaped kinds above; the destructive ones (Deleting/Cleanup) and
111
+ // PolicyException take amber so they read as "changes or bypasses things".
112
+ Policy: 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-700/40',
113
+ ClusterPolicy: 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-700/40',
114
+ ValidatingPolicy: 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-700/40',
115
+ NamespacedValidatingPolicy: 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-700/40',
116
+ ImageValidatingPolicy: 'bg-violet-100 text-violet-800 border-violet-300 dark:bg-violet-950/50 dark:text-violet-300 dark:border-violet-700/40',
117
+ NamespacedImageValidatingPolicy: 'bg-violet-100 text-violet-800 border-violet-300 dark:bg-violet-950/50 dark:text-violet-300 dark:border-violet-700/40',
118
+ MutatingPolicy: 'bg-violet-100 text-violet-800 border-violet-300 dark:bg-violet-950/50 dark:text-violet-300 dark:border-violet-700/40',
119
+ NamespacedMutatingPolicy: 'bg-violet-100 text-violet-800 border-violet-300 dark:bg-violet-950/50 dark:text-violet-300 dark:border-violet-700/40',
120
+ GeneratingPolicy: 'bg-violet-100 text-violet-800 border-violet-300 dark:bg-violet-950/50 dark:text-violet-300 dark:border-violet-700/40',
121
+ NamespacedGeneratingPolicy: 'bg-violet-100 text-violet-800 border-violet-300 dark:bg-violet-950/50 dark:text-violet-300 dark:border-violet-700/40',
122
+ DeletingPolicy: 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950/50 dark:text-amber-300 dark:border-amber-700/40',
123
+ NamespacedDeletingPolicy: 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950/50 dark:text-amber-300 dark:border-amber-700/40',
124
+ PolicyException: 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950/50 dark:text-amber-300 dark:border-amber-700/40',
125
+ CleanupPolicy: 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950/50 dark:text-amber-300 dark:border-amber-700/40',
126
+ ClusterCleanupPolicy: 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950/50 dark:text-amber-300 dark:border-amber-700/40',
127
+
109
128
  // Cluster-scoped
110
129
  Node: 'bg-sky-100 text-sky-700 border-sky-300 dark:bg-sky-950/50 dark:text-sky-400 dark:border-sky-700/40',
111
130
  Namespace: 'bg-gray-100 text-gray-700 border-gray-300 dark:bg-gray-950/50 dark:text-gray-400 dark:border-gray-700/40',
@@ -144,6 +163,14 @@ const KIND: Record<string, string> = {
144
163
  // Contour
145
164
  HTTPProxy: 'bg-violet-100 text-violet-700 border-violet-300 dark:bg-violet-950/50 dark:text-violet-400 dark:border-violet-700/40',
146
165
 
166
+ // CloudNativePG. Only Pooler is keyed here. getKindColorClass takes the kind
167
+ // string alone, and CNPG's other kinds arrive as bare `Cluster` / `Backup` /
168
+ // `ScheduledBackup`, which collide with CAPI and Velero — keying those would
169
+ // paint another operator's resource in Postgres colors, the exact collision
170
+ // class this integration fixes elsewhere. Coloring them needs the API group
171
+ // threaded through Badge; until then they take the default.
172
+ Pooler: 'bg-sky-100 text-sky-700 border-sky-300 dark:bg-sky-950/50 dark:text-sky-400 dark:border-sky-700/40',
173
+
147
174
  // Cluster API
148
175
  CAPICluster: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
149
176
  MachineDeployment: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
@@ -154,6 +181,15 @@ const KIND: Record<string, string> = {
154
181
  ClusterClass: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
155
182
  MachineHealthCheck: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
156
183
 
184
+ // Velero. Only kinds whose names no other operator claims are mapped: this
185
+ // table is keyed on kind alone with no group awareness, so a shared name would
186
+ // paint a foreign resource in Velero's colour. That rules out `Backup` (CNPG),
187
+ // `Restore` (rancher/backup-restore-operator) and `Schedule` (several
188
+ // operators) — the same collisions the renderers guard on velero.io.
189
+ BackupStorageLocation: 'bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950/50 dark:text-teal-400 dark:border-teal-700/40',
190
+ VolumeSnapshotLocation: 'bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950/50 dark:text-teal-400 dark:border-teal-700/40',
191
+ BackupRepository: 'bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950/50 dark:text-teal-400 dark:border-teal-700/40',
192
+
157
193
  // Events
158
194
  Event: 'bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-950/50 dark:text-slate-400 dark:border-slate-700/40',
159
195
  }
@@ -1,5 +1,5 @@
1
1
  import { useState, useMemo } from 'react'
2
- import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react'
2
+ import { AlertTriangle, ChevronDown, ChevronRight, Loader2 } from 'lucide-react'
3
3
  import { ConfirmDialog } from './ConfirmDialog'
4
4
  import { pluralize } from '../../utils/pluralize'
5
5
 
@@ -20,6 +20,7 @@ interface ForceDeleteConfirmDialogProps {
20
20
  isLoading: boolean
21
21
  cascadeDependents?: CascadeDependent[]
22
22
  cascadeLoading?: boolean
23
+ cascadeRootResolved?: boolean
23
24
  }
24
25
 
25
26
  export function ForceDeleteConfirmDialog({
@@ -32,6 +33,7 @@ export function ForceDeleteConfirmDialog({
32
33
  isLoading,
33
34
  cascadeDependents,
34
35
  cascadeLoading,
36
+ cascadeRootResolved,
35
37
  }: ForceDeleteConfirmDialogProps) {
36
38
  const [forceDelete, setForceDelete] = useState(false)
37
39
 
@@ -68,6 +70,15 @@ export function ForceDeleteConfirmDialog({
68
70
  <CascadeDependentsList dependents={cascadeDependents} />
69
71
  )}
70
72
 
73
+ {!cascadeLoading && cascadeRootResolved === false && (
74
+ <div className="flex items-start gap-2 rounded border border-theme-border bg-theme-elevated px-3 py-2 text-xs text-warning-text">
75
+ <AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
76
+ <span>
77
+ Radar couldn&apos;t verify which dependent resources Kubernetes will also delete. Additional resources may be deleted but aren&apos;t shown here.
78
+ </span>
79
+ </div>
80
+ )}
81
+
71
82
  <label className="flex items-center gap-2 text-sm text-theme-text-secondary">
72
83
  <input
73
84
  type="checkbox"
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { inClusterConsentDetails } from './InClusterConsentDialog'
3
+
4
+ const req = (route: string, request: string) => ({ route, request })
5
+
6
+ describe('inClusterConsentDetails', () => {
7
+ it('names the requests it is about to send, not just where they land', () => {
8
+ // This is the last screen before real pod-to-pod traffic. It used to show
9
+ // only the cluster and namespace, so the operator confirmed requests they
10
+ // were never shown.
11
+ const out = inClusterConsentDetails({
12
+ cluster: 'prod-us-east1',
13
+ namespace: 'payments',
14
+ requests: [req('checkout.example.com/', 'GET https://checkout.example.com/healthz')],
15
+ })
16
+ expect(out).toContain('prod-us-east1')
17
+ expect(out).toContain('payments')
18
+ expect(out).toContain('GET https://checkout.example.com/healthz')
19
+ })
20
+
21
+ it('shows each route its OWN path — a single "GET /" was wrong in the default case', () => {
22
+ // With no path override the server keeps every route's declared path, so one
23
+ // shared "/" is least accurate exactly when it used to be displayed.
24
+ const out = inClusterConsentDetails({
25
+ namespace: 'payments',
26
+ requests: [
27
+ req('a.example.com/api', 'GET https://a.example.com/api'),
28
+ req('b.example.com/health', 'GET https://b.example.com/health'),
29
+ ],
30
+ })
31
+ expect(out).toContain('/api')
32
+ expect(out).toContain('/health')
33
+ })
34
+
35
+ it('counts declared paths that have no derivable request', () => {
36
+ // They are part of the scope being agreed to; omitting them undercounted
37
+ // what the run covers.
38
+ const out = inClusterConsentDetails({
39
+ namespace: 'payments',
40
+ requests: [req('a/', 'GET http://svc:80/')],
41
+ untestedCount: 2,
42
+ })
43
+ expect(out).toMatch(/Declared:\s+3 paths/)
44
+ expect(out).toMatch(/2 paths with no derivable request/)
45
+ })
46
+
47
+ it('says "path" singular for one', () => {
48
+ expect(inClusterConsentDetails({ namespace: 'p', requests: [req('a', 'GET /')] })).toMatch(/Declared:\s+1 path\b(?!s)/)
49
+ })
50
+
51
+ it('caps the listed requests but says how many are hidden', () => {
52
+ const many = Array.from({ length: 9 }, (_, i) => req(`r${i}`, `GET http://svc:80/${i}`))
53
+ const out = inClusterConsentDetails({ namespace: 'p', requests: many })
54
+ expect(out).toMatch(/Declared:\s+9 paths/)
55
+ expect(out).toMatch(/and 3 more/)
56
+ })
57
+
58
+ it('omits the coverage block entirely when nothing is known, rather than guessing', () => {
59
+ const out = inClusterConsentDetails({ namespace: 'payments' })
60
+ expect(out).not.toMatch(/Declared:/)
61
+ expect(out).toMatch(/Namespace: payments/)
62
+ })
63
+
64
+ it('omits the cluster line when identity is unavailable', () => {
65
+ expect(inClusterConsentDetails({ namespace: 'payments' })).not.toMatch(/Cluster:/)
66
+ })
67
+ it('discloses the probe-Pod cap instead of implying full coverage', () => {
68
+ // Route order is deterministic server-side, so routes past the cap are not
69
+ // deferred - re-running starts from the same first five and never reaches
70
+ // them. A screen that counts them as covered is the misleading part.
71
+ const many = Array.from({ length: 8 }, (_, i) => req(`r${i}`, `GET http://svc:80/${i}`))
72
+ const out = inClusterConsentDetails({ namespace: 'p', requests: many, maxProbes: 5 })
73
+ expect(out).toMatch(/Will run:\s+5 requests/)
74
+ expect(out).toMatch(/3 beyond the 5-Pod limit/)
75
+ expect(out).toMatch(/same first 5/)
76
+ })
77
+
78
+ it('says nothing about a cap when everything fits under it', () => {
79
+ const out = inClusterConsentDetails({ namespace: 'p', requests: [req('a', 'GET http://svc:80/')], maxProbes: 5 })
80
+ expect(out).toMatch(/Will run:\s+1 request\b(?!s)/)
81
+ expect(out).not.toMatch(/beyond the/)
82
+ })
83
+ })