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}