@skyhook-io/radar-app 1.13.5 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/App.tsx +66 -28
- package/src/api/client.authRedirect.test.ts +12 -1
- package/src/api/client.prometheusStatus.test.ts +24 -0
- package/src/api/client.ts +324 -16
- package/src/api/diagnose.ts +32 -1
- 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 +343 -46
- package/src/components/CloudFunnelButton.tsx +110 -37
- package/src/components/ConnectionErrorView.tsx +9 -10
- package/src/components/DebugOverlay.tsx +10 -6
- package/src/components/SyncProgressPanel.tsx +73 -0
- package/src/components/audit/AuditView.tsx +2 -0
- package/src/components/cloudConnectHandoff.test.ts +245 -0
- package/src/components/cloudConnectHandoff.ts +276 -0
- package/src/components/curl/ServiceCurlButton.tsx +19 -26
- package/src/components/diagnose/AgentCase.tsx +18 -5
- package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
- package/src/components/diagnose/AnalysisStory.tsx +564 -0
- package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
- package/src/components/diagnose/DiagnoseContext.tsx +26 -8
- package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
- package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
- package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
- package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
- package/src/components/diagnose/InvestigationView.tsx +493 -529
- package/src/components/diagnose/investigationCase.test.tsx +267 -129
- package/src/components/diagnose/investigationCase.ts +122 -77
- 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 +81 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +285 -0
- package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +427 -0
- package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +632 -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 +978 -0
- package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +2736 -0
- package/src/components/diagnose/investigationEvidence/types.ts +514 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
- package/src/components/diagnose/investigationState.test.ts +316 -58
- package/src/components/diagnose/investigationState.ts +257 -44
- package/src/components/diagnose/investigationStory.test.ts +161 -0
- package/src/components/diagnose/investigationStory.ts +207 -0
- package/src/components/diagnose/parts.test.tsx +9 -6
- package/src/components/diagnose/parts.tsx +977 -193
- package/src/components/diagnose/storyParts.test.tsx +426 -0
- package/src/components/diagnose/target.test.ts +19 -0
- package/src/components/diagnose/target.ts +3 -2
- package/src/components/diagnose/toolCallLabel.test.ts +51 -0
- package/src/components/diagnose/toolCallLabel.ts +135 -0
- package/src/components/diagnose/useDisclosureReveal.ts +10 -11
- package/src/components/gitops/GitOpsView.tsx +7 -0
- package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
- package/src/components/helm/HelmCompareRoute.tsx +18 -4
- package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
- package/src/components/helm/InstallWizard.tsx +8 -3
- package/src/components/home/MCPSetupDialog.tsx +15 -9
- package/src/components/portforward/PortForwardManager.tsx +6 -13
- 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 +480 -0
- package/src/components/resource/workloadMetricValues.test.ts +65 -0
- package/src/components/resource/workloadMetricValues.ts +30 -0
- package/src/components/resources/PodFilePreview.tsx +3 -3
- 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/ServiceRenderer.tsx +2 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
- package/src/components/settings/SettingsDialog.tsx +98 -16
- 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/ui/CommandPalette.tsx +10 -6
- package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
- package/src/components/ui/Disclosure.tsx +47 -0
- package/src/components/ui/Markdown.tsx +23 -11
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
- package/src/components/ui/UpdateNotification.tsx +18 -2
- 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 +36 -6
- 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
|
@@ -33,6 +33,8 @@ export interface InvestigationCaseItem {
|
|
|
33
33
|
index: number;
|
|
34
34
|
role: DiagnosisEvidenceRole;
|
|
35
35
|
claim: string;
|
|
36
|
+
/** What the agent says this result does not cover. */
|
|
37
|
+
gap?: string;
|
|
36
38
|
subject?: DiagnosisEvidenceSubject;
|
|
37
39
|
source: InvestigationEvidenceSource;
|
|
38
40
|
placement: InvestigationCasePlacement;
|
|
@@ -51,65 +53,6 @@ export interface InvestigationCaseResolution {
|
|
|
51
53
|
ruledOut: InvestigationCaseRuledOut[];
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
/**
|
|
55
|
-
* Notes the agent put on cards in earlier assessments stay on those cards
|
|
56
|
-
* when a later turn takes over the pane's case, unless the later turn
|
|
57
|
-
* addressed the same card; a follow-up about one chart must not strip the
|
|
58
|
-
* initial assessment's reading from everything else. Ordering and the
|
|
59
|
-
* ruled-out list follow the live turn alone, and source-placed notes stay
|
|
60
|
-
* listed under their own assessment. `earlier` is newest first.
|
|
61
|
-
*/
|
|
62
|
-
export function mergeInvestigationCases(
|
|
63
|
-
live: InvestigationCaseResolution | undefined,
|
|
64
|
-
earlier: readonly (InvestigationCaseResolution | undefined)[],
|
|
65
|
-
): InvestigationCaseResolution | undefined {
|
|
66
|
-
const liveItems = live?.items ?? [];
|
|
67
|
-
const covered = new Set(
|
|
68
|
-
liveItems.flatMap((item) => (item.groupId ? [item.groupId] : [])),
|
|
69
|
-
);
|
|
70
|
-
const carried: InvestigationCaseItem[] = [];
|
|
71
|
-
for (const resolution of earlier) {
|
|
72
|
-
// The newest assessment that spoke about a group wins it outright, and it
|
|
73
|
-
// wins with everything it said: a card note and a pinned revision note are
|
|
74
|
-
// two readings of one group, not rivals. Coverage is therefore taken after
|
|
75
|
-
// the whole resolution, not as each of its items is carried.
|
|
76
|
-
const takenHere = new Set<string>();
|
|
77
|
-
for (const item of resolution?.items ?? []) {
|
|
78
|
-
if (item.placement === "source" || !item.groupId) continue;
|
|
79
|
-
if (covered.has(item.groupId)) continue;
|
|
80
|
-
takenHere.add(item.groupId);
|
|
81
|
-
carried.push(item);
|
|
82
|
-
}
|
|
83
|
-
for (const groupId of takenHere) covered.add(groupId);
|
|
84
|
-
}
|
|
85
|
-
if (carried.length === 0) return live;
|
|
86
|
-
return { items: [...liveItems, ...carried], ruledOut: live?.ruledOut ?? [] };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* The subset of one assessment's items that survived into the case the pane
|
|
91
|
-
* actually renders. Matching is by value, not object identity: the merge is
|
|
92
|
-
* fed freshly resolved copies of every turn, so an identity test would report
|
|
93
|
-
* that an assessment's own note had vanished the moment any other turn was
|
|
94
|
-
* re-resolved.
|
|
95
|
-
*/
|
|
96
|
-
export function investigationCaseItemsStillRendered(
|
|
97
|
-
assessmentItems: readonly InvestigationCaseItem[] | undefined,
|
|
98
|
-
renderedItems: readonly InvestigationCaseItem[] | undefined,
|
|
99
|
-
): InvestigationCaseItem[] {
|
|
100
|
-
if (!assessmentItems?.length || !renderedItems?.length) return [];
|
|
101
|
-
const key = (item: InvestigationCaseItem) =>
|
|
102
|
-
[
|
|
103
|
-
item.index,
|
|
104
|
-
item.source.id,
|
|
105
|
-
item.placement,
|
|
106
|
-
item.groupId ?? "",
|
|
107
|
-
item.observation ? investigationCaseObservationKey(item.observation) : "",
|
|
108
|
-
].join("\u0000");
|
|
109
|
-
const rendered = new Set(renderedItems.map(key));
|
|
110
|
-
return assessmentItems.filter((item) => rendered.has(key(item)));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
56
|
/**
|
|
114
57
|
* One half of a Go↔TS contract: `evidenceRoles` in internal/ai/parse.go and the
|
|
115
58
|
* DiagnosisEvidenceRole union in api/diagnose.ts must list exactly these roles.
|
|
@@ -134,10 +77,12 @@ export function investigationCaseObservationKey(
|
|
|
134
77
|
|
|
135
78
|
/**
|
|
136
79
|
* Resolves the agent's case for one assessment. Refs are re-validated against
|
|
137
|
-
* the assessment turn exactly like root-cause
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
80
|
+
* the run up to and including the assessment turn, exactly like root-cause
|
|
81
|
+
* refs — a revised assessment may rest on a result read in an earlier turn,
|
|
82
|
+
* and the observation it resolves to is that earlier read, never a later one.
|
|
83
|
+
* An item whose ref does not resolve is dropped on its own. Placement follows
|
|
84
|
+
* the observation the (ref, subject) pair names: exactly one match pins the
|
|
85
|
+
* claim to that observation, anything else falls back to the source row.
|
|
141
86
|
*/
|
|
142
87
|
export function resolveInvestigationCase(
|
|
143
88
|
projection: InvestigationEvidenceProjection,
|
|
@@ -149,17 +94,17 @@ export function resolveInvestigationCase(
|
|
|
149
94
|
): InvestigationCaseResolution {
|
|
150
95
|
const evidence = diagnosis?.evidence ?? [];
|
|
151
96
|
if (evidence.length === 0) return { items: [], ruledOut: [] };
|
|
97
|
+
const targetPods = new Set(projection.targetPods);
|
|
152
98
|
const byRef = new Map<string, InvestigationEvidenceSource[]>();
|
|
153
99
|
for (const source of projection.evidenceRefSources) {
|
|
154
|
-
if (source.turnIndex
|
|
155
|
-
continue;
|
|
100
|
+
if (source.turnIndex > assessmentTurnIndex || !source.evidenceRef) continue;
|
|
156
101
|
const matches = byRef.get(source.evidenceRef) ?? [];
|
|
157
102
|
matches.push(source);
|
|
158
103
|
byRef.set(source.evidenceRef, matches);
|
|
159
104
|
}
|
|
160
105
|
const citableSourceIds = new Set(
|
|
161
106
|
projection.citableSources
|
|
162
|
-
.filter((source) => source.turnIndex
|
|
107
|
+
.filter((source) => source.turnIndex <= assessmentTurnIndex)
|
|
163
108
|
.map((source) => source.id),
|
|
164
109
|
);
|
|
165
110
|
const items: InvestigationCaseItem[] = [];
|
|
@@ -188,6 +133,9 @@ export function resolveInvestigationCase(
|
|
|
188
133
|
index,
|
|
189
134
|
role: entry.role,
|
|
190
135
|
claim: typeof entry.claim === "string" ? entry.claim.trim() : "",
|
|
136
|
+
...(typeof entry.gap === "string" && entry.gap.trim()
|
|
137
|
+
? { gap: entry.gap.trim() }
|
|
138
|
+
: {}),
|
|
191
139
|
...(subject ? { subject } : {}),
|
|
192
140
|
source,
|
|
193
141
|
placement: "source",
|
|
@@ -200,7 +148,12 @@ export function resolveInvestigationCase(
|
|
|
200
148
|
(observation) =>
|
|
201
149
|
observation.source.id === source.id &&
|
|
202
150
|
(!subject ||
|
|
203
|
-
observationMatchesSubject(
|
|
151
|
+
observationMatchesSubject(
|
|
152
|
+
group,
|
|
153
|
+
observation,
|
|
154
|
+
subject,
|
|
155
|
+
targetPods,
|
|
156
|
+
)),
|
|
204
157
|
)
|
|
205
158
|
.map((observation) => ({ group, observation })),
|
|
206
159
|
);
|
|
@@ -241,11 +194,11 @@ export function resolveInvestigationCase(
|
|
|
241
194
|
function validCaseSubject(
|
|
242
195
|
value: DiagnosisEvidenceSubject | undefined,
|
|
243
196
|
): DiagnosisEvidenceSubject | undefined {
|
|
244
|
-
if (!value || !nonEmptyString(value.kind)
|
|
245
|
-
return undefined;
|
|
197
|
+
if (!value || !nonEmptyString(value.kind)) return undefined;
|
|
246
198
|
const optional = (field: unknown) =>
|
|
247
199
|
field === undefined || typeof field === "string";
|
|
248
200
|
if (
|
|
201
|
+
!optional(value.name) ||
|
|
249
202
|
!optional(value.group) ||
|
|
250
203
|
!optional(value.namespace) ||
|
|
251
204
|
!optional(value.container) ||
|
|
@@ -268,6 +221,8 @@ interface ObservationSubjectIdentity {
|
|
|
268
221
|
name: string;
|
|
269
222
|
container?: string;
|
|
270
223
|
stream?: "current" | "previous";
|
|
224
|
+
/** A listing names a kind (and scope) but no single object. */
|
|
225
|
+
listing?: boolean;
|
|
271
226
|
}
|
|
272
227
|
|
|
273
228
|
/**
|
|
@@ -319,17 +274,44 @@ function observationSubjectIdentity(
|
|
|
319
274
|
};
|
|
320
275
|
}
|
|
321
276
|
const args = investigationSourceArgs(observation.source);
|
|
322
|
-
|
|
323
|
-
|
|
277
|
+
// A listing whose call names no kind (list_namespaces) is still a listing
|
|
278
|
+
// of the one kind its rows carry.
|
|
279
|
+
if (data.type === "inventory" && !nonEmptyString(args?.kind)) {
|
|
280
|
+
const kinds = new Set(data.resources.map((resource) => resource.kind));
|
|
281
|
+
if (kinds.size !== 1) return undefined;
|
|
282
|
+
return {
|
|
283
|
+
kind: [...kinds][0],
|
|
284
|
+
namespace: nonEmptyString(args?.namespace) ? args.namespace : undefined,
|
|
285
|
+
name: "",
|
|
286
|
+
listing: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (!args || !nonEmptyString(args.kind)) return undefined;
|
|
324
290
|
return {
|
|
325
291
|
kind: args.kind,
|
|
326
292
|
group: nonEmptyString(args.group) ? args.group : undefined,
|
|
327
293
|
namespace: nonEmptyString(args.namespace) ? args.namespace : undefined,
|
|
328
|
-
name: args.name,
|
|
294
|
+
name: nonEmptyString(args.name) ? args.name : "",
|
|
295
|
+
...(nonEmptyString(args.name) ? {} : { listing: true }),
|
|
329
296
|
};
|
|
330
297
|
}
|
|
331
298
|
|
|
332
|
-
|
|
299
|
+
// No CRD lives in a built-in API group: the core group under its three
|
|
300
|
+
// spellings, the four legacy unsuffixed groups, or any group under k8s.io.
|
|
301
|
+
const LEGACY_BUILT_IN_GROUPS = new Set([
|
|
302
|
+
"",
|
|
303
|
+
"core",
|
|
304
|
+
"v1",
|
|
305
|
+
"apps",
|
|
306
|
+
"batch",
|
|
307
|
+
"autoscaling",
|
|
308
|
+
"policy",
|
|
309
|
+
]);
|
|
310
|
+
function isBuiltInGroup(group: string): boolean {
|
|
311
|
+
return LEGACY_BUILT_IN_GROUPS.has(group) || group.endsWith(".k8s.io");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function sameKind(left: string, right: string): boolean {
|
|
333
315
|
return pluralToKind(left).toLowerCase() === pluralToKind(right).toLowerCase();
|
|
334
316
|
}
|
|
335
317
|
|
|
@@ -342,6 +324,7 @@ function observationMatchesSubject(
|
|
|
342
324
|
group: InvestigationEvidenceGroup,
|
|
343
325
|
observation: InvestigationEvidenceObservation,
|
|
344
326
|
subject: DiagnosisEvidenceSubject,
|
|
327
|
+
targetPods: ReadonlySet<string>,
|
|
345
328
|
): boolean {
|
|
346
329
|
if (subject.observation !== undefined) {
|
|
347
330
|
// A diagnose bundle captures several vitals charts for one resource, so
|
|
@@ -349,7 +332,10 @@ function observationMatchesSubject(
|
|
|
349
332
|
// whose identity ends in that category. An agent-run query is one chart,
|
|
350
333
|
// so a qualifier on it carries no meaning.
|
|
351
334
|
const [kind, qualifier] = subject.observation.toLowerCase().split(":", 2);
|
|
352
|
-
|
|
335
|
+
// A listing is resources too: "resource" names an inventory card as well.
|
|
336
|
+
const inventoryAsResource =
|
|
337
|
+
kind === "resource" && observation.data.type === "inventory";
|
|
338
|
+
if (kind !== observation.data.type && !inventoryAsResource) return false;
|
|
353
339
|
if (
|
|
354
340
|
qualifier !== undefined &&
|
|
355
341
|
observation.data.type === "metrics" &&
|
|
@@ -359,6 +345,10 @@ function observationMatchesSubject(
|
|
|
359
345
|
return false;
|
|
360
346
|
}
|
|
361
347
|
}
|
|
348
|
+
// One rules result holds many rules and none states a resource; the rule's
|
|
349
|
+
// name is the handle that tells them apart.
|
|
350
|
+
if (observation.data.type === "alerts" && subject.name !== undefined)
|
|
351
|
+
return observation.data.rule.name === subject.name;
|
|
362
352
|
const identities = observationIdentities(observation);
|
|
363
353
|
// An observation that states no resource of its own (an agent-run query
|
|
364
354
|
// whose selectors do not name the target exactly, a topology summary) can
|
|
@@ -366,7 +356,7 @@ function observationMatchesSubject(
|
|
|
366
356
|
// the rest.
|
|
367
357
|
if (identities.length === 0) return subject.observation !== undefined;
|
|
368
358
|
return identities.some((identity) =>
|
|
369
|
-
identityMatchesSubject(identity, observation, subject),
|
|
359
|
+
identityMatchesSubject(identity, observation, subject, targetPods),
|
|
370
360
|
);
|
|
371
361
|
}
|
|
372
362
|
|
|
@@ -397,17 +387,72 @@ function observationIdentities(
|
|
|
397
387
|
return identities;
|
|
398
388
|
}
|
|
399
389
|
|
|
390
|
+
// Pod names are DNS subdomains (letters, digits, hyphens, dots), so a name
|
|
391
|
+
// is a whole word between characters that cannot be part of one; "api"
|
|
392
|
+
// inside "api-other" names another Pod.
|
|
393
|
+
function namesPod(text: string, name: string): boolean {
|
|
394
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
395
|
+
return new RegExp(`(^|[^A-Za-z0-9.-])${escaped}(?![A-Za-z0-9.-])`).test(text);
|
|
396
|
+
}
|
|
397
|
+
|
|
400
398
|
function identityMatchesSubject(
|
|
401
399
|
identity: ObservationSubjectIdentity,
|
|
402
400
|
observation: InvestigationEvidenceObservation,
|
|
403
401
|
subject: DiagnosisEvidenceSubject,
|
|
402
|
+
targetPods: ReadonlySet<string>,
|
|
404
403
|
): boolean {
|
|
405
|
-
|
|
404
|
+
// "The events of namespace X" names a namespace-scoped read whose
|
|
405
|
+
// observation inherits the resource its call was asked about; the agent
|
|
406
|
+
// naming the namespace instead is not wrong, so a stated evidence kind plus
|
|
407
|
+
// a matching namespace is enough for that shape.
|
|
408
|
+
if (
|
|
409
|
+
subject.observation !== undefined &&
|
|
410
|
+
sameKind(subject.kind, "Namespace") &&
|
|
411
|
+
identity.namespace !== undefined &&
|
|
412
|
+
identity.namespace === subject.name
|
|
413
|
+
)
|
|
414
|
+
return true;
|
|
415
|
+
// A workload read collects its pods' events, so "the events of Pod X" names
|
|
416
|
+
// the events observation a diagnose bundle or workload call inherited from
|
|
417
|
+
// the workload; the ref already fixes the call, uniqueness fixes the card.
|
|
418
|
+
if (
|
|
419
|
+
subject.observation !== undefined &&
|
|
420
|
+
observation.data.type === "events" &&
|
|
421
|
+
sameKind(subject.kind, "Pod") &&
|
|
422
|
+
!sameKind(identity.kind, "Pod") &&
|
|
423
|
+
(identity.namespace === undefined ||
|
|
424
|
+
subject.namespace === undefined ||
|
|
425
|
+
identity.namespace === subject.namespace) &&
|
|
426
|
+
// The Pod has to be the target's own on events the target's read produced,
|
|
427
|
+
// or named whole by an event: a Pod nothing here establishes is not this card.
|
|
428
|
+
(subject.name === undefined ||
|
|
429
|
+
(targetPods.has(subject.name) && observation.relevance !== "broader") ||
|
|
430
|
+
observation.data.events.some((event) =>
|
|
431
|
+
namesPod(event.message, subject.name!),
|
|
432
|
+
))
|
|
433
|
+
)
|
|
434
|
+
return true;
|
|
435
|
+
if (!sameKind(identity.kind, subject.kind)) return false;
|
|
436
|
+
// A subject with no name (a listing cited for what it does not contain) is
|
|
437
|
+
// a wildcard that uniqueness still gates. A listing has no name of its own; the agent naming the entry it means
|
|
438
|
+
// ("ConfigMap kube-root-ca.crt" in the ConfigMaps of a namespace) still
|
|
439
|
+
// points at that listing, whether or not the listing holds the entry: an
|
|
440
|
+
// absent entry is often the point, and the card says which it is.
|
|
441
|
+
if (
|
|
442
|
+
!identity.listing &&
|
|
443
|
+
subject.name !== undefined &&
|
|
444
|
+
identity.name !== subject.name
|
|
445
|
+
)
|
|
406
446
|
return false;
|
|
447
|
+
// A built-in group on a core kind ("apps" on a Pod) is the agent
|
|
448
|
+
// misremembering the API, not naming a different object: no CRD lives in a
|
|
449
|
+
// built-in group, so nothing else could be meant. A vendor group on a core
|
|
450
|
+
// kind (a Knative Service) still names a different object.
|
|
407
451
|
if (
|
|
408
452
|
subject.group !== undefined &&
|
|
409
453
|
identity.group !== undefined &&
|
|
410
|
-
subject.group.toLowerCase() !== identity.group.toLowerCase()
|
|
454
|
+
subject.group.toLowerCase() !== identity.group.toLowerCase() &&
|
|
455
|
+
!(identity.group === "" && isBuiltInGroup(subject.group.toLowerCase()))
|
|
411
456
|
) {
|
|
412
457
|
return false;
|
|
413
458
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type IssueRecentChange } from "@skyhook-io/k8s-ui";
|
|
2
|
+
import {
|
|
3
|
+
ProjectionBuilder,
|
|
4
|
+
addChanges,
|
|
5
|
+
addNarrowHint,
|
|
6
|
+
changesSubjectFromArgs,
|
|
7
|
+
invalidPayload,
|
|
8
|
+
scopeFromArgs,
|
|
9
|
+
sourceArgsRelevance,
|
|
10
|
+
} from "../observations";
|
|
11
|
+
import { nonEmptyString, parseJSON, recentChange, record } from "../parse";
|
|
12
|
+
import { type InvestigationEvidenceSource } from "../types";
|
|
13
|
+
|
|
14
|
+
export function adaptChanges(
|
|
15
|
+
builder: ProjectionBuilder,
|
|
16
|
+
source: InvestigationEvidenceSource,
|
|
17
|
+
payload: unknown,
|
|
18
|
+
): void {
|
|
19
|
+
const value = record(payload);
|
|
20
|
+
// The producer serializes an empty result as a nil slice, which is null.
|
|
21
|
+
const changesRaw = value?.changes === null ? [] : value?.changes;
|
|
22
|
+
if (!value || !Array.isArray(changesRaw)) {
|
|
23
|
+
invalidPayload(builder, source, "Recent changes");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const changes = changesRaw
|
|
27
|
+
.map(recentChange)
|
|
28
|
+
.filter((item): item is IssueRecentChange => Boolean(item));
|
|
29
|
+
if (changes.length !== changesRaw.length) {
|
|
30
|
+
invalidPayload(builder, source, "Recent changes");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
addNarrowHint(builder, source, value);
|
|
34
|
+
let sourceErrors = 0;
|
|
35
|
+
if (Array.isArray(value.sourcesErrored)) {
|
|
36
|
+
for (const sourceError of value.sourcesErrored) {
|
|
37
|
+
if (nonEmptyString(sourceError)) {
|
|
38
|
+
sourceErrors += 1;
|
|
39
|
+
builder.limit(source, "Recent changes", sourceError, "error");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} else if (value.sourcesErrored !== undefined) {
|
|
43
|
+
invalidPayload(builder, source, "Recent changes source coverage");
|
|
44
|
+
}
|
|
45
|
+
// Reads of one resource's history are revisions of one card however the
|
|
46
|
+
// window or cap differs; the window is stated in the title instead.
|
|
47
|
+
const args = record(source.args ? parseJSON(source.args) : undefined);
|
|
48
|
+
addChanges(
|
|
49
|
+
builder,
|
|
50
|
+
source,
|
|
51
|
+
changes,
|
|
52
|
+
`changes:${scopeFromArgs(source)}`,
|
|
53
|
+
undefined,
|
|
54
|
+
!nonEmptyString(value.narrowHint) && sourceErrors === 0,
|
|
55
|
+
false,
|
|
56
|
+
sourceArgsRelevance(builder, source),
|
|
57
|
+
changesSubjectFromArgs(source),
|
|
58
|
+
nonEmptyString(args?.since) ? args.since : undefined,
|
|
59
|
+
);
|
|
60
|
+
}
|