@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.
Files changed (152) hide show
  1. package/package.json +3 -3
  2. package/src/components/applications/ApplicationDetail.test.tsx +15 -0
  3. package/src/components/applications/ApplicationDetail.tsx +17 -15
  4. package/src/components/checks/types.ts +2 -13
  5. package/src/components/cnpg/CNPGBackupSummary.tsx +223 -0
  6. package/src/components/cnpg/CNPGClusterSummary.tsx +222 -0
  7. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +264 -0
  8. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +80 -0
  9. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +176 -0
  10. package/src/components/cnpg/CNPGObjectSummary.test.tsx +236 -0
  11. package/src/components/cnpg/CNPGPoolerSummary.tsx +70 -0
  12. package/src/components/cnpg/CNPGSharedSummary.tsx +88 -0
  13. package/src/components/cnpg/index.ts +14 -0
  14. package/src/components/cnpg/primitives.tsx +134 -0
  15. package/src/components/cnpg/relations.test.ts +304 -0
  16. package/src/components/cnpg/relations.ts +417 -0
  17. package/src/components/cnpg/workspace.test.ts +285 -0
  18. package/src/components/cnpg/workspace.ts +700 -0
  19. package/src/components/gitops/GitOpsTableView.tsx +9 -24
  20. package/src/components/gitops/detail-helpers.ts +1 -1
  21. package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
  22. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
  23. package/src/components/issues/IssuesView.tsx +10 -1
  24. package/src/components/issues/issue-timing.ts +2 -2
  25. package/src/components/issues/issues.test.ts +1 -1
  26. package/src/components/issues/types.ts +24 -0
  27. package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
  28. package/src/components/logs/WorkloadLogsViewer.tsx +17 -3
  29. package/src/components/logs/useLogBuffer.test.ts +17 -0
  30. package/src/components/logs/useLogStream.ts +11 -0
  31. package/src/components/resources/ResourcesSidebar.test.tsx +76 -0
  32. package/src/components/resources/ResourcesSidebar.tsx +183 -11
  33. package/src/components/resources/ResourcesView.tsx +12 -9
  34. package/src/components/resources/index.ts +1 -1
  35. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
  36. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
  37. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
  38. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
  39. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
  40. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
  41. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
  42. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
  43. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
  44. package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +25 -0
  45. package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +16 -7
  46. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
  47. package/src/components/resources/renderers/CNPGShared.tsx +2 -2
  48. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  49. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  50. package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
  51. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
  52. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
  53. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
  54. package/src/components/resources/renderers/EventRenderer.tsx +3 -3
  55. package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
  56. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  57. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  58. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
  59. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
  60. package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
  61. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
  62. package/src/components/resources/renderers/HPARenderer.tsx +2 -2
  63. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
  64. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
  65. package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
  66. package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
  67. package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
  68. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
  69. package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
  70. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
  71. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
  72. package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
  73. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
  74. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
  75. package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
  76. package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
  77. package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
  78. package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
  79. package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
  80. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
  81. package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
  82. package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
  83. package/src/components/resources/renderers/PodRenderer.tsx +21 -7
  84. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  85. package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
  86. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  87. package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
  88. package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
  89. package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
  90. package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
  91. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
  92. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
  93. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
  94. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
  95. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
  96. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
  97. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  98. package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
  99. package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
  100. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
  101. package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +1 -1
  102. package/src/components/resources/resource-utils-cnpg.ts +3 -7
  103. package/src/components/resources/resource-utils-keda.ts +1 -7
  104. package/src/components/resources/resource-utils-kyverno.ts +1 -7
  105. package/src/components/resources/resource-utils-velero.ts +0 -12
  106. package/src/components/resources/resource-utils.ts +17 -33
  107. package/src/components/resources/status-overclaim.test.ts +1 -1
  108. package/src/components/shared/DrainPlanDialog.tsx +1 -0
  109. package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
  110. package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
  111. package/src/components/shared/ResourceActionsBar.tsx +9 -20
  112. package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
  113. package/src/components/shared/SetImageDialog.test.ts +75 -6
  114. package/src/components/shared/SetImageDialog.tsx +92 -84
  115. package/src/components/shared/index.ts +2 -1
  116. package/src/components/timeline/TimelineList.tsx +187 -38
  117. package/src/components/timeline/TimelineListFold.test.tsx +56 -0
  118. package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
  119. package/src/components/timeline/TimelineToolbarState.test.tsx +98 -0
  120. package/src/components/timeline/index.ts +1 -0
  121. package/src/components/timeline/timeline-filters.ts +7 -0
  122. package/src/components/topology/TopologyGraph.tsx +5 -3
  123. package/src/components/ui/ScheduleValue.test.tsx +5 -0
  124. package/src/components/ui/ScheduleValue.tsx +3 -2
  125. package/src/components/ui/drawer-components.test.tsx +66 -1
  126. package/src/components/ui/drawer-components.tsx +43 -29
  127. package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
  128. package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
  129. package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
  130. package/src/components/workload/WorkloadView.tsx +216 -48
  131. package/src/components/workload/index.ts +1 -0
  132. package/src/components/workload/workload-logs-availability.test.ts +6 -0
  133. package/src/index.ts +3 -0
  134. package/src/types/capacity.ts +10 -0
  135. package/src/types/core.ts +32 -1
  136. package/src/utils/format.test.ts +40 -2
  137. package/src/utils/format.ts +5 -1
  138. package/src/utils/gateway-references.test.ts +17 -0
  139. package/src/utils/gateway-references.ts +29 -0
  140. package/src/utils/gitops-write-guard.test.ts +401 -0
  141. package/src/utils/gitops-write-guard.ts +509 -0
  142. package/src/utils/index.ts +2 -0
  143. package/src/utils/ingress-class-references.ts +22 -0
  144. package/src/utils/log-level.ts +14 -0
  145. package/src/utils/navigation.test.ts +13 -0
  146. package/src/utils/navigation.ts +5 -0
  147. package/src/utils/node-lifecycle.test.ts +36 -0
  148. package/src/utils/node-lifecycle.ts +77 -0
  149. package/src/utils/resource-hierarchy.test.ts +32 -0
  150. package/src/utils/resource-hierarchy.ts +28 -2
  151. package/src/utils/resource-refs.test.ts +23 -0
  152. package/src/utils/resource-refs.ts +30 -0
@@ -0,0 +1,304 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ appliedFact,
4
+ backupDestination,
5
+ backupsForScheduledBackup,
6
+ clustersUsingCatalog,
7
+ databaseForDeclaration,
8
+ gitopsSourceOf,
9
+ inferredObjectStoreHealth,
10
+ isBackupFromSchedule,
11
+ issuesForObject,
12
+ missingManagedRole,
13
+ objectStoreForBackup,
14
+ relationUnavailable,
15
+ replicationForDatabase,
16
+ scheduledBackupOf,
17
+ usersOfObjectStore,
18
+ } from './relations'
19
+ import { CNPG_WORKSPACE_KEYS, type CNPGWorkspaceIssue, type CNPGWorkspaceResponse } from './workspace'
20
+
21
+ const PG = 'postgresql.cnpg.io/v1'
22
+ const BARMAN = 'barmancloud.cnpg.io/v1'
23
+ const VELERO = 'velero.io/v1'
24
+ const PLUGIN = 'barman-cloud.cloudnative-pg.io'
25
+
26
+ function cluster(name: string, ns = 'pg', spec: any = {}, status: any = {}): any {
27
+ return { apiVersion: PG, kind: 'Cluster', metadata: { name, namespace: ns }, spec, status }
28
+ }
29
+
30
+ function pluginCluster(name: string, store: string, serverName?: string, conditions?: any[]): any {
31
+ return cluster(
32
+ name,
33
+ 'pg',
34
+ { plugins: [{ name: PLUGIN, isWALArchiver: true, parameters: { barmanObjectName: store, ...(serverName ? { serverName } : {}) } }] },
35
+ conditions ? { conditions } : {},
36
+ )
37
+ }
38
+
39
+ function backup(name: string, extra: any = {}): any {
40
+ return {
41
+ apiVersion: PG,
42
+ kind: 'Backup',
43
+ metadata: { name, namespace: 'pg', ...(extra.metadata ?? {}) },
44
+ spec: { cluster: { name: 'main' }, ...(extra.spec ?? {}) },
45
+ status: extra.status ?? {},
46
+ }
47
+ }
48
+
49
+ describe('scheduledBackupOf / backupsForScheduledBackup', () => {
50
+ const sched = { apiVersion: PG, kind: 'ScheduledBackup', metadata: { name: 'nightly', namespace: 'pg', uid: 'uid-new' } }
51
+ const ownedBy = (name: string, uid: string, extra: any = {}) =>
52
+ backup(name, {
53
+ ...extra,
54
+ metadata: { ...(extra.metadata ?? {}), ownerReferences: [{ apiVersion: PG, kind: 'ScheduledBackup', name: 'nightly', uid }] },
55
+ })
56
+
57
+ it('reads the owner reference name', () => {
58
+ expect(scheduledBackupOf(ownedBy('b1', 'uid-new'))).toBe('nightly')
59
+ })
60
+
61
+ it('falls back to the operator label when the owner is the Cluster or unset', () => {
62
+ const b = backup('b1', {
63
+ metadata: {
64
+ labels: { 'cnpg.io/scheduled-backup': 'nightly' },
65
+ ownerReferences: [{ apiVersion: PG, kind: 'Cluster', name: 'main' }],
66
+ },
67
+ })
68
+ expect(scheduledBackupOf(b)).toBe('nightly')
69
+ expect(isBackupFromSchedule(b, sched)).toBe(true)
70
+ })
71
+
72
+ it('returns null for an on-demand backup', () => {
73
+ expect(scheduledBackupOf(backup('b1'))).toBeNull()
74
+ })
75
+
76
+ it('ignores an owner reference from another API group', () => {
77
+ const b = backup('b1', { metadata: { ownerReferences: [{ apiVersion: 'example.com/v1', kind: 'ScheduledBackup', name: 'nightly' }] } })
78
+ expect(scheduledBackupOf(b)).toBeNull()
79
+ })
80
+
81
+ it('does not attribute a recreated schedule the old schedule\'s Backups', () => {
82
+ const old = ownedBy('old', 'uid-old', { metadata: { labels: { 'cnpg.io/scheduled-backup': 'nightly' } } })
83
+ expect(isBackupFromSchedule(old, sched)).toBe(false)
84
+ expect(isBackupFromSchedule(ownedBy('cur', 'uid-new'), sched)).toBe(true)
85
+ })
86
+
87
+ it('lists owned backups newest first and never a Velero Backup', () => {
88
+ const owned = (name: string, startedAt: string, apiVersion = PG) => ({
89
+ ...backup(name, { metadata: { labels: { 'cnpg.io/scheduled-backup': 'nightly' } }, status: { startedAt } }),
90
+ apiVersion,
91
+ })
92
+ const list = [
93
+ owned('old', '2026-09-01T00:00:00Z'),
94
+ owned('new', '2026-09-02T00:00:00Z'),
95
+ owned('velero', '2026-09-03T00:00:00Z', VELERO),
96
+ ownedBy('stale', 'uid-old', { status: { startedAt: '2026-09-04T00:00:00Z' } }),
97
+ backup('other'),
98
+ { ...owned('elsewhere', '2026-09-03T00:00:00Z'), metadata: { name: 'elsewhere', namespace: 'x', labels: { 'cnpg.io/scheduled-backup': 'nightly' } } },
99
+ ]
100
+ expect(backupsForScheduledBackup(sched, list).map((b) => b.metadata.name)).toEqual(['new', 'old'])
101
+ })
102
+ })
103
+
104
+ describe('objectStoreForBackup / backupDestination', () => {
105
+ const clusters = [pluginCluster('main', 'store-a')]
106
+
107
+ it('prefers the store the backup recorded', () => {
108
+ const b = backup('b', { spec: { method: 'plugin', pluginConfiguration: { name: PLUGIN, parameters: { barmanObjectName: 'store-b' } } } })
109
+ expect(objectStoreForBackup(b, clusters)).toEqual({ name: 'store-b', inferred: false })
110
+ })
111
+
112
+ it("marks a store taken from the Cluster's current plugin as inferred", () => {
113
+ const b = backup('b', { spec: { method: 'plugin', pluginConfiguration: { name: PLUGIN } } })
114
+ expect(objectStoreForBackup(b, clusters)).toEqual({ name: 'store-a', inferred: true })
115
+ expect(backupDestination(b, clusters)).toEqual({ type: 'objectStore', name: 'store-a', inferred: true })
116
+ })
117
+
118
+ it('checks plugin identity before reading barmanObjectName', () => {
119
+ const other = backup('b', { spec: { method: 'plugin', pluginConfiguration: { name: 'other.example.com', parameters: { barmanObjectName: 'store-b' } } } })
120
+ expect(objectStoreForBackup(other, clusters)).toBeNull()
121
+ const unnamed = backup('b', { spec: { method: 'plugin', pluginConfiguration: { parameters: { barmanObjectName: 'store-b' } } } })
122
+ expect(objectStoreForBackup(unnamed, clusters)).toBeNull()
123
+ })
124
+
125
+ it('reports in-tree paths and volume snapshots', () => {
126
+ expect(backupDestination(backup('b', { status: { method: 'barmanObjectStore', destinationPath: 's3://x' } }), clusters)).toEqual({ type: 'path', path: 's3://x' })
127
+ expect(backupDestination(backup('b', { spec: { method: 'volumeSnapshot' } }), clusters)).toEqual({ type: 'volumeSnapshot' })
128
+ expect(backupDestination(backup('b'), clusters)).toEqual({ type: 'unknown' })
129
+ })
130
+ })
131
+
132
+ describe('ObjectStore users and inferred health', () => {
133
+ const store = {
134
+ apiVersion: BARMAN,
135
+ kind: 'ObjectStore',
136
+ metadata: { name: 'store', namespace: 'pg' },
137
+ status: {
138
+ serverRecoveryWindow: {
139
+ 'srv-a': { firstRecoverabilityPoint: '2026-09-01T00:00:00Z', lastSuccessfulBackupTime: '2026-09-02T00:00:00Z' },
140
+ b: { lastSuccessfulBackupTime: '2026-09-02T00:00:00Z', lastFailedBackupTime: '2026-09-03T00:00:00Z' },
141
+ },
142
+ },
143
+ }
144
+ const ok = [{ type: 'ContinuousArchiving', status: 'True' }]
145
+
146
+ it('finds users in the same namespace, keyed by serverName', () => {
147
+ const clusters = [
148
+ pluginCluster('a', 'store', 'srv-a', ok),
149
+ pluginCluster('b', 'store'),
150
+ pluginCluster('c', 'other'),
151
+ { ...pluginCluster('d', 'store'), metadata: { name: 'd', namespace: 'elsewhere' } },
152
+ { ...pluginCluster('e', 'store'), apiVersion: 'cluster.x-k8s.io/v1beta1' },
153
+ ]
154
+ const users = usersOfObjectStore(store, clusters)
155
+ expect(users.map((u) => [u.cluster.metadata.name, u.serverName])).toEqual([
156
+ ['a', 'srv-a'],
157
+ ['b', 'b'],
158
+ ])
159
+ })
160
+
161
+ it('reports failing when any user has a failure newer than its last success', () => {
162
+ const users = usersOfObjectStore(store, [pluginCluster('a', 'store', 'srv-a', ok), pluginCluster('b', 'store', undefined, ok)])
163
+ const h = inferredObjectStoreHealth(store, users)
164
+ expect(h.summary.tone).toBe('unhealthy')
165
+ expect(h.summary.text).toBe('Uploads failing for 1 of 2 clusters')
166
+ expect(h.summary.at).toBe('2026-09-03T00:00:00Z')
167
+ })
168
+
169
+ it('reports failing from a False ContinuousArchiving condition', () => {
170
+ const users = usersOfObjectStore(store, [pluginCluster('a', 'store', 'srv-a', [{ type: 'ContinuousArchiving', status: 'False', message: 'boom' }])])
171
+ expect(inferredObjectStoreHealth(store, users).summary).toMatchObject({ text: 'Uploads failing', tone: 'unhealthy' })
172
+ })
173
+
174
+ it('is healthy only when every user reports archiving', () => {
175
+ const users = usersOfObjectStore(store, [pluginCluster('a', 'store', 'srv-a', ok)])
176
+ expect(inferredObjectStoreHealth(store, users).summary.tone).toBe('healthy')
177
+ const unreported = usersOfObjectStore(store, [pluginCluster('a', 'store', 'srv-a')])
178
+ expect(inferredObjectStoreHealth(store, unreported).summary).toMatchObject({ text: 'No failures reported', tone: 'unknown' })
179
+ })
180
+
181
+ it('says so when nothing uses the store', () => {
182
+ expect(inferredObjectStoreHealth(store, []).summary).toMatchObject({ text: 'No visible cluster uses this store', tone: 'unknown' })
183
+ })
184
+ })
185
+
186
+ describe('clustersUsingCatalog', () => {
187
+ const catalog = { apiVersion: PG, kind: 'ImageCatalog', metadata: { name: 'pg', namespace: 'pg' } }
188
+ const clusterCatalog = { apiVersion: PG, kind: 'ClusterImageCatalog', metadata: { name: 'pg' } }
189
+
190
+ it('treats an omitted ref kind as ImageCatalog and stays in the namespace', () => {
191
+ const clusters = [
192
+ cluster('a', 'pg', { imageCatalogRef: { name: 'pg', major: 16 } }),
193
+ cluster('b', 'pg', { imageCatalogRef: { kind: 'ImageCatalog', name: 'pg', major: 17 } }),
194
+ cluster('c', 'other', { imageCatalogRef: { name: 'pg', major: 16 } }),
195
+ cluster('d', 'pg', { imageCatalogRef: { kind: 'ClusterImageCatalog', name: 'pg', major: 16 } }),
196
+ ]
197
+ expect(clustersUsingCatalog(catalog, clusters).map((u) => [u.cluster.metadata.name, u.major])).toEqual([
198
+ ['a', 16],
199
+ ['b', 17],
200
+ ])
201
+ })
202
+
203
+ it('matches ClusterImageCatalog refs from any namespace, never an omitted kind', () => {
204
+ const clusters = [
205
+ cluster('a', 'x', { imageCatalogRef: { kind: 'ClusterImageCatalog', name: 'pg', major: 16 } }),
206
+ cluster('b', 'y', { imageCatalogRef: { kind: 'ClusterImageCatalog', name: 'pg' } }),
207
+ cluster('c', 'pg', { imageCatalogRef: { name: 'pg', major: 16 } }),
208
+ ]
209
+ expect(clustersUsingCatalog(clusterCatalog, clusters).map((u) => [u.cluster.metadata.name, u.major])).toEqual([
210
+ ['a', 16],
211
+ ['b', null],
212
+ ])
213
+ })
214
+ })
215
+
216
+ describe('issuesForObject', () => {
217
+ const issue = (over: Partial<CNPGWorkspaceIssue>): CNPGWorkspaceIssue => ({
218
+ id: Math.random().toString(),
219
+ severity: 'warning',
220
+ kind: 'Backup',
221
+ group: 'postgresql.cnpg.io',
222
+ namespace: 'pg',
223
+ name: 'b1',
224
+ reason: 'CNPGBackupFailed',
225
+ ...over,
226
+ })
227
+
228
+ it('matches kind, group, namespace and name, critical first', () => {
229
+ const issues = [
230
+ issue({ id: 'w' }),
231
+ issue({ id: 'c', severity: 'critical' }),
232
+ issue({ id: 'velero', group: 'velero.io' }),
233
+ issue({ id: 'ns', namespace: 'other' }),
234
+ issue({ id: 'name', name: 'b2' }),
235
+ issue({ id: 'kind', kind: 'ScheduledBackup' }),
236
+ ]
237
+ const got = issuesForObject(issues, { kind: 'Backup', group: 'postgresql.cnpg.io', namespace: 'pg', name: 'b1' })
238
+ expect(got.map((i) => i.id)).toEqual(['c', 'w'])
239
+ })
240
+ })
241
+
242
+ describe('declarations', () => {
243
+ it('distinguishes pending from failed', () => {
244
+ expect(appliedFact({ status: {} }).tone).toBe('unknown')
245
+ expect(appliedFact({ status: { applied: false } }).text).toBe('Not applied')
246
+ expect(appliedFact({ status: { applied: true } }).text).toBe('Applied')
247
+ })
248
+
249
+ it('names a missing role only when it is absent from managed roles', () => {
250
+ const db = { status: { applied: false, message: 'role "app" does not exist' } }
251
+ expect(missingManagedRole(db, cluster('main', 'pg', { managed: { roles: [{ name: 'other' }] } }))).toBe('app')
252
+ expect(missingManagedRole(db, cluster('main', 'pg', { managed: { roles: [{ name: 'app' }] } }))).toBeNull()
253
+ expect(missingManagedRole(db, null)).toBeNull()
254
+ expect(missingManagedRole({ status: { message: 'connection refused' } }, cluster('main'))).toBeNull()
255
+ })
256
+
257
+ it('reads the GitOps owner labels', () => {
258
+ expect(gitopsSourceOf({ metadata: { labels: { 'argocd.argoproj.io/instance': 'app' } } })).toEqual({ tool: 'argocd', name: 'app' })
259
+ expect(gitopsSourceOf({ metadata: { labels: { 'kustomize.toolkit.fluxcd.io/name': 'k', 'kustomize.toolkit.fluxcd.io/namespace': 'flux' } } })).toEqual({
260
+ tool: 'flux',
261
+ name: 'k',
262
+ namespace: 'flux',
263
+ })
264
+ expect(gitopsSourceOf({ metadata: {} })).toBeNull()
265
+ })
266
+
267
+ const decl = (kind: string, name: string, clusterName: string, dbname: string, ns = 'pg') => ({
268
+ apiVersion: PG,
269
+ kind,
270
+ metadata: { name, namespace: ns },
271
+ spec: { cluster: { name: clusterName }, dbname },
272
+ })
273
+ const database = { apiVersion: PG, kind: 'Database', metadata: { name: 'app-db', namespace: 'pg' }, spec: { cluster: { name: 'main' }, name: 'app' } }
274
+
275
+ it('finds publications and subscriptions on the same cluster and database', () => {
276
+ const pubs = [decl('Publication', 'p1', 'main', 'app'), decl('Publication', 'p2', 'main', 'other'), decl('Publication', 'p3', 'other', 'app')]
277
+ const subs = [decl('Subscription', 's1', 'main', 'app'), decl('Subscription', 's2', 'main', 'app', 'x')]
278
+ const r = replicationForDatabase(database, pubs, subs)
279
+ expect(r.publications.map((p) => p.metadata.name)).toEqual(['p1'])
280
+ expect(r.subscriptions.map((s) => s.metadata.name)).toEqual(['s1'])
281
+ })
282
+
283
+ it('finds the Database declaring a publication database', () => {
284
+ expect(databaseForDeclaration(decl('Publication', 'p1', 'main', 'app'), [database])?.metadata.name).toBe('app-db')
285
+ expect(databaseForDeclaration(decl('Publication', 'p1', 'other', 'app'), [database])).toBeNull()
286
+ })
287
+ })
288
+
289
+ describe('relationUnavailable', () => {
290
+ const ws = (state: any, deniedNamespaces?: string[]): CNPGWorkspaceResponse => {
291
+ const coverage: CNPGWorkspaceResponse['coverage'] = {}
292
+ for (const k of CNPG_WORKSPACE_KEYS) coverage[k] = { state: 'full' }
293
+ coverage.backups = { state, deniedNamespaces }
294
+ return { installed: true, context: 'c', namespaces: null, coverage, objects: {}, issues: [], audit: [], backupsOmitted: 0 }
295
+ }
296
+
297
+ it('answers from coverage', () => {
298
+ expect(relationUnavailable(ws('full'), 'backups', 'pg', 'Backups')).toBeNull()
299
+ expect(relationUnavailable(ws('partial', ['pg']), 'backups', 'pg', 'Backups')).toBe('No access to Backups')
300
+ expect(relationUnavailable(ws('partial', ['other']), 'backups', 'pg', 'Backups')).toBeNull()
301
+ expect(relationUnavailable(ws('denied'), 'backups', 'pg', 'Backups')).toBe('No access to Backups')
302
+ expect(relationUnavailable(null, 'backups', 'pg', 'Backups')).toBe('Backups could not be read')
303
+ })
304
+ })