helm_schema_k8s/kubernetes_openapi/version_chain.rs
1/// Configuration for the in-provider K8s version chain.
2#[derive(Debug, Clone)]
3pub struct K8sVersionChain {
4 /// User-supplied versions in their literal CLI order. The first is
5 /// the primary, the rest are explicit fallbacks.
6 pub explicit: Vec<String>,
7 /// Auto-extension policy: `None` = no auto-fallback;
8 /// `Some(n)` = append `n` minors below the smallest
9 /// explicit version, monotonically descending.
10 pub auto_fallback_window: Option<u32>,
11}
12
13impl K8sVersionChain {
14 /// Build a chain from explicit user-ordered versions and an
15 /// optional auto-fallback window. The two are combined as:
16 /// `[explicit..., auto_fallback...]`
17 /// where the auto-fallback list is a descending window of `n` minors
18 /// below the smallest explicit version. Auto-extension is only
19 /// valid when `explicit.len() == 1` — the chain falls back to
20 /// "explicit only" for any other shape.
21 #[must_use]
22 pub fn new(explicit: Vec<String>, auto_fallback_window: Option<u32>) -> Self {
23 Self {
24 explicit,
25 auto_fallback_window,
26 }
27 }
28
29 /// Materialise the ordered list of `version_dirs` to probe.
30 #[must_use]
31 pub fn ordered(&self) -> Vec<String> {
32 let mut out: Vec<String> = self.explicit.clone();
33 if let Some(window) = self.auto_fallback_window
34 && self.explicit.len() == 1
35 && let Some(primary) = self.explicit.first().and_then(|v| parse_minor(v))
36 {
37 for offset in 1..=window {
38 let Some(next_minor) = primary.1.checked_sub(offset) else {
39 break;
40 };
41 out.push(format!("v{}.{next_minor}.0", primary.0));
42 }
43 }
44 out
45 }
46
47 /// The primary (first explicit) version, if any.
48 #[must_use]
49 pub fn primary(&self) -> Option<&str> {
50 self.explicit.first().map(String::as_str)
51 }
52
53 /// Versions that should participate in apiVersion-inference cache
54 /// scanning. This is `explicit` only — auto-fallback versions are
55 /// escape valves for legacy resources whose schemas exist only in
56 /// older K8s minors; they do NOT represent "what the user intends
57 /// to target". Including them in inference would surface
58 /// historical apiVersions (`policy/v1beta1`, `extensions/v1beta1`,
59 /// …) for kinds whose modern version lives at the primary
60 /// version dir, producing spurious `AmbiguousApiVersion`
61 /// diagnostics.
62 #[must_use]
63 pub fn inference_scan_versions(&self) -> Vec<String> {
64 self.explicit.clone()
65 }
66}
67
68fn parse_minor(version_dir: &str) -> Option<(u32, u32)> {
69 let trimmed = version_dir.trim().trim_start_matches('v');
70 let trimmed = trimmed.split('-').next().unwrap_or(trimmed);
71 let mut parts = trimmed.split('.');
72 let major: u32 = parts.next()?.parse().ok()?;
73 let minor: u32 = parts.next()?.parse().ok()?;
74 Some((major, minor))
75}
76
77#[cfg(test)]
78#[path = "tests/version_chain.rs"]
79mod tests;