container_device_interface/
spec_dirs.rs1use std::{
2 collections::HashMap,
3 error::Error,
4 fmt, fs,
5 path::{Path, PathBuf},
6};
7
8use lazy_static::lazy_static;
9use path_clean::clean;
10
11use crate::{
12 cache::{Cache, CdiOption},
13 spec::{read_spec, Spec},
14 utils::is_cdi_spec,
15};
16
17const DEFAULT_STATIC_DIR: &str = "/etc/cdi";
19const DEFAULT_DYNAMIC_DIR: &str = "/var/run/cdi";
21
22lazy_static! {
23 pub static ref DEFAULT_SPEC_DIRS: &'static [&'static str] = &[
30 DEFAULT_STATIC_DIR,
31 DEFAULT_DYNAMIC_DIR,
32 ];
33}
34
35#[derive(Debug)]
36pub struct SpecError {
37 message: String,
38}
39
40impl SpecError {
41 pub fn new(info: &str) -> Self {
42 Self {
43 message: info.to_owned(),
44 }
45 }
46}
47
48impl fmt::Display for SpecError {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(f, "spec error message {}", self.message)
51 }
52}
53
54impl Error for SpecError {}
55
56pub fn convert_errors(
57 spec_errors: &HashMap<String, Vec<Box<dyn Error>>>,
58) -> HashMap<String, Vec<Box<dyn Error + Send + Sync + 'static>>> {
59 spec_errors
60 .iter()
61 .map(|(key, value)| {
62 (
63 key.clone(),
64 value
65 .iter()
66 .map(|error| {
67 Box::new(SpecError::new(&error.to_string()))
68 as Box<dyn Error + Send + Sync + 'static>
69 })
70 .collect(),
71 )
72 })
73 .collect()
74}
75
76pub fn with_spec_dirs(dirs: &[&str]) -> CdiOption {
78 let cleaned_dirs: Vec<String> = dirs
79 .iter()
80 .map(|dir| {
81 clean(PathBuf::from(*dir))
82 .into_os_string()
83 .into_string()
84 .unwrap()
85 })
86 .collect();
87
88 Box::new(move |cache: &mut Cache| {
89 cache.spec_dirs.clone_from(&cleaned_dirs);
90 })
91}
92
93#[allow(dead_code)]
94fn traverse_dir<F>(dir_path: &Path, traverse_fn: &mut F) -> Result<(), Box<dyn Error>>
95where
96 F: FnMut(&Path) -> Result<(), Box<dyn Error>>,
97{
98 if let Ok(entries) = fs::read_dir(dir_path) {
99 for entry in entries.flatten() {
100 let path = entry.path();
101 if path.is_dir() {
102 traverse_dir(&path, traverse_fn)?;
103 } else {
104 traverse_fn(&path)?;
105 }
106 }
107 }
108 Ok(())
109}
110
111#[allow(dead_code)]
117pub(crate) fn scan_spec_dirs<P: AsRef<Path>>(dirs: &[P]) -> Result<Vec<Spec>, Box<dyn Error>> {
118 let mut scaned_specs = Vec::new();
119 for (priority, dir) in dirs.iter().enumerate() {
120 let dir_path = dir.as_ref();
121 if !dir_path.is_dir() {
122 continue;
123 }
124
125 let mut operation = |path: &Path| -> Result<(), Box<dyn Error>> {
126 if !path.is_dir() && is_cdi_spec(path) {
127 let spec = match read_spec(&path.to_path_buf(), priority as i32) {
128 Ok(spec) => spec,
129 Err(err) => {
130 return Err(Box::new(SpecError::new(&err.to_string())));
131 }
132 };
133 scaned_specs.push(spec);
134 }
135 Ok(())
136 };
137
138 if let Err(e) = traverse_dir(dir_path, &mut operation) {
139 return Err(Box::new(SpecError::new(&e.to_string())));
140 }
141 }
142
143 Ok(scaned_specs)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use std::collections::HashMap;
150 use std::fs;
151
152 const SPEC_YAML: &str = r#"cdiVersion: "0.6.0"
153kind: "vendor.com/device"
154devices:
155 - name: "gpu0"
156 containerEdits:
157 env:
158 - "VENDOR=1"
159"#;
160
161 const SPEC_JSON: &str = r#"{
162 "cdiVersion": "0.6.0",
163 "kind": "vendor2.com/device",
164 "devices": [
165 { "name": "d0", "containerEdits": { "env": ["OTHER=1"] } }
166 ]
167}"#;
168
169 #[test]
170 fn with_spec_dirs_cleans_paths() {
171 let mut cache = Cache::default();
172 with_spec_dirs(&["/etc/cdi/../cdi", "/var/run/cdi/"])(&mut cache);
173 assert_eq!(cache.spec_dirs, vec!["/etc/cdi", "/var/run/cdi"]);
174 }
175
176 #[test]
177 fn spec_error_display_and_conversion() {
178 let mut errors: HashMap<String, Vec<Box<dyn Error>>> = HashMap::new();
179 errors.insert(
180 "/etc/cdi/broken.yaml".to_string(),
181 vec![Box::new(SpecError::new("bad spec"))],
182 );
183
184 let converted = convert_errors(&errors);
185 let msgs = &converted["/etc/cdi/broken.yaml"];
186 assert_eq!(msgs.len(), 1);
187 assert!(msgs[0].to_string().contains("bad spec"));
188 }
189
190 #[test]
191 fn scan_finds_specs_recursively_with_dir_index_as_priority() {
192 let low = tempfile::tempdir().unwrap();
193 let high = tempfile::tempdir().unwrap();
194 fs::write(low.path().join("vendor.yaml"), SPEC_YAML).unwrap();
195 let nested = high.path().join("nested");
197 fs::create_dir(&nested).unwrap();
198 fs::write(nested.join("vendor2.json"), SPEC_JSON).unwrap();
199 fs::write(high.path().join("README.txt"), "not a spec").unwrap();
201
202 let specs = scan_spec_dirs(&[low.path(), high.path()]).unwrap();
203
204 assert_eq!(specs.len(), 2);
205 let by_vendor: HashMap<_, _> = specs
206 .iter()
207 .map(|s| (s.get_vendor().to_string(), s.get_priority()))
208 .collect();
209 assert_eq!(by_vendor["vendor.com"], 0);
210 assert_eq!(by_vendor["vendor2.com"], 1);
211 }
212
213 #[test]
214 fn scan_skips_missing_dirs() {
215 let specs = scan_spec_dirs(&["/nonexistent/cdi/dir"]).unwrap();
216 assert!(specs.is_empty());
217 }
218
219 #[test]
220 fn scan_fails_on_invalid_spec() {
221 let dir = tempfile::tempdir().unwrap();
222 fs::write(dir.path().join("broken.yaml"), "cdiVersion: [not a spec").unwrap();
223 assert!(scan_spec_dirs(&[dir.path()]).is_err());
224 }
225}