@skyhook-io/k8s-ui 1.17.0 → 1.18.0
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 +6 -6
- 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 +7 -3
- 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 +142 -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 +40 -81
- package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
- package/src/components/gitops/insights/insights-helpers.ts +7 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
- 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 +23 -0
- package/src/components/gitops/tree/tree-helpers.ts +12 -2
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +44 -6
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +8 -2
- package/src/components/issues/diagnostic.ts +2 -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 +75 -1
- package/src/components/issues/severity.ts +30 -2
- package/src/components/issues/types.ts +55 -4
- package/src/components/logs/LogCore.theme.test.tsx +206 -3
- package/src/components/logs/LogCore.tsx +180 -53
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +369 -0
- package/src/components/logs/LogsViewer.tsx +91 -13
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +23 -31
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
- package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogBuffer.ts +48 -18
- package/src/components/logs/useLogSearch.ts +3 -1
- 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/ColumnPickerList.test.tsx +46 -0
- package/src/components/resources/ColumnPickerList.tsx +174 -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 +296 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +392 -181
- package/src/components/resources/column-order.test.ts +104 -0
- package/src/components/resources/column-order.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
- 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/CronJobRenderer.test.tsx +31 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
- 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.sidecar-terminal.test.tsx +200 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -40
- 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 +96 -7
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +31 -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/DrainPlanDialog.test.tsx +7 -0
- package/src/components/shared/DrainPlanDialog.tsx +6 -2
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +14 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/DiffViewer.test.tsx +69 -0
- package/src/components/timeline/DiffViewer.tsx +113 -4
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/shared.test.tsx +48 -0
- package/src/components/timeline/shared.tsx +14 -19
- 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/Toast.tsx +15 -4
- 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 +4 -2
- 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 +193 -71
- 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 +53 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +20 -2
- package/src/types/gitops-tree.ts +12 -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
package/src/types/core.ts
CHANGED
|
@@ -126,6 +126,7 @@ export interface Capabilities {
|
|
|
126
126
|
helmWrite: boolean // Helm write operations (install, upgrade, rollback, uninstall, apply values)
|
|
127
127
|
nodeWrite: boolean // Node write operations (cordon, uncordon, drain)
|
|
128
128
|
workloadWrites?: WorkloadWritePermissions // Workload patch permissions (restart/scale controls)
|
|
129
|
+
mcpReadOnly?: boolean // Full MCP endpoint is disabled; read-only endpoints remain available
|
|
129
130
|
mcpEnabled: boolean // MCP server is running
|
|
130
131
|
// Karpenter discovery and NodePool read state. Optional on the wire for the
|
|
131
132
|
// same newer-frontend/older-backend reason as `deployment` below — consumers
|
|
@@ -160,16 +161,20 @@ export interface CloudConnectCapability {
|
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
export interface FeatureCapabilities {
|
|
164
|
+
helmSourceIdentity?: boolean
|
|
163
165
|
yamlReview?: boolean
|
|
164
166
|
yamlSchemas?: boolean
|
|
165
167
|
workloadImages?: boolean
|
|
166
168
|
resourceIssues?: boolean
|
|
169
|
+
resourceIssueCoverage?: boolean
|
|
167
170
|
podEnvironment?: boolean
|
|
168
171
|
policyResource?: boolean
|
|
169
172
|
workloadHistory?: boolean
|
|
173
|
+
cnpgWorkspace?: boolean
|
|
174
|
+
cnpgProtectionSetup?: boolean
|
|
175
|
+
gitopsWriteEvidence?: boolean
|
|
170
176
|
serviceEndpointSlices?: boolean
|
|
171
177
|
trafficRecords?: boolean
|
|
172
|
-
gitopsWriteEvidence?: boolean
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
// DeploymentMode is the closed set of topologies Radar can run in.
|
|
@@ -201,6 +206,7 @@ export type CoreNodeKind =
|
|
|
201
206
|
| 'HTTPRoute'
|
|
202
207
|
| 'GRPCRoute'
|
|
203
208
|
| 'TCPRoute'
|
|
209
|
+
| 'UDPRoute'
|
|
204
210
|
| 'TLSRoute'
|
|
205
211
|
| 'Service'
|
|
206
212
|
| 'Deployment'
|
|
@@ -639,6 +645,8 @@ export interface ResourceRef {
|
|
|
639
645
|
|
|
640
646
|
// Computed relationships for a resource
|
|
641
647
|
export interface Relationships {
|
|
648
|
+
/** Istio registry domain supplied by the producer; omitted when unknown. */
|
|
649
|
+
istioServiceDomain?: string
|
|
642
650
|
reflection?: {
|
|
643
651
|
source?: ResourceRef
|
|
644
652
|
sourceResourceVersion?: string
|
|
@@ -658,6 +666,13 @@ export interface Relationships {
|
|
|
658
666
|
storageRefs?: ResourceRef[]
|
|
659
667
|
dependencies?: ResourceRef[]
|
|
660
668
|
dependents?: ResourceRef[]
|
|
669
|
+
monitors?: ResourceRef[]
|
|
670
|
+
monitorTargets?: ResourceRef[]
|
|
671
|
+
backends?: ResourceRef[]
|
|
672
|
+
routedFrom?: ResourceRef[]
|
|
673
|
+
protects?: ResourceRef[]
|
|
674
|
+
healthChecks?: ResourceRef[]
|
|
675
|
+
stagedPolicies?: ResourceRef[]
|
|
661
676
|
scaleTarget?: ResourceRef
|
|
662
677
|
pdbs?: ResourceRef[] // PodDisruptionBudgets protecting this workload
|
|
663
678
|
networkPolicies?: ResourceRef[] // NetworkPolicy / CiliumNetworkPolicy / ClusterNetworkPolicy variants selecting this workload
|
|
@@ -728,6 +743,7 @@ export interface HPADiagnosis extends HPADiagnosisView {
|
|
|
728
743
|
name?: string
|
|
729
744
|
}
|
|
730
745
|
bounds: HPABounds
|
|
746
|
+
/** Spec order keeps source references paired with values; unmatched status entries follow. */
|
|
731
747
|
metrics?: HPAMetricSummary[]
|
|
732
748
|
}
|
|
733
749
|
|
|
@@ -785,6 +801,8 @@ export interface APIResource {
|
|
|
785
801
|
name: string // Plural name (e.g., "deployments")
|
|
786
802
|
namespaced: boolean
|
|
787
803
|
isCrd: boolean
|
|
804
|
+
/** Confirmed, readable cached CRD identity; never inferred from isCrd. */
|
|
805
|
+
definitionName?: string
|
|
788
806
|
featured?: boolean
|
|
789
807
|
verbs: string[]
|
|
790
808
|
observation?: DynamicResourceObservation
|
|
@@ -1073,12 +1091,42 @@ export interface UpgradeInfo {
|
|
|
1073
1091
|
error?: string
|
|
1074
1092
|
// Machine-readable source-resolution failure, used to make the Helm drawer
|
|
1075
1093
|
// explain whether tracking an OCI source can help.
|
|
1076
|
-
sourceIssue?: 'untracked' | 'repo_index_error' | 'ambiguous_repository'
|
|
1094
|
+
sourceIssue?: 'source_confirmation_required' | 'source_change_required' | 'release_changing' | 'untracked' | 'repo_index_error' | 'ambiguous_repository' | 'ambiguous_source' | 'source_unavailable'
|
|
1077
1095
|
// True only when the error is a genuinely untracked source (registering a
|
|
1078
1096
|
// chart source could fix it). Kept for compatibility; prefer sourceIssue.
|
|
1079
1097
|
untracked?: boolean
|
|
1080
1098
|
}
|
|
1081
1099
|
|
|
1100
|
+
export type ChartSourceCandidate =
|
|
1101
|
+
| { type: 'repository'; reference: string; url: string }
|
|
1102
|
+
| { type: 'oci'; reference: string; url?: never }
|
|
1103
|
+
|
|
1104
|
+
export interface SourceUpgradeSelection {
|
|
1105
|
+
source: ChartSourceCandidate
|
|
1106
|
+
expectedRevision: number
|
|
1107
|
+
expectedSource: ChartSourceCandidate | null
|
|
1108
|
+
previewDigest?: string
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export interface ChartSourceSuggestion {
|
|
1112
|
+
source: ChartSourceCandidate
|
|
1113
|
+
current: boolean
|
|
1114
|
+
hasInstalledVersion: boolean
|
|
1115
|
+
state: 'available' | 'index_unavailable' | 'unchecked'
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
export interface ChartSourceStatus {
|
|
1119
|
+
changing?: boolean
|
|
1120
|
+
revision?: number
|
|
1121
|
+
confirmed?: boolean
|
|
1122
|
+
sourceChangeRequired?: boolean
|
|
1123
|
+
suggestions?: ChartSourceSuggestion[]
|
|
1124
|
+
recorded?: ChartSourceCandidate
|
|
1125
|
+
configured: boolean
|
|
1126
|
+
available: boolean
|
|
1127
|
+
candidates?: ChartSourceCandidate[]
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1082
1130
|
// Batch upgrade info keyed by "storageNamespace/name".
|
|
1083
1131
|
export interface BatchUpgradeInfo {
|
|
1084
1132
|
releases: Record<string, UpgradeInfo>
|
|
@@ -1093,6 +1141,9 @@ export interface ApplyValuesRequest {
|
|
|
1093
1141
|
|
|
1094
1142
|
// Response for previewing values changes
|
|
1095
1143
|
export interface ValuesPreviewResponse {
|
|
1144
|
+
baselineSource?: ChartSourceCandidate
|
|
1145
|
+
previewDigest?: string
|
|
1146
|
+
baselineRevision?: number
|
|
1096
1147
|
currentValues: Record<string, unknown>
|
|
1097
1148
|
newValues: Record<string, unknown>
|
|
1098
1149
|
manifestDiff: string
|
|
@@ -29,13 +29,44 @@ describe('fluxConditionsToGitOpsStatus truth table', () => {
|
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
it('treats the -1 sentinel as drift', () => {
|
|
32
|
-
|
|
32
|
+
// A first reconcile reports Ready=Unknown; a reported failure outranks drift.
|
|
33
|
+
const got = fluxConditionsToGitOpsStatus([c('Ready', 'Unknown'), c('Reconciling', 'True')], false, {
|
|
33
34
|
generation: 1,
|
|
34
35
|
observedGeneration: -1,
|
|
35
36
|
})
|
|
36
37
|
expect(got.sync).toBe('Reconciling')
|
|
37
38
|
})
|
|
38
39
|
|
|
40
|
+
it('a failure while retrying and never reconciled is degraded', () => {
|
|
41
|
+
// Live shape of a GitRepository whose repository doesn't exist.
|
|
42
|
+
const got = fluxConditionsToGitOpsStatus(
|
|
43
|
+
[c('Reconciling', 'True', { reason: 'ProgressingWithRetry' }), c('Ready', 'False', { reason: 'GitOperationFailed', observedGeneration: 1 })],
|
|
44
|
+
false,
|
|
45
|
+
{ generation: 1, observedGeneration: -1 },
|
|
46
|
+
)
|
|
47
|
+
expect(got.sync).toBe('OutOfSync')
|
|
48
|
+
expect(got.health).toBe('Degraded')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('a failure about an older generation does not outrank a new spec', () => {
|
|
52
|
+
const got = fluxConditionsToGitOpsStatus([c('Ready', 'False', { reason: 'BuildFailed', observedGeneration: 2 })], false, {
|
|
53
|
+
generation: 3,
|
|
54
|
+
observedGeneration: 1,
|
|
55
|
+
})
|
|
56
|
+
expect(got.sync).toBe('Reconciling')
|
|
57
|
+
expect(got.health).toBe('Progressing')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('a retry while reconciling stays progressing', () => {
|
|
61
|
+
const got = fluxConditionsToGitOpsStatus(
|
|
62
|
+
[c('Reconciling', 'True', { reason: 'Progressing' }), c('Ready', 'False', { reason: 'ProgressingWithRetry' })],
|
|
63
|
+
false,
|
|
64
|
+
{ generation: 2, observedGeneration: 1 },
|
|
65
|
+
)
|
|
66
|
+
expect(got.sync).toBe('Reconciling')
|
|
67
|
+
expect(got.health).toBe('Progressing')
|
|
68
|
+
})
|
|
69
|
+
|
|
39
70
|
it('does not treat an absent observedGeneration as drift', () => {
|
|
40
71
|
// Kinds and controller versions that never set it must not all read stale.
|
|
41
72
|
expect(fluxConditionsToGitOpsStatus([c('Ready', 'True')], false, { generation: 3 }).sync).toBe('Synced')
|
|
@@ -93,6 +93,12 @@ export interface GitOpsInsightSummary {
|
|
|
93
93
|
// The controller's API server was asked and didn't answer usefully — why,
|
|
94
94
|
// in the user's words. Only set when the integration is configured.
|
|
95
95
|
resourceHealthApiError?: string
|
|
96
|
+
// The host is still connecting to the controller's API server; ask again
|
|
97
|
+
// shortly.
|
|
98
|
+
resourceHealthApiPending?: boolean
|
|
99
|
+
// The host couldn't reach the controller's API server and is retrying;
|
|
100
|
+
// ask again now and then.
|
|
101
|
+
resourceHealthApiRetrying?: boolean
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
export interface GitOpsIgnoredDifferences {
|
|
@@ -164,6 +170,7 @@ export interface GitOpsChange {
|
|
|
164
170
|
hookPhase?: string
|
|
165
171
|
hasDesired: boolean
|
|
166
172
|
hasLive: boolean
|
|
173
|
+
missing?: boolean
|
|
167
174
|
// Structured per-field diff between the desired state (parsed from
|
|
168
175
|
// kubectl.kubernetes.io/last-applied-configuration) and the live spec.
|
|
169
176
|
// Undefined when the diff couldn't be computed (no annotation, SSA-applied
|
|
@@ -177,6 +184,8 @@ export interface GitOpsChange {
|
|
|
177
184
|
recentEvents?: GitOpsEventSummary[]
|
|
178
185
|
partial: boolean
|
|
179
186
|
partialNote?: string
|
|
187
|
+
// A Flux source or dependsOn target the root consumes but doesn't manage.
|
|
188
|
+
dependency?: boolean
|
|
180
189
|
}
|
|
181
190
|
|
|
182
191
|
export interface GitOpsDrift {
|
|
@@ -247,6 +256,12 @@ export interface GitOpsCapabilities {
|
|
|
247
256
|
// tell "not set up" (offer Connect) from "set up but disconnected" (offer
|
|
248
257
|
// Reconnect) — instead of advertising a diff that would fail.
|
|
249
258
|
argoConfigured?: boolean
|
|
259
|
+
// Configured, and the host is still making the first connection; ask
|
|
260
|
+
// again shortly instead of reporting the diff as down.
|
|
261
|
+
argoConnecting?: boolean
|
|
262
|
+
// Configured, the connection failed in a way that may pass on its own, and
|
|
263
|
+
// the host is retrying; ask again now and then.
|
|
264
|
+
argoRetrying?: boolean
|
|
250
265
|
// True on an Argo CD Application detail when the integration is connected,
|
|
251
266
|
// meaning the revision-metadata endpoint can resolve Git commit details
|
|
252
267
|
// (author, message, signature) for this app's deployed revisions.
|
|
@@ -269,13 +284,16 @@ export interface ArgoRevisionMetadata {
|
|
|
269
284
|
// Response of GET /api/argo/applications/{ns}/{name}/resource-diff. `desired`
|
|
270
285
|
// is the Git-rendered manifest, `live` the normalized cluster state — both YAML
|
|
271
286
|
// for the line-by-line view. `fieldEntries` is the same diff structured per
|
|
272
|
-
// path for a compact summary
|
|
287
|
+
// path for a compact summary; Radars before the list was made non-null send
|
|
288
|
+
// null when it is empty. `fieldEntriesTruncated` is set when the list was cut
|
|
289
|
+
// at the server's entry cap. `redacted` is set when Secret values were masked
|
|
273
290
|
// server-side; `hook` marks Argo sync-hook resources.
|
|
274
291
|
export interface GitOpsResourceDiff {
|
|
275
292
|
source: string
|
|
276
293
|
desired: string
|
|
277
294
|
live: string
|
|
278
|
-
fieldEntries: GitOpsResourceDiffFieldEntry[]
|
|
295
|
+
fieldEntries: GitOpsResourceDiffFieldEntry[] | null
|
|
296
|
+
fieldEntriesTruncated?: boolean
|
|
279
297
|
redacted: boolean
|
|
280
298
|
hook: boolean
|
|
281
299
|
}
|
package/src/types/gitops-tree.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface GitOpsTreeNode {
|
|
|
24
24
|
tool: GitOpsTreeTool
|
|
25
25
|
sync?: SyncStatus
|
|
26
26
|
health?: GitOpsHealthStatus
|
|
27
|
+
// A local lookup confirmed NotFound; independent of controller health.
|
|
28
|
+
missing?: boolean
|
|
27
29
|
// Who assessed `health`. 'controller' is the GitOps controller's own
|
|
28
30
|
// verdict; 'radar' is Radar's read of the live object (topology status, or
|
|
29
31
|
// the issues engine's finding when the controller's verdict isn't
|
|
@@ -43,6 +45,9 @@ export interface GitOpsTreeNode {
|
|
|
43
45
|
// Lives on the destination cluster of a remote tree. This cluster's API
|
|
44
46
|
// would answer for a different, same-named object.
|
|
45
47
|
remote?: boolean
|
|
48
|
+
// Reached only through a Flux source, chart or dependsOn declaration: the
|
|
49
|
+
// root consumes it but does not manage it. Not counted as a managed resource.
|
|
50
|
+
dependency?: boolean
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
// 'controllerApi' is the controller's own verdict read from its API server
|
|
@@ -69,6 +74,7 @@ export interface GitOpsTreeSummary {
|
|
|
69
74
|
grouped: number
|
|
70
75
|
degraded: number
|
|
71
76
|
outOfSync: number
|
|
77
|
+
dependencies?: number
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
export interface GitOpsResourceTree {
|
|
@@ -83,6 +89,12 @@ export interface GitOpsResourceTree {
|
|
|
83
89
|
healthFromApi?: boolean
|
|
84
90
|
// The controller's API server was asked and didn't answer usefully.
|
|
85
91
|
healthApiError?: string
|
|
92
|
+
// The host is still connecting to the controller's API server; ask again
|
|
93
|
+
// shortly.
|
|
94
|
+
healthApiPending?: boolean
|
|
95
|
+
// The host couldn't reach the controller's API server and is retrying;
|
|
96
|
+
// ask again now and then.
|
|
97
|
+
healthApiRetrying?: boolean
|
|
86
98
|
// The Application (or Flux object with spec.kubeConfig) deploys to another
|
|
87
99
|
// cluster; Radar derives nothing about its resources from here.
|
|
88
100
|
remoteDestination?: boolean
|
package/src/types/gitops.ts
CHANGED
|
@@ -99,6 +99,7 @@ export interface FluxCondition {
|
|
|
99
99
|
reason?: string
|
|
100
100
|
message?: string
|
|
101
101
|
lastTransitionTime?: string
|
|
102
|
+
observedGeneration?: number
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
// ============================================================================
|
|
@@ -141,8 +142,11 @@ function hasGenerationDrift(gen?: FluxGeneration): boolean {
|
|
|
141
142
|
* - suspend → Suspended (sync from Ready, unless drifted)
|
|
142
143
|
* - Stalled=True → OutOfSync / Degraded
|
|
143
144
|
* - Ready=True and no generation drift → Synced, health from Healthy
|
|
145
|
+
* - Ready=False with a failure reason about
|
|
146
|
+
* the current generation → OutOfSync / Degraded
|
|
144
147
|
* - drift, or Reconciling with Ready not True → Reconciling / Progressing
|
|
145
|
-
* - Ready=False → OutOfSync
|
|
148
|
+
* - Ready=False → OutOfSync / Degraded, or
|
|
149
|
+
* Progressing for a retry/wait reason
|
|
146
150
|
*/
|
|
147
151
|
export function fluxConditionsToGitOpsStatus(
|
|
148
152
|
conditions: FluxCondition[],
|
|
@@ -203,6 +207,25 @@ export function fluxConditionsToGitOpsStatus(
|
|
|
203
207
|
}
|
|
204
208
|
}
|
|
205
209
|
|
|
210
|
+
// A failure the controller reported outranks activity too: a source that
|
|
211
|
+
// can't clone keeps Reconciling=True and never records observedGeneration
|
|
212
|
+
// while it retries, and reading that as "in progress" hid it.
|
|
213
|
+
const readyReason = readyCondition?.reason || ''
|
|
214
|
+
const transientReason = readyReason.includes('Progress') || readyReason.includes('Retry') || readyReason.includes('Wait')
|
|
215
|
+
// Only a failure about the current spec counts: after a fix, the old
|
|
216
|
+
// Ready=False describes an older generation until the controller looks again.
|
|
217
|
+
const currentFailure = readyCondition?.observedGeneration !== undefined && readyCondition.observedGeneration === generation?.generation
|
|
218
|
+
if (readyCondition?.status === 'False' && !transientReason && currentFailure) {
|
|
219
|
+
return {
|
|
220
|
+
sync: 'OutOfSync',
|
|
221
|
+
health: 'Degraded',
|
|
222
|
+
message: readyCondition.message,
|
|
223
|
+
lastSyncTime: readyCondition.lastTransitionTime,
|
|
224
|
+
suspended: false,
|
|
225
|
+
...activity,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
206
229
|
// Genuinely not applied: the controller has not observed the current spec, or
|
|
207
230
|
// it is working and has not reached Ready.
|
|
208
231
|
if (drifted || isReconciling) {
|
|
@@ -216,14 +239,11 @@ export function fluxConditionsToGitOpsStatus(
|
|
|
216
239
|
}
|
|
217
240
|
}
|
|
218
241
|
|
|
219
|
-
//
|
|
242
|
+
// A retry or remediation in flight is out of sync, but not a settled failure.
|
|
220
243
|
if (readyCondition?.status === 'False') {
|
|
221
|
-
const reason = readyCondition.reason || ''
|
|
222
|
-
const isTransient = reason.includes('Progress') || reason.includes('Retry') || reason.includes('Wait')
|
|
223
|
-
|
|
224
244
|
return {
|
|
225
245
|
sync: 'OutOfSync',
|
|
226
|
-
health:
|
|
246
|
+
health: transientReason ? 'Progressing' : 'Degraded',
|
|
227
247
|
message: readyCondition.message,
|
|
228
248
|
lastSyncTime: readyCondition.lastTransitionTime,
|
|
229
249
|
suspended: false,
|
|
@@ -63,6 +63,13 @@ const ADDITIONAL_BUILTIN_IDENTITIES: Record<string, string> = {
|
|
|
63
63
|
deviceclass: 'resource.k8s.io', deviceclasses: 'resource.k8s.io',
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
// Kind spellings only: a resource name such as `services` is not a Kind.
|
|
67
|
+
const ADDITIONAL_CORE_KINDS: ReadonlySet<string> = new Set(['Endpoints'])
|
|
68
|
+
|
|
69
|
+
export function isBuiltinCoreKind(kind: string): boolean {
|
|
70
|
+
return ADDITIONAL_CORE_KINDS.has(kind) || CORE_RESOURCES.some(r => r.group === '' && r.kind === kind)
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
export function builtinGroupForKind(kindOrResource: string): string | undefined {
|
|
67
74
|
const value = kindOrResource.toLowerCase()
|
|
68
75
|
return CORE_RESOURCES.find(resource =>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { cnpgBackupDeclaration, cnpgBackupDestinationBlocker, cnpgBarmanPlugin, cnpgDestinationPlugin, cnpgHasBackupDestination } from './cnpg-backup'
|
|
4
|
+
|
|
5
|
+
const cases: Array<{
|
|
6
|
+
name: string
|
|
7
|
+
cluster: Parameters<typeof cnpgBackupDeclaration>[0]
|
|
8
|
+
method: string
|
|
9
|
+
plugin: string
|
|
10
|
+
hasDestination: boolean
|
|
11
|
+
destinationPlugin: string
|
|
12
|
+
blockerCode: string
|
|
13
|
+
barman: { objectStore: string; serverName: string } | null
|
|
14
|
+
}> = JSON.parse(readFileSync(new URL('../../../../pkg/cnpg/testdata/backup-declarations.json', import.meta.url), 'utf8'))
|
|
15
|
+
|
|
16
|
+
describe('CNPG backup declarations shared with Go', () => {
|
|
17
|
+
it.each(cases)('$name', (test) => {
|
|
18
|
+
const declaration = cnpgBackupDeclaration(test.cluster)
|
|
19
|
+
expect(cnpgHasBackupDestination(declaration)).toBe(test.hasDestination)
|
|
20
|
+
expect(cnpgDestinationPlugin(declaration)?.name ?? '').toBe(test.destinationPlugin)
|
|
21
|
+
expect(cnpgBackupDestinationBlocker(declaration, test.method || undefined, test.plugin)?.code ?? '').toBe(test.blockerCode)
|
|
22
|
+
const barman = cnpgBarmanPlugin(declaration)
|
|
23
|
+
if (test.barman) expect(barman).toMatchObject({ barmanObjectName: test.barman.objectStore || undefined, serverName: test.barman.serverName })
|
|
24
|
+
else expect(barman).toBeUndefined()
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const CNPG_BARMAN_PLUGIN_NAME = 'barman-cloud.cloudnative-pg.io'
|
|
2
|
+
|
|
3
|
+
interface BackupCluster {
|
|
4
|
+
metadata?: { name?: string }
|
|
5
|
+
spec?: {
|
|
6
|
+
backup?: { barmanObjectStore?: { destinationPath?: string }; volumeSnapshot?: unknown }
|
|
7
|
+
plugins?: { name: string; enabled?: boolean; isWALArchiver?: boolean; parameters?: { barmanObjectName?: string; serverName?: string } }[]
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CNPGBackupPlugin {
|
|
12
|
+
name: string
|
|
13
|
+
enabled: boolean
|
|
14
|
+
isWALArchiver: boolean
|
|
15
|
+
barmanObjectName?: string
|
|
16
|
+
serverName?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CNPGBackupDeclaration {
|
|
20
|
+
plugins: CNPGBackupPlugin[]
|
|
21
|
+
inTreeConfigured: boolean
|
|
22
|
+
inTreeDestination?: string
|
|
23
|
+
snapshotsConfigured: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cnpgBackupDeclaration(cluster: BackupCluster): CNPGBackupDeclaration {
|
|
27
|
+
const backup = cluster.spec?.backup
|
|
28
|
+
return {
|
|
29
|
+
inTreeConfigured: !!backup?.barmanObjectStore,
|
|
30
|
+
inTreeDestination: backup?.barmanObjectStore?.destinationPath,
|
|
31
|
+
snapshotsConfigured: backup?.volumeSnapshot != null,
|
|
32
|
+
plugins: (cluster.spec?.plugins ?? []).filter((p) => p?.name).map((p) => ({
|
|
33
|
+
name: p.name,
|
|
34
|
+
enabled: p.enabled !== false,
|
|
35
|
+
isWALArchiver: p.isWALArchiver === true,
|
|
36
|
+
barmanObjectName: p.parameters?.barmanObjectName,
|
|
37
|
+
serverName: p.parameters?.serverName || cluster.metadata?.name,
|
|
38
|
+
})),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function cnpgBarmanPlugin(declaration: CNPGBackupDeclaration): CNPGBackupPlugin | undefined {
|
|
43
|
+
return declaration.plugins.find((p) => p.enabled && p.name === CNPG_BARMAN_PLUGIN_NAME)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function cnpgDestinationPlugin(declaration: CNPGBackupDeclaration): CNPGBackupPlugin | undefined {
|
|
47
|
+
return declaration.plugins.find((p) => p.enabled && (p.name !== CNPG_BARMAN_PLUGIN_NAME || !!p.barmanObjectName))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function cnpgHasBackupDestination(declaration: CNPGBackupDeclaration): boolean {
|
|
51
|
+
return !!declaration.inTreeDestination || declaration.snapshotsConfigured || !!cnpgDestinationPlugin(declaration)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface CNPGBackupBlocker {
|
|
55
|
+
code: 'no_destination' | 'method_destination_missing'
|
|
56
|
+
method: string
|
|
57
|
+
plugin?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function cnpgBackupDestinationBlocker(declaration: CNPGBackupDeclaration, method = 'barmanObjectStore', plugin?: string): CNPGBackupBlocker | null {
|
|
61
|
+
const blocked = method === 'barmanObjectStore' ? !declaration.inTreeDestination
|
|
62
|
+
: method === 'volumeSnapshot' ? !declaration.snapshotsConfigured
|
|
63
|
+
: method === 'plugin' ? !declaration.plugins.some((p) => p.name === plugin && p.enabled && (p.name !== CNPG_BARMAN_PLUGIN_NAME || !!p.barmanObjectName))
|
|
64
|
+
: false
|
|
65
|
+
return blocked ? { code: cnpgHasBackupDestination(declaration) ? 'method_destination_missing' : 'no_destination', method, plugin: cnpgDestinationPlugin(declaration)?.name } : null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function cnpgBackupBlockerText(blocker: CNPGBackupBlocker): string {
|
|
69
|
+
return blocker.code === 'no_destination' ? 'No backup destination' : `No ${blocker.method} destination`
|
|
70
|
+
}
|
package/src/utils/format.test.ts
CHANGED
|
@@ -41,6 +41,15 @@ describe('parseMemoryToBytes', () => {
|
|
|
41
41
|
expect(parseMemoryToBytes('1P')).toBe(1000 ** 5)
|
|
42
42
|
expect(parseMemoryToBytes('1E')).toBe(1000 ** 6)
|
|
43
43
|
})
|
|
44
|
+
|
|
45
|
+
// The API server keeps a quoted exponent quantity as written, and defaults a
|
|
46
|
+
// request from it the same way ("1e9" limit → "500e6" request).
|
|
47
|
+
it('parses decimal-exponent quantities', () => {
|
|
48
|
+
expect(parseMemoryToBytes('1e9')).toBe(1e9)
|
|
49
|
+
expect(parseMemoryToBytes('500e6')).toBe(5e8)
|
|
50
|
+
expect(parseMemoryToBytes('1E3')).toBe(1000)
|
|
51
|
+
expect(formatMemoryString('1e9')).toBe('954 MiB')
|
|
52
|
+
})
|
|
44
53
|
})
|
|
45
54
|
|
|
46
55
|
describe('parseQuantityToNumber', () => {
|
package/src/utils/format.ts
CHANGED
|
@@ -120,6 +120,10 @@ export function parseMemoryToBytes(memString: string): number {
|
|
|
120
120
|
if (!memString) return 0
|
|
121
121
|
|
|
122
122
|
const str = memString.trim()
|
|
123
|
+
// Decimal-exponent form ("1e9") is decided before the suffix table, as in
|
|
124
|
+
// parseQuantityToNumber, so "1E3" reads as 1000 and not as exa.
|
|
125
|
+
if (/^[+-]?\d+(?:\.\d+)?[eE][+-]?\d+$/.test(str)) return Number(str)
|
|
126
|
+
|
|
123
127
|
const match = str.match(/^(-?\d+(?:\.\d+)?)\s*([A-Za-z]*)$/)
|
|
124
128
|
if (!match) return 0
|
|
125
129
|
|
|
@@ -38,10 +38,11 @@ export function gitOpsOwnerFromRelationships(
|
|
|
38
38
|
): GitOpsOwnerRef | null {
|
|
39
39
|
const refs = rel?.managedBy
|
|
40
40
|
if (!refs || refs.length === 0) return null
|
|
41
|
-
return
|
|
41
|
+
return gitOpsOwnerFromRef(refs[0])
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
/** A manager ref (relationships.managedBy, a workspace's managedBy) as a GitOps owner, or null when the manager is not a GitOps controller. */
|
|
45
|
+
export function gitOpsOwnerFromRef(ref: ResourceRef): GitOpsOwnerRef | null {
|
|
45
46
|
switch (true) {
|
|
46
47
|
case ref.kind === 'Application' && ref.group === ARGO_APPLICATION_GROUP:
|
|
47
48
|
return { tool: 'argocd', kind: 'applications', namespace: ref.namespace, name: ref.name }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { formatGrant } from './grant'
|
|
3
|
+
|
|
4
|
+
describe('formatGrant', () => {
|
|
5
|
+
it('words a namespaced grant like the server', () => {
|
|
6
|
+
expect(formatGrant({ verb: 'patch', group: 'postgresql.cnpg.io', resource: 'clusters', subresource: 'status', namespace: 'pg' })).toBe(
|
|
7
|
+
'patch clusters/status (postgresql.cnpg.io) in namespace pg',
|
|
8
|
+
)
|
|
9
|
+
expect(formatGrant({ verb: 'get', resource: 'pods', subresource: 'proxy', namespace: 'pg' })).toBe('get pods/proxy in namespace pg')
|
|
10
|
+
})
|
|
11
|
+
it('words a cluster-wide grant with the group as resource.group', () => {
|
|
12
|
+
expect(formatGrant({ verb: 'patch', resource: 'clusters', group: 'example.io', subresource: 'status' })).toBe('patch clusters.example.io/status cluster-wide')
|
|
13
|
+
expect(formatGrant({ verb: 'get', resource: 'nodes' })).toBe('get nodes cluster-wide')
|
|
14
|
+
expect(formatGrant({ verb: 'list', group: 'postgresql.cnpg.io', resource: 'clusters' })).toBe('list clusters.postgresql.cnpg.io cluster-wide')
|
|
15
|
+
})
|
|
16
|
+
it('is undefined without a grant', () => {
|
|
17
|
+
expect(formatGrant(undefined)).toBeUndefined()
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One RBAC permission a read or action needs, as the server sends it. An
|
|
3
|
+
* empty namespace means cluster-wide (a cluster-scoped resource, or a list
|
|
4
|
+
* across every namespace).
|
|
5
|
+
*/
|
|
6
|
+
export interface Grant {
|
|
7
|
+
verb: string
|
|
8
|
+
group?: string
|
|
9
|
+
resource: string
|
|
10
|
+
subresource?: string
|
|
11
|
+
namespace?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function grantResource(g: Grant): string {
|
|
15
|
+
return g.subresource ? `${g.resource}/${g.subresource}` : g.resource
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The grant's two halves for display: what ("patch clusters/status
|
|
20
|
+
* (postgresql.cnpg.io)") and where (" in namespace pg", " cluster-wide").
|
|
21
|
+
*/
|
|
22
|
+
export function grantParts(g: Grant): { what: string; scope: string } {
|
|
23
|
+
if (g.namespace) {
|
|
24
|
+
return { what: `${g.verb} ${grantResource(g)}${g.group ? ` (${g.group})` : ''}`, scope: ` in namespace ${g.namespace}` }
|
|
25
|
+
}
|
|
26
|
+
return { what: `${g.verb} ${g.resource}${g.group ? `.${g.group}` : ''}${g.subresource ? `/${g.subresource}` : ''}`, scope: ' cluster-wide' }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatGrant(g: Grant): string
|
|
30
|
+
export function formatGrant(g: Grant | undefined): string | undefined
|
|
31
|
+
export function formatGrant(g: Grant | undefined): string | undefined {
|
|
32
|
+
if (!g) return undefined
|
|
33
|
+
const { what, scope } = grantParts(g)
|
|
34
|
+
return what + scope
|
|
35
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -32,4 +32,8 @@ export * from './bulk-workload-actions'
|
|
|
32
32
|
export * from './inClusterConsent'
|
|
33
33
|
export * from './workflow-execution'
|
|
34
34
|
export * from './network-policy'
|
|
35
|
+
export * from './cnpg-backup'
|
|
36
|
+
export * from './url-path'
|
|
35
37
|
export * from './resource-refs'
|
|
38
|
+
|
|
39
|
+
export * from './grant'
|
package/src/utils/k8s-errors.ts
CHANGED
|
@@ -6,7 +6,7 @@ interface FriendlyError {
|
|
|
6
6
|
suggestion: string
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
const patterns: Array<{ test: (msg: string) => boolean; friendly: (msg: string) => FriendlyError }> = [
|
|
9
|
+
const patterns: Array<{ test: (msg: string) => boolean; friendly: (msg: string, allowApply: boolean) => FriendlyError }> = [
|
|
10
10
|
// Admission webhook timeout
|
|
11
11
|
{
|
|
12
12
|
test: (msg) => /failed calling webhook.*context deadline exceeded/i.test(msg),
|
|
@@ -75,9 +75,9 @@ const patterns: Array<{ test: (msg: string) => boolean; friendly: (msg: string)
|
|
|
75
75
|
// Already exists (create mode)
|
|
76
76
|
{
|
|
77
77
|
test: (msg) => /already exists/i.test(msg),
|
|
78
|
-
friendly: () => ({
|
|
78
|
+
friendly: (_msg, allowApply) => ({
|
|
79
79
|
summary: 'Resource already exists',
|
|
80
|
-
suggestion: 'Switch to Apply mode to update the existing resource, or change the name to create a new one.',
|
|
80
|
+
suggestion: allowApply ? 'Switch to Apply mode to update the existing resource, or change the name to create a new one.' : 'Change the name to create a new resource, or remove this document if it was already created.',
|
|
81
81
|
}),
|
|
82
82
|
},
|
|
83
83
|
// Quota exceeded
|
|
@@ -122,11 +122,11 @@ function stripPrefix(msg: string): string {
|
|
|
122
122
|
return msg.replace(/^failed to (apply|create) resource:\s*/i, '')
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
export function getFriendlyError(rawError: string): { summary: string; suggestion: string; raw: string } | null {
|
|
125
|
+
export function getFriendlyError(rawError: string, allowApply = true): { summary: string; suggestion: string; raw: string } | null {
|
|
126
126
|
const cleaned = stripPrefix(rawError)
|
|
127
127
|
for (const pattern of patterns) {
|
|
128
128
|
if (pattern.test(cleaned)) {
|
|
129
|
-
const { summary, suggestion } = pattern.friendly(cleaned)
|
|
129
|
+
const { summary, suggestion } = pattern.friendly(cleaned, allowApply)
|
|
130
130
|
return { summary, suggestion, raw: cleaned }
|
|
131
131
|
}
|
|
132
132
|
}
|
|
@@ -134,8 +134,8 @@ export function getFriendlyError(rawError: string): { summary: string; suggestio
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// Always returns something displayable — friendly if matched, raw if not
|
|
137
|
-
export function formatApplyError(rawError: string): { summary: string; suggestion?: string; raw: string } {
|
|
138
|
-
const friendly = getFriendlyError(rawError)
|
|
137
|
+
export function formatApplyError(rawError: string, { allowApply = true }: { allowApply?: boolean } = {}): { summary: string; suggestion?: string; raw: string } {
|
|
138
|
+
const friendly = getFriendlyError(rawError, allowApply)
|
|
139
139
|
if (friendly) return friendly
|
|
140
140
|
const cleaned = stripPrefix(rawError)
|
|
141
141
|
return { summary: cleaned, raw: rawError }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { highlightJson, tokenizeJson, unescapeJsonStrings } from './log-format'
|
|
3
|
+
|
|
4
|
+
describe('unescapeJsonStrings', () => {
|
|
5
|
+
it('expands whitespace inside strings while leaving escapes outside them untouched', () => {
|
|
6
|
+
expect(unescapeJsonStrings('{"msg":"first\\r\\nsecond\\nthird\\tend"}')).toBe('{"msg":"first\r\nsecond\nthird\tend"}')
|
|
7
|
+
expect(unescapeJsonStrings('outside\\n {"msg":"inside\\n"} outside\\t')).toBe('outside\\n {"msg":"inside\n"} outside\\t')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('keeps escaped quotes and backslashes in their original string', () => {
|
|
11
|
+
expect(unescapeJsonStrings('{"msg":"say \\"hello\\"\\nnext","path":"C:\\\\docs"}')).toBe('{"msg":"say \\"hello\\"\nnext","path":"C:\\\\docs"}')
|
|
12
|
+
expect(unescapeJsonStrings(JSON.stringify({ empty: '', nested: ['a\nb', 'c\td'], number: 42, yes: true }))).toBe('{"empty":"","nested":["a\nb","c\td"],"number":42,"yes":true}')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('stops once at an unterminated string with many escaped quotes', () => {
|
|
16
|
+
const text = '"' + '\\"'.repeat(100_000) + '\\n'
|
|
17
|
+
expect(unescapeJsonStrings(text)).toBe(text)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('JSON highlighting', () => {
|
|
22
|
+
it('keeps primitive-looking words inside strings and preserves JSON token colors', () => {
|
|
23
|
+
const tokens = tokenizeJson('{ "text" : "true null 123 \\"quoted\\"", "values": [-1.2e+3, true, false, null, ""] }')
|
|
24
|
+
expect(tokens.filter((token) => token.type === 'string').map((token) => token.value)).toEqual(['"true null 123 \\"quoted\\""', '""'])
|
|
25
|
+
expect(tokens.filter((token) => token.type === 'number').map((token) => token.value)).toEqual(['-1.2e+3'])
|
|
26
|
+
expect(highlightJson('{"a":true,"b":null,"n":12}')).toBe('{<span style="color:#7cacf8">"a"</span>:<span style="color:#c678dd">true</span>,<span style="color:#7cacf8">"b"</span>:<span style="color:#808080">null</span>,<span style="color:#7cacf8">"n"</span>:<span style="color:#e5c07b">12</span>}')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('escapes HTML in log values before inserting highlighting markup', () => {
|
|
30
|
+
const html = highlightJson(JSON.stringify({ msg: '<script>alert("x")</script>&' }))
|
|
31
|
+
expect(html).toContain('<script>')
|
|
32
|
+
expect(html).toContain('</script>&')
|
|
33
|
+
expect(html).not.toContain('<script>')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('leaves a long unterminated string as plain text without rescanning escaped quotes', () => {
|
|
37
|
+
const text = '"' + '\\"'.repeat(100_000)
|
|
38
|
+
expect(tokenizeJson(text)).toEqual([{ type: 'text', value: text }])
|
|
39
|
+
expect(highlightJson(text)).toBe(text)
|
|
40
|
+
})
|
|
41
|
+
})
|