@skyhook-io/k8s-ui 1.14.4 → 1.14.6
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/audit/AuditCard.tsx +18 -7
- package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
- package/src/components/charts/AreaChart.test.tsx +173 -0
- package/src/components/charts/AreaChart.tsx +208 -53
- package/src/components/charts/PrometheusChartsView.test.tsx +36 -0
- package/src/components/charts/PrometheusChartsView.tsx +205 -76
- package/src/components/charts/SeriesLegend.tsx +8 -3
- package/src/components/charts/annotations.ts +59 -0
- package/src/components/charts/axis.test.ts +48 -0
- package/src/components/charts/axis.ts +62 -0
- package/src/components/charts/colors.test.ts +27 -0
- package/src/components/charts/colors.ts +23 -0
- package/src/components/charts/format.test.ts +19 -0
- package/src/components/charts/format.ts +21 -1
- package/src/components/charts/index.ts +3 -1
- package/src/components/charts/nearestSample.test.ts +31 -0
- package/src/components/charts/nearestSample.ts +15 -0
- package/src/components/charts/types.ts +12 -0
- package/src/components/checks/ChecksView.tsx +20 -2
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +141 -0
- package/src/components/gitops/health-provenance.test.ts +54 -0
- package/src/components/gitops/health-provenance.ts +24 -0
- package/src/components/gitops/index.ts +2 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -21
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +59 -8
- package/src/components/gitops/tree/merge.test.ts +34 -0
- package/src/components/gitops/tree/merge.ts +7 -0
- package/src/components/resources/HPADiagnosisSummary.test.tsx +178 -0
- package/src/components/resources/HPADiagnosisSummary.tsx +218 -0
- package/src/components/resources/ResourcesView.tsx +67 -2
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/index.ts +7 -0
- package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +8 -1
- package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
- package/src/components/resources/renderers/HPARenderer.tsx +4 -57
- package/src/components/resources/renderers/LimitRangeRenderer.test.tsx +156 -0
- package/src/components/resources/renderers/LimitRangeRenderer.tsx +277 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +24 -1
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +9 -16
- package/src/components/resources/renderers/NetworkPolicyRenderer.test.tsx +75 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +50 -36
- package/src/components/resources/renderers/PodRenderer.tsx +11 -0
- package/src/components/resources/renderers/ReflectorSection.test.tsx +281 -0
- package/src/components/resources/renderers/ReflectorSection.tsx +296 -0
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +117 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +219 -56
- package/src/components/resources/renderers/SecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -21
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/renderers/rollout/CanaryStepTimeline.tsx +199 -0
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +116 -0
- package/src/components/resources/resource-utils.ts +18 -9
- package/src/components/shared/CreateResourceDialog.tsx +231 -11
- package/src/components/shared/DrainPlanDialog.test.tsx +154 -0
- package/src/components/shared/DrainPlanDialog.tsx +297 -0
- package/src/components/shared/ResourceActionsBar.tsx +53 -26
- package/src/components/shared/ResourceRendererDispatch.test.tsx +45 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +27 -6
- package/src/components/shared/index.ts +4 -0
- package/src/components/topology/K8sResourceNode.test.ts +9 -0
- package/src/components/topology/K8sResourceNode.tsx +56 -5
- package/src/components/ui/Badge.tsx +5 -1
- package/src/components/ui/FetchResult.test.tsx +20 -2
- package/src/components/ui/FetchResult.tsx +33 -3
- package/src/components/ui/FreshnessControl.tsx +8 -2
- package/src/components/ui/index.ts +5 -0
- package/src/components/ui/monacoRuntime.ts +14 -0
- package/src/components/workload/WorkloadView.tsx +2 -2
- package/src/monaco-deep.d.ts +10 -0
- package/src/theme/components.css +16 -0
- package/src/types/core.ts +38 -11
- package/src/types/gitops-insights.ts +23 -0
- package/src/types/gitops-tree.ts +29 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +20 -1
- package/src/utils/navigation.ts +25 -0
- package/src/utils/network-policy.test.ts +42 -0
- package/src/utils/network-policy.ts +59 -0
- package/src/utils/reflector.ts +139 -0
- package/src/utils/yaml-file-import.test.ts +189 -0
- package/src/utils/yaml-file-import.ts +95 -0
- package/src/components/logs/JsonLogLine.tsx +0 -93
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Copy } from "lucide-react";
|
|
3
|
+
import type { ResourceRef } from "../../../types";
|
|
4
|
+
import { pluralize } from "../../../utils/pluralize";
|
|
5
|
+
import { formatRelativeAgeTime } from "../../../utils/format";
|
|
6
|
+
import {
|
|
7
|
+
hasReflectorDetails,
|
|
8
|
+
parseReflector,
|
|
9
|
+
type Reflection,
|
|
10
|
+
type ReflectorResource,
|
|
11
|
+
} from "../../../utils/reflector";
|
|
12
|
+
export {
|
|
13
|
+
hasReflectorDetails,
|
|
14
|
+
isReflectorMirror,
|
|
15
|
+
reflectorEditNotice,
|
|
16
|
+
} from "../../../utils/reflector";
|
|
17
|
+
import { Badge } from "../../ui/Badge";
|
|
18
|
+
import {
|
|
19
|
+
AlertBanner,
|
|
20
|
+
Property,
|
|
21
|
+
PropertyList,
|
|
22
|
+
ResourceLink,
|
|
23
|
+
Section,
|
|
24
|
+
} from "../../ui/drawer-components";
|
|
25
|
+
|
|
26
|
+
interface ReflectorSectionProps {
|
|
27
|
+
data: ReflectorResource;
|
|
28
|
+
reflection?: Reflection;
|
|
29
|
+
onNavigate?: (ref: ResourceRef) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function NamespaceRule({
|
|
33
|
+
patterns,
|
|
34
|
+
selector,
|
|
35
|
+
}: {
|
|
36
|
+
patterns?: string;
|
|
37
|
+
selector?: string;
|
|
38
|
+
}) {
|
|
39
|
+
if (!patterns && !selector) return <>No namespace restriction configured</>;
|
|
40
|
+
return (
|
|
41
|
+
<span className="space-y-1 block">
|
|
42
|
+
{patterns && (
|
|
43
|
+
<span className="block break-all">
|
|
44
|
+
<span className="text-theme-text-tertiary">Name patterns: </span>
|
|
45
|
+
{patterns}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
{patterns && selector && (
|
|
49
|
+
<span className="block text-theme-text-tertiary">or</span>
|
|
50
|
+
)}
|
|
51
|
+
{selector && (
|
|
52
|
+
<span className="block break-all">
|
|
53
|
+
<span className="text-theme-text-tertiary">Label selector: </span>
|
|
54
|
+
{selector}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</span>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function ReflectorSection({
|
|
62
|
+
data,
|
|
63
|
+
reflection,
|
|
64
|
+
onNavigate,
|
|
65
|
+
}: ReflectorSectionProps) {
|
|
66
|
+
const [showAll, setShowAll] = useState(false);
|
|
67
|
+
if (!hasReflectorDetails(data, reflection)) return null;
|
|
68
|
+
const {
|
|
69
|
+
value,
|
|
70
|
+
hasDeclaration,
|
|
71
|
+
isMirror,
|
|
72
|
+
allowed,
|
|
73
|
+
autoEnabled,
|
|
74
|
+
automatic,
|
|
75
|
+
mirrors,
|
|
76
|
+
sourceSettings,
|
|
77
|
+
copiedVersion,
|
|
78
|
+
sourceVersion,
|
|
79
|
+
warnings,
|
|
80
|
+
} = parseReflector(data, reflection);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Section
|
|
84
|
+
title={
|
|
85
|
+
mirrors.length
|
|
86
|
+
? `Reflector (${pluralize(mirrors.length, "visible mirror")})`
|
|
87
|
+
: "Reflector"
|
|
88
|
+
}
|
|
89
|
+
icon={Copy}
|
|
90
|
+
defaultExpanded={warnings.length > 0}
|
|
91
|
+
>
|
|
92
|
+
<div className="space-y-3">
|
|
93
|
+
<PropertyList>
|
|
94
|
+
<Property
|
|
95
|
+
label="Role"
|
|
96
|
+
value={
|
|
97
|
+
<Badge tone="structural">
|
|
98
|
+
{isMirror
|
|
99
|
+
? automatic === true
|
|
100
|
+
? "Automatic mirror"
|
|
101
|
+
: automatic === false
|
|
102
|
+
? "Manual mirror"
|
|
103
|
+
: "Mirror"
|
|
104
|
+
: hasDeclaration
|
|
105
|
+
? "Invalid source reference"
|
|
106
|
+
: "Source"}
|
|
107
|
+
</Badge>
|
|
108
|
+
}
|
|
109
|
+
/>
|
|
110
|
+
{sourceSettings && (
|
|
111
|
+
<>
|
|
112
|
+
<Property
|
|
113
|
+
label="Reflection allowed"
|
|
114
|
+
value={
|
|
115
|
+
allowed === undefined
|
|
116
|
+
? "Invalid annotation"
|
|
117
|
+
: allowed
|
|
118
|
+
? "Yes"
|
|
119
|
+
: "No"
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
<Property
|
|
123
|
+
label="Allowed namespaces"
|
|
124
|
+
value={
|
|
125
|
+
<NamespaceRule
|
|
126
|
+
patterns={value("reflection-allowed-namespaces")}
|
|
127
|
+
selector={value("reflection-allowed-namespaces-selector")}
|
|
128
|
+
/>
|
|
129
|
+
}
|
|
130
|
+
/>
|
|
131
|
+
<Property
|
|
132
|
+
label="Automatic creation"
|
|
133
|
+
value={
|
|
134
|
+
autoEnabled === undefined
|
|
135
|
+
? "Invalid annotation"
|
|
136
|
+
: autoEnabled
|
|
137
|
+
? "Enabled"
|
|
138
|
+
: "Disabled"
|
|
139
|
+
}
|
|
140
|
+
/>
|
|
141
|
+
{(autoEnabled ||
|
|
142
|
+
value("reflection-auto-namespaces") ||
|
|
143
|
+
value("reflection-auto-namespaces-selector")) && (
|
|
144
|
+
<Property
|
|
145
|
+
label="Automatic namespaces"
|
|
146
|
+
value={
|
|
147
|
+
<NamespaceRule
|
|
148
|
+
patterns={value("reflection-auto-namespaces")}
|
|
149
|
+
selector={value("reflection-auto-namespaces-selector")}
|
|
150
|
+
/>
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
155
|
+
Automatic destinations must also satisfy the allowed rule.
|
|
156
|
+
Controller version and settings may further limit destinations.
|
|
157
|
+
</p>
|
|
158
|
+
</>
|
|
159
|
+
)}
|
|
160
|
+
{isMirror && (
|
|
161
|
+
<>
|
|
162
|
+
<Property
|
|
163
|
+
label="Copied source version"
|
|
164
|
+
value={copiedVersion || "No version recorded"}
|
|
165
|
+
/>
|
|
166
|
+
<Property
|
|
167
|
+
label="Source version (snapshot)"
|
|
168
|
+
value={sourceVersion}
|
|
169
|
+
/>
|
|
170
|
+
<Property
|
|
171
|
+
label="Recorded copy time"
|
|
172
|
+
value={
|
|
173
|
+
value("reflected-at") ? (
|
|
174
|
+
<span title={value("reflected-at")}>
|
|
175
|
+
{formatRelativeAgeTime(value("reflected-at"))}
|
|
176
|
+
</span>
|
|
177
|
+
) : undefined
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
{copiedVersion && sourceVersion && (
|
|
181
|
+
<p className="text-xs text-theme-text-secondary">
|
|
182
|
+
{copiedVersion === sourceVersion
|
|
183
|
+
? "The recorded version matches the source snapshot."
|
|
184
|
+
: "The recorded version differs from the source snapshot."}{" "}
|
|
185
|
+
Cached versions can lag reconciliation; matching versions do
|
|
186
|
+
not prove matching contents.
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
</>
|
|
190
|
+
)}
|
|
191
|
+
</PropertyList>
|
|
192
|
+
{mirrors.length > 0 && (
|
|
193
|
+
<div className="space-y-2">
|
|
194
|
+
<div className="text-sm text-theme-text-secondary">
|
|
195
|
+
Visible mirrors{" "}
|
|
196
|
+
<Badge tone="structural" size="sm">
|
|
197
|
+
{mirrors.length}
|
|
198
|
+
</Badge>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="space-y-1 max-h-60 overflow-y-auto text-sm">
|
|
201
|
+
{(showAll ? mirrors : mirrors.slice(0, 5)).map((ref) => (
|
|
202
|
+
<div key={`${ref.namespace}/${ref.name}`} className="break-all">
|
|
203
|
+
<ResourceLink
|
|
204
|
+
{...ref}
|
|
205
|
+
label={`${ref.namespace}/${ref.name}`}
|
|
206
|
+
onNavigate={onNavigate}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
{mirrors.length > 5 && (
|
|
212
|
+
<button
|
|
213
|
+
className="text-xs text-accent-text hover:underline"
|
|
214
|
+
onClick={() => setShowAll(!showAll)}
|
|
215
|
+
>
|
|
216
|
+
{showAll
|
|
217
|
+
? "Show fewer"
|
|
218
|
+
: `Show all ${mirrors.length} visible mirrors`}
|
|
219
|
+
</button>
|
|
220
|
+
)}
|
|
221
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
222
|
+
Visible relationships only; other mirrors may exist.
|
|
223
|
+
</p>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
{sourceSettings && mirrors.length === 0 && (
|
|
227
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
228
|
+
No mirrors visible in this view.
|
|
229
|
+
</p>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</Section>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function ReflectorSummary({
|
|
237
|
+
data,
|
|
238
|
+
reflection,
|
|
239
|
+
onNavigate,
|
|
240
|
+
}: ReflectorSectionProps) {
|
|
241
|
+
if (!hasReflectorDetails(data, reflection)) return null;
|
|
242
|
+
const { isMirror, declaration, source, warnings } = parseReflector(
|
|
243
|
+
data,
|
|
244
|
+
reflection,
|
|
245
|
+
);
|
|
246
|
+
if (!isMirror && warnings.length === 0) return null;
|
|
247
|
+
return (
|
|
248
|
+
<div className="space-y-2">
|
|
249
|
+
{isMirror && (
|
|
250
|
+
<AlertBanner
|
|
251
|
+
variant="info"
|
|
252
|
+
title="Reflector mirror"
|
|
253
|
+
message={
|
|
254
|
+
<>
|
|
255
|
+
<span className="block break-all">
|
|
256
|
+
Source:{" "}
|
|
257
|
+
{source ? (
|
|
258
|
+
<ResourceLink
|
|
259
|
+
{...source}
|
|
260
|
+
label={`${source.namespace}/${source.name}`}
|
|
261
|
+
onNavigate={onNavigate}
|
|
262
|
+
/>
|
|
263
|
+
) : (
|
|
264
|
+
declaration
|
|
265
|
+
)}
|
|
266
|
+
</span>
|
|
267
|
+
{!source && (
|
|
268
|
+
<span className="block">
|
|
269
|
+
Source is not available in this view; its existence and
|
|
270
|
+
version are unverified.
|
|
271
|
+
</span>
|
|
272
|
+
)}
|
|
273
|
+
<span className="block mt-1">
|
|
274
|
+
Edit the source for lasting changes. Local edits may persist
|
|
275
|
+
until the source changes, then be overwritten.
|
|
276
|
+
</span>
|
|
277
|
+
</>
|
|
278
|
+
}
|
|
279
|
+
/>
|
|
280
|
+
)}
|
|
281
|
+
{warnings.length > 0 && (
|
|
282
|
+
<AlertBanner
|
|
283
|
+
variant="warning"
|
|
284
|
+
title="Check reflection configuration"
|
|
285
|
+
message={
|
|
286
|
+
<ul className="list-disc pl-4 space-y-1">
|
|
287
|
+
{warnings.map((warning) => (
|
|
288
|
+
<li key={warning}>{warning}</li>
|
|
289
|
+
))}
|
|
290
|
+
</ul>
|
|
291
|
+
}
|
|
292
|
+
/>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
RolloutRenderer,
|
|
5
5
|
rolloutActions,
|
|
6
6
|
canaryStepLabel,
|
|
7
|
+
canaryStepTemplateRefs,
|
|
7
8
|
rolloutAnalysisRuns,
|
|
8
9
|
rolloutConditionTone,
|
|
9
10
|
rolloutProblems,
|
|
11
|
+
blueGreenPhases,
|
|
10
12
|
type RolloutAction,
|
|
11
13
|
type RolloutCapabilities,
|
|
12
14
|
} from './RolloutRenderer'
|
|
@@ -173,6 +175,46 @@ describe('RolloutRenderer step display', () => {
|
|
|
173
175
|
})
|
|
174
176
|
})
|
|
175
177
|
|
|
178
|
+
describe('RolloutRenderer analysis history availability', () => {
|
|
179
|
+
const rollout = canaryRollout({ phase: 'Healthy', currentStepIndex: 4 }, 4)
|
|
180
|
+
const forbidden = Object.assign(new Error('forbidden'), { status: 403 })
|
|
181
|
+
const oneRun = [
|
|
182
|
+
{ name: 'run-1', phase: 'Successful', createdAt: '2026-01-01T00:00:00Z', metricsTotal: 1, metricsPassing: 1, metricsNotPassing: 0 },
|
|
183
|
+
]
|
|
184
|
+
// Section keeps collapsed content in the DOM and flattens it to zero height,
|
|
185
|
+
// so "is the text present" says nothing about whether anyone can read it.
|
|
186
|
+
const historySection = (html: string) => html.slice(html.indexOf('AnalysisRun History'))
|
|
187
|
+
const isOpen = (html: string) => historySection(html).includes('grid-template-rows:1fr')
|
|
188
|
+
|
|
189
|
+
it('says the history could not be read instead of rendering nothing', () => {
|
|
190
|
+
const html = renderToString(<RolloutRenderer data={rollout} analysisRunHistoryError={forbidden} />)
|
|
191
|
+
expect(html).toContain('AnalysisRun History')
|
|
192
|
+
expect(html).toContain('permission')
|
|
193
|
+
expect(isOpen(html)).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// React Query keeps the last good data when a refetch fails, so a refusal can
|
|
197
|
+
// arrive on top of rows that are already on screen.
|
|
198
|
+
it('opens and calls itself partial when rows are already showing', () => {
|
|
199
|
+
const html = renderToString(
|
|
200
|
+
<RolloutRenderer data={rollout} analysisRunHistoryError={forbidden} analysisRunHistory={oneRun} />
|
|
201
|
+
)
|
|
202
|
+
expect(html).toContain('run-1')
|
|
203
|
+
expect(html).toContain('may be incomplete')
|
|
204
|
+
expect(isOpen(html)).toBe(true)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('leaves a readable history collapsed, since it is backward-looking', () => {
|
|
208
|
+
const html = renderToString(<RolloutRenderer data={rollout} analysisRunHistory={oneRun} />)
|
|
209
|
+
expect(html).toContain('run-1')
|
|
210
|
+
expect(isOpen(html)).toBe(false)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('renders no history section when there is neither history nor an error', () => {
|
|
214
|
+
expect(renderToString(<RolloutRenderer data={rollout} />)).not.toContain('AnalysisRun History')
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
176
218
|
describe('rolloutProblems', () => {
|
|
177
219
|
it('reports an aborted rollout once, not once per matching branch', () => {
|
|
178
220
|
const problems = rolloutProblems(
|
|
@@ -403,6 +445,81 @@ describe('canaryStepLabel', () => {
|
|
|
403
445
|
})
|
|
404
446
|
})
|
|
405
447
|
|
|
448
|
+
describe('canaryStepTemplateRefs', () => {
|
|
449
|
+
it('reads a canary step analysis.templates[] entry', () => {
|
|
450
|
+
const refs = canaryStepTemplateRefs({ analysis: { templates: [{ templateName: 'success-rate' }] } })
|
|
451
|
+
expect(refs).toEqual([{ name: 'success-rate', clusterScoped: false }])
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
// Real Argo Rollouts schema uses ONE field name (templateName) for both
|
|
455
|
+
// namespaced and cluster-scoped refs, disambiguated by a separate
|
|
456
|
+
// clusterScope boolean — not a distinct clusterTemplateName field.
|
|
457
|
+
it('reads clusterScoped off the clusterScope flag, not a distinct field name', () => {
|
|
458
|
+
const refs = canaryStepTemplateRefs({ analysis: { templates: [{ templateName: 'shared-check', clusterScope: true }] } })
|
|
459
|
+
expect(refs).toEqual([{ name: 'shared-check', clusterScoped: true }])
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
// Experiment spec.analyses[] entries carry templateName/clusterScope flat
|
|
463
|
+
// on the entry itself, not nested under a templates[] array.
|
|
464
|
+
it('reads a flat Experiment spec.analyses[] entry', () => {
|
|
465
|
+
const refs = canaryStepTemplateRefs({ name: 'baseline-check', templateName: 'success-rate', clusterScope: false })
|
|
466
|
+
expect(refs).toEqual([{ name: 'success-rate', clusterScoped: false }])
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('reads a flat, cluster-scoped Experiment analysis entry', () => {
|
|
470
|
+
const refs = canaryStepTemplateRefs({ name: 'shared', templateName: 'shared-check', clusterScope: true })
|
|
471
|
+
expect(refs).toEqual([{ name: 'shared-check', clusterScoped: true }])
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('returns an empty list for a step with no analysis refs', () => {
|
|
475
|
+
expect(canaryStepTemplateRefs({ setWeight: 25 })).toEqual([])
|
|
476
|
+
expect(canaryStepTemplateRefs(null)).toEqual([])
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
describe('blueGreenPhases', () => {
|
|
481
|
+
// Argo Rollouts clears previewSelector once there's no active preview to
|
|
482
|
+
// track — a settled, fully-promoted blueGreen commonly has none, so
|
|
483
|
+
// activeSelector === previewSelector alone never fires here even though
|
|
484
|
+
// cutover genuinely completed.
|
|
485
|
+
it('treats activeSelector matching currentPodHash as promoted, even with an empty previewSelector', () => {
|
|
486
|
+
const data = {
|
|
487
|
+
spec: { strategy: { blueGreen: {} } },
|
|
488
|
+
status: {
|
|
489
|
+
currentPodHash: 'abc123',
|
|
490
|
+
blueGreen: { activeSelector: 'abc123', previewSelector: '' },
|
|
491
|
+
},
|
|
492
|
+
}
|
|
493
|
+
const phases = blueGreenPhases(data)
|
|
494
|
+
expect(phases.find((p) => p.label === 'Active cutover')?.state).toBe('completed')
|
|
495
|
+
expect(phases.every((p) => p.state !== 'current')).toBe(true)
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
it('still recognizes the narrower activeSelector === previewSelector window', () => {
|
|
499
|
+
const data = {
|
|
500
|
+
spec: { strategy: { blueGreen: {} } },
|
|
501
|
+
status: {
|
|
502
|
+
currentPodHash: 'def456',
|
|
503
|
+
blueGreen: { activeSelector: 'abc123', previewSelector: 'abc123' },
|
|
504
|
+
},
|
|
505
|
+
}
|
|
506
|
+
const phases = blueGreenPhases(data)
|
|
507
|
+
expect(phases.find((p) => p.label === 'Active cutover')?.state).toBe('completed')
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
it('is not promoted before either selector matches anything', () => {
|
|
511
|
+
const data = {
|
|
512
|
+
spec: { strategy: { blueGreen: {} } },
|
|
513
|
+
status: {
|
|
514
|
+
currentPodHash: 'abc123',
|
|
515
|
+
blueGreen: { activeSelector: 'old999', previewSelector: 'abc123' },
|
|
516
|
+
},
|
|
517
|
+
}
|
|
518
|
+
const phases = blueGreenPhases(data)
|
|
519
|
+
expect(phases.find((p) => p.label === 'Active cutover')?.state).not.toBe('completed')
|
|
520
|
+
})
|
|
521
|
+
})
|
|
522
|
+
|
|
406
523
|
describe('rolloutConditionTone', () => {
|
|
407
524
|
it('reads Paused=False as the healthy state', () => {
|
|
408
525
|
expect(rolloutConditionTone({ type: 'Paused', status: 'False' })).toBe('ok')
|