@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.
Files changed (227) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  4. package/src/components/charts/AreaChart.test.tsx +25 -0
  5. package/src/components/charts/AreaChart.tsx +141 -12
  6. package/src/components/charts/index.ts +1 -0
  7. package/src/components/checks/ChecksView.tsx +18 -1
  8. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  9. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  10. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  11. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  12. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  13. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  14. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  15. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  16. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  17. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  18. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  19. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  20. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  21. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  22. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  23. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  24. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  25. package/src/components/cnpg/backupRuns.test.ts +17 -0
  26. package/src/components/cnpg/backupRuns.ts +19 -0
  27. package/src/components/cnpg/connect.test.ts +124 -0
  28. package/src/components/cnpg/connect.ts +155 -0
  29. package/src/components/cnpg/databaseRole.test.ts +38 -0
  30. package/src/components/cnpg/databaseRole.ts +80 -0
  31. package/src/components/cnpg/ha.test.ts +446 -0
  32. package/src/components/cnpg/ha.ts +621 -0
  33. package/src/components/cnpg/index.ts +18 -0
  34. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  35. package/src/components/cnpg/logicalReplication.ts +252 -0
  36. package/src/components/cnpg/pooler.test.ts +84 -0
  37. package/src/components/cnpg/pooler.ts +225 -0
  38. package/src/components/cnpg/primitives.tsx +21 -117
  39. package/src/components/cnpg/problems.ts +12 -0
  40. package/src/components/cnpg/relations.test.ts +63 -13
  41. package/src/components/cnpg/relations.ts +61 -52
  42. package/src/components/cnpg/schedule.ts +35 -0
  43. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  44. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  45. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  46. package/src/components/cnpg/workspace.test.ts +380 -9
  47. package/src/components/cnpg/workspace.ts +1137 -136
  48. package/src/components/dock/DockContext.test.tsx +32 -0
  49. package/src/components/dock/DockContext.tsx +18 -4
  50. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  51. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  52. package/src/components/dock/TerminalTab.tsx +20 -8
  53. package/src/components/facts/certainty.tsx +36 -0
  54. package/src/components/facts/facts.test.tsx +24 -0
  55. package/src/components/facts/facts.tsx +62 -0
  56. package/src/components/facts/index.ts +6 -0
  57. package/src/components/facts/managed-by.test.tsx +17 -0
  58. package/src/components/facts/managed-by.tsx +36 -0
  59. package/src/components/facts/public-exports.test.tsx +35 -0
  60. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  61. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  62. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  63. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  64. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  65. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  66. package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
  67. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  68. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
  69. package/src/components/gitops/tree/layout.test.ts +111 -0
  70. package/src/components/gitops/tree/layout.ts +128 -0
  71. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  72. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  73. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  74. package/src/components/issues/IssuesView.tsx +40 -5
  75. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  76. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  77. package/src/components/issues/index.ts +3 -1
  78. package/src/components/issues/issue-timing.ts +12 -1
  79. package/src/components/issues/issues.test.ts +49 -1
  80. package/src/components/issues/severity.ts +21 -0
  81. package/src/components/issues/types.ts +23 -0
  82. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  83. package/src/components/logs/LogCore.tsx +25 -17
  84. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  85. package/src/components/logs/LogsViewer.test.tsx +291 -0
  86. package/src/components/logs/LogsViewer.tsx +72 -11
  87. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  88. package/src/components/logs/StructuredLogLine.tsx +22 -30
  89. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  90. package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
  91. package/src/components/logs/useLogBuffer.test.ts +8 -0
  92. package/src/components/logs/useLogStream.ts +17 -1
  93. package/src/components/problems/index.ts +1 -0
  94. package/src/components/problems/problems.test.tsx +63 -0
  95. package/src/components/problems/problems.tsx +165 -0
  96. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  97. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  98. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  99. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  100. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  101. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  102. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  103. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  104. package/src/components/resources/ResourcesView.tsx +302 -109
  105. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  106. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  107. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  108. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  109. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  110. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  111. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  112. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  113. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  114. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  115. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  116. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  117. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  118. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  119. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  120. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  121. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  122. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  123. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  124. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  125. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  126. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  127. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  128. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  129. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  130. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  131. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  132. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  133. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  134. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  135. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  136. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  137. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  138. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  139. package/src/components/resources/renderers/PodRenderer.tsx +41 -29
  140. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  141. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  142. package/src/components/resources/resource-utils-capi.ts +1 -1
  143. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  144. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  145. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  146. package/src/components/resources/resource-utils-contour.ts +16 -0
  147. package/src/components/resources/resource-utils-istio.ts +12 -8
  148. package/src/components/resources/resource-utils-knative.ts +18 -0
  149. package/src/components/resources/resource-utils.ts +82 -6
  150. package/src/components/resources/rules-column-width.test.ts +1 -1
  151. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  152. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  153. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  154. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  155. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  156. package/src/components/shared/DetailShell.test.tsx +80 -0
  157. package/src/components/shared/DetailShell.tsx +85 -10
  158. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  159. package/src/components/shared/EditableYamlView.tsx +1 -1
  160. package/src/components/shared/ResourceActionsBar.tsx +11 -4
  161. package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
  162. package/src/components/shared/index.ts +2 -0
  163. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  164. package/src/components/timeline/timeline-filters.ts +1 -0
  165. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  166. package/src/components/topology/K8sResourceNode.tsx +44 -2
  167. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  168. package/src/components/topology/TopologyGraph.tsx +3 -71
  169. package/src/components/topology/TopologySearch.tsx +1 -0
  170. package/src/components/topology/internet-groups.test.ts +35 -0
  171. package/src/components/topology/internet-groups.ts +55 -0
  172. package/src/components/topology/layout.ts +1 -1
  173. package/src/components/ui/Badge.tsx +1 -0
  174. package/src/components/ui/FoldSection.tsx +68 -0
  175. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  176. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  177. package/src/components/ui/RefLink.tsx +20 -0
  178. package/src/components/ui/RowActionMenu.tsx +8 -3
  179. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  180. package/src/components/ui/drawer-components.test.tsx +110 -1
  181. package/src/components/ui/drawer-components.tsx +62 -9
  182. package/src/components/ui/index.ts +3 -1
  183. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  184. package/src/components/ui/monacoRuntime.ts +14 -19
  185. package/src/components/ui/status-tone.test.ts +10 -1
  186. package/src/components/ui/status-tone.tsx +29 -0
  187. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  188. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  189. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  190. package/src/components/workload/WorkloadView.tsx +180 -64
  191. package/src/index.ts +14 -1
  192. package/src/monaco-deep.d.ts +19 -12
  193. package/src/theme/components.css +13 -0
  194. package/src/types/capacity.ts +11 -2
  195. package/src/types/core.ts +29 -2
  196. package/src/types/gitops-flux-status.test.ts +32 -1
  197. package/src/types/gitops-insights.ts +19 -2
  198. package/src/types/gitops-tree.ts +10 -0
  199. package/src/types/gitops.ts +26 -6
  200. package/src/utils/api-resources.ts +7 -0
  201. package/src/utils/application-history.ts +1 -0
  202. package/src/utils/cnpg-backup.test.ts +26 -0
  203. package/src/utils/cnpg-backup.ts +70 -0
  204. package/src/utils/format.test.ts +9 -0
  205. package/src/utils/format.ts +4 -0
  206. package/src/utils/gitops-owner.ts +3 -2
  207. package/src/utils/grant.test.ts +19 -0
  208. package/src/utils/grant.ts +35 -0
  209. package/src/utils/index.ts +4 -0
  210. package/src/utils/k8s-errors.ts +7 -7
  211. package/src/utils/log-format.test.ts +41 -0
  212. package/src/utils/log-format.ts +71 -15
  213. package/src/utils/log-level.ts +12 -8
  214. package/src/utils/navigation.test.ts +57 -0
  215. package/src/utils/navigation.ts +86 -8
  216. package/src/utils/object-reference-navigation.test.ts +43 -0
  217. package/src/utils/resource-hierarchy.test.ts +37 -0
  218. package/src/utils/resource-hierarchy.ts +15 -7
  219. package/src/utils/resource-icons.ts +2 -0
  220. package/src/utils/skeleton-yaml.test.ts +21 -0
  221. package/src/utils/skeleton-yaml.ts +16 -0
  222. package/src/utils/url-path.test.ts +35 -0
  223. package/src/utils/url-path.ts +21 -0
  224. package/src/utils/validators.test.ts +4 -13
  225. package/src/utils/validators.ts +5 -19
  226. package/src/utils/view-transition.test.ts +35 -1
  227. package/src/utils/view-transition.ts +16 -1
@@ -0,0 +1,155 @@
1
+ import { cnpgHASourceText, type CNPGClusterHA } from './ha'
2
+ import { getCNPGClusterIsReplica } from '../resources/resource-utils-cnpg'
3
+
4
+ /** The port CloudNativePG gives PostgreSQL and PgBouncer when a Service template sets none. */
5
+ export const CNPG_DEFAULT_PORT = 5432
6
+
7
+ export type CNPGConnectRole = 'rw' | 'ro' | 'r' | 'pooler' | 'additional'
8
+
9
+ export interface CNPGConnectEndpoint {
10
+ role: CNPGConnectRole
11
+ /** The Service name. */
12
+ name: string
13
+ host: string
14
+ port: number
15
+ /** True when the port comes from a Service template rather than CloudNativePG's default. */
16
+ portFromTemplate: boolean
17
+ /** What the Service selects: the primary, standbys only, any instance, or through a Pooler. */
18
+ selects: string
19
+ /** Pooler `spec.type`, for pooler endpoints. */
20
+ poolerType?: string
21
+ }
22
+
23
+ export interface CNPGConnectValue {
24
+ value?: string
25
+ /** The field the value was read from, or why it is inferred. */
26
+ source: string
27
+ }
28
+
29
+ export interface CNPGConnectInfo {
30
+ endpoints: CNPGConnectEndpoint[]
31
+ database: CNPGConnectValue
32
+ owner: CNPGConnectValue
33
+ secret: { name: string; source: string; byConvention: boolean }
34
+ /** A replica cluster's services all reach instances that only replay. */
35
+ replicaCluster: boolean
36
+ /** Services named in `spec.managed.services.disabledDefaultServices`. */
37
+ disabled: ('ro' | 'r')[]
38
+ }
39
+
40
+ const SELECTS: Record<'rw' | 'ro' | 'r', string> = {
41
+ rw: 'the primary (read-write)',
42
+ ro: 'standbys only (read-only)',
43
+ // -r selects every instance Pod (cnpg.io/podRole=instance), the primary included.
44
+ r: 'any instance (may reach the primary; not read-only)',
45
+ }
46
+
47
+ const BOOTSTRAP_ORDER = ['recovery', 'pg_basebackup', 'initdb'] as const
48
+
49
+ function templatePort(template: any): number | undefined {
50
+ const port = template?.spec?.ports?.[0]?.port
51
+ return typeof port === 'number' && port > 0 ? port : undefined
52
+ }
53
+
54
+ function endpoint(role: CNPGConnectRole, name: string, ns: string, selects: string, template?: any): CNPGConnectEndpoint {
55
+ const port = templatePort(template)
56
+ return { role, name, host: `${name}.${ns}.svc`, port: port ?? CNPG_DEFAULT_PORT, portFromTemplate: port !== undefined, selects }
57
+ }
58
+
59
+ // Mirrors CloudNativePG's GetApplicationDatabaseName / Owner / SecretName:
60
+ // recovery, then pg_basebackup, then initdb, first non-empty wins.
61
+ function fromBootstrap(bootstrap: any, pick: (section: any) => string | undefined, field: string): CNPGConnectValue | undefined {
62
+ for (const method of BOOTSTRAP_ORDER) {
63
+ const v = pick(bootstrap?.[method])
64
+ if (v) return { value: v, source: `spec.bootstrap.${method}.${field}` }
65
+ }
66
+ return undefined
67
+ }
68
+
69
+ /**
70
+ * How applications reach a CloudNativePG Cluster, derived from its spec and
71
+ * the Poolers that front it. Nothing here reads a Secret: the credentials
72
+ * Secret is named from the spec or CloudNativePG's `<cluster>-app` convention.
73
+ */
74
+ export function cnpgConnectInfo(cluster: any, poolers: any[] = []): CNPGConnectInfo {
75
+ const name: string = cluster?.metadata?.name ?? ''
76
+ const ns: string = cluster?.metadata?.namespace ?? ''
77
+ const spec = cluster?.spec ?? {}
78
+ const bootstrap = spec.bootstrap
79
+
80
+ const disabledRaw: unknown[] = spec.managed?.services?.disabledDefaultServices ?? []
81
+ const disabled = (['ro', 'r'] as const).filter((t) => disabledRaw.includes(t))
82
+ const endpoints: CNPGConnectEndpoint[] = [endpoint('rw', `${name}-rw`, ns, SELECTS.rw)]
83
+ for (const t of ['ro', 'r'] as const) if (!disabled.includes(t)) endpoints.push(endpoint(t, `${name}-${t}`, ns, SELECTS[t]))
84
+ for (const svc of spec.managed?.services?.additional ?? []) {
85
+ const svcName = svc?.serviceTemplate?.metadata?.name
86
+ if (!svcName) continue
87
+ const sel = svc.selectorType as 'rw' | 'ro' | 'r'
88
+ endpoints.push(endpoint('additional', svcName, ns, SELECTS[sel] ?? `selector ${sel ?? 'unknown'}`, svc.serviceTemplate))
89
+ }
90
+ for (const p of poolers) {
91
+ if (p?.metadata?.namespace !== ns || p?.spec?.cluster?.name !== name || !p?.metadata?.name) continue
92
+ const type: string = p.spec?.type || 'rw'
93
+ endpoints.push({
94
+ ...endpoint('pooler', p.metadata.name, ns, `PgBouncer in front of ${type === 'rw' ? 'the primary' : type === 'ro' ? 'the standbys' : type === 'r' ? 'any instance (may reach the primary; not read-only)' : `selector ${type}`}`, p.spec?.serviceTemplate),
95
+ poolerType: type,
96
+ })
97
+ }
98
+
99
+ const monolith = bootstrap?.initdb?.import?.type === 'monolith'
100
+ const database =
101
+ fromBootstrap(bootstrap, (s) => s?.database, 'database') ??
102
+ (monolith
103
+ ? { source: 'not set: a monolithic import creates no application database' }
104
+ : { value: 'app', source: "CloudNativePG's default" })
105
+ const owner =
106
+ fromBootstrap(bootstrap, (s) => s?.owner, 'owner') ??
107
+ (database.value ? { value: database.value, source: "CloudNativePG's default: the database's name" } : { source: 'not set' })
108
+ const secretFromSpec = fromBootstrap(bootstrap, (s) => s?.secret?.name, 'secret.name')
109
+ const secret = secretFromSpec?.value
110
+ ? { name: secretFromSpec.value, source: secretFromSpec.source, byConvention: false }
111
+ : { name: `${name}-app`, source: 'name by convention (<cluster>-app)', byConvention: true }
112
+
113
+ return {
114
+ endpoints,
115
+ database,
116
+ owner,
117
+ secret,
118
+ replicaCluster: cluster ? getCNPGClusterIsReplica(cluster) : false,
119
+ disabled,
120
+ }
121
+ }
122
+
123
+ /** A connection URI with the password left as a placeholder; never a real credential. */
124
+ export function cnpgConnectionURI(ep: CNPGConnectEndpoint, info: CNPGConnectInfo): string {
125
+ const user = encodeURIComponent(info.owner.value ?? '<user>')
126
+ const db = encodeURIComponent(info.database.value ?? '<database>')
127
+ return `postgresql://${user}:<password>@${ep.host}:${ep.port}/${db}`
128
+ }
129
+
130
+ function shellWord(v: string): string {
131
+ return /^[A-Za-z0-9_.@%+=:,/-]+$/.test(v) ? v : `'${v.replace(/'/g, `'\\''`)}'`
132
+ }
133
+
134
+ /** psql prompts for the password; nothing secret is put on the command line. */
135
+ export function cnpgPsqlCommand(ep: CNPGConnectEndpoint, info: CNPGConnectInfo): string {
136
+ return `psql -h ${ep.host} -p ${ep.port} -U ${shellWord(info.owner.value ?? '<user>')} -d ${shellWord(info.database.value ?? '<database>')}`
137
+ }
138
+
139
+ export function cnpgPortForwardCommand(ep: CNPGConnectEndpoint, namespace: string, localPort = CNPG_DEFAULT_PORT, kubeconfigContext?: string): string {
140
+ return `kubectl${kubeconfigContext ? ` --context ${shellWord(kubeconfigContext)}` : ''} -n ${shellWord(namespace)} port-forward ${shellWord(`service/${ep.name}`)} ${localPort}:${ep.port}`
141
+ }
142
+
143
+ export function cnpgEndpointAvailability(ep: CNPGConnectEndpoint, ha?: CNPGClusterHA, unreadReason?: string): { text: string; source?: string } {
144
+ if (ep.role === 'rw' && ha?.rwEndpoints.state === 'ok') {
145
+ return { text: ha.rwEndpoints.pods.length > 0 ? 'Ready endpoints' : 'Unavailable: no ready endpoint', source: 'Service EndpointSlices' }
146
+ }
147
+ if ((ep.role === 'ro' || ep.role === 'r') && ha?.pods.state === 'ok') {
148
+ const candidates = ep.role === 'ro' ? ha.instances.filter((i) => i.role === 'replica') : ha.instances
149
+ if (ep.role === 'ro' && ha.instances.some((i) => i.ready && i.role === 'unknown') && !candidates.some((i) => i.ready)) return { text: 'Not checked', source: 'Ready instance roles were not reported' }
150
+ return { text: candidates.some((i) => i.ready) ? 'Ready instance observed' : ep.role === 'ro' ? 'Unavailable: no ready standby' : 'Unavailable: no ready instance', source: 'Instance Pod readiness' }
151
+ }
152
+ const source = ep.role === 'rw' ? ha?.rwEndpoints : ep.role === 'ro' || ep.role === 'r' ? ha?.pods : undefined
153
+ const what = ep.role === 'rw' ? 'Service EndpointSlices' : 'instance Pods'
154
+ return { text: 'Not checked', source: ep.role === 'pooler' || ep.role === 'additional' ? 'This Service’s availability has not been read' : source ? cnpgHASourceText(source, what) : unreadReason ?? 'HA evidence has not been read' }
155
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { cnpgDatabaseRoleFacts, cnpgDatabaseRoleMeta } from './databaseRole'
3
+
4
+ const role = (spec: any, status: any = {}) => ({
5
+ apiVersion: 'postgresql.cnpg.io/v1',
6
+ kind: 'DatabaseRole',
7
+ metadata: { name: 'app-reader', namespace: 'db' },
8
+ spec: { cluster: { name: 'pg' }, name: 'reader', ...spec },
9
+ status,
10
+ })
11
+
12
+ describe('cnpgDatabaseRoleFacts', () => {
13
+ it('reports the Cluster’s managed.roles precedence as three-valued', () => {
14
+ const cluster = { spec: { managed: { roles: [{ name: 'reader' }] } } }
15
+ expect(cnpgDatabaseRoleFacts(role({}), cluster).overriddenByCluster).toBe(true)
16
+ expect(cnpgDatabaseRoleFacts(role({}), { spec: {} }).overriddenByCluster).toBe(false)
17
+ expect(cnpgDatabaseRoleFacts(role({}), null).overriddenByCluster).toBeNull()
18
+ })
19
+
20
+ it('reads applied three ways and the operator message', () => {
21
+ expect(cnpgDatabaseRoleFacts(role({}), null).state).toBe('pending')
22
+ const failed = cnpgDatabaseRoleFacts(role({}, { applied: false, message: 'database role is already managed by the CNPG cluster' }), null)
23
+ expect(failed.state).toBe('failed')
24
+ expect(failed.message).toContain('already managed')
25
+ })
26
+
27
+ it('treats an omitted login as false and names the client certificate Secret', () => {
28
+ const f = cnpgDatabaseRoleFacts(role({ clientCertificate: {}, validUntil: '2027-01-01T00:00:00Z' }, { clientCertificate: { expiration: '2026-12-01T00:00:00Z' } }), null)
29
+ expect(f.login).toBe(false)
30
+ expect(f.clientCertificate?.secret).toBe('app-reader-client-cert')
31
+ expect(cnpgDatabaseRoleMeta(f)).toBe('no login · password valid until 2027-01-01T00:00:00Z · client cert until 2026-12-01T00:00:00Z')
32
+ })
33
+ })
34
+
35
+ it('keeps a DatabaseRole pending until its current generation is observed', () => {
36
+ const r = { ...role({}, { applied: true, observedGeneration: 1 }), metadata: { name: 'reader', generation: 2 } }
37
+ expect(cnpgDatabaseRoleFacts(r, null).state).toBe('pending')
38
+ })
@@ -0,0 +1,80 @@
1
+ import { appliedFact } from './relations'
2
+
3
+ // CloudNativePG DatabaseRole (1.30+): one PostgreSQL role declared as its own
4
+ // object. The same role name in the Cluster's spec.managed.roles always wins:
5
+ // the operator does not reconcile the DatabaseRole and reports it not applied
6
+ // ("database role is already managed by the CNPG cluster").
7
+
8
+ export type CNPGRoleState = 'applied' | 'failed' | 'pending'
9
+
10
+ export interface CNPGDatabaseRoleFacts {
11
+ pgName: string
12
+ cluster?: string
13
+ state: CNPGRoleState
14
+ message?: string
15
+ /** Absent in the spec means false: the operator's field is omitempty. */
16
+ login: boolean
17
+ superuser: boolean
18
+ /** spec.validUntil: when the role's password stops being accepted (PostgreSQL VALID UNTIL). */
19
+ passwordValidUntil?: string
20
+ passwordSecret?: string
21
+ passwordDisabled: boolean
22
+ clientCertificate?: { enabled: boolean; secret: string; expiration?: string; message?: string }
23
+ reclaimPolicy: 'delete' | 'retain'
24
+ /**
25
+ * true when the target Cluster declares the same role in spec.managed.roles,
26
+ * false when it does not, null when the Cluster is not visible.
27
+ */
28
+ overriddenByCluster: boolean | null
29
+ }
30
+
31
+ export function cnpgRoleState(obj: any): CNPGRoleState {
32
+ const fact = appliedFact(obj)
33
+ if (fact.tone === 'healthy') return 'applied'
34
+ if (fact.tone === 'unhealthy') return 'failed'
35
+ return 'pending'
36
+ }
37
+
38
+ export function cnpgDatabaseRoleFacts(role: any, cluster: any | null | undefined): CNPGDatabaseRoleFacts {
39
+ const spec = role?.spec ?? {}
40
+ const status = role?.status ?? {}
41
+ const pgName: string = spec.name ?? role?.metadata?.name ?? ''
42
+ let overriddenByCluster: boolean | null = null
43
+ if (cluster) {
44
+ const roles: any[] = Array.isArray(cluster?.spec?.managed?.roles) ? cluster.spec.managed.roles : []
45
+ overriddenByCluster = roles.some((r) => r?.name === pgName)
46
+ }
47
+ const cc = spec.clientCertificate
48
+ const ccEnabled = !!cc && cc.enabled !== false
49
+ return {
50
+ pgName,
51
+ cluster: spec.cluster?.name,
52
+ state: cnpgRoleState(role),
53
+ message: status.message || undefined,
54
+ login: spec.login === true,
55
+ superuser: spec.superuser === true,
56
+ passwordValidUntil: spec.validUntil || undefined,
57
+ passwordSecret: spec.passwordSecret?.name || undefined,
58
+ passwordDisabled: spec.disablePassword === true,
59
+ clientCertificate: ccEnabled
60
+ ? {
61
+ enabled: true,
62
+ secret: `${role?.metadata?.name}-client-cert`,
63
+ expiration: status.clientCertificate?.expiration || undefined,
64
+ message: status.clientCertificate?.message || undefined,
65
+ }
66
+ : undefined,
67
+ reclaimPolicy: spec.databaseRoleReclaimPolicy === 'delete' ? 'delete' : 'retain',
68
+ overriddenByCluster,
69
+ }
70
+ }
71
+
72
+ /** Short, factual meta line for a list row. */
73
+ export function cnpgDatabaseRoleMeta(f: CNPGDatabaseRoleFacts): string {
74
+ const parts: string[] = []
75
+ if (f.overriddenByCluster) parts.push('overridden by spec.managed.roles')
76
+ parts.push(f.login ? 'login' : 'no login')
77
+ if (f.passwordValidUntil) parts.push(`password valid until ${f.passwordValidUntil}`)
78
+ if (f.clientCertificate) parts.push(f.clientCertificate.expiration ? `client cert until ${f.clientCertificate.expiration}` : 'client cert expiry not reported')
79
+ return parts.join(' · ')
80
+ }