@skyhook-io/k8s-ui 1.16.2 → 1.17.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 +3 -3
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- package/src/components/checks/types.ts +2 -13
- package/src/components/cnpg/CNPGBackupSummary.tsx +223 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +222 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +264 -0
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +80 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +176 -0
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +236 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +70 -0
- package/src/components/cnpg/CNPGSharedSummary.tsx +88 -0
- package/src/components/cnpg/index.ts +14 -0
- package/src/components/cnpg/primitives.tsx +134 -0
- package/src/components/cnpg/relations.test.ts +304 -0
- package/src/components/cnpg/relations.ts +417 -0
- package/src/components/cnpg/workspace.test.ts +285 -0
- package/src/components/cnpg/workspace.ts +700 -0
- package/src/components/gitops/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
- package/src/components/issues/IssuesView.tsx +10 -1
- package/src/components/issues/issue-timing.ts +2 -2
- package/src/components/issues/issues.test.ts +1 -1
- package/src/components/issues/types.ts +24 -0
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +17 -3
- package/src/components/logs/useLogBuffer.test.ts +17 -0
- package/src/components/logs/useLogStream.ts +11 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +76 -0
- package/src/components/resources/ResourcesSidebar.tsx +183 -11
- package/src/components/resources/ResourcesView.tsx +12 -9
- package/src/components/resources/index.ts +1 -1
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +25 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +16 -7
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +2 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.tsx +21 -7
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.ts +3 -7
- package/src/components/resources/resource-utils-keda.ts +1 -7
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +17 -33
- package/src/components/resources/status-overclaim.test.ts +1 -1
- package/src/components/shared/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +9 -20
- package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +2 -1
- package/src/components/timeline/TimelineList.tsx +187 -38
- package/src/components/timeline/TimelineListFold.test.tsx +56 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
- package/src/components/timeline/TimelineToolbarState.test.tsx +98 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/timeline-filters.ts +7 -0
- package/src/components/topology/TopologyGraph.tsx +5 -3
- package/src/components/ui/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/drawer-components.test.tsx +66 -1
- package/src/components/ui/drawer-components.tsx +43 -29
- package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.tsx +216 -48
- package/src/components/workload/index.ts +1 -0
- package/src/components/workload/workload-logs-availability.test.ts +6 -0
- package/src/index.ts +3 -0
- package/src/types/capacity.ts +10 -0
- package/src/types/core.ts +32 -1
- package/src/utils/format.test.ts +40 -2
- package/src/utils/format.ts +5 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/ingress-class-references.ts +22 -0
- package/src/utils/log-level.ts +14 -0
- package/src/utils/navigation.test.ts +13 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/resource-hierarchy.test.ts +32 -0
- package/src/utils/resource-hierarchy.ts +28 -2
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { buildCNPGFleet, type CNPGWorkspaceResponse, type CNPGWorkspaceKey, CNPG_WORKSPACE_KEYS } from './workspace'
|
|
3
|
+
|
|
4
|
+
const G = 'postgresql.cnpg.io/v1'
|
|
5
|
+
|
|
6
|
+
function cluster(name: string, ns: string, extra: any = {}): any {
|
|
7
|
+
return {
|
|
8
|
+
apiVersion: G,
|
|
9
|
+
kind: 'Cluster',
|
|
10
|
+
metadata: { name, namespace: ns, ...(extra.metadata ?? {}) },
|
|
11
|
+
spec: { instances: 3, ...(extra.spec ?? {}) },
|
|
12
|
+
status: {
|
|
13
|
+
phase: 'Cluster in healthy state',
|
|
14
|
+
readyInstances: 3,
|
|
15
|
+
currentPrimary: `${name}-1`,
|
|
16
|
+
...(extra.status ?? {}),
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pod(name: string, ns: string, clusterName: string, role: string, ready = true): any {
|
|
22
|
+
return {
|
|
23
|
+
apiVersion: 'v1',
|
|
24
|
+
kind: 'Pod',
|
|
25
|
+
metadata: { name, namespace: ns, labels: { 'cnpg.io/cluster': clusterName, 'cnpg.io/instanceRole': role } },
|
|
26
|
+
status: { conditions: [{ type: 'Ready', status: ready ? 'True' : 'False' }] },
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resp(objects: Partial<Record<CNPGWorkspaceKey, any[]>>, over: Partial<CNPGWorkspaceResponse> = {}): CNPGWorkspaceResponse {
|
|
31
|
+
const coverage: CNPGWorkspaceResponse['coverage'] = {}
|
|
32
|
+
for (const k of CNPG_WORKSPACE_KEYS) coverage[k] = { state: 'full' }
|
|
33
|
+
return {
|
|
34
|
+
installed: true,
|
|
35
|
+
context: 'test',
|
|
36
|
+
namespaces: null,
|
|
37
|
+
coverage: { ...coverage, ...(over.coverage ?? {}) },
|
|
38
|
+
objects,
|
|
39
|
+
issues: over.issues ?? [],
|
|
40
|
+
audit: over.audit ?? [],
|
|
41
|
+
backupsOmitted: 0,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('buildCNPGFleet', () => {
|
|
46
|
+
it('never reports replication as healthy from pod readiness alone', () => {
|
|
47
|
+
const fleet = buildCNPGFleet(
|
|
48
|
+
resp({
|
|
49
|
+
clusters: [cluster('pg-a', 'db')],
|
|
50
|
+
pods: [pod('pg-a-1', 'db', 'pg-a', 'primary'), pod('pg-a-2', 'db', 'pg-a', 'replica'), pod('pg-a-3', 'db', 'pg-a', 'replica')],
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
const row = fleet.rows[0]
|
|
54
|
+
expect(row.replication.tone).toBe('unknown')
|
|
55
|
+
expect(row.replication.text).toContain('2/2 replicas ready')
|
|
56
|
+
expect(row.pods[0].role).toBe('primary')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('attributes child-object issues to their cluster and marks attention', () => {
|
|
60
|
+
const fleet = buildCNPGFleet(
|
|
61
|
+
resp(
|
|
62
|
+
{
|
|
63
|
+
clusters: [cluster('pg-a', 'db'), cluster('pg-b', 'db')],
|
|
64
|
+
databases: [{ apiVersion: G, kind: 'Database', metadata: { name: 'reporting', namespace: 'db' }, spec: { cluster: { name: 'pg-b' } }, status: { applied: false } }],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
issues: [
|
|
68
|
+
{ id: 'i1', severity: 'warning', kind: 'Database', group: 'postgresql.cnpg.io', namespace: 'db', name: 'reporting', reason: 'CNPGDeclarativeNotApplied', message: 'role "x" does not exist' },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
const b = fleet.rows.find((r) => r.name === 'pg-b')!
|
|
74
|
+
const a = fleet.rows.find((r) => r.name === 'pg-a')!
|
|
75
|
+
expect(b.attention).toBe(true)
|
|
76
|
+
expect(b.categories.has('declarations')).toBe(true)
|
|
77
|
+
expect(b.declarations.failed).toBe(1)
|
|
78
|
+
expect(a.attention).toBe(false)
|
|
79
|
+
expect(fleet.attentionCount).toBe(1)
|
|
80
|
+
expect(fleet.categoryCounts.declarations).toBe(1)
|
|
81
|
+
expect(fleet.rows[0].name).toBe('pg-b')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('treats the no-schedule audit finding as posture, not attention, and words it narrowly', () => {
|
|
85
|
+
const fleet = buildCNPGFleet(
|
|
86
|
+
resp({ clusters: [cluster('pg-a', 'db')] }, {
|
|
87
|
+
audit: [{ checkId: 'cnpgNoDeclarativeBackup', severity: 'warning', kind: 'Cluster', namespace: 'db', name: 'pg-a', message: 'no ScheduledBackup' }],
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
const row = fleet.rows[0]
|
|
91
|
+
expect(row.attention).toBe(false)
|
|
92
|
+
expect(row.problems[0].title).toBe('No declarative backup schedule')
|
|
93
|
+
expect(row.protection.schedule.text).toBe('No declarative schedule')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('says "no access" rather than "none" when a kind is not readable', () => {
|
|
97
|
+
const fleet = buildCNPGFleet(
|
|
98
|
+
resp({ clusters: [cluster('pg-a', 'db')] }, { coverage: { backups: { state: 'denied' }, scheduledBackups: { state: 'partial', deniedNamespaces: ['db'] } } }),
|
|
99
|
+
)
|
|
100
|
+
const p = fleet.rows[0].protection
|
|
101
|
+
expect(p.lastSuccessfulBackup.text).toBe('No access to Backups')
|
|
102
|
+
expect(p.lastSuccessfulBackup.tone).toBe('unknown')
|
|
103
|
+
expect(p.schedule.text).toBe('No access to ScheduledBackups')
|
|
104
|
+
expect(fleet.incompleteKinds).toEqual(expect.arrayContaining(['backups', 'scheduledBackups']))
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('prefers the newest successful backup across Backup CRs and ObjectStore status, citing the source', () => {
|
|
108
|
+
const fleet = buildCNPGFleet(
|
|
109
|
+
resp({
|
|
110
|
+
clusters: [cluster('pg-a', 'db', { spec: { plugins: [{ name: 'barman-cloud.cloudnative-pg.io', parameters: { barmanObjectName: 'store' } }] } })],
|
|
111
|
+
backups: [{ apiVersion: G, kind: 'Backup', metadata: { name: 'pg-a-old', namespace: 'db' }, spec: { cluster: { name: 'pg-a' } }, status: { phase: 'completed', stoppedAt: '2026-09-20T02:00:00Z' } }],
|
|
112
|
+
objectStores: [{
|
|
113
|
+
apiVersion: 'barmancloud.cnpg.io/v1', kind: 'ObjectStore', metadata: { name: 'store', namespace: 'db' },
|
|
114
|
+
status: { serverRecoveryWindow: { 'pg-a': { firstRecoverabilityPoint: '2026-09-01T00:00:00Z', lastSuccessfulBackupTime: '2026-09-28T02:00:00Z' } } },
|
|
115
|
+
}],
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
const p = fleet.rows[0].protection
|
|
119
|
+
expect(p.lastSuccessfulBackup.at).toBe('2026-09-28T02:00:00Z')
|
|
120
|
+
expect(p.lastSuccessfulBackup.source).toBe('ObjectStore store status')
|
|
121
|
+
expect(p.recoveryWindow.from).toBe('2026-09-01T00:00:00Z')
|
|
122
|
+
expect(p.destination.method).toBe('plugin')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('ignores deprecated Cluster status backup fields for plugin clusters', () => {
|
|
126
|
+
const fleet = buildCNPGFleet(
|
|
127
|
+
resp({
|
|
128
|
+
clusters: [cluster('pg-a', 'db', {
|
|
129
|
+
spec: { plugins: [{ name: 'barman-cloud.cloudnative-pg.io', parameters: { barmanObjectName: 'store' } }] },
|
|
130
|
+
status: { lastSuccessfulBackup: '2020-01-01T00:00:00Z' },
|
|
131
|
+
})],
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
expect(fleet.rows[0].protection.lastSuccessfulBackup.text).toBe('None observed')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('never marks restore validation healthy', () => {
|
|
138
|
+
const src = cluster('pg-a', 'db', { spec: { plugins: [{ name: 'barman-cloud.cloudnative-pg.io', parameters: { barmanObjectName: 'store' } }] } })
|
|
139
|
+
const restored = cluster('pg-a-restore', 'db', {
|
|
140
|
+
spec: {
|
|
141
|
+
bootstrap: { recovery: { source: 'origin' } },
|
|
142
|
+
externalClusters: [{ name: 'origin', plugin: { name: 'barman-cloud.cloudnative-pg.io', parameters: { barmanObjectName: 'store', serverName: 'pg-a' } } }],
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
const fleet = buildCNPGFleet(resp({ clusters: [src, restored] }))
|
|
146
|
+
const a = fleet.rows.find((r) => r.name === 'pg-a')!
|
|
147
|
+
expect(a.protection.restoreValidation.text).toBe('Restored into pg-a-restore')
|
|
148
|
+
expect(a.protection.restoreValidation.tone).toBe('neutral')
|
|
149
|
+
const r = fleet.rows.find((x) => x.name === 'pg-a-restore')!
|
|
150
|
+
expect(r.protection.restoreValidation.text).toBe('None recorded')
|
|
151
|
+
expect(r.protection.restoreValidation.tone).toBe('unknown')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('ends the recovery window at WAL archiving, not at the last base backup', () => {
|
|
155
|
+
const plugin = { plugins: [{ name: 'barman-cloud.cloudnative-pg.io', parameters: { barmanObjectName: 'store' } }] }
|
|
156
|
+
const store = {
|
|
157
|
+
apiVersion: 'barmancloud.cnpg.io/v1', kind: 'ObjectStore', metadata: { name: 'store', namespace: 'db' },
|
|
158
|
+
status: { serverRecoveryWindow: { 'pg-a': { firstRecoverabilityPoint: '2026-09-01T00:00:00Z', lastSuccessfulBackupTime: '2026-09-02T00:00:00Z', lastFailedBackupTime: '2026-09-03T00:00:00Z' } } },
|
|
159
|
+
}
|
|
160
|
+
const archiving = cluster('pg-a', 'db', { spec: plugin, status: { conditions: [{ type: 'ContinuousArchiving', status: 'True' }] } })
|
|
161
|
+
const w = buildCNPGFleet(resp({ clusters: [archiving], objectStores: [store] })).rows[0].protection.recoveryWindow
|
|
162
|
+
expect(w).toMatchObject({ from: '2026-09-01T00:00:00Z', tone: 'neutral' })
|
|
163
|
+
expect(w).not.toHaveProperty('to')
|
|
164
|
+
const failing = cluster('pg-a', 'db', { spec: plugin, status: { conditions: [{ type: 'ContinuousArchiving', status: 'False' }] } })
|
|
165
|
+
expect(buildCNPGFleet(resp({ clusters: [failing], objectStores: [store] })).rows[0].protection.recoveryWindow.tone).toBe('degraded')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('reports WAL archiving from the condition and unknown when absent', () => {
|
|
169
|
+
const failing = cluster('pg-a', 'db', { status: { conditions: [{ type: 'ContinuousArchiving', status: 'False', message: 'exit status 1' }] } })
|
|
170
|
+
const silent = cluster('pg-b', 'db')
|
|
171
|
+
const fleet = buildCNPGFleet(resp({ clusters: [failing, silent] }))
|
|
172
|
+
expect(fleet.rows.find((r) => r.name === 'pg-a')!.protection.walArchiving.tone).toBe('unhealthy')
|
|
173
|
+
expect(fleet.rows.find((r) => r.name === 'pg-a')!.protection.summary.text).toBe('WAL archiving failing')
|
|
174
|
+
expect(fleet.rows.find((r) => r.name === 'pg-b')!.protection.walArchiving.tone).toBe('unknown')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('ignores same-named kinds from other API groups', () => {
|
|
178
|
+
const capi = { apiVersion: 'cluster.x-k8s.io/v1beta1', kind: 'Cluster', metadata: { name: 'workload', namespace: 'db' } }
|
|
179
|
+
const fleet = buildCNPGFleet(resp({ clusters: [capi, cluster('pg-a', 'db')] }))
|
|
180
|
+
expect(fleet.rows.map((r) => r.name)).toEqual(['pg-a'])
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('counts declared managed roles and their reconcile errors', () => {
|
|
184
|
+
const c = cluster('pg-a', 'db', {
|
|
185
|
+
spec: { managed: { roles: [{ name: 'app' }, { name: 'audit' }] } },
|
|
186
|
+
status: { managedRolesStatus: { cannotReconcile: { audit: ['permission denied'] } } },
|
|
187
|
+
})
|
|
188
|
+
const d = buildCNPGFleet(resp({ clusters: [c] })).rows[0].declarations
|
|
189
|
+
expect(d.total).toBe(2)
|
|
190
|
+
expect(d.failed).toBe(1)
|
|
191
|
+
expect(d.summary.tone).toBe('degraded')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('treats declared managed roles without status as pending, not reconciled', () => {
|
|
195
|
+
const c = cluster('pg-a', 'db', { spec: { managed: { roles: [{ name: 'app' }, { name: 'audit' }] } } })
|
|
196
|
+
const d = buildCNPGFleet(resp({ clusters: [c] })).rows[0].declarations
|
|
197
|
+
expect(d.pending).toBe(2)
|
|
198
|
+
expect(d.summary.text).toBe('2 of 2 pending')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('does not claim "no schedule" when ScheduledBackups are unreadable', () => {
|
|
202
|
+
const fleet = buildCNPGFleet(resp({ clusters: [cluster('pg-a', 'db')] }, { coverage: { scheduledBackups: { state: 'denied' } } }))
|
|
203
|
+
expect(fleet.rows[0].protection.summary.text).not.toBe('No backup destination or schedule')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('says recovery is only declared until the restored cluster has a ready instance', () => {
|
|
207
|
+
const src = cluster('pg-a', 'db')
|
|
208
|
+
const restoring = cluster('pg-a-restore', 'db', {
|
|
209
|
+
spec: { bootstrap: { recovery: { backup: { name: 'nightly-1' } } } },
|
|
210
|
+
status: { readyInstances: 0, currentPrimary: undefined },
|
|
211
|
+
})
|
|
212
|
+
const backups = [{ apiVersion: G, kind: 'Backup', metadata: { name: 'nightly-1', namespace: 'db' }, spec: { cluster: { name: 'pg-a' } }, status: { phase: 'completed' } }]
|
|
213
|
+
const a = buildCNPGFleet(resp({ clusters: [src, restoring], backups })).rows.find((r) => r.name === 'pg-a')!
|
|
214
|
+
expect(a.protection.restoreValidation.text).toBe('Recovery declared in pg-a-restore')
|
|
215
|
+
expect(a.protection.restoreValidation.tone).toBe('unknown')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('does not say no restore was recorded when the Backup it names is unreadable', () => {
|
|
219
|
+
const src = cluster('pg-a', 'db')
|
|
220
|
+
const restoring = cluster('pg-a-restore', 'db', { spec: { bootstrap: { recovery: { backup: { name: 'nightly-1' } } } } })
|
|
221
|
+
const a = buildCNPGFleet(resp({ clusters: [src, restoring] }, { coverage: { backups: { state: 'denied' } } })).rows.find((r) => r.name === 'pg-a')!
|
|
222
|
+
expect(a.protection.restoreValidation.text).toBe('Unknown: no access to Backups')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('does not attribute a restore by backup-name prefix alone', () => {
|
|
226
|
+
const src = cluster('pg', 'db')
|
|
227
|
+
const other = cluster('pg-orders-restore', 'db', { spec: { bootstrap: { recovery: { backup: { name: 'pg-orders-backup' } } } } })
|
|
228
|
+
const backups = [{ apiVersion: G, kind: 'Backup', metadata: { name: 'pg-orders-backup', namespace: 'db' }, spec: { cluster: { name: 'pg-orders' } }, status: { phase: 'completed' } }]
|
|
229
|
+
const row = buildCNPGFleet(resp({ clusters: [src, other], backups })).rows.find((r) => r.name === 'pg')!
|
|
230
|
+
expect(row.protection.restoreValidation.text).toBe('None recorded')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('marks Poolers unknown when they are not readable', () => {
|
|
234
|
+
const fleet = buildCNPGFleet(resp({ clusters: [cluster('pg-a', 'db')] }, { coverage: { poolers: { state: 'denied' } } }))
|
|
235
|
+
expect(fleet.rows[0].poolersKnown).toBe(false)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('attributes instance Pod issues to their cluster', () => {
|
|
239
|
+
const fleet = buildCNPGFleet(
|
|
240
|
+
resp({ clusters: [cluster('pg-a', 'db')], pods: [pod('pg-a-2', 'db', 'pg-a', 'replica', false)] }, {
|
|
241
|
+
issues: [{ id: 'p1', severity: 'critical', kind: 'Pod', namespace: 'db', name: 'pg-a-2', reason: 'CrashLoopBackOff', message: 'Back-off restarting failed container' }],
|
|
242
|
+
}),
|
|
243
|
+
)
|
|
244
|
+
expect(fleet.rows[0].attention).toBe(true)
|
|
245
|
+
expect(fleet.rows[0].categories.has('availability')).toBe(true)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('reads partial coverage by allowed namespaces and treats unnamed partial coverage as unknown', () => {
|
|
249
|
+
const named = buildCNPGFleet(resp({ clusters: [cluster('pg-a', 'db')] }, { coverage: { backups: { state: 'partial', allowedNamespaces: ['db'] } } }))
|
|
250
|
+
expect(named.rows[0].protection.lastSuccessfulBackup.text).toBe('None observed')
|
|
251
|
+
const unnamed = buildCNPGFleet(resp({ clusters: [cluster('pg-a', 'db')] }, { coverage: { backups: { state: 'partial' } } }))
|
|
252
|
+
expect(unnamed.rows[0].protection.lastSuccessfulBackup.text).toBe('No access to Backups')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('says no access instead of "no replica pods" when Pods are unreadable', () => {
|
|
256
|
+
const fleet = buildCNPGFleet(resp({ clusters: [cluster('pg-a', 'db')] }, { coverage: { pods: { state: 'denied' } } }))
|
|
257
|
+
expect(fleet.rows[0].replication.text).toBe('No access to Pods')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('does not report the recovery window or last backup as absent when ObjectStores are unreadable', () => {
|
|
261
|
+
const c = cluster('pg-a', 'db', { spec: { plugins: [{ name: 'barman-cloud.cloudnative-pg.io', parameters: { barmanObjectName: 'store' } }] } })
|
|
262
|
+
const p = buildCNPGFleet(resp({ clusters: [c] }, { coverage: { objectStores: { state: 'denied' } } })).rows[0].protection
|
|
263
|
+
expect(p.recoveryWindow.text).toBe('No access to ObjectStores')
|
|
264
|
+
expect(p.lastSuccessfulBackup.text).toBe('No access to ObjectStores')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('ignores recovery sources from other plugins that reuse the barman parameter names', () => {
|
|
268
|
+
const src = cluster('pg-a', 'db', { spec: { plugins: [{ name: 'barman-cloud.cloudnative-pg.io', parameters: { barmanObjectName: 'store' } }] } })
|
|
269
|
+
const other = cluster('pg-x', 'db', {
|
|
270
|
+
spec: {
|
|
271
|
+
bootstrap: { recovery: { source: 'origin' } },
|
|
272
|
+
externalClusters: [{ name: 'origin', plugin: { name: 'some-other-plugin', parameters: { barmanObjectName: 'store', serverName: 'pg-a' } } }],
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
const row = buildCNPGFleet(resp({ clusters: [src, other] })).rows.find((r) => r.name === 'pg-a')!
|
|
276
|
+
expect(row.protection.restoreValidation.text).toBe('None recorded')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('words unreadable ObjectStores by coverage state', () => {
|
|
280
|
+
const c = cluster('pg-a', 'db', { spec: { plugins: [{ name: 'barman-cloud.cloudnative-pg.io', parameters: { barmanObjectName: 'store' } }] } })
|
|
281
|
+
const p = buildCNPGFleet(resp({ clusters: [c] }, { coverage: { objectStores: { state: 'syncing' } } })).rows[0].protection
|
|
282
|
+
expect(p.lastSuccessfulBackup.text).toBe('Loading…')
|
|
283
|
+
expect(p.recoveryWindow.text).toBe('Loading…')
|
|
284
|
+
})
|
|
285
|
+
})
|