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}
90
91/// User-facing diagnostic. Every event helm-schema emits at runtime is
92/// one of these. `Diagnostic::key` produces the deduplication key.
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94#[serde(tag = "type")]
95pub enum Diagnostic {
96    /// No configured provider could supply a resource schema.
97    MissingSchema {
98        /// Kubernetes resource kind.
99        kind: String,
100        /// API version requested by the chart.
101        api_version: String,
102        /// Kubernetes releases consulted in order.
103        k8s_versions_tried: Vec<String>,
104        /// Candidate schema filenames consulted.
105        tried_filenames: Vec<String>,
106        /// Releases whose local caches contain the resource.
107        available_in_cache_versions: Vec<String>,
108        /// Older release likely to contain the removed API.
109        suggested_k8s_version: Option<String>,
110        /// Actionable context for resolving the lookup failure.
111        hint: Option<String>,
112    },
113    /// A fallback Kubernetes release supplied the requested API schema.
114    ResolvedFromFallbackVersion {
115        /// Kubernetes resource kind.
116        kind: String,
117        /// API version requested by the chart.
118        api_version: String,
119        /// Primary Kubernetes release selected by policy.
120        primary_version: String,
121        /// Fallback Kubernetes release that supplied the schema.
122        resolved_version: String,
123    },
124    /// Analysis inferred an API version omitted from the resource.
125    InferredApiVersion {
126        /// Kubernetes resource kind.
127        kind: String,
128        /// API version selected by inference.
129        inferred_api_version: String,
130        /// Evidence tier that produced the candidate.
131        source: InferenceSource,
132        /// Provider family that supplied the evidence.
133        origin: ProviderOrigin,
134    },
135    /// Several inferred API versions remain equally viable.
136    AmbiguousApiVersion {
137        /// Kubernetes resource kind.
138        kind: String,
139        /// Stable set of candidates and their provenance.
140        candidates: Vec<ApiVersionCandidate>,
141    },
142    /// A CRD catalog lacks the requested version.
143    CrdVersionNotFound {
144        /// CRD API group.
145        group: String,
146        /// CRD resource kind.
147        kind: String,
148        /// Version requested by the chart.
149        requested_version: String,
150        /// Cache paths and upstream locations consulted.
151        locations_tried: Vec<String>,
152    },
153    /// A CRD exists, but only at other versions.
154    CrdVersionAvailableAtOtherVersions {
155        /// CRD API group.
156        group: String,
157        /// CRD resource kind.
158        kind: String,
159        /// Version requested by the chart.
160        requested_version: String,
161        /// Versions found for the same group and kind.
162        available_versions: Vec<String>,
163    },
164    /// A configured local schema override could not be read.
165    LocalOverrideUnreadable {
166        /// Kubernetes resource kind.
167        kind: String,
168        /// API version requested by the chart.
169        api_version: String,
170        /// Filesystem path of the override.
171        override_path: String,
172        /// Human-readable I/O failure.
173        io_error: String,
174    },
175    /// An obsolete cache layout was discarded.
176    CacheLayoutInvalidated {
177        /// Root directory containing the cache.
178        cache_root: String,
179        /// Layout marker previously found on disk.
180        previous_marker: Option<u32>,
181        /// Layout marker required by this binary.
182        current_marker: u32,
183    },
184    /// A newer on-disk cache layout cannot be read safely.
185    CacheLayoutForwardIncompatible {
186        /// Root directory containing the cache.
187        cache_root: String,
188        /// Newer layout marker found on disk.
189        on_disk_marker: u32,
190        /// Layout marker understood by this binary.
191        compiled_marker: u32,
192    },
193    /// A one-variable Helm range can iterate an integer supplied through
194    /// `--set`, while the same JSON number supplied by a values file or
195    /// `--set-json` has a non-rangeable runtime kind. JSON Schema cannot
196    /// distinguish those input channels.
197    InputChannelNumericRangeAmbiguity {
198        /// Values path affected by channel-dependent numeric semantics.
199        value_path: String,
200    },
201}
202
203impl Diagnostic {
204    /// The deduplication key for this diagnostic.
205    #[must_use]
206    pub fn key(&self) -> DiagnosticKey {
207        match self {
208            Diagnostic::MissingSchema {
209                kind, api_version, ..
210            } => DiagnosticKey::MissingSchema {
211                kind: kind.clone(),
212                api_version: api_version.clone(),
213            },
214            Diagnostic::ResolvedFromFallbackVersion {
215                kind,
216                api_version,
217                resolved_version,
218                ..
219            } => DiagnosticKey::ResolvedFromFallbackVersion {
220                kind: kind.clone(),
221                api_version: api_version.clone(),
222                resolved_version: resolved_version.clone(),
223            },
224            Diagnostic::InferredApiVersion {
225                kind,
226                inferred_api_version,
227                source,
228                ..
229            } => DiagnosticKey::InferredApiVersion {
230                kind: kind.clone(),
231                inferred_api_version: inferred_api_version.clone(),
232                source: *source,
233            },
234            Diagnostic::AmbiguousApiVersion { kind, .. } => {
235                DiagnosticKey::AmbiguousApiVersion { kind: kind.clone() }
236            }
237            Diagnostic::CrdVersionNotFound {
238                group,
239                kind,
240                requested_version,
241                ..
242            } => DiagnosticKey::CrdVersionNotFound {
243                group: group.clone(),
244                kind: kind.clone(),
245                requested_version: requested_version.clone(),
246            },
247            Diagnostic::CrdVersionAvailableAtOtherVersions {
248                group,
249                kind,
250                requested_version,
251                ..
252            } => DiagnosticKey::CrdVersionAvailableAtOtherVersions {
253                group: group.clone(),
254                kind: kind.clone(),
255                requested_version: requested_version.clone(),
256            },
257            Diagnostic::LocalOverrideUnreadable {
258                kind,
259                api_version,
260                override_path,
261                ..
262            } => DiagnosticKey::LocalOverrideUnreadable {
263                kind: kind.clone(),
264                api_version: api_version.clone(),
265                override_path: override_path.clone(),
266            },
267            Diagnostic::CacheLayoutInvalidated {
268                cache_root,
269                previous_marker,
270                ..
271            } => DiagnosticKey::CacheLayoutInvalidated {
272                cache_root: cache_root.clone(),
273                previous_marker: *previous_marker,
274            },
275            Diagnostic::CacheLayoutForwardIncompatible {
276                cache_root,
277                on_disk_marker,
278                ..
279            } => DiagnosticKey::CacheLayoutForwardIncompatible {
280                cache_root: cache_root.clone(),
281                on_disk_marker: *on_disk_marker,
282            },
283            Diagnostic::InputChannelNumericRangeAmbiguity { value_path } => {
284                DiagnosticKey::InputChannelNumericRangeAmbiguity {
285                    value_path: value_path.clone(),
286                }
287            }
288        }
289    }
290
291    /// Canonicalise mutable list fields so two emissions of the same
292    /// logical event produce identical payloads regardless of probe
293    /// order.
294    pub(crate) fn canonicalise(&mut self) {
295        match self {
296            Diagnostic::MissingSchema {
297                k8s_versions_tried,
298                tried_filenames,
299                available_in_cache_versions,
300                ..
301            } => {
302                canonicalise_strings(k8s_versions_tried);
303                canonicalise_strings(tried_filenames);
304                canonicalise_strings(available_in_cache_versions);
305            }
306            Diagnostic::AmbiguousApiVersion { candidates, .. } => {
307                canonicalise_candidates(candidates);
308            }
309            Diagnostic::CrdVersionNotFound {
310                locations_tried, ..
311            } => canonicalise_strings(locations_tried),
312            Diagnostic::CrdVersionAvailableAtOtherVersions {
313                available_versions, ..
314            } => canonicalise_strings(available_versions),
315            Diagnostic::ResolvedFromFallbackVersion { .. }
316            | Diagnostic::InferredApiVersion { .. }
317            | Diagnostic::LocalOverrideUnreadable { .. }
318            | Diagnostic::CacheLayoutInvalidated { .. }
319            | Diagnostic::CacheLayoutForwardIncompatible { .. }
320            | Diagnostic::InputChannelNumericRangeAmbiguity { .. } => {}
321        }
322    }
323}