@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
|
@@ -628,6 +628,7 @@ export function SettingsDialog({
|
|
|
628
628
|
locked={!canEditConfig}
|
|
629
629
|
>
|
|
630
630
|
<PrometheusConfigField
|
|
631
|
+
local={deploymentMode === 'local'}
|
|
631
632
|
value={editedConfig.prometheusUrl ?? ''}
|
|
632
633
|
configuredHeaderKeys={configData?.prometheusHeaderKeys ?? []}
|
|
633
634
|
onChange={(v) => updateConfigField('prometheusUrl', v || undefined)}
|
|
@@ -698,6 +699,9 @@ export function SettingsDialog({
|
|
|
698
699
|
envManaged={configData?.argoCdEnvManaged ?? false}
|
|
699
700
|
envError={configData?.argoCdEnvError}
|
|
700
701
|
cliSession={configData?.argoCdCliSession}
|
|
702
|
+
anonymous={argoSectionStatus?.connected && argoSectionStatus.anonymous}
|
|
703
|
+
connectedAddress={argoSectionStatus?.connected && !argoSectionStatus.anonymous ? argoSectionStatus.address : undefined}
|
|
704
|
+
active={section === 'argocd'}
|
|
701
705
|
statusReason={
|
|
702
706
|
argoSectionStatus?.configured && !argoSectionStatus.connected
|
|
703
707
|
? argoSectionStatus.reason
|
|
@@ -716,6 +720,12 @@ export function SettingsDialog({
|
|
|
716
720
|
: prev
|
|
717
721
|
)
|
|
718
722
|
void refetchArgoSectionStatus()
|
|
723
|
+
// The GitOps detail page the user came from doesn't poll
|
|
724
|
+
// when idle; without this the notice that sent them here
|
|
725
|
+
// would still be up when they get back.
|
|
726
|
+
void queryClient.invalidateQueries({
|
|
727
|
+
predicate: (query) => typeof query.queryKey[0] === 'string' && query.queryKey[0].startsWith('gitops-'),
|
|
728
|
+
})
|
|
719
729
|
}}
|
|
720
730
|
/>
|
|
721
731
|
</SectionPane>
|
|
@@ -1105,9 +1115,9 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
|
|
|
1105
1115
|
},
|
|
1106
1116
|
{
|
|
1107
1117
|
id: 'prometheus', icon: Activity, label: 'Metrics',
|
|
1108
|
-
tone: prom?.connected ? 'ok' : prom?.
|
|
1109
|
-
value: prom?.connected ? 'Connected' : prom?.
|
|
1110
|
-
detail: prom?.connected ? prom.address : undefined,
|
|
1118
|
+
tone: prom?.connected ? 'ok' : prom?.discovering ? 'unknown' : prom?.error ? 'warn' : 'off',
|
|
1119
|
+
value: prom?.connected ? 'Connected' : prom?.discovering ? 'Discovering…' : prom?.error ? 'Not connected' : 'Not configured',
|
|
1120
|
+
detail: prom?.connected ? prom.address : prom?.discovering ? undefined : prom?.error,
|
|
1111
1121
|
},
|
|
1112
1122
|
{
|
|
1113
1123
|
id: costConfigurationAction(cost?.reason).section, icon: Coins, label: 'Cost',
|
|
@@ -1127,7 +1137,7 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
|
|
|
1127
1137
|
// Configured-but-not-connected is often a permanently rejected/expired
|
|
1128
1138
|
// token, not a transient reconnect — "Not reachable" matches Prometheus and
|
|
1129
1139
|
// doesn't imply it will recover on its own.
|
|
1130
|
-
value: argo?.connected ? 'Connected' : argo?.configured ? 'Not reachable' : 'Not connected',
|
|
1140
|
+
value: argo?.connected ? (argo.anonymous ? 'Connected · no token needed' : 'Connected') : argo?.configured ? 'Not reachable' : 'Not connected',
|
|
1131
1141
|
detail: argo?.connected ? argo.address : argo?.reason,
|
|
1132
1142
|
},
|
|
1133
1143
|
{
|
|
@@ -1899,11 +1909,13 @@ type CurrencySaveState =
|
|
|
1899
1909
|
type HeaderRow = { key: string; value: string }
|
|
1900
1910
|
|
|
1901
1911
|
function PrometheusConfigField({
|
|
1912
|
+
local,
|
|
1902
1913
|
value,
|
|
1903
1914
|
onChange,
|
|
1904
1915
|
configuredHeaderKeys,
|
|
1905
1916
|
onApplied,
|
|
1906
1917
|
}: {
|
|
1918
|
+
local: boolean
|
|
1907
1919
|
value: string
|
|
1908
1920
|
onChange: (value: string) => void
|
|
1909
1921
|
configuredHeaderKeys: string[]
|
|
@@ -1981,14 +1993,14 @@ function PrometheusConfigField({
|
|
|
1981
1993
|
return (
|
|
1982
1994
|
<div>
|
|
1983
1995
|
<p className="text-xs text-theme-text-tertiary mb-3">
|
|
1984
|
-
|
|
1996
|
+
Connect existing Prometheus-compatible data for resource usage, workload HTTP metrics and rightsizing. Available charts depend on collected metrics.
|
|
1985
1997
|
</p>
|
|
1986
1998
|
<label className="block text-sm font-medium text-theme-text-primary mb-1">
|
|
1987
|
-
|
|
1999
|
+
Metrics backend URL
|
|
1988
2000
|
</label>
|
|
1989
2001
|
<p className="text-xs text-theme-text-tertiary mb-1">
|
|
1990
|
-
|
|
1991
|
-
Mimir.
|
|
2002
|
+
Base URL reachable from Radar, not your browser — Prometheus, VictoriaMetrics, Thanos or
|
|
2003
|
+
Mimir. Include any backend path prefix, but not /api/v1/query. Leave empty for cluster discovery; headers require a URL.
|
|
1992
2004
|
</p>
|
|
1993
2005
|
<div className="flex items-center gap-2">
|
|
1994
2006
|
<Input
|
|
@@ -1997,7 +2009,7 @@ function PrometheusConfigField({
|
|
|
1997
2009
|
placeholder="http://prometheus-server.monitoring:9090"
|
|
1998
2010
|
className="flex-1 min-w-0 px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:border-skyhook-500"
|
|
1999
2011
|
/>
|
|
2000
|
-
<Tooltip content="Apply
|
|
2012
|
+
<Tooltip content="Apply the connection now — no restart. Changing the URL or headers clears any workload scope override and resumes automatic identity matching." wrapperClassName="shrink-0">
|
|
2001
2013
|
<button
|
|
2002
2014
|
onClick={handleApply}
|
|
2003
2015
|
disabled={apply.status === 'applying'}
|
|
@@ -2028,6 +2040,12 @@ function PrometheusConfigField({
|
|
|
2028
2040
|
Applies immediately — no restart needed.
|
|
2029
2041
|
</p>
|
|
2030
2042
|
)}
|
|
2043
|
+
<p className="mt-2 text-xs text-theme-text-tertiary">
|
|
2044
|
+
{local
|
|
2045
|
+
? 'Saved URL and headers apply across all local cluster contexts.'
|
|
2046
|
+
: 'Changes affect this Radar installation. Use deployment settings for configuration that survives Pod replacement.'}
|
|
2047
|
+
{' URL changes do not clear headers. Replace or clear credentials before switching backends.'}
|
|
2048
|
+
</p>
|
|
2031
2049
|
|
|
2032
2050
|
{/* Auth headers — for token / multi-tenant backends (Bearer, X-Scope-OrgID). */}
|
|
2033
2051
|
<div className="mt-3">
|
|
@@ -2095,7 +2113,8 @@ function PrometheusConfigField({
|
|
|
2095
2113
|
<p className="text-xs text-theme-text-tertiary">
|
|
2096
2114
|
Saved when you click Apply now. Entered headers replace all stored
|
|
2097
2115
|
ones — values are hidden, so re-enter any you want to keep. Leave
|
|
2098
|
-
blank to keep existing headers unchanged.
|
|
2116
|
+
all rows blank to keep existing headers unchanged. To clear all saved
|
|
2117
|
+
headers, remove every row and click Apply now.
|
|
2099
2118
|
</p>
|
|
2100
2119
|
</div>
|
|
2101
2120
|
)}
|
|
@@ -2128,6 +2147,9 @@ function ArgoCDConfigField({
|
|
|
2128
2147
|
envError,
|
|
2129
2148
|
cliSession,
|
|
2130
2149
|
statusReason,
|
|
2150
|
+
anonymous,
|
|
2151
|
+
connectedAddress,
|
|
2152
|
+
active,
|
|
2131
2153
|
onChangeUrl,
|
|
2132
2154
|
onChangeInsecureTls,
|
|
2133
2155
|
onApplied,
|
|
@@ -2139,12 +2161,15 @@ function ArgoCDConfigField({
|
|
|
2139
2161
|
envError?: string
|
|
2140
2162
|
cliSession?: { server: string; user: string; insecure?: boolean }
|
|
2141
2163
|
statusReason?: string
|
|
2164
|
+
anonymous?: boolean
|
|
2165
|
+
connectedAddress?: string
|
|
2166
|
+
active?: boolean
|
|
2142
2167
|
onChangeUrl: (value: string) => void
|
|
2143
2168
|
onChangeInsecureTls: (value: boolean) => void
|
|
2144
2169
|
onApplied?: (v: { url: string; insecureTls: boolean; tokenSet: boolean }) => void
|
|
2145
2170
|
}) {
|
|
2146
2171
|
if (envManaged) {
|
|
2147
|
-
return <ArgoCDEnvManagedField url={url} insecureTls={insecureTls} envError={envError} />
|
|
2172
|
+
return <ArgoCDEnvManagedField url={url} insecureTls={insecureTls} envError={envError} connectedAddress={connectedAddress} statusReason={statusReason} />
|
|
2148
2173
|
}
|
|
2149
2174
|
return (
|
|
2150
2175
|
<ArgoCDEditableField
|
|
@@ -2153,6 +2178,9 @@ function ArgoCDConfigField({
|
|
|
2153
2178
|
tokenSet={tokenSet}
|
|
2154
2179
|
cliSession={cliSession}
|
|
2155
2180
|
statusReason={statusReason}
|
|
2181
|
+
anonymous={anonymous}
|
|
2182
|
+
connectedAddress={connectedAddress}
|
|
2183
|
+
active={active}
|
|
2156
2184
|
onChangeUrl={onChangeUrl}
|
|
2157
2185
|
onChangeInsecureTls={onChangeInsecureTls}
|
|
2158
2186
|
onApplied={onApplied}
|
|
@@ -2168,10 +2196,17 @@ function ArgoCDEnvManagedField({
|
|
|
2168
2196
|
url,
|
|
2169
2197
|
insecureTls,
|
|
2170
2198
|
envError,
|
|
2199
|
+
connectedAddress,
|
|
2200
|
+
statusReason,
|
|
2171
2201
|
}: {
|
|
2172
2202
|
url: string
|
|
2173
2203
|
insecureTls: boolean
|
|
2174
2204
|
envError?: string
|
|
2205
|
+
// Live connection, from the status endpoint: the address when the
|
|
2206
|
+
// token-backed session is up, else why it isn't. The card is read-only,
|
|
2207
|
+
// so this is the one thing on it that can change after deploy.
|
|
2208
|
+
connectedAddress?: string
|
|
2209
|
+
statusReason?: string
|
|
2175
2210
|
}) {
|
|
2176
2211
|
if (envError) {
|
|
2177
2212
|
return (
|
|
@@ -2224,6 +2259,21 @@ function ArgoCDEnvManagedField({
|
|
|
2224
2259
|
<dt className="w-24 shrink-0 text-theme-text-tertiary">Token</dt>
|
|
2225
2260
|
<dd className="text-theme-text-secondary">provided via environment</dd>
|
|
2226
2261
|
</div>
|
|
2262
|
+
<div className="flex gap-2">
|
|
2263
|
+
<dt className="w-24 shrink-0 text-theme-text-tertiary">Status</dt>
|
|
2264
|
+
<dd className="min-w-0 truncate text-theme-text-secondary">
|
|
2265
|
+
{connectedAddress ? (
|
|
2266
|
+
<span className="inline-flex items-center gap-1">
|
|
2267
|
+
<Check className="w-3 h-3 shrink-0 text-green-600 dark:text-green-400/80" />
|
|
2268
|
+
Connected at {connectedAddress}
|
|
2269
|
+
</span>
|
|
2270
|
+
) : statusReason ? (
|
|
2271
|
+
<span className="text-warning-text">Not reachable — {statusReason}</span>
|
|
2272
|
+
) : (
|
|
2273
|
+
'Not connected'
|
|
2274
|
+
)}
|
|
2275
|
+
</dd>
|
|
2276
|
+
</div>
|
|
2227
2277
|
{insecureTls && (
|
|
2228
2278
|
<div className="flex gap-2">
|
|
2229
2279
|
<dt className="w-24 shrink-0 text-theme-text-tertiary">TLS</dt>
|
|
@@ -2246,6 +2296,9 @@ function ArgoCDEditableField({
|
|
|
2246
2296
|
tokenSet,
|
|
2247
2297
|
cliSession,
|
|
2248
2298
|
statusReason,
|
|
2299
|
+
anonymous,
|
|
2300
|
+
connectedAddress,
|
|
2301
|
+
active,
|
|
2249
2302
|
onChangeUrl,
|
|
2250
2303
|
onChangeInsecureTls,
|
|
2251
2304
|
onApplied,
|
|
@@ -2255,6 +2308,9 @@ function ArgoCDEditableField({
|
|
|
2255
2308
|
tokenSet: boolean
|
|
2256
2309
|
cliSession?: { server: string; user: string; insecure?: boolean }
|
|
2257
2310
|
statusReason?: string
|
|
2311
|
+
anonymous?: boolean
|
|
2312
|
+
connectedAddress?: string
|
|
2313
|
+
active?: boolean
|
|
2258
2314
|
onChangeUrl: (value: string) => void
|
|
2259
2315
|
onChangeInsecureTls: (value: boolean) => void
|
|
2260
2316
|
onApplied?: (v: { url: string; insecureTls: boolean; tokenSet: boolean }) => void
|
|
@@ -2348,20 +2404,43 @@ function ArgoCDEditableField({
|
|
|
2348
2404
|
|
|
2349
2405
|
const showConfiguredPlaceholder = effectiveTokenSet && !tokenTouched && !tokenCleared
|
|
2350
2406
|
const connecting = state.status === 'connecting'
|
|
2407
|
+
// Opening this section with nothing set up (typically from the GitOps
|
|
2408
|
+
// page's "connect Radar to your Argo CD server") starts the user at the
|
|
2409
|
+
// token — unless a CLI session offers a one-click connect, or reads
|
|
2410
|
+
// already work anonymously.
|
|
2411
|
+
const tokenInputRef = useRef<HTMLInputElement>(null)
|
|
2412
|
+
useEffect(() => {
|
|
2413
|
+
if (active && !effectiveTokenSet && !cliSession && !anonymous) tokenInputRef.current?.focus()
|
|
2414
|
+
}, [active, effectiveTokenSet, cliSession, anonymous])
|
|
2351
2415
|
|
|
2352
2416
|
return (
|
|
2353
2417
|
<div>
|
|
2354
2418
|
<p className="text-xs text-theme-text-tertiary mb-3">
|
|
2355
2419
|
Connect your Argo CD server for the full Git-rendered desired-vs-live diff on GitOps
|
|
2356
|
-
Application pages — what Git declares vs what's actually running
|
|
2357
|
-
|
|
2420
|
+
Application pages — what Git declares vs what's actually running — and, on Argo CD 3, for
|
|
2421
|
+
Argo's own health verdict on each resource. Without it, Radar falls back to a lighter
|
|
2422
|
+
annotation-based drift that can miss fields, and to its own read of each resource.
|
|
2358
2423
|
</p>
|
|
2359
2424
|
|
|
2425
|
+
{state.status !== 'connected' && (anonymous || connectedAddress) && (
|
|
2426
|
+
<p className="mb-3 flex items-center gap-1.5 text-xs text-theme-text-secondary">
|
|
2427
|
+
<Check className="w-3.5 h-3.5 shrink-0 text-green-600 dark:text-green-400/80" />
|
|
2428
|
+
{anonymous ? (
|
|
2429
|
+
<>
|
|
2430
|
+
Your Argo CD server lets Radar read without a token, so it's already connected. Add a token only
|
|
2431
|
+
if anonymous access gets restricted, or a URL to point Radar at a specific server.
|
|
2432
|
+
</>
|
|
2433
|
+
) : (
|
|
2434
|
+
<>Connected to Argo CD at {connectedAddress}.</>
|
|
2435
|
+
)}
|
|
2436
|
+
</p>
|
|
2437
|
+
)}
|
|
2438
|
+
|
|
2360
2439
|
{statusReason && state.status !== 'connected' && (
|
|
2361
2440
|
<div className="mb-3 rounded-md border border-theme-border bg-theme-elevated p-3">
|
|
2362
2441
|
<p className="flex items-center gap-1.5 text-sm font-medium text-warning-text">
|
|
2363
2442
|
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
|
|
2364
|
-
Argo CD
|
|
2443
|
+
Argo CD connection needs attention
|
|
2365
2444
|
</p>
|
|
2366
2445
|
<p className="mt-1 text-xs text-theme-text-secondary">{statusReason}</p>
|
|
2367
2446
|
</div>
|
|
@@ -2419,6 +2498,7 @@ function ArgoCDEditableField({
|
|
|
2419
2498
|
</p>
|
|
2420
2499
|
<div className="flex items-center gap-2">
|
|
2421
2500
|
<input
|
|
2501
|
+
ref={tokenInputRef}
|
|
2422
2502
|
type="password"
|
|
2423
2503
|
value={showConfiguredPlaceholder ? '' : token}
|
|
2424
2504
|
onChange={(e) => { setToken(e.target.value); setTokenTouched(true); setTokenCleared(false); clearStatus() }}
|
|
@@ -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
|
}
|