container_device_interface/
spec.rs

1use std::{collections::BTreeMap, fs::File, path::PathBuf};
2
3use anyhow::{anyhow, Context, Result};
4use oci_spec::runtime as oci;
5use path_clean::clean;
6
7use crate::{
8    container_edits::ContainerEdits,
9    container_edits::Validate,
10    device::new_device,
11    device::Device,
12    internal::validation::validate::validate_spec_annotations,
13    parser::parse_qualifier,
14    parser::validate_class_name,
15    parser::validate_vendor_name,
16    specs::config::Spec as CDISpec,
17    utils::is_cdi_spec,
18    version::{minimum_required_version, VersionWrapper, VALID_SPEC_VERSIONS},
19};
20
21const DEFAULT_SPEC_EXT_SUFFIX: &str = ".yaml";
22
23// Spec represents a single CDI Spec. It is usually loaded from a
24// file and stored in a cache. The Spec has an associated priority.
25// This priority is inherited from the associated priority of the
26// CDI Spec directory that contains the CDI Spec file and is used
27// to resolve conflicts if multiple CDI Spec files contain entries
28// for the same fully qualified device.
29#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
30pub struct Spec {
31    pub cdi_spec: CDISpec,
32    vendor: String,
33    class: String,
34    path: String,
35    priority: i32,
36    pub devices: BTreeMap<String, Device>,
37}
38
39impl Spec {
40    // get_vendor returns the vendor of this Spec.
41    pub fn get_vendor(&self) -> String {
42        self.vendor.clone()
43    }
44
45    // get_class returns the device class of this Spec.
46    pub fn get_class(&self) -> String {
47        self.class.clone()
48    }
49
50    // get_devices returns the devices list.
51    pub fn get_devices(&self) -> BTreeMap<String, Device> {
52        self.devices.clone()
53    }
54
55    // get_device returns the device for the given unqualified name.
56    pub fn get_device(&self, key: &str) -> Option<&Device> {
57        self.devices.get(key)
58    }
59
60    // get_path returns the filesystem path of this Spec.
61    pub fn get_path(&self) -> String {
62        self.path.clone()
63    }
64
65    // get_priority returns the priority of this Spec.
66    pub fn get_priority(&self) -> i32 {
67        self.priority
68    }
69
70    // edits returns the applicable global container edits for this spec.
71    pub fn edits(&mut self) -> Option<ContainerEdits> {
72        self.cdi_spec
73            .container_edits
74            .clone()
75            .map(|ce| ContainerEdits {
76                container_edits: ce,
77            })
78    }
79
80    // validate the Spec.
81    pub fn validate(&mut self) -> Result<BTreeMap<String, Device>> {
82        validate_version(&self.cdi_spec).context("validate cdi version failed")?;
83        validate_vendor_name(&self.vendor).context("validate vendor name failed")?;
84        validate_class_name(&self.class).context("validate class name failed")?;
85        validate_spec_annotations(&self.cdi_spec.kind, &self.cdi_spec.annotations)
86            .context("validate spec annotations failed")?;
87
88        if let Some(ref mut ce) = self.edits() {
89            ce.validate().context("validate container edits failed")?;
90        }
91
92        let mut devices = BTreeMap::new();
93        for d in &self.cdi_spec.devices {
94            let dev =
95                new_device(self, d).with_context(|| format!("failed to add device {}", d.name))?;
96            if devices.contains_key(&d.name) {
97                return Err(anyhow::anyhow!("invalid spec, multiple device {}", d.name));
98            }
99            devices.insert(d.name.clone(), dev);
100        }
101
102        Ok(devices)
103    }
104
105    // apply_edits applies the Spec's global-scope container edits to an OCI Spec.
106    pub fn apply_edits(&mut self, oci_spec: &mut oci::Spec) -> Result<()> {
107        if let Some(ref mut ce) = self.edits() {
108            ce.apply(oci_spec)
109                .context("container edits applys failed.")?;
110        }
111
112        Ok(())
113    }
114}
115
116pub fn parse_spec(path: &PathBuf) -> Result<CDISpec> {
117    if !path.exists() {
118        return Err(anyhow!("CDI spec path not found"));
119    }
120
121    let config_file = File::open(path).context("open config file")?;
122    let cdi_spec: CDISpec =
123        serde_yaml::from_reader(config_file).context("serde yaml read from file")?;
124
125    Ok(cdi_spec)
126}
127
128// validate_spec validates the Spec using the extneral validator.
129pub fn validate_spec(_raw_spec: &CDISpec) -> Result<()> {
130    // TODO
131    Ok(())
132}
133
134// read_spec reads the given CDI Spec file. The resulting Spec is
135// assigned the given priority. If reading or parsing the Spec
136// data fails read_spec returns a nil Spec and an error.
137pub fn read_spec(path: &PathBuf, priority: i32) -> Result<Spec> {
138    let raw_spec = parse_spec(path).context("parse spec file failed")?;
139    let cdi_spec = new_spec(&raw_spec, path, priority).context("create a new cdi spec failed")?;
140
141    Ok(cdi_spec)
142}
143
144// new_spec creates a new Spec from the given CDI Spec data. The
145// Spec is marked as loaded from the given path with the given
146// priority. If Spec data validation fails new_spec returns an error.
147pub fn new_spec(raw_spec: &CDISpec, path: &PathBuf, priority: i32) -> Result<Spec> {
148    validate_spec(raw_spec).context("invalid CDI Spec")?;
149
150    let mut cleaned_path = clean(path);
151    if !is_cdi_spec(&cleaned_path) {
152        cleaned_path.set_extension(DEFAULT_SPEC_EXT_SUFFIX);
153    }
154
155    let (vendor, class) = parse_qualifier(&raw_spec.kind);
156
157    let mut spec: Spec = Spec {
158        cdi_spec: raw_spec.clone(),
159        path: cleaned_path.display().to_string(),
160        priority,
161        vendor: vendor.to_owned(),
162        class: class.to_owned(),
163        ..Default::default()
164    };
165    spec.devices = spec.validate().context("validate spec failed")?;
166
167    Ok(spec)
168}
169
170fn validate_version(cdi_spec: &CDISpec) -> Result<()> {
171    let version = &cdi_spec.version;
172    if !VALID_SPEC_VERSIONS.is_valid_version(version) {
173        return Err(anyhow::anyhow!("invalid version {}", version));
174    }
175
176    let min_version = minimum_required_version(cdi_spec)
177        .with_context(|| "could not determine minimum required version")?;
178
179    if min_version.is_greater_than(&VersionWrapper::new(version)) {
180        return Err(anyhow::anyhow!(
181            "the spec version must be at least v{}",
182            min_version.to_string()
183        ));
184    }
185
186    Ok(())
187}