Skip to main content

container_device_interface/generate/
generator.rs

1use std::cmp::Ordering;
2use std::path::PathBuf;
3
4use oci_spec::runtime::{
5    Hook, LinuxDevice, LinuxDeviceCgroup, LinuxDeviceType, LinuxIntelRdt, LinuxNetDevice, Mount,
6};
7
8use super::config::Generator;
9
10impl Generator {
11    // remove_device removes a device from g.config.linux.devices
12    pub fn remove_device(&mut self, path: &str) {
13        self.init_config_linux();
14        if let Some(linux) = self.config.as_mut().unwrap().linux_mut() {
15            if let Some(devices) = linux.devices_mut() {
16                if let Some(index) = devices
17                    .iter()
18                    .position(|device| device.path() == &PathBuf::from(path))
19                {
20                    devices.remove(index);
21                }
22            }
23        }
24    }
25
26    // add_device adds a device into g.config.linux.devices
27    pub fn add_device(&mut self, device: LinuxDevice) {
28        self.init_config_linux();
29
30        if let Some(linux) = self.config.as_mut().unwrap().linux_mut() {
31            if let Some(devices) = linux.devices_mut() {
32                if let Some(index) = devices.iter().position(|dev| dev.path() == device.path()) {
33                    devices[index] = device;
34                } else {
35                    devices.push(device);
36                }
37            } else {
38                linux.set_devices(Some(vec![device]));
39            }
40        }
41    }
42
43    // add_linux_resources_device adds a device into g.config.linux.resources.devices
44    pub fn add_linux_resources_device(
45        &mut self,
46        allow: bool,
47        dev_type: LinuxDeviceType,
48        major: Option<i64>,
49        minor: Option<i64>,
50        access: Option<String>,
51    ) {
52        self.init_config_linux_resources_devices();
53        if let Some(linux) = self.config.as_mut().unwrap().linux_mut() {
54            if let Some(resource) = linux.resources_mut() {
55                if let Some(devices) = resource.devices_mut() {
56                    let mut device = LinuxDeviceCgroup::default();
57                    device.set_allow(allow);
58                    device.set_typ(Some(dev_type));
59                    device.set_major(major);
60                    device.set_minor(minor);
61                    device.set_access(access);
62
63                    devices.push(device);
64                }
65            }
66        }
67    }
68
69    pub fn add_linux_net_device(
70        &mut self,
71        host_interface_name: String,
72        net_device: LinuxNetDevice,
73    ) {
74        self.init_config_linux_net_devices();
75        if let Some(linux) = self.config.as_mut().unwrap().linux_mut() {
76            if let Some(net_devices) = linux.net_devices_mut() {
77                net_devices.insert(host_interface_name, net_device);
78            }
79        }
80    }
81
82    pub fn set_linux_intel_rdt(&mut self, intel_rdt: LinuxIntelRdt) {
83        self.init_config_linux();
84        if let Some(linux) = self.config.as_mut().unwrap().linux_mut() {
85            linux.set_intel_rdt(Some(intel_rdt));
86        }
87    }
88
89    /// set Linux Intel RDT ClosID
90    pub fn set_linux_intel_rdt_clos_id(&mut self, clos_id: String) {
91        self.init_config_linux_intel_rdt();
92        if let Some(linux) = self.config.as_mut().unwrap().linux_mut() {
93            if let Some(intel_rdt) = linux.intel_rdt_mut() {
94                intel_rdt.set_clos_id(Some(clos_id));
95            }
96        }
97    }
98
99    // add_process_additional_gid adds an additional gid into g.config.process.additional_gids.
100    pub fn add_process_additional_gid(&mut self, gid: u32) {
101        self.init_config_process();
102        if let Some(process) = self.config.as_mut().unwrap().process_mut() {
103            let mut gids = process.user().additional_gids().clone().unwrap_or_default();
104            if !gids.contains(&gid) {
105                gids.push(gid);
106            }
107            process.user_mut().set_additional_gids(Some(gids));
108        }
109    }
110
111    pub fn add_multiple_process_env(&mut self, envs: &[String]) {
112        self.init_config_process();
113
114        if let Some(process) = self.config.as_mut().unwrap().process_mut() {
115            let mut env_vec: Vec<String> = process.env_mut().get_or_insert_with(Vec::new).to_vec();
116            for env in envs {
117                let split: Vec<&str> = env.splitn(2, '=').collect();
118                let key = split[0].to_string();
119                let idx = self.env_map.entry(key.clone()).or_insert(env_vec.len());
120
121                if let Some(elem) = env_vec.get_mut(*idx) {
122                    elem.clone_from(env);
123                } else {
124                    env_vec.push(env.clone());
125                    self.env_map.insert(key, env_vec.len() - 1);
126                }
127            }
128            process.set_env(Some(env_vec));
129        }
130    }
131
132    // add_prestart_hook adds a prestart hook into g.config.hooks.prestart.
133    pub fn add_prestart_hook(&mut self, hook: Hook) {
134        self.init_config_hooks();
135        if let Some(hooks) = self.config.as_mut().unwrap().hooks_mut() {
136            if let Some(prestart_hooks) = hooks.prestart_mut() {
137                prestart_hooks.push(hook);
138            } else {
139                hooks.set_prestart(Some(vec![hook]));
140            }
141        }
142    }
143
144    // add_poststop_hook adds a poststop hook into g.config.hooks.poststop.
145    pub fn add_poststop_hook(&mut self, hook: Hook) {
146        self.init_config_hooks();
147        if let Some(hooks) = self.config.as_mut().unwrap().hooks_mut() {
148            if let Some(poststop_hooks) = hooks.poststop_mut() {
149                poststop_hooks.push(hook);
150            } else {
151                hooks.set_poststop(Some(vec![hook]));
152            }
153        }
154    }
155
156    // add_poststart_hook adds a poststart hook into g.config.hooks.poststart.
157    pub fn add_poststart_hook(&mut self, hook: Hook) {
158        self.init_config_hooks();
159        if let Some(hooks) = self.config.as_mut().unwrap().hooks_mut() {
160            if let Some(poststart_hooks) = hooks.poststart_mut() {
161                poststart_hooks.push(hook);
162            } else {
163                hooks.set_poststart(Some(vec![hook]));
164            }
165        }
166    }
167
168    // add_createruntime_hook adds a create_runtime hook into g.config.hooks.create_runtime.
169    pub fn add_createruntime_hook(&mut self, hook: Hook) {
170        self.init_config_hooks();
171        if let Some(hooks) = self.config.as_mut().unwrap().hooks_mut() {
172            if let Some(create_runtime) = hooks.create_runtime_mut() {
173                create_runtime.push(hook);
174            } else {
175                hooks.set_create_runtime(Some(vec![hook]));
176            }
177        }
178    }
179
180    // add_createcontainer_hook adds a create_container hook into g.config.hooks.create_container.
181    pub fn add_createcontainer_hook(&mut self, hook: Hook) {
182        self.init_config_hooks();
183        if let Some(hooks) = self.config.as_mut().unwrap().hooks_mut() {
184            if let Some(create_container) = hooks.create_container_mut() {
185                create_container.push(hook);
186            } else {
187                hooks.set_create_container(Some(vec![hook]));
188            }
189        }
190    }
191
192    // add_start_container_hook adds a start container hook into g.config.hooks.start_container.
193    pub fn add_startcontainer_hook(&mut self, hook: Hook) {
194        self.init_config_hooks();
195        if let Some(hooks) = self.config.as_mut().unwrap().hooks_mut() {
196            if let Some(start_container) = hooks.start_container_mut() {
197                start_container.push(hook);
198            } else {
199                hooks.set_start_container(Some(vec![hook]));
200            }
201        }
202    }
203
204    // remove_mount removes a mount point on the dest directory
205    pub fn remove_mount(&mut self, dest: &str) {
206        if let Some(mounts) = self.config.as_mut().unwrap().mounts_mut() {
207            if let Some(index) = mounts
208                .iter()
209                .position(|m| m.destination() == &PathBuf::from(dest))
210            {
211                mounts.remove(index);
212            }
213        }
214    }
215
216    // add_mount adds a mount into g.config.mounts.
217    pub fn add_mount(&mut self, mount: Mount) {
218        self.init_config_mounts();
219
220        if let Some(mounts) = self.config.as_mut().unwrap().mounts_mut() {
221            mounts.push(mount);
222        }
223    }
224
225    // sort_mounts sorts the mounts in the given OCI Spec.
226    pub fn sort_mounts(&mut self) {
227        if let Some(ref mut mounts) = self.config.as_mut().unwrap().mounts_mut() {
228            mounts.sort_by(|a, b| a.destination().cmp(b.destination()));
229        }
230    }
231
232    // list_mounts returns the list of mounts
233    pub fn list_mounts(&self) -> Option<&Vec<Mount>> {
234        self.config.as_ref().and_then(|spec| spec.mounts().as_ref())
235    }
236
237    // clear_mounts clear g.Config.Mounts
238    pub fn clear_mounts(&mut self) {
239        if let Some(spec) = self.config.as_mut() {
240            spec.set_mounts(None);
241        }
242    }
243}
244
245// OrderedMounts defines how to sort an OCI Spec Mount slice.
246// This is the almost the same implementation sa used by CRI-O and Docker,
247// with a minor tweak for stable sorting order (easier to test):
248//
249//	https://github.com/moby/moby/blob/17.05.x/daemon/volumes.go#L26
250struct OrderedMounts(Vec<Mount>);
251
252#[allow(dead_code)]
253impl OrderedMounts {
254    fn new(mounts: Vec<Mount>) -> Self {
255        OrderedMounts(mounts)
256    }
257
258    // parts returns the number of parts in the destination of a mount. Used in sorting.
259    fn parts(&self, i: usize) -> usize {
260        self.0[i].destination().components().count()
261    }
262}
263
264impl Ord for OrderedMounts {
265    fn cmp(&self, other: &Self) -> Ordering {
266        let self_parts = self.parts(0);
267        let other_parts = other.parts(0);
268        self_parts
269            .cmp(&other_parts)
270            .then_with(|| self.0[0].destination().cmp(other.0[0].destination()))
271    }
272}
273
274impl PartialOrd for OrderedMounts {
275    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
276        Some(self.cmp(other))
277    }
278}
279
280impl PartialEq for OrderedMounts {
281    fn eq(&self, other: &Self) -> bool {
282        self.parts(0) == other.parts(0) && self.0[0].destination() == other.0[0].destination()
283    }
284}
285
286impl Eq for OrderedMounts {}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use oci_spec::runtime::{LinuxNetDevice, Spec};
292
293    fn gen() -> Generator {
294        Generator::spec_gen(Some(Spec::default()))
295    }
296
297    #[test]
298    fn add_device_inserts_replaces_and_remove_deletes() {
299        let mut g = gen();
300        let mut dev = LinuxDevice::default();
301        dev.set_path(PathBuf::from("/dev/x"));
302        g.add_device(dev.clone());
303
304        // same path replaces instead of duplicating
305        let mut replacement = LinuxDevice::default();
306        replacement.set_path(PathBuf::from("/dev/x"));
307        replacement.set_major(7);
308        g.add_device(replacement);
309
310        let devices = |g: &Generator| {
311            g.config
312                .as_ref()
313                .unwrap()
314                .linux()
315                .as_ref()
316                .unwrap()
317                .devices()
318                .clone()
319                .unwrap_or_default()
320        };
321        assert_eq!(devices(&g).len(), 1);
322        assert_eq!(devices(&g)[0].major(), 7);
323
324        g.remove_device("/dev/x");
325        assert!(devices(&g).is_empty());
326        // removing an absent device is a no-op
327        g.remove_device("/dev/x");
328    }
329
330    #[test]
331    fn add_linux_resources_device_records_cgroup_entry() {
332        let mut g = gen();
333        g.add_linux_resources_device(
334            true,
335            LinuxDeviceType::C,
336            Some(1),
337            Some(3),
338            Some("rwm".to_string()),
339        );
340
341        let binding = g.config.unwrap();
342        let devices = binding
343            .linux()
344            .as_ref()
345            .unwrap()
346            .resources()
347            .as_ref()
348            .unwrap()
349            .devices()
350            .as_ref()
351            .unwrap();
352        assert_eq!(devices.len(), 1);
353        assert!(devices[0].allow());
354        assert_eq!(devices[0].typ(), Some(LinuxDeviceType::C));
355        assert_eq!(devices[0].major(), Some(1));
356        assert_eq!(devices[0].minor(), Some(3));
357        assert_eq!(devices[0].access().as_deref(), Some("rwm"));
358    }
359
360    #[test]
361    fn intel_rdt_set_and_clos_id_update() {
362        let mut g = gen();
363        let mut rdt = LinuxIntelRdt::default();
364        rdt.set_clos_id(Some("initial".to_string()));
365        g.set_linux_intel_rdt(rdt);
366        g.set_linux_intel_rdt_clos_id("updated".to_string());
367
368        let binding = g.config.unwrap();
369        let rdt = binding
370            .linux()
371            .as_ref()
372            .unwrap()
373            .intel_rdt()
374            .as_ref()
375            .unwrap();
376        assert_eq!(rdt.clos_id().as_deref(), Some("updated"));
377    }
378
379    #[test]
380    fn additional_gids_deduplicate() {
381        let mut g = gen();
382        g.add_process_additional_gid(1000);
383        g.add_process_additional_gid(1000);
384        g.add_process_additional_gid(2000);
385
386        let binding = g.config.unwrap();
387        let gids = binding
388            .process()
389            .as_ref()
390            .unwrap()
391            .user()
392            .additional_gids()
393            .clone()
394            .unwrap();
395        assert_eq!(gids, vec![1000, 2000]);
396    }
397
398    #[test]
399    fn env_updates_existing_keys_and_appends_new_ones() {
400        let mut g = gen();
401        g.add_multiple_process_env(&["A=1".to_string(), "B=2".to_string()]);
402        g.add_multiple_process_env(&["A=3".to_string(), "C=4".to_string()]);
403
404        let binding = g.config.unwrap();
405        let env = binding.process().as_ref().unwrap().env().clone().unwrap();
406        // Spec::default() seeds PATH/TERM; assert semantics, not the seed.
407        assert_eq!(env.iter().filter(|e| e.starts_with("A=")).count(), 1);
408        let tail = &env[env.len() - 3..];
409        assert_eq!(tail, ["A=3", "B=2", "C=4"]);
410    }
411
412    #[test]
413    fn hooks_initialize_then_append() {
414        let mut g = gen();
415        for _ in 0..2 {
416            g.add_prestart_hook(Hook::default());
417            g.add_poststart_hook(Hook::default());
418            g.add_poststop_hook(Hook::default());
419            g.add_createruntime_hook(Hook::default());
420            g.add_createcontainer_hook(Hook::default());
421            g.add_startcontainer_hook(Hook::default());
422        }
423
424        let binding = g.config.unwrap();
425        let hooks = binding.hooks().as_ref().unwrap();
426        assert_eq!(hooks.prestart().as_ref().unwrap().len(), 2);
427        assert_eq!(hooks.poststart().as_ref().unwrap().len(), 2);
428        assert_eq!(hooks.poststop().as_ref().unwrap().len(), 2);
429        assert_eq!(hooks.create_runtime().as_ref().unwrap().len(), 2);
430        assert_eq!(hooks.create_container().as_ref().unwrap().len(), 2);
431        assert_eq!(hooks.start_container().as_ref().unwrap().len(), 2);
432    }
433
434    #[test]
435    fn mounts_add_sort_list_remove_clear() {
436        let mut g = gen();
437        let mount = |dest: &str| {
438            let mut m = Mount::default();
439            m.set_destination(PathBuf::from(dest));
440            m
441        };
442        g.add_mount(mount("/z"));
443        g.add_mount(mount("/a"));
444
445        g.sort_mounts();
446        let dests: Vec<_> = g
447            .list_mounts()
448            .unwrap()
449            .iter()
450            .map(|m| m.destination().clone())
451            .collect();
452        assert!(dests.windows(2).all(|w| w[0] <= w[1]));
453
454        g.remove_mount("/z");
455        assert!(!g
456            .list_mounts()
457            .unwrap()
458            .iter()
459            .any(|m| m.destination() == &PathBuf::from("/z")));
460
461        g.clear_mounts();
462        assert!(g.list_mounts().is_none());
463    }
464
465    #[test]
466    fn add_process_additional_gid_initializes_empty_gid_list() {
467        let mut generator = Generator::spec_gen(Some(oci_spec::runtime::Spec::default()));
468
469        generator.add_process_additional_gid(1000);
470
471        let gids = generator
472            .config
473            .as_ref()
474            .unwrap()
475            .process()
476            .as_ref()
477            .unwrap()
478            .user()
479            .additional_gids()
480            .as_ref()
481            .unwrap();
482        assert_eq!(gids, &vec![1000]);
483    }
484
485    #[test]
486    fn add_linux_net_device_initializes_map_and_sets_entry() {
487        let mut generator = Generator::spec_gen(Some(oci_spec::runtime::Spec::default()));
488        let mut net_device = LinuxNetDevice::default();
489        net_device.set_name(Some("container_eth0".to_string()));
490
491        generator.add_linux_net_device("eth0".to_string(), net_device);
492
493        let net_devices = generator
494            .config
495            .as_ref()
496            .unwrap()
497            .linux()
498            .as_ref()
499            .unwrap()
500            .net_devices()
501            .as_ref()
502            .unwrap();
503        assert_eq!(
504            net_devices.get("eth0").unwrap().name().as_ref().unwrap(),
505            "container_eth0"
506        );
507    }
508
509    // An all-None spec (unlike Spec::default(), which comes pre-populated)
510    // drives the create-the-layer arms of every add_/init_ helper.
511    fn empty_gen() -> Generator {
512        let mut bare = Spec::default();
513        bare.set_process(None)
514            .set_linux(None)
515            .set_hooks(None)
516            .set_mounts(None);
517        Generator::spec_gen(Some(bare))
518    }
519
520    #[test]
521    fn helpers_create_missing_layers_on_empty_specs() {
522        let mut g = empty_gen();
523        let mut dev = LinuxDevice::default();
524        dev.set_path(PathBuf::from("/dev/x"));
525        g.remove_device("/dev/x"); // no devices list yet: no-op arm
526        g.add_device(dev);
527        g.add_linux_resources_device(true, LinuxDeviceType::C, Some(1), Some(3), None);
528        let mut net_device = LinuxNetDevice::default();
529        net_device.set_name(Some("c".to_string()));
530        g.add_linux_net_device("eth0".to_string(), net_device);
531        g.set_linux_intel_rdt_clos_id("clos".to_string());
532        g.add_process_additional_gid(9);
533        g.add_multiple_process_env(&["K=V".to_string()]);
534        g.add_prestart_hook(Hook::default());
535        g.add_poststart_hook(Hook::default());
536        g.add_poststop_hook(Hook::default());
537        g.add_createruntime_hook(Hook::default());
538        g.add_createcontainer_hook(Hook::default());
539        g.add_startcontainer_hook(Hook::default());
540        g.add_mount(Mount::default());
541        g.sort_mounts();
542        g.clear_mounts();
543        assert!(g.list_mounts().is_none());
544
545        let binding = g.config.unwrap();
546        assert_eq!(
547            binding
548                .linux()
549                .as_ref()
550                .unwrap()
551                .devices()
552                .as_ref()
553                .unwrap()
554                .len(),
555            1
556        );
557        assert!(binding.hooks().is_some());
558    }
559
560    #[test]
561    fn ordered_mounts_compare_by_depth_then_path() {
562        let mount = |dest: &str| {
563            let mut m = Mount::default();
564            m.set_destination(PathBuf::from(dest));
565            m
566        };
567        let shallow = OrderedMounts::new(vec![mount("/a")]);
568        let deep = OrderedMounts::new(vec![mount("/a/b")]);
569        let deep_b = OrderedMounts::new(vec![mount("/a/c")]);
570
571        assert!(shallow < deep);
572        assert!(deep < deep_b);
573        assert_eq!(deep.partial_cmp(&deep_b), Some(Ordering::Less));
574        assert_eq!(shallow.parts(0), 2);
575        assert!(shallow == OrderedMounts::new(vec![mount("/a")]));
576    }
577}