Skip to main content

container_device_interface/
device.rs

1use std::collections::BTreeMap;
2
3use anyhow::{anyhow, Context, Result};
4use oci_spec::runtime as oci;
5
6use crate::{
7    container_edits::{ContainerEdits, Validate},
8    internal::validation::validate::validate_spec_annotations,
9    parser::{qualified_name, validate_device_name},
10    spec::Spec,
11    specs::config::Device as CDIDevice,
12};
13
14// Device represents a CDI device of a Spec.
15#[derive(Clone, Debug, Eq, PartialEq, Hash)]
16pub struct Device {
17    pub cdi_device: CDIDevice,
18    cdi_spec: Spec,
19}
20
21impl Default for Device {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27// new_device creates a new Device, associate it with the given Spec.
28pub fn new_device(spec: &Spec, device: &CDIDevice) -> Result<Device> {
29    let device = Device {
30        cdi_device: device.clone(),
31        cdi_spec: spec.clone(),
32    };
33
34    if let Err(e) = device.validate() {
35        return Err(anyhow!(
36            "device validated failed with error: {:?}",
37            e.to_string()
38        ));
39    }
40
41    Ok(device)
42}
43
44impl Device {
45    // new returns a default Device
46    pub fn new() -> Self {
47        Self {
48            cdi_device: Default::default(),
49            cdi_spec: Default::default(),
50        }
51    }
52
53    // get_spec returns the Spec this device is defined in.
54    pub fn get_spec(&self) -> Spec {
55        self.cdi_spec.clone()
56    }
57
58    // get_qualified_name returns the qualified name for this device.
59    pub fn get_qualified_name(&self) -> String {
60        qualified_name(
61            &self.cdi_spec.get_vendor(),
62            &self.cdi_spec.get_class(),
63            &self.cdi_device.name,
64        )
65    }
66
67    // edits returns the applicable global container edits for this spec.
68    pub fn edits(&self) -> ContainerEdits {
69        ContainerEdits {
70            container_edits: self.cdi_device.container_edits.clone(),
71        }
72    }
73    // apply_edits applies the device-speific container edits to an OCI Spec.
74    pub fn apply_edits(&mut self, oci_spec: &mut oci::Spec) -> Result<()> {
75        self.edits().apply(oci_spec)?;
76        Ok(())
77    }
78
79    // validate the device.
80    pub fn validate(&self) -> Result<()> {
81        validate_device_name(&self.cdi_device.name).context("validate device name failed")?;
82        let name = self.get_qualified_name();
83
84        let annotations: &BTreeMap<String, String> =
85            &<BTreeMap<String, String> as Clone>::clone(&self.cdi_device.annotations)
86                .into_iter()
87                .collect();
88        if let Err(e) = validate_spec_annotations(&name, annotations) {
89            return Err(anyhow!(
90                "validate spec annotations failed with error: {:?}",
91                e
92            ));
93        }
94
95        let edits = self.edits();
96        if edits.is_empty() {
97            return Err(anyhow!("invalid device, empty device edits"));
98        }
99        edits
100            .validate()
101            .context(format!("invalid device {:?} ", self.cdi_device.name))?;
102
103        Ok(())
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::{
111        spec::new_spec,
112        specs::config::{
113            ContainerEdits as CDIContainerEdits, Device as CDIDeviceSpec, Spec as CDISpec,
114        },
115    };
116    use std::path::PathBuf;
117
118    fn spec_with(device: CDIDeviceSpec) -> Spec {
119        let raw = CDISpec {
120            version: "0.6.0".to_string(),
121            kind: "vendor.com/class".to_string(),
122            devices: vec![device.clone()],
123            ..Default::default()
124        };
125        new_spec(&raw, &PathBuf::from("/tmp/spec.yaml"), 0).unwrap()
126    }
127
128    fn cdi_device(name: &str) -> CDIDeviceSpec {
129        CDIDeviceSpec {
130            name: name.to_string(),
131            container_edits: CDIContainerEdits {
132                env: Some(vec!["X=1".to_string()]),
133                ..Default::default()
134            },
135            ..Default::default()
136        }
137    }
138
139    #[test]
140    fn new_device_builds_and_applies_edits() {
141        let raw = cdi_device("gpu0");
142        let spec = spec_with(raw.clone());
143        let mut device = new_device(&spec, &raw).unwrap();
144
145        assert_eq!(device.get_qualified_name(), "vendor.com/class=gpu0");
146        assert_eq!(device.get_spec().get_vendor(), "vendor.com");
147
148        let mut oci_spec = oci::Spec::default();
149        device.apply_edits(&mut oci_spec).unwrap();
150        let env = oci_spec.process().as_ref().unwrap().env().as_ref().unwrap();
151        assert!(env.contains(&"X=1".to_string()));
152    }
153
154    #[test]
155    fn new_device_rejects_invalid_names_and_empty_edits() {
156        let bad_name = cdi_device("not a name");
157        let spec = spec_with(cdi_device("gpu0"));
158        let err = new_device(&spec, &bad_name).unwrap_err();
159        assert!(err.to_string().contains("validate device name failed"));
160
161        let empty_edits = CDIDeviceSpec {
162            name: "gpu1".to_string(),
163            ..Default::default()
164        };
165        let err = new_device(&spec, &empty_edits).unwrap_err();
166        assert!(err.to_string().contains("empty device edits"));
167    }
168
169    #[test]
170    fn default_device_is_constructible() {
171        let device = Device::default();
172        assert_eq!(device.cdi_device.name, "");
173    }
174}