@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,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
|
+
}
|