oci_spec/runtime/
mod.rs

1//! [OCI runtime spec](https://github.com/opencontainers/runtime-spec) types and definitions.
2//!
3//! [`Spec`] represents the root object from the specification.
4
5use derive_builder::Builder;
6use getset::{Getters, MutGetters, Setters};
7use serde::{Deserialize, Serialize};
8use std::{
9    collections::HashMap,
10    fs,
11    io::{BufReader, BufWriter, Write},
12    path::{Path, PathBuf},
13};
14
15use crate::error::{oci_error, OciSpecError, Result};
16
17mod capability;
18mod features;
19mod hooks;
20mod linux;
21mod miscellaneous;
22mod process;
23mod solaris;
24mod state;
25mod test;
26mod version;
27mod vm;
28mod windows;
29mod zos;
30
31// re-export for ease of use
32pub use capability::*;
33pub use features::*;
34pub use hooks::*;
35pub use linux::*;
36pub use miscellaneous::*;
37pub use process::*;
38pub use solaris::*;
39pub use state::*;
40pub use version::*;
41pub use vm::*;
42pub use windows::*;
43pub use zos::*;
44
45/// `config.json` file root object.
46#[derive(
47    Builder, Clone, Debug, Deserialize, Getters, MutGetters, Setters, PartialEq, Eq, Serialize,
48)]
49#[serde(rename_all = "camelCase")]
50#[builder(
51    default,
52    pattern = "owned",
53    setter(into, strip_option),
54    build_fn(error = "OciSpecError")
55)]
56#[getset(get_mut = "pub", get = "pub", set = "pub")]
57pub struct Spec {
58    #[serde(default, rename = "ociVersion")]
59    ///  MUST be in SemVer v2.0.0 format and specifies the version of the
60    /// Open Container Initiative  Runtime Specification with which
61    /// the bundle complies. The Open Container Initiative
62    ///  Runtime Specification follows semantic versioning and retains
63    /// forward and backward  compatibility within major versions.
64    /// For example, if a configuration is compliant with
65    ///  version 1.1 of this specification, it is compatible with all
66    /// runtimes that support any 1.1  or later release of this
67    /// specification, but is not compatible with a runtime that supports
68    ///  1.0 and not 1.1.
69    version: String,
70
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    /// Specifies the container's root filesystem. On Windows, for Windows
73    /// Server Containers, this field is REQUIRED. For Hyper-V
74    /// Containers, this field MUST NOT be set.
75    ///
76    /// On all other platforms, this field is REQUIRED.
77    root: Option<Root>,
78
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    /// Specifies additional mounts beyond `root`. The runtime MUST mount
81    /// entries in the listed order.
82    ///
83    /// For Linux, the parameters are as documented in
84    /// [`mount(2)`](http://man7.org/linux/man-pages/man2/mount.2.html) system call man page. For
85    /// Solaris, the mount entry corresponds to the 'fs' resource in the
86    /// [`zonecfg(1M)`](http://docs.oracle.com/cd/E86824_01/html/E54764/zonecfg-1m.html) man page.
87    mounts: Option<Vec<Mount>>,
88
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    /// Specifies the container process. This property is REQUIRED when
91    /// [`start`](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md#start) is
92    /// called.
93    process: Option<Process>,
94
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    /// Specifies the container's hostname as seen by processes running
97    /// inside the container. On Linux, for example, this will
98    /// change the hostname in the container [UTS namespace](http://man7.org/linux/man-pages/man7/namespaces.7.html). Depending on your
99    /// [namespace
100    /// configuration](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#namespaces),
101    /// the container UTS namespace may be the runtime UTS namespace.
102    hostname: Option<String>,
103
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    /// Specifies the container's domainame as seen by processes running
106    /// inside the container. On Linux, for example, this will
107    /// change the domainame in the container [UTS namespace](http://man7.org/linux/man-pages/man7/namespaces.7.html). Depending on your
108    /// [namespace
109    /// configuration](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#namespaces),
110    /// the container UTS namespace may be the runtime UTS namespace.
111    domainname: Option<String>,
112
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    /// Hooks allow users to specify programs to run before or after various
115    /// lifecycle events. Hooks MUST be called in the listed order.
116    /// The state of the container MUST be passed to hooks over
117    /// stdin so that they may do work appropriate to the current state of
118    /// the container.
119    hooks: Option<Hooks>,
120
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    /// Annotations contains arbitrary metadata for the container. This
123    /// information MAY be structured or unstructured. Annotations
124    /// MUST be a key-value map. If there are no annotations then
125    /// this property MAY either be absent or an empty map.
126    ///
127    /// Keys MUST be strings. Keys MUST NOT be an empty string. Keys SHOULD
128    /// be named using a reverse domain notation - e.g.
129    /// com.example.myKey. Keys using the org.opencontainers
130    /// namespace are reserved and MUST NOT be used by subsequent
131    /// specifications. Runtimes MUST handle unknown annotation keys
132    /// like any other unknown property.
133    ///
134    /// Values MUST be strings. Values MAY be an empty string.
135    annotations: Option<HashMap<String, String>>,
136
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    /// Linux is platform-specific configuration for Linux based containers.
139    linux: Option<Linux>,
140
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    /// Solaris is platform-specific configuration for Solaris based
143    /// containers.
144    solaris: Option<Solaris>,
145
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    /// Windows is platform-specific configuration for Windows based
148    /// containers.
149    windows: Option<Windows>,
150
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    /// VM specifies configuration for Virtual Machine based containers.
153    vm: Option<VM>,
154
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    /// z/OS is platform-specific configuration for z/OS based containers.
157    zos: Option<ZOS>,
158
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    /// UID mappings used for changing file owners w/o calling chown, fs should support it.
161    /// Every mount point could have its own mapping.
162    uid_mappings: Option<Vec<LinuxIdMapping>>,
163
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    /// GID mappings used for changing file owners w/o calling chown, fs should support it.
166    /// Every mount point could have its own mapping.
167    gid_mappings: Option<Vec<LinuxIdMapping>>,
168}
169
170// This gives a basic boilerplate for Spec that can be used calling
171// Default::default(). The values given are similar to the defaults seen in
172// docker and runc, it creates a containerized shell! (see respective types
173// default impl for more info)
174impl Default for Spec {
175    fn default() -> Self {
176        Spec {
177            // Defaults to most current oci version
178            version: String::from("1.0.2-dev"),
179            process: Some(Default::default()),
180            root: Some(Default::default()),
181            hostname: "youki".to_string().into(),
182            domainname: None,
183            mounts: get_default_mounts().into(),
184            // Defaults to empty metadata
185            annotations: Some(Default::default()),
186            linux: Some(Default::default()),
187            hooks: None,
188            solaris: None,
189            windows: None,
190            vm: None,
191            zos: None,
192            uid_mappings: None,
193            gid_mappings: None,
194        }
195    }
196}
197
198impl Spec {
199    /// Load a new `Spec` from the provided JSON file `path`.
200    /// # Errors
201    /// This function will return an [OciSpecError::Io] if the spec does not exist or an
202    /// [OciSpecError::SerDe] if it is invalid.
203    /// # Example
204    /// ``` no_run
205    /// use oci_spec::runtime::Spec;
206    ///
207    /// let spec = Spec::load("config.json").unwrap();
208    /// ```
209    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
210        let path = path.as_ref();
211        let file = fs::File::open(path)?;
212        let reader = BufReader::new(file);
213        let s = serde_json::from_reader(reader)?;
214        Ok(s)
215    }
216
217    /// Save a `Spec` to the provided JSON file `path`.
218    /// # Errors
219    /// This function will return an [OciSpecError::Io] if a file cannot be created at the provided
220    /// path or an [OciSpecError::SerDe] if the spec cannot be serialized.
221    /// # Example
222    /// ``` no_run
223    /// use oci_spec::runtime::Spec;
224    ///
225    /// let mut spec = Spec::load("config.json").unwrap();
226    /// spec.save("my_config.json").unwrap();
227    /// ```
228    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
229        let path = path.as_ref();
230        let file = fs::File::create(path)?;
231        let mut writer = BufWriter::new(file);
232        serde_json::to_writer(&mut writer, self)?;
233        writer.flush()?;
234        Ok(())
235    }
236
237    /// Canonicalize the `root.path` of the `Spec` for the provided `bundle`.
238    pub fn canonicalize_rootfs<P: AsRef<Path>>(&mut self, bundle: P) -> Result<()> {
239        let root = self
240            .root
241            .as_ref()
242            .ok_or_else(|| oci_error("no root path provided for canonicalization"))?;
243        let path = Self::canonicalize_path(bundle, root.path())?;
244        self.root = Some(
245            RootBuilder::default()
246                .path(path)
247                .readonly(root.readonly().unwrap_or(false))
248                .build()
249                .map_err(|_| oci_error("failed to set canonicalized root"))?,
250        );
251        Ok(())
252    }
253
254    /// Return default rootless spec.
255    /// # Example
256    /// ``` no_run
257    /// use oci_spec::runtime::Spec;
258    ///
259    /// let spec = Spec::rootless(1000, 1000);
260    /// ```
261    pub fn rootless(uid: u32, gid: u32) -> Self {
262        Self {
263            mounts: get_rootless_mounts().into(),
264            linux: Some(Linux::rootless(uid, gid)),
265            ..Default::default()
266        }
267    }
268
269    fn canonicalize_path<B, P>(bundle: B, path: P) -> Result<PathBuf>
270    where
271        B: AsRef<Path>,
272        P: AsRef<Path>,
273    {
274        Ok(if path.as_ref().is_absolute() {
275            fs::canonicalize(path.as_ref())?
276        } else {
277            let canonical_bundle_path = fs::canonicalize(&bundle)?;
278            fs::canonicalize(canonical_bundle_path.join(path.as_ref()))?
279        })
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_canonicalize_rootfs() {
289        let rootfs_name = "rootfs";
290        let bundle = tempfile::tempdir().expect("failed to create tmp test bundle dir");
291
292        // On macOS, `$TMPDIR` may not point to canonicalized path.
293        // ```
294        // $ echo $TMPDIR; realpath $TMPDIR
295        // /var/folders/_h/j_17023n23s3_50cq_gwhrrc0000gq/T/
296        // /private/var/folders/_h/j_17023n23s3_50cq_gwhrrc0000gq/T
297        // ```
298        let bundle = fs::canonicalize(bundle.path()).expect("failed to canonicalize bundle");
299
300        let rootfs_absolute_path = bundle.join(rootfs_name);
301        assert!(
302            rootfs_absolute_path.is_absolute(),
303            "rootfs path is not absolute path"
304        );
305        fs::create_dir_all(&rootfs_absolute_path).expect("failed to create the testing rootfs");
306        {
307            // Test the case with absolute path
308            let mut spec = SpecBuilder::default()
309                .root(
310                    RootBuilder::default()
311                        .path(rootfs_absolute_path.clone())
312                        .build()
313                        .unwrap(),
314                )
315                .build()
316                .unwrap();
317
318            spec.canonicalize_rootfs(&bundle)
319                .expect("failed to canonicalize rootfs");
320
321            assert_eq!(
322                &rootfs_absolute_path,
323                spec.root.expect("no root in spec").path()
324            );
325        }
326        {
327            // Test the case with relative path
328            let mut spec = SpecBuilder::default()
329                .root(RootBuilder::default().path(rootfs_name).build().unwrap())
330                .build()
331                .unwrap();
332
333            spec.canonicalize_rootfs(&bundle)
334                .expect("failed to canonicalize rootfs");
335
336            assert_eq!(
337                &rootfs_absolute_path,
338                spec.root.expect("no root in spec").path()
339            );
340        }
341    }
342
343    #[test]
344    fn test_load_save() {
345        let spec = Spec {
346            ..Default::default()
347        };
348        let test_dir = tempfile::tempdir().expect("failed to create tmp test dir");
349        let spec_path = test_dir.keep().join("config.json");
350
351        // Test first save the default config, and then load the saved config.
352        // The before and after should be the same.
353        spec.save(&spec_path).expect("failed to save spec");
354        let loaded_spec = Spec::load(&spec_path).expect("failed to load the saved spec.");
355        assert_eq!(
356            spec, loaded_spec,
357            "The saved spec is not the same as the loaded spec"
358        );
359    }
360
361    #[test]
362    fn test_rootless() {
363        const UID: u32 = 1000;
364        const GID: u32 = 1000;
365
366        let spec = Spec::default();
367        let spec_rootless = Spec::rootless(UID, GID);
368        assert!(
369            spec != spec_rootless,
370            "default spec and rootless spec should be different"
371        );
372
373        // Check rootless linux object.
374        let linux = spec_rootless
375            .linux
376            .expect("linux object should not be empty");
377        let uid_mappings = linux
378            .uid_mappings()
379            .clone()
380            .expect("uid mappings should not be empty");
381        let gid_mappings = linux
382            .gid_mappings()
383            .clone()
384            .expect("gid mappings should not be empty");
385        let namespaces = linux
386            .namespaces()
387            .clone()
388            .expect("namespaces should not be empty");
389        assert_eq!(uid_mappings.len(), 1, "uid mappings length should be 1");
390        assert_eq!(
391            uid_mappings[0].host_id(),
392            UID,
393            "uid mapping host id should be as defined"
394        );
395        assert_eq!(gid_mappings.len(), 1, "gid mappings length should be 1");
396        assert_eq!(
397            gid_mappings[0].host_id(),
398            GID,
399            "gid mapping host id should be as defined"
400        );
401        assert!(
402            !namespaces
403                .iter()
404                .any(|ns| ns.typ() == LinuxNamespaceType::Network),
405            "rootless spec should not contain network namespace type"
406        );
407        assert!(
408            namespaces
409                .iter()
410                .any(|ns| ns.typ() == LinuxNamespaceType::User),
411            "rootless spec should contain user namespace type"
412        );
413        assert!(
414            linux.resources().is_none(),
415            "resources in rootless spec should be empty"
416        );
417
418        // Check rootless mounts.
419        let mounts = spec_rootless.mounts.expect("mounts should not be empty");
420        assert!(
421            !mounts.iter().any(|m| {
422                if m.destination().to_string_lossy() == "/dev/pts" {
423                    return m
424                        .options()
425                        .clone()
426                        .expect("options should not be empty")
427                        .iter()
428                        .any(|o| o == "gid=5");
429                } else {
430                    false
431                }
432            }),
433            "gid=5 in rootless should not be present"
434        );
435        let sys_mount = mounts
436            .iter()
437            .find(|m| m.destination().to_string_lossy() == "/sys")
438            .expect("sys mount should be present");
439        assert_eq!(
440            sys_mount.typ(),
441            &Some("none".to_string()),
442            "type should be changed in sys mount"
443        );
444        assert_eq!(
445            sys_mount
446                .source()
447                .clone()
448                .expect("source should not be empty in sys mount")
449                .to_string_lossy(),
450            "/sys",
451            "source should be changed in sys mount"
452        );
453        assert!(
454            sys_mount
455                .options()
456                .clone()
457                .expect("options should not be empty in sys mount")
458                .iter()
459                .any(|o| o == "rbind"),
460            "rbind option should be present in sys mount"
461        );
462
463        // Check that some other objects have same values.
464        assert!(spec.process == spec_rootless.process);
465        assert!(spec.root == spec_rootless.root);
466        assert!(spec.hooks == spec_rootless.hooks);
467        assert!(spec.uid_mappings == spec_rootless.uid_mappings);
468        assert!(spec.gid_mappings == spec_rootless.gid_mappings);
469    }
470}