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}