@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,623 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { traceToSubgraph, outcomeToStatus } from './traceToSubgraph'
|
|
3
|
+
import { reachVerdict } from './reachVerdict'
|
|
4
|
+
import type { Trace } from './types'
|
|
5
|
+
|
|
6
|
+
const baseTrace = (o: Partial<Trace>): Trace => ({
|
|
7
|
+
subject: { kind: 'Service', namespace: 'prod', name: 'echo' },
|
|
8
|
+
upstreams: [],
|
|
9
|
+
downstream: [],
|
|
10
|
+
verdict: 'healthy',
|
|
11
|
+
brokenAt: -1,
|
|
12
|
+
...o,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('outcomeToStatus - node = own health, NOT vantage', () => {
|
|
16
|
+
it('verified over REAL traffic is healthy (green)', () => {
|
|
17
|
+
expect(outcomeToStatus('verified', 'real')).toBe('healthy')
|
|
18
|
+
})
|
|
19
|
+
it('verified via the API proxy (indirect) is STILL healthy - the resource responded; the vantage caveat lives on the edge', () => {
|
|
20
|
+
expect(outcomeToStatus('verified', 'indirect')).toBe('healthy')
|
|
21
|
+
})
|
|
22
|
+
it('reached is healthy (it responded); server-error is degraded (app 5xx); unreachable is unhealthy; not-tested is unknown', () => {
|
|
23
|
+
expect(outcomeToStatus('reached', 'indirect')).toBe('healthy')
|
|
24
|
+
expect(outcomeToStatus('server-error', 'real')).toBe('degraded')
|
|
25
|
+
expect(outcomeToStatus('unreachable', 'real')).toBe('unhealthy')
|
|
26
|
+
expect(outcomeToStatus('not-tested')).toBe('unknown')
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('traceToSubgraph', () => {
|
|
31
|
+
it('maps 2 upstream ingresses + service + pods into nodes + routes-to edges', () => {
|
|
32
|
+
const t = baseTrace({
|
|
33
|
+
upstreams: [
|
|
34
|
+
{ resource: { kind: 'Ingress', namespace: 'prod', name: 'a' }, edge: 'Ingress->Service', findings: [] },
|
|
35
|
+
{ resource: { kind: 'Ingress', namespace: 'prod', name: 'b' }, edge: 'Ingress->Service', findings: [] },
|
|
36
|
+
],
|
|
37
|
+
downstream: [
|
|
38
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
39
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [] },
|
|
40
|
+
],
|
|
41
|
+
routes: [{ route: 'echo', target: 'echo:80', outcome: 'verified', confidence: 'real' }],
|
|
42
|
+
})
|
|
43
|
+
const topo = traceToSubgraph(t)
|
|
44
|
+
// Ingress a, Ingress b, Service echo, Pods = 4 nodes
|
|
45
|
+
expect(topo.nodes).toHaveLength(4)
|
|
46
|
+
const svc = topo.nodes.find((n) => n.kind === 'Service')
|
|
47
|
+
expect(svc?.status).toBe('healthy') // verified+real
|
|
48
|
+
// each ingress → service, service → pods = 3 edges, all routes-to
|
|
49
|
+
expect(topo.edges).toHaveLength(3)
|
|
50
|
+
expect(topo.edges.every((e) => e.type === 'routes-to')).toBe(true)
|
|
51
|
+
const svcId = svc!.id
|
|
52
|
+
expect(topo.edges.filter((e) => e.target === svcId)).toHaveLength(2) // two ingresses converge
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('a proxy-only reached service node stays HEALTHY - vantage lives on the edge, not the node', () => {
|
|
56
|
+
const t = baseTrace({
|
|
57
|
+
verdict: 'unknown',
|
|
58
|
+
downstream: [
|
|
59
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
60
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [] },
|
|
61
|
+
],
|
|
62
|
+
routes: [{ route: 'echo', target: 'echo:80', outcome: 'verified', confidence: 'indirect' }],
|
|
63
|
+
})
|
|
64
|
+
const topo = traceToSubgraph(t)
|
|
65
|
+
const svc = topo.nodes.find((n) => n.kind === 'Service')
|
|
66
|
+
// The service RESPONDED (just via the proxy) → its own health is fine → green.
|
|
67
|
+
// The "real in-cluster traffic not confirmed" caveat belongs to the PATH (the
|
|
68
|
+
// edge style + the verdict), never to recoloring the node amber.
|
|
69
|
+
expect(svc?.status).toBe('healthy')
|
|
70
|
+
expect(svc?.status).not.toBe('degraded')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('flags a critical-finding hop as unhealthy (the break)', () => {
|
|
74
|
+
const t = baseTrace({
|
|
75
|
+
verdict: 'broken',
|
|
76
|
+
brokenAt: 1,
|
|
77
|
+
downstream: [
|
|
78
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
79
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [{ code: 'problem:x', severity: 'critical', message: '0/2 ready' }] },
|
|
80
|
+
],
|
|
81
|
+
routes: [{ route: 'echo', target: 'echo:80', outcome: 'unreachable', confidence: 'real' }],
|
|
82
|
+
})
|
|
83
|
+
const topo = traceToSubgraph(t)
|
|
84
|
+
expect(topo.nodes.find((n) => n.kind === 'Service')?.status).toBe('unhealthy') // unreachable route
|
|
85
|
+
expect(topo.nodes.find((n) => n.kind === 'Pods')?.status).toBe('unhealthy') // critical finding
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('Service "0/N selected pods ready" is a backend SYMPTOM → Service amber (wired, no backend); the red fault sits on the Pods node', () => {
|
|
89
|
+
const t = baseTrace({
|
|
90
|
+
verdict: 'broken',
|
|
91
|
+
brokenAt: 0,
|
|
92
|
+
downstream: [
|
|
93
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [
|
|
94
|
+
{ code: 'problem:0/1 selected pods ready', severity: 'critical', message: '0/1 selected pods ready' },
|
|
95
|
+
] },
|
|
96
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', meta: { ready: 0, selected: 1 }, findings: [
|
|
97
|
+
{ code: 'problem:CrashLoopBackOff', severity: 'critical', message: 'CrashLoopBackOff' },
|
|
98
|
+
] },
|
|
99
|
+
],
|
|
100
|
+
// Indirect (proxy-only) unreachable → the route doesn't independently condemn
|
|
101
|
+
// the Service node; only the finding does, and that's the symptom we cap.
|
|
102
|
+
routes: [{ route: 'echo', target: 'echo:80', outcome: 'unreachable', confidence: 'indirect' }],
|
|
103
|
+
})
|
|
104
|
+
const topo = traceToSubgraph(t)
|
|
105
|
+
expect(topo.nodes.find((n) => n.kind === 'Service')?.status).toBe('degraded') // wired, no backend - NOT red
|
|
106
|
+
expect(topo.nodes.find((n) => n.kind === 'Pods')?.status).toBe('unhealthy') // the real fault
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('partial-ready backend (ready>0, one pod crashing) → Pods node amber, NOT red - it still serves traffic', () => {
|
|
110
|
+
const t = baseTrace({
|
|
111
|
+
verdict: 'degraded',
|
|
112
|
+
brokenAt: -1,
|
|
113
|
+
downstream: [
|
|
114
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'dual' }, edge: 'entry:Service', findings: [] },
|
|
115
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', meta: { ready: 1, selected: 2 }, findings: [
|
|
116
|
+
{ code: 'problem:CrashLoopBackOff', severity: 'critical', message: 'CrashLoopBackOff' },
|
|
117
|
+
] },
|
|
118
|
+
],
|
|
119
|
+
routes: [{ route: 'dual', outcome: 'reached', confidence: 'indirect' }],
|
|
120
|
+
})
|
|
121
|
+
const topo = traceToSubgraph(t)
|
|
122
|
+
expect(topo.nodes.find((n) => n.kind === 'Pods')?.status).toBe('degraded') // serves via the ready pod - not the false red
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('does NOT color the Pods node for a NetworkPolicy finding (path concern, not own-health)', () => {
|
|
126
|
+
// A would-deny is a PATH restriction; the pod itself answered HTTP 200. The
|
|
127
|
+
// node must stay healthy - the policy lives in the hop detail + edge, never
|
|
128
|
+
// the node dot. Painting it amber would over-claim the pod is unwell.
|
|
129
|
+
const t = baseTrace({
|
|
130
|
+
verdict: 'healthy',
|
|
131
|
+
brokenAt: -1,
|
|
132
|
+
downstream: [
|
|
133
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [], probes: [{ layer: 'http', target: 'echo:80', vantage: 'local', path: 'apiserver', ok: true }] },
|
|
134
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [{ code: 'netpol:would-deny', severity: 'warning', message: 'A cluster network rule would block traffic on :80' }], probes: [{ layer: 'http', target: 'pod port 80', vantage: 'local', path: 'apiserver', ok: true }] },
|
|
135
|
+
],
|
|
136
|
+
routes: [{ route: 'echo', target: 'echo:80', outcome: 'reached', confidence: '' }],
|
|
137
|
+
})
|
|
138
|
+
const topo = traceToSubgraph(t)
|
|
139
|
+
expect(topo.nodes.find((n) => n.kind === 'Pods')?.status).toBe('healthy')
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('traceToSubgraph - router (Ingress/Gateway) node = front-door OWN health', () => {
|
|
144
|
+
const router = (routes: Trace['routes']): Trace =>
|
|
145
|
+
baseTrace({
|
|
146
|
+
subject: { kind: 'Ingress', namespace: 'prod', name: 'gw' },
|
|
147
|
+
verdict: 'degraded',
|
|
148
|
+
downstream: [{ resource: { kind: 'Ingress', namespace: 'prod', name: 'gw' }, edge: 'entry:Ingress', findings: [] }],
|
|
149
|
+
routes,
|
|
150
|
+
})
|
|
151
|
+
const routerStatus = (t: Trace) => traceToSubgraph(t).nodes.find((n) => n.kind === 'Ingress')?.status
|
|
152
|
+
|
|
153
|
+
it('all routes server-error (5xx) → front door is amber, NOT red (traffic flowed, backend errored)', () => {
|
|
154
|
+
expect(routerStatus(router([
|
|
155
|
+
{ route: '/a', target: 'a:80', outcome: 'server-error' },
|
|
156
|
+
{ route: '/b', target: 'b:80', outcome: 'server-error' },
|
|
157
|
+
]))).toBe('degraded')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('benign scale-to-zero routes → front door NOT red (dormant ≠ broken)', () => {
|
|
161
|
+
expect(routerStatus(router([
|
|
162
|
+
{ route: '/a', target: 'a:80', outcome: 'unreachable', benign: true },
|
|
163
|
+
]))).not.toBe('unhealthy')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('every route a genuine (non-benign) unreachable → front door red', () => {
|
|
167
|
+
expect(routerStatus(router([
|
|
168
|
+
{ route: '/a', target: 'a:80', outcome: 'unreachable' },
|
|
169
|
+
{ route: '/b', target: 'b:80', outcome: 'unreachable' },
|
|
170
|
+
]))).toBe('unhealthy')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('a Service subject scaled to zero (benign unreachable) is amber, NOT red', () => {
|
|
174
|
+
const t = baseTrace({
|
|
175
|
+
subject: { kind: 'Service', namespace: 'prod', name: 'echo' },
|
|
176
|
+
verdict: 'degraded',
|
|
177
|
+
downstream: [{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] }],
|
|
178
|
+
routes: [{ route: 'echo', target: 'echo:80', outcome: 'unreachable', benign: true }],
|
|
179
|
+
})
|
|
180
|
+
expect(traceToSubgraph(t).nodes.find((n) => n.kind === 'Service')?.status).not.toBe('unhealthy')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('a benign scale-to-0 route does NOT color its edge red nor cascade-block the pods behind it', () => {
|
|
184
|
+
const t = baseTrace({
|
|
185
|
+
subject: { kind: 'Ingress', namespace: 'prod', name: 'ing' },
|
|
186
|
+
verdict: 'degraded',
|
|
187
|
+
downstream: [
|
|
188
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'dormant' }, edge: 'Ingress->Service', findings: [] },
|
|
189
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [] },
|
|
190
|
+
],
|
|
191
|
+
routes: [{ route: '/x', target: 'dormant:80', outcome: 'unreachable', benign: true }],
|
|
192
|
+
})
|
|
193
|
+
const topo = traceToSubgraph(t)
|
|
194
|
+
const svc = topo.nodes.find((n) => n.kind === 'Service')!
|
|
195
|
+
expect(topo.edges.find((e) => e.target === svc.id)?.reachOutcome).not.toBe('unreachable')
|
|
196
|
+
const pods = topo.nodes.find((n) => n.kind === 'Pods')!
|
|
197
|
+
expect(topo.edges.find((e) => e.target === pods.id)?.reachOutcome).not.toBe('blocked')
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const multiTrace = (): Trace =>
|
|
202
|
+
baseTrace({
|
|
203
|
+
subject: { kind: 'Ingress', namespace: 'prod', name: 'multi' },
|
|
204
|
+
verdict: 'degraded',
|
|
205
|
+
downstream: [
|
|
206
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'Ingress->Service', findings: [] },
|
|
207
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [] },
|
|
208
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'ghost' }, edge: 'Ingress->Service', findings: [{ code: 'missing_ref:x', severity: 'critical', message: 'Service ghost does not exist' }] },
|
|
209
|
+
],
|
|
210
|
+
routes: [
|
|
211
|
+
{ route: '/web', target: 'echo:80', outcome: 'verified', confidence: 'real' },
|
|
212
|
+
{ route: '/api', target: 'ghost', outcome: 'unreachable', confidence: '' },
|
|
213
|
+
],
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('traceToSubgraph - multi-backend BRANCHES (no linear chaining)', () => {
|
|
217
|
+
it('a second backend hangs off the ENTRY, not after the pods', () => {
|
|
218
|
+
const topo = traceToSubgraph(multiTrace())
|
|
219
|
+
const subjId = topo.nodes.find((n) => n.kind === 'Ingress')!.id
|
|
220
|
+
const echoId = topo.nodes.find((n) => n.name === 'echo')!.id
|
|
221
|
+
const ghostId = topo.nodes.find((n) => n.name === 'ghost')!.id
|
|
222
|
+
const podsId = topo.nodes.find((n) => n.kind === 'Pods')!.id
|
|
223
|
+
// ghost branches off the subject (multi), NOT off pods (the cycle-1 linear bug)
|
|
224
|
+
expect(topo.edges.find((e) => e.target === ghostId)?.source).toBe(subjId)
|
|
225
|
+
expect(topo.edges.find((e) => e.target === echoId)?.source).toBe(subjId)
|
|
226
|
+
// pods hang off THEIR parent service (echo), not the subject
|
|
227
|
+
expect(topo.edges.find((e) => e.target === podsId)?.source).toBe(echoId)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('per-route truth on the EDGE; the router node stays NEUTRAL (no over-attribution)', () => {
|
|
231
|
+
const topo = traceToSubgraph(multiTrace())
|
|
232
|
+
const ghostId = topo.nodes.find((n) => n.name === 'ghost')!.id
|
|
233
|
+
const echoId = topo.nodes.find((n) => n.name === 'echo')!.id
|
|
234
|
+
// the multi Ingress node is NOT painted red though one of its routes is broken
|
|
235
|
+
expect(topo.nodes.find((n) => n.kind === 'Ingress')?.status).not.toBe('unhealthy')
|
|
236
|
+
// the broken route's color is on its edge
|
|
237
|
+
expect(topo.edges.find((e) => e.target === ghostId)?.reachOutcome).toBe('unreachable')
|
|
238
|
+
expect(['verified', 'reached']).toContain(topo.edges.find((e) => e.target === echoId)?.reachOutcome)
|
|
239
|
+
// route labels on the edges
|
|
240
|
+
expect(topo.edges.find((e) => e.target === ghostId)?.label).toBe('/api')
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('traceToSubgraph - frontier rule (no flow past a break)', () => {
|
|
245
|
+
it('a broken Service does NOT draw a reached edge to its pods - it is blocked', () => {
|
|
246
|
+
const t = baseTrace({
|
|
247
|
+
verdict: 'broken',
|
|
248
|
+
brokenAt: 0,
|
|
249
|
+
downstream: [
|
|
250
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'notready' }, edge: 'entry:Service', findings: [{ code: 'svc:no-endpoints', severity: 'critical', message: '0/2 ready' }] },
|
|
251
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [] },
|
|
252
|
+
],
|
|
253
|
+
routes: [{ route: 'notready', target: 'notready:80', outcome: 'unreachable', confidence: 'real' }],
|
|
254
|
+
subject: { kind: 'Service', namespace: 'prod', name: 'notready' },
|
|
255
|
+
})
|
|
256
|
+
const topo = traceToSubgraph(t)
|
|
257
|
+
const podsId = topo.nodes.find((n) => n.kind === 'Pods')!.id
|
|
258
|
+
const edge = topo.edges.find((e) => e.target === podsId)
|
|
259
|
+
expect(edge?.reachOutcome).toBe('blocked')
|
|
260
|
+
expect(edge?.reachOutcome).not.toBe('reached')
|
|
261
|
+
expect(edge?.reachOutcome).not.toBe('verified')
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe('traceToSubgraph - multi-upstream convergence (a broken sibling upstream must not block the subject)', () => {
|
|
266
|
+
it('keeps a reached subject→pods edge + neutral upstream nodes when a sibling upstream has an unrelated broken route', () => {
|
|
267
|
+
const t = baseTrace({
|
|
268
|
+
subject: { kind: 'Service', namespace: 'prod', name: 'echo' },
|
|
269
|
+
upstreams: [
|
|
270
|
+
// `multi` carries a critical from its OWN other route (/api→ghost) - it must NOT
|
|
271
|
+
// paint the multi node red, nor cascade-block the shared echo→pods.
|
|
272
|
+
{ resource: { kind: 'Ingress', namespace: 'prod', name: 'multi' }, edge: 'Ingress->Service', findings: [{ code: 'missing_ref:x', severity: 'critical', message: 'ghost missing' }] },
|
|
273
|
+
{ resource: { kind: 'Ingress', namespace: 'prod', name: 'wild' }, edge: 'Ingress->Service', findings: [] },
|
|
274
|
+
],
|
|
275
|
+
downstream: [
|
|
276
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [], probes: [{ layer: 'http', target: 'echo:80', vantage: 'local', path: 'apiserver', ok: true }] },
|
|
277
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [], probes: [{ layer: 'http', target: 'pod:80', vantage: 'local', path: 'apiserver', ok: true }] },
|
|
278
|
+
],
|
|
279
|
+
verdict: 'unknown',
|
|
280
|
+
routes: [{ route: 'echo', target: 'echo:80', outcome: 'reached', confidence: 'indirect' }],
|
|
281
|
+
})
|
|
282
|
+
const topo = traceToSubgraph(t)
|
|
283
|
+
// upstream entry nodes are NEUTRAL - never colored by their own/other-route findings
|
|
284
|
+
expect(topo.nodes.find((n) => n.name === 'multi')?.status).toBe('unknown')
|
|
285
|
+
expect(topo.nodes.find((n) => n.name === 'wild')?.status).toBe('unknown')
|
|
286
|
+
const svc = topo.nodes.find((n) => n.kind === 'Service')!
|
|
287
|
+
const pods = topo.nodes.find((n) => n.kind === 'Pods')!
|
|
288
|
+
// the subject→pods edge is REACHED, NOT blocked by the sibling upstream's break
|
|
289
|
+
const svcToPods = topo.edges.find((e) => e.source === svc.id && e.target === pods.id)
|
|
290
|
+
expect(svcToPods?.reachOutcome).toBe('reached')
|
|
291
|
+
expect(svcToPods?.reachOutcome).not.toBe('blocked')
|
|
292
|
+
// both UNPROBED converging upstream→subject edges fall back to the subject's reach
|
|
293
|
+
const upEdges = topo.edges.filter((e) => e.target === svc.id)
|
|
294
|
+
expect(upEdges).toHaveLength(2)
|
|
295
|
+
expect(upEdges.every((e) => e.reachOutcome === 'reached')).toBe(true)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('a PROBED-and-failed upstream entry shows unreachable, not the subject reach', () => {
|
|
299
|
+
// The Service itself is healthy, but this entry path was probed and every probe
|
|
300
|
+
// failed - the edge must reflect the failed entry, not inherit the subject's green.
|
|
301
|
+
const t = baseTrace({
|
|
302
|
+
subject: { kind: 'Service', namespace: 'prod', name: 'echo' },
|
|
303
|
+
upstreams: [
|
|
304
|
+
{
|
|
305
|
+
resource: { kind: 'Ingress', namespace: 'prod', name: 'down' },
|
|
306
|
+
edge: 'Ingress->Service',
|
|
307
|
+
findings: [],
|
|
308
|
+
probes: [{ layer: 'http', target: 'shop.example.com:80', vantage: 'in-cluster', ok: false }],
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
downstream: [
|
|
312
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [], probes: [{ layer: 'http', target: 'echo:80', vantage: 'in-cluster', ok: true }] },
|
|
313
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [] },
|
|
314
|
+
],
|
|
315
|
+
verdict: 'broken',
|
|
316
|
+
reason: 'all entry paths into this resource are unreachable; the resource itself looks healthy',
|
|
317
|
+
routes: [{ route: 'echo', target: 'echo:80', outcome: 'verified', confidence: 'real' }],
|
|
318
|
+
})
|
|
319
|
+
const topo = traceToSubgraph(t)
|
|
320
|
+
const svc = topo.nodes.find((n) => n.kind === 'Service')!
|
|
321
|
+
const upEdge = topo.edges.find((e) => e.target === svc.id)
|
|
322
|
+
expect(upEdge?.reachOutcome).toBe('unreachable')
|
|
323
|
+
expect(upEdge?.reachOutcome).not.toBe('reached')
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
describe('reachVerdict - confidence is a level, not an alarm', () => {
|
|
328
|
+
it('reached only via proxy → NEUTRAL (no green ✓ over-claim), but NEVER ⚠', () => {
|
|
329
|
+
const v = reachVerdict(baseTrace({ verdict: 'unknown', routes: [{ route: 'echo', outcome: 'reached', confidence: 'indirect' }], headline: 'Reached via API server' }))
|
|
330
|
+
// Proxy-only must not wear the green ✓ (a non-expert reads it as "works, done")
|
|
331
|
+
// - it's neutral with a caveat. It is also never a ⚠ alarm (the path did reach).
|
|
332
|
+
expect(v.icon).toBe('')
|
|
333
|
+
expect(v.icon).not.toBe('⚠')
|
|
334
|
+
expect(v.tone).toBe('neutral')
|
|
335
|
+
})
|
|
336
|
+
it('broken → ✗', () => {
|
|
337
|
+
expect(reachVerdict(baseTrace({ verdict: 'broken', routes: [{ route: 'x', outcome: 'unreachable' }] })).icon).toBe('✗')
|
|
338
|
+
})
|
|
339
|
+
it('server-error → ⚠ (a real problem)', () => {
|
|
340
|
+
expect(reachVerdict(baseTrace({ verdict: 'degraded', routes: [{ route: 'x', outcome: 'server-error', confidence: 'real' }] })).icon).toBe('⚠')
|
|
341
|
+
})
|
|
342
|
+
it('partial (some reachable, some not) → ⚠', () => {
|
|
343
|
+
const v = reachVerdict(baseTrace({ verdict: 'degraded', routes: [{ route: 'a', outcome: 'reached', confidence: 'real' }, { route: 'b', outcome: 'unreachable' }] }))
|
|
344
|
+
expect(v.icon).toBe('⚠')
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
describe('traceToSubgraph - cycle 5 honesty fixes', () => {
|
|
349
|
+
// Defect 8: a DNS-only live probe is not own-health - the node must read unknown.
|
|
350
|
+
it('a backend whose only live probe is a DNS success is unknown, not green healthy', () => {
|
|
351
|
+
const t = baseTrace({
|
|
352
|
+
subject: { kind: 'Ingress', namespace: 'prod', name: 'ing' },
|
|
353
|
+
downstream: [
|
|
354
|
+
{ resource: { kind: 'Ingress', namespace: 'prod', name: 'ing' }, edge: 'entry:Ingress', findings: [], probes: [] },
|
|
355
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'ext' }, edge: 'Ingress->Service', findings: [], probes: [
|
|
356
|
+
{ layer: 'dns', target: 'ext', vantage: 'local', ok: true },
|
|
357
|
+
] },
|
|
358
|
+
],
|
|
359
|
+
routes: [],
|
|
360
|
+
})
|
|
361
|
+
const g = traceToSubgraph(t)
|
|
362
|
+
const ext = g.nodes.find((n) => n.name === 'ext')
|
|
363
|
+
expect(ext?.status).not.toBe('healthy')
|
|
364
|
+
expect(ext?.status).toBe('unknown')
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
// Defect 9: worst-route coloring must not cascade-block pods another route reached.
|
|
368
|
+
it('two routes to one backend (port 80 verified, 9090 unreachable) - pods reached by port 80 are NOT blocked', () => {
|
|
369
|
+
const t = baseTrace({
|
|
370
|
+
subject: { kind: 'Ingress', namespace: 'prod', name: 'ing' },
|
|
371
|
+
downstream: [
|
|
372
|
+
{ resource: { kind: 'Ingress', namespace: 'prod', name: 'ing' }, edge: 'entry:Ingress', findings: [], probes: [] },
|
|
373
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'svc' }, edge: 'Ingress->Service', findings: [], probes: [] },
|
|
374
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [], probes: [
|
|
375
|
+
{ layer: 'http', target: ':80', vantage: 'in-cluster', ok: true, tone: 'healthy' },
|
|
376
|
+
] },
|
|
377
|
+
],
|
|
378
|
+
routes: [
|
|
379
|
+
{ route: '/web', target: 'svc:80', outcome: 'verified', confidence: 'real' },
|
|
380
|
+
{ route: '/api', target: 'svc:9090', outcome: 'unreachable', confidence: 'real' },
|
|
381
|
+
],
|
|
382
|
+
})
|
|
383
|
+
const g = traceToSubgraph(t)
|
|
384
|
+
const pods = g.nodes.find((n) => n.kind === 'Pods')
|
|
385
|
+
expect((pods?.data as { subtitleOverride?: string })?.subtitleOverride).not.toBe('not reached - break upstream')
|
|
386
|
+
expect(pods?.status).not.toBe('unknown')
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
describe('traceToSubgraph - failed apiserver-proxy probes are indirect evidence, never red', () => {
|
|
391
|
+
it('a node whose ONLY live evidence is a failed proxy probe stays unknown, not unhealthy', () => {
|
|
392
|
+
const t = baseTrace({
|
|
393
|
+
verdict: 'unknown',
|
|
394
|
+
downstream: [
|
|
395
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [], probes: [
|
|
396
|
+
{ layer: 'http', target: 'echo:80', vantage: 'local', path: 'apiserver', ok: false, tone: 'unhealthy' },
|
|
397
|
+
] },
|
|
398
|
+
],
|
|
399
|
+
routes: [{ route: 'echo', target: 'echo:80', outcome: 'unreachable', confidence: 'indirect' }],
|
|
400
|
+
})
|
|
401
|
+
const svc = traceToSubgraph(t).nodes.find((n) => n.kind === 'Service')
|
|
402
|
+
expect(svc?.status).not.toBe('unhealthy')
|
|
403
|
+
expect(svc?.status).toBe('unknown')
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('a successful proxy probe still counts as positive evidence beside a failed one', () => {
|
|
407
|
+
const t = baseTrace({
|
|
408
|
+
verdict: 'unknown',
|
|
409
|
+
downstream: [
|
|
410
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
411
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [], probes: [
|
|
412
|
+
{ layer: 'http', target: 'pod-a port 80', vantage: 'local', path: 'apiserver', ok: true, tone: 'healthy' },
|
|
413
|
+
{ layer: 'http', target: 'pod-b port 80', vantage: 'local', path: 'apiserver', ok: false, tone: 'unhealthy' },
|
|
414
|
+
] },
|
|
415
|
+
],
|
|
416
|
+
routes: [{ route: 'echo', target: 'echo:80', outcome: 'reached', confidence: 'indirect' }],
|
|
417
|
+
})
|
|
418
|
+
const pods = traceToSubgraph(t).nodes.find((n) => n.kind === 'Pods')
|
|
419
|
+
// The failed proxy knock is dropped (indirect); the passing one keeps the
|
|
420
|
+
// node green - it answered.
|
|
421
|
+
expect(pods?.status).toBe('healthy')
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('a REAL (non-proxy) failure still paints the node red', () => {
|
|
425
|
+
const t = baseTrace({
|
|
426
|
+
verdict: 'broken',
|
|
427
|
+
downstream: [
|
|
428
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [], probes: [
|
|
429
|
+
{ layer: 'http', target: '10.0.0.1:80', vantage: 'in-cluster', path: 'data', ok: false, tone: 'unhealthy' },
|
|
430
|
+
] },
|
|
431
|
+
],
|
|
432
|
+
routes: [],
|
|
433
|
+
})
|
|
434
|
+
expect(traceToSubgraph(t).nodes.find((n) => n.kind === 'Pods')?.status).toBe('unhealthy')
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('a no-route edge whose failures are ALL proxy-vantage reads not-tested, never unreachable/blocked', () => {
|
|
438
|
+
const t = baseTrace({
|
|
439
|
+
verdict: 'unknown',
|
|
440
|
+
downstream: [
|
|
441
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
442
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [], probes: [
|
|
443
|
+
{ layer: 'http', target: 'pod port 80', vantage: 'local', path: 'apiserver', ok: false, tone: 'unhealthy' },
|
|
444
|
+
] },
|
|
445
|
+
],
|
|
446
|
+
routes: [{ route: 'echo', target: 'echo:80', outcome: 'unreachable', confidence: 'indirect' }],
|
|
447
|
+
})
|
|
448
|
+
const topo = traceToSubgraph(t)
|
|
449
|
+
const podsId = topo.nodes.find((n) => n.kind === 'Pods')!.id
|
|
450
|
+
expect(topo.edges.find((e) => e.target === podsId)?.reachOutcome).toBe('not-tested')
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
describe('traceToSubgraph - cycle 11 honesty fixes', () => {
|
|
455
|
+
// Defect 3: routeMatchesRef's namespace guard only fired when targetNamespace was
|
|
456
|
+
// SET. A same-ns route (no targetNamespace) could match a cross-ns same-named
|
|
457
|
+
// backend hop, so routeFor's worst-match attached the same-ns route's unreachable
|
|
458
|
+
// to the cross-ns backend and false-condemned it.
|
|
459
|
+
it('a same-ns route\'s unreachable must NOT attach to a cross-ns same-named backend', () => {
|
|
460
|
+
const t = baseTrace({
|
|
461
|
+
subject: { kind: 'Ingress', namespace: 'prod', name: 'ing' },
|
|
462
|
+
verdict: 'degraded',
|
|
463
|
+
downstream: [
|
|
464
|
+
{ resource: { kind: 'Ingress', namespace: 'prod', name: 'ing' }, edge: 'entry:Ingress', findings: [] },
|
|
465
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'api' }, edge: 'Ingress->Service', findings: [] },
|
|
466
|
+
{ resource: { kind: 'Service', namespace: 'other', name: 'api' }, edge: 'Ingress->Service', findings: [] },
|
|
467
|
+
],
|
|
468
|
+
routes: [
|
|
469
|
+
// Same-ns route (no targetNamespace → targets the subject ns 'prod').
|
|
470
|
+
{ route: 'api', target: 'api:80', outcome: 'unreachable', confidence: 'real' },
|
|
471
|
+
// Cross-ns backendRef route - actually verified.
|
|
472
|
+
{ route: 'api', target: 'api:80', outcome: 'verified', confidence: 'real', targetNamespace: 'other' },
|
|
473
|
+
],
|
|
474
|
+
})
|
|
475
|
+
const g = traceToSubgraph(t)
|
|
476
|
+
const crossNsEdge = g.edges.find((e) => e.target === 'Service/other/api')
|
|
477
|
+
const sameNsEdge = g.edges.find((e) => e.target === 'Service/prod/api')
|
|
478
|
+
// The cross-ns backend is verified - its own route - never the same-ns unreachable.
|
|
479
|
+
expect(crossNsEdge?.reachOutcome).toBe('verified')
|
|
480
|
+
// The same-ns backend keeps its own unreachable.
|
|
481
|
+
expect(sameNsEdge?.reachOutcome).toBe('unreachable')
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
describe('traceToSubgraph - overridden test path echoes on the route edge', () => {
|
|
486
|
+
const ingressTrace = (): Trace =>
|
|
487
|
+
baseTrace({
|
|
488
|
+
subject: { kind: 'Ingress', namespace: 'prod', name: 'wild' },
|
|
489
|
+
downstream: [
|
|
490
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
491
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [] },
|
|
492
|
+
],
|
|
493
|
+
routes: [{ route: '/web', target: 'echo:80', outcome: 'reached', confidence: 'indirect' }],
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('defaults to the DECLARED route path, no tooltip', () => {
|
|
497
|
+
const e = traceToSubgraph(ingressTrace()).edges.find((e) => e.label === '/web')
|
|
498
|
+
expect(e).toBeTruthy()
|
|
499
|
+
expect(e?.labelTitle).toBeUndefined()
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('shows the OVERRIDDEN tested path as the label, keeps the declared path in the tooltip (never overwrites route identity)', () => {
|
|
503
|
+
const edges = traceToSubgraph(ingressTrace(), '/test').edges
|
|
504
|
+
expect(edges.find((e) => e.label === '/web')).toBeFalsy() // declared path no longer the visible label
|
|
505
|
+
const e = edges.find((e) => e.label === '/test')
|
|
506
|
+
expect(e).toBeTruthy()
|
|
507
|
+
expect(e?.labelTitle).toContain('/web') // declared
|
|
508
|
+
expect(e?.labelTitle).toContain('/test') // tested
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('the default "/" path is not treated as an override', () => {
|
|
512
|
+
const e = traceToSubgraph(ingressTrace(), '/').edges.find((e) => e.label === '/web')
|
|
513
|
+
expect(e).toBeTruthy()
|
|
514
|
+
expect(e?.labelTitle).toBeUndefined()
|
|
515
|
+
})
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
describe('traceToSubgraph - DNS success is not transport evidence', () => {
|
|
519
|
+
// An ExternalName whose name resolves but whose every port fails is DOWN. The
|
|
520
|
+
// DNS-ok row must not soften the node to "partially healthy" amber or keep
|
|
521
|
+
// the edge green - resolving a name reaches nothing.
|
|
522
|
+
it('dns-ok + every port failed → node unhealthy (not degraded), edge unreachable', () => {
|
|
523
|
+
const t = baseTrace({
|
|
524
|
+
verdict: 'broken',
|
|
525
|
+
downstream: [
|
|
526
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
527
|
+
{ resource: { kind: 'ExternalName', namespace: 'prod', name: 'db.example.com' }, edge: 'Service->ExternalName', findings: [], probes: [
|
|
528
|
+
{ layer: 'dns', target: 'db.example.com', vantage: 'local', ok: true },
|
|
529
|
+
{ layer: 'tcp', target: 'db.example.com:5432', vantage: 'local', ok: false, tone: 'unhealthy' },
|
|
530
|
+
{ layer: 'http', target: 'db.example.com:80', vantage: 'local', ok: false, tone: 'unhealthy' },
|
|
531
|
+
] },
|
|
532
|
+
],
|
|
533
|
+
routes: [],
|
|
534
|
+
})
|
|
535
|
+
const topo = traceToSubgraph(t)
|
|
536
|
+
const ext = topo.nodes.find((n) => n.kind === 'ExternalName')
|
|
537
|
+
expect(ext?.status).toBe('unhealthy')
|
|
538
|
+
expect(topo.edges.find((e) => e.target === ext?.id)?.reachOutcome).toBe('unreachable')
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('dns-ok only (transport skipped) → node unknown, edge not-tested', () => {
|
|
542
|
+
const t = baseTrace({
|
|
543
|
+
verdict: 'unknown',
|
|
544
|
+
downstream: [
|
|
545
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
546
|
+
{ resource: { kind: 'ExternalName', namespace: 'prod', name: 'db.example.com' }, edge: 'Service->ExternalName', findings: [], probes: [
|
|
547
|
+
{ layer: 'dns', target: 'db.example.com', vantage: 'local', ok: true },
|
|
548
|
+
{ layer: 'tcp', target: 'db.example.com:5432', vantage: 'local', ok: false, skipped: true, reason: 'not reachable from your machine' },
|
|
549
|
+
] },
|
|
550
|
+
],
|
|
551
|
+
routes: [],
|
|
552
|
+
})
|
|
553
|
+
const topo = traceToSubgraph(t)
|
|
554
|
+
const ext = topo.nodes.find((n) => n.kind === 'ExternalName')
|
|
555
|
+
expect(ext?.status).toBe('unknown')
|
|
556
|
+
expect(topo.edges.find((e) => e.target === ext?.id)?.reachOutcome).toBe('not-tested')
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it('dns FAILURE is still a real break - node unhealthy, edge unreachable', () => {
|
|
560
|
+
const t = baseTrace({
|
|
561
|
+
verdict: 'broken',
|
|
562
|
+
downstream: [
|
|
563
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
564
|
+
{ resource: { kind: 'ExternalName', namespace: 'prod', name: 'ghost.example.com' }, edge: 'Service->ExternalName', findings: [], probes: [
|
|
565
|
+
{ layer: 'dns', target: 'ghost.example.com', vantage: 'local', ok: false, error: 'NXDOMAIN' },
|
|
566
|
+
] },
|
|
567
|
+
],
|
|
568
|
+
routes: [],
|
|
569
|
+
})
|
|
570
|
+
const topo = traceToSubgraph(t)
|
|
571
|
+
const ext = topo.nodes.find((n) => n.kind === 'ExternalName')
|
|
572
|
+
expect(ext?.status).toBe('unhealthy')
|
|
573
|
+
expect(topo.edges.find((e) => e.target === ext?.id)?.reachOutcome).toBe('unreachable')
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('a real port success still reads reached/healthy beside the dns row', () => {
|
|
577
|
+
const t = baseTrace({
|
|
578
|
+
downstream: [
|
|
579
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
580
|
+
{ resource: { kind: 'ExternalName', namespace: 'prod', name: 'db.example.com' }, edge: 'Service->ExternalName', findings: [], probes: [
|
|
581
|
+
{ layer: 'dns', target: 'db.example.com', vantage: 'local', ok: true },
|
|
582
|
+
{ layer: 'http', target: 'db.example.com:80', vantage: 'local', ok: true, tone: 'healthy' },
|
|
583
|
+
] },
|
|
584
|
+
],
|
|
585
|
+
routes: [],
|
|
586
|
+
})
|
|
587
|
+
const topo = traceToSubgraph(t)
|
|
588
|
+
const ext = topo.nodes.find((n) => n.kind === 'ExternalName')
|
|
589
|
+
expect(ext?.status).toBe('healthy')
|
|
590
|
+
expect(topo.edges.find((e) => e.target === ext?.id)?.reachOutcome).toBe('reached')
|
|
591
|
+
})
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
describe('traceToSubgraph - publishNotReadyAddresses subtitle honesty', () => {
|
|
595
|
+
// meta.ready counts PUBLISHED endpoints for a publishNotReadyAddresses
|
|
596
|
+
// Service (the Go side stamps ready = selected there) - "3/3 ready" over
|
|
597
|
+
// crashlooping pods is a lie; say what the count actually is.
|
|
598
|
+
it('renders a published count, not "N/M ready", when publishNotReadyAddresses is set', () => {
|
|
599
|
+
const t = baseTrace({
|
|
600
|
+
downstream: [
|
|
601
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
602
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [], meta: { ready: 3, selected: 3, publishNotReadyAddresses: true } },
|
|
603
|
+
],
|
|
604
|
+
routes: [],
|
|
605
|
+
})
|
|
606
|
+
const pods = traceToSubgraph(t).nodes.find((n) => n.kind === 'Pods')
|
|
607
|
+
const sub = (pods?.data as { subtitleOverride?: string })?.subtitleOverride
|
|
608
|
+
expect(sub).toBe('3 published (readiness not required)')
|
|
609
|
+
expect(sub).not.toContain('3/3')
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
it('an ordinary Service keeps the "N/M ready" subtitle', () => {
|
|
613
|
+
const t = baseTrace({
|
|
614
|
+
downstream: [
|
|
615
|
+
{ resource: { kind: 'Service', namespace: 'prod', name: 'echo' }, edge: 'entry:Service', findings: [] },
|
|
616
|
+
{ resource: { kind: 'Pods', namespace: 'prod', name: '' }, edge: 'Service->Pods', findings: [], meta: { ready: 2, selected: 3 } },
|
|
617
|
+
],
|
|
618
|
+
routes: [],
|
|
619
|
+
})
|
|
620
|
+
const pods = traceToSubgraph(t).nodes.find((n) => n.kind === 'Pods')
|
|
621
|
+
expect((pods?.data as { subtitleOverride?: string })?.subtitleOverride).toBe('2/3 ready')
|
|
622
|
+
})
|
|
623
|
+
})
|