@skyhook-io/radar-app 1.13.5 → 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/api/client.authRedirect.test.ts +12 -1
- package/src/api/client.prometheusStatus.test.ts +24 -0
- package/src/api/client.ts +321 -16
- package/src/api/diagnose.ts +1 -0
- 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/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/DiagnoseContext.tsx +2 -1
- 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/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 +75 -35
- 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/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.tsx +1 -0
- package/src/index.css +17 -0
- package/src/utils/navigation.ts +1 -1
- 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 -6617
- package/src/components/diagnose/investigationEvidence.ts +0 -5572
- 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
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useState, useMemo } from 'react'
|
|
2
2
|
import { Virtuoso } from 'react-virtuoso'
|
|
3
|
-
import type { TrafficFlow } from '../../types'
|
|
3
|
+
import type { TrafficFlow, TrafficEndpoint } from '../../types'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import { ChevronDown, ChevronUp, ShieldCheck } from 'lucide-react'
|
|
6
6
|
import { SEVERITY_BADGE, SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
|
|
7
|
-
import { pluralize } from '@skyhook-io/k8s-ui'
|
|
7
|
+
import { pluralize, StatusDot } from '@skyhook-io/k8s-ui'
|
|
8
|
+
import type { StatusTone } from '@skyhook-io/k8s-ui'
|
|
8
9
|
import { useFlowSearch } from './TrafficFlowListContext'
|
|
10
|
+
import { isPolicyDropReason } from './trafficFilters'
|
|
9
11
|
import { useQuery } from '@tanstack/react-query'
|
|
10
12
|
import { fetchJSON } from '../../api/client'
|
|
11
13
|
import { Tooltip } from '../ui/Tooltip'
|
|
@@ -250,14 +252,14 @@ export function TrafficFlowList({ flows }: TrafficFlowListProps) {
|
|
|
250
252
|
<div className="grid grid-cols-2 gap-x-6 gap-y-1">
|
|
251
253
|
<div>
|
|
252
254
|
<span className="text-theme-text-tertiary">Source: </span>
|
|
253
|
-
<span className="text-theme-text-primary">{flow.source
|
|
254
|
-
{flow.source
|
|
255
|
+
<span className="text-theme-text-primary">{endpointLabel(flow.source)}</span>
|
|
256
|
+
{endpointNote(flow.source) && <span className="text-theme-text-tertiary"> ({endpointNote(flow.source)})</span>}
|
|
255
257
|
{flow.sourceService && <span className="text-theme-text-tertiary"> via {flow.sourceService}</span>}
|
|
256
258
|
</div>
|
|
257
259
|
<div>
|
|
258
260
|
<span className="text-theme-text-tertiary">Destination: </span>
|
|
259
|
-
<span className="text-theme-text-primary">{flow.destination
|
|
260
|
-
{flow.destination
|
|
261
|
+
<span className="text-theme-text-primary">{endpointLabel(flow.destination)}</span>
|
|
262
|
+
{endpointNote(flow.destination) && <span className="text-theme-text-tertiary"> ({endpointNote(flow.destination)})</span>}
|
|
261
263
|
{flow.destService && <span className="text-theme-text-tertiary"> via {flow.destService}</span>}
|
|
262
264
|
</div>
|
|
263
265
|
<div>
|
|
@@ -334,7 +336,8 @@ export function TrafficFlowList({ flows }: TrafficFlowListProps) {
|
|
|
334
336
|
{/* Drop reason + policy correlation */}
|
|
335
337
|
{flow.dropReasonDesc && (
|
|
336
338
|
<div className="pt-1 border-t border-theme-border/50">
|
|
337
|
-
<span className={SEVERITY_TEXT.error}>
|
|
339
|
+
<span className={SEVERITY_TEXT.error}>Dropped: {describeDropReason(flow.dropReasonDesc)}</span>
|
|
340
|
+
<span className="text-theme-text-tertiary font-mono text-[10px]"> · {flow.dropReasonDesc}</span>
|
|
338
341
|
</div>
|
|
339
342
|
)}
|
|
340
343
|
{flow.verdict === 'dropped' && (
|
|
@@ -357,88 +360,244 @@ export function TrafficFlowList({ flows }: TrafficFlowListProps) {
|
|
|
357
360
|
)
|
|
358
361
|
}
|
|
359
362
|
|
|
363
|
+
type PolicyVerdictWord = 'admitted' | 'denied' | 'undecidable' | 'no-policy'
|
|
364
|
+
|
|
360
365
|
interface PolicyEvaluation {
|
|
361
366
|
selectingPolicies: { name: string; namespace?: string; kind: string; effect: string; reason: string }[]
|
|
362
|
-
verdict:
|
|
367
|
+
verdict: PolicyVerdictWord
|
|
368
|
+
reason?: string
|
|
369
|
+
/** What Kubernetes NetworkPolicies alone said, when the verdict was capped
|
|
370
|
+
* because policies of another kind also govern the pod. */
|
|
371
|
+
coreVerdict?: PolicyVerdictWord
|
|
372
|
+
evaluated?: { direction?: string; pod?: string; peer?: string; port?: number; protocol?: string }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function isPolicyDrop(flow: TrafficFlow): boolean {
|
|
376
|
+
return isPolicyDropReason(flow.dropReasonDesc, (flow.policyVerdict?.deniedBy?.length ?? 0) + (flow.policyVerdict?.withheld ?? 0))
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const EFFECT_TONE: Record<string, StatusTone> = {
|
|
380
|
+
admits: 'healthy',
|
|
381
|
+
does_not_admit: 'neutral',
|
|
382
|
+
undecidable: 'unknown',
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const EFFECT_LABEL: Record<string, string> = {
|
|
386
|
+
admits: 'allows this traffic',
|
|
387
|
+
does_not_admit: 'no matching allow rule',
|
|
388
|
+
undecidable: "can't evaluate",
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Plain words for the drop reasons an operator meets most; anything else is
|
|
392
|
+
* the code itself, humanized. The code stays beside it either way. */
|
|
393
|
+
const DROP_REASON_TEXT: Record<string, string> = {
|
|
394
|
+
POLICY_DENIED: 'blocked by network policy — no rule allowed it',
|
|
395
|
+
POLICY_DENY: 'blocked by an explicit deny rule',
|
|
396
|
+
AUTH_REQUIRED: 'mutual authentication required and not established',
|
|
397
|
+
NO_CONFIGURATION_AVAILABLE_TO_PERFORM_POLICY_DECISION: 'policy not yet loaded for this endpoint',
|
|
398
|
+
STALE_OR_UNROUTABLE_IP: 'no route to the destination address',
|
|
399
|
+
UNSUPPORTED_L3_PROTOCOL: 'unsupported network protocol',
|
|
400
|
+
INVALID_SOURCE_IP: 'the source address is not valid here',
|
|
401
|
+
NO_TUNNEL_OR_ENCAPSULATION_ENDPOINT: 'no tunnel endpoint for the destination node',
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function describeDropReason(code: string | undefined): string {
|
|
405
|
+
if (!code) return 'reason not reported'
|
|
406
|
+
return DROP_REASON_TEXT[code] ?? code.toLowerCase().replace(/_/g, ' ')
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** A pod is named; anything else is best identified by the address the user
|
|
410
|
+
* can act on, with the classification as the note. */
|
|
411
|
+
function endpointLabel(ep: TrafficEndpoint): string {
|
|
412
|
+
if (ep.kind === 'Pod' || ep.kind === 'Service' || !ep.ip) return ep.name
|
|
413
|
+
return ep.ip
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function endpointNote(ep: TrafficEndpoint): string {
|
|
417
|
+
if (ep.kind === 'Pod' || ep.kind === 'Service') return ep.namespace ?? ''
|
|
418
|
+
if (ep.kind === 'External') return 'outside the cluster'
|
|
419
|
+
if (ep.kind === 'Host') return ep.name === 'kube-apiserver' ? 'the API server' : 'a node or the host network'
|
|
420
|
+
return ep.ip ? 'not identified' : ''
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function policyRef(p: { kind: string; namespace?: string; name: string }): string {
|
|
424
|
+
return p.namespace ? `${p.namespace}/${p.name}` : p.name
|
|
363
425
|
}
|
|
364
426
|
|
|
365
427
|
function PolicyCorrelation({ flow }: { flow: TrafficFlow }) {
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
const evalLabels = direction === 'egress' ? srcLabelsParam : labelsParam
|
|
381
|
-
|
|
382
|
-
// Need either labels or pod name to resolve the evaluated pod
|
|
383
|
-
const canQuery = !!evalNs && (!!evalLabels || !!evalName)
|
|
384
|
-
|
|
385
|
-
const { data, isLoading, isError } = useQuery<PolicyEvaluation>({
|
|
386
|
-
queryKey: ['policy-evaluate', destNs, destName, labelsParam, srcNs, srcName, srcLabelsParam, direction],
|
|
428
|
+
const src = flow.source
|
|
429
|
+
const dst = flow.destination
|
|
430
|
+
const direction = flow.trafficDirection === 'ingress' || flow.trafficDirection === 'egress' ? flow.trafficDirection : ''
|
|
431
|
+
const evaluated = direction === 'egress' ? src : dst
|
|
432
|
+
const evaluatedRef = evaluated ? `${evaluated.namespace}/${evaluated.name}` : ''
|
|
433
|
+
const intoOutOf = direction === 'egress' ? 'out of' : 'into'
|
|
434
|
+
const observed = flow.lastSeen ? new Date(flow.lastSeen).toLocaleTimeString() : ''
|
|
435
|
+
|
|
436
|
+
// Only a pod has policies applied to it; a drop at a node or an external
|
|
437
|
+
// endpoint is not a NetworkPolicy question.
|
|
438
|
+
const canQuery = isPolicyDrop(flow) && (!direction || (evaluated?.kind === 'Pod' && !!evaluated.namespace && !!evaluated.name))
|
|
439
|
+
|
|
440
|
+
const { data, isLoading, error, dataUpdatedAt } = useQuery<PolicyEvaluation, Error>({
|
|
441
|
+
queryKey: ['policy-evaluate', direction, dst?.namespace, dst?.name, dst?.kind, dst?.ip, src?.namespace, src?.name, src?.kind, src?.ip, flow.port, flow.protocol],
|
|
387
442
|
queryFn: () => {
|
|
388
|
-
const params = new URLSearchParams(
|
|
389
|
-
if (
|
|
390
|
-
if (
|
|
391
|
-
if (
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
if (
|
|
443
|
+
const params = new URLSearchParams()
|
|
444
|
+
if (direction) params.set('direction', direction)
|
|
445
|
+
if (dst?.namespace) params.set('namespace', dst.namespace)
|
|
446
|
+
if (dst?.name) params.set('podName', dst.name)
|
|
447
|
+
if (dst?.kind) params.set('destinationKind', dst.kind)
|
|
448
|
+
if (dst?.ip) params.set('destinationIP', dst.ip)
|
|
449
|
+
if (src?.namespace) params.set('sourceNamespace', src.namespace)
|
|
450
|
+
if (src?.name) params.set('sourcePodName', src.name)
|
|
451
|
+
if (src?.kind) params.set('sourceKind', src.kind)
|
|
452
|
+
if (src?.ip) params.set('sourceIP', src.ip)
|
|
453
|
+
if (flow.port) params.set('port', String(flow.port))
|
|
454
|
+
if (flow.protocol) params.set('protocol', flow.protocol)
|
|
395
455
|
return fetchJSON(`/network-policies/evaluate?${params}`)
|
|
396
456
|
},
|
|
397
457
|
enabled: canQuery,
|
|
398
458
|
staleTime: 30000,
|
|
459
|
+
// "As they are now" has to stay true while the row is open.
|
|
460
|
+
refetchInterval: 30000,
|
|
399
461
|
})
|
|
462
|
+
const checkedAt = dataUpdatedAt ? new Date(dataUpdatedAt).toLocaleTimeString() : ''
|
|
463
|
+
|
|
464
|
+
const hubble = flow.policyVerdict
|
|
465
|
+
const hubbleDenied = hubble?.deniedBy ?? []
|
|
466
|
+
const hubbleAllowed = hubble?.allowedBy ?? []
|
|
467
|
+
const withheld = hubble?.withheld ?? 0
|
|
468
|
+
|
|
469
|
+
if (!isPolicyDrop(flow)) {
|
|
470
|
+
return (
|
|
471
|
+
<div className="pt-1 border-t border-theme-border/50 text-[10px] text-theme-text-tertiary">
|
|
472
|
+
{flow.dropReasonDesc
|
|
473
|
+
? `Radar checks NetworkPolicies only for drops the plugin reports as policy drops; this one was reported as ${flow.dropReasonDesc}`
|
|
474
|
+
: "The plugin did not report why this flow was dropped, so Radar can't tell whether a policy was involved"}
|
|
475
|
+
</div>
|
|
476
|
+
)
|
|
477
|
+
}
|
|
400
478
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
479
|
+
// What the plugin recorded when the packet was dropped is the ground
|
|
480
|
+
// truth; the current-policy check below is a reading of the policies as
|
|
481
|
+
// they are now, and is presented as a note under it rather than as a
|
|
482
|
+
// second verdict.
|
|
483
|
+
//
|
|
484
|
+
// Only a named deny is the account of a drop. Hubble can also record an
|
|
485
|
+
// allow on a dropped flow — at another layer (L3 allowed, then dropped at
|
|
486
|
+
// L7) or at the other end — and that is not why the packet was dropped.
|
|
487
|
+
const attribution = hubbleDenied.length > 0
|
|
488
|
+
? { text: `Blocked by ${hubbleDenied.map((p) => `${p.kind} ${policyRef(p)}`).join(', ')}`, tone: 'unhealthy' as StatusTone }
|
|
489
|
+
: withheld > 0
|
|
490
|
+
? { text: `Blocked by ${withheld === 1 ? 'a policy' : `${withheld} policies`} you don't have permission to read`, tone: 'unhealthy' as StatusTone }
|
|
491
|
+
: null
|
|
492
|
+
const alsoAllowed = hubbleAllowed.length > 0
|
|
493
|
+
? `The plugin also recorded ${hubbleAllowed.map((p) => `${p.kind} ${policyRef(p)}`).join(', ')} allowing this flow at another layer or at the other end — not the reason for the drop.`
|
|
494
|
+
: ''
|
|
495
|
+
|
|
496
|
+
if (!canQuery && !attribution) {
|
|
497
|
+
return (
|
|
498
|
+
<div className="pt-1 border-t border-theme-border/50 text-[10px] text-theme-text-tertiary">
|
|
499
|
+
{evaluated?.kind && evaluated.kind !== 'Pod'
|
|
500
|
+
? `NetworkPolicies apply to pods, and the ${direction === 'egress' ? 'source' : 'destination'} here is ${endpointNote(evaluated) || 'not a pod'}`
|
|
501
|
+
: 'Not enough is known about this flow to check policies'}
|
|
417
502
|
</div>
|
|
418
|
-
|
|
419
|
-
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// The current check, scoped to what it can establish: which policies
|
|
507
|
+
// allow this traffic at the evaluated pod, now. Never "the connection
|
|
508
|
+
// works" — the flow was dropped, and policies may have changed since.
|
|
509
|
+
// When another policy kind governs the pod, the Kubernetes reading is
|
|
510
|
+
// still worth stating — as a reading, not a verdict.
|
|
511
|
+
const v = data?.verdict
|
|
512
|
+
const core = data?.coreVerdict
|
|
513
|
+
const capped = v === 'undecidable' && !!core && core !== 'undecidable'
|
|
514
|
+
const current = v === 'admitted'
|
|
515
|
+
? { text: `A current NetworkPolicy allows this traffic ${intoOutOf} ${evaluatedRef}`, tone: 'healthy' as StatusTone,
|
|
516
|
+
note: "Policies may have changed since the drop; this doesn't confirm a new connection will succeed." }
|
|
517
|
+
: v === 'denied'
|
|
518
|
+
? { text: `No current NetworkPolicy allows this traffic ${intoOutOf} ${evaluatedRef}`, tone: 'unhealthy' as StatusTone, note: '' }
|
|
519
|
+
: v === 'no-policy'
|
|
520
|
+
? { text: `No NetworkPolicy selects ${evaluatedRef} for ${direction === 'egress' ? 'outgoing' : 'incoming'} traffic`, tone: 'neutral' as StatusTone, note: '' }
|
|
521
|
+
: capped
|
|
522
|
+
? { text: core === 'admitted'
|
|
523
|
+
? `Kubernetes NetworkPolicies alone would allow this traffic ${intoOutOf} ${evaluatedRef} — but another policy kind also applies and was not evaluated`
|
|
524
|
+
: core === 'denied'
|
|
525
|
+
? `No Kubernetes NetworkPolicy allows this traffic ${intoOutOf} ${evaluatedRef} — but another policy kind also applies and could`
|
|
526
|
+
: `No Kubernetes NetworkPolicy selects ${evaluatedRef} — but another policy kind applies and was not evaluated`,
|
|
527
|
+
tone: 'unknown' as StatusTone, note: '' }
|
|
528
|
+
: v === 'undecidable'
|
|
529
|
+
? { text: "Radar can't complete the current NetworkPolicy check", tone: 'unknown' as StatusTone, note: '' }
|
|
530
|
+
: null
|
|
531
|
+
const peerNote = data?.evaluated?.peer && data.evaluated.peer.includes('(') ? `Peer: ${data.evaluated.peer}.` : ''
|
|
532
|
+
const currentNote = [current?.note, data?.reason, peerNote].filter(Boolean).join(' ')
|
|
533
|
+
const checkedText = checkedAt ? ` · checked ${checkedAt}` : ''
|
|
534
|
+
|
|
535
|
+
const rows = data?.selectingPolicies ?? []
|
|
536
|
+
const explainAdditive = rows.length > 1 && rows.some((r) => r.effect === 'does_not_admit')
|
|
420
537
|
|
|
421
538
|
return (
|
|
422
539
|
<div className="pt-1 border-t border-theme-border/50 space-y-1">
|
|
423
|
-
|
|
424
|
-
<
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
540
|
+
{attribution && (
|
|
541
|
+
<div className="flex items-start gap-1.5">
|
|
542
|
+
<StatusDot tone={attribution.tone} className="mt-1 shrink-0" />
|
|
543
|
+
<div className="min-w-0 text-[10px]">
|
|
544
|
+
<span className="text-theme-text-primary font-medium">{attribution.text}</span>
|
|
545
|
+
<span className="text-theme-text-tertiary"> · reported by the network plugin when the packet was dropped</span>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
)}
|
|
549
|
+
{alsoAllowed && (
|
|
550
|
+
<div className={clsx('text-[10px] text-theme-text-tertiary', attribution && 'ml-4')}>{alsoAllowed}</div>
|
|
551
|
+
)}
|
|
552
|
+
{canQuery && isLoading && (
|
|
553
|
+
<div className="text-[10px] text-theme-text-tertiary">Checking current NetworkPolicies…</div>
|
|
554
|
+
)}
|
|
555
|
+
{canQuery && error && (
|
|
556
|
+
<div className="text-[10px] text-theme-text-tertiary">Couldn't check current NetworkPolicies: {error.message}</div>
|
|
557
|
+
)}
|
|
558
|
+
{current && (attribution ? (
|
|
559
|
+
<div className="ml-4 text-[10px]">
|
|
560
|
+
<span className="text-theme-text-secondary">Current check: </span>
|
|
561
|
+
<span className="text-theme-text-primary">{current.text}</span>
|
|
562
|
+
<span className="text-theme-text-tertiary">{observed ? ` · flow seen ${observed}` : ''}{checkedText}</span>
|
|
563
|
+
{currentNote && <div className="text-theme-text-tertiary">{currentNote}</div>}
|
|
564
|
+
</div>
|
|
565
|
+
) : (
|
|
566
|
+
<div className="flex items-start gap-1.5">
|
|
567
|
+
<StatusDot tone={current.tone} className="mt-1 shrink-0" />
|
|
568
|
+
<div className="min-w-0 text-[10px]">
|
|
569
|
+
<span className="text-theme-text-primary font-medium">{current.text}</span>
|
|
570
|
+
<span className="text-theme-text-tertiary"> · against policies as they are now{observed ? `, flow seen ${observed}` : ''}{checkedText}</span>
|
|
571
|
+
{currentNote && <div className="text-theme-text-tertiary">{currentNote}</div>}
|
|
439
572
|
</div>
|
|
440
573
|
</div>
|
|
441
574
|
))}
|
|
575
|
+
{rows.length > 0 && (
|
|
576
|
+
<div className="ml-4 space-y-1">
|
|
577
|
+
<div className="flex items-center gap-1 text-theme-text-secondary">
|
|
578
|
+
<ShieldCheck className="w-3 h-3" />
|
|
579
|
+
<span className="text-[10px] font-medium">
|
|
580
|
+
{rows.length} {rows.length === 1 ? 'policy applies' : 'policies apply'} to {evaluatedRef}
|
|
581
|
+
</span>
|
|
582
|
+
</div>
|
|
583
|
+
{explainAdditive && (
|
|
584
|
+
<div className="text-[10px] text-theme-text-tertiary">
|
|
585
|
+
NetworkPolicies combine their allow rules: a policy with no matching rule doesn't override another policy's allow.
|
|
586
|
+
</div>
|
|
587
|
+
)}
|
|
588
|
+
{rows.map((p, i) => (
|
|
589
|
+
<div key={i} className="flex items-start gap-1.5">
|
|
590
|
+
<StatusDot tone={EFFECT_TONE[p.effect] ?? 'unknown'} className="mt-1 shrink-0" />
|
|
591
|
+
<div className="min-w-0">
|
|
592
|
+
<span className="text-[10px] text-theme-text-primary font-medium">{p.name}</span>
|
|
593
|
+
<span className="text-[10px] text-theme-text-tertiary ml-1">({p.kind})</span>
|
|
594
|
+
<span className="text-[10px] text-theme-text-secondary ml-1">— {EFFECT_LABEL[p.effect] ?? p.effect}</span>
|
|
595
|
+
<div className="text-[10px] text-theme-text-tertiary">{p.reason}</div>
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
))}
|
|
599
|
+
</div>
|
|
600
|
+
)}
|
|
442
601
|
</div>
|
|
443
602
|
)
|
|
444
603
|
}
|
|
@@ -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}
|