@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.
Files changed (201) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/api/client.authRedirect.test.ts +12 -1
  4. package/src/api/client.prometheusStatus.test.ts +24 -0
  5. package/src/api/client.ts +321 -16
  6. package/src/api/diagnose.ts +1 -0
  7. package/src/api/drain-plan.test.ts +86 -0
  8. package/src/api/quotas.test.ts +26 -0
  9. package/src/api/quotas.ts +20 -0
  10. package/src/api/workloadMetrics.test.ts +40 -0
  11. package/src/api/workloadMetrics.ts +68 -0
  12. package/src/components/CloudConnectFlow.tsx +179 -42
  13. package/src/components/CloudFunnelButton.tsx +107 -36
  14. package/src/components/SyncProgressPanel.tsx +73 -0
  15. package/src/components/audit/AuditView.tsx +2 -0
  16. package/src/components/cloudConnectHandoff.test.ts +140 -0
  17. package/src/components/cloudConnectHandoff.ts +124 -0
  18. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  19. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  20. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  21. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  22. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  23. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  24. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  25. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  26. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  27. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  28. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  29. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  30. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  35. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  36. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  37. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  38. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  39. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  40. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  41. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  42. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  43. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  44. package/src/components/diagnose/target.test.ts +19 -0
  45. package/src/components/diagnose/target.ts +3 -2
  46. package/src/components/gitops/GitOpsView.tsx +7 -0
  47. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  48. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  49. package/src/components/resource/PrometheusCharts.tsx +4 -3
  50. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  51. package/src/components/resource/PrometheusChartsGrid.tsx +75 -35
  52. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  53. package/src/components/resource/RightsizingStrip.tsx +15 -3
  54. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  55. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  56. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  57. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  58. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  59. package/src/components/resource/workloadMetricValues.ts +30 -0
  60. package/src/components/resources/ResourcesView.tsx +44 -6
  61. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  62. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  63. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  64. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  65. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  66. package/src/components/settings/SettingsDialog.tsx +94 -14
  67. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  68. package/src/components/traffic/TrafficGraph.tsx +6 -6
  69. package/src/components/traffic/TrafficView.tsx +10 -9
  70. package/src/components/traffic/trafficFilters.test.ts +28 -1
  71. package/src/components/traffic/trafficFilters.ts +31 -0
  72. package/src/components/workload/WorkloadView.tsx +19 -8
  73. package/src/context/ConnectionContext.tsx +48 -3
  74. package/src/context/DiagnoseCustomization.tsx +1 -0
  75. package/src/index.css +17 -0
  76. package/src/utils/navigation.ts +1 -1
  77. package/src/utils/topology-namespace.test.ts +64 -0
  78. package/src/utils/topology-namespace.ts +18 -0
  79. package/src/components/diagnose/investigationEvidence.test.ts +0 -6617
  80. package/src/components/diagnose/investigationEvidence.ts +0 -5572
  81. package/src/components/gitops/GitOpsActions.tsx +0 -1
  82. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  83. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  84. package/src/components/gitops/SyncCountdown.tsx +0 -1
  85. package/src/components/gitops/index.ts +0 -4
  86. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  87. package/src/components/logs/JsonLogLine.tsx +0 -1
  88. package/src/components/logs/LogCore.tsx +0 -2
  89. package/src/components/logs/useLogBuffer.ts +0 -2
  90. package/src/components/logs/useLogSearch.ts +0 -1
  91. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  92. package/src/components/resources/drawer-components.tsx +0 -1
  93. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  94. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  95. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  96. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  97. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  98. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  99. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  100. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  101. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  102. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  103. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  104. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  105. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  106. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  107. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  108. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  109. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  110. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  111. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  112. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  113. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  114. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  115. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  116. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  117. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  118. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  119. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  120. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  121. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  122. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  162. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  169. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  170. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  171. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  172. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  173. package/src/components/resources/renderers/index.ts +0 -94
  174. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  175. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  176. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  177. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  178. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  179. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  180. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  181. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  182. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  183. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  184. package/src/components/resources/resource-utils-argo.ts +0 -2
  185. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  186. package/src/components/resources/resource-utils-eso.ts +0 -2
  187. package/src/components/resources/resource-utils-flux.ts +0 -2
  188. package/src/components/resources/resource-utils-istio.ts +0 -2
  189. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  190. package/src/components/resources/resource-utils-knative.ts +0 -2
  191. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  192. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  193. package/src/components/resources/resource-utils-traefik.ts +0 -1
  194. package/src/components/resources/resource-utils-trivy.ts +0 -2
  195. package/src/components/resources/resource-utils-velero.ts +0 -2
  196. package/src/components/shared/EditableYamlView.tsx +0 -24
  197. package/src/components/timeline/DiffViewer.tsx +0 -1
  198. package/src/components/traffic/index.ts +0 -3
  199. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  200. package/src/components/ui/MetricsChart.tsx +0 -1
  201. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -0,0 +1,172 @@
1
+ import { type Issue, type IssueRecentChange } from "@skyhook-io/k8s-ui";
2
+ import { type DiagnosisDNSContext } from "../../diagnoseEvidenceTypes";
3
+ import {
4
+ ProjectionBuilder,
5
+ addChanges,
6
+ addIssueObservation,
7
+ addNarrowHint,
8
+ evidenceTierForRelevance,
9
+ invalidPayload,
10
+ scopeFromArgs,
11
+ sourceArgsRelevance,
12
+ } from "../observations";
13
+ import {
14
+ issue,
15
+ nonEmptyString,
16
+ parseJSON,
17
+ recentChange,
18
+ record,
19
+ stringArray,
20
+ } from "../parse";
21
+ import { type InvestigationEvidenceSource } from "../types";
22
+
23
+ export function adaptIssues(
24
+ builder: ProjectionBuilder,
25
+ source: InvestigationEvidenceSource,
26
+ payload: unknown,
27
+ ): void {
28
+ const value = record(payload);
29
+ const raw = value?.issues;
30
+ if (
31
+ !value ||
32
+ !Array.isArray(raw) ||
33
+ typeof value.total !== "number" ||
34
+ typeof value.total_matched !== "number"
35
+ ) {
36
+ invalidPayload(builder, source);
37
+ return;
38
+ }
39
+ const issues = raw.map(issue).filter((item): item is Issue => Boolean(item));
40
+ if (issues.length !== raw.length) {
41
+ invalidPayload(builder, source);
42
+ return;
43
+ }
44
+ addNarrowHint(builder, source, value);
45
+ if (
46
+ value.total_matched > issues.length &&
47
+ !nonEmptyString(value.narrowHint)
48
+ ) {
49
+ builder.limit(
50
+ source,
51
+ "Issues",
52
+ `The query returned ${issues.length} of ${value.total_matched} matching issues.`,
53
+ "truncated",
54
+ );
55
+ }
56
+ if (typeof value.filter_errors === "number" && value.filter_errors > 0) {
57
+ builder.limit(
58
+ source,
59
+ "Issues filter",
60
+ nonEmptyString(value.filter_error_sample)
61
+ ? value.filter_error_sample
62
+ : `${value.filter_errors} issue rows could not be evaluated by the filter.`,
63
+ "error",
64
+ );
65
+ }
66
+ if (value.recent_changes_truncated === true) {
67
+ builder.limit(
68
+ source,
69
+ "Issue-related changes",
70
+ "The issue response omitted some recent changes.",
71
+ "truncated",
72
+ );
73
+ }
74
+ if (value.correlation_truncated === true) {
75
+ builder.limit(
76
+ source,
77
+ "Issue change correlation",
78
+ "Change correlation was not evaluated for every returned issue.",
79
+ "truncated",
80
+ );
81
+ }
82
+ const visibility = record(value.visibility);
83
+ if (
84
+ visibility &&
85
+ nonEmptyString(visibility.state) &&
86
+ visibility.state !== "ok"
87
+ ) {
88
+ builder.limit(
89
+ source,
90
+ "Issue visibility",
91
+ nonEmptyString(visibility.impact)
92
+ ? visibility.impact
93
+ : `Radar reported ${visibility.state} visibility for this issue query.`,
94
+ "unknown",
95
+ );
96
+ }
97
+ const issueChangesRaw = value.recent_changes;
98
+ if (Array.isArray(issueChangesRaw)) {
99
+ const changes = issueChangesRaw
100
+ .map(recentChange)
101
+ .filter((item): item is IssueRecentChange => Boolean(item));
102
+ if (changes.length === issueChangesRaw.length) {
103
+ addChanges(
104
+ builder,
105
+ source,
106
+ changes,
107
+ `changes:issues:${source.args ?? scopeFromArgs(source)}`,
108
+ undefined,
109
+ value.recent_changes_truncated !== true,
110
+ );
111
+ } else {
112
+ invalidPayload(builder, source, "Issue-related changes");
113
+ }
114
+ } else if (issueChangesRaw !== undefined) {
115
+ invalidPayload(builder, source, "Issue-related changes");
116
+ }
117
+ const clusterDNS = record(record(value.cluster_context)?.dns);
118
+ if (clusterDNS) {
119
+ const signals = stringArray(clusterDNS.signals) ?? [];
120
+ const findings = Array.isArray(clusterDNS.findings)
121
+ ? clusterDNS.findings
122
+ : [];
123
+ if (signals.length > 0 || findings.length > 0) {
124
+ builder.observe(`dns:issues:${scopeFromArgs(source)}`, "dns", source, {
125
+ tier: "context",
126
+ relevance: "broader",
127
+ tone: "warning",
128
+ title: "Cluster DNS signals",
129
+ summary:
130
+ signals[0] ||
131
+ `${findings.length} CoreDNS finding${findings.length === 1 ? "" : "s"}`,
132
+ data: {
133
+ type: "dns",
134
+ dns: {
135
+ signals,
136
+ coreDNSFindings: findings as DiagnosisDNSContext["coreDNSFindings"],
137
+ },
138
+ },
139
+ });
140
+ }
141
+ }
142
+ if (issues.length === 0) {
143
+ if (!source.confirmedSuccess || builder.limitedSources.has(source.id))
144
+ return;
145
+ const args = record(source.args ? parseJSON(source.args) : undefined);
146
+ if (!nonEmptyString(args?.namespace)) {
147
+ builder.limit(
148
+ source,
149
+ "Issues",
150
+ "Radar found no matching issues, but the search covered only namespaces the current user can access.",
151
+ "unknown",
152
+ );
153
+ return;
154
+ }
155
+ const scope = scopeFromArgs(source);
156
+ const relevance = sourceArgsRelevance(builder, source);
157
+ builder.observe(`issues:${source.args ?? scope}`, "receipt", source, {
158
+ tier: evidenceTierForRelevance("checked", relevance),
159
+ relevance,
160
+ tone: "neutral",
161
+ title: "No live issues matched this search",
162
+ summary: scope,
163
+ data: {
164
+ type: "receipt",
165
+ checked: "issues",
166
+ scope,
167
+ },
168
+ });
169
+ } else {
170
+ for (const item of issues) addIssueObservation(builder, source, item);
171
+ }
172
+ }
@@ -0,0 +1,283 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ investigationEvidenceSubjectRef,
4
+ projectInvestigationEvidence,
5
+ resolveInvestigationRootCauseEvidence,
6
+ } from "../index";
7
+ import {
8
+ evidenceRef,
9
+ groupsOf,
10
+ project,
11
+ tool,
12
+ workloadLogsArgs,
13
+ workloadLogsPayload,
14
+ } from "../evidenceFixtures";
15
+
16
+ // The get_workload_logs response shape, with the stream rows fetchPodLogs
17
+ // emits per pod and container.
18
+
19
+ describe("workload logs adapter", () => {
20
+ it("keeps one multi-container result as one source with several observations", () => {
21
+ const ref = evidenceRef("a", "b");
22
+ const projection = project([
23
+ tool("wl-logs", "get_workload_logs", workloadLogsPayload, {
24
+ summary: workloadLogsArgs,
25
+ evidenceRef: ref,
26
+ }),
27
+ ]);
28
+ const logs = groupsOf(projection.groups, "logs");
29
+ expect(logs.map((group) => group.identity)).toEqual([
30
+ "logs:current:api-68c7b766dc-fmphn:api",
31
+ "logs:current:api-68c7b766dc-fmphn:istio-proxy",
32
+ "logs:current:api-68c7b766dc-q2zxr:api",
33
+ ]);
34
+ expect(projection.sources).toHaveLength(1);
35
+ expect(projection.coverage).toMatchObject({
36
+ attempted: 1,
37
+ projected: 1,
38
+ limited: 1,
39
+ });
40
+ expect(new Set(logs.map((group) => group.latest.source.id)).size).toBe(1);
41
+ // Diagnose relates a target workload to its pods the same way.
42
+ expect(logs.map((group) => group.latest.relevance)).toEqual([
43
+ "producer-related",
44
+ "producer-related",
45
+ "producer-related",
46
+ ]);
47
+ expect(logs[0].latest.tier).toBe("supporting");
48
+ expect(logs[0].latest.tone).toBe("warning");
49
+ expect(logs[0].latest.data).toMatchObject({
50
+ type: "logs",
51
+ namespace: "shop",
52
+ previous: false,
53
+ warnings: workloadLogsPayload.warnings,
54
+ });
55
+ expect(
56
+ (logs[0].latest.data as { logs?: { lines: string[] } }).logs?.lines[0],
57
+ ).not.toContain("[");
58
+ expect(logs[1].latest.tier).toBe("context");
59
+ expect(investigationEvidenceSubjectRef(logs[2].latest.data)).toEqual({
60
+ kind: "Pod",
61
+ namespace: "shop",
62
+ name: "api-68c7b766dc-q2zxr",
63
+ });
64
+
65
+ const resolution = resolveInvestigationRootCauseEvidence(
66
+ projection,
67
+ { status: "linked", refs: [ref] },
68
+ 0,
69
+ );
70
+ expect(resolution.status).toBe("linked");
71
+ expect(resolution.links[0].source.stepId).toBe("wl-logs");
72
+ expect(resolution.links[0].originalGroupId).toBe(logs[0].id);
73
+ expect(projection.sources[0].primaryGroupId).toBe(logs[0].id);
74
+ expect(
75
+ projection.limitations.map((limitation) => limitation.source),
76
+ ).toEqual(["Workload logs"]);
77
+ expect(projection.limitations[0].kind).toBe("truncated");
78
+ });
79
+
80
+ it("accepts every workload kind the producer resolves, including Rollouts", () => {
81
+ const rollout = projectInvestigationEvidence(
82
+ [
83
+ {
84
+ timeline: [
85
+ tool(
86
+ "wl-rollout",
87
+ "get_workload_logs",
88
+ { ...workloadLogsPayload, workload: "rollouts/shop/api" },
89
+ {
90
+ summary: JSON.stringify({
91
+ namespace: "shop",
92
+ name: "api",
93
+ kind: "rollout",
94
+ }),
95
+ },
96
+ ),
97
+ ],
98
+ },
99
+ ],
100
+ { kind: "Rollout", group: "argoproj.io", namespace: "shop", name: "api" },
101
+ );
102
+ expect(
103
+ groupsOf(rollout.groups, "logs").map((group) => group.latest.relevance),
104
+ ).toEqual(["producer-related", "producer-related", "producer-related"]);
105
+ expect(rollout.limitations.map((limitation) => limitation.kind)).toEqual([
106
+ "truncated",
107
+ ]);
108
+ });
109
+
110
+ it("scopes a sibling workload's logs as broader and a Pod target's own row as target", () => {
111
+ const sibling = project([
112
+ tool(
113
+ "wl-sibling",
114
+ "get_workload_logs",
115
+ { ...workloadLogsPayload, workload: "deployments/shop/api-worker" },
116
+ {
117
+ summary: JSON.stringify({
118
+ namespace: "shop",
119
+ name: "api-worker",
120
+ kind: "deployment",
121
+ }),
122
+ },
123
+ ),
124
+ ]);
125
+ expect(
126
+ groupsOf(sibling.groups, "logs").map((group) => group.latest.relevance),
127
+ ).toEqual(["broader", "broader", "broader"]);
128
+ expect(
129
+ groupsOf(sibling.groups, "logs").map((group) => group.latest.tier),
130
+ ).toEqual(["context", "context", "context"]);
131
+
132
+ const podTarget = projectInvestigationEvidence(
133
+ [
134
+ {
135
+ timeline: [
136
+ tool("wl-pod", "get_workload_logs", workloadLogsPayload, {
137
+ summary: workloadLogsArgs,
138
+ }),
139
+ ],
140
+ },
141
+ ],
142
+ {
143
+ kind: "Pod",
144
+ group: "",
145
+ namespace: "shop",
146
+ name: "api-68c7b766dc-q2zxr",
147
+ },
148
+ );
149
+ expect(
150
+ groupsOf(podTarget.groups, "logs").map((group) => group.latest.relevance),
151
+ ).toEqual(["broader", "broader", "target"]);
152
+ });
153
+
154
+ it("reports a failed read as a limitation and no pods as a scoped receipt", () => {
155
+ const failed = project([
156
+ tool(
157
+ "wl-error",
158
+ "get_workload_logs",
159
+ {
160
+ workload: "deployments/shop/api",
161
+ pods: 2,
162
+ logsError: "no kube client in request context",
163
+ },
164
+ { summary: workloadLogsArgs },
165
+ ),
166
+ ]);
167
+ expect(failed.groups).toHaveLength(0);
168
+ expect(failed.limitations).toEqual([
169
+ expect.objectContaining({
170
+ source: "Workload logs",
171
+ message: "no kube client in request context",
172
+ kind: "error",
173
+ }),
174
+ ]);
175
+
176
+ const empty = project([
177
+ tool(
178
+ "wl-empty",
179
+ "get_workload_logs",
180
+ {
181
+ workload: "jobs/shop/api",
182
+ pods: 0,
183
+ logs: "No pods found for this Job yet. Check scheduling, admission, or controller events.",
184
+ emptyReason: "no-pods",
185
+ emptyMessage:
186
+ "No pods found for this Job yet. Check scheduling, admission, or controller events.",
187
+ command: "kubectl logs job/api -n shop",
188
+ },
189
+ {
190
+ summary: JSON.stringify({
191
+ namespace: "shop",
192
+ name: "api",
193
+ kind: "job",
194
+ }),
195
+ },
196
+ ),
197
+ ]);
198
+ const receipt = groupsOf(empty.groups, "receipt")[0].latest;
199
+ expect(receipt.tier).toBe("context");
200
+ expect(receipt.relevance).toBe("broader");
201
+ expect(receipt.data).toMatchObject({
202
+ type: "receipt",
203
+ checked: "logs",
204
+ scope: "Job shop/api",
205
+ message:
206
+ "No pods found for this Job yet. Check scheduling, admission, or controller events.",
207
+ });
208
+ expect(empty.limitations).toHaveLength(0);
209
+
210
+ const unconfirmed = project([
211
+ tool(
212
+ "wl-unconfirmed",
213
+ "get_workload_logs",
214
+ { workload: "deployments/shop/api", pods: 0, logs: "no pods" },
215
+ { summary: workloadLogsArgs, isError: undefined },
216
+ ),
217
+ ]);
218
+ expect(groupsOf(unconfirmed.groups, "receipt")).toHaveLength(0);
219
+ });
220
+
221
+ it("rejects malformed payloads and per-stream errors honestly", () => {
222
+ const malformed = project([
223
+ tool(
224
+ "wl-bad",
225
+ "get_workload_logs",
226
+ { workload: "pods/shop/api", pods: 1, logs: [] },
227
+ { summary: workloadLogsArgs },
228
+ ),
229
+ tool(
230
+ "wl-stream-error",
231
+ "get_workload_logs",
232
+ {
233
+ workload: "deployments/shop/api",
234
+ pods: 1,
235
+ logs: [
236
+ {
237
+ pod: "api-68c7b766dc-fmphn",
238
+ container: "api",
239
+ error: "failed to get logs: container is waiting to start",
240
+ },
241
+ ],
242
+ },
243
+ { summary: workloadLogsArgs },
244
+ ),
245
+ tool(
246
+ "wl-null",
247
+ "get_workload_logs",
248
+ { workload: "deployments/shop/api", pods: 3, logs: null },
249
+ { summary: workloadLogsArgs },
250
+ ),
251
+ tool(
252
+ "wl-zero-with-streams",
253
+ "get_workload_logs",
254
+ {
255
+ workload: "deployments/shop/api",
256
+ pods: 0,
257
+ logs: workloadLogsPayload.logs,
258
+ },
259
+ { summary: workloadLogsArgs },
260
+ ),
261
+ ]);
262
+ expect(malformed.groups).toHaveLength(0);
263
+ expect(
264
+ malformed.limitations.map((limitation) => [
265
+ limitation.source,
266
+ limitation.kind,
267
+ ]),
268
+ ).toEqual([
269
+ ["Workload logs", "unknown"],
270
+ ["api-68c7b766dc-fmphn / api", "error"],
271
+ ["Workload logs", "unknown"],
272
+ ]);
273
+ expect(malformed.limitations[0].sources.map((item) => item.stepId)).toEqual(
274
+ ["wl-bad", "wl-zero-with-streams"],
275
+ );
276
+ expect(malformed.limitations[0].message).toContain(
277
+ "couldn't summarize this investigation step",
278
+ );
279
+ expect(malformed.limitations[2].message).toContain(
280
+ "Radar found 3 pods but got no logs from them",
281
+ );
282
+ });
283
+ });
@@ -0,0 +1,183 @@
1
+ import { displayKind } from "@skyhook-io/k8s-ui";
2
+ import {
3
+ ProjectionBuilder,
4
+ addLogs,
5
+ addNarrowHint,
6
+ evidenceTierForRelevance,
7
+ invalidPayload,
8
+ previousFromArgs,
9
+ relevanceForResource,
10
+ resourceMatchesTarget,
11
+ sourceArgsRelevance,
12
+ } from "../observations";
13
+ import {
14
+ filteredLogs,
15
+ nonEmptyString,
16
+ nonNegativeInteger,
17
+ parseJSON,
18
+ parseLogEntry,
19
+ record,
20
+ stringArray,
21
+ } from "../parse";
22
+ import {
23
+ type InvestigationEvidenceRelevance,
24
+ type InvestigationEvidenceSource,
25
+ } from "../types";
26
+
27
+ export function adaptPodLogs(
28
+ builder: ProjectionBuilder,
29
+ source: InvestigationEvidenceSource,
30
+ payload: unknown,
31
+ ): void {
32
+ const value = record(payload);
33
+ const logs = filteredLogs(payload) ?? filteredLogs(value);
34
+ if (!logs) {
35
+ invalidPayload(builder, source);
36
+ return;
37
+ }
38
+ if (value) addNarrowHint(builder, source, value);
39
+ const warnings = stringArray(value?.warnings) ?? [];
40
+ const args = record(source.args ? parseJSON(source.args) : undefined);
41
+ const pod = nonEmptyString(args?.name) ? args.name : "Pod";
42
+ const container = nonEmptyString(args?.container)
43
+ ? args.container
44
+ : "default container";
45
+ addLogs(
46
+ builder,
47
+ source,
48
+ { pod, container, logs },
49
+ previousFromArgs(source),
50
+ warnings,
51
+ sourceArgsRelevance(builder, source, "Pod"),
52
+ nonEmptyString(args?.namespace) ? args.namespace : undefined,
53
+ );
54
+ }
55
+ const WORKLOAD_LOG_KINDS: Readonly<Record<string, string>> = {
56
+ deployments: "Deployment",
57
+ statefulsets: "StatefulSet",
58
+ daemonsets: "DaemonSet",
59
+ rollouts: "Rollout",
60
+ jobs: "Job",
61
+ workflows: "Workflow",
62
+ };
63
+ /** `get_workload_logs` states what it read as `<plural>/<namespace>/<name>`. */
64
+ function workloadLogsSubject(
65
+ value: string,
66
+ ): { kind: string; namespace: string; name: string } | undefined {
67
+ const parts = value.split("/");
68
+ if (parts.length !== 3) return undefined;
69
+ const kind = WORKLOAD_LOG_KINDS[parts[0]];
70
+ if (!kind || !parts[1] || !parts[2]) return undefined;
71
+ return { kind, namespace: parts[1], name: parts[2] };
72
+ }
73
+ export function adaptWorkloadLogs(
74
+ builder: ProjectionBuilder,
75
+ source: InvestigationEvidenceSource,
76
+ payload: unknown,
77
+ ): void {
78
+ const value = record(payload);
79
+ const workload = nonEmptyString(value?.workload)
80
+ ? workloadLogsSubject(value.workload)
81
+ : undefined;
82
+ if (!value || !workload || !nonNegativeInteger(value.pods)) {
83
+ invalidPayload(builder, source);
84
+ return;
85
+ }
86
+ // The producer names the workload without an API group; as with other
87
+ // group-less producers, the investigation target supplies that dimension.
88
+ const workloadRelevance = relevanceForResource(builder, {
89
+ ...workload,
90
+ group: builder.target.group,
91
+ });
92
+ const scope = `${displayKind(workload.kind)} ${workload.namespace}/${workload.name}`;
93
+ const previous = previousFromArgs(source);
94
+ addNarrowHint(builder, source, value);
95
+ if (nonEmptyString(value.logsError)) {
96
+ builder.limit(source, "Workload logs", value.logsError, "error");
97
+ return;
98
+ }
99
+ if (value.pods === 0) {
100
+ // The producer replaces the stream list with a message when it resolved
101
+ // no pods; any other shape is not this response.
102
+ if (
103
+ typeof value.logs !== "string" ||
104
+ (value.emptyMessage !== undefined &&
105
+ typeof value.emptyMessage !== "string") ||
106
+ value.narrowHint !== undefined
107
+ ) {
108
+ invalidPayload(builder, source);
109
+ return;
110
+ }
111
+ if (!source.confirmedSuccess) return;
112
+ // The producer's own sentence when it gave one; otherwise nothing, because
113
+ // "no pods to read logs from" is already the whole answer.
114
+ const message = nonEmptyString(value.emptyMessage)
115
+ ? value.emptyMessage
116
+ : nonEmptyString(value.logs)
117
+ ? value.logs
118
+ : undefined;
119
+ builder.observe(
120
+ `workload-logs:${previous ? "previous" : "current"}:${scope}`,
121
+ "receipt",
122
+ source,
123
+ {
124
+ tier: evidenceTierForRelevance("checked", workloadRelevance),
125
+ relevance: workloadRelevance,
126
+ tone: "neutral",
127
+ title: "No pods to read logs from",
128
+ summary: scope,
129
+ data: { type: "receipt", checked: "logs", scope, message },
130
+ },
131
+ );
132
+ return;
133
+ }
134
+ const logsRaw = value.logs;
135
+ const noStreams = `Radar found ${value.pods} pod${value.pods === 1 ? "" : "s"} but got no logs from ${value.pods === 1 ? "it" : "them"}, so the logs were not checked.`;
136
+ if (!Array.isArray(logsRaw)) {
137
+ if (logsRaw === undefined || logsRaw === null) {
138
+ builder.limit(source, "Workload logs", noStreams, "unknown");
139
+ } else {
140
+ invalidPayload(builder, source);
141
+ }
142
+ return;
143
+ }
144
+ const warnings = stringArray(value.warnings) ?? [];
145
+ for (const raw of logsRaw) {
146
+ const entry = parseLogEntry(raw);
147
+ if (!entry) {
148
+ invalidPayload(
149
+ builder,
150
+ source,
151
+ previous ? "Previous logs" : "Current logs",
152
+ );
153
+ continue;
154
+ }
155
+ // A Pod target is named exactly by its own row; a workload target relates
156
+ // to its rows the way diagnose relates to the pods it resolved.
157
+ const rowRelevance: InvestigationEvidenceRelevance = resourceMatchesTarget(
158
+ builder.target,
159
+ {
160
+ kind: "Pod",
161
+ group: "",
162
+ namespace: workload.namespace,
163
+ name: entry.pod,
164
+ },
165
+ )
166
+ ? "target"
167
+ : workloadRelevance === "target"
168
+ ? "producer-related"
169
+ : "broader";
170
+ addLogs(
171
+ builder,
172
+ source,
173
+ entry,
174
+ previous,
175
+ warnings,
176
+ rowRelevance,
177
+ workload.namespace,
178
+ );
179
+ }
180
+ if (logsRaw.length === 0) {
181
+ builder.limit(source, "Workload logs", noStreams, "unknown");
182
+ }
183
+ }