@skyhook-io/k8s-ui 1.11.0 → 1.13.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 +4 -4
- package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
- package/src/components/gitops/GitOpsTableView.tsx +22 -2
- package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
- package/src/components/gitops/detail-helpers.ts +4 -1
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
- package/src/components/resources/ResourcesView.tsx +412 -5
- package/src/components/resources/index.ts +27 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
- package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +71 -4
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +11 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
- package/src/components/resources/renderers/CNPGShared.tsx +27 -0
- package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
- package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
- package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
- package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
- package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
- package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/PodRenderer.tsx +13 -0
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +159 -0
- package/src/components/resources/renderers/PolicySection.tsx +197 -0
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
- package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
- package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
- package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
- package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
- package/src/components/resources/renderers/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +90 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
- package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
- package/src/components/resources/renderers/velero-cells.tsx +29 -7
- package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
- package/src/components/resources/resource-utils-calico.test.ts +215 -0
- package/src/components/resources/resource-utils-calico.ts +261 -0
- package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +117 -0
- package/src/components/resources/resource-utils-cnpg.ts +324 -3
- package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
- package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
- package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
- package/src/components/resources/resource-utils-kyverno.ts +43 -7
- package/src/components/resources/resource-utils-velero.test.ts +26 -0
- package/src/components/resources/resource-utils-velero.ts +68 -0
- package/src/components/resources/resource-utils.ts +98 -0
- package/src/components/shared/ResourceActionsBar.tsx +101 -16
- package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
- package/src/components/shared/index.ts +1 -1
- package/src/components/shared/revision-role-badges.test.ts +120 -0
- package/src/components/topology/K8sResourceNode.test.ts +10 -0
- package/src/components/topology/K8sResourceNode.tsx +22 -0
- package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
- package/src/components/topology/TopologyGraph.tsx +6 -6
- package/src/components/topology/TopologySearch.tsx +7 -0
- package/src/components/topology/layout.ts +3 -0
- package/src/components/trace/ReachabilityView.tsx +1 -0
- package/src/components/trace/TracePanel.tsx +12 -2
- package/src/components/trace/reachFixtures.ts +26 -1
- package/src/components/trace/reachGraphModel.test.ts +20 -0
- package/src/components/trace/reachGraphModel.ts +8 -1
- package/src/components/trace/reachOrigins.test.ts +31 -0
- package/src/components/trace/reachOrigins.ts +11 -1
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/ScheduleValue.test.tsx +80 -0
- package/src/components/ui/ScheduleValue.tsx +85 -0
- package/src/components/ui/drawer-components.test.tsx +17 -1
- package/src/components/ui/drawer-components.tsx +9 -2
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +3 -0
- package/src/types/core.ts +29 -0
- package/src/types/gitops-flux-status.test.ts +163 -0
- package/src/types/gitops.ts +80 -24
- package/src/types/index.ts +1 -0
- package/src/types/policy.ts +193 -0
- package/src/utils/navigation.test.ts +5 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/resource-icons.test.ts +49 -2
- package/src/utils/resource-icons.ts +29 -2
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import {
|
|
4
|
+
RolloutRenderer,
|
|
5
|
+
rolloutActions,
|
|
6
|
+
canaryStepLabel,
|
|
7
|
+
rolloutAnalysisRuns,
|
|
8
|
+
rolloutConditionTone,
|
|
9
|
+
rolloutProblems,
|
|
10
|
+
type RolloutAction,
|
|
11
|
+
type RolloutCapabilities,
|
|
12
|
+
} from './RolloutRenderer'
|
|
13
|
+
|
|
14
|
+
const allCapabilities: RolloutCapabilities = {
|
|
15
|
+
abort: true,
|
|
16
|
+
retry: true,
|
|
17
|
+
promote: true,
|
|
18
|
+
promoteFull: true,
|
|
19
|
+
skipStep: true,
|
|
20
|
+
rollback: true,
|
|
21
|
+
restart: true,
|
|
22
|
+
strategy: 'canary',
|
|
23
|
+
terminating: false,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canaryRollout(status: Record<string, unknown>, steps = 4) {
|
|
27
|
+
return {
|
|
28
|
+
metadata: { name: 'web', namespace: 'prod' },
|
|
29
|
+
spec: {
|
|
30
|
+
replicas: 4,
|
|
31
|
+
strategy: { canary: { steps: Array.from({ length: steps }, () => ({ setWeight: 25 })) } },
|
|
32
|
+
template: { spec: { containers: [{ name: 'web', image: 'web:v2' }] } },
|
|
33
|
+
},
|
|
34
|
+
status,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function offered(data: unknown, capabilities?: RolloutCapabilities): RolloutAction[] {
|
|
39
|
+
return rolloutActions(data, capabilities).map((a) => a.action)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function blockedReason(data: unknown, action: RolloutAction, capabilities = allCapabilities) {
|
|
43
|
+
return rolloutActions(data, capabilities).find((a) => a.action === action)?.blocked
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('rolloutActions capability gating', () => {
|
|
47
|
+
it('offers nothing until the capability probe resolves', () => {
|
|
48
|
+
expect(rolloutActions(canaryRollout({ phase: 'Paused' }), undefined)).toEqual([])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('omits a denied verb rather than disabling it', () => {
|
|
52
|
+
const caps = { ...allCapabilities, promoteFull: false, abort: false }
|
|
53
|
+
expect(offered(canaryRollout({ phase: 'Paused' }), caps)).toEqual(['promote', 'skip-step'])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('omits every verb when the probe denies all of them', () => {
|
|
57
|
+
const caps: RolloutCapabilities = {
|
|
58
|
+
...allCapabilities,
|
|
59
|
+
abort: false,
|
|
60
|
+
retry: false,
|
|
61
|
+
promote: false,
|
|
62
|
+
promoteFull: false,
|
|
63
|
+
skipStep: false,
|
|
64
|
+
}
|
|
65
|
+
expect(offered(canaryRollout({ phase: 'Paused' }), caps)).toEqual([])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('offers only the blueGreen-relevant verbs when the probe reports no skipStep', () => {
|
|
69
|
+
const blueGreen = {
|
|
70
|
+
spec: { strategy: { blueGreen: { activeService: 'active' } } },
|
|
71
|
+
status: { phase: 'Paused', pauseConditions: [{ reason: 'BlueGreenPause' }] },
|
|
72
|
+
}
|
|
73
|
+
const caps = { ...allCapabilities, skipStep: false, strategy: 'blueGreen' }
|
|
74
|
+
expect(offered(blueGreen, caps)).toEqual(['promote', 'promote-full', 'abort'])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('rolloutActions state gating', () => {
|
|
79
|
+
it('swaps Abort for Retry once the rollout is aborted', () => {
|
|
80
|
+
expect(offered(canaryRollout({ phase: 'Paused' }), allCapabilities)).toContain('abort')
|
|
81
|
+
expect(offered(canaryRollout({ phase: 'Paused' }), allCapabilities)).not.toContain('retry')
|
|
82
|
+
|
|
83
|
+
const aborted = offered(canaryRollout({ phase: 'Degraded', abort: true }), allCapabilities)
|
|
84
|
+
expect(aborted).toContain('retry')
|
|
85
|
+
expect(aborted).not.toContain('abort')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('blocks Promote unless the rollout is paused', () => {
|
|
89
|
+
expect(blockedReason(canaryRollout({ phase: 'Paused' }), 'promote')).toBeUndefined()
|
|
90
|
+
expect(blockedReason(canaryRollout({ phase: 'Progressing', currentStepIndex: 1 }), 'promote')).toBe(
|
|
91
|
+
'Rollout is not paused and no analysis is running',
|
|
92
|
+
)
|
|
93
|
+
expect(blockedReason(canaryRollout({ phase: 'Healthy' }), 'promote')).toBe(
|
|
94
|
+
'Rollout is not paused and no analysis is running',
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('blocks the promotion verbs on a fully promoted rollout', () => {
|
|
99
|
+
const settled = canaryRollout({ phase: 'Healthy', currentStepIndex: 4 }, 4)
|
|
100
|
+
expect(blockedReason(settled, 'promote-full')).toBe('Nothing left to promote')
|
|
101
|
+
expect(blockedReason(settled, 'skip-step')).toBe('No canary step left to skip')
|
|
102
|
+
expect(blockedReason(settled, 'abort')).toBe('Rollout is fully promoted — roll back instead')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('routes an aborted rollout through Retry before it will promote again', () => {
|
|
106
|
+
const aborted = canaryRollout({ phase: 'Degraded', abort: true, currentStepIndex: 1 })
|
|
107
|
+
expect(blockedReason(aborted, 'promote-full')).toBe('Retry the rollout first')
|
|
108
|
+
expect(blockedReason(aborted, 'skip-step')).toBe('Retry the rollout first')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// An aborted rollout is Degraded, not Healthy, so isSettled must not swallow it.
|
|
112
|
+
it('does not treat an aborted rollout as settled', () => {
|
|
113
|
+
const abortedHealthy = canaryRollout({ phase: 'Healthy', abort: true })
|
|
114
|
+
expect(blockedReason(abortedHealthy, 'promote-full')).toBe('Retry the rollout first')
|
|
115
|
+
expect(offered(abortedHealthy, allCapabilities)).toContain('retry')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('blocks Skip step once the last step is reached', () => {
|
|
119
|
+
expect(blockedReason(canaryRollout({ phase: 'Paused', currentStepIndex: 3 }, 4), 'skip-step')).toBeUndefined()
|
|
120
|
+
expect(blockedReason(canaryRollout({ phase: 'Paused', currentStepIndex: 4 }, 4), 'skip-step')).toBe(
|
|
121
|
+
'No canary step left to skip',
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('treats a missing currentStepIndex as step 0 with steps remaining', () => {
|
|
126
|
+
expect(blockedReason(canaryRollout({ phase: 'Paused' }, 4), 'skip-step')).toBeUndefined()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('blocks Skip step on a stepless canary', () => {
|
|
130
|
+
expect(blockedReason(canaryRollout({ phase: 'Paused' }, 0), 'skip-step')).toBe('No canary step left to skip')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('survives a Rollout with no status at all', () => {
|
|
134
|
+
expect(() => rolloutActions({ spec: {} }, allCapabilities)).not.toThrow()
|
|
135
|
+
expect(blockedReason({ spec: {} }, 'promote')).toBe(
|
|
136
|
+
'Rollout is not paused and no analysis is running',
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('rolloutProblems', () => {
|
|
142
|
+
it('reports an aborted rollout once, not once per matching branch', () => {
|
|
143
|
+
const problems = rolloutProblems(
|
|
144
|
+
canaryRollout({
|
|
145
|
+
phase: 'Degraded',
|
|
146
|
+
abort: true,
|
|
147
|
+
message: 'RolloutAborted: Rollout aborted update to revision 6',
|
|
148
|
+
}),
|
|
149
|
+
)
|
|
150
|
+
expect(problems).toEqual([
|
|
151
|
+
{ color: 'red', message: 'RolloutAborted: Rollout aborted update to revision 6' },
|
|
152
|
+
])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('collapses a Progressing condition that echoes status.message', () => {
|
|
156
|
+
const problems = rolloutProblems(
|
|
157
|
+
canaryRollout({
|
|
158
|
+
phase: 'Degraded',
|
|
159
|
+
message: 'ProgressDeadlineExceeded: ReplicaSet has timed out',
|
|
160
|
+
conditions: [
|
|
161
|
+
{
|
|
162
|
+
type: 'Progressing',
|
|
163
|
+
status: 'False',
|
|
164
|
+
reason: 'ProgressDeadlineExceeded',
|
|
165
|
+
message: 'ProgressDeadlineExceeded: ReplicaSet has timed out',
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
170
|
+
expect(problems).toHaveLength(1)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('keeps genuinely different messages apart', () => {
|
|
174
|
+
const problems = rolloutProblems(
|
|
175
|
+
canaryRollout({
|
|
176
|
+
phase: 'Degraded',
|
|
177
|
+
abort: true,
|
|
178
|
+
message: 'RolloutAborted: analysis failed',
|
|
179
|
+
canary: { currentStepAnalysisRunStatus: { name: 'ar-1', status: 'Failed', message: 'error rate 12%' } },
|
|
180
|
+
}),
|
|
181
|
+
)
|
|
182
|
+
expect(problems).toHaveLength(2)
|
|
183
|
+
expect(problems[1].message).toBe('Step analysis failed (ar-1): error rate 12%')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('names every pause reason with how long it has been waiting', () => {
|
|
187
|
+
const problems = rolloutProblems(
|
|
188
|
+
canaryRollout({
|
|
189
|
+
phase: 'Paused',
|
|
190
|
+
pauseConditions: [{ reason: 'CanaryPauseStep', startTime: '2020-01-01T00:00:00Z' }],
|
|
191
|
+
}),
|
|
192
|
+
)
|
|
193
|
+
expect(problems).toHaveLength(1)
|
|
194
|
+
expect(problems[0].color).toBe('yellow')
|
|
195
|
+
expect(problems[0].message).toMatch(/^Rollout is paused: CanaryPauseStep \(since .+\)$/)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('does not report a paused rollout that is actually aborted', () => {
|
|
199
|
+
const problems = rolloutProblems(
|
|
200
|
+
canaryRollout({ phase: 'Paused', abort: true, message: 'aborted mid-step' }),
|
|
201
|
+
)
|
|
202
|
+
expect(problems.map((p) => p.message)).toEqual(['aborted mid-step'])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('reports nothing for a healthy rollout', () => {
|
|
206
|
+
expect(rolloutProblems(canaryRollout({ phase: 'Healthy' }))).toEqual([])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('surfaces an invalid spec', () => {
|
|
210
|
+
const problems = rolloutProblems(
|
|
211
|
+
canaryRollout({
|
|
212
|
+
phase: 'Progressing',
|
|
213
|
+
conditions: [{ type: 'InvalidSpec', status: 'True', message: 'spec.strategy is required' }],
|
|
214
|
+
}),
|
|
215
|
+
)
|
|
216
|
+
expect(problems).toEqual([{ color: 'red', message: 'spec.strategy is required' }])
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe('rolloutAnalysisRuns', () => {
|
|
221
|
+
it('labels every populated slot with its trigger', () => {
|
|
222
|
+
const runs = rolloutAnalysisRuns({
|
|
223
|
+
canary: {
|
|
224
|
+
currentStepAnalysisRunStatus: { name: 'ar-step', status: 'Inconclusive', message: 'no verdict' },
|
|
225
|
+
currentBackgroundAnalysisRunStatus: { name: 'ar-bg', status: 'Successful' },
|
|
226
|
+
},
|
|
227
|
+
blueGreen: {
|
|
228
|
+
prePromotionAnalysisRunStatus: { name: 'ar-pre', status: 'Running' },
|
|
229
|
+
postPromotionAnalysisRunStatus: { name: 'ar-post', status: 'Failed' },
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
expect(runs.map((r) => [r.label, r.name, r.status])).toEqual([
|
|
233
|
+
['Step analysis', 'ar-step', 'Inconclusive'],
|
|
234
|
+
['Background analysis', 'ar-bg', 'Successful'],
|
|
235
|
+
['Pre-promotion analysis', 'ar-pre', 'Running'],
|
|
236
|
+
['Post-promotion analysis', 'ar-post', 'Failed'],
|
|
237
|
+
])
|
|
238
|
+
expect(runs[0].message).toBe('no verdict')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// A slot with no name is a slot the controller has not populated.
|
|
242
|
+
it('drops nameless and absent slots', () => {
|
|
243
|
+
expect(rolloutAnalysisRuns({})).toEqual([])
|
|
244
|
+
expect(rolloutAnalysisRuns(undefined)).toEqual([])
|
|
245
|
+
expect(rolloutAnalysisRuns({ canary: { currentStepAnalysisRunStatus: { status: 'Running' } } })).toEqual([])
|
|
246
|
+
expect(rolloutAnalysisRuns({ canary: { currentStepAnalysisRunStatus: { name: '', status: 'Running' } } })).toEqual(
|
|
247
|
+
[],
|
|
248
|
+
)
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('rolloutActions confirmation gating', () => {
|
|
253
|
+
it('confirms exactly the verbs that shift production traffic in one click', () => {
|
|
254
|
+
const specs = rolloutActions(canaryRollout({ phase: 'Paused' }), allCapabilities)
|
|
255
|
+
const confirmed = specs.filter((a) => a.confirm).map((a) => a.action)
|
|
256
|
+
expect(confirmed.sort()).toEqual(['abort', 'promote-full'])
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe('rolloutActions promote reachability', () => {
|
|
261
|
+
// The controller reports Progressing, not Paused, while a step analysis runs —
|
|
262
|
+
// and promote is what advances past it, so it must not be blocked there.
|
|
263
|
+
it('offers promote while a step analysis is in flight', () => {
|
|
264
|
+
for (const status of ['Running', 'Pending', 'Inconclusive']) {
|
|
265
|
+
const data = canaryRollout({
|
|
266
|
+
phase: 'Progressing',
|
|
267
|
+
currentStepIndex: 2,
|
|
268
|
+
canary: { currentStepAnalysisRunStatus: { name: 'ar-1', status } },
|
|
269
|
+
})
|
|
270
|
+
expect(blockedReason(data, 'promote')).toBeUndefined()
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('still blocks promote on a plain progressing rollout with no analysis', () => {
|
|
275
|
+
const data = canaryRollout({ phase: 'Progressing', currentStepIndex: 1 })
|
|
276
|
+
expect(blockedReason(data, 'promote')).toBe('Rollout is not paused and no analysis is running')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('leaves promote unblocked when paused', () => {
|
|
280
|
+
expect(blockedReason(canaryRollout({ phase: 'Paused' }), 'promote')).toBeUndefined()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// Live state after aborting a Rollout paused on an inconclusive analysis: the
|
|
284
|
+
// analysis status persists, so promote must not ride analysisInFlight past the abort.
|
|
285
|
+
it('blocks promote on an aborted rollout whose analysis stayed inconclusive', () => {
|
|
286
|
+
const data = canaryRollout({
|
|
287
|
+
phase: 'Degraded',
|
|
288
|
+
abort: true,
|
|
289
|
+
currentStepIndex: 0,
|
|
290
|
+
canary: { currentStepAnalysisRunStatus: { name: 'ar-1', status: 'Inconclusive' } },
|
|
291
|
+
})
|
|
292
|
+
expect(blockedReason(data, 'promote')).toBe('Retry the rollout first')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('blocks promote, promote-full and skip-step alike on an aborted rollout', () => {
|
|
296
|
+
const data = canaryRollout({ phase: 'Degraded', abort: true, currentStepIndex: 1 })
|
|
297
|
+
for (const action of ['promote', 'promote-full', 'skip-step']) {
|
|
298
|
+
expect(blockedReason(data, action)).toBe('Retry the rollout first')
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe('canaryStepLabel', () => {
|
|
304
|
+
it('labels every step type Argo defines without leaking JSON', () => {
|
|
305
|
+
const cases: Array<[any, string]> = [
|
|
306
|
+
[{ setWeight: 25 }, 'Set weight: 25%'],
|
|
307
|
+
[{ pause: {} }, 'Pause: until promoted'],
|
|
308
|
+
[{ pause: { duration: '30s' } }, 'Pause: 30s'],
|
|
309
|
+
[{ analysis: { templates: [{ templateName: 'success-rate' }] } }, 'Analysis: success-rate'],
|
|
310
|
+
[
|
|
311
|
+
{ analysis: { templates: [{ clusterTemplateName: 'shared-check' }] } },
|
|
312
|
+
'Analysis: shared-check',
|
|
313
|
+
],
|
|
314
|
+
[
|
|
315
|
+
{ experiment: { templates: [{ name: 'baseline' }], duration: '5m' } },
|
|
316
|
+
'Experiment: baseline for 5m',
|
|
317
|
+
],
|
|
318
|
+
[{ setCanaryScale: { weight: 40 } }, 'Set canary scale: 40%'],
|
|
319
|
+
[{ setCanaryScale: { replicas: 3 } }, 'Set canary scale: 3 replicas'],
|
|
320
|
+
[{ setCanaryScale: { matchTrafficWeight: true } }, 'Set canary scale: match traffic weight'],
|
|
321
|
+
[
|
|
322
|
+
{ setHeaderRoute: { name: 'canary-hdr', match: [{ headerName: 'x-canary' }] } },
|
|
323
|
+
'Header route canary-hdr: x-canary',
|
|
324
|
+
],
|
|
325
|
+
[{ setHeaderRoute: { name: 'canary-hdr', match: [] } }, 'Remove header route: canary-hdr'],
|
|
326
|
+
[
|
|
327
|
+
{ setMirrorRoute: { name: 'mirror', match: [{}], percentage: 20 } },
|
|
328
|
+
'Mirror route mirror (20%)',
|
|
329
|
+
],
|
|
330
|
+
[{ setMirrorRoute: { name: 'mirror' } }, 'Remove mirror route: mirror'],
|
|
331
|
+
[{ plugin: { name: 'acme/step' } }, 'Plugin: acme/step'],
|
|
332
|
+
]
|
|
333
|
+
for (const [step, want] of cases) {
|
|
334
|
+
expect(canaryStepLabel(step)).toBe(want)
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('names an unrecognized step instead of dumping its body', () => {
|
|
339
|
+
const label = canaryStepLabel({ someFutureStep: { nested: { a: 1 } } })
|
|
340
|
+
expect(label).toBe('Unrecognized step: someFutureStep')
|
|
341
|
+
expect(label).not.toContain('{')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('survives malformed steps', () => {
|
|
345
|
+
expect(canaryStepLabel({})).toBe('Unknown step')
|
|
346
|
+
expect(canaryStepLabel(null)).toBe('Unknown step')
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('renders an analysis step as prose in the step list', () => {
|
|
350
|
+
const html = renderToString(
|
|
351
|
+
<RolloutRenderer
|
|
352
|
+
data={{
|
|
353
|
+
metadata: { name: 'web', namespace: 'prod' },
|
|
354
|
+
spec: {
|
|
355
|
+
strategy: {
|
|
356
|
+
canary: {
|
|
357
|
+
steps: [{ analysis: { templates: [{ templateName: 'success-rate' }] } }],
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
template: { spec: { containers: [{ name: 'web', image: 'web:v2' }] } },
|
|
361
|
+
},
|
|
362
|
+
status: { phase: 'Progressing' },
|
|
363
|
+
}}
|
|
364
|
+
/>,
|
|
365
|
+
)
|
|
366
|
+
expect(html).toContain('Analysis: success-rate')
|
|
367
|
+
expect(html).not.toContain('templateName')
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
describe('rolloutConditionTone', () => {
|
|
372
|
+
it('reads Paused=False as the healthy state', () => {
|
|
373
|
+
expect(rolloutConditionTone({ type: 'Paused', status: 'False' })).toBe('ok')
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('warns rather than fails when actually paused', () => {
|
|
377
|
+
expect(rolloutConditionTone({ type: 'Paused', status: 'True' })).toBe('warning')
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('reads InvalidSpec=False as the healthy state', () => {
|
|
381
|
+
expect(rolloutConditionTone({ type: 'InvalidSpec', status: 'False' })).toBe('ok')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('fails on InvalidSpec=True', () => {
|
|
385
|
+
expect(rolloutConditionTone({ type: 'InvalidSpec', status: 'True' })).toBe('fail')
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('defers to the generic rule for ordinary condition types', () => {
|
|
389
|
+
expect(rolloutConditionTone({ type: 'Available', status: 'True' })).toBeUndefined()
|
|
390
|
+
expect(rolloutConditionTone({ type: 'Progressing', status: 'False' })).toBeUndefined()
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('defers when the status is neither True nor False', () => {
|
|
394
|
+
expect(rolloutConditionTone({ type: 'Paused', status: 'Unknown' })).toBeUndefined()
|
|
395
|
+
expect(rolloutConditionTone({ type: 'InvalidSpec' })).toBeUndefined()
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
describe('RolloutRenderer', () => {
|
|
400
|
+
it('renders no action row without an onAction handler', () => {
|
|
401
|
+
const html = renderToString(
|
|
402
|
+
<RolloutRenderer data={canaryRollout({ phase: 'Paused' })} capabilities={allCapabilities} />,
|
|
403
|
+
)
|
|
404
|
+
expect(html).not.toContain('Skip step')
|
|
405
|
+
expect(html).not.toContain('Promote')
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('counts no failing conditions on a settled healthy Rollout', () => {
|
|
409
|
+
const html = renderToString(
|
|
410
|
+
<RolloutRenderer
|
|
411
|
+
data={canaryRollout({
|
|
412
|
+
phase: 'Healthy',
|
|
413
|
+
conditions: [
|
|
414
|
+
{ type: 'Available', status: 'True' },
|
|
415
|
+
{ type: 'Paused', status: 'False' },
|
|
416
|
+
{ type: 'Healthy', status: 'True' },
|
|
417
|
+
{ type: 'Progressing', status: 'True' },
|
|
418
|
+
{ type: 'Completed', status: 'True' },
|
|
419
|
+
],
|
|
420
|
+
})}
|
|
421
|
+
/>,
|
|
422
|
+
)
|
|
423
|
+
expect(html).not.toContain('failing')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('renders the offered verbs when wired', () => {
|
|
427
|
+
const html = renderToString(
|
|
428
|
+
<RolloutRenderer
|
|
429
|
+
data={canaryRollout({ phase: 'Paused' })}
|
|
430
|
+
capabilities={allCapabilities}
|
|
431
|
+
onAction={() => {}}
|
|
432
|
+
/>,
|
|
433
|
+
)
|
|
434
|
+
expect(html).toContain('Promote full')
|
|
435
|
+
expect(html).toContain('Skip step')
|
|
436
|
+
expect(html).toContain('Abort')
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('marks the in-flight action', () => {
|
|
440
|
+
const html = renderToString(
|
|
441
|
+
<RolloutRenderer
|
|
442
|
+
data={canaryRollout({ phase: 'Paused' })}
|
|
443
|
+
capabilities={allCapabilities}
|
|
444
|
+
onAction={() => {}}
|
|
445
|
+
pendingAction="promote"
|
|
446
|
+
/>,
|
|
447
|
+
)
|
|
448
|
+
expect(html).toContain('Promoting…')
|
|
449
|
+
expect(html).toContain('animate-pulse')
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('names the inconclusive AnalysisRun and its message alongside the pause reason', () => {
|
|
453
|
+
const html = renderToString(
|
|
454
|
+
<RolloutRenderer
|
|
455
|
+
data={canaryRollout({
|
|
456
|
+
phase: 'Paused',
|
|
457
|
+
currentStepIndex: 1,
|
|
458
|
+
pauseConditions: [{ reason: 'InconclusiveAnalysisRun' }],
|
|
459
|
+
canary: {
|
|
460
|
+
currentStepAnalysisRunStatus: {
|
|
461
|
+
name: 'web-6c4f-2',
|
|
462
|
+
status: 'Inconclusive',
|
|
463
|
+
message: 'success-rate returned no verdict',
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
})}
|
|
467
|
+
/>,
|
|
468
|
+
)
|
|
469
|
+
expect(html).toContain('InconclusiveAnalysisRun')
|
|
470
|
+
expect(html).toContain('web-6c4f-2')
|
|
471
|
+
expect(html).toContain('success-rate returned no verdict')
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('escalates a failed analysis to an error alert, not a warning', () => {
|
|
475
|
+
const html = renderToString(
|
|
476
|
+
<RolloutRenderer
|
|
477
|
+
data={canaryRollout({
|
|
478
|
+
phase: 'Degraded',
|
|
479
|
+
canary: { currentStepAnalysisRunStatus: { name: 'web-6c4f-2', status: 'Failed' } },
|
|
480
|
+
})}
|
|
481
|
+
/>,
|
|
482
|
+
)
|
|
483
|
+
expect(html).toContain('Step analysis failed')
|
|
484
|
+
expect(html).toContain('Issue Detected')
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
it('renders no Analysis section when no slot is populated', () => {
|
|
488
|
+
const html = renderToString(<RolloutRenderer data={canaryRollout({ phase: 'Healthy' })} />)
|
|
489
|
+
expect(html).not.toContain('Step analysis')
|
|
490
|
+
})
|
|
491
|
+
})
|