@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,90 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { CNPGClusterHASection } from './CNPGClusterHASection'
|
|
5
|
+
import type { CNPGClusterHA } from './ha'
|
|
6
|
+
|
|
7
|
+
const noInstances: CNPGClusterHA = {
|
|
8
|
+
cluster: { namespace: 'db', name: 'pg', uid: 'u' },
|
|
9
|
+
sampledAt: '2026-09-30T12:00:00Z',
|
|
10
|
+
desiredImage: 'pg:17',
|
|
11
|
+
instances: [],
|
|
12
|
+
pods: { state: 'ok' },
|
|
13
|
+
nodes: { state: 'ok' },
|
|
14
|
+
quorum: { enabled: false, object: { state: 'notFound' } },
|
|
15
|
+
pdbs: { state: 'ok', enabled: true, items: [] },
|
|
16
|
+
primaryLease: { state: 'notFound' },
|
|
17
|
+
operatorLease: { state: 'unavailable' },
|
|
18
|
+
jobs: { state: 'ok', items: [] },
|
|
19
|
+
rwEndpoints: { state: 'ok', service: 'pg-rw', pods: [] },
|
|
20
|
+
certificates: [],
|
|
21
|
+
maintenance: { declared: false, inProgress: false, reusePVC: true },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('CNPGClusterHASection', () => {
|
|
25
|
+
it('says there is nothing to place rather than showing only the zone source', () => {
|
|
26
|
+
const html = renderToStaticMarkup(<CNPGClusterHASection ha={noInstances} showCertificates={false} />)
|
|
27
|
+
expect(html).toContain('No instance Pods to place')
|
|
28
|
+
expect(html).not.toContain('topology.kubernetes.io/zone of each instance')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('shows the read-write Service, its Pods and primary mismatch beside Reachability', () => {
|
|
33
|
+
const ha: CNPGClusterHA = { ...noInstances, rwEndpoints: { state: 'ok', service: 'pg-rw', pods: ['pg-2'] } }
|
|
34
|
+
const html = renderToStaticMarkup(<CNPGClusterHASection ha={ha} currentPrimary="pg-1" onOpenReachability={() => {}} />)
|
|
35
|
+
expect(html).toContain('Read-write Service')
|
|
36
|
+
expect(html).toContain('pg-rw')
|
|
37
|
+
expect(html).toContain('pg-2')
|
|
38
|
+
expect(html).toContain('not on the reported primary pg-1')
|
|
39
|
+
expect(html).toContain('Reachability')
|
|
40
|
+
expect(html).toContain('aria-expanded="true"')
|
|
41
|
+
})
|
|
42
|
+
it('distinguishes absent endpoints from denied endpoint reads', () => {
|
|
43
|
+
expect(renderToStaticMarkup(<CNPGClusterHASection ha={noInstances} />)).toContain('No ready endpoints')
|
|
44
|
+
const denied: CNPGClusterHA = { ...noInstances, rwEndpoints: { state: 'denied', service: 'pg-rw', pods: [], grant: { verb: 'list', resource: 'endpointslices', group: 'discovery.k8s.io', namespace: 'db' } } }
|
|
45
|
+
const html = renderToStaticMarkup(<CNPGClusterHASection ha={denied} />)
|
|
46
|
+
expect(html).toContain('endpointslices')
|
|
47
|
+
expect(html).not.toContain('No ready endpoints')
|
|
48
|
+
})
|
|
49
|
+
it('does not flag deliberately absent read-write endpoints while hibernated', () => {
|
|
50
|
+
const html = renderToStaticMarkup(<CNPGClusterHASection ha={noInstances} currentPrimary="pg-1" hibernated />)
|
|
51
|
+
expect(html).toContain('None expected while hibernated')
|
|
52
|
+
expect(html).not.toContain('No ready endpoints')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('shows the desired image without claiming any instance runs it', () => {
|
|
56
|
+
const html = renderToStaticMarkup(<CNPGClusterHASection ha={noInstances} />)
|
|
57
|
+
expect(html).toContain('Desired image pg:17; no instance running')
|
|
58
|
+
expect(html).not.toContain('every instance runs it')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('describes matching Pod images without claiming the Pods are running', () => {
|
|
62
|
+
const ha: CNPGClusterHA = { ...noInstances, instances: [{ pod: 'pg-1', podUID: 'p1', role: 'primary', ready: false, restartCount: 0, image: 'pg:17', imageMatches: true }] }
|
|
63
|
+
const html = renderToStaticMarkup(<CNPGClusterHASection ha={ha} />)
|
|
64
|
+
expect(html).toContain('observed Pod images match')
|
|
65
|
+
expect(html).not.toContain('instances run it')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('keeps the readiness statement inside expanded HA and lists missing expected instances without an observed role', () => {
|
|
69
|
+
const ha: CNPGClusterHA = { ...noInstances, declaredInstances: 2, expectedInstances: ['orders-1', 'orders-2'], instances: [{ pod: 'orders-1', podUID: 'a', role: 'primary', ready: true, restartCount: 0 }], jobs: { state: 'ok', items: [{ name: 'orders-2-join', role: 'join', phase: 'pending', reason: 'Pod cannot be scheduled: Unschedulable: 0/2 nodes are available: 2 Too many pods. preemption: no victims.' }] } }
|
|
70
|
+
const html = renderToStaticMarkup(<CNPGClusterHASection ha={ha} showInstances={false} />)
|
|
71
|
+
expect(html).toContain('1 of 2 declared instances ready; no instance Pod observed for orders-2')
|
|
72
|
+
expect(html).toContain('orders-1')
|
|
73
|
+
expect(html).toContain('orders-2</span> · not running')
|
|
74
|
+
expect(html).toContain('Cannot be scheduled: both nodes have reached their Pod limit')
|
|
75
|
+
expect(html).toContain('Scheduler message')
|
|
76
|
+
expect(html).toContain('aria-expanded="false"')
|
|
77
|
+
expect(html).toContain('preemption: no victims')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('keeps each Job reason and scheduler disclosure inside its own block', () => {
|
|
81
|
+
const jobs = ['pg-1-initdb', 'pg-2-join'].map((name) => ({ name, role: 'join', phase: 'pending' as const, reason: `Pod cannot be scheduled: ${name}: 0/2 nodes are available: 2 Too many pods.` }))
|
|
82
|
+
const host = document.createElement('div'); host.innerHTML = renderToStaticMarkup(<CNPGClusterHASection ha={{ ...noInstances, jobs: { state: 'ok', items: jobs } }} />)
|
|
83
|
+
const blocks = [...host.querySelectorAll('div.space-y-1.text-xs')]
|
|
84
|
+
expect(blocks).toHaveLength(2)
|
|
85
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
86
|
+
expect(blocks[i].textContent).toContain(jobs[i].name)
|
|
87
|
+
expect(blocks[i].querySelector('[inert]')?.textContent).toContain(jobs[i].reason)
|
|
88
|
+
expect(blocks[i].textContent).not.toContain(jobs[1 - i].name)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import { Badge } from '../ui/Badge'
|
|
3
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
4
|
+
import { formatAge, summarizeSchedulerMessage } from '../resources/resource-utils'
|
|
5
|
+
import { PrimaryConflictNote } from './primitives'
|
|
6
|
+
import {
|
|
7
|
+
CNPG_ROLE_DETAIL_TEXT,
|
|
8
|
+
cnpgCertificateViews,
|
|
9
|
+
cnpgCertificatesSummary,
|
|
10
|
+
cnpgHASourceText,
|
|
11
|
+
cnpgHASummary,
|
|
12
|
+
cnpgImageDrift,
|
|
13
|
+
cnpgLeaseHolderPod,
|
|
14
|
+
cnpgLiveGap,
|
|
15
|
+
cnpgPDBFact,
|
|
16
|
+
cnpgPendingRestart,
|
|
17
|
+
cnpgQuorumFact,
|
|
18
|
+
cnpgZoneSpread,
|
|
19
|
+
type CNPGClusterHA,
|
|
20
|
+
type CNPGHAJob,
|
|
21
|
+
type CNPGHALease,
|
|
22
|
+
type CNPGInstanceLive,
|
|
23
|
+
} from './ha'
|
|
24
|
+
import { type NavigateToRef, RefLink } from '../ui/RefLink'
|
|
25
|
+
import { StatusDot, toneTextClass } from '../ui/status-tone'
|
|
26
|
+
import { FactGrid, FactRow, FactValue } from '../facts'
|
|
27
|
+
import { FoldSection, SectionHeading } from '../ui/FoldSection'
|
|
28
|
+
|
|
29
|
+
function Unknown({ text }: { text: string }) {
|
|
30
|
+
return <span className="text-theme-text-tertiary">{text}</span>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function LeaseValue({ lease, what }: { lease: CNPGHALease; what: string }) {
|
|
34
|
+
if (lease.state !== 'ok') return <Unknown text={cnpgHASourceText(lease, what)} />
|
|
35
|
+
return (
|
|
36
|
+
<span>
|
|
37
|
+
held by{' '}
|
|
38
|
+
{lease.holder && cnpgLeaseHolderPod(lease.holder) !== lease.holder ? (
|
|
39
|
+
<Tooltip content={lease.holder}>
|
|
40
|
+
<span className="font-mono text-xs break-all">{cnpgLeaseHolderPod(lease.holder)}</span>
|
|
41
|
+
</Tooltip>
|
|
42
|
+
) : (
|
|
43
|
+
<span className="font-mono text-xs break-all">{lease.holder || '(nobody)'}</span>
|
|
44
|
+
)}
|
|
45
|
+
{lease.renewTime && <span className="text-theme-text-secondary"> · renewed {formatAge(lease.renewTime)} ago</span>}
|
|
46
|
+
{lease.expired && <span className={toneTextClass('degraded')}> · expired</span>}
|
|
47
|
+
{lease.controlledByCluster === false && <span className={toneTextClass('degraded')}> · not owned by this Cluster</span>}
|
|
48
|
+
</span>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const JOB_SEVERITY: Record<CNPGHAJob['phase'], 'success' | 'error' | 'info' | 'neutral'> = {
|
|
53
|
+
succeeded: 'success',
|
|
54
|
+
failed: 'error',
|
|
55
|
+
running: 'info',
|
|
56
|
+
active: 'info',
|
|
57
|
+
pending: 'neutral',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* "HA and instances": whether the cluster survives losing an instance, and what
|
|
62
|
+
* a planned switchover will meet. Every row names its source; a fact the caller
|
|
63
|
+
* cannot read says so instead of reading as none.
|
|
64
|
+
*/
|
|
65
|
+
export function CNPGClusterHASection({
|
|
66
|
+
ha,
|
|
67
|
+
live,
|
|
68
|
+
liveUnavailable,
|
|
69
|
+
loading,
|
|
70
|
+
error,
|
|
71
|
+
onNavigate,
|
|
72
|
+
primaryConflict,
|
|
73
|
+
title = 'HA and instances',
|
|
74
|
+
showInstances = true,
|
|
75
|
+
showCertificates = true,
|
|
76
|
+
currentPrimary,
|
|
77
|
+
hibernated = false,
|
|
78
|
+
onOpenReachability,
|
|
79
|
+
}: {
|
|
80
|
+
currentPrimary?: string
|
|
81
|
+
hibernated?: boolean
|
|
82
|
+
onOpenReachability?: (service: { namespace: string; name: string }) => void
|
|
83
|
+
title?: string
|
|
84
|
+
/** False where the host lists the instances itself (with their replication state). */
|
|
85
|
+
showInstances?: boolean
|
|
86
|
+
/** False where the host shows certificates elsewhere (CNPGClusterCertificates). */
|
|
87
|
+
showCertificates?: boolean
|
|
88
|
+
/** status.currentPrimary vs the Pod labelled primary, when they disagree. */
|
|
89
|
+
primaryConflict?: { status: string; labelled: string }
|
|
90
|
+
ha?: CNPGClusterHA
|
|
91
|
+
/** Instance-manager facts, when the caller can read them. */
|
|
92
|
+
live?: CNPGInstanceLive[]
|
|
93
|
+
/** Why `live` is absent (e.g. "needs get pods/proxy in db"); read from `live` itself when it is present. */
|
|
94
|
+
liveUnavailable?: string
|
|
95
|
+
loading?: boolean
|
|
96
|
+
error?: string
|
|
97
|
+
onNavigate?: NavigateToRef
|
|
98
|
+
}) {
|
|
99
|
+
if (!ha) {
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<SectionHeading>{title}</SectionHeading>
|
|
103
|
+
<div className="text-sm text-theme-text-tertiary">{loading ? 'Reading HA facts…' : `HA facts could not be read${error ? `: ${error}` : ''}`}</div>
|
|
104
|
+
</>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
const ns = ha.cluster.namespace
|
|
108
|
+
const spread = cnpgZoneSpread(ha)
|
|
109
|
+
const drift = cnpgImageDrift(ha)
|
|
110
|
+
const pending = cnpgPendingRestart(live)
|
|
111
|
+
const liveBy = new Map((live ?? []).map((l) => [l.pod, l]))
|
|
112
|
+
const jobs = [...ha.jobs.items].sort((a, b) => (a.phase === 'succeeded' ? 1 : 0) - (b.phase === 'succeeded' ? 1 : 0))
|
|
113
|
+
const versions = new Set((live ?? []).map((l) => l.instanceManagerVersion).filter(Boolean))
|
|
114
|
+
|
|
115
|
+
const haSummary = cnpgHASummary(ha, live, primaryConflict)
|
|
116
|
+
const endpointProblem = !hibernated && currentPrimary && ha.rwEndpoints.state === 'ok' && !ha.rwEndpoints.pods.includes(currentPrimary)
|
|
117
|
+
? ha.rwEndpoints.pods.length === 0 ? 'no read-write endpoint' : 'read-write endpoint not on the primary'
|
|
118
|
+
: undefined
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
<FoldSection title={title} hint={`checked ${formatAge(ha.sampledAt)} ago`} summary={[endpointProblem, haSummary.text].filter(Boolean).join(' · ')} attention={haSummary.attention || !!endpointProblem}>
|
|
123
|
+
{<div className="mb-3 text-sm text-theme-text-secondary">{haSummary.text}</div>}
|
|
124
|
+
{ha.pods.state === 'ok' && <div className="mb-3 flex flex-wrap gap-x-4 gap-y-1 text-xs">
|
|
125
|
+
{ha.instances.map((i) => <span key={i.pod}><RefLink refTo={{ kind: 'Pod', group: '', namespace: ns, name: i.pod }} onNavigate={onNavigate} mono /> · Pod {i.ready ? 'ready' : 'not ready'}</span>)}
|
|
126
|
+
{[...new Set(ha.expectedInstances ?? [])].filter((name) => !ha.instances.some((i) => i.pod === name)).map((name) => <span key={name} className="text-theme-text-tertiary"><span className="font-mono">{name}</span> · not running</span>)}
|
|
127
|
+
</div>}
|
|
128
|
+
<FactGrid>
|
|
129
|
+
<FactRow label="Read-write Service">
|
|
130
|
+
<div className="space-y-1">
|
|
131
|
+
<RefLink refTo={{ kind: 'Service', group: '', namespace: ns, name: ha.rwEndpoints.service }} onNavigate={onNavigate} mono />
|
|
132
|
+
{ha.rwEndpoints.state !== 'ok' ? <Unknown text={cnpgHASourceText(ha.rwEndpoints, 'read-write endpoints')} /> : (
|
|
133
|
+
<>
|
|
134
|
+
<div className="flex flex-wrap gap-2">
|
|
135
|
+
{ha.rwEndpoints.pods.length === 0 ? <span className={hibernated ? 'text-theme-text-secondary' : toneTextClass('unhealthy')}>{hibernated ? 'None expected while hibernated' : 'No ready endpoints'}</span> : ha.rwEndpoints.pods.map((pod) => <RefLink key={pod} refTo={{ kind: 'Pod', group: '', namespace: ns, name: pod }} onNavigate={onNavigate} mono />)}
|
|
136
|
+
</div>
|
|
137
|
+
{!hibernated && currentPrimary && ha.rwEndpoints.pods.length > 0 && !ha.rwEndpoints.pods.includes(currentPrimary) && <div className={clsx('text-xs', toneTextClass('unhealthy'))}>Read-write endpoint is not on the reported primary {currentPrimary}.</div>}
|
|
138
|
+
</>
|
|
139
|
+
)}
|
|
140
|
+
{onOpenReachability && <button type="button" className="text-xs text-accent-text hover:underline" onClick={() => onOpenReachability({ namespace: ns, name: ha.rwEndpoints.service })}>Reachability →</button>}
|
|
141
|
+
<div className="text-[11.5px] text-theme-text-tertiary">Ready Pods from the Service’s EndpointSlices</div>
|
|
142
|
+
</div>
|
|
143
|
+
</FactRow>
|
|
144
|
+
<FactRow label="Failure domains">
|
|
145
|
+
{!spread.known ? (
|
|
146
|
+
<div>
|
|
147
|
+
<Unknown text={ha.pods.state !== 'ok' ? cnpgHASourceText(ha.pods, 'instance Pods') : cnpgHASourceText(ha.nodes, 'Nodes (zones)')} />
|
|
148
|
+
{spread.nodes.length > 0 && (
|
|
149
|
+
<div className="text-xs text-theme-text-secondary">
|
|
150
|
+
Nodes: {spread.nodes.map((n) => `${n.node} (${n.pods.join(', ')})`).join(' · ')}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
) : spread.zones.length === 0 && spread.unlabelled.length === 0 ? (
|
|
155
|
+
<Unknown text="No instance Pods to place" />
|
|
156
|
+
) : (
|
|
157
|
+
<div>
|
|
158
|
+
<div className="flex flex-wrap gap-x-3">
|
|
159
|
+
{spread.zones.map((z) => (
|
|
160
|
+
<span key={z.zone}>
|
|
161
|
+
<span className="font-mono">{z.zone}</span>
|
|
162
|
+
<span className="text-theme-text-secondary">: {z.pods.join(', ')}</span>
|
|
163
|
+
</span>
|
|
164
|
+
))}
|
|
165
|
+
{spread.unlabelled.length > 0 && <Unknown text={`no zone label: ${spread.unlabelled.join(', ')}`} />}
|
|
166
|
+
</div>
|
|
167
|
+
{(spread.singleZone || spread.sharedNode) && (
|
|
168
|
+
<div className={clsx('text-xs', toneTextClass('degraded'))}>
|
|
169
|
+
{spread.singleZone ? 'Every instance is in one zone: losing it loses the cluster. ' : ''}
|
|
170
|
+
{spread.sharedNode ? 'Two or more instances share a Node.' : ''}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
<div className="text-[11.5px] text-theme-text-tertiary">topology.kubernetes.io/zone of each instance’s Node</div>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</FactRow>
|
|
177
|
+
|
|
178
|
+
{showInstances && <FactRow label="Instances">
|
|
179
|
+
{ha.pods.state !== 'ok' ? (
|
|
180
|
+
<Unknown text={cnpgHASourceText(ha.pods, 'instance Pods')} />
|
|
181
|
+
) : (
|
|
182
|
+
<div className="space-y-1">
|
|
183
|
+
{ha.instances.map((i) => {
|
|
184
|
+
const l = liveBy.get(i.pod)
|
|
185
|
+
return (
|
|
186
|
+
<div key={i.pod} className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs">
|
|
187
|
+
<Tooltip content={`Pod ${i.ready ? 'ready' : 'not ready'}${l?.roleDetail === 'replayPaused' || l?.roleDetail === 'fileBased' ? ` · ${CNPG_ROLE_DETAIL_TEXT[l.roleDetail]}` : ''}`}>
|
|
188
|
+
<StatusDot tone={!i.ready ? 'unhealthy' : l?.roleDetail === 'replayPaused' || l?.roleDetail === 'fileBased' ? 'degraded' : 'healthy'} />
|
|
189
|
+
</Tooltip>
|
|
190
|
+
<RefLink refTo={{ kind: 'Pod', group: '', namespace: ns, name: i.pod }} onNavigate={onNavigate} mono />
|
|
191
|
+
<span className="text-theme-text-secondary">
|
|
192
|
+
{l?.roleDetail ? CNPG_ROLE_DETAIL_TEXT[l.roleDetail] : i.role === 'unknown' ? 'role unknown' : i.role}
|
|
193
|
+
</span>
|
|
194
|
+
{l?.timeline !== undefined && <span className="text-theme-text-tertiary">timeline {l.timeline}</span>}
|
|
195
|
+
{i.qosClass && <span className="text-theme-text-tertiary">QoS {i.qosClass}</span>}
|
|
196
|
+
{l?.pendingRestart && <Badge severity="warning" size="sm">pending restart</Badge>}
|
|
197
|
+
{i.imageMatches === false && <Badge severity="warning" size="sm">image differs</Badge>}
|
|
198
|
+
{l?.instanceManagerVersion && versions.size > 1 && <span className="text-theme-text-tertiary">CNPG instance manager {l.instanceManagerVersion}</span>}
|
|
199
|
+
</div>
|
|
200
|
+
)
|
|
201
|
+
})}
|
|
202
|
+
{primaryConflict && <PrimaryConflictNote conflict={primaryConflict} />}
|
|
203
|
+
<div className="text-[11.5px] text-theme-text-tertiary">
|
|
204
|
+
{live ? 'Role detail and pending restart from each instance manager' : `Role from Pod labels; role detail ${cnpgLiveGap(undefined, liveUnavailable)}`}
|
|
205
|
+
{versions.size === 1 ? ` · instance manager ${[...versions][0]}` : ''}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</FactRow>}
|
|
210
|
+
|
|
211
|
+
<FactRow label="Pending restart">
|
|
212
|
+
{!pending.known && pending.pods.length === 0 ? (
|
|
213
|
+
<Unknown text={`Unknown: ${cnpgLiveGap(live, liveUnavailable)}`} />
|
|
214
|
+
) : pending.pods.length === 0 ? (
|
|
215
|
+
<span className="text-theme-text-secondary">None reported</span>
|
|
216
|
+
) : (
|
|
217
|
+
<span className={toneTextClass('degraded')}>
|
|
218
|
+
{pending.pods.join(', ')} {pending.pods.length === 1 ? 'needs' : 'need'} a restart to apply changed parameters
|
|
219
|
+
{pending.forDecrease ? ' (a lowered setting: the primary restarts first)' : ''}
|
|
220
|
+
{!pending.known ? ` · ${cnpgLiveGap(live, liveUnavailable)}` : ''}
|
|
221
|
+
</span>
|
|
222
|
+
)}
|
|
223
|
+
</FactRow>
|
|
224
|
+
|
|
225
|
+
<FactRow label="Image">
|
|
226
|
+
{!drift.known && drift.drifted.length === 0 ? (
|
|
227
|
+
<Unknown text={!ha.desiredImage ? 'Desired image not reported' : ha.pods.state === 'ok' && ha.instances.length === 0 ? `Desired image ${ha.desiredImage}; no instance running` : `Desired image ${ha.desiredImage}; running images unknown`} />
|
|
228
|
+
) : drift.drifted.length === 0 ? (
|
|
229
|
+
<span>
|
|
230
|
+
<span className="font-mono text-xs break-all">{ha.desiredImage}</span>
|
|
231
|
+
<span className="text-theme-text-secondary"> · observed Pod images match</span>
|
|
232
|
+
</span>
|
|
233
|
+
) : (
|
|
234
|
+
<span className={toneTextClass('degraded')}>
|
|
235
|
+
{drift.drifted.map((d) => `${d.pod} has image ${d.image}`).join(' · ')} · desired <span className="font-mono">{ha.desiredImage}</span>
|
|
236
|
+
</span>
|
|
237
|
+
)}
|
|
238
|
+
</FactRow>
|
|
239
|
+
|
|
240
|
+
<FactRow label="Failover quorum">
|
|
241
|
+
<FactValue fact={cnpgQuorumFact(ha.quorum)} />
|
|
242
|
+
</FactRow>
|
|
243
|
+
|
|
244
|
+
<FactRow label="Disruption budgets">
|
|
245
|
+
<FactValue fact={cnpgPDBFact(ha.pdbs)} />
|
|
246
|
+
</FactRow>
|
|
247
|
+
|
|
248
|
+
<FactRow label="Primary lease">
|
|
249
|
+
<LeaseValue lease={ha.primaryLease} what="the primary Lease" />
|
|
250
|
+
</FactRow>
|
|
251
|
+
|
|
252
|
+
<FactRow label="Operator leader">
|
|
253
|
+
<LeaseValue lease={ha.operatorLease} what="the operator’s Lease" />
|
|
254
|
+
</FactRow>
|
|
255
|
+
|
|
256
|
+
<FactRow label="Cluster Jobs">
|
|
257
|
+
{ha.jobs.state !== 'ok' ? (
|
|
258
|
+
<Unknown text={cnpgHASourceText(ha.jobs, 'Jobs')} />
|
|
259
|
+
) : jobs.length === 0 ? (
|
|
260
|
+
<span className="text-theme-text-secondary">None present</span>
|
|
261
|
+
) : (
|
|
262
|
+
<div className="space-y-3">
|
|
263
|
+
{jobs.slice(0, 6).map((j) => (
|
|
264
|
+
<div key={j.name} className="space-y-1 text-xs">
|
|
265
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
266
|
+
<Badge severity={JOB_SEVERITY[j.phase]} size="sm">{j.phase}</Badge>
|
|
267
|
+
<span className="text-theme-text-secondary">{j.role ?? 'job'}</span>
|
|
268
|
+
<RefLink refTo={{ kind: 'Job', group: 'batch', namespace: ns, name: j.name }} onNavigate={onNavigate} mono />
|
|
269
|
+
</div>
|
|
270
|
+
{j.reason && (/schedul|Too many pods|Insufficient/i.test(j.reason) ? <div className="w-full">
|
|
271
|
+
<span className={toneTextClass('degraded')}>Cannot be scheduled: {summarizeSchedulerMessage(j.reason, { plain: true })}</span>
|
|
272
|
+
<div className="mt-1"><FoldSection title="Scheduler message" summary="" attention={false}><div className="break-words text-xs text-theme-text-secondary">{j.reason}</div></FoldSection></div>
|
|
273
|
+
</div> : <span className={toneTextClass('degraded')}>{j.reason}</span>)}
|
|
274
|
+
</div>
|
|
275
|
+
))}
|
|
276
|
+
{jobs.length > 6 && <div className="text-xs text-theme-text-tertiary">+{jobs.length - 6} more</div>}
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</FactRow>
|
|
280
|
+
</FactGrid>
|
|
281
|
+
</FoldSection>
|
|
282
|
+
|
|
283
|
+
{showCertificates && <CNPGClusterCertificates ha={ha} onNavigate={onNavigate} />}
|
|
284
|
+
</>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Certificate expiry and who renews each certificate, folded to one line unless one needs attention. */
|
|
289
|
+
export function CNPGClusterCertificates({ ha, onNavigate }: { ha: CNPGClusterHA; onNavigate?: NavigateToRef }) {
|
|
290
|
+
const ns = ha.cluster.namespace
|
|
291
|
+
const certs = cnpgCertificateViews(ha.certificates)
|
|
292
|
+
const certSummary = cnpgCertificatesSummary(ha.certificates)
|
|
293
|
+
return (
|
|
294
|
+
<FoldSection title="Certificates" summary={certSummary.text} attention={certSummary.attention}>
|
|
295
|
+
<FactGrid>
|
|
296
|
+
<FactRow label="Expiry">
|
|
297
|
+
{certs.length === 0 ? (
|
|
298
|
+
<Unknown text="No expiry reported by the operator" />
|
|
299
|
+
) : (
|
|
300
|
+
<div className="space-y-0.5">
|
|
301
|
+
{certs.map((c) => (
|
|
302
|
+
<div key={c.secret} className="text-xs">
|
|
303
|
+
<RefLink refTo={{ kind: 'Secret', group: '', namespace: ns, name: c.secret }} onNavigate={onNavigate} mono />{' '}
|
|
304
|
+
<span className={toneTextClass(c.tone)}>
|
|
305
|
+
{c.expiresAt ? (c.daysLeft !== undefined && c.daysLeft < 0 ? `expired ${c.expiresAt}` : `expires in ${c.daysLeft} d`) : `expiry unreadable (“${c.raw}”)`}
|
|
306
|
+
</span>
|
|
307
|
+
<span className="text-theme-text-secondary">
|
|
308
|
+
{' · '}
|
|
309
|
+
{c.renewal === 'operator' ? 'CloudNativePG renews it' : 'you renew it (spec.certificates)'}
|
|
310
|
+
</span>
|
|
311
|
+
{c.renewal === 'user' &&
|
|
312
|
+
(c.certManager ? (
|
|
313
|
+
<span className="text-theme-text-secondary">
|
|
314
|
+
{' · cert-manager '}
|
|
315
|
+
<RefLink refTo={{ kind: 'Certificate', group: 'cert-manager.io', namespace: ns, name: c.certManager.certificate }} onNavigate={onNavigate} mono />
|
|
316
|
+
</span>
|
|
317
|
+
) : c.metadata?.state === 'ok' ? (
|
|
318
|
+
<span className="text-theme-text-tertiary"> · not issued by cert-manager</span>
|
|
319
|
+
) : (
|
|
320
|
+
<span className="text-theme-text-tertiary"> · issuer unknown ({cnpgHASourceText(c.metadata, 'Secret metadata')})</span>
|
|
321
|
+
))}
|
|
322
|
+
</div>
|
|
323
|
+
))}
|
|
324
|
+
<div className="text-[11.5px] text-theme-text-tertiary">status.certificates.expirations</div>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
</FactRow>
|
|
328
|
+
</FactGrid>
|
|
329
|
+
</FoldSection>
|
|
330
|
+
)
|
|
331
|
+
}
|