kube_core/
version.rs

1use std::{cmp::Reverse, convert::Infallible, str::FromStr};
2
3/// Version parser for Kubernetes version patterns
4///
5/// This type implements two orderings for sorting by:
6///
7/// - [`Version::priority`] for [Kubernetes/kubectl version priority](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#version-priority)
8/// - [`Version::generation`] for sorting strictly by version generation in a semver style
9///
10/// To get the api versions sorted by `kubectl` priority:
11///
12/// ```
13/// use kube_core::Version;
14/// use std::cmp::Reverse; // for DESCENDING sort
15/// let mut versions = vec![
16///     "v10beta3",
17///     "v2",
18///     "foo10",
19///     "v1",
20///     "v3beta1",
21///     "v11alpha2",
22///     "v11beta2",
23///     "v12alpha1",
24///     "foo1",
25///     "v10",
26/// ];
27/// versions.sort_by_cached_key(|v| Reverse(Version::parse(v).priority()));
28/// assert_eq!(versions, vec![
29///     "v10",
30///     "v2",
31///     "v1",
32///     "v11beta2",
33///     "v10beta3",
34///     "v3beta1",
35///     "v12alpha1",
36///     "v11alpha2",
37///     "foo1",
38///     "foo10",
39/// ]);
40/// ```
41///
42#[derive(PartialEq, Eq, Debug, Clone)]
43pub enum Version {
44    /// A major/GA release
45    ///
46    /// Always considered higher priority than a beta release.
47    Stable(u32),
48
49    /// A beta release for a specific major version
50    ///
51    /// Always considered higher priority than an alpha release.
52    Beta(u32, Option<u32>),
53
54    /// An alpha release for a specific major version
55    ///
56    /// Always considered higher priority than a nonconformant version
57    Alpha(u32, Option<u32>),
58    /// An non-conformant api string
59    ///
60    /// CRDs and APIServices can use arbitrary strings as versions.
61    Nonconformant(String),
62}
63
64impl Version {
65    fn try_parse(v: &str) -> Option<Version> {
66        let v = v.strip_prefix('v')?;
67        let major = v.split_terminator(|ch: char| !ch.is_ascii_digit()).next()?;
68        let v = &v[major.len()..];
69        let major: u32 = major.parse().ok()?;
70        if v.is_empty() {
71            return Some(Version::Stable(major));
72        }
73        if let Some(suf) = v.strip_prefix("alpha") {
74            return if suf.is_empty() {
75                Some(Version::Alpha(major, None))
76            } else {
77                Some(Version::Alpha(major, Some(suf.parse().ok()?)))
78            };
79        }
80        if let Some(suf) = v.strip_prefix("beta") {
81            return if suf.is_empty() {
82                Some(Version::Beta(major, None))
83            } else {
84                Some(Version::Beta(major, Some(suf.parse().ok()?)))
85            };
86        }
87        None
88    }
89
90    /// An infallble parse of a Kubernetes version string
91    ///
92    /// ```
93    /// use kube_core::Version;
94    /// assert_eq!(Version::parse("v10beta12"), Version::Beta(10, Some(12)));
95    /// ```
96    pub fn parse(v: &str) -> Version {
97        match Self::try_parse(v) {
98            Some(ver) => ver,
99            None => Version::Nonconformant(v.to_string()),
100        }
101    }
102}
103
104/// An infallible FromStr implementation for more generic users
105impl FromStr for Version {
106    type Err = Infallible;
107
108    fn from_str(s: &str) -> Result<Self, Self::Err> {
109        Ok(Version::parse(s))
110    }
111}
112
113#[derive(PartialEq, Eq, PartialOrd, Ord)]
114enum Stability {
115    Nonconformant,
116    Alpha,
117    Beta,
118    Stable,
119}
120
121/// See [`Version::priority`]
122#[derive(PartialEq, Eq, PartialOrd, Ord)]
123struct Priority {
124    stability: Stability,
125    major: u32,
126    minor: Option<u32>,
127    nonconformant: Option<Reverse<String>>,
128}
129
130/// See [`Version::generation`]
131#[derive(PartialEq, Eq, PartialOrd, Ord)]
132struct Generation {
133    major: u32,
134    stability: Stability,
135    minor: Option<u32>,
136    nonconformant: Option<Reverse<String>>,
137}
138
139impl Version {
140    /// An [`Ord`] for `Version` that orders by [Kubernetes version priority](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#version-priority)
141    ///
142    /// This order will favour stable versions over newer pre-releases and is used by `kubectl`.
143    ///
144    /// For example:
145    ///
146    /// ```
147    /// # use kube_core::Version;
148    /// assert!(Version::Stable(2).priority() > Version::Stable(1).priority());
149    /// assert!(Version::Stable(1).priority() > Version::Beta(1, None).priority());
150    /// assert!(Version::Stable(1).priority() > Version::Beta(2, None).priority());
151    /// assert!(Version::Stable(2).priority() > Version::Alpha(1, Some(2)).priority());
152    /// assert!(Version::Stable(1).priority() > Version::Alpha(2, Some(2)).priority());
153    /// assert!(Version::Beta(1, None).priority() > Version::Nonconformant("ver3".into()).priority());
154    /// ```
155    ///
156    /// Note that the type of release matters more than the version numbers:
157    /// `Stable(x)` > `Beta(y)` > `Alpha(z)` > `Nonconformant(w)` for all `x`,`y`,`z`,`w`
158    ///
159    /// `Nonconformant` versions are ordered alphabetically.
160    pub fn priority(&self) -> impl Ord {
161        match self {
162            &Self::Stable(major) => Priority {
163                stability: Stability::Stable,
164                major,
165                minor: None,
166                nonconformant: None,
167            },
168            &Self::Beta(major, minor) => Priority {
169                stability: Stability::Beta,
170                major,
171                minor,
172                nonconformant: None,
173            },
174            &Self::Alpha(major, minor) => Priority {
175                stability: Stability::Alpha,
176                major,
177                minor,
178                nonconformant: None,
179            },
180            Self::Nonconformant(nonconformant) => Priority {
181                stability: Stability::Nonconformant,
182                major: 0,
183                minor: None,
184                nonconformant: Some(Reverse(nonconformant.clone())),
185            },
186        }
187    }
188
189    /// An [`Ord`] for `Version` that orders by version generation
190    ///
191    /// This order will favour higher version numbers even if it's a pre-release.
192    ///
193    /// For example:
194    ///
195    /// ```
196    /// # use kube_core::Version;
197    /// assert!(Version::Stable(2).generation() > Version::Stable(1).generation());
198    /// assert!(Version::Stable(1).generation() > Version::Beta(1, None).generation());
199    /// assert!(Version::Beta(2, None).generation() > Version::Stable(1).generation());
200    /// assert!(Version::Stable(2).generation() > Version::Alpha(1, Some(2)).generation());
201    /// assert!(Version::Alpha(2, Some(2)).generation() > Version::Stable(1).generation());
202    /// assert!(Version::Beta(1, None).generation() > Version::Nonconformant("ver3".into()).generation());
203    /// ```
204    pub fn generation(&self) -> impl Ord {
205        match self {
206            &Self::Stable(major) => Generation {
207                stability: Stability::Stable,
208                major,
209                minor: None,
210                nonconformant: None,
211            },
212            &Self::Beta(major, minor) => Generation {
213                stability: Stability::Beta,
214                major,
215                minor,
216                nonconformant: None,
217            },
218            &Self::Alpha(major, minor) => Generation {
219                stability: Stability::Alpha,
220                major,
221                minor,
222                nonconformant: None,
223            },
224            Self::Nonconformant(nonconformant) => Generation {
225                stability: Stability::Nonconformant,
226                major: 0,
227                minor: None,
228                nonconformant: Some(Reverse(nonconformant.clone())),
229            },
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::Version;
237    use std::{cmp::Reverse, str::FromStr};
238
239    #[test]
240    fn test_stable() {
241        assert_eq!(Version::parse("v1"), Version::Stable(1));
242        assert_eq!(Version::parse("v3"), Version::Stable(3));
243        assert_eq!(Version::parse("v10"), Version::Stable(10));
244    }
245
246    #[test]
247    fn test_prerelease() {
248        assert_eq!(Version::parse("v1beta"), Version::Beta(1, None));
249        assert_eq!(Version::parse("v2alpha1"), Version::Alpha(2, Some(1)));
250        assert_eq!(Version::parse("v10beta12"), Version::Beta(10, Some(12)));
251    }
252
253    fn check_not_parses(s: &str) {
254        assert_eq!(Version::parse(s), Version::Nonconformant(s.to_string()))
255    }
256
257    #[test]
258    fn test_nonconformant() {
259        check_not_parses("");
260        check_not_parses("foo");
261        check_not_parses("v");
262        check_not_parses("v-1");
263        check_not_parses("valpha");
264        check_not_parses("vbeta3");
265        check_not_parses("vv1");
266        check_not_parses("v1alpha1hi");
267        check_not_parses("v1zeta3");
268    }
269
270    #[test]
271    fn test_version_fromstr() {
272        assert_eq!(
273            Version::from_str("infallible").unwrap(),
274            Version::Nonconformant("infallible".to_string())
275        );
276    }
277
278    #[test]
279    fn test_version_priority_ord() {
280        // sorting makes sense from a "greater than" generation perspective:
281        assert!(Version::Stable(2).priority() > Version::Stable(1).priority());
282        assert!(Version::Stable(1).priority() > Version::Beta(1, None).priority());
283        assert!(Version::Stable(1).priority() > Version::Beta(2, None).priority());
284        assert!(Version::Stable(2).priority() > Version::Alpha(1, Some(2)).priority());
285        assert!(Version::Stable(1).priority() > Version::Alpha(2, Some(2)).priority());
286        assert!(Version::Beta(1, None).priority() > Version::Nonconformant("ver3".into()).priority());
287
288        assert!(Version::Stable(2).priority() > Version::Stable(1).priority());
289        assert!(Version::Stable(1).priority() > Version::Beta(2, None).priority());
290        assert!(Version::Stable(1).priority() > Version::Beta(2, Some(2)).priority());
291        assert!(Version::Stable(1).priority() > Version::Alpha(2, None).priority());
292        assert!(Version::Stable(1).priority() > Version::Alpha(2, Some(3)).priority());
293        assert!(Version::Stable(1).priority() > Version::Nonconformant("foo".to_string()).priority());
294        assert!(Version::Beta(1, Some(1)).priority() > Version::Beta(1, None).priority());
295        assert!(Version::Beta(1, Some(2)).priority() > Version::Beta(1, Some(1)).priority());
296        assert!(Version::Beta(1, None).priority() > Version::Alpha(1, None).priority());
297        assert!(Version::Beta(1, None).priority() > Version::Alpha(1, Some(3)).priority());
298        assert!(Version::Beta(1, None).priority() > Version::Nonconformant("foo".to_string()).priority());
299        assert!(Version::Beta(1, Some(2)).priority() > Version::Nonconformant("foo".to_string()).priority());
300        assert!(Version::Alpha(1, Some(1)).priority() > Version::Alpha(1, None).priority());
301        assert!(Version::Alpha(1, Some(2)).priority() > Version::Alpha(1, Some(1)).priority());
302        assert!(Version::Alpha(1, None).priority() > Version::Nonconformant("foo".to_string()).priority());
303        assert!(Version::Alpha(1, Some(2)).priority() > Version::Nonconformant("foo".to_string()).priority());
304        assert!(
305            Version::Nonconformant("bar".to_string()).priority()
306                > Version::Nonconformant("foo".to_string()).priority()
307        );
308        assert!(
309            Version::Nonconformant("foo1".to_string()).priority()
310                > Version::Nonconformant("foo10".to_string()).priority()
311        );
312
313        // sort orders by default are ascending
314        // sorting with std::cmp::Reverse on priority gives you the highest priority first
315        let mut vers = vec![
316            Version::Beta(2, Some(2)),
317            Version::Stable(1),
318            Version::Nonconformant("hi".into()),
319            Version::Alpha(3, Some(2)),
320            Version::Stable(2),
321            Version::Beta(2, Some(3)),
322        ];
323        vers.sort_by_cached_key(|x| Reverse(x.priority()));
324        assert_eq!(vers, vec![
325            Version::Stable(2),
326            Version::Stable(1),
327            Version::Beta(2, Some(3)),
328            Version::Beta(2, Some(2)),
329            Version::Alpha(3, Some(2)),
330            Version::Nonconformant("hi".into()),
331        ]);
332    }
333
334    #[test]
335    fn test_version_generation_ord() {
336        assert!(Version::Stable(2).generation() > Version::Stable(1).generation());
337        assert!(Version::Stable(1).generation() > Version::Beta(1, None).generation());
338        assert!(Version::Stable(1).generation() < Version::Beta(2, None).generation());
339        assert!(Version::Stable(2).generation() > Version::Alpha(1, Some(2)).generation());
340        assert!(Version::Stable(1).generation() < Version::Alpha(2, Some(2)).generation());
341        assert!(Version::Beta(1, None).generation() > Version::Nonconformant("ver3".into()).generation());
342
343        assert!(Version::Stable(2).generation() > Version::Stable(1).generation());
344        assert!(Version::Stable(1).generation() < Version::Beta(2, None).generation());
345        assert!(Version::Stable(1).generation() < Version::Beta(2, Some(2)).generation());
346        assert!(Version::Stable(1).generation() < Version::Alpha(2, None).generation());
347        assert!(Version::Stable(1).generation() < Version::Alpha(2, Some(3)).generation());
348        assert!(Version::Stable(1).generation() > Version::Nonconformant("foo".to_string()).generation());
349        assert!(Version::Beta(1, Some(1)).generation() > Version::Beta(1, None).generation());
350        assert!(Version::Beta(1, Some(2)).generation() > Version::Beta(1, Some(1)).generation());
351        assert!(Version::Beta(1, None).generation() > Version::Alpha(1, None).generation());
352        assert!(Version::Beta(1, None).generation() > Version::Alpha(1, Some(3)).generation());
353        assert!(Version::Beta(1, None).generation() > Version::Nonconformant("foo".to_string()).generation());
354        assert!(
355            Version::Beta(1, Some(2)).generation() > Version::Nonconformant("foo".to_string()).generation()
356        );
357        assert!(Version::Alpha(1, Some(1)).generation() > Version::Alpha(1, None).generation());
358        assert!(Version::Alpha(1, Some(2)).generation() > Version::Alpha(1, Some(1)).generation());
359        assert!(
360            Version::Alpha(1, None).generation() > Version::Nonconformant("foo".to_string()).generation()
361        );
362        assert!(
363            Version::Alpha(1, Some(2)).generation() > Version::Nonconformant("foo".to_string()).generation()
364        );
365        assert!(
366            Version::Nonconformant("bar".to_string()).generation()
367                > Version::Nonconformant("foo".to_string()).generation()
368        );
369        assert!(
370            Version::Nonconformant("foo1".to_string()).generation()
371                > Version::Nonconformant("foo10".to_string()).generation()
372        );
373
374        // sort orders by default is ascending
375        // sorting with std::cmp::Reverse on generation gives you the latest generation versions first
376        let mut vers = vec![
377            Version::Beta(2, Some(2)),
378            Version::Stable(1),
379            Version::Nonconformant("hi".into()),
380            Version::Alpha(3, Some(2)),
381            Version::Stable(2),
382            Version::Beta(2, Some(3)),
383        ];
384        vers.sort_by_cached_key(|x| Reverse(x.generation()));
385        assert_eq!(vers, vec![
386            Version::Alpha(3, Some(2)),
387            Version::Stable(2),
388            Version::Beta(2, Some(3)),
389            Version::Beta(2, Some(2)),
390            Version::Stable(1),
391            Version::Nonconformant("hi".into()),
392        ]);
393    }
394}