container_device_interface/
spec.rs1use std::{collections::BTreeMap, 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::{
19 minimum_required_version, validate_declared_version_fields, VersionWrapper,
20 VALID_SPEC_VERSIONS,
21 },
22};
23
24const DEFAULT_SPEC_EXT_SUFFIX: &str = "yaml";
27
28#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
35pub struct Spec {
36 pub cdi_spec: CDISpec,
37 vendor: String,
38 class: String,
39 path: String,
40 priority: i32,
41 pub devices: BTreeMap<String, Device>,
42}
43
44impl Spec {
45 pub fn get_vendor(&self) -> String {
47 self.vendor.clone()
48 }
49
50 pub fn get_class(&self) -> String {
52 self.class.clone()
53 }
54
55 pub fn get_devices(&self) -> BTreeMap<String, Device> {
57 self.devices.clone()
58 }
59
60 pub fn get_device(&self, key: &str) -> Option<&Device> {
62 self.devices.get(key)
63 }
64
65 pub fn get_path(&self) -> String {
67 self.path.clone()
68 }
69
70 pub fn get_priority(&self) -> i32 {
72 self.priority
73 }
74
75 pub fn edits(&mut self) -> Option<ContainerEdits> {
77 self.cdi_spec
78 .container_edits
79 .clone()
80 .map(|ce| ContainerEdits {
81 container_edits: ce,
82 })
83 }
84
85 pub fn validate(&mut self) -> Result<BTreeMap<String, Device>> {
87 validate_version(&self.cdi_spec).context("validate cdi version failed")?;
88 validate_vendor_name(&self.vendor).context("validate vendor name failed")?;
89 validate_class_name(&self.class).context("validate class name failed")?;
90 validate_spec_annotations(&self.cdi_spec.kind, &self.cdi_spec.annotations)
91 .context("validate spec annotations failed")?;
92
93 if let Some(ref mut ce) = self.edits() {
94 ce.validate().context("validate container edits failed")?;
95 }
96
97 let mut devices = BTreeMap::new();
98 for d in &self.cdi_spec.devices {
99 let dev =
100 new_device(self, d).with_context(|| format!("failed to add device {}", d.name))?;
101 if devices.contains_key(&d.name) {
102 return Err(anyhow::anyhow!("invalid spec, multiple device {}", d.name));
103 }
104 devices.insert(d.name.clone(), dev);
105 }
106
107 if devices.is_empty() {
108 return Err(anyhow::anyhow!("invalid spec, no devices"));
109 }
110
111 Ok(devices)
112 }
113
114 pub fn apply_edits(&mut self, oci_spec: &mut oci::Spec) -> Result<()> {
116 if let Some(ref mut ce) = self.edits() {
117 ce.apply(oci_spec)
118 .context("container edits applys failed.")?;
119 }
120
121 Ok(())
122 }
123}
124
125pub fn parse_spec(path: &PathBuf) -> Result<CDISpec> {
126 if !path.exists() {
127 return Err(anyhow!("CDI spec path not found"));
128 }
129
130 let data = std::fs::read(path).context("read config file")?;
131 let cdi_spec: CDISpec = serde_yaml::from_slice(&data).context("serde yaml read from file")?;
132
133 Ok(cdi_spec)
134}
135
136pub fn validate_spec(raw_spec: &CDISpec) -> Result<()> {
138 let data = serde_yaml::to_string(raw_spec).context("marshal CDI spec for schema validation")?;
139 crate::schema::validate_builtin(data.as_bytes()).context("invalid CDI Spec schema")?;
140 Ok(())
141}
142
143pub fn read_spec(path: &PathBuf, priority: i32) -> Result<Spec> {
147 let raw_spec = parse_spec(path).context("parse spec file failed")?;
148 let cdi_spec = new_spec(&raw_spec, path, priority).context("create a new cdi spec failed")?;
149
150 Ok(cdi_spec)
151}
152
153pub fn new_spec(raw_spec: &CDISpec, path: &PathBuf, priority: i32) -> Result<Spec> {
157 if raw_spec.devices.is_empty() {
158 return Err(anyhow::anyhow!("invalid spec, no devices"));
159 }
160
161 validate_spec(raw_spec).context("invalid CDI Spec")?;
162
163 let mut cleaned_path = clean(path);
164 if !is_cdi_spec(&cleaned_path) {
165 cleaned_path.set_extension(DEFAULT_SPEC_EXT_SUFFIX);
166 }
167
168 let (vendor, class) = parse_qualifier(&raw_spec.kind);
169
170 let mut spec: Spec = Spec {
171 cdi_spec: raw_spec.clone(),
172 path: cleaned_path.display().to_string(),
173 priority,
174 vendor: vendor.to_owned(),
175 class: class.to_owned(),
176 ..Default::default()
177 };
178 spec.devices = spec.validate().context("validate spec failed")?;
179
180 Ok(spec)
181}
182
183fn validate_version(cdi_spec: &CDISpec) -> Result<()> {
184 let version = &cdi_spec.version;
185 if !VALID_SPEC_VERSIONS.is_valid_version(version) {
186 return Err(anyhow::anyhow!("invalid version {}", version));
187 }
188
189 validate_declared_version_fields(cdi_spec)?;
190
191 let min_version = minimum_required_version(cdi_spec)
192 .with_context(|| "could not determine minimum required version")?;
193
194 if min_version.is_greater_than(&VersionWrapper::new(version)) {
195 return Err(anyhow::anyhow!(
196 "the spec version must be at least v{}",
197 min_version
198 ));
199 }
200
201 Ok(())
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use oci_spec::runtime as oci;
208 use std::path::PathBuf;
209
210 #[test]
211 fn parse_spec_rejects_unknown_fields() {
212 let path = PathBuf::from("tests/fixtures/cdi-unknown-field.yaml");
213 let err = parse_spec(&path).expect_err("unknown field should fail");
214 assert!(err.to_string().contains("serde yaml read from file"));
215 }
216
217 #[test]
218 fn new_spec_rejects_empty_devices() {
219 let path = PathBuf::from("tests/fixtures/cdi-empty-devices.yaml");
220 let raw = parse_spec(&path).expect("empty device fixture parses");
221 let err = new_spec(&raw, &path, 0).expect_err("empty devices should fail");
222 assert!(err.to_string().contains("no devices"));
223 }
224
225 #[test]
226 fn new_spec_rejects_legacy_intel_rdt_fields_in_v1_1() {
227 let path = PathBuf::from("tests/fixtures/cdi-v1.1-legacy-intel-rdt.yaml");
228 let raw = parse_spec(&path).expect("legacy fields should parse before version validation");
229 let err = new_spec(&raw, &path, 0).expect_err("v1.1.0 should reject legacy fields");
230
231 assert!(format!("{err:#}").contains("enableCMT"));
232 }
233
234 #[test]
235 fn new_spec_processes_legacy_intel_rdt_fields_before_v1_1() {
236 let path = PathBuf::from("tests/fixtures/cdi-v1.0-legacy-intel-rdt.yaml");
237 let raw = parse_spec(&path).expect("v1.0.0 legacy Intel RDT fixture parses");
238 let mut spec = new_spec(&raw, &path, 0).expect("v1.0.0 legacy Intel RDT fixture validates");
239 let mut oci_spec = oci::Spec::default();
240
241 spec.apply_edits(&mut oci_spec)
242 .expect("legacy Intel RDT edits apply");
243
244 let rdt = oci_spec
245 .linux()
246 .as_ref()
247 .and_then(|linux| linux.intel_rdt().as_ref())
248 .expect("Intel RDT should be set");
249
250 #[allow(deprecated)]
251 {
252 assert_eq!(&Some(true), rdt.enable_cmt());
253 assert_eq!(&Some(true), rdt.enable_mbm());
254 }
255 }
256
257 #[test]
258 fn new_spec_rejects_empty_device_edits() {
259 let raw = CDISpec {
260 version: "1.1.0".to_string(),
261 kind: "vendor.com/device".to_string(),
262 devices: vec![crate::specs::config::Device {
263 name: "gpu0".to_string(),
264 container_edits: crate::specs::config::ContainerEdits::default(),
265 ..Default::default()
266 }],
267 ..Default::default()
268 };
269 let path = PathBuf::from("/tmp/vendor-device.yaml");
270
271 let err = new_spec(&raw, &path, 0).expect_err("empty device edits should fail");
272 let err = format!("{err:?}");
273
274 assert!(err.contains("empty device edits"), "{err}");
275 }
276
277 fn one_device(name: &str) -> crate::specs::config::Device {
278 crate::specs::config::Device {
279 name: name.to_string(),
280 container_edits: crate::specs::config::ContainerEdits {
281 env: Some(vec!["X=1".to_string()]),
282 ..Default::default()
283 },
284 ..Default::default()
285 }
286 }
287
288 #[test]
289 fn new_spec_rejects_duplicate_device_names() {
290 let raw = crate::specs::config::Spec {
291 version: "0.6.0".to_string(),
292 kind: "vendor.com/device".to_string(),
293 devices: vec![one_device("d0"), one_device("d0")],
294 ..Default::default()
295 };
296 let err = new_spec(&raw, &PathBuf::from("/tmp/x.yaml"), 0).unwrap_err();
297 assert!(format!("{err:?}").contains("multiple device"));
298 }
299
300 #[test]
301 fn new_spec_normalizes_non_spec_extensions() {
302 let raw = crate::specs::config::Spec {
303 version: "0.6.0".to_string(),
304 kind: "vendor.com/device".to_string(),
305 devices: vec![one_device("d0")],
306 ..Default::default()
307 };
308 let spec = new_spec(&raw, &PathBuf::from("/tmp/spec.conf"), 0).unwrap();
309 assert!(spec.get_path().ends_with("spec.yaml"));
310 }
311
312 #[test]
313 fn spec_level_edits_apply_to_oci_spec() {
314 let raw = crate::specs::config::Spec {
315 version: "0.6.0".to_string(),
316 kind: "vendor.com/device".to_string(),
317 container_edits: Some(crate::specs::config::ContainerEdits {
318 env: Some(vec!["GLOBAL=1".to_string()]),
319 ..Default::default()
320 }),
321 devices: vec![one_device("d0")],
322 ..Default::default()
323 };
324 let mut spec = new_spec(&raw, &PathBuf::from("/tmp/x.yaml"), 0).unwrap();
325 let mut oci_spec = oci::Spec::default();
326 spec.apply_edits(&mut oci_spec).unwrap();
327 let env = oci_spec.process().as_ref().unwrap().env().as_ref().unwrap();
328 assert!(env.contains(&"GLOBAL=1".to_string()));
329 }
330
331 #[test]
332 fn parse_spec_requires_an_existing_path() {
333 let err = parse_spec(&PathBuf::from("/nonexistent/spec.yaml")).unwrap_err();
334 assert!(err.to_string().contains("not found"));
335 }
336
337 #[test]
338 fn validate_version_rejects_unknown_and_understated_versions() {
339 let mut raw = crate::specs::config::Spec {
340 version: "9.9.9".to_string(),
341 kind: "vendor.com/device".to_string(),
342 devices: vec![one_device("d0")],
343 ..Default::default()
344 };
345 let err = new_spec(&raw, &PathBuf::from("/tmp/x.yaml"), 0).unwrap_err();
346 assert!(format!("{err:?}").contains("invalid version"));
347
348 raw.version = "0.5.0".to_string();
350 raw.devices[0].container_edits.additional_gids = Some(vec![5]);
351 let err = new_spec(&raw, &PathBuf::from("/tmp/x.yaml"), 0).unwrap_err();
352 assert!(format!("{err:?}").contains("must be at least"));
353 }
354}