Skip to main content

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}