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