@skyhook-io/k8s-ui 1.16.3 → 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.
Files changed (309) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/applications/ApplicationDetail.test.tsx +15 -0
  4. package/src/components/applications/ApplicationDetail.tsx +17 -15
  5. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  6. package/src/components/charts/AreaChart.test.tsx +25 -0
  7. package/src/components/charts/AreaChart.tsx +141 -12
  8. package/src/components/charts/index.ts +1 -0
  9. package/src/components/checks/ChecksView.tsx +18 -1
  10. package/src/components/checks/types.ts +2 -13
  11. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  12. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  13. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  14. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  15. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  16. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  17. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  18. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  19. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  20. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  21. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  22. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  23. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  24. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  25. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  26. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  27. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  28. package/src/components/cnpg/backupRuns.test.ts +17 -0
  29. package/src/components/cnpg/backupRuns.ts +19 -0
  30. package/src/components/cnpg/connect.test.ts +124 -0
  31. package/src/components/cnpg/connect.ts +155 -0
  32. package/src/components/cnpg/databaseRole.test.ts +38 -0
  33. package/src/components/cnpg/databaseRole.ts +80 -0
  34. package/src/components/cnpg/ha.test.ts +446 -0
  35. package/src/components/cnpg/ha.ts +621 -0
  36. package/src/components/cnpg/index.ts +18 -0
  37. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  38. package/src/components/cnpg/logicalReplication.ts +252 -0
  39. package/src/components/cnpg/pooler.test.ts +84 -0
  40. package/src/components/cnpg/pooler.ts +225 -0
  41. package/src/components/cnpg/primitives.tsx +21 -117
  42. package/src/components/cnpg/problems.ts +12 -0
  43. package/src/components/cnpg/relations.test.ts +63 -13
  44. package/src/components/cnpg/relations.ts +61 -52
  45. package/src/components/cnpg/schedule.ts +35 -0
  46. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  47. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  48. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  49. package/src/components/cnpg/workspace.test.ts +380 -9
  50. package/src/components/cnpg/workspace.ts +1137 -136
  51. package/src/components/dock/DockContext.test.tsx +32 -0
  52. package/src/components/dock/DockContext.tsx +18 -4
  53. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  54. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  55. package/src/components/dock/TerminalTab.tsx +20 -8
  56. package/src/components/facts/certainty.tsx +36 -0
  57. package/src/components/facts/facts.test.tsx +24 -0
  58. package/src/components/facts/facts.tsx +62 -0
  59. package/src/components/facts/index.ts +6 -0
  60. package/src/components/facts/managed-by.test.tsx +17 -0
  61. package/src/components/facts/managed-by.tsx +36 -0
  62. package/src/components/facts/public-exports.test.tsx +35 -0
  63. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  64. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  65. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  66. package/src/components/gitops/GitOpsTableView.tsx +9 -24
  67. package/src/components/gitops/detail-helpers.ts +1 -1
  68. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  69. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  70. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  71. package/src/components/gitops/insights/GitOpsInsightViews.tsx +29 -83
  72. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  73. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
  74. package/src/components/gitops/tree/layout.test.ts +111 -0
  75. package/src/components/gitops/tree/layout.ts +128 -0
  76. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  77. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  78. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  79. package/src/components/issues/IssuesView.tsx +40 -5
  80. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  81. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  82. package/src/components/issues/index.ts +3 -1
  83. package/src/components/issues/issue-timing.ts +14 -3
  84. package/src/components/issues/issues.test.ts +50 -2
  85. package/src/components/issues/severity.ts +21 -0
  86. package/src/components/issues/types.ts +23 -0
  87. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  88. package/src/components/logs/LogCore.tsx +25 -17
  89. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  90. package/src/components/logs/LogsViewer.test.tsx +291 -0
  91. package/src/components/logs/LogsViewer.tsx +72 -11
  92. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  93. package/src/components/logs/StructuredLogLine.tsx +22 -30
  94. package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
  95. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  96. package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
  97. package/src/components/logs/useLogBuffer.test.ts +8 -0
  98. package/src/components/logs/useLogStream.ts +28 -1
  99. package/src/components/problems/index.ts +1 -0
  100. package/src/components/problems/problems.test.tsx +63 -0
  101. package/src/components/problems/problems.tsx +165 -0
  102. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  103. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  104. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  105. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  106. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  107. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  108. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  109. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  110. package/src/components/resources/ResourcesView.tsx +309 -117
  111. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  112. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
  113. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
  114. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  115. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
  116. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  117. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
  118. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
  119. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
  120. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
  121. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
  122. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  124. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +7 -8
  125. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  126. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  127. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +9 -17
  128. package/src/components/resources/renderers/CNPGShared.tsx +2 -2
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  130. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  131. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  132. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  133. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  134. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  135. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  136. package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
  137. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
  138. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +62 -0
  139. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
  140. package/src/components/resources/renderers/EventRenderer.tsx +3 -3
  141. package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
  142. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  143. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  144. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
  145. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
  146. package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
  147. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
  148. package/src/components/resources/renderers/HPARenderer.tsx +57 -17
  149. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
  150. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
  151. package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
  152. package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
  153. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  154. package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
  155. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  156. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  157. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  158. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
  159. package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
  160. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +15 -13
  161. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  163. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  164. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  165. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  166. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  167. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
  170. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
  171. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
  172. package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
  173. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  174. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  175. package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
  176. package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
  177. package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
  178. package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
  179. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
  180. package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
  181. package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
  182. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  183. package/src/components/resources/renderers/PodRenderer.tsx +62 -36
  184. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  185. package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
  186. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  187. package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
  188. package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
  189. package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
  190. package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
  191. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +9 -22
  192. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  193. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
  194. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
  195. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
  196. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
  197. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
  198. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  199. package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
  200. package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
  201. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
  202. package/src/components/resources/resource-utils-capi.ts +1 -1
  203. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  204. package/src/components/resources/resource-utils-cnpg.ts +51 -24
  205. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  206. package/src/components/resources/resource-utils-contour.ts +16 -0
  207. package/src/components/resources/resource-utils-istio.ts +12 -8
  208. package/src/components/resources/resource-utils-keda.ts +1 -7
  209. package/src/components/resources/resource-utils-knative.ts +18 -0
  210. package/src/components/resources/resource-utils-kyverno.ts +1 -7
  211. package/src/components/resources/resource-utils-velero.ts +0 -12
  212. package/src/components/resources/resource-utils.ts +99 -39
  213. package/src/components/resources/rules-column-width.test.ts +1 -1
  214. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  215. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  216. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  217. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  218. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  219. package/src/components/shared/DetailShell.test.tsx +80 -0
  220. package/src/components/shared/DetailShell.tsx +85 -10
  221. package/src/components/shared/DrainPlanDialog.tsx +1 -0
  222. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  223. package/src/components/shared/EditableYamlView.tsx +1 -1
  224. package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
  225. package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
  226. package/src/components/shared/ResourceActionsBar.tsx +20 -24
  227. package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
  228. package/src/components/shared/SetImageDialog.test.ts +75 -6
  229. package/src/components/shared/SetImageDialog.tsx +92 -84
  230. package/src/components/shared/index.ts +4 -1
  231. package/src/components/timeline/TimelineList.tsx +2 -15
  232. package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
  233. package/src/components/timeline/timeline-filters.ts +1 -0
  234. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  235. package/src/components/topology/K8sResourceNode.tsx +44 -2
  236. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  237. package/src/components/topology/TopologyGraph.tsx +8 -74
  238. package/src/components/topology/TopologySearch.tsx +1 -0
  239. package/src/components/topology/internet-groups.test.ts +35 -0
  240. package/src/components/topology/internet-groups.ts +55 -0
  241. package/src/components/topology/layout.ts +1 -1
  242. package/src/components/ui/Badge.tsx +1 -0
  243. package/src/components/ui/FoldSection.tsx +68 -0
  244. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  245. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  246. package/src/components/ui/RefLink.tsx +20 -0
  247. package/src/components/ui/RowActionMenu.tsx +8 -3
  248. package/src/components/ui/ScheduleValue.test.tsx +5 -0
  249. package/src/components/ui/ScheduleValue.tsx +3 -2
  250. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  251. package/src/components/ui/drawer-components.test.tsx +175 -1
  252. package/src/components/ui/drawer-components.tsx +103 -36
  253. package/src/components/ui/index.ts +3 -1
  254. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  255. package/src/components/ui/monacoRuntime.ts +14 -19
  256. package/src/components/ui/status-tone.test.ts +10 -1
  257. package/src/components/ui/status-tone.tsx +29 -0
  258. package/src/components/workload/WorkloadView.deleted.test.tsx +101 -0
  259. package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
  260. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  261. package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
  262. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  263. package/src/components/workload/WorkloadView.tsx +297 -104
  264. package/src/index.ts +14 -1
  265. package/src/monaco-deep.d.ts +19 -12
  266. package/src/theme/components.css +13 -0
  267. package/src/types/capacity.ts +21 -2
  268. package/src/types/core.ts +60 -2
  269. package/src/types/gitops-flux-status.test.ts +32 -1
  270. package/src/types/gitops-insights.ts +19 -2
  271. package/src/types/gitops-tree.ts +10 -0
  272. package/src/types/gitops.ts +26 -6
  273. package/src/utils/api-resources.ts +7 -0
  274. package/src/utils/application-history.ts +1 -0
  275. package/src/utils/cnpg-backup.test.ts +26 -0
  276. package/src/utils/cnpg-backup.ts +70 -0
  277. package/src/utils/format.test.ts +49 -2
  278. package/src/utils/format.ts +9 -1
  279. package/src/utils/gateway-references.test.ts +17 -0
  280. package/src/utils/gateway-references.ts +29 -0
  281. package/src/utils/gitops-owner.ts +3 -2
  282. package/src/utils/gitops-write-guard.test.ts +401 -0
  283. package/src/utils/gitops-write-guard.ts +509 -0
  284. package/src/utils/grant.test.ts +19 -0
  285. package/src/utils/grant.ts +35 -0
  286. package/src/utils/index.ts +6 -0
  287. package/src/utils/ingress-class-references.ts +22 -0
  288. package/src/utils/k8s-errors.ts +7 -7
  289. package/src/utils/log-format.test.ts +41 -0
  290. package/src/utils/log-format.ts +71 -15
  291. package/src/utils/log-level.ts +12 -8
  292. package/src/utils/navigation.test.ts +70 -0
  293. package/src/utils/navigation.ts +91 -8
  294. package/src/utils/node-lifecycle.test.ts +36 -0
  295. package/src/utils/node-lifecycle.ts +77 -0
  296. package/src/utils/object-reference-navigation.test.ts +43 -0
  297. package/src/utils/resource-hierarchy.test.ts +69 -0
  298. package/src/utils/resource-hierarchy.ts +41 -7
  299. package/src/utils/resource-icons.ts +2 -0
  300. package/src/utils/resource-refs.test.ts +23 -0
  301. package/src/utils/resource-refs.ts +30 -0
  302. package/src/utils/skeleton-yaml.test.ts +21 -0
  303. package/src/utils/skeleton-yaml.ts +16 -0
  304. package/src/utils/url-path.test.ts +35 -0
  305. package/src/utils/url-path.ts +21 -0
  306. package/src/utils/validators.test.ts +4 -13
  307. package/src/utils/validators.ts +5 -19
  308. package/src/utils/view-transition.test.ts +35 -1
  309. 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 type { HealthLevel } from '../resources/resource-utils'
4
- import { formatAge } from '../resources/resource-utils'
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'
5
8
  import { StatusDot } from '../ui/status-tone'
6
- import { Tooltip } from '../ui/Tooltip'
7
- import { AlertBanner } from '../ui/drawer-components'
8
- import { TONE_TEXT_CLASS } from '../ui/severity-tone'
9
- import type { CNPGFact, CNPGProblem } from './workspace'
10
9
 
11
- export interface CNPGRef {
12
- kind: string
13
- group?: string
14
- namespace: string
15
- name: string
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 age = fact.at ? formatAge(fact.at) : null
39
- const body = (
40
- <span className={clsx(toneTextClass(fact.tone), className)}>
41
- {fact.text}
42
- {age && <span className="text-theme-text-secondary">{fact.text ? ' · ' : ''}{age} ago</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 FactGrid({ children }: { children: ReactNode }) {
59
- return <dl className="grid grid-cols-[9.5rem_minmax(0,1fr)] gap-x-4 gap-y-2 text-sm">{children}</dl>
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 FactRow({ label, children }: { label: ReactNode; children: ReactNode }) {
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
- export function SummaryHeading({ children, hint }: { children: ReactNode; hint?: ReactNode }) {
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="mb-2 mt-5 flex items-baseline gap-2 first:mt-0">
74
- <h3 className="text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">{children}</h3>
75
- {hint && <span className="text-[11px] text-theme-text-tertiary">{hint}</span>}
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('prefers the store the backup recorded', () => {
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-b', inferred: false })
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
+ })