@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,392 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { buildOrigins, headlineEvidenceOrigin, defaultOrigin, strongestGap, originOf, localIdentity, originSkipReason } from './reachOrigins'
|
|
3
|
+
import type { Trace, ProbeResult, RouteResult } from './types'
|
|
4
|
+
|
|
5
|
+
const p = (o: Partial<ProbeResult>): ProbeResult => ({ layer: 'http', target: 'svc:80', vantage: 'in-cluster', ok: true, ...o })
|
|
6
|
+
|
|
7
|
+
const traceWith = (probes: ProbeResult[]): Trace => ({
|
|
8
|
+
subject: { kind: 'Service', name: 'shop', namespace: 'store' },
|
|
9
|
+
verdict: 'healthy',
|
|
10
|
+
brokenAt: -1,
|
|
11
|
+
upstreams: [],
|
|
12
|
+
downstream: [{ resource: { kind: 'Pods', name: '', namespace: 'store' }, edge: 'service->pods', findings: [], probes }],
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('originOf', () => {
|
|
16
|
+
it('the apiserver path wins over vantage — a relay is control plane wherever it is called from', () => {
|
|
17
|
+
expect(originOf(p({ vantage: 'in-cluster', path: 'apiserver' }))).toBe('apiserver')
|
|
18
|
+
expect(originOf(p({ vantage: 'in-cluster', path: 'data' }))).toBe('incluster')
|
|
19
|
+
expect(originOf(p({ vantage: 'local', path: 'data' }))).toBe('local')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('buildOrigins', () => {
|
|
24
|
+
it('always surfaces the origins Radar cannot use, so synthetic evidence never looks complete', () => {
|
|
25
|
+
const os = buildOrigins(traceWith([p({})]))
|
|
26
|
+
const caller = os.find((o) => o.id === 'caller')
|
|
27
|
+
const external = os.find((o) => o.id === 'external')
|
|
28
|
+
expect(caller?.unsupported).toBe(true)
|
|
29
|
+
expect(external?.unsupported).toBe(true)
|
|
30
|
+
expect(caller?.unavailable).toBeTruthy()
|
|
31
|
+
expect(external?.unavailable).toBeTruthy()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('orders by evidence strength, strongest first', () => {
|
|
35
|
+
const os = buildOrigins(traceWith([]))
|
|
36
|
+
// radar-incluster is absent here: this fixture's Radar runs on a laptop.
|
|
37
|
+
expect(os.map((o) => o.id)).toEqual(['caller', 'external', 'incluster', 'local', 'apiserver'])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('marks the in-cluster probe proved on clean dataplane evidence', () => {
|
|
41
|
+
const os = buildOrigins(traceWith([p({ vantage: 'in-cluster', path: 'data', ok: true })]))
|
|
42
|
+
expect(os.find((o) => o.id === 'incluster')?.mark).toBe('proved')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('never marks the apiserver origin proved, however clean the response', () => {
|
|
46
|
+
const os = buildOrigins(traceWith([p({ vantage: 'local', path: 'apiserver', ok: true, tone: 'healthy' })]))
|
|
47
|
+
expect(os.find((o) => o.id === 'apiserver')?.mark).toBe('proxied')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('a failure among an origin’s probes wins over its successes', () => {
|
|
51
|
+
const os = buildOrigins(traceWith([p({ ok: true }), p({ ok: false, tone: 'unhealthy' })]))
|
|
52
|
+
expect(os.find((o) => o.id === 'incluster')?.mark).toBe('failed')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('an all-skipped origin is blocked, not failed, and says why', () => {
|
|
56
|
+
const os = buildOrigins(traceWith([p({ vantage: 'local', path: 'data', skipped: true, reason: 'ClusterIP is not routable from your host' })]))
|
|
57
|
+
const local = os.find((o) => o.id === 'local')
|
|
58
|
+
expect(local?.mark).toBe('blocked')
|
|
59
|
+
expect(local?.unavailable).toMatch(/not routable/)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('reports denied rather than untested when the probe is not permitted', () => {
|
|
63
|
+
const os = buildOrigins(traceWith([]), { inClusterAllowed: false, inClusterDeniedReason: 'RBAC denies create on jobs' })
|
|
64
|
+
const ic = os.find((o) => o.id === 'incluster')
|
|
65
|
+
expect(ic?.mark).toBe('denied')
|
|
66
|
+
expect(ic?.unavailable).toMatch(/RBAC/)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('running beats any prior mark so nothing turns green optimistically', () => {
|
|
70
|
+
const os = buildOrigins(traceWith([p({ ok: true })]), { inClusterRunning: true })
|
|
71
|
+
expect(os.find((o) => o.id === 'incluster')?.mark).toBe('running')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('stale evidence is greyed rather than carried forward as proof', () => {
|
|
75
|
+
const os = buildOrigins(traceWith([p({ ok: true })]), { stale: true })
|
|
76
|
+
expect(os.find((o) => o.id === 'incluster')?.mark).toBe('stale')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('defaultOrigin', () => {
|
|
81
|
+
it('opens on the strongest origin that actually produced evidence', () => {
|
|
82
|
+
const os = buildOrigins(traceWith([p({ vantage: 'local', path: 'apiserver' })]))
|
|
83
|
+
expect(defaultOrigin(os)).toBe('apiserver')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('prefers real dataplane evidence over a relay when both exist', () => {
|
|
87
|
+
const os = buildOrigins(traceWith([p({ vantage: 'local', path: 'apiserver' }), p({ vantage: 'in-cluster', path: 'data' })]))
|
|
88
|
+
expect(defaultOrigin(os)).toBe('incluster')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('falls back to a usable origin when nothing has run', () => {
|
|
92
|
+
expect(defaultOrigin(buildOrigins(traceWith([])))).toBe('incluster')
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('strongestGap', () => {
|
|
97
|
+
it('names the strongest untested origin as the remaining gap', () => {
|
|
98
|
+
const os = buildOrigins(traceWith([p({ vantage: 'in-cluster', path: 'data' })]))
|
|
99
|
+
// caller is unsupported+blocked and is the strongest thing still missing.
|
|
100
|
+
expect(strongestGap(os)?.id).toBe('caller')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Once the graph and inspector became route-scoped, the rail scoring an origin
|
|
105
|
+
// across EVERY route at once could contradict them: a vantage that failed on one
|
|
106
|
+
// path read as failed while the graph showed it succeeding on the selected one.
|
|
107
|
+
describe('rail marks follow the selected route', () => {
|
|
108
|
+
const twoRoutes = (): RouteResult => ({
|
|
109
|
+
route: 'a.example.com/',
|
|
110
|
+
target: 'a:80',
|
|
111
|
+
outcome: 'unreachable',
|
|
112
|
+
confidence: 'real',
|
|
113
|
+
byVantage: [
|
|
114
|
+
{ vantage: 'in-cluster', path: 'data', outcome: 'verified', confidence: 'real' },
|
|
115
|
+
{ vantage: 'local', path: 'data', outcome: 'unreachable', confidence: 'real' },
|
|
116
|
+
],
|
|
117
|
+
})
|
|
118
|
+
const t = traceWith([
|
|
119
|
+
{ layer: 'http', target: 'a:80', vantage: 'in-cluster', path: 'data', ok: false, tone: 'unhealthy' },
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
it('scores each origin by its own result for THIS route', () => {
|
|
123
|
+
const os = buildOrigins(t, { route: twoRoutes() })
|
|
124
|
+
expect(os.find((o) => o.id === 'incluster')!.mark).toBe('proved')
|
|
125
|
+
expect(os.find((o) => o.id === 'local')!.mark).toBe('failed')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('without a route it falls back to the pooled scan, as before', () => {
|
|
129
|
+
// A failing in-cluster probe is on the trace, so the pooled path marks it failed.
|
|
130
|
+
const os = buildOrigins(t, {})
|
|
131
|
+
expect(os.find((o) => o.id === 'incluster')!.mark).toBe('failed')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('an origin with no result for this route is untested, not inherited', () => {
|
|
135
|
+
const onlyLocal: RouteResult = {
|
|
136
|
+
route: 'a/',
|
|
137
|
+
outcome: 'verified',
|
|
138
|
+
byVantage: [{ vantage: 'local', path: 'data', outcome: 'verified', confidence: 'real' }],
|
|
139
|
+
}
|
|
140
|
+
const os = buildOrigins(traceWith([]), { route: onlyLocal })
|
|
141
|
+
expect(os.find((o) => o.id === 'local')!.mark).toBe('proved')
|
|
142
|
+
expect(os.find((o) => o.id === 'incluster')!.mark).toBe('untested')
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// The laptop vantage used to answer "we can't tell where on the network you are"
|
|
147
|
+
// for every trace forever, even though DNS had already resolved the address.
|
|
148
|
+
describe('the laptop vantage says where it actually dialled', () => {
|
|
149
|
+
const dns = (scope?: 'public' | 'private' | 'mixed', skipped = false): ProbeResult =>
|
|
150
|
+
({ layer: 'dns', target: 'checkout.example.com', vantage: 'local', ok: true, addressScope: scope, skipped })
|
|
151
|
+
|
|
152
|
+
it('a globally routable address is stated, with the inference it does NOT support', () => {
|
|
153
|
+
const s = localIdentity([dns('public')])
|
|
154
|
+
expect(s).toMatch(/public address/)
|
|
155
|
+
// a public address is not proof the packet crossed the internet
|
|
156
|
+
expect(s).toMatch(/VPN|split-horizon/)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('a private address rules the public internet out', () => {
|
|
160
|
+
expect(localIdentity([dns('private')])).toMatch(/did not come from the public internet/)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('split-horizon resolves both ways and commits to neither', () => {
|
|
164
|
+
expect(localIdentity([dns('mixed')])).toMatch(/both public and private/)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('falls back to the honest unknown when nothing resolved', () => {
|
|
168
|
+
expect(localIdentity([])).toMatch(/can’t tell where/)
|
|
169
|
+
expect(localIdentity([dns(undefined)])).toMatch(/can’t tell where/)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('ignores skipped lookups, which observed nothing', () => {
|
|
173
|
+
expect(localIdentity([dns('public', true)])).toMatch(/can’t tell where/)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('does not make the external vantage supported - that is a different claim', () => {
|
|
177
|
+
const t = traceWith([{ layer: 'dns', target: 'h', vantage: 'local', ok: true, addressScope: 'public' }])
|
|
178
|
+
expect(buildOrigins(t, {}).find((o) => o.id === 'external')!.unsupported).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('Radar-as-a-Pod is not the throwaway probe Job', () => {
|
|
183
|
+
// Both are (in-cluster, data). Collapsed into one origin, Radar's own dials
|
|
184
|
+
// were credited to a Job that was never created, under the namespace default
|
|
185
|
+
// account rather than Radar's own.
|
|
186
|
+
it('routes an explicitly-sourced probe to its own origin', () => {
|
|
187
|
+
expect(originOf(p({ vantage: 'in-cluster', path: 'data', source: 'radar' }))).toBe('radar-incluster')
|
|
188
|
+
expect(originOf(p({ vantage: 'in-cluster', path: 'data', source: 'probe-job' }))).toBe('incluster')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// An older producer sends no source. Radar can only have dialled in-cluster
|
|
192
|
+
// itself when it IS in the cluster; otherwise the only way to get such a
|
|
193
|
+
// probe is the Job. runVantage is the producer's own statement about itself.
|
|
194
|
+
it('resolves an unsourced in-cluster probe from runVantage', () => {
|
|
195
|
+
const probe = p({ vantage: 'in-cluster', path: 'data' })
|
|
196
|
+
expect(originOf(probe, 'in-cluster')).toBe('radar-incluster')
|
|
197
|
+
expect(originOf(probe, 'local')).toBe('incluster')
|
|
198
|
+
expect(originOf(probe)).toBe('incluster')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('never reassigns an apiserver relay, whatever the source', () => {
|
|
202
|
+
expect(originOf(p({ vantage: 'in-cluster', path: 'apiserver', source: 'radar' }), 'in-cluster')).toBe('apiserver')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// Radar not being a Pod is a deployment detail, not a hole in the evidence -
|
|
206
|
+
// the throwaway Job covers the same ground. Listing it as "never tested" pads
|
|
207
|
+
// the coverage caveat beside genuine gaps like "as your application".
|
|
208
|
+
it('omits Radar-in-cluster entirely when Radar runs on a laptop', () => {
|
|
209
|
+
const os = buildOrigins(traceWith([]))
|
|
210
|
+
expect(os.find((o) => o.id === 'radar-incluster')).toBeUndefined()
|
|
211
|
+
expect(os.map((o) => o.id)).toEqual(['caller', 'external', 'incluster', 'local', 'apiserver'])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('offers it once Radar actually runs in the cluster', () => {
|
|
215
|
+
const t = traceWith([p({ vantage: 'in-cluster', path: 'data', source: 'radar', ok: true })])
|
|
216
|
+
t.runVantage = 'in-cluster'
|
|
217
|
+
const radar = buildOrigins(t).find((o) => o.id === 'radar-incluster')
|
|
218
|
+
expect(radar?.unsupported).toBeFalsy()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('describes Radar-in-cluster with Radar\'s own identity, not the Job\'s', () => {
|
|
222
|
+
const t = traceWith([p({ vantage: 'in-cluster', path: 'data', source: 'radar', ok: true })])
|
|
223
|
+
t.runVantage = 'in-cluster'
|
|
224
|
+
const radar = buildOrigins(t).find((o) => o.id === 'radar-incluster')
|
|
225
|
+
expect(radar?.identity).toMatch(/Radar/)
|
|
226
|
+
expect(radar?.identity).not.toMatch(/default account/)
|
|
227
|
+
expect(radar?.mark).toBe('proved')
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('an unanswered capability check is not a denial', () => {
|
|
232
|
+
// The SSAR takes a moment. While it is in flight the answer is UNKNOWN, and
|
|
233
|
+
// saying "not permitted" for those frames claims a denial that was never
|
|
234
|
+
// established - the exact overclaim this view exists to prevent.
|
|
235
|
+
it('reads as untested, not denied, while the check is in flight', () => {
|
|
236
|
+
const t = traceWith([])
|
|
237
|
+
const pending = buildOrigins(t, { inClusterAllowed: undefined }).find((o) => o.id === 'incluster')!
|
|
238
|
+
expect(pending.mark).toBe('untested')
|
|
239
|
+
expect(pending.unavailable).toBeUndefined()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('reads as denied once the check actually says no', () => {
|
|
243
|
+
const t = traceWith([])
|
|
244
|
+
const denied = buildOrigins(t, { inClusterAllowed: false, inClusterDeniedReason: 'needs create jobs' }).find(
|
|
245
|
+
(o) => o.id === 'incluster',
|
|
246
|
+
)!
|
|
247
|
+
expect(denied.mark).toBe('denied')
|
|
248
|
+
expect(denied.unavailable).toMatch(/create jobs/)
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('a demoted Job run is inconclusive, never "never tried"', () => {
|
|
253
|
+
// The producer demotes a probe that DIALLED and was refused/degraded to a
|
|
254
|
+
// skip with skipClass 'informational' - a disposition, not a coverage gap.
|
|
255
|
+
it('maps all-skipped-with-informational to inconclusive', () => {
|
|
256
|
+
const t: Trace = {
|
|
257
|
+
subject: { kind: 'Service', name: 'web', namespace: 'prod' },
|
|
258
|
+
verdict: 'unknown',
|
|
259
|
+
brokenAt: -1,
|
|
260
|
+
upstreams: [],
|
|
261
|
+
downstream: [
|
|
262
|
+
{
|
|
263
|
+
resource: { kind: 'Service', name: 'web', namespace: 'prod' },
|
|
264
|
+
edge: 'service',
|
|
265
|
+
findings: [],
|
|
266
|
+
probes: [
|
|
267
|
+
{ layer: 'http', target: 'web:80', vantage: 'in-cluster', path: 'data', source: 'probe-job', ok: false, skipped: true, skipClass: 'informational', reason: 'kept informational' },
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
}
|
|
272
|
+
const o = buildOrigins(t).find((x) => x.id === 'incluster')!
|
|
273
|
+
expect(o.mark).toBe('inconclusive')
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('an ordinary all-skipped origin still reads blocked', () => {
|
|
277
|
+
const t: Trace = {
|
|
278
|
+
subject: { kind: 'Service', name: 'web', namespace: 'prod' },
|
|
279
|
+
verdict: 'unknown',
|
|
280
|
+
brokenAt: -1,
|
|
281
|
+
upstreams: [],
|
|
282
|
+
downstream: [
|
|
283
|
+
{
|
|
284
|
+
resource: { kind: 'Service', name: 'web', namespace: 'prod' },
|
|
285
|
+
edge: 'service',
|
|
286
|
+
findings: [],
|
|
287
|
+
probes: [
|
|
288
|
+
{ layer: 'tcp', target: '10.96.0.1:80', vantage: 'local', path: 'data', ok: false, skipped: true, reason: 'a ClusterIP is not routable from a laptop' },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
}
|
|
293
|
+
const o = buildOrigins(t).find((x) => x.id === 'local')!
|
|
294
|
+
expect(o.mark).toBe('blocked')
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
describe('first paint agrees with the headline', () => {
|
|
299
|
+
it('an indirect rollup default-selects the API-server vantage', () => {
|
|
300
|
+
expect(headlineEvidenceOrigin({ route: 'GET /', outcome: 'reached', confidence: 'indirect' } as any)).toBe('apiserver')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('a real in-cluster verify default-selects the probe vantage', () => {
|
|
304
|
+
expect(
|
|
305
|
+
headlineEvidenceOrigin({
|
|
306
|
+
route: 'GET /',
|
|
307
|
+
outcome: 'verified',
|
|
308
|
+
confidence: 'real',
|
|
309
|
+
byVantage: [{ vantage: 'in-cluster', path: 'data', source: 'probe-job', outcome: 'verified', confidence: 'real' }],
|
|
310
|
+
} as any),
|
|
311
|
+
).toBe('incluster')
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('an untested route decides nothing', () => {
|
|
315
|
+
expect(headlineEvidenceOrigin({ route: 'GET /', outcome: 'not-tested' } as any)).toBeUndefined()
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
describe('a laptop with nothing to dial says so', () => {
|
|
320
|
+
const base = () => ({
|
|
321
|
+
subject: { kind: 'Service', name: 'shop', namespace: 'store' },
|
|
322
|
+
verdict: 'unknown', brokenAt: -1, upstreams: [],
|
|
323
|
+
downstream: [{ resource: { kind: 'Service', name: 'shop', namespace: 'store' }, edge: 'service', findings: [],
|
|
324
|
+
probes: [{ layer: 'http', target: 'shop:80', vantage: 'local', path: 'apiserver', ok: false, skipped: true, reason: 'x', port: 80 }] }],
|
|
325
|
+
}) as never
|
|
326
|
+
it('after a run with no front door, the local vantage states why it cannot be used', () => {
|
|
327
|
+
const local = buildOrigins(base()).find((o) => o.id === 'local')!
|
|
328
|
+
expect(local.mark).toBe('blocked')
|
|
329
|
+
expect(local.unavailable).toMatch(/no entry point .* from your machine|no Ingress/i)
|
|
330
|
+
})
|
|
331
|
+
it('with no probes at all it stays plainly untested - nothing is claimed', () => {
|
|
332
|
+
const t = base() as { downstream: { probes?: unknown[] }[] }
|
|
333
|
+
t.downstream[0].probes = []
|
|
334
|
+
const local = buildOrigins(t as never).find((o) => o.id === 'local')!
|
|
335
|
+
expect(local.mark).toBe('untested')
|
|
336
|
+
expect(local.unavailable).toBeUndefined()
|
|
337
|
+
})
|
|
338
|
+
it('with a front door the laptop CAN dial, nothing is claimed either', () => {
|
|
339
|
+
const t = base() as { upstreams: unknown[] }
|
|
340
|
+
t.upstreams = [{ resource: { kind: 'Ingress', name: 'in', namespace: 'store' }, edge: 'ingress->service', findings: [] }]
|
|
341
|
+
const local = buildOrigins(t as never).find((o) => o.id === 'local')!
|
|
342
|
+
expect(local.unavailable).toBeUndefined()
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
describe('an externally addressable Service is not told it has no entry point', () => {
|
|
347
|
+
const mkTyped = (serviceType?: string) => ({
|
|
348
|
+
subject: { kind: 'Service', name: 'shop', namespace: 'store' },
|
|
349
|
+
verdict: 'unknown', brokenAt: -1, upstreams: [],
|
|
350
|
+
downstream: [{ resource: { kind: 'Service', name: 'shop', namespace: 'store' }, edge: 'service', findings: [],
|
|
351
|
+
config: { serviceType },
|
|
352
|
+
probes: [{ layer: 'http', target: 'shop:80', vantage: 'local', path: 'apiserver', ok: false, skipped: true, reason: 'x', port: 80 }] }],
|
|
353
|
+
}) as never
|
|
354
|
+
it('LoadBalancer: the claim is about Radar, not the Service', () => {
|
|
355
|
+
const local = buildOrigins(mkTyped('LoadBalancer')).find((o) => o.id === 'local')!
|
|
356
|
+
expect(local.unavailable).toMatch(/doesn.t dial/i)
|
|
357
|
+
expect(local.unavailable).not.toMatch(/no Ingress/i)
|
|
358
|
+
})
|
|
359
|
+
it('ClusterIP keeps the structural no-entry-point wording', () => {
|
|
360
|
+
const local = buildOrigins(mkTyped('ClusterIP')).find((o) => o.id === 'local')!
|
|
361
|
+
expect(local.unavailable).toMatch(/no Ingress/i)
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
describe('a skip reason never crosses backends', () => {
|
|
366
|
+
it('a named backend takes reasons only from its own hop', () => {
|
|
367
|
+
const t = {
|
|
368
|
+
subject: { kind: 'Ingress', name: 'entry', namespace: 'store' },
|
|
369
|
+
verdict: 'unknown', brokenAt: -1, upstreams: [], runVantage: 'local',
|
|
370
|
+
downstream: [
|
|
371
|
+
{ resource: { kind: 'Service', name: 'shop-a', namespace: 'store' }, edge: 'service', findings: [],
|
|
372
|
+
probes: [{ layer: 'http', target: 'port 80', vantage: 'local', path: 'apiserver', ok: false, skipped: true, reason: 'A-side reason', port: 80 }] },
|
|
373
|
+
{ resource: { kind: 'Service', name: 'shop-b', namespace: 'store' }, edge: 'service', findings: [],
|
|
374
|
+
probes: [{ layer: 'http', target: 'port 80', vantage: 'local', path: 'apiserver', ok: false, skipped: true, reason: 'B-side reason', port: 80 }] },
|
|
375
|
+
],
|
|
376
|
+
} as never
|
|
377
|
+
expect(originSkipReason(t, 'apiserver', { route: 'r', target: 'shop-b:80', outcome: 'not-tested' } as never)).toBe('B-side reason')
|
|
378
|
+
expect(originSkipReason(t, 'apiserver', { route: 'r', target: 'shop-a:80', outcome: 'not-tested' } as never)).toBe('A-side reason')
|
|
379
|
+
})
|
|
380
|
+
it("a named backend with no skips of its own borrows nothing", () => {
|
|
381
|
+
const t = {
|
|
382
|
+
subject: { kind: 'Ingress', name: 'entry', namespace: 'store' },
|
|
383
|
+
verdict: 'unknown', brokenAt: -1, upstreams: [], runVantage: 'local',
|
|
384
|
+
downstream: [
|
|
385
|
+
{ resource: { kind: 'Service', name: 'shop-a', namespace: 'store' }, edge: 'service', findings: [],
|
|
386
|
+
probes: [{ layer: 'http', target: 'port 80', vantage: 'local', path: 'apiserver', ok: false, skipped: true, reason: 'A-side reason', port: 80 }] },
|
|
387
|
+
{ resource: { kind: 'Service', name: 'shop-b', namespace: 'store' }, edge: 'service', findings: [], probes: [] },
|
|
388
|
+
],
|
|
389
|
+
} as never
|
|
390
|
+
expect(originSkipReason(t, 'apiserver', { route: 'r', target: 'shop-b:80', outcome: 'not-tested' } as never)).toBeUndefined()
|
|
391
|
+
})
|
|
392
|
+
})
|