Skip to main content

use_oci_runtime/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6
7use use_oci_hook::OciHook;
8use use_oci_namespace::NamespaceKind;
9
10/// Errors returned when runtime metadata is invalid.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum RuntimeError {
13    Empty,
14    InvalidMount,
15    InvalidResource,
16}
17
18impl fmt::Display for RuntimeError {
19    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::Empty => formatter.write_str("OCI runtime value cannot be empty"),
22            Self::InvalidMount => formatter.write_str("invalid OCI mount metadata"),
23            Self::InvalidResource => formatter.write_str("invalid OCI resource metadata"),
24        }
25    }
26}
27
28impl Error for RuntimeError {}
29
30macro_rules! text_value {
31    ($name:ident) => {
32        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33        pub struct $name(String);
34
35        impl $name {
36            /// Creates a non-empty runtime text value.
37            pub fn new(value: impl AsRef<str>) -> Result<Self, RuntimeError> {
38                let trimmed = value.as_ref().trim();
39                if trimmed.is_empty() {
40                    return Err(RuntimeError::Empty);
41                }
42                if trimmed.contains('\0') {
43                    return Err(RuntimeError::InvalidMount);
44                }
45                Ok(Self(trimmed.to_string()))
46            }
47
48            /// Returns the text value.
49            #[must_use]
50            pub fn as_str(&self) -> &str {
51                &self.0
52            }
53        }
54
55        impl AsRef<str> for $name {
56            fn as_ref(&self) -> &str {
57                self.as_str()
58            }
59        }
60
61        impl fmt::Display for $name {
62            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63                formatter.write_str(self.as_str())
64            }
65        }
66    };
67}
68
69text_value!(ProcessArg);
70text_value!(RuntimeEnv);
71text_value!(Cwd);
72text_value!(Capability);
73text_value!(RootFilesystem);
74
75/// OCI mount kind metadata.
76#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
77pub enum MountKind {
78    Bind,
79    Tmpfs,
80    Proc,
81    Sysfs,
82    Cgroup,
83    Custom,
84}
85
86/// OCI mount metadata. This type does not mount filesystems.
87#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
88pub struct Mount {
89    kind: MountKind,
90    source: String,
91    destination: String,
92    options: Vec<String>,
93}
94
95impl Mount {
96    /// Creates mount metadata.
97    pub fn new(
98        kind: MountKind,
99        source: impl AsRef<str>,
100        destination: impl AsRef<str>,
101    ) -> Result<Self, RuntimeError> {
102        let source = non_empty(source.as_ref(), RuntimeError::InvalidMount)?;
103        let destination = non_empty(destination.as_ref(), RuntimeError::InvalidMount)?;
104        Ok(Self {
105            kind,
106            source: source.to_string(),
107            destination: destination.to_string(),
108            options: Vec::new(),
109        })
110    }
111
112    /// Adds a mount option.
113    #[must_use]
114    pub fn with_option(mut self, option: impl Into<String>) -> Self {
115        self.options.push(option.into());
116        self
117    }
118
119    /// Returns the mount kind.
120    #[must_use]
121    pub const fn kind(&self) -> MountKind {
122        self.kind
123    }
124
125    /// Returns the mount destination.
126    #[must_use]
127    pub fn destination(&self) -> &str {
128        &self.destination
129    }
130}
131
132/// Resource limit metadata.
133#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
134pub struct ResourceLimit {
135    name: String,
136    value: u64,
137}
138
139impl ResourceLimit {
140    /// Creates resource limit metadata.
141    pub fn new(name: impl AsRef<str>, value: u64) -> Result<Self, RuntimeError> {
142        let name = non_empty(name.as_ref(), RuntimeError::InvalidResource)?;
143        Ok(Self {
144            name: name.to_string(),
145            value,
146        })
147    }
148
149    /// Returns the resource name.
150    #[must_use]
151    pub fn name(&self) -> &str {
152        &self.name
153    }
154
155    /// Returns the resource value.
156    #[must_use]
157    pub const fn value(&self) -> u64 {
158        self.value
159    }
160}
161
162/// OCI runtime metadata. This type does not execute a runtime.
163#[derive(Clone, Debug, Eq, PartialEq)]
164pub struct RuntimeSpec {
165    root: RootFilesystem,
166    args: Vec<ProcessArg>,
167    env: Vec<RuntimeEnv>,
168    cwd: Option<Cwd>,
169    mounts: Vec<Mount>,
170    hooks: Vec<OciHook>,
171    namespaces: Vec<NamespaceKind>,
172    capabilities: Vec<Capability>,
173    resources: Vec<ResourceLimit>,
174}
175
176impl RuntimeSpec {
177    /// Creates runtime metadata from a root filesystem marker.
178    #[must_use]
179    pub fn new(root: RootFilesystem) -> Self {
180        Self {
181            root,
182            args: Vec::new(),
183            env: Vec::new(),
184            cwd: None,
185            mounts: Vec::new(),
186            hooks: Vec::new(),
187            namespaces: Vec::new(),
188            capabilities: Vec::new(),
189            resources: Vec::new(),
190        }
191    }
192
193    /// Adds a process argument.
194    #[must_use]
195    pub fn with_arg(mut self, arg: ProcessArg) -> Self {
196        self.args.push(arg);
197        self
198    }
199
200    /// Adds a mount.
201    #[must_use]
202    pub fn with_mount(mut self, mount: Mount) -> Self {
203        self.mounts.push(mount);
204        self
205    }
206
207    /// Adds a hook.
208    #[must_use]
209    pub fn with_hook(mut self, hook: OciHook) -> Self {
210        self.hooks.push(hook);
211        self
212    }
213
214    /// Adds a namespace kind.
215    #[must_use]
216    pub fn with_namespace(mut self, namespace: NamespaceKind) -> Self {
217        self.namespaces.push(namespace);
218        self
219    }
220
221    /// Returns the root filesystem marker.
222    #[must_use]
223    pub const fn root(&self) -> &RootFilesystem {
224        &self.root
225    }
226
227    /// Returns namespaces.
228    #[must_use]
229    pub fn namespaces(&self) -> &[NamespaceKind] {
230        &self.namespaces
231    }
232
233    /// Returns hooks.
234    #[must_use]
235    pub fn hooks(&self) -> &[OciHook] {
236        &self.hooks
237    }
238}
239
240fn non_empty(value: &str, error: RuntimeError) -> Result<&str, RuntimeError> {
241    let trimmed = value.trim();
242    if trimmed.is_empty() || trimmed.contains('\0') {
243        Err(error)
244    } else {
245        Ok(trimmed)
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::{Mount, MountKind, ProcessArg, RootFilesystem, RuntimeSpec};
252    use use_oci_hook::{HookKind, HookPath, OciHook};
253    use use_oci_namespace::NamespaceKind;
254
255    #[test]
256    fn models_runtime_metadata_without_execution() -> Result<(), Box<dyn std::error::Error>> {
257        let hook = OciHook::new(HookKind::Prestart, HookPath::new("/bin/check")?);
258        let spec = RuntimeSpec::new(RootFilesystem::new("rootfs")?)
259            .with_arg(ProcessArg::new("/bin/sh")?)
260            .with_mount(Mount::new(MountKind::Bind, "/host", "/container")?)
261            .with_namespace(NamespaceKind::Pid)
262            .with_hook(hook);
263
264        assert_eq!(spec.root().as_str(), "rootfs");
265        assert_eq!(spec.namespaces(), &[NamespaceKind::Pid]);
266        assert_eq!(spec.hooks().len(), 1);
267        Ok(())
268    }
269}