@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.
Files changed (230) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/RadarApp.tsx +5 -0
  4. package/src/api/client.authRedirect.test.ts +12 -1
  5. package/src/api/client.prometheusStatus.test.ts +24 -0
  6. package/src/api/client.ts +325 -16
  7. package/src/api/diagnose.ts +62 -15
  8. package/src/api/drain-plan.test.ts +86 -0
  9. package/src/api/quotas.test.ts +26 -0
  10. package/src/api/quotas.ts +20 -0
  11. package/src/api/workloadMetrics.test.ts +40 -0
  12. package/src/api/workloadMetrics.ts +68 -0
  13. package/src/components/CloudConnectFlow.tsx +179 -42
  14. package/src/components/CloudFunnelButton.tsx +107 -36
  15. package/src/components/ConnectionErrorView.test.tsx +30 -0
  16. package/src/components/ConnectionErrorView.tsx +31 -19
  17. package/src/components/SyncProgressPanel.tsx +73 -0
  18. package/src/components/audit/AuditView.tsx +2 -0
  19. package/src/components/cloudConnectHandoff.test.ts +140 -0
  20. package/src/components/cloudConnectHandoff.ts +124 -0
  21. package/src/components/diagnose/AgentCase.tsx +131 -0
  22. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  23. package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
  24. package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
  25. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
  26. package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
  27. package/src/components/diagnose/InvestigationView.tsx +222 -5
  28. package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
  29. package/src/components/diagnose/investigationCase.test.tsx +1568 -0
  30. package/src/components/diagnose/investigationCase.ts +439 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  48. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  49. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  50. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  51. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  52. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  53. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  54. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  55. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  56. package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
  57. package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
  58. package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
  59. package/src/components/diagnose/investigationMetrics.test.ts +712 -0
  60. package/src/components/diagnose/investigationMetrics.ts +393 -0
  61. package/src/components/diagnose/investigationSourceFocus.ts +2 -0
  62. package/src/components/diagnose/investigationState.test.ts +322 -0
  63. package/src/components/diagnose/investigationState.ts +147 -25
  64. package/src/components/diagnose/parts.test.tsx +482 -0
  65. package/src/components/diagnose/parts.tsx +418 -41
  66. package/src/components/diagnose/target.test.ts +19 -0
  67. package/src/components/diagnose/target.ts +3 -2
  68. package/src/components/gitops/GitOpsView.tsx +7 -0
  69. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  70. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  71. package/src/components/resource/PrometheusCharts.tsx +4 -3
  72. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  73. package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
  74. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  75. package/src/components/resource/RightsizingStrip.tsx +15 -3
  76. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  77. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  78. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  79. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  80. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  81. package/src/components/resource/workloadMetricValues.ts +30 -0
  82. package/src/components/resources/PodFilePreview.test.tsx +131 -0
  83. package/src/components/resources/PodFilePreview.tsx +394 -0
  84. package/src/components/resources/PodFilesystemModal.tsx +157 -67
  85. package/src/components/resources/ResourcesView.tsx +44 -6
  86. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  87. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  88. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  89. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  90. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  91. package/src/components/settings/SettingsDialog.tsx +94 -14
  92. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  93. package/src/components/traffic/TrafficGraph.tsx +6 -6
  94. package/src/components/traffic/TrafficView.tsx +10 -9
  95. package/src/components/traffic/trafficFilters.test.ts +28 -1
  96. package/src/components/traffic/trafficFilters.ts +31 -0
  97. package/src/components/workload/WorkloadView.tsx +19 -8
  98. package/src/context/ConnectionContext.tsx +48 -3
  99. package/src/context/DiagnoseCustomization.test.tsx +26 -0
  100. package/src/context/DiagnoseCustomization.tsx +15 -2
  101. package/src/index.css +17 -0
  102. package/src/index.ts +1 -0
  103. package/src/utils/navigation.ts +1 -1
  104. package/src/utils/shell-safe.test.ts +25 -1
  105. package/src/utils/shell-safe.ts +16 -0
  106. package/src/utils/topology-namespace.test.ts +64 -0
  107. package/src/utils/topology-namespace.ts +18 -0
  108. package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
  109. package/src/components/diagnose/investigationEvidence.ts +0 -4784
  110. package/src/components/gitops/GitOpsActions.tsx +0 -1
  111. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  112. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  113. package/src/components/gitops/SyncCountdown.tsx +0 -1
  114. package/src/components/gitops/index.ts +0 -4
  115. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  116. package/src/components/logs/JsonLogLine.tsx +0 -1
  117. package/src/components/logs/LogCore.tsx +0 -2
  118. package/src/components/logs/useLogBuffer.ts +0 -2
  119. package/src/components/logs/useLogSearch.ts +0 -1
  120. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  121. package/src/components/resources/drawer-components.tsx +0 -1
  122. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  191. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  198. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  199. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  200. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  201. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  202. package/src/components/resources/renderers/index.ts +0 -94
  203. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  204. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  205. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  206. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  207. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  208. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  209. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  210. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  211. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  212. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  213. package/src/components/resources/resource-utils-argo.ts +0 -2
  214. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  215. package/src/components/resources/resource-utils-eso.ts +0 -2
  216. package/src/components/resources/resource-utils-flux.ts +0 -2
  217. package/src/components/resources/resource-utils-istio.ts +0 -2
  218. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  219. package/src/components/resources/resource-utils-knative.ts +0 -2
  220. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  221. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  222. package/src/components/resources/resource-utils-traefik.ts +0 -1
  223. package/src/components/resources/resource-utils-trivy.ts +0 -2
  224. package/src/components/resources/resource-utils-velero.ts +0 -2
  225. package/src/components/shared/EditableYamlView.tsx +0 -24
  226. package/src/components/timeline/DiffViewer.tsx +0 -1
  227. package/src/components/traffic/index.ts +0 -3
  228. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  229. package/src/components/ui/MetricsChart.tsx +0 -1
  230. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -0,0 +1,467 @@
1
+ import {
2
+ type Issue,
3
+ type IssueRecentChange,
4
+ stripAnsi,
5
+ } from "@skyhook-io/k8s-ui";
6
+ import { type TimeSeries } from "@skyhook-io/k8s-ui/components/charts";
7
+ import {
8
+ type DiagnosisChangeContext,
9
+ type DiagnosisCrashCause,
10
+ type DiagnosisFilteredLogs,
11
+ type DiagnosisPodContainerRef,
12
+ type DiagnosisPodLogEntry,
13
+ type DiagnosisResourceRef,
14
+ type DiagnosisStartupBlocker,
15
+ } from "../diagnoseEvidenceTypes";
16
+ import {
17
+ type InvestigationEventEvidence,
18
+ type InvestigationGitOpsDiagnosis,
19
+ type InvestigationKubernetesResource,
20
+ type InvestigationNetworkEvidence,
21
+ type InvestigationNetworkRoute,
22
+ type InvestigationResourceSummary,
23
+ type InvestigationTopologyEdge,
24
+ type InvestigationTopologyNode,
25
+ } from "./types";
26
+
27
+ export function record(value: unknown): Record<string, unknown> | undefined {
28
+ return value !== null && typeof value === "object" && !Array.isArray(value)
29
+ ? (value as Record<string, unknown>)
30
+ : undefined;
31
+ }
32
+ export function nonEmptyString(value: unknown): value is string {
33
+ return typeof value === "string" && value.trim().length > 0;
34
+ }
35
+ export function stringArray(value: unknown): string[] | undefined {
36
+ return Array.isArray(value) && value.every((item) => typeof item === "string")
37
+ ? value
38
+ : undefined;
39
+ }
40
+ export function parseJSON(value: string): unknown {
41
+ try {
42
+ return JSON.parse(value);
43
+ } catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ export function kubernetesResource(
48
+ value: unknown,
49
+ ): InvestigationKubernetesResource | undefined {
50
+ const resource = record(value);
51
+ const metadata = record(resource?.metadata);
52
+ // Core Secrets intentionally use Radar's current safe detail contract rather
53
+ // than a Kubernetes object: identity + type + key names, with no values. Make
54
+ // that producer shape canonical for the projection instead of rejecting the
55
+ // exact evidence the agent saw.
56
+ if (
57
+ resource?.kind === "Secret" &&
58
+ !metadata &&
59
+ nonEmptyString(resource.name) &&
60
+ (resource.namespace === undefined ||
61
+ typeof resource.namespace === "string") &&
62
+ (resource.type === undefined || typeof resource.type === "string") &&
63
+ Array.isArray(resource.keys) &&
64
+ resource.keys.every((key) => typeof key === "string")
65
+ ) {
66
+ return {
67
+ ...resource,
68
+ apiVersion: "v1",
69
+ metadata: {
70
+ name: resource.name,
71
+ namespace: resource.namespace as string | undefined,
72
+ ...(record(resource.labels) ? { labels: resource.labels } : {}),
73
+ ...(record(resource.annotations)
74
+ ? { annotations: resource.annotations }
75
+ : {}),
76
+ },
77
+ } as InvestigationKubernetesResource;
78
+ }
79
+ if (
80
+ !resource ||
81
+ !nonEmptyString(resource.apiVersion) ||
82
+ !nonEmptyString(resource.kind) ||
83
+ !metadata ||
84
+ !nonEmptyString(metadata.name)
85
+ ) {
86
+ return undefined;
87
+ }
88
+ return resource as unknown as InvestigationKubernetesResource;
89
+ }
90
+ export function gitOpsDiagnosis(
91
+ value: unknown,
92
+ ): InvestigationGitOpsDiagnosis | undefined {
93
+ const candidate = record(value);
94
+ if (
95
+ !candidate ||
96
+ (candidate.tool !== "argocd" && candidate.tool !== "flux")
97
+ ) {
98
+ return undefined;
99
+ }
100
+ for (const field of [
101
+ "sync",
102
+ "health",
103
+ "operationPhase",
104
+ "ready",
105
+ "appliedRevision",
106
+ ] as const) {
107
+ if (candidate[field] !== undefined && typeof candidate[field] !== "string")
108
+ return undefined;
109
+ }
110
+ if (
111
+ candidate.suspended !== undefined &&
112
+ typeof candidate.suspended !== "boolean"
113
+ )
114
+ return undefined;
115
+ return candidate as unknown as InvestigationGitOpsDiagnosis;
116
+ }
117
+ export function resourceSummary(
118
+ value: unknown,
119
+ ): InvestigationResourceSummary | undefined {
120
+ const candidate = record(value);
121
+ if (
122
+ !candidate ||
123
+ !nonEmptyString(candidate.kind) ||
124
+ !nonEmptyString(candidate.name)
125
+ ) {
126
+ return undefined;
127
+ }
128
+ if (
129
+ candidate.namespace !== undefined &&
130
+ typeof candidate.namespace !== "string"
131
+ ) {
132
+ return undefined;
133
+ }
134
+ for (const field of ["status", "ready", "issue", "age"] as const) {
135
+ if (
136
+ candidate[field] !== undefined &&
137
+ typeof candidate[field] !== "string"
138
+ ) {
139
+ return undefined;
140
+ }
141
+ }
142
+ if (
143
+ (candidate.terminating !== undefined &&
144
+ typeof candidate.terminating !== "boolean") ||
145
+ (candidate.restarts !== undefined && typeof candidate.restarts !== "number")
146
+ ) {
147
+ return undefined;
148
+ }
149
+ const summaryContext = record(candidate.summaryContext);
150
+ if (
151
+ candidate.summaryContext !== undefined &&
152
+ (!summaryContext ||
153
+ (summaryContext.health !== undefined &&
154
+ typeof summaryContext.health !== "string") ||
155
+ (summaryContext.issueCount !== undefined &&
156
+ typeof summaryContext.issueCount !== "number"))
157
+ ) {
158
+ return undefined;
159
+ }
160
+ return candidate as unknown as InvestigationResourceSummary;
161
+ }
162
+ export function issue(value: unknown): Issue | undefined {
163
+ const candidate = record(value);
164
+ if (
165
+ !candidate ||
166
+ !nonEmptyString(candidate.id) ||
167
+ (candidate.severity !== "critical" && candidate.severity !== "warning") ||
168
+ !nonEmptyString(candidate.kind) ||
169
+ !nonEmptyString(candidate.name) ||
170
+ !nonEmptyString(candidate.reason)
171
+ ) {
172
+ return undefined;
173
+ }
174
+ return candidate as unknown as Issue;
175
+ }
176
+ export function recentChange(value: unknown): IssueRecentChange | undefined {
177
+ const candidate = record(value);
178
+ if (
179
+ !candidate ||
180
+ !nonEmptyString(candidate.kind) ||
181
+ (candidate.apiVersion !== undefined &&
182
+ !nonEmptyString(candidate.apiVersion)) ||
183
+ !nonEmptyString(candidate.name) ||
184
+ !nonEmptyString(candidate.changeType) ||
185
+ !nonEmptyString(candidate.timestamp)
186
+ ) {
187
+ return undefined;
188
+ }
189
+ return candidate as unknown as IssueRecentChange;
190
+ }
191
+ export function diagnosisChangeContext(
192
+ value: unknown,
193
+ ): DiagnosisChangeContext | undefined {
194
+ const candidate = record(value);
195
+ if (!candidate || typeof candidate.changed !== "boolean") return undefined;
196
+ for (const field of ["what", "when", "evidence"] as const) {
197
+ if (candidate[field] !== undefined && typeof candidate[field] !== "string")
198
+ return undefined;
199
+ }
200
+ return {
201
+ changed: candidate.changed,
202
+ ...(typeof candidate.what === "string"
203
+ ? {
204
+ what:
205
+ candidate.what === "pod_template"
206
+ ? "The workload's Pod template changed"
207
+ : candidate.what,
208
+ }
209
+ : {}),
210
+ ...(typeof candidate.when === "string" ? { when: candidate.when } : {}),
211
+ ...(typeof candidate.evidence === "string"
212
+ ? { evidence: candidate.evidence }
213
+ : {}),
214
+ };
215
+ }
216
+ export function event(value: unknown): InvestigationEventEvidence | undefined {
217
+ const candidate = record(value);
218
+ if (
219
+ !candidate ||
220
+ !nonEmptyString(candidate.reason) ||
221
+ !nonEmptyString(candidate.message) ||
222
+ !nonEmptyString(candidate.type) ||
223
+ typeof candidate.count !== "number" ||
224
+ !nonEmptyString(candidate.lastTimestamp)
225
+ ) {
226
+ return undefined;
227
+ }
228
+ return candidate as unknown as InvestigationEventEvidence;
229
+ }
230
+ export function filteredLogs(
231
+ value: unknown,
232
+ ): DiagnosisFilteredLogs | undefined {
233
+ const candidate = record(value);
234
+ if (
235
+ !candidate ||
236
+ (!Array.isArray(candidate.lines) && candidate.lines !== null) ||
237
+ (Array.isArray(candidate.lines) &&
238
+ !candidate.lines.every((line) => typeof line === "string")) ||
239
+ typeof candidate.totalLines !== "number" ||
240
+ typeof candidate.matchedLines !== "number" ||
241
+ typeof candidate.fallback !== "boolean"
242
+ ) {
243
+ return undefined;
244
+ }
245
+ return {
246
+ ...(candidate as unknown as DiagnosisFilteredLogs),
247
+ lines: Array.isArray(candidate.lines)
248
+ ? candidate.lines.map((line) => stripAnsi(line))
249
+ : candidate.lines,
250
+ } as DiagnosisFilteredLogs;
251
+ }
252
+ export function resourceRef(value: unknown): DiagnosisResourceRef | undefined {
253
+ const candidate = record(value);
254
+ if (
255
+ !candidate ||
256
+ !nonEmptyString(candidate.kind) ||
257
+ !nonEmptyString(candidate.name)
258
+ ) {
259
+ return undefined;
260
+ }
261
+ return candidate as unknown as DiagnosisResourceRef;
262
+ }
263
+ function networkRoute(value: unknown): InvestigationNetworkRoute | undefined {
264
+ const candidate = record(value);
265
+ if (
266
+ !candidate ||
267
+ !nonEmptyString(candidate.route) ||
268
+ !nonEmptyString(candidate.outcome)
269
+ ) {
270
+ return undefined;
271
+ }
272
+ for (const field of [
273
+ "target",
274
+ "failedLayer",
275
+ "confidence",
276
+ "evidence",
277
+ ] as const) {
278
+ if (candidate[field] !== undefined && typeof candidate[field] !== "string")
279
+ return undefined;
280
+ }
281
+ if (candidate.benign !== undefined && typeof candidate.benign !== "boolean")
282
+ return undefined;
283
+ return candidate as unknown as InvestigationNetworkRoute;
284
+ }
285
+ export function networkEvidence(
286
+ value: Record<string, unknown>,
287
+ ): InvestigationNetworkEvidence | undefined {
288
+ const subject = resourceRef(value.subject);
289
+ const summary = record(value.summary);
290
+ const verdict = value.verdict;
291
+ const routesRaw = value.routes === undefined ? [] : value.routes;
292
+ if (
293
+ !subject ||
294
+ (verdict !== "healthy" &&
295
+ verdict !== "degraded" &&
296
+ verdict !== "broken" &&
297
+ verdict !== "unknown") ||
298
+ !summary ||
299
+ typeof summary.tested !== "number" ||
300
+ typeof summary.passed !== "number" ||
301
+ typeof summary.failed !== "number" ||
302
+ typeof summary.skipped !== "number" ||
303
+ !nonEmptyString(summary.headline) ||
304
+ (summary.derived !== undefined && typeof summary.derived !== "number") ||
305
+ !Array.isArray(routesRaw)
306
+ ) {
307
+ return undefined;
308
+ }
309
+ const routes = routesRaw
310
+ .map(networkRoute)
311
+ .filter((route): route is InvestigationNetworkRoute => Boolean(route));
312
+ if (routes.length !== routesRaw.length) return undefined;
313
+ const diagnosis = record(value.diagnosis);
314
+ if (
315
+ diagnosis &&
316
+ (!nonEmptyString(diagnosis.summary) ||
317
+ ["class", "severity", "route", "nextAction"].some(
318
+ (field) =>
319
+ diagnosis[field] !== undefined &&
320
+ typeof diagnosis[field] !== "string",
321
+ ))
322
+ ) {
323
+ return undefined;
324
+ }
325
+ if (value.reason !== undefined && typeof value.reason !== "string")
326
+ return undefined;
327
+ return {
328
+ subject,
329
+ verdict,
330
+ reason: value.reason as string | undefined,
331
+ diagnosis: diagnosis as
332
+ InvestigationNetworkEvidence["diagnosis"] | undefined,
333
+ summary: summary as unknown as InvestigationNetworkEvidence["summary"],
334
+ routes,
335
+ };
336
+ }
337
+ export function topologyNode(
338
+ value: unknown,
339
+ ): InvestigationTopologyNode | undefined {
340
+ const candidate = record(value);
341
+ if (
342
+ !candidate ||
343
+ !nonEmptyString(candidate.id) ||
344
+ !nonEmptyString(candidate.kind) ||
345
+ !nonEmptyString(candidate.name)
346
+ ) {
347
+ return undefined;
348
+ }
349
+ return candidate as unknown as InvestigationTopologyNode;
350
+ }
351
+ export function topologyEdge(
352
+ value: unknown,
353
+ ): InvestigationTopologyEdge | undefined {
354
+ const candidate = record(value);
355
+ if (
356
+ !candidate ||
357
+ !nonEmptyString(candidate.source) ||
358
+ !nonEmptyString(candidate.target) ||
359
+ !nonEmptyString(candidate.type)
360
+ ) {
361
+ return undefined;
362
+ }
363
+ return candidate as unknown as InvestigationTopologyEdge;
364
+ }
365
+ export function startupBlocker(
366
+ value: unknown,
367
+ ): DiagnosisStartupBlocker | undefined {
368
+ const candidate = record(value);
369
+ if (
370
+ !candidate ||
371
+ !nonEmptyString(candidate.kind) ||
372
+ !nonEmptyString(candidate.name) ||
373
+ !nonEmptyString(candidate.reason) ||
374
+ !nonEmptyString(candidate.severity) ||
375
+ !nonEmptyString(candidate.message)
376
+ ) {
377
+ return undefined;
378
+ }
379
+ return candidate as unknown as DiagnosisStartupBlocker;
380
+ }
381
+ export function crashCause(value: unknown): DiagnosisCrashCause | undefined {
382
+ const candidate = record(value);
383
+ if (
384
+ !candidate ||
385
+ !Array.isArray(candidate.pods) ||
386
+ !candidate.pods.every((pod) => typeof pod === "string") ||
387
+ !nonEmptyString(candidate.container) ||
388
+ !nonEmptyString(candidate.state) ||
389
+ typeof candidate.exitCode !== "number" ||
390
+ !nonEmptyString(candidate.logLine) ||
391
+ !nonEmptyString(candidate.logSource) ||
392
+ !nonEmptyString(candidate.logLineSelection)
393
+ ) {
394
+ return undefined;
395
+ }
396
+ return {
397
+ ...(candidate as unknown as DiagnosisCrashCause),
398
+ logLine: stripAnsi(candidate.logLine as string),
399
+ };
400
+ }
401
+ export function podContainerRef(
402
+ value: unknown,
403
+ ): DiagnosisPodContainerRef | undefined {
404
+ const candidate = record(value);
405
+ if (
406
+ !candidate ||
407
+ !nonEmptyString(candidate.pod) ||
408
+ !nonEmptyString(candidate.container)
409
+ ) {
410
+ return undefined;
411
+ }
412
+ return candidate as unknown as DiagnosisPodContainerRef;
413
+ }
414
+ export function parseLogEntry(
415
+ value: unknown,
416
+ ): DiagnosisPodLogEntry | undefined {
417
+ const candidate = record(value);
418
+ if (
419
+ !candidate ||
420
+ !nonEmptyString(candidate.pod) ||
421
+ !nonEmptyString(candidate.container)
422
+ ) {
423
+ return undefined;
424
+ }
425
+ if (candidate.logs !== undefined && !filteredLogs(candidate.logs))
426
+ return undefined;
427
+ if (candidate.error !== undefined && typeof candidate.error !== "string")
428
+ return undefined;
429
+ return candidate as unknown as DiagnosisPodLogEntry;
430
+ }
431
+ export function nonNegativeInteger(value: unknown): value is number {
432
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
433
+ }
434
+ export function stringRecord(
435
+ value: unknown,
436
+ ): Record<string, string> | undefined {
437
+ const candidate = record(value);
438
+ if (!candidate) return undefined;
439
+ return Object.values(candidate).every((item) => typeof item === "string")
440
+ ? (candidate as Record<string, string>)
441
+ : undefined;
442
+ }
443
+ export function timeSeries(value: unknown): TimeSeries | undefined {
444
+ const item = record(value);
445
+ if (!item || !Array.isArray(item.dataPoints)) return undefined;
446
+ const labels = record(item.labels) ?? {};
447
+ if (Object.values(labels).some((label) => typeof label !== "string")) {
448
+ return undefined;
449
+ }
450
+ const dataPoints: TimeSeries["dataPoints"] = [];
451
+ for (const raw of item.dataPoints) {
452
+ const point = record(raw);
453
+ if (!point || typeof point.timestamp !== "number") return undefined;
454
+ if (
455
+ point.value !== undefined &&
456
+ point.value !== null &&
457
+ typeof point.value !== "number"
458
+ ) {
459
+ return undefined;
460
+ }
461
+ dataPoints.push({
462
+ timestamp: point.timestamp,
463
+ value: typeof point.value === "number" ? point.value : null,
464
+ });
465
+ }
466
+ return { labels: labels as Record<string, string>, dataPoints };
467
+ }