@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.
- package/package.json +5 -5
- package/src/components/applications/ApplicationDetail.test.tsx +46 -0
- package/src/components/applications/ApplicationDetail.tsx +14 -15
- package/src/components/issues/IssuesView.tsx +9 -0
- package/src/components/issues/diagnostic.ts +3 -0
- package/src/components/issues/issues.test.ts +11 -0
- package/src/components/issues/severity.ts +3 -0
- package/src/components/issues/types.ts +3 -0
- package/src/components/resources/ResourcesSidebar.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +259 -30
- package/src/components/resources/index.ts +2 -0
- package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
- package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +109 -9
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +8 -4
- package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/EventRenderer.tsx +10 -18
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
- package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
- package/src/components/resources/renderers/index.ts +4 -0
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
- package/src/components/resources/renderers/velero-cells.tsx +128 -9
- package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
- package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +527 -1
- package/src/components/resources/resource-utils-cnpg.ts +373 -56
- package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
- package/src/components/resources/resource-utils-velero.test.ts +237 -0
- package/src/components/resources/resource-utils-velero.ts +214 -22
- package/src/components/resources/resource-utils.ts +70 -3
- package/src/components/shared/ResourceActionsBar.tsx +3 -1
- package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
- package/src/components/topology/K8sResourceNode.tsx +11 -0
- package/src/components/topology/TopologyGraph.tsx +85 -5
- package/src/components/trace/ReachabilityGraph.tsx +517 -0
- package/src/components/trace/ReachabilityView.tsx +1173 -0
- package/src/components/trace/TracePanel.test.ts +172 -0
- package/src/components/trace/TracePanel.tsx +558 -0
- package/src/components/trace/TraceSummary.test.ts +213 -0
- package/src/components/trace/TraceSummary.tsx +168 -0
- package/src/components/trace/inClusterSummary.test.ts +98 -0
- package/src/components/trace/inClusterSummary.ts +81 -0
- package/src/components/trace/index.ts +19 -0
- package/src/components/trace/podReach.test.ts +71 -0
- package/src/components/trace/podReach.ts +37 -0
- package/src/components/trace/probe-display.test.ts +107 -0
- package/src/components/trace/probe-display.ts +37 -0
- package/src/components/trace/problemRows.test.ts +107 -0
- package/src/components/trace/reachFixtures.ts +261 -0
- package/src/components/trace/reachGraphModel.test.ts +1423 -0
- package/src/components/trace/reachGraphModel.ts +1521 -0
- package/src/components/trace/reachInspector.test.ts +892 -0
- package/src/components/trace/reachInspector.ts +797 -0
- package/src/components/trace/reachMarks.test.ts +698 -0
- package/src/components/trace/reachMarks.ts +623 -0
- package/src/components/trace/reachOrigins.test.ts +392 -0
- package/src/components/trace/reachOrigins.ts +449 -0
- package/src/components/trace/reachVerdict.test.ts +537 -0
- package/src/components/trace/reachVerdict.ts +601 -0
- package/src/components/trace/traceFingerprint.test.ts +214 -0
- package/src/components/trace/traceFingerprint.ts +122 -0
- package/src/components/trace/traceToSubgraph.test.ts +623 -0
- package/src/components/trace/traceToSubgraph.ts +463 -0
- package/src/components/trace/types.ts +414 -0
- package/src/components/ui/Badge.tsx +36 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
- package/src/components/ui/InClusterConsentDialog.test.tsx +83 -0
- package/src/components/ui/InClusterConsentDialog.tsx +141 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +58 -5
- package/src/components/workload/index.ts +1 -1
- package/src/components/workload/reachabilityTab.test.ts +47 -0
- package/src/index.ts +5 -0
- package/src/theme/components.css +64 -0
- package/src/theme/variables.css +5 -0
- package/src/types/core.ts +8 -0
- package/src/utils/api-resources.ts +2 -0
- package/src/utils/application-history.test.ts +1 -1
- package/src/utils/applications.test.ts +63 -1
- package/src/utils/applications.ts +27 -0
- package/src/utils/inClusterConsent.test.ts +109 -0
- package/src/utils/inClusterConsent.ts +72 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +61 -0
- package/src/utils/navigation.ts +11 -9
- package/src/utils/pluralize.test.ts +80 -1
- package/src/utils/pluralize.ts +34 -0
- package/src/utils/resource-icons.test.ts +33 -0
- 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't verify which dependent resources Kubernetes will also delete. Additional resources may be deleted but aren'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
|
+
})
|