Skip to main content

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    #[deprecated(
160        since = "0.10.0",
161        note = "uid_mappings on the top-level Spec struct has never existed in the OCI runtime spec. Use Linux::uid_mappings or Mount::uid_mappings instead."
162    )]
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    /// UID mappings used for changing file owners w/o calling chown, fs should support it.
165    /// Every mount point could have its own mapping.
166    ///
167    /// # Deprecated
168    /// This field has never existed on the top-level `Spec` struct in the OCI runtime spec.
169    /// Use [`Linux::uid_mappings`] or [`Mount::uid_mappings`] instead.
170    uid_mappings: Option<Vec<LinuxIdMapping>>,
171
172    #[deprecated(
173        since = "0.10.0",
174        note = "gid_mappings on the top-level Spec struct has never existed in the OCI runtime spec. Use Linux::gid_mappings or Mount::gid_mappings instead."
175    )]
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    /// GID mappings used for changing file owners w/o calling chown, fs should support it.
178    /// Every mount point could have its own mapping.
179    ///
180    /// # Deprecated
181    /// This field has never existed on the top-level `Spec` struct in the OCI runtime spec.
182    /// Use [`Linux::gid_mappings`] or [`Mount::gid_mappings`] instead.
183    gid_mappings: Option<Vec<LinuxIdMapping>>,
184}
185
186// This gives a basic boilerplate for Spec that can be used calling
187// Default::default(). The values given are similar to the defaults seen in
188// docker and runc, it creates a containerized shell! (see respective types
189// default impl for more info)
190#[allow(deprecated)]
191impl Default for Spec {
192    fn default() -> Self {
193        Spec {
194            // Defaults to most current oci version
195            version: String::from("1.0.2-dev"),
196            process: Some(Default::default()),
197            root: Some(Default::default()),
198            hostname: "youki".to_string().into(),
199            domainname: None,
200            mounts: get_default_mounts().into(),
201            // Defaults to empty metadata
202            annotations: Some(Default::default()),
203            linux: Some(Default::default()),
204            hooks: None,
205            solaris: None,
206            windows: None,
207            vm: None,
208            zos: None,
209            uid_mappings: None,
210            gid_mappings: None,
211        }
212    }
213}
214
215impl Spec {
216    /// Load a new `Spec` from the provided JSON file `path`.
217    /// # Errors
218    /// This function will return an [OciSpecError::Io] if the spec does not exist or an
219    /// [OciSpecError::SerDe] if it is invalid.
220    /// # Example
221    /// ``` no_run
222    /// use oci_spec::runtime::Spec;
223    ///
224    /// let spec = Spec::load("config.json").unwrap();
225    /// ```
226    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
227        let path = path.as_ref();
228        let file = fs::File::open(path)?;
229        let reader = BufReader::new(file);
230        let s = serde_json::from_reader(reader)?;
231        Ok(s)
232    }
233
234    /// Save a `Spec` to the provided JSON file `path`.
235    /// # Errors
236    /// This function will return an [OciSpecError::Io] if a file cannot be created at the provided
237    /// path or an [OciSpecError::SerDe] if the spec cannot be serialized.
238    /// # Example
239    /// ``` no_run
240    /// use oci_spec::runtime::Spec;
241    ///
242    /// let mut spec = Spec::load("config.json").unwrap();
243    /// spec.save("my_config.json").unwrap();
244    /// ```
245    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
246        let path = path.as_ref();
247        let file = fs::File::create(path)?;
248        let mut writer = BufWriter::new(file);
249        serde_json::to_writer(&mut writer, self)?;
250        writer.flush()?;
251        Ok(())
252    }
253
254    /// Canonicalize the `root.path` of the `Spec` for the provided `bundle`.
255    pub fn canonicalize_rootfs<P: AsRef<Path>>(&mut self, bundle: P) -> Result<()> {
256        let root = self
257            .root
258            .as_ref()
259            .ok_or_else(|| oci_error("no root path provided for canonicalization"))?;
260        let path = Self::canonicalize_path(bundle, root.path())?;
261        self.root = Some(
262            RootBuilder::default()
263                .path(path)
264                .readonly(root.readonly().unwrap_or(false))
265                .build()
266                .map_err(|_| oci_error("failed to set canonicalized root"))?,
267        );
268        Ok(())
269    }
270
271    /// Return default rootless spec.
272    /// # Example
273    /// ``` no_run
274    /// use oci_spec::runtime::Spec;
275    ///
276    /// let spec = Spec::rootless(1000, 1000);
277    /// ```
278    pub fn rootless(uid: u32, gid: u32) -> Self {
279        Self {
280            mounts: get_rootless_mounts().into(),
281            linux: Some(Linux::rootless(uid, gid)),
282            ..Default::default()
283        }
284    }
285
286    fn canonicalize_path<B, P>(bundle: B, path: P) -> Result<PathBuf>
287    where
288        B: AsRef<Path>,
289        P: AsRef<Path>,
290    {
291        Ok(if path.as_ref().is_absolute() {
292            fs::canonicalize(path.as_ref())?
293        } else {
294            let canonical_bundle_path = fs::canonicalize(&bundle)?;
295            fs::canonicalize(canonical_bundle_path.join(path.as_ref()))?
296        })
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_canonicalize_rootfs() {
306        let rootfs_name = "rootfs";
307        let bundle = tempfile::tempdir().expect("failed to create tmp test bundle dir");
308
309        // On macOS, `$TMPDIR` may not point to canonicalized path.
310        // ```
311        // $ echo $TMPDIR; realpath $TMPDIR
312        // /var/folders/_h/j_17023n23s3_50cq_gwhrrc0000gq/T/
313        // /private/var/folders/_h/j_17023n23s3_50cq_gwhrrc0000gq/T
314        // ```
315        let bundle = fs::canonicalize(bundle.path()).expect("failed to canonicalize bundle");
316
317        let rootfs_absolute_path = bundle.join(rootfs_name);
318        assert!(
319            rootfs_absolute_path.is_absolute(),
320            "rootfs path is not absolute path"
321        );
322        fs::create_dir_all(&rootfs_absolute_path).expect("failed to create the testing rootfs");
323        {
324            // Test the case with absolute path
325            let mut spec = SpecBuilder::default()
326                .root(
327                    RootBuilder::default()
328                        .path(rootfs_absolute_path.clone())
329                        .build()
330                        .unwrap(),
331                )
332                .build()
333                .unwrap();
334
335            spec.canonicalize_rootfs(&bundle)
336                .expect("failed to canonicalize rootfs");
337
338            assert_eq!(
339                &rootfs_absolute_path,
340                spec.root.expect("no root in spec").path()
341            );
342        }
343        {
344            // Test the case with relative path
345            let mut spec = SpecBuilder::default()
346                .root(RootBuilder::default().path(rootfs_name).build().unwrap())
347                .build()
348                .unwrap();
349
350            spec.canonicalize_rootfs(&bundle)
351                .expect("failed to canonicalize rootfs");
352
353            assert_eq!(
354                &rootfs_absolute_path,
355                spec.root.expect("no root in spec").path()
356            );
357        }
358    }
359
360    #[test]
361    fn test_load_save() {
362        let spec = Spec {
363            ..Default::default()
364        };
365        let test_dir = tempfile::tempdir().expect("failed to create tmp test dir");
366        let spec_path = test_dir.keep().join("config.json");
367
368        // Test first save the default config, and then load the saved config.
369        // The before and after should be the same.
370        spec.save(&spec_path).expect("failed to save spec");
371        let loaded_spec = Spec::load(&spec_path).expect("failed to load the saved spec.");
372        assert_eq!(
373            spec, loaded_spec,
374            "The saved spec is not the same as the loaded spec"
375        );
376    }
377
378    #[test]
379    fn test_rootless() {
380        const UID: u32 = 1000;
381        const GID: u32 = 1000;
382
383        let spec = Spec::default();
384        let spec_rootless = Spec::rootless(UID, GID);
385        assert!(
386            spec != spec_rootless,
387            "default spec and rootless spec should be different"
388        );
389
390        // Check rootless linux object.
391        let linux = spec_rootless
392            .linux
393            .expect("linux object should not be empty");
394        let uid_mappings = linux
395            .uid_mappings()
396            .clone()
397            .expect("uid mappings should not be empty");
398        let gid_mappings = linux
399            .gid_mappings()
400            .clone()
401            .expect("gid mappings should not be empty");
402        let namespaces = linux
403            .namespaces()
404            .clone()
405            .expect("namespaces should not be empty");
406        assert_eq!(uid_mappings.len(), 1, "uid mappings length should be 1");
407        assert_eq!(
408            uid_mappings[0].host_id(),
409            UID,
410            "uid mapping host id should be as defined"
411        );
412        assert_eq!(gid_mappings.len(), 1, "gid mappings length should be 1");
413        assert_eq!(
414            gid_mappings[0].host_id(),
415            GID,
416            "gid mapping host id should be as defined"
417        );
418        assert!(
419            !namespaces
420                .iter()
421                .any(|ns| ns.typ() == LinuxNamespaceType::Network),
422            "rootless spec should not contain network namespace type"
423        );
424        assert!(
425            namespaces
426                .iter()
427                .any(|ns| ns.typ() == LinuxNamespaceType::User),
428            "rootless spec should contain user namespace type"
429        );
430        assert!(
431            linux.resources().is_none(),
432            "resources in rootless spec should be empty"
433        );
434
435        // Check rootless mounts.
436        let mounts = spec_rootless.mounts.expect("mounts should not be empty");
437        assert!(
438            !mounts.iter().any(|m| {
439                if m.destination().to_string_lossy() == "/dev/pts" {
440                    m.options()
441                        .clone()
442                        .expect("options should not be empty")
443                        .iter()
444                        .any(|o| o == "gid=5")
445                } else {
446                    false
447                }
448            }),
449            "gid=5 in rootless should not be present"
450        );
451        let sys_mount = mounts
452            .iter()
453            .find(|m| m.destination().to_string_lossy() == "/sys")
454            .expect("sys mount should be present");
455        assert_eq!(
456            sys_mount.typ(),
457            &Some("none".to_string()),
458            "type should be changed in sys mount"
459        );
460        assert_eq!(
461            sys_mount
462                .source()
463                .clone()
464                .expect("source should not be empty in sys mount")
465                .to_string_lossy(),
466            "/sys",
467            "source should be changed in sys mount"
468        );
469        assert!(
470            sys_mount
471                .options()
472                .clone()
473                .expect("options should not be empty in sys mount")
474                .iter()
475                .any(|o| o == "rbind"),
476            "rbind option should be present in sys mount"
477        );
478
479        // Check that some other objects have same values.
480        assert!(spec.process == spec_rootless.process);
481        assert!(spec.root == spec_rootless.root);
482        assert!(spec.hooks == spec_rootless.hooks);
483    }
484}