@skyhook-io/k8s-ui 1.17.0 → 1.17.1
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/README.md +41 -0
- package/package.json +3 -3
- package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
- package/src/components/charts/AreaChart.test.tsx +25 -0
- package/src/components/charts/AreaChart.tsx +141 -12
- package/src/components/charts/index.ts +1 -0
- package/src/components/checks/ChecksView.tsx +18 -1
- package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
- package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
- package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
- package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
- package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
- package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
- package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
- package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
- package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
- package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
- package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
- package/src/components/cnpg/backupRuns.test.ts +17 -0
- package/src/components/cnpg/backupRuns.ts +19 -0
- package/src/components/cnpg/connect.test.ts +124 -0
- package/src/components/cnpg/connect.ts +155 -0
- package/src/components/cnpg/databaseRole.test.ts +38 -0
- package/src/components/cnpg/databaseRole.ts +80 -0
- package/src/components/cnpg/ha.test.ts +446 -0
- package/src/components/cnpg/ha.ts +621 -0
- package/src/components/cnpg/index.ts +18 -0
- package/src/components/cnpg/logicalReplication.test.ts +121 -0
- package/src/components/cnpg/logicalReplication.ts +252 -0
- package/src/components/cnpg/pooler.test.ts +84 -0
- package/src/components/cnpg/pooler.ts +225 -0
- package/src/components/cnpg/primitives.tsx +21 -117
- package/src/components/cnpg/problems.ts +12 -0
- package/src/components/cnpg/relations.test.ts +63 -13
- package/src/components/cnpg/relations.ts +61 -52
- package/src/components/cnpg/schedule.ts +35 -0
- package/src/components/cnpg/workspace-disk.test.ts +97 -0
- package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
- package/src/components/cnpg/workspace-problems.test.ts +175 -0
- package/src/components/cnpg/workspace.test.ts +380 -9
- package/src/components/cnpg/workspace.ts +1137 -136
- package/src/components/dock/DockContext.test.tsx +32 -0
- package/src/components/dock/DockContext.tsx +18 -4
- package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
- package/src/components/dock/LocalTerminalTab.tsx +4 -3
- package/src/components/dock/TerminalTab.tsx +20 -8
- package/src/components/facts/certainty.tsx +36 -0
- package/src/components/facts/facts.test.tsx +24 -0
- package/src/components/facts/facts.tsx +62 -0
- package/src/components/facts/index.ts +6 -0
- package/src/components/facts/managed-by.test.tsx +17 -0
- package/src/components/facts/managed-by.tsx +36 -0
- package/src/components/facts/public-exports.test.tsx +35 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
- package/src/components/gitops/insights/DriftPanel.tsx +62 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
- package/src/components/gitops/tree/layout.test.ts +111 -0
- package/src/components/gitops/tree/layout.ts +128 -0
- package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
- package/src/components/gitops/tree/tree-helpers.ts +7 -1
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +40 -5
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +3 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +12 -1
- package/src/components/issues/issues.test.ts +49 -1
- package/src/components/issues/severity.ts +21 -0
- package/src/components/issues/types.ts +23 -0
- package/src/components/logs/LogCore.theme.test.tsx +81 -3
- package/src/components/logs/LogCore.tsx +25 -17
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +291 -0
- package/src/components/logs/LogsViewer.tsx +72 -11
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +22 -30
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +17 -1
- package/src/components/problems/index.ts +1 -0
- package/src/components/problems/problems.test.tsx +63 -0
- package/src/components/problems/problems.tsx +165 -0
- package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
- package/src/components/resources/ActiveFilterBar.tsx +86 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
- package/src/components/resources/ResourcesSidebar.tsx +92 -41
- package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +302 -109
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
- package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/HPARenderer.tsx +56 -16
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
- package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
- package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
- package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
- package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +41 -29
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
- package/src/components/resources/resource-utils-cnpg.ts +51 -20
- package/src/components/resources/resource-utils-contour.test.ts +44 -0
- package/src/components/resources/resource-utils-contour.ts +16 -0
- package/src/components/resources/resource-utils-istio.ts +12 -8
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils.ts +82 -6
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
- package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
- package/src/components/shared/ActionConfirmDialog.tsx +246 -0
- package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
- package/src/components/shared/CreateResourceDialog.tsx +126 -80
- package/src/components/shared/DetailShell.test.tsx +80 -0
- package/src/components/shared/DetailShell.tsx +85 -10
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +11 -4
- package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/timeline-filters.ts +1 -0
- package/src/components/topology/K8sResourceNode.test.ts +18 -1
- package/src/components/topology/K8sResourceNode.tsx +44 -2
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +3 -71
- package/src/components/topology/TopologySearch.tsx +1 -0
- package/src/components/topology/internet-groups.test.ts +35 -0
- package/src/components/topology/internet-groups.ts +55 -0
- package/src/components/topology/layout.ts +1 -1
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/FoldSection.tsx +68 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
- package/src/components/ui/RefLink.tsx +20 -0
- package/src/components/ui/RowActionMenu.tsx +8 -3
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +110 -1
- package/src/components/ui/drawer-components.tsx +62 -9
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/monacoEditorFeatures.ts +77 -0
- package/src/components/ui/monacoRuntime.ts +14 -19
- package/src/components/ui/status-tone.test.ts +10 -1
- package/src/components/ui/status-tone.tsx +29 -0
- package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +180 -64
- package/src/index.ts +14 -1
- package/src/monaco-deep.d.ts +19 -12
- package/src/theme/components.css +13 -0
- package/src/types/capacity.ts +11 -2
- package/src/types/core.ts +29 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +10 -0
- package/src/types/gitops.ts +26 -6
- package/src/utils/api-resources.ts +7 -0
- package/src/utils/application-history.ts +1 -0
- package/src/utils/cnpg-backup.test.ts +26 -0
- package/src/utils/cnpg-backup.ts +70 -0
- package/src/utils/format.test.ts +9 -0
- package/src/utils/format.ts +4 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/k8s-errors.ts +7 -7
- package/src/utils/log-format.test.ts +41 -0
- package/src/utils/log-format.ts +71 -15
- package/src/utils/log-level.ts +12 -8
- package/src/utils/navigation.test.ts +57 -0
- package/src/utils/navigation.ts +86 -8
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +37 -0
- package/src/utils/resource-hierarchy.ts +15 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.test.ts +21 -0
- package/src/utils/skeleton-yaml.ts +16 -0
- package/src/utils/url-path.test.ts +35 -0
- package/src/utils/url-path.ts +21 -0
- package/src/utils/validators.test.ts +4 -13
- package/src/utils/validators.ts +5 -19
- package/src/utils/view-transition.test.ts +35 -1
- package/src/utils/view-transition.ts +16 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Fact } from '../facts'
|
|
2
|
+
import type { HealthLevel } from '../resources/resource-utils'
|
|
3
|
+
import { formatGrant, type Grant } from '../../utils/grant'
|
|
4
|
+
|
|
5
|
+
/** The Deployment a Pooler runs, as the host read it. */
|
|
6
|
+
export interface CNPGPoolerDeploymentLive {
|
|
7
|
+
name: string
|
|
8
|
+
state: 'ok' | 'missing' | 'unreadable' | 'foreign'
|
|
9
|
+
replicas?: number
|
|
10
|
+
readyReplicas?: number
|
|
11
|
+
updatedReplicas?: number
|
|
12
|
+
availableReplicas?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CNPGPoolerServiceLive {
|
|
16
|
+
name: string
|
|
17
|
+
state: 'ok' | 'missing' | 'unreadable' | 'foreign'
|
|
18
|
+
type?: string
|
|
19
|
+
port?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CNPGPoolerPoolSample {
|
|
23
|
+
database: string
|
|
24
|
+
user: string
|
|
25
|
+
clActive?: number
|
|
26
|
+
clWaiting?: number
|
|
27
|
+
svActive?: number
|
|
28
|
+
svIdle?: number
|
|
29
|
+
svUsed?: number
|
|
30
|
+
maxwaitSeconds?: number
|
|
31
|
+
poolMode?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Live PgBouncer exporter reads, one per pooler Pod. */
|
|
35
|
+
export interface CNPGPoolerPressureLive {
|
|
36
|
+
state: 'loading' | 'denied' | 'error' | 'ok'
|
|
37
|
+
reason?: string
|
|
38
|
+
pods: { pod: string; state: string; error?: string; reason?: string; schedulingReason?: string; pools?: CNPGPoolerPoolSample[] }[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Each PgBouncer's own SHOW STATE. */
|
|
42
|
+
export interface CNPGPoolerObservedLive {
|
|
43
|
+
state: 'loading' | 'denied' | 'error' | 'ok'
|
|
44
|
+
grant?: Grant
|
|
45
|
+
reason?: string
|
|
46
|
+
pods: { pod: string; state: string; paused?: boolean; error?: string }[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What a host that reads the cluster adds to the Pooler summary. Each part is
|
|
51
|
+
* optional: a part the host does not provide keeps the resource-only wording.
|
|
52
|
+
*/
|
|
53
|
+
export interface CNPGPoolerLive {
|
|
54
|
+
deployment?: CNPGPoolerDeploymentLive
|
|
55
|
+
service?: CNPGPoolerServiceLive
|
|
56
|
+
pressure?: CNPGPoolerPressureLive
|
|
57
|
+
observed?: CNPGPoolerObservedLive
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CNPGPoolerReadiness {
|
|
61
|
+
text: string
|
|
62
|
+
level: HealthLevel
|
|
63
|
+
detail?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Readiness from the Pooler's Deployment; the Pooler's status only counts scheduled Pods. */
|
|
67
|
+
export function poolerReadiness(d: CNPGPoolerDeploymentLive | undefined): CNPGPoolerReadiness {
|
|
68
|
+
if (!d) return { text: 'Unknown', level: 'unknown', detail: 'Readiness is on the Pooler’s Deployment, which was not read' }
|
|
69
|
+
switch (d.state) {
|
|
70
|
+
case 'missing':
|
|
71
|
+
return { text: 'No Deployment', level: 'unhealthy', detail: `Deployment ${d.name} does not exist` }
|
|
72
|
+
case 'unreadable':
|
|
73
|
+
return { text: 'Unknown', level: 'unknown', detail: `No access to Deployment ${d.name}` }
|
|
74
|
+
case 'foreign':
|
|
75
|
+
return { text: 'Unknown', level: 'unknown', detail: `Deployment ${d.name} is not controlled by this Pooler` }
|
|
76
|
+
}
|
|
77
|
+
const want = d.replicas ?? 0
|
|
78
|
+
const ready = d.readyReplicas ?? 0
|
|
79
|
+
const detail = `from Deployment ${d.name}`
|
|
80
|
+
if (want === 0) return { text: 'Scaled to zero', level: 'neutral', detail }
|
|
81
|
+
if (ready === 0) return { text: `0/${want} ready`, level: 'unhealthy', detail }
|
|
82
|
+
if (ready < want) return { text: `${ready}/${want} ready`, level: 'degraded', detail }
|
|
83
|
+
return { text: `${ready}/${want} ready`, level: 'healthy', detail }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CNPGPoolerPoolRow {
|
|
87
|
+
database: string
|
|
88
|
+
user: string
|
|
89
|
+
poolModes: string[]
|
|
90
|
+
clActive?: number
|
|
91
|
+
clWaiting?: number
|
|
92
|
+
svActive?: number
|
|
93
|
+
svIdle?: number
|
|
94
|
+
svUsed?: number
|
|
95
|
+
maxwaitSeconds?: number
|
|
96
|
+
/** Pods whose exporter reported this pool. */
|
|
97
|
+
pods: number
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* One row per database/user pool, summed over the Pods that reported it
|
|
102
|
+
* (max wait is the maximum). A field no Pod reported stays undefined.
|
|
103
|
+
*/
|
|
104
|
+
export function aggregatePoolerPools(pods: CNPGPoolerPressureLive['pods']): CNPGPoolerPoolRow[] {
|
|
105
|
+
const rows = new Map<string, CNPGPoolerPoolRow>()
|
|
106
|
+
const add = (a: number | undefined, b: number | undefined) => (b === undefined ? a : (a ?? 0) + b)
|
|
107
|
+
for (const p of pods) {
|
|
108
|
+
for (const pool of p.pools ?? []) {
|
|
109
|
+
const key = `${pool.database}\u0000${pool.user}`
|
|
110
|
+
const row = rows.get(key) ?? { database: pool.database, user: pool.user, poolModes: [], pods: 0 }
|
|
111
|
+
row.pods++
|
|
112
|
+
row.clActive = add(row.clActive, pool.clActive)
|
|
113
|
+
row.clWaiting = add(row.clWaiting, pool.clWaiting)
|
|
114
|
+
row.svActive = add(row.svActive, pool.svActive)
|
|
115
|
+
row.svIdle = add(row.svIdle, pool.svIdle)
|
|
116
|
+
row.svUsed = add(row.svUsed, pool.svUsed)
|
|
117
|
+
if (pool.maxwaitSeconds !== undefined) row.maxwaitSeconds = Math.max(row.maxwaitSeconds ?? 0, pool.maxwaitSeconds)
|
|
118
|
+
if (pool.poolMode && !row.poolModes.includes(pool.poolMode)) row.poolModes.push(pool.poolMode)
|
|
119
|
+
rows.set(key, row)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return [...rows.values()].sort((a, b) => a.database.localeCompare(b.database) || a.user.localeCompare(b.user))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface CNPGPoolerPodRow {
|
|
126
|
+
pod: string
|
|
127
|
+
/** ok | partial | the read's failure state. */
|
|
128
|
+
state: string
|
|
129
|
+
error?: string
|
|
130
|
+
clActive?: number
|
|
131
|
+
clWaiting?: number
|
|
132
|
+
svActive?: number
|
|
133
|
+
maxwaitSeconds?: number
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Each PgBouncer Pod's own totals across its pools, so one saturated Pod is
|
|
138
|
+
* not hidden in the sum. A Pod that did not report keeps its state and no
|
|
139
|
+
* numbers; a field none of its pools reported stays undefined.
|
|
140
|
+
*/
|
|
141
|
+
export function poolerPodPressure(pods: CNPGPoolerPressureLive['pods']): CNPGPoolerPodRow[] {
|
|
142
|
+
const add = (a: number | undefined, b: number | undefined) => (b === undefined ? a : (a ?? 0) + b)
|
|
143
|
+
return pods
|
|
144
|
+
.map((p) => {
|
|
145
|
+
const row: CNPGPoolerPodRow = { pod: p.pod, state: p.state, error: p.error }
|
|
146
|
+
for (const pool of p.pools ?? []) {
|
|
147
|
+
row.clActive = add(row.clActive, pool.clActive)
|
|
148
|
+
row.clWaiting = add(row.clWaiting, pool.clWaiting)
|
|
149
|
+
row.svActive = add(row.svActive, pool.svActive)
|
|
150
|
+
if (pool.maxwaitSeconds !== undefined) row.maxwaitSeconds = Math.max(row.maxwaitSeconds ?? 0, pool.maxwaitSeconds)
|
|
151
|
+
}
|
|
152
|
+
return row
|
|
153
|
+
})
|
|
154
|
+
.sort((a, b) => a.pod.localeCompare(b.pod))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* PgBouncer settings worth showing, with the value PgBouncer uses when the
|
|
159
|
+
* Pooler leaves one unset. Defaults are named only where they were read from
|
|
160
|
+
* PgBouncer itself (SHOW CONFIG on PgBouncer 1.24); CloudNativePG writes only
|
|
161
|
+
* the parameters the Pooler sets.
|
|
162
|
+
*/
|
|
163
|
+
export const POOLER_LIMIT_PARAMETERS: { key: string; label: string; pgbouncerDefault?: string }[] = [
|
|
164
|
+
{ key: 'default_pool_size', label: 'Pool size (per database/user)', pgbouncerDefault: '20' },
|
|
165
|
+
{ key: 'max_client_conn', label: 'Max client connections', pgbouncerDefault: '100' },
|
|
166
|
+
{ key: 'max_db_connections', label: 'Max connections per database', pgbouncerDefault: '0 (unlimited)' },
|
|
167
|
+
{ key: 'max_user_connections', label: 'Max connections per user', pgbouncerDefault: '0 (unlimited)' },
|
|
168
|
+
{ key: 'reserve_pool_size', label: 'Reserve pool', pgbouncerDefault: '0' },
|
|
169
|
+
{ key: 'min_pool_size', label: 'Min pool size', pgbouncerDefault: '0' },
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
/** The Cluster Service PgBouncer forwards to, by the Pooler's type. */
|
|
173
|
+
export function poolerBackendService(cluster: string | undefined, type: string | undefined): string | undefined {
|
|
174
|
+
if (!cluster || !type) return undefined
|
|
175
|
+
if (type === 'rw' || type === 'ro' || type === 'r') return `${cluster}-${type}`
|
|
176
|
+
return undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Observed pause across PgBouncers: every one, none, some, or unknown. */
|
|
180
|
+
export function observedPause(o: CNPGPoolerObservedLive | undefined): { text: string; level: HealthLevel } | null {
|
|
181
|
+
if (!o) return null
|
|
182
|
+
if (o.state === 'loading') return { text: 'Reading…', level: 'unknown' }
|
|
183
|
+
if (o.state === 'denied') return { text: `Not observable: needs ${formatGrant(o.grant) ?? 'create pods/exec'}`, level: 'unknown' }
|
|
184
|
+
if (o.state === 'error') return { text: `Not observable: ${o.reason ?? 'read failed'}`, level: 'unknown' }
|
|
185
|
+
if (o.pods.length === 0) return { text: 'No PgBouncer Pods', level: 'unknown' }
|
|
186
|
+
const read = o.pods.filter((p) => p.state === 'ok' && p.paused !== undefined)
|
|
187
|
+
const paused = read.filter((p) => p.paused).length
|
|
188
|
+
const unread = o.pods.length - read.length
|
|
189
|
+
const tail = unread > 0 ? ` · ${unread} not read: ${o.pods.filter((p) => p.state !== 'ok' || p.paused === undefined).map((p) => `${p.pod} (${p.error ?? p.state})`).join('; ')}` : ''
|
|
190
|
+
if (read.length === 0) return { text: `Not observable${tail}`, level: 'unknown' }
|
|
191
|
+
if (paused === 0) return { text: `Serving (not paused) on ${read.length} of ${o.pods.length} PgBouncers${tail}`, level: unread ? 'unknown' : 'healthy' }
|
|
192
|
+
if (paused === read.length) return { text: `Paused on ${paused} of ${o.pods.length} PgBouncers${tail}`, level: 'degraded' }
|
|
193
|
+
return { text: `Paused on ${paused} of ${o.pods.length} PgBouncers${tail}`, level: 'alert' }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function poolerPressureCoverage(pods: CNPGPoolerPressureLive['pods']) {
|
|
197
|
+
const reporting = pods.filter((p) => p.state === 'ok' || p.state === 'partial')
|
|
198
|
+
const complete = pods.length > 0 && pods.every((p) => p.state === 'ok')
|
|
199
|
+
const gaps = pods.filter((p) => p.state !== 'ok').map((p) => {
|
|
200
|
+
const reason = [p.reason, p.error].filter(Boolean).join(' · ')
|
|
201
|
+
return `${p.pod}: ${p.state === 'partial' ? 'partial' : 'not read'}${reason ? ` (${reason})` : p.state === 'partial' ? '' : ` (${p.state})`}`
|
|
202
|
+
})
|
|
203
|
+
return {
|
|
204
|
+
reporting,
|
|
205
|
+
complete,
|
|
206
|
+
limitation: gaps.join('; ') || (pods.length === 0 ? 'No PgBouncer Pods answered' : undefined),
|
|
207
|
+
empty: complete ? 'Idle: no client pools open' : 'No pools seen in what was read',
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
type PoolerMetric = 'clActive' | 'clWaiting' | 'svActive' | 'svIdle' | 'svUsed' | 'maxwaitSeconds'
|
|
212
|
+
|
|
213
|
+
export function poolerPressureFact(pods: CNPGPoolerPressureLive['pods'], field: PoolerMetric, pool?: { database: string; user: string }): Fact {
|
|
214
|
+
const coverage = poolerPressureCoverage(pods)
|
|
215
|
+
const pools = coverage.reporting.flatMap((p) => p.pools ?? []).filter((p) => !pool || (p.database === pool.database && p.user === pool.user))
|
|
216
|
+
const values = pools.map((p) => p[field]).filter((v): v is number => v !== undefined)
|
|
217
|
+
const complete = coverage.complete && values.length === pools.length
|
|
218
|
+
const value = field === 'maxwaitSeconds' ? Math.max(0, ...values) : values.reduce((a, b) => a + b, 0)
|
|
219
|
+
const label = { clActive: 'Active clients', clWaiting: 'Waiting clients', svActive: 'Active servers', svIdle: 'Idle servers', svUsed: 'Used servers', maxwaitSeconds: 'Max wait' }[field]
|
|
220
|
+
if ((!complete && value === 0) || (pools.length > 0 && values.length === 0)) {
|
|
221
|
+
return { text: 'Unknown', tone: 'unknown', source: coverage.limitation ?? `${label} not reported for every pool` }
|
|
222
|
+
}
|
|
223
|
+
const text = field === 'maxwaitSeconds' ? `${value.toFixed(1)} s` : String(value)
|
|
224
|
+
return { text: `${complete ? '' : '≥'}${text}`, tone: field === 'clWaiting' && value > 0 ? 'degraded' : complete ? 'neutral' : 'unknown', source: complete ? undefined : coverage.limitation ?? `${label} not reported for every pool` }
|
|
225
|
+
}
|
|
@@ -1,134 +1,38 @@
|
|
|
1
|
-
import type { ReactNode } from 'react'
|
|
2
1
|
import { clsx } from 'clsx'
|
|
3
|
-
import
|
|
2
|
+
import { toneTextClass } from '../ui/status-tone'
|
|
3
|
+
import type { ComponentProps } from 'react'
|
|
4
|
+
import type { ResourceRef } from '../../types/core'
|
|
5
|
+
import { WorkspaceProblemCallout } from '../problems'
|
|
6
|
+
import type { CNPGProblem } from './workspace'
|
|
7
|
+
import { cnpgProblemForPresentation } from './problems'
|
|
4
8
|
import { StatusDot } from '../ui/status-tone'
|
|
5
|
-
import { Tooltip } from '../ui/Tooltip'
|
|
6
|
-
import { AlertBanner } from '../ui/drawer-components'
|
|
7
|
-
import { TONE_TEXT_CLASS } from '../ui/severity-tone'
|
|
8
|
-
import type { CNPGFact, CNPGProblem } from './workspace'
|
|
9
|
-
import { formatRelativeAgeTime } from '../../utils/format'
|
|
10
9
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type CNPGNavigate = (ref: CNPGRef) => void
|
|
19
|
-
|
|
20
|
-
const TONE_TEXT: Record<HealthLevel, string> = {
|
|
21
|
-
healthy: 'text-theme-text-primary',
|
|
22
|
-
degraded: TONE_TEXT_CLASS.amber,
|
|
23
|
-
alert: TONE_TEXT_CLASS.orange,
|
|
24
|
-
unhealthy: TONE_TEXT_CLASS.red,
|
|
25
|
-
unknown: 'text-theme-text-tertiary',
|
|
26
|
-
neutral: 'text-theme-text-secondary',
|
|
27
|
-
}
|
|
10
|
+
export { FactValue, FactSource, FactGrid, FactRow } from '../facts'
|
|
11
|
+
export { RefLink, type NavigateToRef as CNPGNavigate } from '../ui/RefLink'
|
|
12
|
+
export { SectionHeading as SummaryHeading } from '../ui/FoldSection'
|
|
13
|
+
export { toneTextClass } from '../ui/status-tone'
|
|
14
|
+
export type CNPGRef = ResourceRef
|
|
15
|
+
export { CNPG_PROBLEM_TONE, cnpgProblemForPresentation } from './problems'
|
|
28
16
|
|
|
29
17
|
export const CNPG_PRIMARY_BUTTON = 'btn-brand inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium'
|
|
30
18
|
export const CNPG_SECONDARY_BUTTON =
|
|
31
19
|
'inline-flex items-center gap-1.5 rounded-lg border border-theme-border bg-theme-surface px-3 py-1.5 text-sm text-theme-text-primary transition-colors hover:bg-theme-hover'
|
|
32
20
|
|
|
33
|
-
export function toneTextClass(tone: HealthLevel): string {
|
|
34
|
-
return TONE_TEXT[tone]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function FactValue({ fact, className }: { fact: CNPGFact; className?: string }) {
|
|
38
|
-
const when = fact.at ? formatRelativeAgeTime(fact.at) : null
|
|
39
|
-
const body = (
|
|
40
|
-
<span className={clsx(toneTextClass(fact.tone), className)}>
|
|
41
|
-
{fact.text}
|
|
42
|
-
{when && <span className="text-theme-text-secondary">{fact.text ? ' · ' : ''}{when}</span>}
|
|
43
|
-
</span>
|
|
44
|
-
)
|
|
45
|
-
if (!fact.source && !fact.at) return body
|
|
46
|
-
return (
|
|
47
|
-
<Tooltip content={[fact.at ? new Date(fact.at).toUTCString() : null, fact.source].filter(Boolean).join(' · ')} position="top">
|
|
48
|
-
{body}
|
|
49
|
-
</Tooltip>
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function FactSource({ fact }: { fact: CNPGFact }) {
|
|
54
|
-
if (!fact.source) return null
|
|
55
|
-
return <div className="mt-0.5 text-[11.5px] text-theme-text-tertiary">{fact.source}</div>
|
|
56
|
-
}
|
|
57
21
|
|
|
58
|
-
export function
|
|
59
|
-
return <
|
|
22
|
+
export function ProblemCallout({ problem, ...props }: Omit<ComponentProps<typeof WorkspaceProblemCallout>, 'rootKind' | 'problem'> & { problem: CNPGProblem }) {
|
|
23
|
+
return <WorkspaceProblemCallout {...props} problem={cnpgProblemForPresentation(problem)} rootKind="Cluster" />
|
|
60
24
|
}
|
|
61
25
|
|
|
62
|
-
export function
|
|
63
|
-
return
|
|
64
|
-
<>
|
|
65
|
-
<dt className="text-theme-text-tertiary">{label}</dt>
|
|
66
|
-
<dd className="min-w-0 break-words text-theme-text-primary">{children}</dd>
|
|
67
|
-
</>
|
|
68
|
-
)
|
|
26
|
+
export function ToneDot({ tone }: Pick<ComponentProps<typeof StatusDot>, 'tone'>) {
|
|
27
|
+
return <StatusDot tone={tone} size="sm" />
|
|
69
28
|
}
|
|
70
29
|
|
|
71
|
-
|
|
30
|
+
/** status.currentPrimary and the primary role label disagree: both are named rather than one silently winning. */
|
|
31
|
+
export function PrimaryConflictNote({ conflict }: { conflict: { status: string; labelled: string } }) {
|
|
72
32
|
return (
|
|
73
|
-
<div className=
|
|
74
|
-
<
|
|
75
|
-
|
|
33
|
+
<div className={clsx('mt-0.5 text-xs', toneTextClass('degraded'))}>
|
|
34
|
+
CNPG status says primary <span className="font-mono">{conflict.status}</span>; the Pod labelled primary is{' '}
|
|
35
|
+
<span className="font-mono">{conflict.labelled}</span>. Status may be stale, or a failover is under way.
|
|
76
36
|
</div>
|
|
77
37
|
)
|
|
78
38
|
}
|
|
79
|
-
|
|
80
|
-
export function RefLink({ refTo, onNavigate, children, mono }: { refTo: CNPGRef; onNavigate?: CNPGNavigate; children?: ReactNode; mono?: boolean }) {
|
|
81
|
-
const label = children ?? refTo.name
|
|
82
|
-
if (!onNavigate) return <span className={clsx(mono && 'font-mono')}>{label}</span>
|
|
83
|
-
return (
|
|
84
|
-
<button
|
|
85
|
-
type="button"
|
|
86
|
-
onClick={() => onNavigate(refTo)}
|
|
87
|
-
className={clsx('text-accent-text hover:underline text-left break-all', mono && 'font-mono')}
|
|
88
|
-
>
|
|
89
|
-
{label}
|
|
90
|
-
</button>
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const PROBLEM_VARIANT: Record<CNPGProblem['severity'], 'error' | 'warning' | 'info'> = {
|
|
95
|
-
critical: 'error',
|
|
96
|
-
warning: 'warning',
|
|
97
|
-
posture: 'info',
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function ProblemCallout({
|
|
101
|
-
problem,
|
|
102
|
-
more,
|
|
103
|
-
onNavigate,
|
|
104
|
-
action,
|
|
105
|
-
subjectIsSelf,
|
|
106
|
-
}: {
|
|
107
|
-
problem: CNPGProblem
|
|
108
|
-
more?: ReactNode
|
|
109
|
-
onNavigate?: CNPGNavigate
|
|
110
|
-
action?: ReactNode
|
|
111
|
-
/** The callout sits on the subject's own page, so linking to it would loop. */
|
|
112
|
-
subjectIsSelf?: boolean
|
|
113
|
-
}) {
|
|
114
|
-
const aboutChild = !subjectIsSelf && problem.subject.kind !== 'Cluster'
|
|
115
|
-
return (
|
|
116
|
-
<AlertBanner variant={PROBLEM_VARIANT[problem.severity]} title={problem.title} message={problem.detail}>
|
|
117
|
-
<div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-theme-text-tertiary">
|
|
118
|
-
{aboutChild && (
|
|
119
|
-
<span>
|
|
120
|
-
{problem.subject.kind}{' '}
|
|
121
|
-
<RefLink refTo={problem.subject} onNavigate={onNavigate} mono />
|
|
122
|
-
</span>
|
|
123
|
-
)}
|
|
124
|
-
<span>{problem.source === 'audit' ? 'Radar check' : 'Radar issue'}</span>
|
|
125
|
-
{action}
|
|
126
|
-
{more}
|
|
127
|
-
</div>
|
|
128
|
-
</AlertBanner>
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function ToneDot({ tone }: { tone: HealthLevel }) {
|
|
133
|
-
return <StatusDot tone={tone} size="sm" />
|
|
134
|
-
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WorkspaceProblem } from '../problems'
|
|
2
|
+
import type { CNPGProblem, CNPGProblemCategory } from './workspace'
|
|
3
|
+
|
|
4
|
+
export const CNPG_PROBLEM_TONE: Record<CNPGProblem['severity'], WorkspaceProblem['tone']> = {
|
|
5
|
+
critical: 'unhealthy',
|
|
6
|
+
warning: 'degraded',
|
|
7
|
+
posture: 'neutral',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function cnpgProblemForPresentation(problem: CNPGProblem): WorkspaceProblem<CNPGProblemCategory> {
|
|
11
|
+
return { ...problem, tone: CNPG_PROBLEM_TONE[problem.severity] }
|
|
12
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
+
cnpgScheduleDestinationBlocker,
|
|
4
|
+
cnpgArchiveMatchesCluster,
|
|
3
5
|
appliedFact,
|
|
4
6
|
backupDestination,
|
|
5
7
|
backupsForScheduledBackup,
|
|
6
8
|
clustersUsingCatalog,
|
|
7
9
|
databaseForDeclaration,
|
|
8
|
-
gitopsSourceOf,
|
|
9
10
|
inferredObjectStoreHealth,
|
|
10
11
|
isBackupFromSchedule,
|
|
11
12
|
issuesForObject,
|
|
@@ -16,6 +17,23 @@ import {
|
|
|
16
17
|
scheduledBackupOf,
|
|
17
18
|
usersOfObjectStore,
|
|
18
19
|
} from './relations'
|
|
20
|
+
|
|
21
|
+
it('recognizes archive origin aliases without merging distinct paths', () => {
|
|
22
|
+
const cluster = { metadata: { name: 'pg' }, spec: { backup: { barmanObjectStore: { destinationPath: 's3://bucket/path', endpointURL: 'https://storage.example' } } } }
|
|
23
|
+
const source = { kind: 'inTree' as const, serverName: 'pg', barmanObjectStore: { destinationPath: 's3://BUCKET/path/', endpointURL: 'https://STORAGE.EXAMPLE:443/' } }
|
|
24
|
+
expect(cnpgArchiveMatchesCluster(cluster, source)).toBe(true)
|
|
25
|
+
expect(cnpgArchiveMatchesCluster(cluster, { ...source, barmanObjectStore: { ...source.barmanObjectStore, destinationPath: 's3://bucket/other' } })).toBe(false)
|
|
26
|
+
expect(cnpgArchiveMatchesCluster(cluster, { ...source, barmanObjectStore: { ...source.barmanObjectStore, endpointURL: 'https://storage.example/prefix' } })).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('checks the destination for the schedule method and keeps unread targets unknown', () => {
|
|
30
|
+
const cluster = { apiVersion: 'postgresql.cnpg.io/v1', kind: 'Cluster', metadata: { name: 'payments', namespace: 'pg' }, spec: { backup: { volumeSnapshot: {} }, plugins: [{ name: 'barman-cloud.cloudnative-pg.io', parameters: { barmanObjectName: 'store' } }] } }
|
|
31
|
+
const schedule = { metadata: { namespace: 'pg' }, spec: { cluster: { name: 'payments' } } }
|
|
32
|
+
expect(cnpgScheduleDestinationBlocker(schedule, [cluster])).toBe('No barmanObjectStore destination')
|
|
33
|
+
expect(cnpgScheduleDestinationBlocker(schedule, [])).toBeNull()
|
|
34
|
+
expect(cnpgScheduleDestinationBlocker({ ...schedule, spec: { ...schedule.spec, method: 'volumeSnapshot' } }, [cluster])).toBeNull()
|
|
35
|
+
expect(cnpgScheduleDestinationBlocker({ ...schedule, spec: { ...schedule.spec, method: 'plugin', pluginConfiguration: { name: 'barman-cloud.cloudnative-pg.io' } } }, [cluster])).toBeNull()
|
|
36
|
+
})
|
|
19
37
|
import { CNPG_WORKSPACE_KEYS, type CNPGWorkspaceIssue, type CNPGWorkspaceResponse } from './workspace'
|
|
20
38
|
|
|
21
39
|
const PG = 'postgresql.cnpg.io/v1'
|
|
@@ -104,9 +122,10 @@ describe('scheduledBackupOf / backupsForScheduledBackup', () => {
|
|
|
104
122
|
describe('objectStoreForBackup / backupDestination', () => {
|
|
105
123
|
const clusters = [pluginCluster('main', 'store-a')]
|
|
106
124
|
|
|
107
|
-
it('
|
|
125
|
+
it('ignores the Backup parameter and infers the store from the current Cluster', () => {
|
|
108
126
|
const b = backup('b', { spec: { method: 'plugin', pluginConfiguration: { name: PLUGIN, parameters: { barmanObjectName: 'store-b' } } } })
|
|
109
|
-
expect(objectStoreForBackup(b, clusters)).toEqual({ name: 'store-
|
|
127
|
+
expect(objectStoreForBackup(b, clusters)).toEqual({ name: 'store-a', inferred: true })
|
|
128
|
+
expect(backupDestination(b, clusters)).toEqual({ type: 'objectStore', name: 'store-a', inferred: true })
|
|
110
129
|
})
|
|
111
130
|
|
|
112
131
|
it("marks a store taken from the Cluster's current plugin as inferred", () => {
|
|
@@ -115,6 +134,14 @@ describe('objectStoreForBackup / backupDestination', () => {
|
|
|
115
134
|
expect(backupDestination(b, clusters)).toEqual({ type: 'objectStore', name: 'store-a', inferred: true })
|
|
116
135
|
})
|
|
117
136
|
|
|
137
|
+
it('cannot resolve a Backup parameter without a configured, readable target Cluster', () => {
|
|
138
|
+
const b = backup('b', { spec: { method: 'plugin', pluginConfiguration: { name: PLUGIN, parameters: { barmanObjectName: 'store-b' } } } })
|
|
139
|
+
for (const targets of [[], [cluster('main')], [pluginCluster('other', 'store-a')], [{ ...pluginCluster('main', 'store-a'), metadata: { name: 'main', namespace: 'other' } }], [cluster('main', 'pg', { plugins: [{ name: PLUGIN, enabled: false, parameters: { barmanObjectName: 'store-a' } }] })]]) {
|
|
140
|
+
expect(objectStoreForBackup(b, targets)).toBeNull()
|
|
141
|
+
expect(backupDestination(b, targets)).toEqual({ type: 'unknown' })
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
118
145
|
it('checks plugin identity before reading barmanObjectName', () => {
|
|
119
146
|
const other = backup('b', { spec: { method: 'plugin', pluginConfiguration: { name: 'other.example.com', parameters: { barmanObjectName: 'store-b' } } } })
|
|
120
147
|
expect(objectStoreForBackup(other, clusters)).toBeNull()
|
|
@@ -129,6 +156,23 @@ describe('objectStoreForBackup / backupDestination', () => {
|
|
|
129
156
|
})
|
|
130
157
|
})
|
|
131
158
|
|
|
159
|
+
describe('barman-cloud schedule destinations', () => {
|
|
160
|
+
const schedule = { apiVersion: PG, kind: 'ScheduledBackup', metadata: { name: 'nightly', namespace: 'pg' }, spec: { cluster: { name: 'main' }, method: 'plugin', pluginConfiguration: { name: PLUGIN, parameters: { barmanObjectName: 'schedule-store', serverName: 'schedule-server' } } } }
|
|
161
|
+
|
|
162
|
+
it('blocks a schedule override when the Cluster plugin has no destination', () => {
|
|
163
|
+
const target = cluster('main', 'pg', { plugins: [{ name: PLUGIN }] })
|
|
164
|
+
expect(cnpgScheduleDestinationBlocker(schedule, [target])).toBe('No backup destination')
|
|
165
|
+
expect(objectStoreForBackup(schedule, [target])).toBeNull()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('uses the configured Cluster destination despite a conflicting schedule override', () => {
|
|
169
|
+
const target = pluginCluster('main', 'cluster-store')
|
|
170
|
+
expect(cnpgScheduleDestinationBlocker(schedule, [target])).toBeNull()
|
|
171
|
+
expect(objectStoreForBackup(schedule, [target])).toEqual({ name: 'cluster-store', inferred: true })
|
|
172
|
+
expect(cnpgScheduleDestinationBlocker(schedule, [cluster('main', 'pg', { plugins: [{ name: PLUGIN, enabled: false, parameters: { barmanObjectName: 'cluster-store' } }] })])).toBe('No backup destination')
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
132
176
|
describe('ObjectStore users and inferred health', () => {
|
|
133
177
|
const store = {
|
|
134
178
|
apiVersion: BARMAN,
|
|
@@ -254,16 +298,6 @@ describe('declarations', () => {
|
|
|
254
298
|
expect(missingManagedRole({ status: { message: 'connection refused' } }, cluster('main'))).toBeNull()
|
|
255
299
|
})
|
|
256
300
|
|
|
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
301
|
const decl = (kind: string, name: string, clusterName: string, dbname: string, ns = 'pg') => ({
|
|
268
302
|
apiVersion: PG,
|
|
269
303
|
kind,
|
|
@@ -302,3 +336,19 @@ describe('relationUnavailable', () => {
|
|
|
302
336
|
expect(relationUnavailable(null, 'backups', 'pg', 'Backups')).toBe('Backups could not be read')
|
|
303
337
|
})
|
|
304
338
|
})
|
|
339
|
+
|
|
340
|
+
it('does not apply declaration results from an earlier spec', () => {
|
|
341
|
+
for (const applied of [true, false]) {
|
|
342
|
+
expect(appliedFact({ metadata: { generation: 3 }, status: { applied, observedGeneration: 2 } })).toEqual({ text: 'Pending · awaiting the operator for the current spec', tone: 'unknown' })
|
|
343
|
+
}
|
|
344
|
+
expect(appliedFact({ metadata: { generation: 3 }, status: { applied: true, observedGeneration: 3 } }).tone).toBe('healthy')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
it('never infers a plugin archive from a replacement Cluster', () => {
|
|
349
|
+
const target = { ...pluginCluster('main', 'new-store'), metadata: { name: 'main', namespace: 'pg', uid: 'new', creationTimestamp: '2026-10-01T00:00:00Z' } }
|
|
350
|
+
for (const status of [{ pluginMetadata: { clusterUID: 'old' } }, { startedAt: '2026-09-30T00:00:00Z' }]) {
|
|
351
|
+
const old = backup('old', { spec: { method: 'plugin', pluginConfiguration: { name: PLUGIN } }, status })
|
|
352
|
+
expect(objectStoreForBackup(old, [target])).toBeNull()
|
|
353
|
+
}
|
|
354
|
+
})
|