@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,417 @@
|
|
|
1
|
+
// Pure relationship lookups between CloudNativePG objects in the workspace
|
|
2
|
+
// payload. Each helper answers only from what the objects record; a relation
|
|
3
|
+
// that cannot be established returns null or an empty list, never a guess.
|
|
4
|
+
|
|
5
|
+
import type { BadgeSeverity } from '../ui/Badge'
|
|
6
|
+
import type { HealthLevel } from '../resources/resource-utils'
|
|
7
|
+
import {
|
|
8
|
+
CNPG_BARMAN_PLUGIN_NAME,
|
|
9
|
+
CNPG_GROUP,
|
|
10
|
+
getCNPGClusterBarmanPlugin,
|
|
11
|
+
getCNPGObjectStoreRecoveryWindows,
|
|
12
|
+
isApiGroup,
|
|
13
|
+
type CNPGObjectStoreRecoveryWindow,
|
|
14
|
+
} from '../resources/resource-utils-cnpg'
|
|
15
|
+
import {
|
|
16
|
+
cnpgIssueCategory,
|
|
17
|
+
coverageReadable,
|
|
18
|
+
type CNPGFact,
|
|
19
|
+
type CNPGProblem,
|
|
20
|
+
type CNPGWorkspaceIssue,
|
|
21
|
+
type CNPGWorkspaceKey,
|
|
22
|
+
type CNPGWorkspaceResponse,
|
|
23
|
+
} from './workspace'
|
|
24
|
+
|
|
25
|
+
export interface CNPGObjectRef {
|
|
26
|
+
kind: string
|
|
27
|
+
group: string
|
|
28
|
+
namespace: string
|
|
29
|
+
name: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SCHEDULED_BACKUP_LABEL = 'cnpg.io/scheduled-backup'
|
|
33
|
+
|
|
34
|
+
function nsOf(obj: any): string {
|
|
35
|
+
return obj?.metadata?.namespace ?? ''
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function nameOf(obj: any): string {
|
|
39
|
+
return obj?.metadata?.name ?? ''
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function specCluster(obj: any): string | undefined {
|
|
43
|
+
const n = obj?.spec?.cluster?.name
|
|
44
|
+
return typeof n === 'string' && n ? n : undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseTime(t: unknown): number {
|
|
48
|
+
const ms = typeof t === 'string' ? Date.parse(t) : NaN
|
|
49
|
+
return Number.isFinite(ms) ? ms : 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Kind and API group both match. Kind alone is not identity: Velero also ships `Backup`. */
|
|
53
|
+
export function isCNPGKind(obj: any, kind: string, group: string = CNPG_GROUP): boolean {
|
|
54
|
+
return obj?.kind === kind && isApiGroup(obj?.apiVersion, group)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function refOf(obj: any, kind: string, group: string = CNPG_GROUP): CNPGObjectRef {
|
|
58
|
+
return { kind, group, namespace: nsOf(obj), name: nameOf(obj) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function healthSeverity(level: HealthLevel): BadgeSeverity {
|
|
62
|
+
switch (level) {
|
|
63
|
+
case 'healthy':
|
|
64
|
+
return 'success'
|
|
65
|
+
case 'unhealthy':
|
|
66
|
+
return 'error'
|
|
67
|
+
case 'alert':
|
|
68
|
+
return 'alert'
|
|
69
|
+
case 'degraded':
|
|
70
|
+
return 'warning'
|
|
71
|
+
default:
|
|
72
|
+
return 'neutral'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Workspace access
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
export function workspaceList(ws: CNPGWorkspaceResponse | null | undefined, key: CNPGWorkspaceKey): any[] {
|
|
81
|
+
return ws?.objects?.[key] ?? []
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Why the workspace cannot answer for `key` in `namespace`, or null when it can.
|
|
86
|
+
* A null workspace means the aggregate could not be read at all.
|
|
87
|
+
*/
|
|
88
|
+
export function relationUnavailable(
|
|
89
|
+
ws: CNPGWorkspaceResponse | null | undefined,
|
|
90
|
+
key: CNPGWorkspaceKey,
|
|
91
|
+
namespace: string | undefined,
|
|
92
|
+
what: string,
|
|
93
|
+
): string | null {
|
|
94
|
+
if (!ws) return `${what} could not be read`
|
|
95
|
+
const cov = ws.coverage?.[key] ?? { state: 'notInstalled' as const }
|
|
96
|
+
if (coverageReadable(cov, namespace)) return null
|
|
97
|
+
switch (cov.state) {
|
|
98
|
+
case 'denied':
|
|
99
|
+
case 'partial':
|
|
100
|
+
return `No access to ${what}`
|
|
101
|
+
case 'syncing':
|
|
102
|
+
return 'Loading…'
|
|
103
|
+
case 'error':
|
|
104
|
+
return `Could not read ${what}`
|
|
105
|
+
default:
|
|
106
|
+
return `${what} are not installed`
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function clustersIn(ws: CNPGWorkspaceResponse | null | undefined): any[] {
|
|
111
|
+
return workspaceList(ws, 'clusters').filter((c) => isCNPGKind(c, 'Cluster'))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The Cluster an object declares itself against (`spec.cluster.name`), if visible. */
|
|
115
|
+
export function targetCluster(obj: any, clusters: any[]): any | null {
|
|
116
|
+
const name = specCluster(obj)
|
|
117
|
+
if (!name) return null
|
|
118
|
+
return clusters.find((c) => isCNPGKind(c, 'Cluster') && nsOf(c) === nsOf(obj) && nameOf(c) === name) ?? null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Issues
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
export function issuesForObject(issues: CNPGWorkspaceIssue[] | undefined, ref: CNPGObjectRef): CNPGWorkspaceIssue[] {
|
|
126
|
+
const rank = { critical: 0, warning: 1 } as const
|
|
127
|
+
return (issues ?? [])
|
|
128
|
+
.filter(
|
|
129
|
+
(i) =>
|
|
130
|
+
i.kind === ref.kind &&
|
|
131
|
+
(i.group ?? '') === ref.group &&
|
|
132
|
+
(i.namespace ?? '') === ref.namespace &&
|
|
133
|
+
i.name === ref.name,
|
|
134
|
+
)
|
|
135
|
+
.sort((a, b) => rank[a.severity] - rank[b.severity])
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function problemsForObject(issues: CNPGWorkspaceIssue[] | undefined, ref: CNPGObjectRef): CNPGProblem[] {
|
|
139
|
+
return issuesForObject(issues, ref).map((issue) => ({
|
|
140
|
+
id: `${issue.id}:${issue.kind}/${issue.name}`,
|
|
141
|
+
severity: issue.severity,
|
|
142
|
+
category: cnpgIssueCategory(issue),
|
|
143
|
+
title: issue.message || issue.reason,
|
|
144
|
+
detail: issue.cause || undefined,
|
|
145
|
+
subject: { kind: issue.kind, group: issue.group ?? '', namespace: issue.namespace ?? '', name: issue.name },
|
|
146
|
+
source: 'issue',
|
|
147
|
+
}))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Backups and schedules
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
function scheduleOwnerRefs(backup: any): any[] {
|
|
155
|
+
const refs = backup?.metadata?.ownerReferences
|
|
156
|
+
if (!Array.isArray(refs)) return []
|
|
157
|
+
return refs.filter((r: any) => r?.kind === 'ScheduledBackup' && isApiGroup(r?.apiVersion, CNPG_GROUP))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The name of the ScheduledBackup that created a Backup. The owner reference
|
|
162
|
+
* is only set when the schedule's `backupOwnerReference` is `self`; the
|
|
163
|
+
* operator labels every Backup it creates from a schedule regardless, so the
|
|
164
|
+
* label is the fallback when no such owner reference exists.
|
|
165
|
+
*/
|
|
166
|
+
export function scheduledBackupOf(backup: any): string | null {
|
|
167
|
+
const owner = scheduleOwnerRefs(backup)[0]
|
|
168
|
+
if (owner?.name) return owner.name
|
|
169
|
+
const label = backup?.metadata?.labels?.[SCHEDULED_BACKUP_LABEL]
|
|
170
|
+
return typeof label === 'string' && label ? label : null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether this schedule created the Backup. An owner reference must match by
|
|
175
|
+
* uid: a schedule deleted and recreated under the same name did not create the
|
|
176
|
+
* old one's Backups. The label, which carries only a name, is read only when
|
|
177
|
+
* no ScheduledBackup owner reference exists.
|
|
178
|
+
*/
|
|
179
|
+
export function isBackupFromSchedule(backup: any, schedule: any): boolean {
|
|
180
|
+
if (!isCNPGKind(backup, 'Backup') || nsOf(backup) !== nsOf(schedule)) return false
|
|
181
|
+
const owners = scheduleOwnerRefs(backup)
|
|
182
|
+
if (owners.length > 0) {
|
|
183
|
+
const uid = schedule?.metadata?.uid
|
|
184
|
+
return owners.some((r: any) => r?.name === nameOf(schedule) && !!uid && r?.uid === uid)
|
|
185
|
+
}
|
|
186
|
+
return backup?.metadata?.labels?.[SCHEDULED_BACKUP_LABEL] === nameOf(schedule)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Backups a ScheduledBackup created, newest first. */
|
|
190
|
+
export function backupsForScheduledBackup(schedule: any, backups: any[]): any[] {
|
|
191
|
+
return backups.filter((b) => isBackupFromSchedule(b, schedule)).sort((a, b) => backupTime(b) - backupTime(a))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function backupTime(backup: any): number {
|
|
195
|
+
return parseTime(backup?.status?.startedAt) || parseTime(backup?.metadata?.creationTimestamp)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The ObjectStore a barman-cloud plugin Backup wrote to. The Backup's own
|
|
200
|
+
* plugin parameters are a record of that run; the target Cluster's plugin
|
|
201
|
+
* configuration is only what it is configured with now, so a store taken from
|
|
202
|
+
* there is marked inferred.
|
|
203
|
+
*/
|
|
204
|
+
export function objectStoreForBackup(backup: any, clusters: any[]): { name: string; inferred: boolean } | null {
|
|
205
|
+
const method = backup?.status?.method || backup?.spec?.method
|
|
206
|
+
if (method !== 'plugin') return null
|
|
207
|
+
const cfg = backup?.spec?.pluginConfiguration
|
|
208
|
+
if (cfg?.name !== CNPG_BARMAN_PLUGIN_NAME) return null
|
|
209
|
+
const own = cfg?.parameters?.barmanObjectName
|
|
210
|
+
if (typeof own === 'string' && own) return { name: own, inferred: false }
|
|
211
|
+
const cluster = targetCluster(backup, clusters)
|
|
212
|
+
const current = cluster ? getCNPGClusterBarmanPlugin(cluster)?.barmanObjectName : undefined
|
|
213
|
+
return current ? { name: current, inferred: true } : null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type CNPGBackupDestination =
|
|
217
|
+
| { type: 'objectStore'; name: string; inferred: boolean }
|
|
218
|
+
| { type: 'path'; path: string }
|
|
219
|
+
| { type: 'volumeSnapshot' }
|
|
220
|
+
| { type: 'unknown' }
|
|
221
|
+
|
|
222
|
+
export function backupDestination(backup: any, clusters: any[]): CNPGBackupDestination {
|
|
223
|
+
const store = objectStoreForBackup(backup, clusters)
|
|
224
|
+
if (store) return { type: 'objectStore', ...store }
|
|
225
|
+
const method = backup?.status?.method || backup?.spec?.method
|
|
226
|
+
if (method === 'volumeSnapshot') return { type: 'volumeSnapshot' }
|
|
227
|
+
const path = backup?.status?.destinationPath
|
|
228
|
+
if (typeof path === 'string' && path) return { type: 'path', path }
|
|
229
|
+
return { type: 'unknown' }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// ObjectStore
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
export interface CNPGObjectStoreUser {
|
|
237
|
+
cluster: any
|
|
238
|
+
/** Key of this cluster's archive inside the store's recovery windows. */
|
|
239
|
+
serverName: string
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Clusters in the store's namespace whose barman-cloud plugin archives to it. */
|
|
243
|
+
export function usersOfObjectStore(store: any, clusters: any[]): CNPGObjectStoreUser[] {
|
|
244
|
+
const ns = nsOf(store)
|
|
245
|
+
const name = nameOf(store)
|
|
246
|
+
const out: CNPGObjectStoreUser[] = []
|
|
247
|
+
for (const c of clusters) {
|
|
248
|
+
if (!isCNPGKind(c, 'Cluster') || nsOf(c) !== ns) continue
|
|
249
|
+
const plugin = getCNPGClusterBarmanPlugin(c)
|
|
250
|
+
if (plugin?.barmanObjectName !== name) continue
|
|
251
|
+
out.push({ cluster: c, serverName: plugin.serverName || nameOf(c) })
|
|
252
|
+
}
|
|
253
|
+
return out.sort((a, b) => nameOf(a.cluster).localeCompare(nameOf(b.cluster)))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface CNPGObjectStoreEvidence {
|
|
257
|
+
cluster: CNPGObjectRef
|
|
258
|
+
serverName: string
|
|
259
|
+
archiving: CNPGFact
|
|
260
|
+
window: CNPGObjectStoreRecoveryWindow | null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface CNPGObjectStoreHealth {
|
|
264
|
+
summary: CNPGFact
|
|
265
|
+
evidence: CNPGObjectStoreEvidence[]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function archivingFact(cluster: any): CNPGFact {
|
|
269
|
+
const conds = cluster?.status?.conditions
|
|
270
|
+
const c = Array.isArray(conds) ? conds.find((x: any) => x?.type === 'ContinuousArchiving') : null
|
|
271
|
+
if (!c) return { text: 'WAL archiving not reported', tone: 'unknown' }
|
|
272
|
+
if (c.status === 'True') return { text: 'WAL archiving', tone: 'healthy', at: c.lastTransitionTime }
|
|
273
|
+
if (c.status === 'False') {
|
|
274
|
+
return { text: c.message ? `WAL archiving failing · ${c.message}` : 'WAL archiving failing', tone: 'unhealthy', at: c.lastTransitionTime }
|
|
275
|
+
}
|
|
276
|
+
return { text: 'WAL archiving unknown', tone: 'unknown' }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Upload health for an ObjectStore, inferred from the clusters that use it.
|
|
281
|
+
* ObjectStore publishes no health of its own, so the only evidence is each
|
|
282
|
+
* user cluster's ContinuousArchiving condition and whether the store's
|
|
283
|
+
* recovery window for that cluster records a failure newer than its last
|
|
284
|
+
* success.
|
|
285
|
+
*/
|
|
286
|
+
export function inferredObjectStoreHealth(store: any, users: CNPGObjectStoreUser[]): CNPGObjectStoreHealth {
|
|
287
|
+
const windows = getCNPGObjectStoreRecoveryWindows(store)
|
|
288
|
+
const evidence: CNPGObjectStoreEvidence[] = users.map((u) => ({
|
|
289
|
+
cluster: refOf(u.cluster, 'Cluster'),
|
|
290
|
+
serverName: u.serverName,
|
|
291
|
+
archiving: archivingFact(u.cluster),
|
|
292
|
+
window: windows.find((w) => w.server === u.serverName) ?? null,
|
|
293
|
+
}))
|
|
294
|
+
if (evidence.length === 0) {
|
|
295
|
+
return { summary: { text: 'No visible cluster uses this store', tone: 'unknown' }, evidence }
|
|
296
|
+
}
|
|
297
|
+
const failing = evidence.filter((e) => e.archiving.tone === 'unhealthy' || e.window?.failingSinceLastSuccess)
|
|
298
|
+
if (failing.length > 0) {
|
|
299
|
+
const latestFailure = failing
|
|
300
|
+
.map((e) => e.window?.lastFailedBackupTime ?? (e.archiving.tone === 'unhealthy' ? e.archiving.at : undefined))
|
|
301
|
+
.filter((t): t is string => !!t)
|
|
302
|
+
.sort((a, b) => parseTime(b) - parseTime(a))[0]
|
|
303
|
+
return {
|
|
304
|
+
summary: {
|
|
305
|
+
text: failing.length === evidence.length ? 'Uploads failing' : `Uploads failing for ${failing.length} of ${evidence.length} clusters`,
|
|
306
|
+
tone: 'unhealthy',
|
|
307
|
+
at: latestFailure,
|
|
308
|
+
},
|
|
309
|
+
evidence,
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (evidence.every((e) => e.archiving.tone === 'healthy')) {
|
|
313
|
+
return { summary: { text: 'Uploads succeeding', tone: 'healthy' }, evidence }
|
|
314
|
+
}
|
|
315
|
+
return { summary: { text: 'No failures reported', tone: 'unknown' }, evidence }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Declarative objects
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
export function appliedFact(obj: any): CNPGFact {
|
|
323
|
+
const applied = obj?.status?.applied
|
|
324
|
+
if (applied === true) return { text: 'Applied', tone: 'healthy' }
|
|
325
|
+
if (applied === false) return { text: 'Not applied', tone: 'unhealthy' }
|
|
326
|
+
return { text: 'Pending · the operator has not reported a result yet', tone: 'unknown' }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function observedGenerationFact(obj: any): CNPGFact {
|
|
330
|
+
const observed = obj?.status?.observedGeneration
|
|
331
|
+
const generation = obj?.metadata?.generation
|
|
332
|
+
if (typeof observed !== 'number') return { text: 'Not reported', tone: 'unknown' }
|
|
333
|
+
if (typeof generation !== 'number') return { text: `Generation ${observed}`, tone: 'neutral' }
|
|
334
|
+
if (observed >= generation) return { text: `Current · generation ${generation}`, tone: 'neutral' }
|
|
335
|
+
return { text: `Behind · observed ${observed}, declared ${generation}`, tone: 'degraded' }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* A role the operator says is missing, when that role is also absent from the
|
|
340
|
+
* target Cluster's managed roles. Only an adjacent fact: roles may be created
|
|
341
|
+
* outside `spec.managed.roles`.
|
|
342
|
+
*/
|
|
343
|
+
export function missingManagedRole(obj: any, cluster: any | null): string | null {
|
|
344
|
+
if (!cluster) return null
|
|
345
|
+
const msg = obj?.status?.message
|
|
346
|
+
if (typeof msg !== 'string') return null
|
|
347
|
+
const m = msg.match(/role "([^"]+)" does not exist/)
|
|
348
|
+
if (!m) return null
|
|
349
|
+
const roles = cluster?.spec?.managed?.roles
|
|
350
|
+
const names = Array.isArray(roles) ? roles.map((r: any) => r?.name) : []
|
|
351
|
+
return names.includes(m[1]) ? null : m[1]
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export { cnpgGitOpsSource as gitopsSourceOf } from './workspace'
|
|
355
|
+
|
|
356
|
+
/** Publications and Subscriptions on the same Cluster and PostgreSQL database. */
|
|
357
|
+
export function replicationForDatabase(
|
|
358
|
+
database: any,
|
|
359
|
+
publications: any[],
|
|
360
|
+
subscriptions: any[],
|
|
361
|
+
): { publications: any[]; subscriptions: any[] } {
|
|
362
|
+
const ns = nsOf(database)
|
|
363
|
+
const cluster = specCluster(database)
|
|
364
|
+
const dbname = database?.spec?.name
|
|
365
|
+
const match = (o: any, kind: string) =>
|
|
366
|
+
isCNPGKind(o, kind) && !!cluster && nsOf(o) === ns && specCluster(o) === cluster && !!dbname && o?.spec?.dbname === dbname
|
|
367
|
+
return {
|
|
368
|
+
publications: publications.filter((p) => match(p, 'Publication')),
|
|
369
|
+
subscriptions: subscriptions.filter((s) => match(s, 'Subscription')),
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The Database object declaring the PostgreSQL database a Publication/Subscription runs in. */
|
|
374
|
+
export function databaseForDeclaration(obj: any, databases: any[]): any | null {
|
|
375
|
+
const cluster = specCluster(obj)
|
|
376
|
+
const dbname = obj?.spec?.dbname
|
|
377
|
+
if (!cluster || !dbname) return null
|
|
378
|
+
return (
|
|
379
|
+
databases.find(
|
|
380
|
+
(d) => isCNPGKind(d, 'Database') && nsOf(d) === nsOf(obj) && specCluster(d) === cluster && d?.spec?.name === dbname,
|
|
381
|
+
) ?? null
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// Image catalogs
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
export interface CNPGImageCatalogUser {
|
|
390
|
+
cluster: any
|
|
391
|
+
major: number | null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Clusters pinned to a catalog through `spec.imageCatalogRef`. An ImageCatalog
|
|
396
|
+
* is namespace-local; a ClusterImageCatalog is referenceable from any
|
|
397
|
+
* namespace. A ref without `kind` means ImageCatalog.
|
|
398
|
+
*/
|
|
399
|
+
export function clustersUsingCatalog(catalog: any, clusters: any[]): CNPGImageCatalogUser[] {
|
|
400
|
+
const kind = catalog?.kind
|
|
401
|
+
if (kind !== 'ImageCatalog' && kind !== 'ClusterImageCatalog') return []
|
|
402
|
+
if (!isApiGroup(catalog?.apiVersion, CNPG_GROUP)) return []
|
|
403
|
+
const name = nameOf(catalog)
|
|
404
|
+
return clusters
|
|
405
|
+
.filter((c) => {
|
|
406
|
+
if (!isCNPGKind(c, 'Cluster')) return false
|
|
407
|
+
const ref = c?.spec?.imageCatalogRef
|
|
408
|
+
if (!ref?.name || ref.name !== name) return false
|
|
409
|
+
if ((ref.kind || 'ImageCatalog') !== kind) return false
|
|
410
|
+
return kind === 'ClusterImageCatalog' || nsOf(c) === nsOf(catalog)
|
|
411
|
+
})
|
|
412
|
+
.map((c) => {
|
|
413
|
+
const major = c?.spec?.imageCatalogRef?.major
|
|
414
|
+
return { cluster: c, major: typeof major === 'number' ? major : null }
|
|
415
|
+
})
|
|
416
|
+
.sort((a, b) => nsOf(a.cluster).localeCompare(nsOf(b.cluster)) || nameOf(a.cluster).localeCompare(nameOf(b.cluster)))
|
|
417
|
+
}
|