oci_spec/runtime/
mod.rs

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