@skyhook-io/radar-app 1.13.4 → 1.14.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/package.json +8 -8
- package/src/App.tsx +60 -24
- package/src/RadarApp.tsx +5 -0
- package/src/api/client.authRedirect.test.ts +12 -1
- package/src/api/client.prometheusStatus.test.ts +24 -0
- package/src/api/client.ts +325 -16
- package/src/api/diagnose.ts +62 -15
- package/src/api/drain-plan.test.ts +86 -0
- package/src/api/quotas.test.ts +26 -0
- package/src/api/quotas.ts +20 -0
- package/src/api/workloadMetrics.test.ts +40 -0
- package/src/api/workloadMetrics.ts +68 -0
- package/src/components/CloudConnectFlow.tsx +179 -42
- package/src/components/CloudFunnelButton.tsx +107 -36
- package/src/components/ConnectionErrorView.test.tsx +30 -0
- package/src/components/ConnectionErrorView.tsx +31 -19
- package/src/components/SyncProgressPanel.tsx +73 -0
- package/src/components/audit/AuditView.tsx +2 -0
- package/src/components/cloudConnectHandoff.test.ts +140 -0
- package/src/components/cloudConnectHandoff.ts +124 -0
- package/src/components/diagnose/AgentCase.tsx +131 -0
- package/src/components/diagnose/DiagnoseContext.tsx +2 -1
- package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
- package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
- package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
- package/src/components/diagnose/InvestigationView.tsx +222 -5
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
- package/src/components/diagnose/investigationCase.test.tsx +1568 -0
- package/src/components/diagnose/investigationCase.ts +439 -0
- package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
- package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
- package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
- package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
- package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
- package/src/components/diagnose/investigationEvidence/index.ts +55 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
- package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
- package/src/components/diagnose/investigationEvidence/types.ts +512 -0
- package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
- package/src/components/diagnose/investigationMetrics.test.ts +712 -0
- package/src/components/diagnose/investigationMetrics.ts +393 -0
- package/src/components/diagnose/investigationSourceFocus.ts +2 -0
- package/src/components/diagnose/investigationState.test.ts +322 -0
- package/src/components/diagnose/investigationState.ts +147 -25
- package/src/components/diagnose/parts.test.tsx +482 -0
- package/src/components/diagnose/parts.tsx +418 -41
- package/src/components/diagnose/target.test.ts +19 -0
- package/src/components/diagnose/target.ts +3 -2
- package/src/components/gitops/GitOpsView.tsx +7 -0
- package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
- package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
- package/src/components/resource/PrometheusCharts.tsx +4 -3
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
- package/src/components/resource/RightsizingStrip.test.ts +38 -0
- package/src/components/resource/RightsizingStrip.tsx +15 -3
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
- package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
- package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
- package/src/components/resource/workloadMetricValues.test.ts +65 -0
- package/src/components/resource/workloadMetricValues.ts +30 -0
- package/src/components/resources/PodFilePreview.test.tsx +131 -0
- package/src/components/resources/PodFilePreview.tsx +394 -0
- package/src/components/resources/PodFilesystemModal.tsx +157 -67
- package/src/components/resources/ResourcesView.tsx +44 -6
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
- package/src/components/resources/renderers/PodRenderer.tsx +4 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
- package/src/components/settings/SettingsDialog.tsx +94 -14
- package/src/components/traffic/TrafficFlowList.tsx +229 -70
- package/src/components/traffic/TrafficGraph.tsx +6 -6
- package/src/components/traffic/TrafficView.tsx +10 -9
- package/src/components/traffic/trafficFilters.test.ts +28 -1
- package/src/components/traffic/trafficFilters.ts +31 -0
- package/src/components/workload/WorkloadView.tsx +19 -8
- package/src/context/ConnectionContext.tsx +48 -3
- package/src/context/DiagnoseCustomization.test.tsx +26 -0
- package/src/context/DiagnoseCustomization.tsx +15 -2
- package/src/index.css +17 -0
- package/src/index.ts +1 -0
- package/src/utils/navigation.ts +1 -1
- package/src/utils/shell-safe.test.ts +25 -1
- package/src/utils/shell-safe.ts +16 -0
- package/src/utils/topology-namespace.test.ts +64 -0
- package/src/utils/topology-namespace.ts +18 -0
- package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
- package/src/components/diagnose/investigationEvidence.ts +0 -4784
- package/src/components/gitops/GitOpsActions.tsx +0 -1
- package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
- package/src/components/gitops/ManagedResourcesList.tsx +0 -1
- package/src/components/gitops/SyncCountdown.tsx +0 -1
- package/src/components/gitops/index.ts +0 -4
- package/src/components/helm/ManifestDiffViewer.tsx +0 -69
- package/src/components/logs/JsonLogLine.tsx +0 -1
- package/src/components/logs/LogCore.tsx +0 -2
- package/src/components/logs/useLogBuffer.ts +0 -2
- package/src/components/logs/useLogSearch.ts +0 -1
- package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
- package/src/components/resources/drawer-components.tsx +0 -1
- package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
- package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/EventRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/JobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/VPARenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
- package/src/components/resources/renderers/argo-cells.tsx +0 -1
- package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
- package/src/components/resources/renderers/eso-cells.tsx +0 -1
- package/src/components/resources/renderers/flux-cells.tsx +0 -1
- package/src/components/resources/renderers/index.ts +0 -94
- package/src/components/resources/renderers/istio-cells.tsx +0 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
- package/src/components/resources/renderers/keda-cells.tsx +0 -1
- package/src/components/resources/renderers/knative-cells.tsx +0 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
- package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
- package/src/components/resources/renderers/traefik-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-shared.tsx +0 -1
- package/src/components/resources/renderers/velero-cells.tsx +0 -1
- package/src/components/resources/resource-utils-argo.ts +0 -2
- package/src/components/resources/resource-utils-certmanager.ts +0 -2
- package/src/components/resources/resource-utils-eso.ts +0 -2
- package/src/components/resources/resource-utils-flux.ts +0 -2
- package/src/components/resources/resource-utils-istio.ts +0 -2
- package/src/components/resources/resource-utils-karpenter.ts +0 -2
- package/src/components/resources/resource-utils-knative.ts +0 -2
- package/src/components/resources/resource-utils-kyverno.ts +0 -2
- package/src/components/resources/resource-utils-prometheus.ts +0 -2
- package/src/components/resources/resource-utils-traefik.ts +0 -1
- package/src/components/resources/resource-utils-trivy.ts +0 -2
- package/src/components/resources/resource-utils-velero.ts +0 -2
- package/src/components/shared/EditableYamlView.tsx +0 -24
- package/src/components/timeline/DiffViewer.tsx +0 -1
- package/src/components/traffic/index.ts +0 -3
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
- package/src/components/ui/MetricsChart.tsx +0 -1
- package/src/components/ui/ResourceBar.tsx +0 -1
|
@@ -23,7 +23,7 @@ import { isClusterAddon, type AddonMode } from './TrafficView'
|
|
|
23
23
|
import { SEVERITY_BADGE, SEVERITY_DOT, SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
|
|
24
24
|
import { getNamespaceColor } from '../../utils/traffic-colors'
|
|
25
25
|
import { Tooltip } from '../ui/Tooltip'
|
|
26
|
-
import { isRateBasedSource } from './trafficFilters'
|
|
26
|
+
import { isRateBasedSource, isExternalKind } from './trafficFilters'
|
|
27
27
|
|
|
28
28
|
const elk = new ELK()
|
|
29
29
|
|
|
@@ -172,7 +172,7 @@ const MAX_VISIBLE_PORTS = 4 // Maximum ports to show before "+N more"
|
|
|
172
172
|
|
|
173
173
|
// Custom node component
|
|
174
174
|
function TrafficNode({ data }: { data: TrafficNodeData }) {
|
|
175
|
-
const isExternal = data.kind
|
|
175
|
+
const isExternal = isExternalKind(data.kind)
|
|
176
176
|
const isInternet = data.kind === 'Internet'
|
|
177
177
|
const isAddonInternet = data.kind === 'AddonInternet' // Separate internet for addon traffic
|
|
178
178
|
const isAddon = data.kind === 'Addon'
|
|
@@ -515,7 +515,7 @@ function DetailsPanel({
|
|
|
515
515
|
<Globe className="h-4 w-4 text-sky-400" />
|
|
516
516
|
) : nodeData?.kind === 'Addon' ? (
|
|
517
517
|
<Server className="h-4 w-4 text-purple-400" />
|
|
518
|
-
) : nodeData
|
|
518
|
+
) : nodeData && isExternalKind(nodeData.kind) ? (
|
|
519
519
|
<Globe className="h-4 w-4 text-yellow-500" />
|
|
520
520
|
) : (
|
|
521
521
|
<Server className="h-4 w-4 text-blue-500" />
|
|
@@ -554,7 +554,7 @@ function DetailsPanel({
|
|
|
554
554
|
? 'bg-sky-500/20 text-sky-400'
|
|
555
555
|
: nodeData.kind === 'Addon'
|
|
556
556
|
? 'bg-purple-500/20 text-purple-400'
|
|
557
|
-
: nodeData.kind
|
|
557
|
+
: isExternalKind(nodeData.kind)
|
|
558
558
|
? 'bg-yellow-500/20 text-yellow-400'
|
|
559
559
|
: 'bg-blue-500/20 text-blue-400'
|
|
560
560
|
)}>{nodeData.kind === 'Addon' ? 'Cluster Addons' : nodeData.kind}</span>
|
|
@@ -1192,7 +1192,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1192
1192
|
namespaceColor: showNamespaceGroups && !sourceIsAddon ? getNamespaceColor(flow.source.namespace) : undefined,
|
|
1193
1193
|
isHotPath: hotNodes.has(sourceId),
|
|
1194
1194
|
isAddonNode: sourceIsAddon, // AddonInternet is NOT an addon node
|
|
1195
|
-
serviceCategory: flow.source.kind
|
|
1195
|
+
serviceCategory: isExternalKind(flow.source.kind) ? serviceCategories?.get(flow.source.name) : undefined,
|
|
1196
1196
|
nodeHeight: NODE_BASE_HEIGHT,
|
|
1197
1197
|
connLabel,
|
|
1198
1198
|
},
|
|
@@ -1254,7 +1254,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1254
1254
|
namespaceColor: showNamespaceGroups && !destIsAddon ? getNamespaceColor(flow.destination.namespace) : undefined,
|
|
1255
1255
|
isHotPath: hotNodes.has(destId),
|
|
1256
1256
|
isAddonNode: destIsAddon,
|
|
1257
|
-
serviceCategory: flow.destination.kind
|
|
1257
|
+
serviceCategory: isExternalKind(flow.destination.kind) ? serviceCategories?.get(flow.destination.name) : undefined,
|
|
1258
1258
|
ports: destPorts,
|
|
1259
1259
|
nodeHeight: getNodeHeight(destPorts),
|
|
1260
1260
|
connLabel,
|
|
@@ -13,7 +13,7 @@ import { useDock } from '../dock'
|
|
|
13
13
|
import { AlertBanner, EmptyState, PaneLoader, FreshnessControl } from '@skyhook-io/k8s-ui'
|
|
14
14
|
import { useConnection } from '../../context/ConnectionContext'
|
|
15
15
|
import { Tooltip } from '../ui/Tooltip'
|
|
16
|
-
import { matchesStatusRanges, bucketsFromCounts, bucketsFromStatus, isRateBasedSource, keepAvailable, effectiveThreshold, volumeUnit, type VolumeUnit } from './trafficFilters'
|
|
16
|
+
import { matchesStatusRanges, bucketsFromCounts, bucketsFromStatus, isRateBasedSource, keepAvailable, effectiveThreshold, volumeUnit, type VolumeUnit, isExternalKind } from './trafficFilters'
|
|
17
17
|
|
|
18
18
|
// Addon types for filtering
|
|
19
19
|
export type AddonMode = 'show' | 'group' | 'hide'
|
|
@@ -271,8 +271,12 @@ function isSystemEndpoint(name: string, namespace: string | undefined, kind: str
|
|
|
271
271
|
return true
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
// Cilium reserved identities
|
|
275
|
-
|
|
274
|
+
// Cilium reserved identities: nodes and the host network arrive as Host;
|
|
275
|
+
// health, init and unmanaged endpoints carry no usable identity.
|
|
276
|
+
if (kind === 'Host') {
|
|
277
|
+
return true
|
|
278
|
+
}
|
|
279
|
+
if ((kind === 'External' || kind === 'Unknown') && CILIUM_RESERVED_IDENTITIES.has(name)) {
|
|
276
280
|
return true
|
|
277
281
|
}
|
|
278
282
|
|
|
@@ -330,10 +334,7 @@ function isSystemEndpoint(name: string, namespace: string | undefined, kind: str
|
|
|
330
334
|
return false
|
|
331
335
|
}
|
|
332
336
|
|
|
333
|
-
|
|
334
|
-
function isExternal(kind: string): boolean {
|
|
335
|
-
return kind.toLowerCase() === 'external'
|
|
336
|
-
}
|
|
337
|
+
const isExternal = isExternalKind
|
|
337
338
|
|
|
338
339
|
interface TrafficViewProps {
|
|
339
340
|
namespaces: string[]
|
|
@@ -988,14 +989,14 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
|
|
|
988
989
|
|
|
989
990
|
flows.forEach(flow => {
|
|
990
991
|
// Count source nodes
|
|
991
|
-
if (flow.source.namespace && flow.source.kind
|
|
992
|
+
if (flow.source.namespace && !isExternalKind(flow.source.kind)) {
|
|
992
993
|
if (!nsCounts.has(flow.source.namespace)) {
|
|
993
994
|
nsCounts.set(flow.source.namespace, new Set())
|
|
994
995
|
}
|
|
995
996
|
nsCounts.get(flow.source.namespace)!.add(flow.source.name)
|
|
996
997
|
}
|
|
997
998
|
// Count destination nodes
|
|
998
|
-
if (flow.destination.namespace && flow.destination.kind
|
|
999
|
+
if (flow.destination.namespace && !isExternalKind(flow.destination.kind)) {
|
|
999
1000
|
if (!nsCounts.has(flow.destination.namespace)) {
|
|
1000
1001
|
nsCounts.set(flow.destination.namespace, new Set())
|
|
1001
1002
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { matchesStatusRanges, bucketsFromCounts, bucketsFromStatus, isRateBasedSource, keepAvailable, effectiveThreshold, volumeUnit } from './trafficFilters'
|
|
2
|
+
import { matchesStatusRanges, bucketsFromCounts, bucketsFromStatus, isRateBasedSource, keepAvailable, effectiveThreshold, volumeUnit, isExternalKind, isPolicyDropReason } from './trafficFilters'
|
|
3
3
|
|
|
4
4
|
describe('matchesStatusRanges', () => {
|
|
5
5
|
it('does not filter when nothing is selected', () => {
|
|
@@ -101,3 +101,30 @@ describe('volumeUnit', () => {
|
|
|
101
101
|
expect(volumeUnit(undefined)).toBe('connections')
|
|
102
102
|
})
|
|
103
103
|
})
|
|
104
|
+
|
|
105
|
+
describe('isExternalKind', () => {
|
|
106
|
+
it('places the world, nodes and unidentified endpoints outside the workloads', () => {
|
|
107
|
+
expect(isExternalKind('External')).toBe(true)
|
|
108
|
+
expect(isExternalKind('Host')).toBe(true)
|
|
109
|
+
expect(isExternalKind('Unknown')).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
it('keeps pods and services inside', () => {
|
|
112
|
+
expect(isExternalKind('Pod')).toBe(false)
|
|
113
|
+
expect(isExternalKind('Service')).toBe(false)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('isPolicyDropReason', () => {
|
|
118
|
+
it('recognises both of Hubble\'s policy drop codes', () => {
|
|
119
|
+
expect(isPolicyDropReason('POLICY_DENIED', 0)).toBe(true)
|
|
120
|
+
expect(isPolicyDropReason('POLICY_DENY', 0)).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
it('takes the plugin naming a policy as evidence even without a code', () => {
|
|
123
|
+
expect(isPolicyDropReason(undefined, 1)).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
it('does not assume policy for other or missing reasons', () => {
|
|
126
|
+
expect(isPolicyDropReason('STALE_OR_UNROUTABLE_IP', 0)).toBe(false)
|
|
127
|
+
expect(isPolicyDropReason(undefined, 0)).toBe(false)
|
|
128
|
+
expect(isPolicyDropReason('', 0)).toBe(false)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -115,3 +115,34 @@ export function volumeUnit(isRateBased: boolean | undefined): VolumeUnit {
|
|
|
115
115
|
export function effectiveThreshold(value: number, chosenUnit: VolumeUnit, currentUnit: VolumeUnit): number {
|
|
116
116
|
return chosenUnit === currentUnit ? value : 0
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Is this endpoint kind drawn outside the cluster's workloads?
|
|
121
|
+
*
|
|
122
|
+
* "External" is what every source reports for the world outside; Hubble also
|
|
123
|
+
* tells nodes (`Host`) and endpoints it could not identify (`Unknown`) apart,
|
|
124
|
+
* because a policy evaluation treats each differently. The graph does not:
|
|
125
|
+
* none of them is a workload, so they take the external node's place and
|
|
126
|
+
* styling, and stay out of namespace counts.
|
|
127
|
+
*/
|
|
128
|
+
export function isExternalKind(kind: string): boolean {
|
|
129
|
+
const k = kind.toLowerCase()
|
|
130
|
+
return k === 'external' || k === 'host' || k === 'unknown'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Is a drop a NetworkPolicy question at all?
|
|
135
|
+
*
|
|
136
|
+
* Only on positive evidence: Hubble's two policy drop reasons — `POLICY_DENIED`
|
|
137
|
+
* (no rule allowed it) and `POLICY_DENY` (an explicit deny rule) — or the
|
|
138
|
+
* plugin naming the policy itself. A drop with no reported reason is not
|
|
139
|
+
* assumed to be one. The two codes are spelled differently enough that a
|
|
140
|
+
* substring test on either misses the other.
|
|
141
|
+
*/
|
|
142
|
+
export function isPolicyDropReason(dropReasonDesc: string | undefined, deniedByCount: number): boolean {
|
|
143
|
+
// deniedByCount includes references withheld for permissions: the plugin
|
|
144
|
+
// still named a policy.
|
|
145
|
+
if (deniedByCount > 0) return true
|
|
146
|
+
const code = (dropReasonDesc ?? '').toUpperCase()
|
|
147
|
+
return code === 'POLICY_DENIED' || code === 'POLICY_DENY'
|
|
148
|
+
}
|
|
@@ -70,6 +70,8 @@ import {
|
|
|
70
70
|
useCordonNode,
|
|
71
71
|
useUncordonNode,
|
|
72
72
|
useDrainNode,
|
|
73
|
+
useDrainPlan,
|
|
74
|
+
DrainPlanUnsupportedError,
|
|
73
75
|
useCascadeDeletePreview,
|
|
74
76
|
useResourceEvents,
|
|
75
77
|
useResource,
|
|
@@ -81,7 +83,7 @@ import {
|
|
|
81
83
|
import { PrometheusCharts, isPrometheusSupported } from '../resource/PrometheusCharts'
|
|
82
84
|
import { PrometheusChartsGrid } from '../resource/PrometheusChartsGrid'
|
|
83
85
|
import { RestartEventLane } from '../resource/RestartChart'
|
|
84
|
-
import { RightsizingPanel
|
|
86
|
+
import { RightsizingPanel } from '../resource/RightsizingStrip'
|
|
85
87
|
import { WorkloadCostTab } from '../cost/WorkloadCostTab'
|
|
86
88
|
import { isOpenCostWorkloadKind } from '../cost/kinds'
|
|
87
89
|
import { useResourceAudit, useResourceIssues, useResources, useTrace, fetchTraceWithProbes, fetchInClusterCapability, runInClusterMerged } from '../../api/client'
|
|
@@ -381,6 +383,7 @@ function useActionsBarProps(
|
|
|
381
383
|
const cordonMutation = useCordonNode()
|
|
382
384
|
const uncordonMutation = useUncordonNode()
|
|
383
385
|
const drainMutation = useDrainNode()
|
|
386
|
+
const drainPlanMutation = useDrainPlan()
|
|
384
387
|
|
|
385
388
|
const { renderAction: renderDiagnose } = useDiagnoseCustomization()
|
|
386
389
|
|
|
@@ -485,6 +488,13 @@ function useActionsBarProps(
|
|
|
485
488
|
onDrainNode: (params: Parameters<typeof drainMutation.mutate>[0]) =>
|
|
486
489
|
drainMutation.mutate(params),
|
|
487
490
|
isDrainingNode: drainMutation.isPending,
|
|
491
|
+
onPlanDrain: (params: Parameters<typeof drainPlanMutation.mutate>[0]) =>
|
|
492
|
+
drainPlanMutation.mutate(params),
|
|
493
|
+
onPlanDrainReset: () => drainPlanMutation.reset(),
|
|
494
|
+
drainPlan: drainPlanMutation.data ?? null,
|
|
495
|
+
isPlanningDrain: drainPlanMutation.isPending,
|
|
496
|
+
drainPlanError: drainPlanMutation.error?.message ?? null,
|
|
497
|
+
drainPlanUnsupported: drainPlanMutation.error instanceof DrainPlanUnsupportedError,
|
|
488
498
|
}
|
|
489
499
|
}
|
|
490
500
|
|
|
@@ -2411,16 +2421,9 @@ function MetricsTabContent({
|
|
|
2411
2421
|
resource: any
|
|
2412
2422
|
expanded: boolean
|
|
2413
2423
|
}) {
|
|
2414
|
-
const showRightsizing = expanded && ['Deployment', 'StatefulSet', 'DaemonSet'].includes(kind)
|
|
2415
|
-
|
|
2416
2424
|
if (expanded) {
|
|
2417
2425
|
return (
|
|
2418
2426
|
<div className="flex flex-col h-full">
|
|
2419
|
-
{showRightsizing && (
|
|
2420
|
-
<div className="px-4 pt-4">
|
|
2421
|
-
<RightsizingStrip kind={kind} namespace={namespace} name={name} />
|
|
2422
|
-
</div>
|
|
2423
|
-
)}
|
|
2424
2427
|
<div className="flex-1 min-h-0">
|
|
2425
2428
|
<PrometheusChartsGrid kind={kind} namespace={namespace} name={name} resource={resource} />
|
|
2426
2429
|
</div>
|
|
@@ -2446,9 +2449,17 @@ function DrawerMetricsContent({
|
|
|
2446
2449
|
}) {
|
|
2447
2450
|
const [chartRange, setChartRange] = useState<import('../../api/client').PrometheusTimeRange>('1h')
|
|
2448
2451
|
const showRestartLane = isPrometheusSupported(kind) && kind !== 'Node'
|
|
2452
|
+
const navigate = useNavigate()
|
|
2449
2453
|
|
|
2450
2454
|
return (
|
|
2451
2455
|
<div className="flex flex-col h-full">
|
|
2456
|
+
{['Deployment', 'StatefulSet', 'DaemonSet'].includes(kind) && (
|
|
2457
|
+
<div className="px-4 pt-3">
|
|
2458
|
+
<button className="text-xs text-accent hover:underline" onClick={() => navigate(`${buildWorkloadPath({ kind, namespace, name })}?tab=metrics&metricsRange=${chartRange}`)}>
|
|
2459
|
+
Open request and resource dashboard →
|
|
2460
|
+
</button>
|
|
2461
|
+
</div>
|
|
2462
|
+
)}
|
|
2452
2463
|
<div className="flex-1 min-h-0">
|
|
2453
2464
|
<PrometheusCharts
|
|
2454
2465
|
kind={kind}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createContext, useContext, useState, useCallback, useEffect, useRef, ReactNode } from 'react'
|
|
2
2
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
3
|
import { getApiBase } from '../api/config'
|
|
4
|
-
import { apiFetch } from '../api/client'
|
|
4
|
+
import { apiFetch, type SyncStatusSnapshot } from '../api/client'
|
|
5
5
|
|
|
6
6
|
export type ConnectionStateType = 'connected' | 'disconnected' | 'connecting'
|
|
7
7
|
|
|
@@ -12,6 +12,10 @@ export interface ConnectionState {
|
|
|
12
12
|
error?: string
|
|
13
13
|
errorType?: string // config, auth, auth-rejected, auth-plugin-stuck, rbac, network, timeout, tls, unknown
|
|
14
14
|
progressMessage?: string
|
|
15
|
+
// Per-kind readiness while the initial informer sync runs ('connecting'
|
|
16
|
+
// with informers started). Drives the progressive app shell — absent once
|
|
17
|
+
// connected or before informers exist.
|
|
18
|
+
syncStatus?: SyncStatusSnapshot
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
interface ConnectionStatusResponse extends ConnectionState {
|
|
@@ -157,7 +161,15 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
157
161
|
if (!firstConnect || cacheWarmAtMountRef.current) {
|
|
158
162
|
queryClient.invalidateQueries()
|
|
159
163
|
} else {
|
|
160
|
-
|
|
164
|
+
// Detail responses served during the progressive shell are partial —
|
|
165
|
+
// the server skips relationship computation while the topology cache
|
|
166
|
+
// is unavailable — and they resolve as successes, so the error-only
|
|
167
|
+
// sweep would leave an open drawer without its Related Resources
|
|
168
|
+
// forever (focus refetch is globally off). Refetching the resource
|
|
169
|
+
// detail keys is at most the open drawer, so it stays cheap.
|
|
170
|
+
queryClient.invalidateQueries({
|
|
171
|
+
predicate: (q) => q.state.status === 'error' || q.queryKey[0] === 'resource',
|
|
172
|
+
})
|
|
161
173
|
}
|
|
162
174
|
}, [queryClient])
|
|
163
175
|
|
|
@@ -195,12 +207,35 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
195
207
|
// while the retry is still running; the retry's own result supersedes it.
|
|
196
208
|
if (manualRetryPendingRef.current || autoRetryInFlightRef.current) return
|
|
197
209
|
const current = connectionRef.current
|
|
210
|
+
// The progressive shell renders real resource lists while still
|
|
211
|
+
// 'connecting'. A query client shared across cluster mounts may hold
|
|
212
|
+
// another cluster's data under identical keys — drop it before the
|
|
213
|
+
// shell can show it (the 'connected' path repeats this for the
|
|
214
|
+
// full-refresh case). Deliberately ahead of the SSE-generation guard:
|
|
215
|
+
// live connection_state frames bump the generation and drop the poll,
|
|
216
|
+
// and the shell must not spend the whole sync on another cluster's rows.
|
|
217
|
+
if (current.state === 'connecting' && data.state === 'connecting' &&
|
|
218
|
+
data.syncStatus && cacheWarmAtMountRef.current) {
|
|
219
|
+
cacheWarmAtMountRef.current = false
|
|
220
|
+
// Drop only cluster-scoped data. App chrome (auth identity, kubeconfig
|
|
221
|
+
// contexts) is not another cluster's data, and purging it flips
|
|
222
|
+
// authMePending back on — which holds the whole shell on the splash
|
|
223
|
+
// until /auth/me refetches, delaying the first progressive paint.
|
|
224
|
+
const keep = new Set(['connection-status', 'auth-me', 'contexts'])
|
|
225
|
+
queryClient.removeQueries({ predicate: (q) => !keep.has(String(q.queryKey[0])) })
|
|
226
|
+
}
|
|
198
227
|
if (!shouldApplyPolledConnection(
|
|
199
228
|
current.state,
|
|
200
229
|
data.state,
|
|
201
230
|
data.sseGenerationAtStart,
|
|
202
231
|
sseGenerationRef.current,
|
|
203
232
|
)) {
|
|
233
|
+
// SSE owns the state fields, but connection_state frames don't carry
|
|
234
|
+
// per-kind sync progress — merge it from the poll so the progressive
|
|
235
|
+
// shell keeps ticking while still connecting.
|
|
236
|
+
if (data.state === 'connecting' && data.syncStatus) {
|
|
237
|
+
setConnection(prev => prev.state === 'connecting' ? { ...prev, syncStatus: data.syncStatus } : prev)
|
|
238
|
+
}
|
|
204
239
|
return
|
|
205
240
|
}
|
|
206
241
|
const becameConnected = current.state !== 'connected' && data.state === 'connected'
|
|
@@ -211,11 +246,12 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
211
246
|
error: data.error,
|
|
212
247
|
errorType: data.errorType,
|
|
213
248
|
progressMessage: data.progressMessage,
|
|
249
|
+
syncStatus: data.syncStatus,
|
|
214
250
|
})
|
|
215
251
|
if (becameConnected) {
|
|
216
252
|
refreshCachesOnConnect()
|
|
217
253
|
}
|
|
218
|
-
}, [data, dataUpdatedAt, refreshCachesOnConnect])
|
|
254
|
+
}, [data, dataUpdatedAt, refreshCachesOnConnect, queryClient])
|
|
219
255
|
|
|
220
256
|
// Retry mutation
|
|
221
257
|
const retryMutation = useMutation({
|
|
@@ -231,6 +267,9 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
231
267
|
state: 'connecting',
|
|
232
268
|
error: undefined,
|
|
233
269
|
progressMessage: 'Connecting to cluster...',
|
|
270
|
+
// A retry is a fresh attempt — per-kind progress from the previous
|
|
271
|
+
// attempt would mislabel readiness until the next poll.
|
|
272
|
+
syncStatus: undefined,
|
|
234
273
|
}))
|
|
235
274
|
},
|
|
236
275
|
onSuccess: (result) => {
|
|
@@ -371,6 +410,12 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
371
410
|
if (prev.state === 'connected' && status.state === 'connecting') {
|
|
372
411
|
return prev
|
|
373
412
|
}
|
|
413
|
+
// connection_state frames don't carry per-kind sync progress — keep the
|
|
414
|
+
// last polled snapshot while still connecting, or each SSE progress
|
|
415
|
+
// frame would flicker the progressive shell back to the splash.
|
|
416
|
+
if (status.state === 'connecting' && !status.syncStatus && prev.state === 'connecting') {
|
|
417
|
+
return { ...status, syncStatus: prev.syncStatus }
|
|
418
|
+
}
|
|
374
419
|
return status
|
|
375
420
|
})
|
|
376
421
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
+
import { DiagnoseCustomizationProvider, useDiagnoseCustomization } from './DiagnoseCustomization';
|
|
4
|
+
import type { RunSummary } from '../api/diagnose';
|
|
5
|
+
|
|
6
|
+
const run = { id: 'r1' } as RunSummary;
|
|
7
|
+
const onRunUpdated = vi.fn();
|
|
8
|
+
function Actions() {
|
|
9
|
+
const { renderRunActions } = useDiagnoseCustomization();
|
|
10
|
+
return <>{renderRunActions?.({ run, onRunUpdated })}</>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('investigation action slot', () => {
|
|
14
|
+
it('has no default host actions in OSS', () => {
|
|
15
|
+
expect(renderToStaticMarkup(<Actions />)).toBe('');
|
|
16
|
+
});
|
|
17
|
+
it('passes the run and update callback to the host', () => {
|
|
18
|
+
const renderRunActions = vi.fn(() => <button>Host action</button>);
|
|
19
|
+
expect(renderToStaticMarkup(
|
|
20
|
+
<DiagnoseCustomizationProvider value={undefined} renderRunActions={renderRunActions}>
|
|
21
|
+
<Actions />
|
|
22
|
+
</DiagnoseCustomizationProvider>,
|
|
23
|
+
)).toContain('Host action');
|
|
24
|
+
expect(renderRunActions).toHaveBeenCalledWith({ run, onRunUpdated });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -11,9 +11,19 @@
|
|
|
11
11
|
// agent-free.
|
|
12
12
|
import { createContext, useContext, useMemo } from "react";
|
|
13
13
|
import type { ReactNode } from "react";
|
|
14
|
+
import type { RunSummary } from "../api/diagnose";
|
|
15
|
+
|
|
16
|
+
/** Optional host controls beside shared run actions. Return a component element
|
|
17
|
+
* if hooks are needed: this callback is invoked conditionally. Call onRunUpdated
|
|
18
|
+
* after a mutation to refresh the shared run summary. */
|
|
19
|
+
export type RenderInvestigationRunActions = (props: {
|
|
20
|
+
run: RunSummary;
|
|
21
|
+
onRunUpdated: (run: RunSummary) => void;
|
|
22
|
+
}) => ReactNode;
|
|
14
23
|
|
|
15
24
|
/** Render prop for the resource-level Investigate action. */
|
|
16
25
|
export type RenderDiagnoseAction = (ctx: {
|
|
26
|
+
/** Kubernetes Kind when loaded or discovered; otherwise its API resource name. */
|
|
17
27
|
kind: string;
|
|
18
28
|
/** Kubernetes API group; empty means core. */
|
|
19
29
|
group?: string;
|
|
@@ -52,6 +62,7 @@ export type DiagnoseConsentCopy = {
|
|
|
52
62
|
// set once at mount, so per-value re-render isolation buys nothing.
|
|
53
63
|
export interface DiagnoseCustomization {
|
|
54
64
|
renderAction: RenderDiagnoseAction | undefined;
|
|
65
|
+
renderRunActions?: RenderInvestigationRunActions;
|
|
55
66
|
consentCopy: DiagnoseConsentCopy | undefined;
|
|
56
67
|
// undefined = default (CustomEvent → Radar's own Settings dialog);
|
|
57
68
|
// null = hide the settings affordances.
|
|
@@ -70,19 +81,21 @@ const DiagnoseCustomizationContext =
|
|
|
70
81
|
export function DiagnoseCustomizationProvider({
|
|
71
82
|
value,
|
|
72
83
|
consentCopy,
|
|
84
|
+
renderRunActions,
|
|
73
85
|
onOpenSettings,
|
|
74
86
|
children,
|
|
75
87
|
}: {
|
|
76
88
|
value: RenderDiagnoseAction | undefined;
|
|
77
89
|
consentCopy?: DiagnoseConsentCopy;
|
|
90
|
+
renderRunActions?: RenderInvestigationRunActions;
|
|
78
91
|
/** Where "AI settings" affordances lead. Omit for Radar's own Settings
|
|
79
92
|
* dialog; pass `null` to hide them. */
|
|
80
93
|
onOpenSettings?: (() => void) | null;
|
|
81
94
|
children: ReactNode;
|
|
82
95
|
}) {
|
|
83
96
|
const ctx = useMemo(
|
|
84
|
-
() => ({ renderAction: value, consentCopy, onOpenSettings }),
|
|
85
|
-
[value, consentCopy, onOpenSettings],
|
|
97
|
+
() => ({ renderAction: value, consentCopy, onOpenSettings, renderRunActions }),
|
|
98
|
+
[value, consentCopy, onOpenSettings, renderRunActions],
|
|
86
99
|
);
|
|
87
100
|
return (
|
|
88
101
|
<DiagnoseCustomizationContext.Provider value={ctx}>
|
package/src/index.css
CHANGED
|
@@ -21,6 +21,23 @@
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
@layer components {
|
|
24
|
+
.metrics-layout {
|
|
25
|
+
container-type: inline-size;
|
|
26
|
+
container-name: metrics;
|
|
27
|
+
}
|
|
28
|
+
.metrics-chart-grid {
|
|
29
|
+
display: grid;
|
|
30
|
+
grid-template-columns: minmax(0, 1fr);
|
|
31
|
+
gap: 0.75rem;
|
|
32
|
+
}
|
|
33
|
+
@container metrics (min-width: 760px) {
|
|
34
|
+
.metrics-chart-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
35
|
+
.metrics-chart-wide { grid-column: 1 / -1; }
|
|
36
|
+
}
|
|
37
|
+
@container metrics (min-width: 1200px) {
|
|
38
|
+
.metrics-chart-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
39
|
+
.metrics-chart-wide { grid-column: auto; }
|
|
40
|
+
}
|
|
24
41
|
.investigation-assessment {
|
|
25
42
|
background: var(--color-investigation-assessment);
|
|
26
43
|
border-color: var(--color-investigation-assessment-border);
|
package/src/index.ts
CHANGED
package/src/utils/navigation.ts
CHANGED
|
@@ -14,7 +14,7 @@ export function searchHitToSelectedResource(hit: SearchHit): SelectedResource {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Re-export shared navigation utilities from @skyhook-io/k8s-ui.
|
|
17
|
-
export { kindToPlural, kindToPluralWithGroup, pluralToKind, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
17
|
+
export { kindToPlural, kindToPluralWithGroup, pluralToKind, knownKindForPluralWithGroup, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
18
18
|
export type { NavigateToResource } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
19
19
|
|
|
20
20
|
const NETWORK_POLICY_TOPOLOGY_KINDS = new Set([
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import { allShellSafe, isShellSafeValue } from './shell-safe'
|
|
3
|
+
import { allShellSafe, isShellSafeAWSProfile, isShellSafeValue } from './shell-safe'
|
|
4
4
|
|
|
5
5
|
describe('isShellSafeValue', () => {
|
|
6
6
|
it('accepts the values real provider context names produce', () => {
|
|
@@ -53,3 +53,27 @@ describe('allShellSafe', () => {
|
|
|
53
53
|
expect(allShellSafe('prod', null)).toBe(false)
|
|
54
54
|
})
|
|
55
55
|
})
|
|
56
|
+
|
|
57
|
+
describe('isShellSafeAWSProfile', () => {
|
|
58
|
+
it('accepts simple and slash-separated profile names', () => {
|
|
59
|
+
expect(isShellSafeAWSProfile('default')).toBe(true)
|
|
60
|
+
expect(isShellSafeAWSProfile('myorg/my-account/my-role')).toBe(true)
|
|
61
|
+
expect(isShellSafeAWSProfile('prod-account')).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('rejects shell metacharacters', () => {
|
|
65
|
+
expect(isShellSafeAWSProfile('prod; id')).toBe(false)
|
|
66
|
+
expect(isShellSafeAWSProfile('prod$(id)')).toBe(false)
|
|
67
|
+
expect(isShellSafeAWSProfile('prod profile')).toBe(false)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('rejects leading dash', () => {
|
|
71
|
+
expect(isShellSafeAWSProfile('-rf')).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('rejects empty and absent values', () => {
|
|
75
|
+
expect(isShellSafeAWSProfile('')).toBe(false)
|
|
76
|
+
expect(isShellSafeAWSProfile(null)).toBe(false)
|
|
77
|
+
expect(isShellSafeAWSProfile(undefined)).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
})
|
package/src/utils/shell-safe.ts
CHANGED
|
@@ -19,3 +19,19 @@ export function isShellSafeValue(value: string | null | undefined): value is str
|
|
|
19
19
|
export function allShellSafe(...values: (string | null | undefined)[]): boolean {
|
|
20
20
|
return values.every(isShellSafeValue)
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
// AWS SSO profile names may contain `/` as a path separator
|
|
24
|
+
// (e.g. "myorg/my-account/my-role") — safe in shell arguments since `/`
|
|
25
|
+
// has no special meaning to the shell itself.
|
|
26
|
+
const SHELL_SAFE_AWS_PROFILE = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/
|
|
27
|
+
|
|
28
|
+
export function isShellSafeAWSProfile(value: string | null | undefined): value is string {
|
|
29
|
+
return typeof value === 'string' && SHELL_SAFE_AWS_PROFILE.test(value)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Returns ` --profile <name>` for embedding in an aws CLI hint, or '' when no
|
|
33
|
+
// profile is pinned or the name fails the allowlist — the hint then falls
|
|
34
|
+
// back to the ambient profile rather than offering nothing.
|
|
35
|
+
export function awsProfileFlag(profile: string | null | undefined): string {
|
|
36
|
+
return isShellSafeAWSProfile(profile) ? ` --profile ${profile}` : ''
|
|
37
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { TopologyNode } from '@skyhook-io/k8s-ui/types/core'
|
|
3
|
+
import { isInNamespaceScope, scopeNodesToNamespaces } from './topology-namespace'
|
|
4
|
+
|
|
5
|
+
function node(id: string, kind: string, namespace?: string): TopologyNode {
|
|
6
|
+
return {
|
|
7
|
+
id,
|
|
8
|
+
kind,
|
|
9
|
+
name: id,
|
|
10
|
+
status: 'healthy',
|
|
11
|
+
data: namespace === undefined ? {} : { namespace },
|
|
12
|
+
} as TopologyNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const nodes = [
|
|
16
|
+
node('web', 'Deployment', 'prod'),
|
|
17
|
+
node('ing', 'Ingress', 'staging'),
|
|
18
|
+
node('db', 'StatefulSet', 'prod'),
|
|
19
|
+
node('node-1', 'Node'), // cluster-scoped
|
|
20
|
+
node('pv-1', 'PersistentVolume'), // cluster-scoped
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
describe('scopeNodesToNamespaces', () => {
|
|
24
|
+
it('returns every node when no namespace is selected', () => {
|
|
25
|
+
expect(scopeNodesToNamespaces(nodes, [])).toHaveLength(5)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('keeps only the selected namespace, plus cluster-scoped nodes', () => {
|
|
29
|
+
const scoped = scopeNodesToNamespaces(nodes, ['prod'])
|
|
30
|
+
expect(scoped.map(n => n.id)).toEqual(['web', 'db', 'node-1', 'pv-1'])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('drops a kind that exists only outside the scope', () => {
|
|
34
|
+
const scoped = scopeNodesToNamespaces(nodes, ['prod'])
|
|
35
|
+
expect(scoped.some(n => n.kind === 'Ingress')).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('accepts several namespaces at once', () => {
|
|
39
|
+
const scoped = scopeNodesToNamespaces(nodes, ['prod', 'staging'])
|
|
40
|
+
expect(scoped).toHaveLength(5)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns the same array identity when the scope is empty', () => {
|
|
44
|
+
expect(scopeNodesToNamespaces(nodes, [])).toBe(nodes)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('isInNamespaceScope', () => {
|
|
49
|
+
it('passes everything for a null scope', () => {
|
|
50
|
+
expect(isInNamespaceScope(node('x', 'Pod', 'any'), null)).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('always passes a node with no namespace', () => {
|
|
54
|
+
expect(isInNamespaceScope(node('node-1', 'Node'), new Set(['prod']))).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('treats an empty-string namespace as cluster-scoped', () => {
|
|
58
|
+
expect(isInNamespaceScope(node('n', 'Node', ''), new Set(['prod']))).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('rejects a namespaced node outside the scope', () => {
|
|
62
|
+
expect(isInNamespaceScope(node('ing', 'Ingress', 'staging'), new Set(['prod']))).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TopologyNode } from '@skyhook-io/k8s-ui/types/core'
|
|
2
|
+
|
|
3
|
+
// A node with no namespace is cluster-scoped (Node, PersistentVolume,
|
|
4
|
+
// Namespace, ...) and stays visible in every scope.
|
|
5
|
+
export function isInNamespaceScope(node: TopologyNode, nsSet: Set<string> | null): boolean {
|
|
6
|
+
if (!nsSet) return true
|
|
7
|
+
const ns = node.data?.namespace as string | undefined
|
|
8
|
+
return !ns || nsSet.has(ns)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Shared by the topology graph and the filter sidebar so the two cannot
|
|
12
|
+
// disagree about which nodes are in scope. Namespace carve-out only — kind
|
|
13
|
+
// filtering stays on the graph.
|
|
14
|
+
export function scopeNodesToNamespaces(nodes: TopologyNode[], namespaces: string[]): TopologyNode[] {
|
|
15
|
+
if (namespaces.length === 0) return nodes
|
|
16
|
+
const nsSet = new Set(namespaces)
|
|
17
|
+
return nodes.filter(node => isInNamespaceScope(node, nsSet))
|
|
18
|
+
}
|