helm_schema_k8s/diagnostic/diagnostic.rs
1use serde::{Deserialize, Serialize};
2
3use crate::inference::candidate::{ApiVersionCandidate, InferenceSource};
4use crate::lookup::ProviderOrigin;
5
6use super::canonicalise::{canonicalise_candidates, canonicalise_strings};
7
8/// Identity key used to dedupe diagnostics. Diagnostics with the same
9/// key are considered "the same logical event"; only the first one
10/// inserted into a [`crate::diagnostic::DiagnosticSink`] is kept.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub enum DiagnosticKey {
13 /// No provider could supply the requested resource schema.
14 MissingSchema {
15 /// Kubernetes resource kind.
16 kind: String,
17 /// API version requested by the chart.
18 api_version: String,
19 },
20 /// A compatible Kubernetes release supplied the resource schema.
21 ResolvedFromFallbackVersion {
22 /// Kubernetes resource kind.
23 kind: String,
24 /// API version requested by the chart.
25 api_version: String,
26 /// Kubernetes release that supplied the schema.
27 resolved_version: String,
28 },
29 /// Static or bounded fallback analysis inferred a missing API version.
30 InferredApiVersion {
31 /// Kubernetes resource kind.
32 kind: String,
33 /// API version selected by inference.
34 inferred_api_version: String,
35 /// Evidence tier that produced the candidate.
36 source: InferenceSource,
37 },
38 /// More than one API version remains viable for a resource kind.
39 AmbiguousApiVersion {
40 /// Kubernetes resource kind with ambiguous candidates.
41 kind: String,
42 },
43 /// A CRD catalog owns the group and kind but lacks the requested version.
44 CrdVersionNotFound {
45 /// CRD API group.
46 group: String,
47 /// CRD resource kind.
48 kind: String,
49 /// Version requested by the chart.
50 requested_version: String,
51 },
52 /// A CRD exists at versions other than the one requested.
53 CrdVersionAvailableAtOtherVersions {
54 /// CRD API group.
55 group: String,
56 /// CRD resource kind.
57 kind: String,
58 /// Version requested by the chart.
59 requested_version: String,
60 },
61 /// A configured local override exists but cannot be read.
62 LocalOverrideUnreadable {
63 /// Kubernetes resource kind.
64 kind: String,
65 /// API version requested by the chart.
66 api_version: String,
67 /// Filesystem path of the unreadable override.
68 override_path: String,
69 },
70 /// An older cache layout was invalidated before use.
71 CacheLayoutInvalidated {
72 /// Root directory containing the cache.
73 cache_root: String,
74 /// Layout marker previously found on disk.
75 previous_marker: Option<u32>,
76 },
77 /// The on-disk cache was written by a newer incompatible binary.
78 CacheLayoutForwardIncompatible {
79 /// Root directory containing the cache.
80 cache_root: String,
81 /// Newer layout marker found on disk.
82 on_disk_marker: u32,
83 },
84 /// Input channels give the same JSON number different Helm range semantics.
85 InputChannelNumericRangeAmbiguity {
86 /// Values path affected by the ambiguity.
87 value_path: String,
88 },
89 /// Discovered chart config weakens emission relative to this invocation without it.
90 DiscoveredConfigWeakensEmission,
91}
92
93/// User-facing diagnostic. Every event helm-schema emits at runtime is
94/// one of these. `Diagnostic::key` produces the deduplication key.
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(tag = "type")]
97pub enum Diagnostic {
98 /// No configured provider could supply a resource schema.
99 MissingSchema {
100 /// Kubernetes resource kind.
101 kind: String,
102 /// API version requested by the chart.
103 api_version: String,
104 /// Kubernetes releases consulted in order.
105 k8s_versions_tried: Vec<String>,
106 /// Candidate schema filenames consulted.
107 tried_filenames: Vec<String>,
108 /// Releases whose local caches contain the resource.
109 available_in_cache_versions: Vec<String>,
110 /// Older release likely to contain the removed API.
111 suggested_k8s_version: Option<String>,
112 /// Actionable context for resolving the lookup failure.
113 hint: Option<String>,
114 },
115 /// A fallback Kubernetes release supplied the requested API schema.
116 ResolvedFromFallbackVersion {
117 /// Kubernetes resource kind.
118 kind: String,
119 /// API version requested by the chart.
120 api_version: String,
121 /// Primary Kubernetes release selected by policy.
122 primary_version: String,
123 /// Fallback Kubernetes release that supplied the schema.
124 resolved_version: String,
125 },
126 /// Analysis inferred an API version omitted from the resource.
127 InferredApiVersion {
128 /// Kubernetes resource kind.
129 kind: String,
130 /// API version selected by inference.
131 inferred_api_version: String,
132 /// Evidence tier that produced the candidate.
133 source: InferenceSource,
134 /// Provider family that supplied the evidence.
135 origin: ProviderOrigin,
136 },
137 /// Several inferred API versions remain equally viable.
138 AmbiguousApiVersion {
139 /// Kubernetes resource kind.
140 kind: String,
141 /// Stable set of candidates and their provenance.
142 candidates: Vec<ApiVersionCandidate>,
143 },
144 /// A CRD catalog lacks the requested version.
145 CrdVersionNotFound {
146 /// CRD API group.
147 group: String,
148 /// CRD resource kind.
149 kind: String,
150 /// Version requested by the chart.
151 requested_version: String,
152 /// Cache paths and upstream locations consulted.
153 locations_tried: Vec<String>,
154 },
155 /// A CRD exists, but only at other versions.
156 CrdVersionAvailableAtOtherVersions {
157 /// CRD API group.
158 group: String,
159 /// CRD resource kind.
160 kind: String,
161 /// Version requested by the chart.
162 requested_version: String,
163 /// Versions found for the same group and kind.
164 available_versions: Vec<String>,
165 },
166 /// A configured local schema override could not be read.
167 LocalOverrideUnreadable {
168 /// Kubernetes resource kind.
169 kind: String,
170 /// API version requested by the chart.
171 api_version: String,
172 /// Filesystem path of the override.
173 override_path: String,
174 /// Human-readable I/O failure.
175 io_error: String,
176 },
177 /// An obsolete cache layout was discarded.
178 CacheLayoutInvalidated {
179 /// Root directory containing the cache.
180 cache_root: String,
181 /// Layout marker previously found on disk.
182 previous_marker: Option<u32>,
183 /// Layout marker required by this binary.
184 current_marker: u32,
185 },
186 /// A newer on-disk cache layout cannot be read safely.
187 CacheLayoutForwardIncompatible {
188 /// Root directory containing the cache.
189 cache_root: String,
190 /// Newer layout marker found on disk.
191 on_disk_marker: u32,
192 /// Layout marker understood by this binary.
193 compiled_marker: u32,
194 },
195 /// A one-variable Helm range can iterate an integer supplied through
196 /// `--set`, while the same JSON number supplied by a values file or
197 /// `--set-json` has a non-rangeable runtime kind. JSON Schema cannot
198 /// distinguish those input channels.
199 InputChannelNumericRangeAmbiguity {
200 /// Values path affected by channel-dependent numeric semantics.
201 value_path: String,
202 },
203 /// Chart-local config disables one or more W-class emission constraints.
204 DiscoveredConfigWeakensEmission {
205 /// Stable knob names disabled because the config was applied.
206 disabled_knobs: Vec<String>,
207 /// Whether the config path came from an explicit `--config` argument.
208 explicit: bool,
209 },
210}
211
212impl Diagnostic {
213 /// The deduplication key for this diagnostic.
214 #[must_use]
215 pub fn key(&self) -> DiagnosticKey {
216 match self {
217 Diagnostic::MissingSchema {
218 kind, api_version, ..
219 } => DiagnosticKey::MissingSchema {
220 kind: kind.clone(),
221 api_version: api_version.clone(),
222 },
223 Diagnostic::ResolvedFromFallbackVersion {
224 kind,
225 api_version,
226 resolved_version,
227 ..
228 } => DiagnosticKey::ResolvedFromFallbackVersion {
229 kind: kind.clone(),
230 api_version: api_version.clone(),
231 resolved_version: resolved_version.clone(),
232 },
233 Diagnostic::InferredApiVersion {
234 kind,
235 inferred_api_version,
236 source,
237 ..
238 } => DiagnosticKey::InferredApiVersion {
239 kind: kind.clone(),
240 inferred_api_version: inferred_api_version.clone(),
241 source: *source,
242 },
243 Diagnostic::AmbiguousApiVersion { kind, .. } => {
244 DiagnosticKey::AmbiguousApiVersion { kind: kind.clone() }
245 }
246 Diagnostic::CrdVersionNotFound {
247 group,
248 kind,
249 requested_version,
250 ..
251 } => DiagnosticKey::CrdVersionNotFound {
252 group: group.clone(),
253 kind: kind.clone(),
254 requested_version: requested_version.clone(),
255 },
256 Diagnostic::CrdVersionAvailableAtOtherVersions {
257 group,
258 kind,
259 requested_version,
260 ..
261 } => DiagnosticKey::CrdVersionAvailableAtOtherVersions {
262 group: group.clone(),
263 kind: kind.clone(),
264 requested_version: requested_version.clone(),
265 },
266 Diagnostic::LocalOverrideUnreadable {
267 kind,
268 api_version,
269 override_path,
270 ..
271 } => DiagnosticKey::LocalOverrideUnreadable {
272 kind: kind.clone(),
273 api_version: api_version.clone(),
274 override_path: override_path.clone(),
275 },
276 Diagnostic::CacheLayoutInvalidated {
277 cache_root,
278 previous_marker,
279 ..
280 } => DiagnosticKey::CacheLayoutInvalidated {
281 cache_root: cache_root.clone(),
282 previous_marker: *previous_marker,
283 },
284 Diagnostic::CacheLayoutForwardIncompatible {
285 cache_root,
286 on_disk_marker,
287 ..
288 } => DiagnosticKey::CacheLayoutForwardIncompatible {
289 cache_root: cache_root.clone(),
290 on_disk_marker: *on_disk_marker,
291 },
292 Diagnostic::InputChannelNumericRangeAmbiguity { value_path } => {
293 DiagnosticKey::InputChannelNumericRangeAmbiguity {
294 value_path: value_path.clone(),
295 }
296 }
297 Diagnostic::DiscoveredConfigWeakensEmission { .. } => {
298 DiagnosticKey::DiscoveredConfigWeakensEmission
299 }
300 }
301 }
302
303 /// Canonicalise mutable list fields so two emissions of the same
304 /// logical event produce identical payloads regardless of probe
305 /// order.
306 pub(crate) fn canonicalise(&mut self) {
307 match self {
308 Diagnostic::MissingSchema {
309 k8s_versions_tried,
310 tried_filenames,
311 available_in_cache_versions,
312 ..
313 } => {
314 canonicalise_strings(k8s_versions_tried);
315 canonicalise_strings(tried_filenames);
316 canonicalise_strings(available_in_cache_versions);
317 }
318 Diagnostic::AmbiguousApiVersion { candidates, .. } => {
319 canonicalise_candidates(candidates);
320 }
321 Diagnostic::CrdVersionNotFound {
322 locations_tried, ..
323 } => canonicalise_strings(locations_tried),
324 Diagnostic::CrdVersionAvailableAtOtherVersions {
325 available_versions, ..
326 } => canonicalise_strings(available_versions),
327 Diagnostic::DiscoveredConfigWeakensEmission { disabled_knobs, .. } => {
328 canonicalise_strings(disabled_knobs);
329 }
330 Diagnostic::ResolvedFromFallbackVersion { .. }
331 | Diagnostic::InferredApiVersion { .. }
332 | Diagnostic::LocalOverrideUnreadable { .. }
333 | Diagnostic::CacheLayoutInvalidated { .. }
334 | Diagnostic::CacheLayoutForwardIncompatible { .. }
335 | Diagnostic::InputChannelNumericRangeAmbiguity { .. } => {}
336 }
337 }
338}