Skip to main content

container_device_interface/
version.rs

1use anyhow::Result;
2use once_cell::sync::Lazy;
3use std::collections::BTreeMap;
4
5use crate::parser::parse_qualifier;
6use crate::specs::config;
7use crate::specs::config::Spec as CDISpec;
8use semver::Version;
9
10const CURRENT_VERSION: &str = config::CURRENT_VERSION;
11static VCURRENT: Lazy<String> = Lazy::new(|| format!("v{}", CURRENT_VERSION));
12
13// Released versions of the CDI specification
14const V010: &str = "v0.1.0";
15const V020: &str = "v0.2.0";
16const V030: &str = "v0.3.0";
17const V040: &str = "v0.4.0";
18const V050: &str = "v0.5.0";
19const V060: &str = "v0.6.0";
20const V070: &str = "v0.7.0";
21const V080: &str = "v0.8.0";
22const V100: &str = "v1.0.0";
23const V110: &str = "v1.1.0";
24
25// Earliest supported version of the CDI specification
26const VEARLIEST: &str = V030;
27
28type RequiredFunc = fn(&CDISpec) -> bool;
29
30#[derive(Default)]
31pub struct VersionMap(BTreeMap<String, Option<RequiredFunc>>);
32
33pub static VALID_SPEC_VERSIONS: Lazy<VersionMap> = Lazy::new(|| {
34    let mut map = BTreeMap::new();
35    map.insert(V010.to_string(), None);
36    map.insert(V020.to_string(), None);
37    map.insert(V030.to_string(), None);
38    map.insert(V040.to_string(), Some(requires_v040 as RequiredFunc));
39    map.insert(V050.to_string(), Some(requires_v050));
40    map.insert(V060.to_string(), Some(requires_v060));
41    map.insert(V070.to_string(), Some(requires_v070));
42    map.insert(V080.to_string(), None);
43    map.insert(V100.to_string(), None);
44    map.insert(V110.to_string(), Some(requires_v110 as RequiredFunc));
45    VersionMap(map)
46});
47
48impl VersionMap {
49    pub fn is_valid_version(&self, spec_version: &str) -> bool {
50        self.0
51            .contains_key(&format!("v{}", VersionWrapper::new(spec_version)))
52    }
53
54    pub fn required_version(&self, spec: &CDISpec) -> VersionWrapper {
55        let mut min_version = VersionWrapper::new(VEARLIEST);
56        for (v, is_required) in &self.0 {
57            if let Some(is_required_fn) = is_required {
58                let version_wrapper = VersionWrapper::new(v);
59                if is_required_fn(spec) && version_wrapper.is_greater_than(&min_version) {
60                    min_version = version_wrapper;
61                }
62                if min_version.is_latest() {
63                    break;
64                }
65            }
66        }
67        min_version
68    }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
72pub struct VersionWrapper(String);
73
74impl std::fmt::Display for VersionWrapper {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "{}", self.0.trim_start_matches('v'))
77    }
78}
79
80impl VersionWrapper {
81    pub fn new(v: &str) -> Self {
82        VersionWrapper(format!("v{}", v.trim_start_matches('v')))
83    }
84
85    pub fn is_greater_than(&self, other: &VersionWrapper) -> bool {
86        Version::parse(&self.to_string()).unwrap() > Version::parse(&other.to_string()).unwrap()
87    }
88
89    fn is_latest(&self) -> bool {
90        self.0 == *VCURRENT
91    }
92}
93
94pub fn minimum_required_version(spec: &CDISpec) -> Result<VersionWrapper> {
95    Ok(VALID_SPEC_VERSIONS.required_version(spec))
96}
97
98pub(crate) fn validate_declared_version_fields(spec: &CDISpec) -> Result<()> {
99    if !VALID_SPEC_VERSIONS.is_valid_version(&spec.version) {
100        return Err(anyhow::anyhow!("invalid version {}", spec.version));
101    }
102
103    let declared = VersionWrapper::new(&spec.version);
104    let v110 = VersionWrapper::new(V110);
105    let declared_is_v110_or_newer = !v110.is_greater_than(&declared);
106
107    for (scope, edits) in spec
108        .container_edits
109        .iter()
110        .map(|edits| ("containerEdits", edits))
111        .chain(
112            spec.devices
113                .iter()
114                .map(|device| ("devices[].containerEdits", &device.container_edits)),
115        )
116    {
117        if let Some(intel_rdt) = &edits.intel_rdt {
118            if declared_is_v110_or_newer {
119                if intel_rdt.enable_cmt.is_some() {
120                    return Err(anyhow::anyhow!(
121                        "{}.intelRdt.enableCMT is not valid for CDI spec version {}",
122                        scope,
123                        spec.version
124                    ));
125                }
126                if intel_rdt.enable_mbm.is_some() {
127                    return Err(anyhow::anyhow!(
128                        "{}.intelRdt.enableMBM is not valid for CDI spec version {}",
129                        scope,
130                        spec.version
131                    ));
132                }
133            } else {
134                if intel_rdt.schemata.is_some() {
135                    return Err(anyhow::anyhow!(
136                        "{}.intelRdt.schemata requires CDI spec version 1.1.0",
137                        scope
138                    ));
139                }
140                if intel_rdt.enable_monitoring.is_some() {
141                    return Err(anyhow::anyhow!(
142                        "{}.intelRdt.enableMonitoring requires CDI spec version 1.1.0",
143                        scope
144                    ));
145                }
146            }
147        }
148
149        if !declared_is_v110_or_newer
150            && edits
151                .net_devices
152                .as_ref()
153                .is_some_and(|devices| !devices.is_empty())
154        {
155            return Err(anyhow::anyhow!(
156                "{}.netDevices requires CDI spec version 1.1.0",
157                scope
158            ));
159        }
160    }
161
162    Ok(())
163}
164
165fn requires_v110(spec: &CDISpec) -> bool {
166    if let Some(edits) = &spec.container_edits {
167        if edits
168            .net_devices
169            .as_ref()
170            .is_some_and(|devices| !devices.is_empty())
171        {
172            return true;
173        }
174        if let Some(intel_rdt) = &edits.intel_rdt {
175            if intel_rdt.schemata.is_some() || intel_rdt.enable_monitoring.is_some() {
176                return true;
177            }
178        }
179    }
180
181    for dev in &spec.devices {
182        let edits = &dev.container_edits;
183        if edits
184            .net_devices
185            .as_ref()
186            .is_some_and(|devices| !devices.is_empty())
187        {
188            return true;
189        }
190        if let Some(intel_rdt) = &edits.intel_rdt {
191            if intel_rdt.schemata.is_some() || intel_rdt.enable_monitoring.is_some() {
192                return true;
193            }
194        }
195    }
196
197    false
198}
199
200fn requires_v070(spec: &CDISpec) -> bool {
201    let edits = &spec.container_edits;
202    if let Some(edits) = edits {
203        if edits.intel_rdt.as_ref().is_some() {
204            return true;
205        }
206        if edits
207            .additional_gids
208            .as_ref()
209            .is_some_and(|v| !v.is_empty())
210        {
211            return true;
212        }
213    }
214
215    for d in &spec.devices {
216        let edits = &d.container_edits;
217
218        if edits.intel_rdt.as_ref().is_some() {
219            return true;
220        }
221        if edits
222            .additional_gids
223            .as_ref()
224            .is_some_and(|v| !v.is_empty())
225        {
226            return true;
227        }
228    }
229    false
230}
231
232fn requires_v060(spec: &CDISpec) -> bool {
233    if !spec.annotations.is_empty() {
234        return true;
235    }
236    for d in &spec.devices {
237        if !d.annotations.is_empty() {
238            return true;
239        }
240    }
241    let (vendor, class) = parse_qualifier(&spec.kind);
242    if !vendor.is_empty() && class.contains('.') {
243        return true;
244    }
245    false
246}
247
248fn requires_v050(spec: &CDISpec) -> bool {
249    if spec
250        .devices
251        .iter()
252        .any(|d| !d.name.chars().next().unwrap_or_default().is_alphabetic())
253    {
254        return true;
255    }
256
257    let edits = spec
258        .devices
259        .iter()
260        .map(|d| &d.container_edits)
261        .chain(spec.container_edits.as_ref());
262
263    edits
264        .flat_map(|edits| edits.device_nodes.iter().flat_map(|nodes| nodes.iter()))
265        .any(|node| {
266            node.host_path
267                .as_deref()
268                .is_some_and(|path| !path.is_empty())
269        })
270}
271
272fn requires_v040(spec: &CDISpec) -> bool {
273    spec.devices
274        .iter()
275        .map(|d| &d.container_edits)
276        .chain(spec.container_edits.as_ref())
277        .flat_map(|edits| edits.mounts.iter().flat_map(|mounts| mounts.iter()))
278        .any(|mount| mount.r#type.as_ref().is_some_and(|typ| !typ.is_empty()))
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::specs::config::{ContainerEdits, Device, IntelRdt, LinuxNetDevice, Spec};
285
286    fn spec_with_edits(version: &str, edits: ContainerEdits) -> Spec {
287        Spec {
288            version: version.to_string(),
289            kind: "vendor.com/device".to_string(),
290            devices: vec![Device {
291                name: "gpu0".to_string(),
292                container_edits: ContainerEdits::default(),
293                ..Default::default()
294            }],
295            container_edits: Some(edits),
296            ..Default::default()
297        }
298    }
299
300    #[test]
301    fn accepts_current_v1_1_0_version() {
302        assert!(VALID_SPEC_VERSIONS.is_valid_version("1.1.0"));
303    }
304
305    #[test]
306    fn recognizes_v0_8_and_v1_0_versions() {
307        assert!(VALID_SPEC_VERSIONS.is_valid_version("0.8.0"));
308        assert!(VALID_SPEC_VERSIONS.is_valid_version("1.0.0"));
309    }
310
311    #[test]
312    fn net_devices_require_v1_1_0() {
313        let spec = spec_with_edits(
314            "1.1.0",
315            ContainerEdits {
316                net_devices: Some(vec![LinuxNetDevice {
317                    host_interface_name: "eth0".to_string(),
318                    name: "container_eth0".to_string(),
319                }]),
320                ..Default::default()
321            },
322        );
323
324        assert_eq!(
325            minimum_required_version(&spec).unwrap().to_string(),
326            "1.1.0"
327        );
328    }
329
330    #[test]
331    fn intel_rdt_schemata_requires_v1_1_0() {
332        let spec = spec_with_edits(
333            "1.1.0",
334            ContainerEdits {
335                intel_rdt: Some(IntelRdt {
336                    schemata: Some(vec!["L3:0=ffff".to_string()]),
337                    ..Default::default()
338                }),
339                ..Default::default()
340            },
341        );
342
343        assert_eq!(
344            minimum_required_version(&spec).unwrap().to_string(),
345            "1.1.0"
346        );
347    }
348
349    #[test]
350    fn empty_intel_rdt_schemata_requires_v1_1_0() {
351        let global_spec = spec_with_edits(
352            "1.1.0",
353            ContainerEdits {
354                intel_rdt: Some(IntelRdt {
355                    schemata: Some(Vec::new()),
356                    ..Default::default()
357                }),
358                ..Default::default()
359            },
360        );
361        let device_spec = Spec {
362            version: "1.1.0".to_string(),
363            kind: "vendor.com/device".to_string(),
364            devices: vec![Device {
365                name: "gpu0".to_string(),
366                container_edits: ContainerEdits {
367                    intel_rdt: Some(IntelRdt {
368                        schemata: Some(Vec::new()),
369                        ..Default::default()
370                    }),
371                    ..Default::default()
372                },
373                ..Default::default()
374            }],
375            ..Default::default()
376        };
377
378        assert_eq!(
379            minimum_required_version(&global_spec).unwrap().to_string(),
380            "1.1.0"
381        );
382        assert_eq!(
383            minimum_required_version(&device_spec).unwrap().to_string(),
384            "1.1.0"
385        );
386    }
387
388    #[test]
389    fn intel_rdt_enable_monitoring_requires_v1_1_0() {
390        let spec = spec_with_edits(
391            "1.1.0",
392            ContainerEdits {
393                intel_rdt: Some(IntelRdt {
394                    enable_monitoring: Some(true),
395                    ..Default::default()
396                }),
397                ..Default::default()
398            },
399        );
400
401        assert_eq!(
402            minimum_required_version(&spec).unwrap().to_string(),
403            "1.1.0"
404        );
405    }
406
407    #[test]
408    fn v110_specs_reject_legacy_intel_rdt_fields() {
409        for (cmt, mbm, needle) in [
410            (Some(true), None, "enableCMT"),
411            (None, Some(true), "enableMBM"),
412        ] {
413            let spec = CDISpec {
414                version: V110.to_string(),
415                kind: "vendor.com/device".to_string(),
416                container_edits: Some(ContainerEdits {
417                    intel_rdt: Some(IntelRdt {
418                        enable_cmt: cmt,
419                        enable_mbm: mbm,
420                        ..Default::default()
421                    }),
422                    ..Default::default()
423                }),
424                ..Default::default()
425            };
426            let err = validate_declared_version_fields(&spec).unwrap_err();
427            assert!(err.to_string().contains(needle), "{err}");
428        }
429    }
430
431    #[test]
432    fn pre_v110_specs_reject_net_devices() {
433        let spec = CDISpec {
434            version: V070.to_string(),
435            kind: "vendor.com/device".to_string(),
436            container_edits: Some(ContainerEdits {
437                net_devices: Some(vec![LinuxNetDevice {
438                    host_interface_name: "eth0".to_string(),
439                    name: "c_eth0".to_string(),
440                }]),
441                ..Default::default()
442            }),
443            ..Default::default()
444        };
445        let err = validate_declared_version_fields(&spec).unwrap_err();
446        assert!(err.to_string().contains("netDevices requires"), "{err}");
447    }
448
449    #[test]
450    fn device_level_edits_drive_required_versions() {
451        let with_device_edits = |edits: ContainerEdits| CDISpec {
452            version: VCURRENT.to_string(),
453            kind: "vendor.com/device".to_string(),
454            devices: vec![Device {
455                name: "d0".to_string(),
456                container_edits: edits,
457                ..Default::default()
458            }],
459            ..Default::default()
460        };
461
462        // device-level intelRdt (clos only) -> 0.7.0
463        let spec = with_device_edits(ContainerEdits {
464            intel_rdt: Some(IntelRdt {
465                clos_id: Some("c".to_string()),
466                ..Default::default()
467            }),
468            ..Default::default()
469        });
470        assert_eq!(
471            minimum_required_version(&spec).unwrap().to_string(),
472            "0.7.0"
473        );
474
475        // device-level netDevices -> 1.1.0
476        let spec = with_device_edits(ContainerEdits {
477            net_devices: Some(vec![LinuxNetDevice {
478                host_interface_name: "eth0".to_string(),
479                name: "c".to_string(),
480            }]),
481            ..Default::default()
482        });
483        assert_eq!(
484            minimum_required_version(&spec).unwrap().to_string(),
485            "1.1.0"
486        );
487
488        // device-level additionalGIDs -> 0.7.0
489        let spec = with_device_edits(ContainerEdits {
490            additional_gids: Some(vec![5]),
491            ..Default::default()
492        });
493        assert_eq!(
494            minimum_required_version(&spec).unwrap().to_string(),
495            "0.7.0"
496        );
497    }
498
499    #[test]
500    fn annotations_and_dotted_class_require_v060() {
501        let mut spec = CDISpec {
502            version: VCURRENT.to_string(),
503            kind: "vendor.com/device".to_string(),
504            ..Default::default()
505        };
506        spec.annotations.insert("k".to_string(), "v".to_string());
507        assert_eq!(
508            minimum_required_version(&spec).unwrap().to_string(),
509            "0.6.0"
510        );
511
512        let mut spec = CDISpec {
513            version: VCURRENT.to_string(),
514            kind: "vendor.com/class.with.dots".to_string(),
515            ..Default::default()
516        };
517        assert_eq!(
518            minimum_required_version(&spec).unwrap().to_string(),
519            "0.6.0"
520        );
521        spec.devices = vec![Device {
522            name: "d0".to_string(),
523            annotations: [("a".to_string(), "b".to_string())].into(),
524            ..Default::default()
525        }];
526        assert_eq!(
527            minimum_required_version(&spec).unwrap().to_string(),
528            "0.6.0"
529        );
530    }
531}