oci_spec/runtime/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
//! [OCI runtime spec](https://github.com/opencontainers/runtime-spec) types and definitions.

use derive_builder::Builder;
use getset::{Getters, MutGetters, Setters};
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    fs,
    io::{BufReader, BufWriter, Write},
    path::{Path, PathBuf},
};

use crate::error::{oci_error, OciSpecError, Result};

mod capability;
mod features;
mod hooks;
mod linux;
mod miscellaneous;
mod process;
mod solaris;
mod test;
mod version;
mod vm;
mod windows;

// re-export for ease of use
pub use capability::*;
pub use features::*;
pub use hooks::*;
pub use linux::*;
pub use miscellaneous::*;
pub use process::*;
pub use solaris::*;
pub use version::*;
pub use vm::*;
pub use windows::*;

/// Base configuration for the container.
#[derive(
    Builder, Clone, Debug, Deserialize, Getters, MutGetters, Setters, PartialEq, Eq, Serialize,
)]
#[serde(rename_all = "camelCase")]
#[builder(
    default,
    pattern = "owned",
    setter(into, strip_option),
    build_fn(error = "OciSpecError")
)]
#[getset(get_mut = "pub", get = "pub", set = "pub")]
pub struct Spec {
    #[serde(default, rename = "ociVersion")]
    ///  MUST be in SemVer v2.0.0 format and specifies the version of the
    /// Open Container Initiative  Runtime Specification with which
    /// the bundle complies. The Open Container Initiative
    ///  Runtime Specification follows semantic versioning and retains
    /// forward and backward  compatibility within major versions.
    /// For example, if a configuration is compliant with
    ///  version 1.1 of this specification, it is compatible with all
    /// runtimes that support any 1.1  or later release of this
    /// specification, but is not compatible with a runtime that supports
    ///  1.0 and not 1.1.
    version: String,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Specifies the container's root filesystem. On Windows, for Windows
    /// Server Containers, this field is REQUIRED. For Hyper-V
    /// Containers, this field MUST NOT be set.
    ///
    /// On all other platforms, this field is REQUIRED.
    root: Option<Root>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Specifies additional mounts beyond `root`. The runtime MUST mount
    /// entries in the listed order.
    ///
    /// For Linux, the parameters are as documented in
    /// [`mount(2)`](http://man7.org/linux/man-pages/man2/mount.2.html) system call man page. For
    /// Solaris, the mount entry corresponds to the 'fs' resource in the
    /// [`zonecfg(1M)`](http://docs.oracle.com/cd/E86824_01/html/E54764/zonecfg-1m.html) man page.
    mounts: Option<Vec<Mount>>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Specifies the container process. This property is REQUIRED when
    /// [`start`](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md#start) is
    /// called.
    process: Option<Process>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Specifies the container's hostname as seen by processes running
    /// inside the container. On Linux, for example, this will
    /// change the hostname in the container [UTS namespace](http://man7.org/linux/man-pages/man7/namespaces.7.html). Depending on your
    /// [namespace
    /// configuration](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#namespaces),
    /// the container UTS namespace may be the runtime UTS namespace.
    hostname: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Specifies the container's domainame as seen by processes running
    /// inside the container. On Linux, for example, this will
    /// change the domainame in the container [UTS namespace](http://man7.org/linux/man-pages/man7/namespaces.7.html). Depending on your
    /// [namespace
    /// configuration](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#namespaces),
    /// the container UTS namespace may be the runtime UTS namespace.
    domainname: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Hooks allow users to specify programs to run before or after various
    /// lifecycle events. Hooks MUST be called in the listed order.
    /// The state of the container MUST be passed to hooks over
    /// stdin so that they may do work appropriate to the current state of
    /// the container.
    hooks: Option<Hooks>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Annotations contains arbitrary metadata for the container. This
    /// information MAY be structured or unstructured. Annotations
    /// MUST be a key-value map. If there are no annotations then
    /// this property MAY either be absent or an empty map.
    ///
    /// Keys MUST be strings. Keys MUST NOT be an empty string. Keys SHOULD
    /// be named using a reverse domain notation - e.g.
    /// com.example.myKey. Keys using the org.opencontainers
    /// namespace are reserved and MUST NOT be used by subsequent
    /// specifications. Runtimes MUST handle unknown annotation keys
    /// like any other unknown property.
    ///
    /// Values MUST be strings. Values MAY be an empty string.
    annotations: Option<HashMap<String, String>>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Linux is platform-specific configuration for Linux based containers.
    linux: Option<Linux>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Solaris is platform-specific configuration for Solaris based
    /// containers.
    solaris: Option<Solaris>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Windows is platform-specific configuration for Windows based
    /// containers.
    windows: Option<Windows>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// VM specifies configuration for Virtual Machine based containers.
    vm: Option<VM>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// UID mappings used for changing file owners w/o calling chown, fs should support it.
    /// Every mount point could have its own mapping.
    uid_mappings: Option<Vec<LinuxIdMapping>>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// GID mappings used for changing file owners w/o calling chown, fs should support it.
    /// Every mount point could have its own mapping.
    gid_mappings: Option<Vec<LinuxIdMapping>>,
}

// This gives a basic boilerplate for Spec that can be used calling
// Default::default(). The values given are similar to the defaults seen in
// docker and runc, it creates a containerized shell! (see respective types
// default impl for more info)
impl Default for Spec {
    fn default() -> Self {
        Spec {
            // Defaults to most current oci version
            version: String::from("1.0.2-dev"),
            process: Some(Default::default()),
            root: Some(Default::default()),
            hostname: "youki".to_string().into(),
            domainname: None,
            mounts: get_default_mounts().into(),
            // Defaults to empty metadata
            annotations: Some(Default::default()),
            linux: Some(Default::default()),
            hooks: None,
            solaris: None,
            windows: None,
            vm: None,
            uid_mappings: None,
            gid_mappings: None,
        }
    }
}

impl Spec {
    /// Load a new `Spec` from the provided JSON file `path`.
    /// # Errors
    /// This function will return an [OciSpecError::Io] if the spec does not exist or an
    /// [OciSpecError::SerDe] if it is invalid.
    /// # Example
    /// ``` no_run
    /// use oci_spec::runtime::Spec;
    ///
    /// let spec = Spec::load("config.json").unwrap();
    /// ```
    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
        let path = path.as_ref();
        let file = fs::File::open(path)?;
        let reader = BufReader::new(file);
        let s = serde_json::from_reader(reader)?;
        Ok(s)
    }

    /// Save a `Spec` to the provided JSON file `path`.
    /// # Errors
    /// This function will return an [OciSpecError::Io] if a file cannot be created at the provided
    /// path or an [OciSpecError::SerDe] if the spec cannot be serialized.
    /// # Example
    /// ``` no_run
    /// use oci_spec::runtime::Spec;
    ///
    /// let mut spec = Spec::load("config.json").unwrap();
    /// spec.save("my_config.json").unwrap();
    /// ```
    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
        let path = path.as_ref();
        let file = fs::File::create(path)?;
        let mut writer = BufWriter::new(file);
        serde_json::to_writer(&mut writer, self)?;
        writer.flush()?;
        Ok(())
    }

    /// Canonicalize the `root.path` of the `Spec` for the provided `bundle`.
    pub fn canonicalize_rootfs<P: AsRef<Path>>(&mut self, bundle: P) -> Result<()> {
        let root = self
            .root
            .as_ref()
            .ok_or_else(|| oci_error("no root path provided for canonicalization"))?;
        let path = Self::canonicalize_path(bundle, root.path())?;
        self.root = Some(
            RootBuilder::default()
                .path(path)
                .readonly(root.readonly().unwrap_or(false))
                .build()
                .map_err(|_| oci_error("failed to set canonicalized root"))?,
        );
        Ok(())
    }

    /// Return default rootless spec.
    /// # Example
    /// ``` no_run
    /// use oci_spec::runtime::Spec;
    ///
    /// let spec = Spec::rootless(1000, 1000);
    /// ```
    pub fn rootless(uid: u32, gid: u32) -> Self {
        Self {
            mounts: get_rootless_mounts().into(),
            linux: Some(Linux::rootless(uid, gid)),
            ..Default::default()
        }
    }

    fn canonicalize_path<B, P>(bundle: B, path: P) -> Result<PathBuf>
    where
        B: AsRef<Path>,
        P: AsRef<Path>,
    {
        Ok(if path.as_ref().is_absolute() {
            fs::canonicalize(path.as_ref())?
        } else {
            let canonical_bundle_path = fs::canonicalize(&bundle)?;
            fs::canonicalize(canonical_bundle_path.join(path.as_ref()))?
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_canonicalize_rootfs() {
        let rootfs_name = "rootfs";
        let bundle = tempfile::tempdir().expect("failed to create tmp test bundle dir");
        let rootfs_absolute_path = bundle.path().join(rootfs_name);
        assert!(
            rootfs_absolute_path.is_absolute(),
            "rootfs path is not absolute path"
        );
        fs::create_dir_all(&rootfs_absolute_path).expect("failed to create the testing rootfs");
        {
            // Test the case with absolute path
            let mut spec = SpecBuilder::default()
                .root(
                    RootBuilder::default()
                        .path(rootfs_absolute_path.clone())
                        .build()
                        .unwrap(),
                )
                .build()
                .unwrap();

            spec.canonicalize_rootfs(bundle.path())
                .expect("failed to canonicalize rootfs");

            assert_eq!(
                &rootfs_absolute_path,
                spec.root.expect("no root in spec").path()
            );
        }
        {
            // Test the case with relative path
            let mut spec = SpecBuilder::default()
                .root(RootBuilder::default().path(rootfs_name).build().unwrap())
                .build()
                .unwrap();

            spec.canonicalize_rootfs(bundle.path())
                .expect("failed to canonicalize rootfs");

            assert_eq!(
                &rootfs_absolute_path,
                spec.root.expect("no root in spec").path()
            );
        }
    }

    #[test]
    fn test_load_save() {
        let spec = Spec {
            ..Default::default()
        };
        let test_dir = tempfile::tempdir().expect("failed to create tmp test dir");
        let spec_path = test_dir.into_path().join("config.json");

        // Test first save the default config, and then load the saved config.
        // The before and after should be the same.
        spec.save(&spec_path).expect("failed to save spec");
        let loaded_spec = Spec::load(&spec_path).expect("failed to load the saved spec.");
        assert_eq!(
            spec, loaded_spec,
            "The saved spec is not the same as the loaded spec"
        );
    }

    #[test]
    fn test_rootless() {
        const UID: u32 = 1000;
        const GID: u32 = 1000;

        let spec = Spec::default();
        let spec_rootless = Spec::rootless(UID, GID);
        assert!(
            spec != spec_rootless,
            "default spec and rootless spec should be different"
        );

        // Check rootless linux object.
        let linux = spec_rootless
            .linux
            .expect("linux object should not be empty");
        let uid_mappings = linux
            .uid_mappings()
            .clone()
            .expect("uid mappings should not be empty");
        let gid_mappings = linux
            .gid_mappings()
            .clone()
            .expect("gid mappings should not be empty");
        let namespaces = linux
            .namespaces()
            .clone()
            .expect("namespaces should not be empty");
        assert_eq!(uid_mappings.len(), 1, "uid mappings length should be 1");
        assert_eq!(
            uid_mappings[0].host_id(),
            UID,
            "uid mapping host id should be as defined"
        );
        assert_eq!(gid_mappings.len(), 1, "gid mappings length should be 1");
        assert_eq!(
            gid_mappings[0].host_id(),
            GID,
            "gid mapping host id should be as defined"
        );
        assert!(
            !namespaces
                .iter()
                .any(|ns| ns.typ() == LinuxNamespaceType::Network),
            "rootless spec should not contain network namespace type"
        );
        assert!(
            namespaces
                .iter()
                .any(|ns| ns.typ() == LinuxNamespaceType::User),
            "rootless spec should contain user namespace type"
        );
        assert!(
            linux.resources().is_none(),
            "resources in rootless spec should be empty"
        );

        // Check rootless mounts.
        let mounts = spec_rootless.mounts.expect("mounts should not be empty");
        assert!(
            !mounts.iter().any(|m| {
                if m.destination().to_string_lossy() == "/dev/pts" {
                    return m
                        .options()
                        .clone()
                        .expect("options should not be empty")
                        .iter()
                        .any(|o| o == "gid=5");
                } else {
                    false
                }
            }),
            "gid=5 in rootless should not be present"
        );
        let sys_mount = mounts
            .iter()
            .find(|m| m.destination().to_string_lossy() == "/sys")
            .expect("sys mount should be present");
        assert_eq!(
            sys_mount.typ(),
            &Some("none".to_string()),
            "type should be changed in sys mount"
        );
        assert_eq!(
            sys_mount
                .source()
                .clone()
                .expect("source should not be empty in sys mount")
                .to_string_lossy(),
            "/sys",
            "source should be changed in sys mount"
        );
        assert!(
            sys_mount
                .options()
                .clone()
                .expect("options should not be empty in sys mount")
                .iter()
                .any(|o| o == "rbind"),
            "rbind option should be present in sys mount"
        );

        // Check that some other objects have same values.
        assert!(spec.process == spec_rootless.process);
        assert!(spec.root == spec_rootless.root);
        assert!(spec.hooks == spec_rootless.hooks);
        assert!(spec.uid_mappings == spec_rootless.uid_mappings);
        assert!(spec.gid_mappings == spec_rootless.gid_mappings);
    }
}