syncable_cli/analyzer/helmlint/k8s/
api_versions.rs

1//! Kubernetes API version tracking and deprecation detection.
2//!
3//! Tracks deprecated Kubernetes APIs and their replacements.
4
5use std::collections::HashMap;
6
7/// Kubernetes version as (major, minor).
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
9pub struct K8sVersion {
10    pub major: u32,
11    pub minor: u32,
12}
13
14impl K8sVersion {
15    pub fn new(major: u32, minor: u32) -> Self {
16        Self { major, minor }
17    }
18
19    /// Parse from string like "1.25" or "v1.25".
20    pub fn parse(s: &str) -> Option<Self> {
21        let s = s.trim_start_matches('v');
22        let parts: Vec<&str> = s.split('.').collect();
23        if parts.len() >= 2 {
24            let major = parts[0].parse().ok()?;
25            let minor = parts[1].parse().ok()?;
26            Some(Self { major, minor })
27        } else {
28            None
29        }
30    }
31}
32
33impl std::fmt::Display for K8sVersion {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        write!(f, "{}.{}", self.major, self.minor)
36    }
37}
38
39/// Information about a deprecated API.
40#[derive(Debug, Clone)]
41pub struct DeprecatedApi {
42    /// The deprecated API version (e.g., "extensions/v1beta1")
43    pub api_version: &'static str,
44    /// The kind this deprecation applies to (e.g., "Deployment")
45    pub kind: Option<&'static str>,
46    /// The replacement API version
47    pub replacement: &'static str,
48    /// Kubernetes version where this was deprecated
49    pub deprecated_in: K8sVersion,
50    /// Kubernetes version where this was removed
51    pub removed_in: K8sVersion,
52    /// Additional notes
53    pub notes: Option<&'static str>,
54}
55
56/// Static list of deprecated Kubernetes APIs.
57static DEPRECATED_APIS: &[DeprecatedApi] = &[
58    // extensions/v1beta1 deprecations
59    DeprecatedApi {
60        api_version: "extensions/v1beta1",
61        kind: Some("Deployment"),
62        replacement: "apps/v1",
63        deprecated_in: K8sVersion { major: 1, minor: 9 },
64        removed_in: K8sVersion {
65            major: 1,
66            minor: 16,
67        },
68        notes: None,
69    },
70    DeprecatedApi {
71        api_version: "extensions/v1beta1",
72        kind: Some("DaemonSet"),
73        replacement: "apps/v1",
74        deprecated_in: K8sVersion { major: 1, minor: 9 },
75        removed_in: K8sVersion {
76            major: 1,
77            minor: 16,
78        },
79        notes: None,
80    },
81    DeprecatedApi {
82        api_version: "extensions/v1beta1",
83        kind: Some("ReplicaSet"),
84        replacement: "apps/v1",
85        deprecated_in: K8sVersion { major: 1, minor: 9 },
86        removed_in: K8sVersion {
87            major: 1,
88            minor: 16,
89        },
90        notes: None,
91    },
92    DeprecatedApi {
93        api_version: "extensions/v1beta1",
94        kind: Some("Ingress"),
95        replacement: "networking.k8s.io/v1",
96        deprecated_in: K8sVersion {
97            major: 1,
98            minor: 14,
99        },
100        removed_in: K8sVersion {
101            major: 1,
102            minor: 22,
103        },
104        notes: None,
105    },
106    DeprecatedApi {
107        api_version: "extensions/v1beta1",
108        kind: Some("NetworkPolicy"),
109        replacement: "networking.k8s.io/v1",
110        deprecated_in: K8sVersion { major: 1, minor: 9 },
111        removed_in: K8sVersion {
112            major: 1,
113            minor: 16,
114        },
115        notes: None,
116    },
117    DeprecatedApi {
118        api_version: "extensions/v1beta1",
119        kind: Some("PodSecurityPolicy"),
120        replacement: "policy/v1beta1",
121        deprecated_in: K8sVersion {
122            major: 1,
123            minor: 10,
124        },
125        removed_in: K8sVersion {
126            major: 1,
127            minor: 16,
128        },
129        notes: Some("PodSecurityPolicy is deprecated entirely in 1.21 and removed in 1.25"),
130    },
131    // apps/v1beta1 deprecations
132    DeprecatedApi {
133        api_version: "apps/v1beta1",
134        kind: Some("Deployment"),
135        replacement: "apps/v1",
136        deprecated_in: K8sVersion { major: 1, minor: 9 },
137        removed_in: K8sVersion {
138            major: 1,
139            minor: 16,
140        },
141        notes: None,
142    },
143    DeprecatedApi {
144        api_version: "apps/v1beta1",
145        kind: Some("StatefulSet"),
146        replacement: "apps/v1",
147        deprecated_in: K8sVersion { major: 1, minor: 9 },
148        removed_in: K8sVersion {
149            major: 1,
150            minor: 16,
151        },
152        notes: None,
153    },
154    // apps/v1beta2 deprecations
155    DeprecatedApi {
156        api_version: "apps/v1beta2",
157        kind: Some("Deployment"),
158        replacement: "apps/v1",
159        deprecated_in: K8sVersion { major: 1, minor: 9 },
160        removed_in: K8sVersion {
161            major: 1,
162            minor: 16,
163        },
164        notes: None,
165    },
166    DeprecatedApi {
167        api_version: "apps/v1beta2",
168        kind: Some("DaemonSet"),
169        replacement: "apps/v1",
170        deprecated_in: K8sVersion { major: 1, minor: 9 },
171        removed_in: K8sVersion {
172            major: 1,
173            minor: 16,
174        },
175        notes: None,
176    },
177    DeprecatedApi {
178        api_version: "apps/v1beta2",
179        kind: Some("ReplicaSet"),
180        replacement: "apps/v1",
181        deprecated_in: K8sVersion { major: 1, minor: 9 },
182        removed_in: K8sVersion {
183            major: 1,
184            minor: 16,
185        },
186        notes: None,
187    },
188    DeprecatedApi {
189        api_version: "apps/v1beta2",
190        kind: Some("StatefulSet"),
191        replacement: "apps/v1",
192        deprecated_in: K8sVersion { major: 1, minor: 9 },
193        removed_in: K8sVersion {
194            major: 1,
195            minor: 16,
196        },
197        notes: None,
198    },
199    // networking.k8s.io/v1beta1 deprecations
200    DeprecatedApi {
201        api_version: "networking.k8s.io/v1beta1",
202        kind: Some("Ingress"),
203        replacement: "networking.k8s.io/v1",
204        deprecated_in: K8sVersion {
205            major: 1,
206            minor: 19,
207        },
208        removed_in: K8sVersion {
209            major: 1,
210            minor: 22,
211        },
212        notes: None,
213    },
214    DeprecatedApi {
215        api_version: "networking.k8s.io/v1beta1",
216        kind: Some("IngressClass"),
217        replacement: "networking.k8s.io/v1",
218        deprecated_in: K8sVersion {
219            major: 1,
220            minor: 19,
221        },
222        removed_in: K8sVersion {
223            major: 1,
224            minor: 22,
225        },
226        notes: None,
227    },
228    // rbac.authorization.k8s.io/v1beta1 deprecations
229    DeprecatedApi {
230        api_version: "rbac.authorization.k8s.io/v1beta1",
231        kind: None,
232        replacement: "rbac.authorization.k8s.io/v1",
233        deprecated_in: K8sVersion {
234            major: 1,
235            minor: 17,
236        },
237        removed_in: K8sVersion {
238            major: 1,
239            minor: 22,
240        },
241        notes: Some("Applies to Role, ClusterRole, RoleBinding, ClusterRoleBinding"),
242    },
243    // admissionregistration.k8s.io/v1beta1 deprecations
244    DeprecatedApi {
245        api_version: "admissionregistration.k8s.io/v1beta1",
246        kind: None,
247        replacement: "admissionregistration.k8s.io/v1",
248        deprecated_in: K8sVersion {
249            major: 1,
250            minor: 16,
251        },
252        removed_in: K8sVersion {
253            major: 1,
254            minor: 22,
255        },
256        notes: Some("Applies to MutatingWebhookConfiguration, ValidatingWebhookConfiguration"),
257    },
258    // apiextensions.k8s.io/v1beta1 deprecations
259    DeprecatedApi {
260        api_version: "apiextensions.k8s.io/v1beta1",
261        kind: Some("CustomResourceDefinition"),
262        replacement: "apiextensions.k8s.io/v1",
263        deprecated_in: K8sVersion {
264            major: 1,
265            minor: 16,
266        },
267        removed_in: K8sVersion {
268            major: 1,
269            minor: 22,
270        },
271        notes: None,
272    },
273    // policy/v1beta1 deprecations
274    DeprecatedApi {
275        api_version: "policy/v1beta1",
276        kind: Some("PodDisruptionBudget"),
277        replacement: "policy/v1",
278        deprecated_in: K8sVersion {
279            major: 1,
280            minor: 21,
281        },
282        removed_in: K8sVersion {
283            major: 1,
284            minor: 25,
285        },
286        notes: None,
287    },
288    DeprecatedApi {
289        api_version: "policy/v1beta1",
290        kind: Some("PodSecurityPolicy"),
291        replacement: "None (use Pod Security Admission)",
292        deprecated_in: K8sVersion {
293            major: 1,
294            minor: 21,
295        },
296        removed_in: K8sVersion {
297            major: 1,
298            minor: 25,
299        },
300        notes: Some("PodSecurityPolicy is removed. Use Pod Security Admission instead"),
301    },
302    // batch/v1beta1 deprecations
303    DeprecatedApi {
304        api_version: "batch/v1beta1",
305        kind: Some("CronJob"),
306        replacement: "batch/v1",
307        deprecated_in: K8sVersion {
308            major: 1,
309            minor: 21,
310        },
311        removed_in: K8sVersion {
312            major: 1,
313            minor: 25,
314        },
315        notes: None,
316    },
317    // certificates.k8s.io/v1beta1 deprecations
318    DeprecatedApi {
319        api_version: "certificates.k8s.io/v1beta1",
320        kind: Some("CertificateSigningRequest"),
321        replacement: "certificates.k8s.io/v1",
322        deprecated_in: K8sVersion {
323            major: 1,
324            minor: 19,
325        },
326        removed_in: K8sVersion {
327            major: 1,
328            minor: 22,
329        },
330        notes: None,
331    },
332    // coordination.k8s.io/v1beta1 deprecations
333    DeprecatedApi {
334        api_version: "coordination.k8s.io/v1beta1",
335        kind: Some("Lease"),
336        replacement: "coordination.k8s.io/v1",
337        deprecated_in: K8sVersion {
338            major: 1,
339            minor: 14,
340        },
341        removed_in: K8sVersion {
342            major: 1,
343            minor: 22,
344        },
345        notes: None,
346    },
347    // storage.k8s.io/v1beta1 deprecations
348    DeprecatedApi {
349        api_version: "storage.k8s.io/v1beta1",
350        kind: Some("CSIDriver"),
351        replacement: "storage.k8s.io/v1",
352        deprecated_in: K8sVersion {
353            major: 1,
354            minor: 19,
355        },
356        removed_in: K8sVersion {
357            major: 1,
358            minor: 22,
359        },
360        notes: None,
361    },
362    DeprecatedApi {
363        api_version: "storage.k8s.io/v1beta1",
364        kind: Some("CSINode"),
365        replacement: "storage.k8s.io/v1",
366        deprecated_in: K8sVersion {
367            major: 1,
368            minor: 17,
369        },
370        removed_in: K8sVersion {
371            major: 1,
372            minor: 22,
373        },
374        notes: None,
375    },
376    DeprecatedApi {
377        api_version: "storage.k8s.io/v1beta1",
378        kind: Some("StorageClass"),
379        replacement: "storage.k8s.io/v1",
380        deprecated_in: K8sVersion { major: 1, minor: 6 },
381        removed_in: K8sVersion {
382            major: 1,
383            minor: 22,
384        },
385        notes: None,
386    },
387    DeprecatedApi {
388        api_version: "storage.k8s.io/v1beta1",
389        kind: Some("VolumeAttachment"),
390        replacement: "storage.k8s.io/v1",
391        deprecated_in: K8sVersion {
392            major: 1,
393            minor: 13,
394        },
395        removed_in: K8sVersion {
396            major: 1,
397            minor: 22,
398        },
399        notes: None,
400    },
401    // scheduling.k8s.io/v1beta1 deprecations
402    DeprecatedApi {
403        api_version: "scheduling.k8s.io/v1beta1",
404        kind: Some("PriorityClass"),
405        replacement: "scheduling.k8s.io/v1",
406        deprecated_in: K8sVersion {
407            major: 1,
408            minor: 14,
409        },
410        removed_in: K8sVersion {
411            major: 1,
412            minor: 22,
413        },
414        notes: None,
415    },
416    // discovery.k8s.io/v1beta1 deprecations
417    DeprecatedApi {
418        api_version: "discovery.k8s.io/v1beta1",
419        kind: Some("EndpointSlice"),
420        replacement: "discovery.k8s.io/v1",
421        deprecated_in: K8sVersion {
422            major: 1,
423            minor: 21,
424        },
425        removed_in: K8sVersion {
426            major: 1,
427            minor: 25,
428        },
429        notes: None,
430    },
431    // events.k8s.io/v1beta1 deprecations
432    DeprecatedApi {
433        api_version: "events.k8s.io/v1beta1",
434        kind: Some("Event"),
435        replacement: "events.k8s.io/v1",
436        deprecated_in: K8sVersion {
437            major: 1,
438            minor: 19,
439        },
440        removed_in: K8sVersion {
441            major: 1,
442            minor: 25,
443        },
444        notes: None,
445    },
446    // autoscaling/v2beta1 deprecations
447    DeprecatedApi {
448        api_version: "autoscaling/v2beta1",
449        kind: Some("HorizontalPodAutoscaler"),
450        replacement: "autoscaling/v2",
451        deprecated_in: K8sVersion {
452            major: 1,
453            minor: 23,
454        },
455        removed_in: K8sVersion {
456            major: 1,
457            minor: 26,
458        },
459        notes: None,
460    },
461    // autoscaling/v2beta2 deprecations
462    DeprecatedApi {
463        api_version: "autoscaling/v2beta2",
464        kind: Some("HorizontalPodAutoscaler"),
465        replacement: "autoscaling/v2",
466        deprecated_in: K8sVersion {
467            major: 1,
468            minor: 23,
469        },
470        removed_in: K8sVersion {
471            major: 1,
472            minor: 26,
473        },
474        notes: None,
475    },
476];
477
478/// Check if an API version is deprecated for a given kind.
479pub fn is_api_deprecated(api_version: &str, kind: Option<&str>) -> Option<&'static DeprecatedApi> {
480    DEPRECATED_APIS
481        .iter()
482        .find(|api| api.api_version == api_version && (api.kind.is_none() || api.kind == kind))
483}
484
485/// Get the replacement API for a deprecated API.
486pub fn get_replacement_api(api_version: &str, kind: Option<&str>) -> Option<&'static str> {
487    is_api_deprecated(api_version, kind).map(|api| api.replacement)
488}
489
490/// Check if an API is deprecated in a specific Kubernetes version.
491pub fn is_api_deprecated_in_version(
492    api_version: &str,
493    kind: Option<&str>,
494    k8s_version: K8sVersion,
495) -> Option<&'static DeprecatedApi> {
496    DEPRECATED_APIS.iter().find(|api| {
497        api.api_version == api_version
498            && (api.kind.is_none() || api.kind == kind)
499            && k8s_version >= api.deprecated_in
500    })
501}
502
503/// Check if an API is removed in a specific Kubernetes version.
504pub fn is_api_removed_in_version(
505    api_version: &str,
506    kind: Option<&str>,
507    k8s_version: K8sVersion,
508) -> Option<&'static DeprecatedApi> {
509    DEPRECATED_APIS.iter().find(|api| {
510        api.api_version == api_version
511            && (api.kind.is_none() || api.kind == kind)
512            && k8s_version >= api.removed_in
513    })
514}
515
516/// Build a map of deprecated APIs for quick lookup.
517pub fn build_deprecation_map() -> HashMap<String, Vec<&'static DeprecatedApi>> {
518    let mut map: HashMap<String, Vec<&'static DeprecatedApi>> = HashMap::new();
519    for api in DEPRECATED_APIS {
520        map.entry(api.api_version.to_string())
521            .or_default()
522            .push(api);
523    }
524    map
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_k8s_version_parse() {
533        assert_eq!(K8sVersion::parse("1.25"), Some(K8sVersion::new(1, 25)));
534        assert_eq!(K8sVersion::parse("v1.28"), Some(K8sVersion::new(1, 28)));
535        assert_eq!(K8sVersion::parse("invalid"), None);
536    }
537
538    #[test]
539    fn test_k8s_version_ordering() {
540        assert!(K8sVersion::new(1, 25) > K8sVersion::new(1, 20));
541        assert!(K8sVersion::new(1, 25) < K8sVersion::new(1, 26));
542        assert!(K8sVersion::new(1, 25) == K8sVersion::new(1, 25));
543    }
544
545    #[test]
546    fn test_is_api_deprecated() {
547        // Test known deprecated API
548        let result = is_api_deprecated("extensions/v1beta1", Some("Deployment"));
549        assert!(result.is_some());
550        let api = result.unwrap();
551        assert_eq!(api.replacement, "apps/v1");
552
553        // Test non-deprecated API
554        let result = is_api_deprecated("apps/v1", Some("Deployment"));
555        assert!(result.is_none());
556    }
557
558    #[test]
559    fn test_get_replacement_api() {
560        assert_eq!(
561            get_replacement_api("extensions/v1beta1", Some("Deployment")),
562            Some("apps/v1")
563        );
564        assert_eq!(
565            get_replacement_api("networking.k8s.io/v1beta1", Some("Ingress")),
566            Some("networking.k8s.io/v1")
567        );
568        assert_eq!(get_replacement_api("apps/v1", Some("Deployment")), None);
569    }
570
571    #[test]
572    fn test_deprecated_in_version() {
573        // extensions/v1beta1 Deployment deprecated in 1.9
574        let result = is_api_deprecated_in_version(
575            "extensions/v1beta1",
576            Some("Deployment"),
577            K8sVersion::new(1, 10),
578        );
579        assert!(result.is_some());
580
581        let result = is_api_deprecated_in_version(
582            "extensions/v1beta1",
583            Some("Deployment"),
584            K8sVersion::new(1, 8),
585        );
586        assert!(result.is_none());
587    }
588
589    #[test]
590    fn test_removed_in_version() {
591        // extensions/v1beta1 Deployment removed in 1.16
592        let result = is_api_removed_in_version(
593            "extensions/v1beta1",
594            Some("Deployment"),
595            K8sVersion::new(1, 16),
596        );
597        assert!(result.is_some());
598
599        let result = is_api_removed_in_version(
600            "extensions/v1beta1",
601            Some("Deployment"),
602            K8sVersion::new(1, 15),
603        );
604        assert!(result.is_none());
605    }
606
607    #[test]
608    fn test_build_deprecation_map() {
609        let map = build_deprecation_map();
610        assert!(map.contains_key("extensions/v1beta1"));
611        assert!(map.contains_key("apps/v1beta1"));
612        assert!(!map.contains_key("apps/v1"));
613    }
614}