@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,104 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
splitCNPGDeclarativeByApplied,
|
|
4
|
+
getCNPGDeclarativeStatus,
|
|
5
|
+
getCNPGDeclarativeMessage,
|
|
6
|
+
getCNPGReclaimPolicy,
|
|
7
|
+
getCNPGImageCatalogEntries,
|
|
8
|
+
} from './resource-utils-cnpg'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `status.applied` has three meanings and the middle one is easy to lose.
|
|
12
|
+
* Verified against a live CNPG 1.27 operator: `demo-app` came back true,
|
|
13
|
+
* `demo-broken` came back false with `role "nobody-owns-this" does not exist`,
|
|
14
|
+
* and every object is briefly absent-status before the operator reaches it.
|
|
15
|
+
*/
|
|
16
|
+
describe('declarative object status', () => {
|
|
17
|
+
it('is healthy once the operator has applied it', () => {
|
|
18
|
+
expect(getCNPGDeclarativeStatus({ status: { applied: true } }).level).toBe('healthy')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('is unhealthy when the operator tried and failed', () => {
|
|
22
|
+
expect(getCNPGDeclarativeStatus({ status: { applied: false } }).level).toBe('unhealthy')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// The one that matters: a freshly created object has no `applied` at all, and
|
|
26
|
+
// reporting that as failed condemns everything in its first seconds.
|
|
27
|
+
it('is unknown — not failed — before the operator has reconciled', () => {
|
|
28
|
+
expect(getCNPGDeclarativeStatus({}).level).toBe('unknown')
|
|
29
|
+
expect(getCNPGDeclarativeStatus({ status: {} }).level).toBe('unknown')
|
|
30
|
+
expect(getCNPGDeclarativeStatus({ status: { applied: undefined } }).text).toBe('Pending')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('passes the operator’s message through untouched', () => {
|
|
34
|
+
const msg = 'while creating database "demo_broken": ERROR: role "nobody-owns-this" does not exist'
|
|
35
|
+
expect(getCNPGDeclarativeMessage({ status: { message: msg } })).toBe(msg)
|
|
36
|
+
expect(getCNPGDeclarativeMessage({ status: { message: '' } })).toBeUndefined()
|
|
37
|
+
expect(getCNPGDeclarativeMessage({})).toBeUndefined()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The reclaim policy decides whether deleting a manifest drops real data, so
|
|
43
|
+
* the destructive case has to be identified rather than printed.
|
|
44
|
+
*/
|
|
45
|
+
describe('reclaim policy', () => {
|
|
46
|
+
it('flags delete as destructive across all three kinds', () => {
|
|
47
|
+
expect(getCNPGReclaimPolicy({ spec: { databaseReclaimPolicy: 'delete' } }).destructive).toBe(true)
|
|
48
|
+
expect(getCNPGReclaimPolicy({ spec: { publicationReclaimPolicy: 'delete' } }).destructive).toBe(true)
|
|
49
|
+
expect(getCNPGReclaimPolicy({ spec: { subscriptionReclaimPolicy: 'delete' } }).destructive).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('does not flag retain', () => {
|
|
53
|
+
expect(getCNPGReclaimPolicy({ spec: { databaseReclaimPolicy: 'retain' } }).destructive).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// CNPG defaults to retain, and defaulting the other way in the UI would
|
|
57
|
+
// warn about data loss that is not going to happen.
|
|
58
|
+
it('defaults to retain when unset', () => {
|
|
59
|
+
const r = getCNPGReclaimPolicy({ spec: {} })
|
|
60
|
+
expect(r.value).toBe('retain')
|
|
61
|
+
expect(r.destructive).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('image catalog entries', () => {
|
|
66
|
+
it('sorts by major so the list does not reshuffle between reads', () => {
|
|
67
|
+
const e = getCNPGImageCatalogEntries({
|
|
68
|
+
spec: { images: [{ major: 17, image: 'pg:17' }, { major: 15, image: 'pg:15' }, { major: 16, image: 'pg:16' }] },
|
|
69
|
+
})
|
|
70
|
+
expect(e.map((x) => x.major)).toEqual([15, 16, 17])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('drops entries with no image rather than rendering a blank row', () => {
|
|
74
|
+
expect(getCNPGImageCatalogEntries({ spec: { images: [{ major: 16 }] } })).toEqual([])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('returns nothing for a catalog with no images', () => {
|
|
78
|
+
expect(getCNPGImageCatalogEntries({ spec: {} })).toEqual([])
|
|
79
|
+
expect(getCNPGImageCatalogEntries({})).toEqual([])
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The Database page renders these three buckets with three different claims, so
|
|
85
|
+
* collapsing pending into failed puts "this exists in Kubernetes and not in
|
|
86
|
+
* PostgreSQL" under an object nothing has looked at yet.
|
|
87
|
+
*/
|
|
88
|
+
describe('splitCNPGDeclarativeByApplied', () => {
|
|
89
|
+
it('keeps pending out of the failures', () => {
|
|
90
|
+
const live = { status: { applied: true } }
|
|
91
|
+
const broken = { status: { applied: false, message: 'boom' } }
|
|
92
|
+
const fresh = {}
|
|
93
|
+
const alsoFresh = { status: {} }
|
|
94
|
+
|
|
95
|
+
const out = splitCNPGDeclarativeByApplied([live, broken, fresh, alsoFresh])
|
|
96
|
+
expect(out.applied).toEqual([live])
|
|
97
|
+
expect(out.notApplied).toEqual([broken])
|
|
98
|
+
expect(out.pending).toEqual([fresh, alsoFresh])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('returns three empty buckets for nothing', () => {
|
|
102
|
+
expect(splitCNPGDeclarativeByApplied([])).toEqual({ applied: [], notApplied: [], pending: [] })
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getCNPGObjectStoreRecoveryWindows,
|
|
4
|
+
getCNPGObjectStoreStatus,
|
|
5
|
+
getCNPGObjectStoreProvider,
|
|
6
|
+
isCNPGObjectStore,
|
|
7
|
+
} from './resource-utils-cnpg'
|
|
8
|
+
|
|
9
|
+
// Field names verified against the real CRD (plugin-barman-cloud v0.14.0,
|
|
10
|
+
// barmancloud.cnpg.io/v1) on a kind cluster — `lastSuccessfulBackupTime`, not
|
|
11
|
+
// `lastSuccessfulBackup` as the Cluster status uses.
|
|
12
|
+
const store = (status: any, spec: any = {}) => ({
|
|
13
|
+
apiVersion: 'barmancloud.cnpg.io/v1',
|
|
14
|
+
kind: 'ObjectStore',
|
|
15
|
+
metadata: { name: 'pg-store', namespace: 'pg' },
|
|
16
|
+
spec: { configuration: { destinationPath: 's3://b/pg' }, ...spec },
|
|
17
|
+
status,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('getCNPGObjectStoreRecoveryWindows', () => {
|
|
21
|
+
it('reads the per-server map and sorts by server', () => {
|
|
22
|
+
const w = getCNPGObjectStoreRecoveryWindows(
|
|
23
|
+
store({
|
|
24
|
+
serverRecoveryWindow: {
|
|
25
|
+
zeta: { firstRecoverabilityPoint: '2026-08-01T00:00:00Z', lastSuccessfulBackupTime: '2026-08-10T00:00:00Z' },
|
|
26
|
+
alpha: { firstRecoverabilityPoint: '2026-08-02T00:00:00Z', lastSuccessfulBackupTime: '2026-08-11T00:00:00Z' },
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
expect(w.map((x) => x.server)).toEqual(['alpha', 'zeta'])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('flags a failure newer than the last success', () => {
|
|
34
|
+
const [w] = getCNPGObjectStoreRecoveryWindows(
|
|
35
|
+
store({
|
|
36
|
+
serverRecoveryWindow: {
|
|
37
|
+
pg: {
|
|
38
|
+
lastSuccessfulBackupTime: '2026-08-05T00:00:00Z',
|
|
39
|
+
lastFailedBackupTime: '2026-08-12T00:00:00Z',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
expect(w.failingSinceLastSuccess).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('does not flag a failure that predates the last success', () => {
|
|
48
|
+
const [w] = getCNPGObjectStoreRecoveryWindows(
|
|
49
|
+
store({
|
|
50
|
+
serverRecoveryWindow: {
|
|
51
|
+
pg: {
|
|
52
|
+
lastFailedBackupTime: '2026-08-05T00:00:00Z',
|
|
53
|
+
lastSuccessfulBackupTime: '2026-08-12T00:00:00Z',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
)
|
|
58
|
+
expect(w.failingSinceLastSuccess).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('treats a failure with no success at all as failing', () => {
|
|
62
|
+
const [w] = getCNPGObjectStoreRecoveryWindows(
|
|
63
|
+
store({ serverRecoveryWindow: { pg: { lastFailedBackupTime: '2026-08-12T00:00:00Z' } } }),
|
|
64
|
+
)
|
|
65
|
+
expect(w.failingSinceLastSuccess).toBe(true)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('returns nothing for a store that has never reported', () => {
|
|
69
|
+
expect(getCNPGObjectStoreRecoveryWindows(store({}))).toEqual([])
|
|
70
|
+
expect(getCNPGObjectStoreRecoveryWindows(store(undefined))).toEqual([])
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('getCNPGObjectStoreStatus', () => {
|
|
75
|
+
// The load-bearing one: an empty status means nothing is restorable, and a
|
|
76
|
+
// green badge there would assert a recovery point that does not exist.
|
|
77
|
+
it('is unknown, not healthy, when no server has reported', () => {
|
|
78
|
+
const s = getCNPGObjectStoreStatus(store({}))
|
|
79
|
+
expect(s.level).toBe('unknown')
|
|
80
|
+
expect(s.text).toBe('No backups yet')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('is unhealthy when any server is failing since its last success', () => {
|
|
84
|
+
const s = getCNPGObjectStoreStatus(
|
|
85
|
+
store({
|
|
86
|
+
serverRecoveryWindow: {
|
|
87
|
+
ok: { lastSuccessfulBackupTime: '2026-08-12T00:00:00Z' },
|
|
88
|
+
bad: { lastSuccessfulBackupTime: '2026-08-01T00:00:00Z', lastFailedBackupTime: '2026-08-12T00:00:00Z' },
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
expect(s.level).toBe('unhealthy')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('is healthy when every server has a clean latest backup', () => {
|
|
96
|
+
const s = getCNPGObjectStoreStatus(
|
|
97
|
+
store({ serverRecoveryWindow: { pg: { lastSuccessfulBackupTime: '2026-08-12T00:00:00Z' } } }),
|
|
98
|
+
)
|
|
99
|
+
expect(s.level).toBe('healthy')
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('provider + group guards', () => {
|
|
104
|
+
it('names the provider without exposing credentials', () => {
|
|
105
|
+
expect(
|
|
106
|
+
getCNPGObjectStoreProvider(
|
|
107
|
+
store({}, { configuration: { destinationPath: 's3://b', s3Credentials: { accessKeyId: { name: 'x' } } } }),
|
|
108
|
+
),
|
|
109
|
+
).toBe('S3')
|
|
110
|
+
expect(
|
|
111
|
+
getCNPGObjectStoreProvider(store({}, { configuration: { azureCredentials: {} } })),
|
|
112
|
+
).toBe('Azure Blob Storage')
|
|
113
|
+
expect(getCNPGObjectStoreProvider(store({}, { configuration: {} }))).toBeUndefined()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// `objectstores` is generic enough for another operator to claim.
|
|
117
|
+
it('does not claim a foreign objectstores CR', () => {
|
|
118
|
+
expect(isCNPGObjectStore(store({}))).toBe(true)
|
|
119
|
+
expect(isCNPGObjectStore({ apiVersion: 'minio.example.io/v1', kind: 'ObjectStore' })).toBe(false)
|
|
120
|
+
expect(isCNPGObjectStore({})).toBe(false)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
+
getCNPGScheduledBackupStatus,
|
|
4
|
+
getCNPGScheduledBackupNextSchedule,
|
|
3
5
|
getCNPGClusterCertificateExpirations,
|
|
4
6
|
getCNPGClusterStatus,
|
|
5
7
|
getCNPGBackupStatus,
|
|
6
8
|
getCNPGPoolerStatus,
|
|
9
|
+
isCNPGPoolerPaused,
|
|
10
|
+
getCNPGVolumeHealth,
|
|
7
11
|
getCNPGClusterInstancesReportedState,
|
|
8
12
|
getCNPGClusterBackupConfig,
|
|
9
13
|
getCNPGClusterBarmanPlugin,
|
|
@@ -599,3 +603,116 @@ describe('an absent count is unknown, never zero', () => {
|
|
|
599
603
|
.toMatchObject({ text: 'Scheduled', level: 'healthy' })
|
|
600
604
|
})
|
|
601
605
|
})
|
|
606
|
+
|
|
607
|
+
describe('paused Pooler', () => {
|
|
608
|
+
// PgBouncer PAUSE holds client connections instead of serving them, while
|
|
609
|
+
// every pod stays scheduled and Ready. Reading only the pod counts rendered
|
|
610
|
+
// a pooler that serves nothing in healthy green.
|
|
611
|
+
const paused = { spec: { instances: 2, pgbouncer: { paused: true } }, status: { instances: 2 } }
|
|
612
|
+
|
|
613
|
+
it('does not read as healthy', () => {
|
|
614
|
+
expect(getCNPGPoolerStatus(paused)).toMatchObject({ text: 'Paused', level: 'degraded' })
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it('treats pausing as intent, not a fault', () => {
|
|
618
|
+
// Same tier as a paused Velero Schedule: amber, not red.
|
|
619
|
+
expect(getCNPGPoolerStatus(paused).level).not.toBe('unhealthy')
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('lets a real fault outrank the pause', () => {
|
|
623
|
+
const pausedAndDown = { spec: { instances: 2, pgbouncer: { paused: true } }, status: { instances: 0 } }
|
|
624
|
+
expect(getCNPGPoolerStatus(pausedAndDown)).toMatchObject({ text: 'Not Scheduled', level: 'unhealthy' })
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
it('only an explicit true pauses', () => {
|
|
628
|
+
expect(getCNPGPoolerStatus({ spec: { instances: 2, pgbouncer: {} }, status: { instances: 2 } }))
|
|
629
|
+
.toMatchObject({ text: 'Scheduled', level: 'healthy' })
|
|
630
|
+
expect(getCNPGPoolerStatus({ spec: { instances: 2, pgbouncer: { paused: false } }, status: { instances: 2 } }))
|
|
631
|
+
.toMatchObject({ text: 'Scheduled', level: 'healthy' })
|
|
632
|
+
expect(isCNPGPoolerPaused({ spec: { pgbouncer: { paused: true } } })).toBe(true)
|
|
633
|
+
expect(isCNPGPoolerPaused({ spec: {} })).toBe(false)
|
|
634
|
+
})
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
describe('volume health', () => {
|
|
638
|
+
// Field names and shape verified against a live CloudNativePG 1.27 CRD and a
|
|
639
|
+
// running cluster: pvcCount is a number, the rest are arrays of PVC names.
|
|
640
|
+
const healthy = {
|
|
641
|
+
status: { pvcCount: 2, healthyPVC: ['pg-healthy-1', 'pg-healthy-2'] },
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
it('reads what the operator reports about volumes that exist', () => {
|
|
645
|
+
const h = getCNPGVolumeHealth(healthy)
|
|
646
|
+
expect(h).toMatchObject({ total: 2, healthy: ['pg-healthy-1', 'pg-healthy-2'], unusable: [] })
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
it('surfaces an unusable volume, which is why an instance cannot start', () => {
|
|
650
|
+
// Upstream: unusable means a paired volume is missing.
|
|
651
|
+
const h = getCNPGVolumeHealth({ status: { pvcCount: 3, healthyPVC: ['a'], unusablePVC: ['pg-main-3'] } })
|
|
652
|
+
expect(h?.unusable).toEqual(['pg-main-3'])
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
it('stays silent when the operator has reported nothing', () => {
|
|
656
|
+
// Absence must not render as "0 problems" — the section is hidden instead,
|
|
657
|
+
// so it never claims a check it did not perform.
|
|
658
|
+
expect(getCNPGVolumeHealth({ status: {} })).toBeNull()
|
|
659
|
+
expect(getCNPGVolumeHealth({})).toBeNull()
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
it('ignores malformed entries rather than rendering them', () => {
|
|
663
|
+
const h = getCNPGVolumeHealth({ status: { pvcCount: 1, healthyPVC: ['ok', 42, null] } })
|
|
664
|
+
expect(h?.healthy).toEqual(['ok'])
|
|
665
|
+
})
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
describe('CNPG scheduled backup lateness', () => {
|
|
669
|
+
const at = (ms: number) => new Date(Date.now() + ms).toISOString()
|
|
670
|
+
const MIN = 60 * 1000
|
|
671
|
+
|
|
672
|
+
const sb = (suspend: boolean, next?: string, lastScheduleTime?: string) => ({
|
|
673
|
+
spec: { schedule: '0 0 0 * * *', suspend },
|
|
674
|
+
status: { ...(next ? { nextScheduleTime: next } : {}), ...(lastScheduleTime ? { lastScheduleTime } : {}) },
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
it('reports a schedule that missed its own next-run time', () => {
|
|
678
|
+
// Ran once, then stopped. Both lastScheduleTime and nextScheduleTime are
|
|
679
|
+
// present, which is exactly the shape that used to read healthy.
|
|
680
|
+
const badge = getCNPGScheduledBackupStatus(sb(false, at(-200 * 24 * 60 * MIN), at(-200 * 24 * 60 * MIN)))
|
|
681
|
+
expect(badge.level).toBe('degraded')
|
|
682
|
+
expect(badge.text).toBe('Overdue')
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
it('stays healthy inside the operator reconcile window', () => {
|
|
686
|
+
const badge = getCNPGScheduledBackupStatus(sb(false, at(-1 * MIN), at(-24 * 60 * MIN)))
|
|
687
|
+
expect(badge.level).toBe('healthy')
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
it('keeps suspended as a deliberate state, not a missed backup', () => {
|
|
691
|
+
// The operator stops maintaining nextScheduleTime once suspended, so the
|
|
692
|
+
// stale value must not be read as a missed run.
|
|
693
|
+
const badge = getCNPGScheduledBackupStatus(sb(true, at(-200 * 24 * 60 * MIN)))
|
|
694
|
+
expect(badge.text).toBe('Suspended')
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
it('never says overdue while the badge still says healthy', () => {
|
|
698
|
+
// The two read from one test. Before this they disagreed: the badge said
|
|
699
|
+
// Active while this field said overdue on the same row.
|
|
700
|
+
const justPastDue = sb(false, at(-1 * MIN), at(-24 * 60 * MIN))
|
|
701
|
+
expect(getCNPGScheduledBackupStatus(justPastDue).level).toBe('healthy')
|
|
702
|
+
expect(getCNPGScheduledBackupNextSchedule(justPastDue)).toBe('due now')
|
|
703
|
+
|
|
704
|
+
const longPastDue = sb(false, at(-200 * 24 * 60 * MIN), at(-200 * 24 * 60 * MIN))
|
|
705
|
+
expect(getCNPGScheduledBackupStatus(longPastDue).level).toBe('degraded')
|
|
706
|
+
expect(getCNPGScheduledBackupNextSchedule(longPastDue)).toContain('overdue by')
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('does not count down to a run that is not coming', () => {
|
|
710
|
+
expect(getCNPGScheduledBackupNextSchedule(sb(true, at(60 * MIN)))).toBe('suspended')
|
|
711
|
+
expect(getCNPGScheduledBackupNextSchedule(sb(true, at(-60 * MIN)))).toBe('suspended')
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
it('says nothing when the operator published no next-run time', () => {
|
|
715
|
+
expect(getCNPGScheduledBackupNextSchedule(sb(false))).toBe('-')
|
|
716
|
+
expect(getCNPGScheduledBackupStatus(sb(false, undefined, at(-24 * 60 * MIN))).level).toBe('healthy')
|
|
717
|
+
})
|
|
718
|
+
})
|