@skyhook-io/k8s-ui 1.10.3 → 1.10.5
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/topology/K8sResourceNode.tsx +5 -0
- package/src/components/topology/TopologyGraph.tsx +85 -5
- package/src/components/topology/layout.worker.ts +2 -1
- package/src/components/trace/ReachabilityGraph.tsx +503 -0
- package/src/components/trace/ReachabilityView.tsx +972 -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/reachFixtures.ts +261 -0
- package/src/components/trace/reachGraphModel.test.ts +1407 -0
- package/src/components/trace/reachGraphModel.ts +1521 -0
- package/src/components/trace/reachInspector.test.ts +669 -0
- package/src/components/trace/reachInspector.ts +667 -0
- package/src/components/trace/reachMarks.test.ts +667 -0
- package/src/components/trace/reachMarks.ts +579 -0
- package/src/components/trace/reachOrigins.test.ts +392 -0
- package/src/components/trace/reachOrigins.ts +437 -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 +389 -0
- 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 +38 -0
- package/src/theme/variables.css +5 -0
- package/src/types/core.ts +23 -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/asset-url.test.ts +113 -0
- package/src/utils/asset-url.ts +33 -1
- package/src/utils/inClusterConsent.test.ts +109 -0
- package/src/utils/inClusterConsent.ts +72 -0
- package/src/utils/index.ts +2 -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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { createElement } from 'react'
|
|
3
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
4
|
+
import { routeOutcomeRank, coverageBannerTone, inClusterOutcome, inClusterEligible, RequestIndicator, completedRequestMode } from './TracePanel'
|
|
5
|
+
import type { ProbeResult, RouteResult, Coverage, Trace } from './types'
|
|
6
|
+
|
|
7
|
+
describe('protocol-honest probe copy', () => {
|
|
8
|
+
it('labels a TCP-only test without inventing an HTTP request', () => {
|
|
9
|
+
const html = renderToStaticMarkup(createElement(RequestIndicator, {
|
|
10
|
+
requestMode: 'tcp',
|
|
11
|
+
path: '/healthz',
|
|
12
|
+
onApplyProbePath: () => {},
|
|
13
|
+
}))
|
|
14
|
+
expect(html).toContain('TCP connection')
|
|
15
|
+
expect(html).not.toContain('GET')
|
|
16
|
+
expect(html).not.toContain('Edit the request path')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('does not claim an application request completed when every applicable probe skipped', () => {
|
|
20
|
+
const html = renderToStaticMarkup(createElement(RequestIndicator, {
|
|
21
|
+
requestMode: 'none',
|
|
22
|
+
path: '/healthz',
|
|
23
|
+
onApplyProbePath: () => {},
|
|
24
|
+
}))
|
|
25
|
+
expect(html).toContain('no application request completed from here')
|
|
26
|
+
expect(html).not.toContain('GET')
|
|
27
|
+
expect(html).not.toContain('TCP connection')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('labels TCP routes as TCP in the test matrix', () => {
|
|
31
|
+
const trace: Trace = {
|
|
32
|
+
subject: { kind: 'Service', namespace: 'ns', name: 'database' },
|
|
33
|
+
upstreams: [],
|
|
34
|
+
verdict: 'unknown',
|
|
35
|
+
brokenAt: -1,
|
|
36
|
+
downstream: [{
|
|
37
|
+
resource: { kind: 'Service', namespace: 'ns', name: 'database' },
|
|
38
|
+
edge: 'entry:Service',
|
|
39
|
+
findings: [],
|
|
40
|
+
probes: [
|
|
41
|
+
{ layer: 'http', target: 'port 6379', port: 6379, vantage: 'local', path: 'apiserver', ok: false, skipped: true, reason: 'non-HTTP' },
|
|
42
|
+
{ layer: 'http', target: 'port 5432', port: 5432, vantage: 'local', path: 'apiserver', ok: false, skipped: true, reason: 'non-HTTP' },
|
|
43
|
+
],
|
|
44
|
+
}],
|
|
45
|
+
routes: [
|
|
46
|
+
{ route: 'database', target: 'database:6379', targetNamespace: 'ns', outcome: 'not-tested', inClusterRequest: { protocol: 'tcp' } },
|
|
47
|
+
{ route: 'database', target: 'database:5432', targetNamespace: 'ns', outcome: 'not-tested', inClusterRequest: { protocol: 'tcp' } },
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
expect(completedRequestMode(trace)).toBe('none')
|
|
51
|
+
|
|
52
|
+
trace.downstream[0].probes?.push({
|
|
53
|
+
layer: 'tcp',
|
|
54
|
+
target: 'database:6379',
|
|
55
|
+
port: 6379,
|
|
56
|
+
vantage: 'in-cluster',
|
|
57
|
+
path: 'data',
|
|
58
|
+
ok: true,
|
|
59
|
+
})
|
|
60
|
+
expect(completedRequestMode(trace)).toBe('tcp')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('distinguishes a preliminary TCP connection from a completed HTTPS request', () => {
|
|
64
|
+
const trace: Trace = {
|
|
65
|
+
subject: { kind: 'Service', namespace: 'ns', name: 'secure-api' },
|
|
66
|
+
upstreams: [],
|
|
67
|
+
verdict: 'unknown',
|
|
68
|
+
brokenAt: -1,
|
|
69
|
+
downstream: [{
|
|
70
|
+
resource: { kind: 'Service', namespace: 'ns', name: 'secure-api' },
|
|
71
|
+
edge: 'entry:Service',
|
|
72
|
+
findings: [],
|
|
73
|
+
probes: [{
|
|
74
|
+
layer: 'tcp',
|
|
75
|
+
target: 'secure-api:443',
|
|
76
|
+
port: 443,
|
|
77
|
+
vantage: 'in-cluster',
|
|
78
|
+
path: 'data',
|
|
79
|
+
ok: true,
|
|
80
|
+
}, {
|
|
81
|
+
layer: 'http',
|
|
82
|
+
target: 'port 443',
|
|
83
|
+
port: 443,
|
|
84
|
+
vantage: 'in-cluster',
|
|
85
|
+
path: 'apiserver',
|
|
86
|
+
ok: false,
|
|
87
|
+
skipped: true,
|
|
88
|
+
reason: 'the API-server proxy cannot test HTTPS',
|
|
89
|
+
}],
|
|
90
|
+
}],
|
|
91
|
+
routes: [{
|
|
92
|
+
route: 'secure-api',
|
|
93
|
+
target: 'secure-api:443',
|
|
94
|
+
targetNamespace: 'ns',
|
|
95
|
+
outcome: 'reached',
|
|
96
|
+
inClusterRequest: { protocol: 'https', scheme: 'https', path: '/' },
|
|
97
|
+
}],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
expect(completedRequestMode(trace)).toBe('none')
|
|
101
|
+
const indicator = renderToStaticMarkup(createElement(RequestIndicator, {
|
|
102
|
+
requestMode: completedRequestMode(trace),
|
|
103
|
+
}))
|
|
104
|
+
expect(indicator).toContain('no application request completed')
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('in-cluster test - eligibility + outcome', () => {
|
|
109
|
+
const p = (o: Partial<ProbeResult>): ProbeResult => ({ layer: 'http', target: 't', vantage: 'in-cluster', ok: true, ...o })
|
|
110
|
+
it('eligible: indirect or failed routes, NOT real-verified or benign', () => {
|
|
111
|
+
expect(inClusterEligible({ route: '/', outcome: 'verified', confidence: 'indirect' })).toBe(true)
|
|
112
|
+
expect(inClusterEligible({ route: '/', outcome: 'unreachable', confidence: 'real' })).toBe(true)
|
|
113
|
+
expect(inClusterEligible({ route: '/', outcome: 'verified', confidence: 'real' })).toBe(false)
|
|
114
|
+
expect(inClusterEligible({ route: '/', outcome: 'unreachable', benign: true })).toBe(false)
|
|
115
|
+
})
|
|
116
|
+
it('outcome: a real-dataplane HTTP 2xx is verified+green (the payoff)', () => {
|
|
117
|
+
expect(inClusterOutcome([p({ layer: 'tcp', ok: true }), p({ layer: 'http', ok: true, tone: 'healthy' })]))
|
|
118
|
+
.toEqual({ label: 'verified', tone: 'healthy', severity: 'success' })
|
|
119
|
+
})
|
|
120
|
+
it('outcome: a failed HTTP is unreachable+red; tcp-only is port reachable', () => {
|
|
121
|
+
expect(inClusterOutcome([p({ layer: 'http', ok: false })]).severity).toBe('error')
|
|
122
|
+
expect(inClusterOutcome([p({ layer: 'tcp', ok: true })])).toEqual({ label: 'port reachable', tone: 'healthy', severity: 'success' })
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('coverageBannerTone - the single honest banner tone', () => {
|
|
127
|
+
const cov = (o: Partial<Coverage>): Coverage => ({ tested: 0, passed: 0, failed: 0, skipped: 0, ...o })
|
|
128
|
+
const real = (outcome: RouteResult['outcome']): RouteResult => ({ route: '/', outcome, confidence: 'real' })
|
|
129
|
+
const indirect = (outcome: RouteResult['outcome']): RouteResult => ({ route: '/', outcome, confidence: 'indirect' })
|
|
130
|
+
|
|
131
|
+
it('green ONLY on a real-traffic pass', () => {
|
|
132
|
+
expect(coverageBannerTone(cov({ tested: 1, passed: 1 }), [real('verified')])).toBe('success')
|
|
133
|
+
})
|
|
134
|
+
it('indirect-only all-pass is info, never green', () => {
|
|
135
|
+
expect(coverageBannerTone(cov({ tested: 1, passed: 1 }), [indirect('verified')])).toBe('info')
|
|
136
|
+
})
|
|
137
|
+
it('a real pass beside a SKIPPED (untested) sibling route is NOT green - partial coverage', () => {
|
|
138
|
+
// Mirrors the backend CoverageVerdict (skipped>0 → unknown) and the full-tab banner.
|
|
139
|
+
expect(coverageBannerTone(cov({ tested: 1, passed: 1, skipped: 1 }), [real('verified')])).toBe('info')
|
|
140
|
+
})
|
|
141
|
+
it('a real pass beside a proxy-only (indirect) unreachable sibling is NOT green', () => {
|
|
142
|
+
expect(coverageBannerTone(cov({ tested: 2, passed: 1 }), [real('verified'), indirect('unreachable')])).toBe('info')
|
|
143
|
+
})
|
|
144
|
+
it('zero-tested is info, never green or red', () => {
|
|
145
|
+
expect(coverageBannerTone(cov({ tested: 0, skipped: 2 }), [])).toBe('info')
|
|
146
|
+
})
|
|
147
|
+
it('partial (some passed, some failed) is warning', () => {
|
|
148
|
+
expect(coverageBannerTone(cov({ tested: 2, passed: 1, failed: 1 }), [real('verified'), real('unreachable')])).toBe('warning')
|
|
149
|
+
})
|
|
150
|
+
it('none reachable is error', () => {
|
|
151
|
+
expect(coverageBannerTone(cov({ tested: 1, passed: 0, failed: 1 }), [real('unreachable')])).toBe('error')
|
|
152
|
+
})
|
|
153
|
+
it('all server-error (reached, no pass) is warning not red - matches the degraded verdict', () => {
|
|
154
|
+
expect(coverageBannerTone(cov({ tested: 1, passed: 0, failed: 1 }), [real('server-error')])).toBe('warning')
|
|
155
|
+
})
|
|
156
|
+
it('all-failed-but-BENIGN (intentional scale-to-0) is warning, never red', () => {
|
|
157
|
+
const scaledZero: RouteResult = { route: '/', outcome: 'unreachable', confidence: 'indirect', benign: true }
|
|
158
|
+
expect(coverageBannerTone(cov({ tested: 1, passed: 0, failed: 1 }), [scaledZero])).toBe('warning')
|
|
159
|
+
})
|
|
160
|
+
it('a genuine unreachable (not benign) stays red even next to a benign one', () => {
|
|
161
|
+
const benign: RouteResult = { route: '/a', outcome: 'unreachable', benign: true }
|
|
162
|
+
const realDown: RouteResult = { route: '/b', outcome: 'unreachable', confidence: 'real' }
|
|
163
|
+
expect(coverageBannerTone(cov({ tested: 2, passed: 0, failed: 2 }), [benign, realDown])).toBe('error')
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('routeOutcomeRank - failures first', () => {
|
|
168
|
+
it('sorts unreachable before reached before verified', () => {
|
|
169
|
+
expect(routeOutcomeRank('unreachable')).toBeLessThan(routeOutcomeRank('reached'))
|
|
170
|
+
expect(routeOutcomeRank('reached')).toBeLessThan(routeOutcomeRank('verified'))
|
|
171
|
+
})
|
|
172
|
+
})
|