container_device_interface/
version.rs1use 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
13const 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
25const 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 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 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 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}