container_device_interface/
container_edits.rs

1use std::{collections::HashSet, str::FromStr};
2
3use anyhow::{anyhow, Context, Error, Result};
4use oci_spec::runtime::{self as oci, LinuxDeviceType};
5
6use crate::{
7    container_edits_unix::{device_info_from_path, DeviceType},
8    generate::config::Generator,
9    specs::config::{
10        ContainerEdits as CDIContainerEdits, DeviceNode as CDIDeviceNode, Hook as CDIHook,
11        IntelRdt as CDIIntelRdt, Mount as CDIMount,
12    },
13    utils::merge,
14};
15
16pub trait Validate {
17    fn validate(&self) -> Result<()>;
18}
19
20fn validate_envs(envs: &[String]) -> Result<()> {
21    if envs.iter().any(|v| !v.contains('=')) {
22        return Err(anyhow!("invalid environment variable: {:?}", envs));
23    }
24
25    Ok(())
26}
27
28// ContainerEdits represent updates to be applied to an OCI Spec.
29// These updates can be specific to a CDI device, or they can be
30// specific to a CDI Spec. In the former case these edits should
31// be applied to all OCI Specs where the corresponding CDI device
32// is injected. In the latter case, these edits should be applied
33// to all OCI Specs where at least one devices from the CDI Spec
34// is injected.
35#[derive(Clone, Default, Debug, Eq, PartialEq, Hash)]
36pub struct ContainerEdits {
37    pub container_edits: CDIContainerEdits,
38}
39
40impl ContainerEdits {
41    pub fn new() -> Self {
42        Self {
43            container_edits: CDIContainerEdits {
44                ..Default::default()
45            },
46        }
47    }
48
49    // apply edits to the given OCI Spec. Updates the OCI Spec in place.
50    // Returns an error if the update fails.
51    pub fn apply(&mut self, oci_spec: &mut oci::Spec) -> Result<()> {
52        let mut spec_gen: Generator = Generator::spec_gen(Some(oci_spec.clone()));
53
54        if let Some(envs) = &self.container_edits.env {
55            if !envs.is_empty() {
56                spec_gen.add_multiple_process_env(envs);
57            }
58        }
59
60        if let Some(device_nodes) = &self.container_edits.device_nodes {
61            for d in device_nodes {
62                let mut dn: DeviceNode = DeviceNode { node: d.clone() };
63
64                dn.fill_missing_info()
65                    .context("filling missing info failed.")?;
66
67                let d = &dn.node;
68                let mut dev = dn.node.to_oci()?;
69                if let Some(process) = oci_spec.process_mut() {
70                    let user = process.user_mut();
71                    let gid = user.gid();
72                    if gid > 0 {
73                        dev.set_gid(Some(gid));
74                    }
75
76                    let uid = user.uid();
77                    if uid > 0 {
78                        dev.set_gid(Some(uid));
79                    }
80                }
81
82                let dev_typ = dev.typ();
83                let typs = [LinuxDeviceType::B, LinuxDeviceType::C];
84                if typs.contains(&dev_typ) {
85                    let perms = "rwm".to_owned();
86                    let dev_access = if let Some(permissions) = &d.permissions {
87                        permissions
88                    } else {
89                        &perms
90                    };
91
92                    let major = dev.major();
93                    let minor = dev.minor();
94                    spec_gen.add_linux_resources_device(
95                        true,
96                        dev_typ,
97                        Some(major),
98                        Some(minor),
99                        Some(dev_access.clone()),
100                    );
101                }
102
103                spec_gen.remove_device(&dev.path().display().to_string());
104                spec_gen.add_device(dev.clone());
105            }
106        }
107
108        if let Some(mounts) = &self.container_edits.mounts {
109            for m in mounts {
110                spec_gen.remove_mount(&m.container_path);
111                spec_gen.add_mount(m.to_oci()?);
112            }
113        }
114
115        if let Some(hooks) = &self.container_edits.hooks {
116            for h in hooks {
117                let hook_name = HookName::from_str(&h.hook_name)
118                    .context(format!("no such hook with name: {:?}", &h.hook_name))?;
119                match hook_name {
120                    HookName::Prestart => spec_gen.add_prestart_hook(h.to_oci()?),
121                    HookName::CreateRuntime => spec_gen.add_createruntime_hook(h.to_oci()?),
122                    HookName::CreateContainer => spec_gen.add_createcontainer_hook(h.to_oci()?),
123                    HookName::StartContainer => spec_gen.add_startcontainer_hook(h.to_oci()?),
124                    HookName::Poststart => spec_gen.add_poststart_hook(h.to_oci()?),
125                    HookName::Poststop => spec_gen.add_poststop_hook(h.to_oci()?),
126                }
127            }
128        }
129
130        if let Some(intel_rdt) = &self.container_edits.intel_rdt {
131            if let Some(clos_id) = &intel_rdt.clos_id {
132                spec_gen.set_linux_intel_rdt_clos_id(clos_id.to_string());
133                // TODO: spec.Linux.IntelRdt = e.IntelRdt.ToOCI()
134            }
135        }
136
137        if let Some(additional_gids) = &self.container_edits.additional_gids {
138            for gid in additional_gids {
139                if *gid > 0 {
140                    spec_gen.add_process_additional_gid(*gid);
141                }
142            }
143        }
144
145        if let Some(ref spec) = spec_gen.config {
146            oci_spec.set_linux(spec.linux().clone());
147            oci_spec.set_mounts(spec.mounts().clone());
148            oci_spec.set_annotations(spec.annotations().clone());
149            oci_spec.set_hooks(spec.hooks().clone());
150            oci_spec.set_process(spec.process().clone());
151        }
152
153        Ok(())
154    }
155
156    // append other edits into this one.
157    pub fn append(&mut self, o: ContainerEdits) -> Result<()> {
158        let intel_rdt = if o.container_edits.intel_rdt.is_some() {
159            o.container_edits.intel_rdt
160        } else {
161            None
162        };
163
164        let ce = CDIContainerEdits {
165            env: merge(&mut self.container_edits.env, &o.container_edits.env),
166            device_nodes: merge(
167                &mut self.container_edits.device_nodes,
168                &o.container_edits.device_nodes,
169            ),
170            hooks: merge(&mut self.container_edits.hooks, &o.container_edits.hooks),
171            mounts: merge(&mut self.container_edits.mounts, &o.container_edits.mounts),
172            intel_rdt,
173            additional_gids: merge(
174                &mut self.container_edits.additional_gids,
175                &o.container_edits.additional_gids,
176            ),
177        };
178
179        self.container_edits = ce;
180
181        Ok(())
182    }
183}
184
185// Validate container edits.
186impl Validate for ContainerEdits {
187    fn validate(&self) -> Result<()> {
188        if let Some(envs) = &self.container_edits.env {
189            validate_envs(envs)
190                .context(format!("invalid container edits with envs: {:?}", envs))?;
191        }
192        if let Some(devices) = &self.container_edits.device_nodes {
193            for d in devices {
194                let dn = DeviceNode { node: d.clone() };
195                dn.validate()
196                    .context(format!("invalid container edits with device: {:?}", &d))?;
197            }
198        }
199        if let Some(hooks) = &self.container_edits.hooks {
200            for h in hooks {
201                let hook = Hook { hook: h.clone() };
202                hook.validate()
203                    .context(format!("invalid container edits with hook: {:?}", &h))?;
204            }
205        }
206        if let Some(mounts) = &self.container_edits.mounts {
207            for m in mounts {
208                let mnt = Mount { mount: m.clone() };
209                mnt.validate()
210                    .context(format!("invalid container edits with mount: {:?}", &m))?;
211            }
212        }
213        if let Some(irdt) = &self.container_edits.intel_rdt {
214            let i_rdt = IntelRdt {
215                intel_rdt: irdt.clone(),
216            };
217            i_rdt
218                .validate()
219                .context(format!("invalid container edits with mount: {:?}", irdt))?;
220        }
221
222        Ok(())
223    }
224}
225
226// DeviceNode is a CDI Spec DeviceNode wrapper, used for validating DeviceNodes.
227pub struct DeviceNode {
228    pub node: CDIDeviceNode,
229}
230
231impl DeviceNode {
232    pub fn fill_missing_info(&mut self) -> Result<()> {
233        let host_path = self
234            .node
235            .host_path
236            .as_deref()
237            .unwrap_or_else(|| &self.node.path);
238
239        if let Some(device_type) = self.node.r#type.as_deref() {
240            if self.node.major.is_some() || device_type == DeviceType::Fifo.to_string() {
241                return Ok(());
242            }
243        }
244
245        let (dev_type, major, minor) = device_info_from_path(host_path)?;
246        match self.node.r#type.as_deref() {
247            None => self.node.r#type = Some(dev_type),
248            Some(node_type) if node_type != dev_type => {
249                return Err(anyhow!(
250                    "CDI device ({}, {}), host type mismatch ({}, {})",
251                    self.node.path,
252                    host_path,
253                    node_type,
254                    dev_type
255                ));
256            }
257            _ => {}
258        }
259
260        if self.node.major.is_none()
261            && self.node.r#type.as_deref() != Some(&DeviceType::Fifo.to_string())
262        {
263            self.node.major = Some(major);
264            self.node.minor = Some(minor);
265        }
266
267        Ok(())
268    }
269}
270
271impl Validate for DeviceNode {
272    fn validate(&self) -> Result<()> {
273        let typs = vec!["b", "c", "u", "p", ""];
274        let valid_typs: HashSet<&str> = typs.into_iter().collect();
275
276        if self.node.path.is_empty() {
277            return Err(anyhow!("invalid (empty) device path"));
278        }
279
280        if let Some(typ) = &self.node.r#type {
281            if valid_typs.contains(&typ.as_str()) {
282                return Err(anyhow!(
283                    "device {:?}: invalid type {:?}",
284                    self.node.path,
285                    typ
286                ));
287            }
288        }
289
290        if let Some(perms) = &self.node.permissions {
291            if !perms.chars().all(|c| matches!(c, 'r' | 'w' | 'm')) {
292                return Err(anyhow!(
293                    "device {}: invalid permissions {}",
294                    self.node.path,
295                    perms
296                ));
297            }
298        }
299
300        Ok(())
301    }
302}
303
304#[derive(Debug, PartialEq, Eq, Hash)]
305enum HookName {
306    Prestart,
307    CreateRuntime,
308    CreateContainer,
309    StartContainer,
310    Poststart,
311    Poststop,
312}
313
314impl FromStr for HookName {
315    type Err = Error;
316    fn from_str(s: &str) -> Result<Self, Self::Err> {
317        match s {
318            "prestart" => Ok(Self::Prestart),
319            "createRuntime" => Ok(Self::CreateRuntime),
320            "createContainer" => Ok(Self::CreateContainer),
321            "startContainer" => Ok(Self::StartContainer),
322            "poststart" => Ok(Self::Poststart),
323            "poststop" => Ok(Self::Poststop),
324            _ => Err(anyhow!("no such hook")),
325        }
326    }
327}
328
329struct Hook {
330    hook: CDIHook,
331}
332
333impl Validate for Hook {
334    fn validate(&self) -> Result<()> {
335        HookName::from_str(&self.hook.hook_name)
336            .context(anyhow!("invalid hook name: {:?}", self.hook.hook_name))?;
337
338        if self.hook.path.is_empty() {
339            return Err(anyhow!(
340                "invalid hook {:?} with empty path",
341                self.hook.hook_name
342            ));
343        }
344        if let Some(envs) = &self.hook.env {
345            validate_envs(envs)
346                .context(anyhow!("hook {:?} with invalid env", &self.hook.hook_name))?;
347        }
348
349        Ok(())
350    }
351}
352
353struct Mount {
354    mount: CDIMount,
355}
356
357impl Validate for Mount {
358    fn validate(&self) -> Result<()> {
359        if self.mount.host_path.is_empty() {
360            return Err(anyhow!("invalid mount, empty host path"));
361        }
362
363        if self.mount.container_path.is_empty() {
364            return Err(anyhow!("invalid mount, empty container path"));
365        }
366
367        Ok(())
368    }
369}
370
371struct IntelRdt {
372    intel_rdt: CDIIntelRdt,
373}
374
375impl Validate for IntelRdt {
376    fn validate(&self) -> Result<()> {
377        if let Some(ref clos_id) = self.intel_rdt.clos_id {
378            if clos_id.len() >= 4096
379                || clos_id == "."
380                || clos_id == ".."
381                || clos_id.contains(&['/', '\n'][..])
382            {
383                return Err(anyhow!("invalid clos id".to_string()));
384            }
385        }
386
387        Ok(())
388    }
389}