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}