@skyhook-io/k8s-ui 1.12.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/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
- package/src/components/resources/ResourcesView.tsx +410 -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/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 +47 -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/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/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/PolicyCoverageSection.test.tsx +348 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +59 -2
- package/src/components/resources/renderers/PolicySection.tsx +32 -5
- 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/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +81 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -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 +54 -0
- package/src/components/resources/resource-utils-cnpg.ts +264 -3
- 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/policy.ts +153 -2
- 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,221 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { VeleroBackupRenderer } from './VeleroBackupRenderer'
|
|
4
|
+
|
|
5
|
+
const backup = (location: string) => ({
|
|
6
|
+
metadata: { name: 'nightly', namespace: 'velero' },
|
|
7
|
+
spec: { storageLocation: location, ttl: '720h0m0s' },
|
|
8
|
+
status: { phase: 'Completed', startTimestamp: '2026-08-14T01:00:00Z' },
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A backup cannot see the bucket behind it. Its own status says Completed and
|
|
13
|
+
* keeps saying it after the storage location goes Unavailable, which is exactly
|
|
14
|
+
* when someone opens this page to ask whether they can restore to this point.
|
|
15
|
+
*/
|
|
16
|
+
describe('a backup whose storage location is unreachable', () => {
|
|
17
|
+
it('says so on the backup, not one screen away', () => {
|
|
18
|
+
const html = renderToString(
|
|
19
|
+
<VeleroBackupRenderer data={backup('dr-replica')} storageLocationPhase="Unavailable" />,
|
|
20
|
+
)
|
|
21
|
+
expect(html).toContain('nothing here to restore from')
|
|
22
|
+
expect(html).toContain('Unavailable')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// On a healthy location this would be a warning about nothing, on every backup
|
|
26
|
+
// in the cluster.
|
|
27
|
+
it('stays quiet when the location is fine', () => {
|
|
28
|
+
const html = renderToString(
|
|
29
|
+
<VeleroBackupRenderer data={backup('default')} storageLocationPhase="Available" />,
|
|
30
|
+
)
|
|
31
|
+
expect(html).not.toContain('nothing here to restore from')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// The distinction that matters: a location we have not read is not a healthy
|
|
35
|
+
// one, and must not render as one.
|
|
36
|
+
it('claims nothing while the lookup is unresolved', () => {
|
|
37
|
+
const html = renderToString(<VeleroBackupRenderer data={backup('default')} />)
|
|
38
|
+
expect(html).not.toContain('nothing here to restore from')
|
|
39
|
+
expect(html).not.toContain('Unavailable')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* FailedValidation is the one outcome where Velero usually explains itself, in
|
|
45
|
+
* status.failureReason. Radar's own sentence is the fallback for when it does
|
|
46
|
+
* not — a run rejected before it started leaves no errors, no warnings and no
|
|
47
|
+
* item counts, so without either the page is an empty red banner.
|
|
48
|
+
*/
|
|
49
|
+
describe('a run Velero refused to start', () => {
|
|
50
|
+
const rejected = (failureReason?: string) => ({
|
|
51
|
+
metadata: { name: 'live-rejected', namespace: 'velero' },
|
|
52
|
+
spec: { storageLocation: 'dr-replica' },
|
|
53
|
+
status: {
|
|
54
|
+
phase: 'FailedValidation',
|
|
55
|
+
...(failureReason ? { failureReason } : {}),
|
|
56
|
+
validationErrors: ['Backup storage location "dr-replica" is unavailable'],
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Velero's own words beat ours whenever it supplies them.
|
|
61
|
+
it('leads with the reason Velero gave', () => {
|
|
62
|
+
const html = renderToString(
|
|
63
|
+
<VeleroBackupRenderer data={rejected('backup storage location is in read-only mode')} />,
|
|
64
|
+
)
|
|
65
|
+
expect(html).toContain('backup storage location is in read-only mode')
|
|
66
|
+
expect(html).not.toContain('Velero rejected this backup before it started')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('says nothing was backed up when Velero gave no reason', () => {
|
|
70
|
+
const html = renderToString(<VeleroBackupRenderer data={rejected()} />)
|
|
71
|
+
expect(html).toContain('nothing was backed up')
|
|
72
|
+
// The validation errors are the actionable half either way.
|
|
73
|
+
expect(html).toContain('dr-replica" is unavailable')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Velero resolves an unset spec.storageLocation to whichever location carries
|
|
79
|
+
* spec.default, which is a flag and not a name. An install that renamed its
|
|
80
|
+
* default location has no object called "default" at all, so resolving by name
|
|
81
|
+
* finds nothing: the unavailable-location warning never appears and the link
|
|
82
|
+
* points at something that does not exist.
|
|
83
|
+
*/
|
|
84
|
+
describe('a backup that names no location', () => {
|
|
85
|
+
const unset = {
|
|
86
|
+
metadata: { name: 'nightly', namespace: 'velero' },
|
|
87
|
+
spec: { ttl: '720h0m0s' },
|
|
88
|
+
status: { phase: 'Completed' },
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
it('links the location that actually holds it', () => {
|
|
92
|
+
const html = renderToString(
|
|
93
|
+
<VeleroBackupRenderer data={unset} storageLocationName="primary" onNavigate={() => {}} />,
|
|
94
|
+
)
|
|
95
|
+
expect(html).toContain('primary')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('still warns when that location is the unavailable one', () => {
|
|
99
|
+
const html = renderToString(
|
|
100
|
+
<VeleroBackupRenderer data={unset} storageLocationName="primary" storageLocationPhase="Unavailable" />,
|
|
101
|
+
)
|
|
102
|
+
expect(html).toContain('nothing here to restore from')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Replaced the by-name fallback assertion: it checked for the substring
|
|
106
|
+
// "default", which the "Velero's default location, not yet resolved" sentence
|
|
107
|
+
// also contains — so it passed while documenting the opposite of what the
|
|
108
|
+
// renderer now does. A host that does not resolve gets no asserted name.
|
|
109
|
+
it('asserts no location name when the host does not resolve one', () => {
|
|
110
|
+
const html = renderToString(<VeleroBackupRenderer data={unset} onNavigate={() => {}} />)
|
|
111
|
+
expect(html).toContain('not yet resolved')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Three states, not two. The lookup can be unanswered, answered-and-found, or
|
|
117
|
+
* answered-and-the-location-is-gone — and the last one has no phase to report,
|
|
118
|
+
* so without saying it the backup renders exactly as it would on a healthy
|
|
119
|
+
* bucket.
|
|
120
|
+
*/
|
|
121
|
+
describe('what the page claims before the location lookup answers', () => {
|
|
122
|
+
const unset = {
|
|
123
|
+
metadata: { name: 'nightly', namespace: 'velero' },
|
|
124
|
+
spec: {},
|
|
125
|
+
status: { phase: 'Completed' },
|
|
126
|
+
}
|
|
127
|
+
const named = {
|
|
128
|
+
metadata: { name: 'nightly', namespace: 'velero' },
|
|
129
|
+
spec: { storageLocation: 'dr-replica' },
|
|
130
|
+
status: { phase: 'Completed' },
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Scoped to the Storage Location row: "default" is also the rendered value of
|
|
134
|
+
// two unset fields in Options, so a whole-document match proves nothing.
|
|
135
|
+
const storageCell = (html: string) => {
|
|
136
|
+
const i = html.indexOf('Storage Location')
|
|
137
|
+
if (i < 0) return ''
|
|
138
|
+
// Up to the end of this property row only. A fixed-width window runs into
|
|
139
|
+
// the next Section's header, which is itself a <button>, so every case
|
|
140
|
+
// matched a link whether or not one was rendered.
|
|
141
|
+
const end = html.indexOf('</div>', i)
|
|
142
|
+
return html.slice(i, end < 0 ? undefined : end)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// An unset location is only knowable from the list. Printing "default" before
|
|
146
|
+
// it answers is a guess that is wrong on any install that renamed it.
|
|
147
|
+
it('does not name a location it has not resolved', () => {
|
|
148
|
+
const cell = storageCell(renderToString(<VeleroBackupRenderer data={unset} onNavigate={() => {}} />))
|
|
149
|
+
expect(cell).toContain('not yet resolved')
|
|
150
|
+
expect(cell).not.toContain('>default<')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// A backup that names its own location is known regardless of the lookup.
|
|
154
|
+
it('still shows a location the backup names itself', () => {
|
|
155
|
+
const html = renderToString(<VeleroBackupRenderer data={named} onNavigate={() => {}} />)
|
|
156
|
+
expect(html).toContain('dr-replica')
|
|
157
|
+
expect(html).not.toContain('not yet resolved')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('says so when the location it names is gone', () => {
|
|
161
|
+
const html = renderToString(
|
|
162
|
+
<VeleroBackupRenderer data={named} storageLocationName="dr-replica" storageLocationMissing onNavigate={() => {}} />,
|
|
163
|
+
)
|
|
164
|
+
expect(html).toContain('Not found')
|
|
165
|
+
expect(html).toContain('no longer exists')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// A link to an object that is not there is worse than plain text. Scoped to
|
|
169
|
+
// the cell: every collapsible Section header is itself a <button>, so the
|
|
170
|
+
// whole document always matches.
|
|
171
|
+
it('does not link a location that is gone', () => {
|
|
172
|
+
const missing = storageCell(renderToString(
|
|
173
|
+
<VeleroBackupRenderer data={named} storageLocationName="dr-replica" storageLocationMissing onNavigate={() => {}} />,
|
|
174
|
+
))
|
|
175
|
+
const present = storageCell(renderToString(
|
|
176
|
+
<VeleroBackupRenderer data={named} storageLocationName="dr-replica" onNavigate={() => {}} />,
|
|
177
|
+
))
|
|
178
|
+
expect(present).toContain('<button')
|
|
179
|
+
expect(missing).not.toContain('<button')
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Where the name came from changes what can be said about it. A backup that
|
|
185
|
+
* names its own location and cannot find it has named something missing. A
|
|
186
|
+
* backup that names nothing has not — the name was produced by Velero's
|
|
187
|
+
* fallback rule, and blaming the object for it invents a claim.
|
|
188
|
+
*/
|
|
189
|
+
describe('a location that cannot be found', () => {
|
|
190
|
+
const named = {
|
|
191
|
+
metadata: { name: 'nightly', namespace: 'velero' },
|
|
192
|
+
spec: { storageLocation: 'dr-replica' },
|
|
193
|
+
status: { phase: 'Completed' },
|
|
194
|
+
}
|
|
195
|
+
const unset = {
|
|
196
|
+
metadata: { name: 'nightly', namespace: 'velero' },
|
|
197
|
+
spec: {},
|
|
198
|
+
status: { phase: 'Completed' },
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
it('blames the backup only when the backup named it', () => {
|
|
202
|
+
const html = renderToString(
|
|
203
|
+
<VeleroBackupRenderer data={named} storageLocationName="dr-replica" storageLocationMissing />,
|
|
204
|
+
)
|
|
205
|
+
expect(html).toContain('names a storage location that no longer exists')
|
|
206
|
+
expect(html).toContain('Not found')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// The fallback resolved to the literal "default" because nothing carries
|
|
210
|
+
// spec.default. Saying the backup "names" that is attributing an invented
|
|
211
|
+
// name to the object.
|
|
212
|
+
it('says the namespace has no default when the backup named nothing', () => {
|
|
213
|
+
const html = renderToString(
|
|
214
|
+
<VeleroBackupRenderer data={unset} storageLocationName="default" storageLocationMissing />,
|
|
215
|
+
)
|
|
216
|
+
expect(html).toContain('names no storage location')
|
|
217
|
+
expect(html).toContain('this namespace has none')
|
|
218
|
+
expect(html).toContain('No default location')
|
|
219
|
+
expect(html).not.toContain('names a storage location that no longer exists')
|
|
220
|
+
})
|
|
221
|
+
})
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { VeleroRunMessages, type VeleroRunMessagesFetch } from './VeleroRunMessages'
|
|
1
2
|
import { Archive, Clock, HardDrive, Filter } from 'lucide-react'
|
|
2
|
-
import {
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import { HEALTH_BADGE_COLORS } from '../../../utils/badge-colors'
|
|
5
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
6
|
import {
|
|
4
7
|
getBackupStatus,
|
|
5
|
-
getBackupStorageLocation,
|
|
6
8
|
getBackupIncludedNamespaces,
|
|
7
9
|
getBackupExcludedNamespaces,
|
|
8
10
|
getBackupIncludedResources,
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
getBackupWarnings,
|
|
15
17
|
getBackupValidationErrors,
|
|
16
18
|
getBackupTTL,
|
|
19
|
+
getBackupItemOperationTimeout,
|
|
17
20
|
getBackupSnapshotVolumes,
|
|
18
21
|
getBackupDefaultVolumesToFsBackup,
|
|
19
22
|
getBackupVolumeSnapshotLocations,
|
|
@@ -22,13 +25,48 @@ import {
|
|
|
22
25
|
isBackupPartialFailurePhase,
|
|
23
26
|
} from '../resource-utils-velero'
|
|
24
27
|
import { VeleroPhaseValue } from './velero-cells'
|
|
28
|
+
import { veleroPhaseLabel } from '../resource-utils-velero'
|
|
25
29
|
import { formatAge } from '../resource-utils'
|
|
26
30
|
|
|
27
31
|
interface VeleroBackupRendererProps {
|
|
28
32
|
data: any
|
|
33
|
+
/**
|
|
34
|
+
* The phase of the storage location this backup was written to, when the host
|
|
35
|
+
* looked it up. Undefined means unresolved — not "healthy".
|
|
36
|
+
*
|
|
37
|
+
* A backup cannot observe the health of the bucket holding it. Its own status
|
|
38
|
+
* keeps saying Completed, accurately, while the location goes Unavailable and
|
|
39
|
+
* there is nothing to restore from. This page is where someone checks whether
|
|
40
|
+
* they can go back to this point, so the fact belongs here rather than one
|
|
41
|
+
* screen away.
|
|
42
|
+
*/
|
|
43
|
+
storageLocationPhase?: string
|
|
44
|
+
/**
|
|
45
|
+
* The location this backup is actually stored in, resolved by the host
|
|
46
|
+
* against the location list. An unset `spec.storageLocation` means "whichever
|
|
47
|
+
* location carries spec.default", which this component cannot see.
|
|
48
|
+
*
|
|
49
|
+
* Undefined means the lookup has not answered. A backup that names its own
|
|
50
|
+
* location is still known by name in that state; one that does not is not
|
|
51
|
+
* known at all, and this page says so rather than printing the conventional
|
|
52
|
+
* name as though it had been resolved.
|
|
53
|
+
*/
|
|
54
|
+
storageLocationName?: string
|
|
55
|
+
/**
|
|
56
|
+
* The lookup answered and no location by that name exists. Distinct from an
|
|
57
|
+
* unresolved lookup: a backup whose storage location has been deleted cannot
|
|
58
|
+
* be restored from, and without this it renders exactly like a healthy one.
|
|
59
|
+
*/
|
|
60
|
+
storageLocationMissing?: boolean
|
|
61
|
+
/** Wired by the host, which owns the fetch. Optional: a consumer that does
|
|
62
|
+
* not wire it gets the counts on their own, with no button. */
|
|
63
|
+
messages?: VeleroRunMessagesFetch
|
|
64
|
+
/** Without it the location renders as plain text: the page names a location as
|
|
65
|
+
* the reason nothing can be restored and gives no way to go look at it. */
|
|
66
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
29
67
|
}
|
|
30
68
|
|
|
31
|
-
export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
69
|
+
export function VeleroBackupRenderer({ data, storageLocationPhase, storageLocationName, storageLocationMissing, messages, onNavigate }: VeleroBackupRendererProps) {
|
|
32
70
|
const status = data.status || {}
|
|
33
71
|
const conditions = status.conditions || []
|
|
34
72
|
|
|
@@ -86,7 +124,9 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
|
86
124
|
<AlertBanner
|
|
87
125
|
variant="warning"
|
|
88
126
|
title={`${warnings} Warning(s)`}
|
|
89
|
-
message={
|
|
127
|
+
message={phase === 'Completed'
|
|
128
|
+
? `This backup completed, with ${warnings} warning(s).`
|
|
129
|
+
: `This backup has reported ${warnings} warning(s) so far. It is ${veleroPhaseLabel(phase).toLowerCase()}, so this is not a final count.`}
|
|
90
130
|
/>
|
|
91
131
|
)}
|
|
92
132
|
|
|
@@ -118,6 +158,7 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
|
118
158
|
: '0'
|
|
119
159
|
} />
|
|
120
160
|
</PropertyList>
|
|
161
|
+
{messages && <VeleroRunMessages {...messages} errors={errors} warnings={warnings} />}
|
|
121
162
|
</Section>
|
|
122
163
|
|
|
123
164
|
{/* Progress section (if in progress) */}
|
|
@@ -196,12 +237,83 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
|
196
237
|
{/* Storage section */}
|
|
197
238
|
<Section title="Storage" icon={HardDrive} defaultExpanded>
|
|
198
239
|
<PropertyList>
|
|
199
|
-
|
|
240
|
+
{/* A backup that names its location is known by name whatever the
|
|
241
|
+
lookup is doing. One that does not is only known once the list
|
|
242
|
+
answers, because "unset" means whichever location carries
|
|
243
|
+
spec.default — so until then this says what the field says and
|
|
244
|
+
claims nothing further. */}
|
|
245
|
+
<Property
|
|
246
|
+
label="Storage Location"
|
|
247
|
+
value={(() => {
|
|
248
|
+
const declared = data?.spec?.storageLocation as string | undefined
|
|
249
|
+
const name = storageLocationName || declared
|
|
250
|
+
if (!name) {
|
|
251
|
+
return (
|
|
252
|
+
<span className="text-sm text-theme-text-tertiary">
|
|
253
|
+
Velero’s default location, not yet resolved
|
|
254
|
+
</span>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
return (
|
|
258
|
+
<span className="flex items-center gap-2">
|
|
259
|
+
{/* Not a link when the object is not there: a link to nothing
|
|
260
|
+
is worse than a name. */}
|
|
261
|
+
{storageLocationMissing ? (
|
|
262
|
+
<span className="text-sm font-mono text-theme-text-primary">{name}</span>
|
|
263
|
+
) : (
|
|
264
|
+
<ResourceLink
|
|
265
|
+
name={name}
|
|
266
|
+
kind="BackupStorageLocation"
|
|
267
|
+
namespace={data?.metadata?.namespace ?? ''}
|
|
268
|
+
group="velero.io"
|
|
269
|
+
onNavigate={onNavigate}
|
|
270
|
+
/>
|
|
271
|
+
)}
|
|
272
|
+
{storageLocationMissing && (
|
|
273
|
+
<span className={clsx('badge', HEALTH_BADGE_COLORS.unhealthy)}>
|
|
274
|
+
{declared ? 'Not found' : 'No default location'}
|
|
275
|
+
</span>
|
|
276
|
+
)}
|
|
277
|
+
{storageLocationPhase && storageLocationPhase !== 'Available' && (
|
|
278
|
+
<span className={clsx('badge', HEALTH_BADGE_COLORS.unhealthy)}>{storageLocationPhase}</span>
|
|
279
|
+
)}
|
|
280
|
+
</span>
|
|
281
|
+
)
|
|
282
|
+
})()}
|
|
283
|
+
/>
|
|
284
|
+
{/* A location that is gone is not a location that is unhealthy, and
|
|
285
|
+
Velero writes no phase for one that does not exist — so without
|
|
286
|
+
this the backup renders exactly as it would on a healthy bucket.
|
|
287
|
+
Worded from where the name came from: a backup that names nothing
|
|
288
|
+
has not "named a location that no longer exists", and saying so
|
|
289
|
+
would attribute to the object a name the fallback invented. */}
|
|
290
|
+
{storageLocationMissing && (
|
|
291
|
+
<div className="text-xs text-warning-text">
|
|
292
|
+
{data?.spec?.storageLocation
|
|
293
|
+
? 'This backup names a storage location that no longer exists in this namespace. Velero restores from the location recorded on the backup, so there is nothing here to restore from until it is recreated.'
|
|
294
|
+
: 'This backup names no storage location, which Velero resolves to the one marked default — and this namespace has none. Which location holds it cannot be determined here.'}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
{/* Only when it changes the answer. On a healthy location this would be
|
|
298
|
+
a warning about nothing, on every backup in the cluster. */}
|
|
299
|
+
{storageLocationPhase && storageLocationPhase !== 'Available' && (
|
|
300
|
+
<div className="text-xs text-warning-text">
|
|
301
|
+
{`This backup is stored in a location Velero reports as ${storageLocationPhase}. Its own phase does not change when that happens — until the location recovers, there is nothing here to restore from.`}
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
200
304
|
{vslLocations.length > 0 && (
|
|
201
305
|
<Property label="Volume Snapshot Locations" value={
|
|
202
306
|
<div className="flex flex-wrap gap-1">
|
|
203
307
|
{vslLocations.map((loc: string) => (
|
|
204
|
-
<span key={loc} className="badge-sm bg-theme-hover text-theme-text-secondary">
|
|
308
|
+
<span key={loc} className="badge-sm bg-theme-hover text-theme-text-secondary">
|
|
309
|
+
<ResourceLink
|
|
310
|
+
name={loc}
|
|
311
|
+
kind="VolumeSnapshotLocation"
|
|
312
|
+
namespace={data?.metadata?.namespace ?? ''}
|
|
313
|
+
group="velero.io"
|
|
314
|
+
onNavigate={onNavigate}
|
|
315
|
+
/>
|
|
316
|
+
</span>
|
|
205
317
|
))}
|
|
206
318
|
</div>
|
|
207
319
|
} />
|
|
@@ -213,6 +325,7 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
|
213
325
|
<Section title="Options" icon={Clock}>
|
|
214
326
|
<PropertyList>
|
|
215
327
|
<Property label="TTL" value={getBackupTTL(data)} />
|
|
328
|
+
<Property label="Item Operation Timeout" value={getBackupItemOperationTimeout(data)} />
|
|
216
329
|
<Property label="Snapshot Volumes" value={getBackupSnapshotVolumes(data)} />
|
|
217
330
|
<Property label="Default FS Backup" value={getBackupDefaultVolumesToFsBackup(data)} />
|
|
218
331
|
</PropertyList>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { VeleroBackupRepositoryRenderer } from './VeleroBackupRepositoryRenderer'
|
|
4
|
+
|
|
5
|
+
const repo = (over: { phase?: string; message?: string } = {}) => ({
|
|
6
|
+
metadata: { name: 'repo-notready', namespace: 'velero' },
|
|
7
|
+
spec: {
|
|
8
|
+
repositoryType: 'kopia',
|
|
9
|
+
volumeNamespace: 'legacy-app',
|
|
10
|
+
backupStorageLocation: 'dr-replica',
|
|
11
|
+
maintenanceFrequency: '1h0m0s',
|
|
12
|
+
},
|
|
13
|
+
status: {
|
|
14
|
+
phase: over.phase ?? 'NotReady',
|
|
15
|
+
...(over.message !== undefined ? { message: over.message } : {}),
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A BackupRepository raises `BackupRepositoryNotReady`, and `status.message` is
|
|
21
|
+
* the only place Velero records why. Without a renderer of its own the issue
|
|
22
|
+
* pointed at a generic drawer, so the one fact worth arriving for was the one
|
|
23
|
+
* the page could not show.
|
|
24
|
+
*/
|
|
25
|
+
describe('a repository Velero reports as not ready', () => {
|
|
26
|
+
it('leads with the reason Velero gave', () => {
|
|
27
|
+
const html = renderToString(
|
|
28
|
+
<VeleroBackupRepositoryRenderer
|
|
29
|
+
data={repo({ message: 'error running maintenance: unable to connect to repository' })}
|
|
30
|
+
/>,
|
|
31
|
+
)
|
|
32
|
+
expect(html).toContain('unable to connect to repository')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Velero can set NotReady with no message. Saying nothing would leave a red
|
|
36
|
+
// badge and no consequence, so the page states what not-ready costs.
|
|
37
|
+
it('names the consequence when Velero gave no reason', () => {
|
|
38
|
+
const html = renderToString(<VeleroBackupRepositoryRenderer data={repo()} />)
|
|
39
|
+
expect(html).toContain('legacy-app')
|
|
40
|
+
expect(html).toContain('cannot be written')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('says nothing alarming when the repository is ready', () => {
|
|
44
|
+
const html = renderToString(
|
|
45
|
+
<VeleroBackupRepositoryRenderer data={repo({ phase: 'Ready' })} />,
|
|
46
|
+
)
|
|
47
|
+
expect(html).not.toContain('Repository Not Ready')
|
|
48
|
+
expect(html).not.toContain('cannot be written')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// The same dead end this integration closed on Backup and Restore: naming a
|
|
52
|
+
// Velero object the reader cannot reach.
|
|
53
|
+
it('lets the reader reach the location it writes into', () => {
|
|
54
|
+
const html = renderToString(
|
|
55
|
+
<VeleroBackupRepositoryRenderer data={repo()} onNavigate={() => {}} />,
|
|
56
|
+
)
|
|
57
|
+
expect(html).toContain('dr-replica')
|
|
58
|
+
expect(html).toMatch(/role="button"|<button|cursor-pointer/)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Maintenance frequency alone does not say whether maintenance is happening.
|
|
62
|
+
// The pair does, which is why both are on the page.
|
|
63
|
+
it('shows the maintenance schedule beside the last run', () => {
|
|
64
|
+
const html = renderToString(<VeleroBackupRepositoryRenderer data={repo({ phase: 'Ready' })} />)
|
|
65
|
+
expect(html).toContain('Maintenance Frequency')
|
|
66
|
+
expect(html).toContain('1h0m0s')
|
|
67
|
+
expect(html).toContain('Last Maintenance')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Database, Clock } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
|
+
import {
|
|
4
|
+
getBackupRepositoryStatus,
|
|
5
|
+
getBackupRepositoryType,
|
|
6
|
+
getBackupRepositoryVolumeNamespace,
|
|
7
|
+
getBackupRepositoryLocation,
|
|
8
|
+
getBackupRepositoryMaintenanceFrequency,
|
|
9
|
+
getBackupRepositoryLastMaintenance,
|
|
10
|
+
getBackupRepositoryMessage,
|
|
11
|
+
} from '../resource-utils-velero'
|
|
12
|
+
import { VeleroPhaseValue } from './velero-cells'
|
|
13
|
+
|
|
14
|
+
interface VeleroBackupRepositoryRendererProps {
|
|
15
|
+
data: any
|
|
16
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A BackupRepository is where Velero's file-level backups of a namespace's
|
|
21
|
+
* volumes are written. It raises its own issue when it goes `NotReady`, and
|
|
22
|
+
* `status.message` is the only place the reason for that exists.
|
|
23
|
+
*/
|
|
24
|
+
export function VeleroBackupRepositoryRenderer({ data, onNavigate }: VeleroBackupRepositoryRendererProps) {
|
|
25
|
+
const status = data.status || {}
|
|
26
|
+
const repoStatus = getBackupRepositoryStatus(data)
|
|
27
|
+
const message = getBackupRepositoryMessage(data)
|
|
28
|
+
const volumeNamespace = getBackupRepositoryVolumeNamespace(data)
|
|
29
|
+
const location = getBackupRepositoryLocation(data)
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
{/* The reason, not just the state. A repository that is not ready means
|
|
34
|
+
volume backups for its namespace are not being written, and Velero puts
|
|
35
|
+
that consequence nowhere on the object. */}
|
|
36
|
+
{repoStatus.level === 'unhealthy' && (
|
|
37
|
+
<AlertBanner
|
|
38
|
+
variant="error"
|
|
39
|
+
title="Repository Not Ready"
|
|
40
|
+
message={message
|
|
41
|
+
? message
|
|
42
|
+
: `Velero reported this repository as not ready and gave no reason. Volume backups for ${volumeNamespace === '-' ? 'its namespace' : volumeNamespace} cannot be written while it stays that way.`}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
<Section title="Status" icon={Clock} defaultExpanded>
|
|
47
|
+
<PropertyList>
|
|
48
|
+
<Property label="Phase" value={
|
|
49
|
+
<VeleroPhaseValue status={repoStatus} phase={status.phase || ''} />
|
|
50
|
+
} />
|
|
51
|
+
{/* Velero runs maintenance on its own schedule; a repository whose
|
|
52
|
+
last run is far older than its frequency is the shape of a
|
|
53
|
+
controller that has stopped. The two are shown together so that
|
|
54
|
+
comparison does not need a second screen. */}
|
|
55
|
+
<Property label="Last Maintenance" value={getBackupRepositoryLastMaintenance(data)} />
|
|
56
|
+
<Property label="Maintenance Frequency" value={getBackupRepositoryMaintenanceFrequency(data)} />
|
|
57
|
+
</PropertyList>
|
|
58
|
+
</Section>
|
|
59
|
+
|
|
60
|
+
<Section title="Repository" icon={Database} defaultExpanded>
|
|
61
|
+
<PropertyList>
|
|
62
|
+
<Property label="Type" value={getBackupRepositoryType(data)} />
|
|
63
|
+
{/* What this repository is for. Velero partitions repositories by
|
|
64
|
+
namespace, so this is the namespace whose volumes it holds, not
|
|
65
|
+
the namespace the object lives in. */}
|
|
66
|
+
<Property label="Volume Namespace" value={
|
|
67
|
+
volumeNamespace === '-' ? '-' : (
|
|
68
|
+
<ResourceLink
|
|
69
|
+
name={volumeNamespace}
|
|
70
|
+
kind="Namespace"
|
|
71
|
+
namespace=""
|
|
72
|
+
onNavigate={onNavigate}
|
|
73
|
+
/>
|
|
74
|
+
)
|
|
75
|
+
} />
|
|
76
|
+
{/* Reachable, like every other location reference in this integration.
|
|
77
|
+
A repository is written into a storage location, and its own status
|
|
78
|
+
says nothing about that location's health. */}
|
|
79
|
+
<Property label="Storage Location" value={
|
|
80
|
+
location === '-' ? '-' : (
|
|
81
|
+
<ResourceLink
|
|
82
|
+
name={location}
|
|
83
|
+
kind="BackupStorageLocation"
|
|
84
|
+
namespace={data?.metadata?.namespace ?? ''}
|
|
85
|
+
group="velero.io"
|
|
86
|
+
onNavigate={onNavigate}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
} />
|
|
90
|
+
</PropertyList>
|
|
91
|
+
</Section>
|
|
92
|
+
|
|
93
|
+
<ConditionsSection conditions={status.conditions || []} />
|
|
94
|
+
</>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { VeleroRestoreRenderer } from './VeleroRestoreRenderer'
|
|
4
|
+
|
|
5
|
+
const restore = (status: Record<string, unknown>) => ({
|
|
6
|
+
metadata: { name: 'live-restore', namespace: 'velero' },
|
|
7
|
+
spec: { backupName: 'live-completed' },
|
|
8
|
+
status,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The restore side carries the same claims as the backup side and its own
|
|
13
|
+
* wording for them, so pinning one does not pin the other. Both banners below
|
|
14
|
+
* were wrong in the same way at the same time.
|
|
15
|
+
*/
|
|
16
|
+
describe('what a restore claims about itself', () => {
|
|
17
|
+
// A count taken mid-run is not a final count. Velero raises warnings as it
|
|
18
|
+
// goes, so a restore still working can already have several.
|
|
19
|
+
it('does not say a restore in flight completed', () => {
|
|
20
|
+
const html = renderToString(
|
|
21
|
+
<VeleroRestoreRenderer data={restore({ phase: 'InProgress', warnings: 2 })} />,
|
|
22
|
+
)
|
|
23
|
+
expect(html).not.toMatch(/completed, with/)
|
|
24
|
+
expect(html).toContain('not a final count')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('still says completed when it did', () => {
|
|
28
|
+
const html = renderToString(
|
|
29
|
+
<VeleroRestoreRenderer data={restore({ phase: 'Completed', warnings: 2 })} />,
|
|
30
|
+
)
|
|
31
|
+
expect(html).toContain('completed, with 2 warning(s)')
|
|
32
|
+
expect(html).not.toContain('not a final count')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Velero's own words beat ours whenever it supplies them.
|
|
36
|
+
it('leads with the reason Velero gave for refusing to start', () => {
|
|
37
|
+
const html = renderToString(
|
|
38
|
+
<VeleroRestoreRenderer
|
|
39
|
+
data={restore({ phase: 'FailedValidation', failureReason: 'backup not found' })}
|
|
40
|
+
/>,
|
|
41
|
+
)
|
|
42
|
+
expect(html).toContain('backup not found')
|
|
43
|
+
expect(html).not.toContain('Velero rejected this restore before it started')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('says nothing was restored when Velero gave no reason', () => {
|
|
47
|
+
const html = renderToString(
|
|
48
|
+
<VeleroRestoreRenderer
|
|
49
|
+
data={restore({ phase: 'FailedValidation', validationErrors: ['Backup live-completed not found'] })}
|
|
50
|
+
/>,
|
|
51
|
+
)
|
|
52
|
+
expect(html).toContain('nothing was restored')
|
|
53
|
+
expect(html).toContain('Backup live-completed not found')
|
|
54
|
+
})
|
|
55
|
+
})
|