@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,198 @@
|
|
|
1
|
+
import { healthColors } from './resource-utils'
|
|
2
|
+
import type { StatusBadge } from './resource-utils'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* UpdateRequest and EphemeralReport — Kyverno's two working records.
|
|
6
|
+
*
|
|
7
|
+
* Both are shaped differently from every other Kyverno object, and in ways the
|
|
8
|
+
* CRD schema does not warn about. Everything here was read off live objects
|
|
9
|
+
* captured with a watch on Kyverno 1.18.2 while the rules that produce them
|
|
10
|
+
* ran, rather than derived from the schema:
|
|
11
|
+
*
|
|
12
|
+
* - A generate UpdateRequest leaves `spec.resource` EMPTY and `spec.rule` an
|
|
13
|
+
* empty string. Its triggers live in `spec.ruleContext[].trigger`, and one
|
|
14
|
+
* request can carry hundreds of them. A mutate UpdateRequest is the
|
|
15
|
+
* opposite: `spec.resource` and `spec.rule` are populated and there is no
|
|
16
|
+
* ruleContext at all.
|
|
17
|
+
* - An EphemeralReport carries its findings in `spec`, not `status`, and its
|
|
18
|
+
* `spec.owner` is present but blank — the subject is only in
|
|
19
|
+
* `metadata.ownerReferences` and in the `audit.kyverno.io/resource.*`
|
|
20
|
+
* labels.
|
|
21
|
+
* - Report timestamps are `{seconds, nanos}` objects, not RFC 3339 strings.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type KyvernoRequestType = 'generate' | 'mutate' | 'unknown'
|
|
25
|
+
|
|
26
|
+
export interface KyvernoTrigger {
|
|
27
|
+
apiVersion?: string
|
|
28
|
+
kind?: string
|
|
29
|
+
name?: string
|
|
30
|
+
namespace?: string
|
|
31
|
+
uid?: string
|
|
32
|
+
/** Whether deleting this trigger also deletes what the rule made for it. */
|
|
33
|
+
deleteDownstream: boolean
|
|
34
|
+
rule?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getKyvernoRequestType(resource: any): KyvernoRequestType {
|
|
38
|
+
const t = resource?.spec?.requestType
|
|
39
|
+
return t === 'generate' || t === 'mutate' ? t : 'unknown'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every resource this request acts on.
|
|
44
|
+
*
|
|
45
|
+
* Reading `spec.resource` alone — the obvious field, and the only one the CRD
|
|
46
|
+
* documents as "the resource" — returns nothing at all for a generate request.
|
|
47
|
+
*/
|
|
48
|
+
export function getKyvernoRequestTriggers(resource: any): KyvernoTrigger[] {
|
|
49
|
+
const ctx = resource?.spec?.ruleContext
|
|
50
|
+
if (Array.isArray(ctx) && ctx.length > 0) {
|
|
51
|
+
return ctx
|
|
52
|
+
.map((c: any) => ({
|
|
53
|
+
...(c?.trigger ?? {}),
|
|
54
|
+
deleteDownstream: c?.deleteDownstream === true,
|
|
55
|
+
rule: c?.rule,
|
|
56
|
+
}))
|
|
57
|
+
.filter((t: KyvernoTrigger) => !!t.name)
|
|
58
|
+
}
|
|
59
|
+
const r = resource?.spec?.resource
|
|
60
|
+
if (r?.name) {
|
|
61
|
+
return [{ ...r, deleteDownstream: resource?.spec?.deleteDownstream === true, rule: resource?.spec?.rule }]
|
|
62
|
+
}
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Resources the request has created so far. Absent on mutate requests. */
|
|
67
|
+
export function getKyvernoRequestGenerated(resource: any): Array<{
|
|
68
|
+
apiVersion?: string
|
|
69
|
+
kind?: string
|
|
70
|
+
name?: string
|
|
71
|
+
namespace?: string
|
|
72
|
+
}> {
|
|
73
|
+
const g = resource?.status?.generatedResources
|
|
74
|
+
return Array.isArray(g) ? g.filter((r: any) => !!r?.name) : []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* State of the request.
|
|
79
|
+
*
|
|
80
|
+
* `Pending` is the one that matters: a request stuck there is generation that
|
|
81
|
+
* has silently stopped, which is the reason to look at this object at all.
|
|
82
|
+
*/
|
|
83
|
+
export function getKyvernoRequestState(resource: any): StatusBadge {
|
|
84
|
+
const state = resource?.status?.state
|
|
85
|
+
switch (state) {
|
|
86
|
+
case 'Completed':
|
|
87
|
+
return { text: 'Completed', color: healthColors.healthy, level: 'healthy' }
|
|
88
|
+
case 'Failed':
|
|
89
|
+
return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
90
|
+
case 'Pending':
|
|
91
|
+
return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
|
|
92
|
+
case 'Skip':
|
|
93
|
+
return { text: 'Skipped', color: healthColors.neutral, level: 'neutral' }
|
|
94
|
+
default:
|
|
95
|
+
return { text: state || 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getKyvernoRequestPolicy(resource: any): string | undefined {
|
|
100
|
+
return resource?.spec?.policy || undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getKyvernoRequestMessage(resource: any): string | undefined {
|
|
104
|
+
const m = resource?.status?.message
|
|
105
|
+
return typeof m === 'string' && m !== '' ? m : undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// EphemeralReport
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
export interface KyvernoReportSubject {
|
|
113
|
+
apiVersion?: string
|
|
114
|
+
kind?: string
|
|
115
|
+
name?: string
|
|
116
|
+
namespace?: string
|
|
117
|
+
uid?: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The resource an EphemeralReport is about.
|
|
122
|
+
*
|
|
123
|
+
* `spec.owner` exists on every one of these and is blank on every one observed,
|
|
124
|
+
* so the owner reference is the real source and the labels are the fallback for
|
|
125
|
+
* a report whose owner has already been deleted.
|
|
126
|
+
*/
|
|
127
|
+
export function getKyvernoReportSubject(resource: any): KyvernoReportSubject | undefined {
|
|
128
|
+
const owner = resource?.spec?.owner
|
|
129
|
+
if (owner?.name) return owner
|
|
130
|
+
|
|
131
|
+
const ref = (resource?.metadata?.ownerReferences ?? [])[0]
|
|
132
|
+
if (ref?.name) {
|
|
133
|
+
return {
|
|
134
|
+
apiVersion: ref.apiVersion,
|
|
135
|
+
kind: ref.kind,
|
|
136
|
+
name: ref.name,
|
|
137
|
+
namespace: resource?.metadata?.namespace,
|
|
138
|
+
uid: ref.uid,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const labels = resource?.metadata?.labels ?? {}
|
|
143
|
+
const kind = labels['audit.kyverno.io/resource.kind']
|
|
144
|
+
if (!kind) return undefined
|
|
145
|
+
const group = labels['audit.kyverno.io/resource.group']
|
|
146
|
+
const version = labels['audit.kyverno.io/resource.version']
|
|
147
|
+
return {
|
|
148
|
+
apiVersion: group ? `${group}/${version}` : version,
|
|
149
|
+
kind,
|
|
150
|
+
namespace: resource?.metadata?.namespace,
|
|
151
|
+
uid: labels['audit.kyverno.io/resource.uid'],
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** How the report was produced — background scan or admission. */
|
|
156
|
+
export function getKyvernoReportSource(resource: any): string | undefined {
|
|
157
|
+
return resource?.metadata?.labels?.['audit.kyverno.io/source'] || undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getKyvernoReportSummary(resource: any): {
|
|
161
|
+
pass: number
|
|
162
|
+
fail: number
|
|
163
|
+
warn: number
|
|
164
|
+
error: number
|
|
165
|
+
skip: number
|
|
166
|
+
total: number
|
|
167
|
+
} {
|
|
168
|
+
const s = resource?.spec?.summary ?? {}
|
|
169
|
+
const n = (v: unknown) => (typeof v === 'number' ? v : 0)
|
|
170
|
+
const out = { pass: n(s.pass), fail: n(s.fail), warn: n(s.warn), error: n(s.error), skip: n(s.skip) }
|
|
171
|
+
return { ...out, total: out.pass + out.fail + out.warn + out.error + out.skip }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function getKyvernoReportResults(resource: any): any[] {
|
|
175
|
+
const r = resource?.spec?.results
|
|
176
|
+
return Array.isArray(r) ? r : []
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Report timestamps are `{seconds, nanos}`, and `new Date(...)` on that object
|
|
181
|
+
* yields Invalid Date rather than failing loudly.
|
|
182
|
+
*/
|
|
183
|
+
export function kyvernoReportTimestamp(ts: any): string | undefined {
|
|
184
|
+
if (typeof ts === 'string') return ts || undefined
|
|
185
|
+
const seconds = ts?.seconds
|
|
186
|
+
if (typeof seconds !== 'number' || seconds <= 0) return undefined
|
|
187
|
+
return new Date(seconds * 1000).toISOString()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** List badge: worst outcome in the report, matching the PolicyReport columns. */
|
|
191
|
+
export function getKyvernoReportStatus(resource: any): StatusBadge {
|
|
192
|
+
const s = getKyvernoReportSummary(resource)
|
|
193
|
+
if (s.total === 0) return { text: 'No results', color: healthColors.neutral, level: 'neutral' }
|
|
194
|
+
if (s.error > 0) return { text: `${s.error} error`, color: healthColors.alert, level: 'alert' }
|
|
195
|
+
if (s.fail > 0) return { text: `${s.fail} fail`, color: healthColors.unhealthy, level: 'unhealthy' }
|
|
196
|
+
if (s.warn > 0) return { text: `${s.warn} warn`, color: healthColors.degraded, level: 'degraded' }
|
|
197
|
+
return { text: `${s.pass} pass`, color: healthColors.healthy, level: 'healthy' }
|
|
198
|
+
}
|
|
@@ -115,14 +115,47 @@ export function getKyvernoPolicyStatus(resource: any): StatusBadge {
|
|
|
115
115
|
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/** The rule blocks whose failure an admission request can be rejected for. */
|
|
119
|
+
function rejectingActions(rule: any): string[] {
|
|
120
|
+
if (!rule) return []
|
|
121
|
+
const actions: string[] = []
|
|
122
|
+
if (rule.validate) actions.push(rule.validate.failureAction)
|
|
123
|
+
if (Array.isArray(rule.verifyImages)) {
|
|
124
|
+
for (const v of rule.verifyImages) actions.push(v?.failureAction)
|
|
125
|
+
}
|
|
126
|
+
return actions.filter(Boolean)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Whether this policy can REJECT an admission request, as Audit or Enforce.
|
|
131
|
+
*
|
|
132
|
+
* Three things make the obvious reading wrong. A rule-level failureAction
|
|
133
|
+
* OVERRIDES `spec.validationFailureAction` rather than deferring to it, so
|
|
134
|
+
* consulting the spec field first answers Audit for a policy whose rule
|
|
135
|
+
* enforces. Image verification carries its own failureAction on
|
|
136
|
+
* `verifyImages[]` rather than under `validate`, so a signing policy blocks
|
|
137
|
+
* without either of the fields the name suggests. And the spec field is
|
|
138
|
+
* defaulted to Audit when absent, so its presence proves nothing.
|
|
139
|
+
*
|
|
140
|
+
* Only validate and verifyImages rules can reject at all, so only they inherit
|
|
141
|
+
* the spec-level action — a mutate or generate rule sitting in the same policy
|
|
142
|
+
* would otherwise carry Enforce into a policy where nothing enforces.
|
|
143
|
+
*
|
|
144
|
+
* Rules can disagree, and one value has to stand for the whole policy. The
|
|
145
|
+
* question worth answering is whether anything here can reject, so any Enforce
|
|
146
|
+
* wins; the Rules section is where a mixed policy is read rule by rule.
|
|
147
|
+
*/
|
|
118
148
|
export function getKyvernoPolicyAction(resource: any): string {
|
|
119
|
-
|
|
120
|
-
const action = resource.spec?.validationFailureAction
|
|
121
|
-
if (action) return action
|
|
122
|
-
// Fallback: check first rule's validate.failureAction
|
|
149
|
+
const specAction = resource.spec?.validationFailureAction
|
|
123
150
|
const rules = resource.spec?.rules || []
|
|
124
|
-
|
|
125
|
-
|
|
151
|
+
if (rules.length === 0) return specAction || 'Audit'
|
|
152
|
+
const rejecting = rules.filter((rule: any) => rule?.validate || rule?.verifyImages)
|
|
153
|
+
// Rules, but none that can reject: the spec-level action governs nothing here.
|
|
154
|
+
if (rejecting.length === 0) return 'Audit'
|
|
155
|
+
for (const rule of rejecting) {
|
|
156
|
+
const overrides = rejectingActions(rule)
|
|
157
|
+
const effective = overrides.length > 0 ? overrides : [specAction]
|
|
158
|
+
if (effective.includes('Enforce')) return 'Enforce'
|
|
126
159
|
}
|
|
127
160
|
return 'Audit'
|
|
128
161
|
}
|
|
@@ -184,7 +217,10 @@ export function getKyvernoPolicyRuleCountByType(resource: any): {
|
|
|
184
217
|
validate: statusCount.validate ?? 0,
|
|
185
218
|
mutate: statusCount.mutate ?? 0,
|
|
186
219
|
generate: statusCount.generate ?? 0,
|
|
187
|
-
|
|
220
|
+
// Kyverno writes this key all-lowercase (`verifyimages`), verified on
|
|
221
|
+
// 1.18.2. Reading the camelCase spelling alone returns 0 for every
|
|
222
|
+
// image-verification policy in existence.
|
|
223
|
+
verifyImages: statusCount.verifyImages ?? statusCount.verifyimages ?? 0,
|
|
188
224
|
}
|
|
189
225
|
}
|
|
190
226
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isVeleroResource,
|
|
10
10
|
getScheduleCronInfo,
|
|
11
11
|
getBackupRepositoryStatus,
|
|
12
|
+
resolveBackupStorageLocation,
|
|
12
13
|
} from './resource-utils-velero'
|
|
13
14
|
|
|
14
15
|
const backup = (phase: string, extra: Record<string, unknown> = {}) => ({
|
|
@@ -235,3 +236,28 @@ describe('getScheduleCronInfo', () => {
|
|
|
235
236
|
expect(getScheduleCronInfo({ spec: {} })).toEqual({ cron: '-', readable: '', malformed: false })
|
|
236
237
|
})
|
|
237
238
|
})
|
|
239
|
+
|
|
240
|
+
describe('which location holds a backup', () => {
|
|
241
|
+
const locations = [
|
|
242
|
+
{ metadata: { name: 'primary' }, spec: { default: true } },
|
|
243
|
+
{ metadata: { name: 'dr-replica' }, spec: { default: false } },
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
it('uses the location the backup names', () => {
|
|
247
|
+
expect(resolveBackupStorageLocation({ spec: { storageLocation: 'dr-replica' } }, locations)).toBe('dr-replica')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// The rule Velero actually applies: the flag, not the name.
|
|
251
|
+
it('resolves an unset location through spec.default', () => {
|
|
252
|
+
expect(resolveBackupStorageLocation({ spec: {} }, locations)).toBe('primary')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// Velero's own fallback when nothing claims the flag.
|
|
256
|
+
it('falls back to the conventional name when no location is marked default', () => {
|
|
257
|
+
expect(resolveBackupStorageLocation({ spec: {} }, [{ metadata: { name: 'only' }, spec: {} }])).toBe('default')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('falls back when the list has not been read', () => {
|
|
261
|
+
expect(resolveBackupStorageLocation({ spec: {} }, undefined)).toBe('default')
|
|
262
|
+
})
|
|
263
|
+
})
|
|
@@ -131,10 +131,34 @@ export function getBackupQueuePosition(resource: any): number | null {
|
|
|
131
131
|
return typeof pos === 'number' ? pos : null
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// The name a Backup carries, applying Velero's fallback by name only. Callers
|
|
135
|
+
// that hold the location list should prefer resolveBackupStorageLocation, which
|
|
136
|
+
// applies Velero's actual rule.
|
|
134
137
|
export function getBackupStorageLocation(resource: any): string {
|
|
135
138
|
return resource.spec?.storageLocation || 'default'
|
|
136
139
|
}
|
|
137
140
|
|
|
141
|
+
// Velero resolves an unset spec.storageLocation to whichever location carries
|
|
142
|
+
// spec.default — a flag, not a name. The literal name "default" is only the
|
|
143
|
+
// fallback when nothing claims the flag, and an install that renamed its
|
|
144
|
+
// default location has no such object at all: the health lookup finds nothing
|
|
145
|
+
// and the link points at a name that does not exist.
|
|
146
|
+
//
|
|
147
|
+
// Mirrors the server-side resolution behind the stored-backups endpoint so the
|
|
148
|
+
// two cannot disagree about which location holds a backup.
|
|
149
|
+
export function getVeleroDefaultLocationName(locations: any[] | undefined): string {
|
|
150
|
+
for (const l of locations ?? []) {
|
|
151
|
+
if (l?.spec?.default === true && l?.metadata?.name) return l.metadata.name
|
|
152
|
+
}
|
|
153
|
+
return ''
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function resolveBackupStorageLocation(resource: any, locations: any[] | undefined): string {
|
|
157
|
+
const declared = resource?.spec?.storageLocation
|
|
158
|
+
if (declared) return declared
|
|
159
|
+
return getVeleroDefaultLocationName(locations) || 'default'
|
|
160
|
+
}
|
|
161
|
+
|
|
138
162
|
export function getBackupIncludedNamespaces(resource: any): string[] {
|
|
139
163
|
return resource.spec?.includedNamespaces || []
|
|
140
164
|
}
|
|
@@ -196,6 +220,22 @@ export function getBackupTTL(resource: any): string {
|
|
|
196
220
|
return resource.spec?.ttl || '-'
|
|
197
221
|
}
|
|
198
222
|
|
|
223
|
+
/**
|
|
224
|
+
* How long this backup allows a single operation before it has outlived its own
|
|
225
|
+
* budget. Shown because the stalled-run issue measures against this field and
|
|
226
|
+
* quotes it — without it on the page, the message cites a number from a spec
|
|
227
|
+
* option the page lists every sibling of.
|
|
228
|
+
*
|
|
229
|
+
* Unset renders as "default", the same word getBackupSnapshotVolumes uses for
|
|
230
|
+
* the same situation — not as a number. Velero's built-in is four hours, but the
|
|
231
|
+
* controller takes `--default-item-operation-timeout` and distributions ship
|
|
232
|
+
* their own, so printing 4h here would state an install setting nothing on this
|
|
233
|
+
* page can read.
|
|
234
|
+
*/
|
|
235
|
+
export function getBackupItemOperationTimeout(resource: any): string {
|
|
236
|
+
return resource.spec?.itemOperationTimeout || 'default'
|
|
237
|
+
}
|
|
238
|
+
|
|
199
239
|
export function getBackupSnapshotVolumes(resource: any): string {
|
|
200
240
|
const val = resource.spec?.snapshotVolumes
|
|
201
241
|
if (val === undefined || val === null) return 'default'
|
|
@@ -483,3 +523,31 @@ export function getBackupRepositoryStatus(resource: any): StatusBadge {
|
|
|
483
523
|
export function getBackupRepositoryType(resource: any): string {
|
|
484
524
|
return resource.spec?.repositoryType || '-'
|
|
485
525
|
}
|
|
526
|
+
|
|
527
|
+
// The namespace whose volumes this repository holds. One repository per
|
|
528
|
+
// (namespace, location, type) is how Velero partitions them, so this is what
|
|
529
|
+
// the repository is *for* rather than where the object lives.
|
|
530
|
+
export function getBackupRepositoryVolumeNamespace(resource: any): string {
|
|
531
|
+
return resource.spec?.volumeNamespace || '-'
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function getBackupRepositoryLocation(resource: any): string {
|
|
535
|
+
return resource.spec?.backupStorageLocation || '-'
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function getBackupRepositoryMaintenanceFrequency(resource: any): string {
|
|
539
|
+
return resource.spec?.maintenanceFrequency || '-'
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function getBackupRepositoryLastMaintenance(resource: any): string {
|
|
543
|
+
const at = resource.status?.lastMaintenanceTime
|
|
544
|
+
if (!at) return '-'
|
|
545
|
+
return formatAge(at)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Why the repository is not ready, in the controller's own words. Velero writes
|
|
549
|
+
// it on the object and nowhere else, so without it the phase is the whole story
|
|
550
|
+
// a reader gets.
|
|
551
|
+
export function getBackupRepositoryMessage(resource: any): string {
|
|
552
|
+
return resource.status?.message || ''
|
|
553
|
+
}
|
|
@@ -16,6 +16,7 @@ import { getBackupStatus as _getBackupStatus, getRestoreStatus as _getRestoreSta
|
|
|
16
16
|
import { getExternalSecretStatus as _getExternalSecretStatus, getClusterExternalSecretStatus as _getClusterExternalSecretStatus, getSecretStoreStatus as _getSecretStoreStatus, getClusterSecretStoreStatus as _getClusterSecretStoreStatus, getSecretStoreProviderType as _getSecretStoreProviderType } from './resource-utils-eso'
|
|
17
17
|
import { getHPATableState, hpaStatusFromState } from './resource-utils-hpa'
|
|
18
18
|
import { getCNPGClusterStatus as _getCNPGClusterStatus, getCNPGBackupStatus as _getCNPGBackupStatus, getCNPGScheduledBackupStatus as _getCNPGScheduledBackupStatus, getCNPGPoolerStatus as _getCNPGPoolerStatus, isApiGroup as _isApiGroup, CNPG_GROUP as _CNPG_GROUP } from './resource-utils-cnpg'
|
|
19
|
+
import { getCalicoIPPoolAllowedUses, getCalicoIPPoolBlockSize, getCalicoIPPoolEncapsulation, getCalicoPolicyNamespaceSelector, getCalicoPolicyServiceAccountSelector, getCalicoPolicyTypes, isCalicoApiVersion, isCalicoPolicyResource } from './resource-utils-calico'
|
|
19
20
|
|
|
20
21
|
// ============================================================================
|
|
21
22
|
// STATUS & HEALTH UTILITIES
|
|
@@ -1492,6 +1493,42 @@ export function getRolloutStatus(rollout: any): StatusBadge {
|
|
|
1492
1493
|
}
|
|
1493
1494
|
}
|
|
1494
1495
|
|
|
1496
|
+
export function getAnalysisRunStatus(run: any): StatusBadge {
|
|
1497
|
+
const phase = run.status?.phase || 'Unknown'
|
|
1498
|
+
switch (phase) {
|
|
1499
|
+
case 'Successful':
|
|
1500
|
+
return { text: 'Successful', color: healthColors.healthy, level: 'healthy' }
|
|
1501
|
+
case 'Running':
|
|
1502
|
+
case 'Pending':
|
|
1503
|
+
return { text: phase, color: healthColors.degraded, level: 'degraded' }
|
|
1504
|
+
case 'Inconclusive':
|
|
1505
|
+
return { text: 'Inconclusive', color: healthColors.alert, level: 'alert' }
|
|
1506
|
+
case 'Failed':
|
|
1507
|
+
case 'Error':
|
|
1508
|
+
return { text: phase, color: healthColors.unhealthy, level: 'unhealthy' }
|
|
1509
|
+
default:
|
|
1510
|
+
return { text: phase, color: healthColors.unknown, level: 'unknown' }
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// Counts only verdict-bearing metrics: Argo excludes dryRun results from the run's
|
|
1515
|
+
// phase, and Running/Pending have not passed yet.
|
|
1516
|
+
export function summarizeAnalysisMetrics(run: any): {
|
|
1517
|
+
total: number
|
|
1518
|
+
passing: number
|
|
1519
|
+
notPassing: number
|
|
1520
|
+
dryRun: number
|
|
1521
|
+
} {
|
|
1522
|
+
const results: any[] = run?.status?.metricResults || []
|
|
1523
|
+
const scored = results.filter((m) => !m.dryRun)
|
|
1524
|
+
return {
|
|
1525
|
+
total: scored.length,
|
|
1526
|
+
passing: scored.filter((m) => m.phase === 'Successful').length,
|
|
1527
|
+
notPassing: scored.filter((m) => ['Failed', 'Error', 'Inconclusive'].includes(m.phase)).length,
|
|
1528
|
+
dryRun: results.length - scored.length,
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1495
1532
|
export function getRolloutStrategy(rollout: any): string {
|
|
1496
1533
|
if (rollout.spec?.strategy?.canary) return 'Canary'
|
|
1497
1534
|
if (rollout.spec?.strategy?.blueGreen) return 'BlueGreen'
|
|
@@ -2114,6 +2151,7 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
2114
2151
|
if (kindLower === 'persistentvolumeclaims') return getPVCStatus(resource).text
|
|
2115
2152
|
if (kindLower === 'persistentvolumes') return getPVStatus(resource).text
|
|
2116
2153
|
if (kindLower === 'rollouts') return getRolloutStatus(resource).text
|
|
2154
|
+
if (kindLower === 'analysisruns') return getAnalysisRunStatus(resource).text
|
|
2117
2155
|
if (kindLower === 'workflows') return getWorkflowStatus(resource).text
|
|
2118
2156
|
if (kindLower === 'cronworkflows') return getCronWorkflowStatus(resource).text
|
|
2119
2157
|
if (kindLower === 'hpas' || kindLower === 'horizontalpodautoscalers') return getHPAStatus(resource).text
|
|
@@ -2200,7 +2238,67 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
2200
2238
|
return getServiceAccountAutomount(resource)
|
|
2201
2239
|
case 'policyTypes':
|
|
2202
2240
|
return getNetworkPolicyTypes(resource)
|
|
2241
|
+
case 'types':
|
|
2242
|
+
if (isCalicoPolicyResource(resource)) return getCalicoPolicyTypes(resource).join(', ')
|
|
2243
|
+
break
|
|
2244
|
+
case 'namespaceSelector':
|
|
2245
|
+
if (isCalicoPolicyResource(resource)) return getCalicoPolicyNamespaceSelector(resource)
|
|
2246
|
+
break
|
|
2247
|
+
case 'serviceAccountSelector':
|
|
2248
|
+
if (isCalicoPolicyResource(resource)) return getCalicoPolicyServiceAccountSelector(resource)
|
|
2249
|
+
break
|
|
2250
|
+
case 'tier':
|
|
2251
|
+
if (isCalicoPolicyResource(resource)) return String(resource.spec?.tier ?? 'default')
|
|
2252
|
+
break
|
|
2253
|
+
case 'cidr':
|
|
2254
|
+
if (isCalicoApiVersion(resource?.apiVersion)) return String(resource.spec?.cidr ?? '')
|
|
2255
|
+
break
|
|
2256
|
+
case 'blockSize':
|
|
2257
|
+
if (isCalicoApiVersion(resource?.apiVersion)) {
|
|
2258
|
+
const size = getCalicoIPPoolBlockSize(resource)
|
|
2259
|
+
return size === undefined ? '' : String(size)
|
|
2260
|
+
}
|
|
2261
|
+
break
|
|
2262
|
+
case 'natOutgoing':
|
|
2263
|
+
if (isCalicoApiVersion(resource?.apiVersion)) return resource.spec?.natOutgoing ? 'Yes' : 'No'
|
|
2264
|
+
break
|
|
2265
|
+
case 'nodeSelector':
|
|
2266
|
+
if (isCalicoApiVersion(resource?.apiVersion)) return String(resource.spec?.nodeSelector ?? 'all()')
|
|
2267
|
+
break
|
|
2268
|
+
case 'allowedUses':
|
|
2269
|
+
if (isCalicoApiVersion(resource?.apiVersion)) return getCalicoIPPoolAllowedUses(resource)
|
|
2270
|
+
break
|
|
2271
|
+
case 'expectedIPs':
|
|
2272
|
+
if (isCalicoApiVersion(resource?.apiVersion)) {
|
|
2273
|
+
return Array.isArray(resource.spec?.expectedIPs) ? resource.spec.expectedIPs.map(String).join(', ') : ''
|
|
2274
|
+
}
|
|
2275
|
+
break
|
|
2276
|
+
case 'profiles':
|
|
2277
|
+
if (isCalicoApiVersion(resource?.apiVersion)) {
|
|
2278
|
+
return Array.isArray(resource.spec?.profiles) ? resource.spec.profiles.map(String).join(', ') : ''
|
|
2279
|
+
}
|
|
2280
|
+
break
|
|
2281
|
+
case 'encapsulation':
|
|
2282
|
+
if (isCalicoApiVersion(resource?.apiVersion)) return getCalicoIPPoolEncapsulation(resource)
|
|
2283
|
+
break
|
|
2284
|
+
case 'disabled':
|
|
2285
|
+
if (isCalicoApiVersion(resource?.apiVersion)) return resource.spec?.disabled === true ? 'Disabled' : 'No'
|
|
2286
|
+
break
|
|
2287
|
+
case 'interfaceName':
|
|
2288
|
+
if (isCalicoApiVersion(resource?.apiVersion)) return String(resource.spec?.interfaceName ?? '')
|
|
2289
|
+
break
|
|
2290
|
+
case 'defaultAction':
|
|
2291
|
+
if (isCalicoApiVersion(resource?.apiVersion)) return String(resource.spec?.defaultAction ?? 'Deny')
|
|
2292
|
+
break
|
|
2293
|
+
case 'order':
|
|
2294
|
+
if (isCalicoPolicyResource(resource) && resource.spec?.order !== undefined) return String(resource.spec.order)
|
|
2295
|
+
break
|
|
2296
|
+
case 'stagedAction':
|
|
2297
|
+
if (isCalicoPolicyResource(resource)) return String(resource.spec?.stagedAction ?? '')
|
|
2298
|
+
break
|
|
2203
2299
|
case 'node':
|
|
2300
|
+
// A Calico HostEndpoint names its node in spec.node; a Pod uses spec.nodeName.
|
|
2301
|
+
if (isCalicoApiVersion(resource?.apiVersion)) return String(resource.spec?.node ?? '')
|
|
2204
2302
|
return resource.spec?.nodeName || ''
|
|
2205
2303
|
case 'version':
|
|
2206
2304
|
if (kindLower === 'nodes') return getNodeVersion(resource)
|