@skyhook-io/k8s-ui 1.16.3 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/package.json +3 -3
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- 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/checks/types.ts +2 -13
- 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/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- 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 +29 -83
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
- 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 +14 -3
- package/src/components/issues/issues.test.ts +50 -2
- 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.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +28 -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 +309 -117
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- 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 +7 -8
- 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 +9 -17
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- 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/ConfigAuditReportRenderer.tsx +2 -2
- 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.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +62 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +57 -17
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
- 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/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +15 -13
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- 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/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +62 -36
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +9 -22
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- 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 -24
- 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-keda.ts +1 -7
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +99 -39
- 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/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +20 -24
- package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +4 -1
- package/src/components/timeline/TimelineList.tsx +2 -15
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
- 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 +8 -74
- 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/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +175 -1
- package/src/components/ui/drawer-components.tsx +103 -36
- 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 +101 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +297 -104
- 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 +21 -2
- package/src/types/core.ts +60 -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 +49 -2
- package/src/utils/format.ts +9 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/ingress-class-references.ts +22 -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 +70 -0
- package/src/utils/navigation.ts +91 -8
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +69 -0
- package/src/utils/resource-hierarchy.ts +41 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -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
|
@@ -1528,3 +1528,72 @@ describe('structural app members survive the window filter', () => {
|
|
|
1528
1528
|
expect(isChildVisibleInWindow(stray, 0, 1000, { pinned: false, userExpanded: false })).toBe(false)
|
|
1529
1529
|
})
|
|
1530
1530
|
})
|
|
1531
|
+
|
|
1532
|
+
describe('uses edges in the timeline hierarchy', () => {
|
|
1533
|
+
const node = (kind: string, apiVersion: string, name: string) => ({
|
|
1534
|
+
id: `${kind.toLowerCase()}/team/${name}`, kind, name, status: 'healthy', data: { namespace: 'team', apiVersion },
|
|
1535
|
+
})
|
|
1536
|
+
const topology = {
|
|
1537
|
+
nodes: [
|
|
1538
|
+
node('HorizontalPodAutoscaler', 'autoscaling/v2', 'web'),
|
|
1539
|
+
node('Deployment', 'apps/v1', 'web'),
|
|
1540
|
+
node('Certificate', 'cert-manager.io/v1', 'tls'),
|
|
1541
|
+
node('Issuer', 'cert-manager.io/v1', 'ca'),
|
|
1542
|
+
],
|
|
1543
|
+
edges: [
|
|
1544
|
+
{ id: 'hpa', source: 'horizontalpodautoscaler/team/web', target: 'deployment/team/web', type: 'uses' },
|
|
1545
|
+
{ id: 'issuer', source: 'certificate/team/tls', target: 'issuer/team/ca', type: 'uses' },
|
|
1546
|
+
],
|
|
1547
|
+
} as unknown as Topology
|
|
1548
|
+
const events = [
|
|
1549
|
+
changeEvent('HorizontalPodAutoscaler', 'team', 'web', { apiVersion: 'autoscaling/v2' }),
|
|
1550
|
+
changeEvent('Deployment', 'team', 'web', { apiVersion: 'apps/v1' }),
|
|
1551
|
+
changeEvent('Certificate', 'team', 'tls', { apiVersion: 'cert-manager.io/v1' }),
|
|
1552
|
+
changeEvent('Issuer', 'team', 'ca', { apiVersion: 'cert-manager.io/v1' }),
|
|
1553
|
+
]
|
|
1554
|
+
|
|
1555
|
+
it('files an autoscaler under its workload but not a dependent under its dependency', () => {
|
|
1556
|
+
const lanes = buildResourceHierarchy({ events, topology, grouping: 'owner' })
|
|
1557
|
+
const top = lanes.map(lane => `${lane.kind}/${lane.name}`).sort()
|
|
1558
|
+
expect(top).toEqual(['Certificate/tls', 'Deployment/web', 'Issuer/ca'])
|
|
1559
|
+
const deployment = lanes.find(lane => lane.kind === 'Deployment')!
|
|
1560
|
+
expect(deployment.children?.map(child => child.kind)).toEqual(['HorizontalPodAutoscaler'])
|
|
1561
|
+
})
|
|
1562
|
+
})
|
|
1563
|
+
|
|
1564
|
+
describe('buildResourceHierarchy Flux sources', () => {
|
|
1565
|
+
it('keeps a shared Flux source as its own lane instead of nesting it under one consumer', () => {
|
|
1566
|
+
const event = (kind: string, name: string, apiVersion: string): TimelineEvent => ({
|
|
1567
|
+
id: `${kind}/flux-system/${name}`,
|
|
1568
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
1569
|
+
source: 'informer',
|
|
1570
|
+
kind,
|
|
1571
|
+
apiVersion,
|
|
1572
|
+
namespace: 'flux-system',
|
|
1573
|
+
name,
|
|
1574
|
+
eventType: 'update',
|
|
1575
|
+
} as TimelineEvent)
|
|
1576
|
+
const events = [
|
|
1577
|
+
event('GitRepository', 'podinfo', 'source.toolkit.fluxcd.io/v1'),
|
|
1578
|
+
event('Kustomization', 'base', 'kustomize.toolkit.fluxcd.io/v1'),
|
|
1579
|
+
event('Kustomization', 'overlay', 'kustomize.toolkit.fluxcd.io/v1'),
|
|
1580
|
+
]
|
|
1581
|
+
const topology = {
|
|
1582
|
+
nodes: [
|
|
1583
|
+
{ id: 'gitrepository/flux-system/podinfo', kind: 'GitRepository', name: 'podinfo', status: 'healthy', data: { namespace: 'flux-system', apiVersion: 'source.toolkit.fluxcd.io/v1' } },
|
|
1584
|
+
{ id: 'kustomization/flux-system/base', kind: 'Kustomization', name: 'base', status: 'healthy', data: { namespace: 'flux-system', apiVersion: 'kustomize.toolkit.fluxcd.io/v1' } },
|
|
1585
|
+
{ id: 'kustomization/flux-system/overlay', kind: 'Kustomization', name: 'overlay', status: 'healthy', data: { namespace: 'flux-system', apiVersion: 'kustomize.toolkit.fluxcd.io/v1' } },
|
|
1586
|
+
],
|
|
1587
|
+
edges: [
|
|
1588
|
+
{ id: 'e1', source: 'gitrepository/flux-system/podinfo', target: 'kustomization/flux-system/base', type: 'configures' },
|
|
1589
|
+
{ id: 'e2', source: 'gitrepository/flux-system/podinfo', target: 'kustomization/flux-system/overlay', type: 'configures' },
|
|
1590
|
+
],
|
|
1591
|
+
} as unknown as Topology
|
|
1592
|
+
const lanes = buildResourceHierarchy({ events, topology, grouping: 'owner' })
|
|
1593
|
+
expect(lanes.map((lane) => `${lane.kind}/${lane.name}`).sort()).toEqual([
|
|
1594
|
+
'GitRepository/podinfo',
|
|
1595
|
+
'Kustomization/base',
|
|
1596
|
+
'Kustomization/overlay',
|
|
1597
|
+
])
|
|
1598
|
+
})
|
|
1599
|
+
})
|
|
@@ -466,7 +466,7 @@ function sortEventsForRendering(events: TimelineEvent[]): TimelineEvent[] {
|
|
|
466
466
|
// then workloads, then their generated children (ReplicaSet/Pod), config last.
|
|
467
467
|
// Applied at EVERY depth so a Job's Pods sort the same way a root's children do.
|
|
468
468
|
const CHILD_SORT_KIND_PRIORITY: Record<string, number> = {
|
|
469
|
-
Service: 1, Gateway: 1, HTTPRoute: 2, GRPCRoute: 2, TCPRoute: 2, TLSRoute: 2,
|
|
469
|
+
Service: 1, Gateway: 1, HTTPRoute: 2, GRPCRoute: 2, TCPRoute: 2, TLSRoute: 2, UDPRoute: 2,
|
|
470
470
|
Deployment: 2, Rollout: 2, StatefulSet: 2, DaemonSet: 2,
|
|
471
471
|
Job: 3, CronJob: 3,
|
|
472
472
|
ReplicaSet: 3, Pod: 4, ConfigMap: 5, Secret: 5,
|
|
@@ -699,6 +699,7 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
|
|
|
699
699
|
|
|
700
700
|
// Source 2: Topology edges (for Service→Deployment, Ingress→Service, ConfigMap→Deployment).
|
|
701
701
|
if (topology?.edges) {
|
|
702
|
+
const topologyNodeById = new Map(topology.nodes?.map(node => [node.id, node]) ?? [])
|
|
702
703
|
for (const edge of topology.edges) {
|
|
703
704
|
const sourceId = topologyLaneIdByNodeId.get(edge.source)
|
|
704
705
|
const targetId = topologyLaneIdByNodeId.get(edge.target)
|
|
@@ -760,11 +761,11 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
|
|
|
760
761
|
const targetKind = parseLaneId(targetId)!.kind
|
|
761
762
|
|
|
762
763
|
// Gateway→Route: Gateway is parent of Route
|
|
763
|
-
if (sourceKind === 'Gateway' && (targetKind === 'HTTPRoute' || targetKind === 'GRPCRoute' || targetKind === 'TCPRoute' || targetKind === 'TLSRoute')) {
|
|
764
|
+
if (sourceKind === 'Gateway' && (targetKind === 'HTTPRoute' || targetKind === 'GRPCRoute' || targetKind === 'TCPRoute' || targetKind === 'TLSRoute' || targetKind === 'UDPRoute')) {
|
|
764
765
|
link(targetId, sourceId, targetExists)
|
|
765
766
|
}
|
|
766
767
|
// Route→Service: reverse (Service is representative, like Ingress)
|
|
767
|
-
else if ((sourceKind === 'HTTPRoute' || sourceKind === 'GRPCRoute' || sourceKind === 'TCPRoute' || sourceKind === 'TLSRoute') && targetKind === 'Service') {
|
|
768
|
+
else if ((sourceKind === 'HTTPRoute' || sourceKind === 'GRPCRoute' || sourceKind === 'TCPRoute' || sourceKind === 'TLSRoute' || sourceKind === 'UDPRoute') && targetKind === 'Service') {
|
|
768
769
|
link(sourceId, targetId, sourceExists)
|
|
769
770
|
}
|
|
770
771
|
// Ingress→Service: reverse relationship (Service is representative)
|
|
@@ -777,8 +778,17 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
|
|
|
777
778
|
}
|
|
778
779
|
}
|
|
779
780
|
|
|
780
|
-
// configures/
|
|
781
|
-
|
|
781
|
+
// configures/protects: ConfigMap→Deployment, PDB→Deployment (target is parent).
|
|
782
|
+
// A Flux source feeds many reconcilers and its timeline is its own, so it
|
|
783
|
+
// never nests under one of them.
|
|
784
|
+
if ((edge.type === 'configures' && !isFluxSource(topologyNodeById.get(edge.source))) || edge.type === 'protects') {
|
|
785
|
+
link(sourceId, targetId, sourceExists)
|
|
786
|
+
}
|
|
787
|
+
// uses: only an autoscaler or a mounted claim belongs under the workload it
|
|
788
|
+
// serves. Other uses edges point from a dependent to what it depends on
|
|
789
|
+
// (Certificate→Issuer, Gateway→GatewayClass); nesting there would file
|
|
790
|
+
// every dependent under one shared dependency.
|
|
791
|
+
if (edge.type === 'uses' && attachesToTarget(topologyNodeById.get(edge.source))) {
|
|
782
792
|
link(sourceId, targetId, sourceExists)
|
|
783
793
|
}
|
|
784
794
|
}
|
|
@@ -805,7 +815,7 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
|
|
|
805
815
|
const appLabelEligibleKinds = new Set([
|
|
806
816
|
'Service', 'Deployment', 'Rollout', 'StatefulSet', 'DaemonSet',
|
|
807
817
|
'Job', 'CronJob', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute',
|
|
808
|
-
'TCPRoute', 'TLSRoute', 'ConfigMap', 'Secret',
|
|
818
|
+
'TCPRoute', 'TLSRoute', 'UDPRoute', 'ConfigMap', 'Secret',
|
|
809
819
|
'Application', 'Kustomization', 'HelmRelease', 'GitRepository',
|
|
810
820
|
'Workflow', 'CronWorkflow',
|
|
811
821
|
'KnativeService', 'KnativeConfiguration', 'KnativeRevision', 'KnativeRoute',
|
|
@@ -836,7 +846,7 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
|
|
|
836
846
|
|
|
837
847
|
const kindPriority: Record<string, number> = {
|
|
838
848
|
Service: 1, Ingress: 2, Gateway: 2,
|
|
839
|
-
HTTPRoute: 2, GRPCRoute: 2, TCPRoute: 2, TLSRoute: 2,
|
|
849
|
+
HTTPRoute: 2, GRPCRoute: 2, TCPRoute: 2, TLSRoute: 2, UDPRoute: 2,
|
|
840
850
|
Deployment: 3, Rollout: 3, StatefulSet: 3, DaemonSet: 3,
|
|
841
851
|
Job: 4, CronJob: 4, Workflow: 4, CronWorkflow: 4,
|
|
842
852
|
ConfigMap: 5, Secret: 5,
|
|
@@ -1468,3 +1478,27 @@ export function countEventsInHierarchy(lanes: ResourceLane[]): number {
|
|
|
1468
1478
|
lanes.forEach(walk)
|
|
1469
1479
|
return count
|
|
1470
1480
|
}
|
|
1481
|
+
|
|
1482
|
+
// The uses-edge sources that attach to their target workload: autoscalers and
|
|
1483
|
+
// PersistentVolumeClaims, matched on exact API group.
|
|
1484
|
+
const ATTACHED_USES_SOURCES = new Set([
|
|
1485
|
+
'autoscaling/HorizontalPodAutoscaler',
|
|
1486
|
+
'autoscaling.k8s.io/VerticalPodAutoscaler',
|
|
1487
|
+
'keda.sh/ScaledObject',
|
|
1488
|
+
'keda.sh/ScaledJob',
|
|
1489
|
+
'/PersistentVolumeClaim',
|
|
1490
|
+
])
|
|
1491
|
+
|
|
1492
|
+
function isFluxSource(node: Topology['nodes'][number] | undefined): boolean {
|
|
1493
|
+
if (!node) return false
|
|
1494
|
+
const apiVersion = typeof node.data?.apiVersion === 'string' ? node.data.apiVersion : ''
|
|
1495
|
+
return apiVersionToGroup(apiVersion) === 'source.toolkit.fluxcd.io'
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function attachesToTarget(node: Topology['nodes'][number] | undefined): boolean {
|
|
1499
|
+
if (!node) return false
|
|
1500
|
+
const kind = topologyNodeResourceKind(node)
|
|
1501
|
+
const apiVersion = typeof node.data?.apiVersion === 'string' ? node.data.apiVersion : ''
|
|
1502
|
+
const group = canonicalResourceGroup(kind, apiVersionToGroup(apiVersion)) ?? ''
|
|
1503
|
+
return ATTACHED_USES_SOURCES.has(`${group}/${kind}`)
|
|
1504
|
+
}
|
|
@@ -111,6 +111,7 @@ const KIND_ICON_MAP: Record<string, LucideIcon> = {
|
|
|
111
111
|
httproute: Globe,
|
|
112
112
|
grpcroute: Globe,
|
|
113
113
|
tcproute: Globe,
|
|
114
|
+
udproute: Globe,
|
|
114
115
|
tlsroute: Globe,
|
|
115
116
|
|
|
116
117
|
// Config
|
|
@@ -219,6 +220,7 @@ const KIND_ICON_MAP: Record<string, LucideIcon> = {
|
|
|
219
220
|
// pseudo-kinds (cnpgcluster/…) belong here only once pkg/topology's
|
|
220
221
|
// KindForGVK emits them — it has no CNPG case today.
|
|
221
222
|
pooler: Waypoints,
|
|
223
|
+
databaserole: UserCog,
|
|
222
224
|
|
|
223
225
|
// Cluster API
|
|
224
226
|
capicluster: Server,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { ResourceRef } from '../types/core'
|
|
3
|
+
import { dedupeResourceRefs, omitResourceRefs, resourceRefKey } from './resource-refs'
|
|
4
|
+
|
|
5
|
+
describe('resource reference identity', () => {
|
|
6
|
+
const core: ResourceRef = { kind: 'Service', namespace: 'team', name: 'app' }
|
|
7
|
+
const knative: ResourceRef = { ...core, group: 'serving.knative.dev' }
|
|
8
|
+
|
|
9
|
+
it('deduplicates core aliases while preserving an identically named custom resource', () => {
|
|
10
|
+
const refs = [core, { ...core, group: '' }, knative]
|
|
11
|
+
expect(dedupeResourceRefs(refs)).toEqual([core, knative])
|
|
12
|
+
expect(refs).toHaveLength(3)
|
|
13
|
+
expect(resourceRefKey(core)).not.toBe(resourceRefKey(knative))
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('omits only the exact mirrored identity without mutating either list', () => {
|
|
17
|
+
const refs = [core, knative]
|
|
18
|
+
const excluded = [{ ...core, group: '' }]
|
|
19
|
+
expect(omitResourceRefs(refs, excluded)).toEqual([knative])
|
|
20
|
+
expect(refs).toEqual([core, knative])
|
|
21
|
+
expect(excluded).toHaveLength(1)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ResourceRef } from '../types/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* resourceKey mirrors Go `audit.ResourceKey(group, kind, namespace, name)`:
|
|
5
|
+
* `group|Kind|namespace|name`. Group first because group and namespace can each
|
|
6
|
+
* independently be empty; `|` is delimiter-safe (K8s API groups follow
|
|
7
|
+
* DNS-subdomain rules and can't contain it).
|
|
8
|
+
*/
|
|
9
|
+
export function resourceKey(group: string, kind: string, namespace: string, name: string): string {
|
|
10
|
+
return `${group}|${kind}|${namespace}|${name}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resourceRefKey(ref: ResourceRef): string {
|
|
14
|
+
return resourceKey(ref.group ?? '', ref.kind, ref.namespace, ref.name);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function dedupeResourceRefs(refs: ResourceRef[]): ResourceRef[] {
|
|
18
|
+
const seen = new Set<string>()
|
|
19
|
+
return refs.filter(ref => {
|
|
20
|
+
const key = resourceRefKey(ref)
|
|
21
|
+
if (seen.has(key)) return false
|
|
22
|
+
seen.add(key)
|
|
23
|
+
return true
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function omitResourceRefs(refs: ResourceRef[], excluded: ResourceRef[]): ResourceRef[] {
|
|
28
|
+
const excludedKeys = new Set(excluded.map(resourceRefKey))
|
|
29
|
+
return refs.filter(ref => !excludedKeys.has(resourceRefKey(ref)))
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getSkeletonYaml } from './skeleton-yaml'
|
|
3
|
+
|
|
4
|
+
describe('getSkeletonYaml', () => {
|
|
5
|
+
it('starts a CloudNativePG Cluster from a spec the operator accepts', () => {
|
|
6
|
+
const yaml = getSkeletonYaml('Cluster', 'postgresql.cnpg.io')
|
|
7
|
+
expect(yaml).toContain('apiVersion: postgresql.cnpg.io/v1')
|
|
8
|
+
expect(yaml).toContain('instances: 3')
|
|
9
|
+
expect(yaml).toContain('size: 1Gi')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('leaves a Cluster API Cluster on the generic template', () => {
|
|
13
|
+
const yaml = getSkeletonYaml('Cluster', 'cluster.x-k8s.io')
|
|
14
|
+
expect(yaml).toContain('apiVersion: cluster.x-k8s.io/v1')
|
|
15
|
+
expect(yaml).not.toContain('instances')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('keeps core kinds on their own skeleton', () => {
|
|
19
|
+
expect(getSkeletonYaml('Pod')).toContain('containers:')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -198,8 +198,24 @@ spec:
|
|
|
198
198
|
image: nginx:latest`,
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// Keyed by group as well as kind: CloudNativePG and Cluster API both name a
|
|
202
|
+
// kind Cluster.
|
|
203
|
+
const groupSkeletons: Record<string, string> = {
|
|
204
|
+
'postgresql.cnpg.io/Cluster': `apiVersion: postgresql.cnpg.io/v1
|
|
205
|
+
kind: Cluster
|
|
206
|
+
metadata:
|
|
207
|
+
name: my-cluster
|
|
208
|
+
namespace: default
|
|
209
|
+
spec:
|
|
210
|
+
instances: 3
|
|
211
|
+
storage:
|
|
212
|
+
size: 1Gi`,
|
|
213
|
+
}
|
|
214
|
+
|
|
201
215
|
// Returns skeleton YAML for a known kind, or a generic template for unknown kinds.
|
|
202
216
|
export function getSkeletonYaml(kind: string, group?: string): string {
|
|
217
|
+
const grouped = group ? groupSkeletons[`${group}/${kind}`] : undefined
|
|
218
|
+
if (grouped) return grouped
|
|
203
219
|
const skeleton = skeletons[kind]
|
|
204
220
|
if (skeleton) return skeleton
|
|
205
221
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { normalizeURLForComparison, stripTrailingSlashes } from './url-path'
|
|
3
|
+
|
|
4
|
+
describe('stripTrailingSlashes', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
['', ''],
|
|
7
|
+
['/', ''],
|
|
8
|
+
['////', ''],
|
|
9
|
+
['/api///', '/api'],
|
|
10
|
+
['https://store.example//bucket///', 'https://store.example//bucket'],
|
|
11
|
+
['https://store.example//bucket?prefix=/', 'https://store.example//bucket?prefix='],
|
|
12
|
+
['/api//nested', '/api//nested'],
|
|
13
|
+
])('preserves the existing path normalization for %s', (value, expected) => {
|
|
14
|
+
expect(stripTrailingSlashes(value)).toBe(expected)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('handles long slash runs without scanning each possible start', () => {
|
|
18
|
+
const slashes = '/'.repeat(100_000)
|
|
19
|
+
expect(stripTrailingSlashes(`prefix${slashes}`)).toBe('prefix')
|
|
20
|
+
expect(stripTrailingSlashes(`prefix${slashes}suffix`)).toBe(`prefix${slashes}suffix`)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('compares equivalent origins while retaining declared path semantics', () => {
|
|
25
|
+
expect(normalizeURLForComparison('https://STORE.example:443/prefix/')).toBe(normalizeURLForComparison('https://store.example/prefix'))
|
|
26
|
+
expect(normalizeURLForComparison('http://STORE.example:080/')).toBe(normalizeURLForComparison('http://store.example'))
|
|
27
|
+
expect(normalizeURLForComparison('s3://BUCKET/path/')).toBe(normalizeURLForComparison('s3://bucket/path'))
|
|
28
|
+
expect(normalizeURLForComparison('https://store.example/Prefix')).not.toBe(normalizeURLForComparison('https://store.example/prefix'))
|
|
29
|
+
expect(normalizeURLForComparison('https://store.example/a/../b')).not.toBe(normalizeURLForComparison('https://store.example/b'))
|
|
30
|
+
expect(normalizeURLForComparison('https://store.example:8443/path')).not.toBe(normalizeURLForComparison('https://store.example/path'))
|
|
31
|
+
expect(normalizeURLForComparison('')).toBe('')
|
|
32
|
+
expect(normalizeURLForComparison('invalid')).toBeNull()
|
|
33
|
+
expect(normalizeURLForComparison('https://host\\prefix')).toBeNull()
|
|
34
|
+
expect(normalizeURLForComparison('https://user:password@store.example')).toBeNull()
|
|
35
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function stripTrailingSlashes(value: string): string {
|
|
2
|
+
let end = value.length
|
|
3
|
+
while (end > 0 && value[end - 1] === '/') end--
|
|
4
|
+
return value.slice(0, end)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function normalizeURLForComparison(value: string): string | null {
|
|
8
|
+
if (!value) return ''
|
|
9
|
+
if (value.includes('\\')) return null
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(value)
|
|
12
|
+
const authorityStart = value.indexOf('://')
|
|
13
|
+
if (authorityStart < 0 || !url.hostname || url.username || url.password || value.includes('?') || value.includes('#')) return null
|
|
14
|
+
const pathStart = value.indexOf('/', authorityStart + 3)
|
|
15
|
+
const path = pathStart < 0 ? '' : value.slice(pathStart)
|
|
16
|
+
const port = url.port || (url.protocol === 'https:' ? '443' : url.protocol === 'http:' ? '80' : '')
|
|
17
|
+
return `${url.protocol}//${url.hostname.toLowerCase()}:${port}${stripTrailingSlashes(path)}`
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -82,25 +82,16 @@ describe('validateRFC1123Subdomain', () => {
|
|
|
82
82
|
if (!r.valid) expect(r.error).toMatch(/253/)
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
-
it('
|
|
86
|
-
|
|
87
|
-
// separated label on top of the 253-char total. Without the
|
|
88
|
-
// per-label check, a single 200-char label slipped through
|
|
89
|
-
// client-side and was rejected only server-side.
|
|
90
|
-
const huge = 'a'.repeat(200)
|
|
91
|
-
const r = validateRFC1123Subdomain(huge)
|
|
92
|
-
expect(r.valid).toBe(false)
|
|
93
|
-
if (!r.valid) expect(r.error).toMatch(/63/)
|
|
85
|
+
it('accepts long undotted names under the Kubernetes total limit', () => {
|
|
86
|
+
expect(validateRFC1123Subdomain('a'.repeat(200)).valid).toBe(true)
|
|
94
87
|
})
|
|
95
88
|
|
|
96
89
|
it('accepts a 63-char label at the boundary', () => {
|
|
97
90
|
expect(validateRFC1123Subdomain('a'.repeat(63)).valid).toBe(true)
|
|
98
91
|
})
|
|
99
92
|
|
|
100
|
-
it('
|
|
101
|
-
|
|
102
|
-
expect(r.valid).toBe(false)
|
|
103
|
-
if (!r.valid) expect(r.error).toMatch(/63/)
|
|
93
|
+
it('accepts long dot-separated parts under the Kubernetes total limit', () => {
|
|
94
|
+
expect(validateRFC1123Subdomain(`ok.${'a'.repeat(64)}.also-ok`).valid).toBe(true)
|
|
104
95
|
})
|
|
105
96
|
})
|
|
106
97
|
|
package/src/utils/validators.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type ValidationResult =
|
|
|
17
17
|
| { valid: false; error: string }
|
|
18
18
|
|
|
19
19
|
const RFC1123_LABEL_MAX = 63
|
|
20
|
-
// Kubernetes namespace
|
|
20
|
+
// Kubernetes namespace names are RFC 1123 labels:
|
|
21
21
|
// lowercase alphanumeric or '-', start and end alphanumeric, ≤63 chars.
|
|
22
22
|
// Source: k8s.io/apimachinery/pkg/util/validation.IsDNS1123Label.
|
|
23
23
|
const RFC1123_LABEL_RE = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/
|
|
@@ -34,9 +34,7 @@ const HELM_RELEASE_NAME_MAX = 53
|
|
|
34
34
|
// https://helm.sh/docs/chart_best_practices/conventions/#chart-names
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
* pod / service / configmap / secret names — anything Kubernetes
|
|
39
|
-
* accepts as a single-segment identifier.
|
|
37
|
+
* Matches Kubernetes IsDNS1123Label, including namespace names.
|
|
40
38
|
*/
|
|
41
39
|
export function validateRFC1123Label(input: string): ValidationResult {
|
|
42
40
|
if (input.length === 0) {
|
|
@@ -64,9 +62,9 @@ export function validateRFC1123Label(input: string): ValidationResult {
|
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
65
|
+
* Matches Kubernetes IsDNS1123Subdomain: the whole name is capped at 253
|
|
66
|
+
* characters, without a separate 63-character cap on dot-separated parts.
|
|
67
|
+
* Namespace names use validateRFC1123Label instead.
|
|
70
68
|
*/
|
|
71
69
|
export function validateRFC1123Subdomain(input: string): ValidationResult {
|
|
72
70
|
if (input.length === 0) {
|
|
@@ -90,18 +88,6 @@ export function validateRFC1123Subdomain(input: string): ValidationResult {
|
|
|
90
88
|
error: 'must contain only lowercase letters, numbers, hyphens, and dots, and each segment must start/end with a letter or number',
|
|
91
89
|
}
|
|
92
90
|
}
|
|
93
|
-
// Per-label cap: K8s' IsDNS1123Subdomain enforces ≤ 63 chars
|
|
94
|
-
// PER dot-separated label on top of the 253-char total. The
|
|
95
|
-
// regex above doesn't catch that; without this check a single
|
|
96
|
-
// 200-char label passes here and only fails server-side.
|
|
97
|
-
for (const label of input.split('.')) {
|
|
98
|
-
if (label.length > RFC1123_LABEL_MAX) {
|
|
99
|
-
return {
|
|
100
|
-
valid: false,
|
|
101
|
-
error: `each dot-separated label must be at most ${RFC1123_LABEL_MAX} characters (got ${label.length} for "${label}")`,
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
91
|
return { valid: true }
|
|
106
92
|
}
|
|
107
93
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import { isInvalidStateError, startViewTransitionSafe } from './view-transition'
|
|
2
|
+
import { isInvalidStateError, isWebKitGTK, startViewTransitionSafe } from './view-transition'
|
|
3
3
|
|
|
4
4
|
// SKY-833 bug 49: rapid tab / route switches surfaced
|
|
5
5
|
// "InvalidStateError: Transition was aborted because of invalid state"
|
|
@@ -79,3 +79,37 @@ describe('startViewTransitionSafe', () => {
|
|
|
79
79
|
expect(isInvalidStateError(new TypeError('y'))).toBe(false)
|
|
80
80
|
})
|
|
81
81
|
})
|
|
82
|
+
|
|
83
|
+
const WEBKITGTK_UA =
|
|
84
|
+
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15'
|
|
85
|
+
|
|
86
|
+
describe('isWebKitGTK', () => {
|
|
87
|
+
it('matches WebKitGTK user agents', () => {
|
|
88
|
+
expect(isWebKitGTK(WEBKITGTK_UA)).toBe(true)
|
|
89
|
+
expect(isWebKitGTK('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko)')).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('does not match other engines on Linux, or Safari', () => {
|
|
93
|
+
expect(isWebKitGTK('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36')).toBe(false)
|
|
94
|
+
expect(isWebKitGTK('Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0')).toBe(false)
|
|
95
|
+
expect(isWebKitGTK('Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36')).toBe(false)
|
|
96
|
+
expect(isWebKitGTK('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15')).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('startViewTransitionSafe under WebKitGTK', () => {
|
|
101
|
+
it('applies the update directly instead of starting a transition', () => {
|
|
102
|
+
const originalDoc = (globalThis as { document?: unknown }).document
|
|
103
|
+
const startSpy = vi.fn()
|
|
104
|
+
;(globalThis as { document: object }).document = { startViewTransition: startSpy }
|
|
105
|
+
const uaSpy = vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(WEBKITGTK_UA)
|
|
106
|
+
|
|
107
|
+
const update = vi.fn()
|
|
108
|
+
startViewTransitionSafe(update)
|
|
109
|
+
expect(update).toHaveBeenCalledTimes(1)
|
|
110
|
+
expect(startSpy).not.toHaveBeenCalled()
|
|
111
|
+
|
|
112
|
+
uaSpy.mockRestore()
|
|
113
|
+
;(globalThis as { document?: unknown }).document = originalDoc
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Other rejection types (programming errors, unsupported configs)
|
|
16
16
|
* are re-thrown so we don't accidentally hide real bugs.
|
|
17
|
+
*
|
|
18
|
+
* Under WebKitGTK (the Linux desktop app's webview) the update is applied
|
|
19
|
+
* without a transition: when WebKitGTK has no renderer buffer transport —
|
|
20
|
+
* WEBKIT_DISABLE_DMABUF_RENDERER or WEBKIT_DISABLE_COMPOSITING_MODE set, a
|
|
21
|
+
* distro patch, or an EGL stack without GBM/surfaceless platforms — starting
|
|
22
|
+
* a view transition segfaults the UI process. The page cannot tell which
|
|
23
|
+
* case it is in, so it skips the cross-fade on every WebKitGTK host.
|
|
17
24
|
*/
|
|
18
25
|
export function startViewTransitionSafe(update: () => void): void {
|
|
19
26
|
// Some lib.dom.d.ts versions don't yet expose `startViewTransition`
|
|
@@ -27,7 +34,7 @@ export function startViewTransitionSafe(update: () => void): void {
|
|
|
27
34
|
}
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
if (typeof doc.startViewTransition !== 'function') {
|
|
37
|
+
if (typeof doc.startViewTransition !== 'function' || isWebKitGTK(navigator.userAgent)) {
|
|
31
38
|
update()
|
|
32
39
|
return
|
|
33
40
|
}
|
|
@@ -60,3 +67,11 @@ export function isInvalidStateError(err: unknown): boolean {
|
|
|
60
67
|
(err as { name: unknown }).name === 'InvalidStateError'
|
|
61
68
|
)
|
|
62
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* WebKitGTK identifies as AppleWebKit on Linux. Chromium-based browsers on
|
|
73
|
+
* Linux and Android carry AppleWebKit too, so they are excluded by name.
|
|
74
|
+
*/
|
|
75
|
+
export function isWebKitGTK(userAgent: string): boolean {
|
|
76
|
+
return /\bLinux\b/.test(userAgent) && /AppleWebKit/.test(userAgent) && !/Chrome|Chromium|Android/.test(userAgent)
|
|
77
|
+
}
|