1use 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
31pub 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#[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 version: String,
70
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 root: Option<Root>,
78
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 mounts: Option<Vec<Mount>>,
88
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 process: Option<Process>,
94
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 hostname: Option<String>,
103
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 domainname: Option<String>,
112
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 hooks: Option<Hooks>,
120
121 #[serde(default, skip_serializing_if = "Option::is_none")]
122 annotations: Option<HashMap<String, String>>,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 linux: Option<Linux>,
140
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 solaris: Option<Solaris>,
145
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 windows: Option<Windows>,
150
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 vm: Option<VM>,
154
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 zos: Option<ZOS>,
158
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 uid_mappings: Option<Vec<LinuxIdMapping>>,
163
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 gid_mappings: Option<Vec<LinuxIdMapping>>,
168}
169
170impl Default for Spec {
175 fn default() -> Self {
176 Spec {
177 version: String::from("1.0.2-dev"),
179 process: Some(Default::default()),
180 root: Some(Default::default()),
181 hostname: "youki".to_string().into(),
182 domainname: None,
183 mounts: get_default_mounts().into(),
184 annotations: Some(Default::default()),
186 linux: Some(Default::default()),
187 hooks: None,
188 solaris: None,
189 windows: None,
190 vm: None,
191 zos: None,
192 uid_mappings: None,
193 gid_mappings: None,
194 }
195 }
196}
197
198impl Spec {
199 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
210 let path = path.as_ref();
211 let file = fs::File::open(path)?;
212 let reader = BufReader::new(file);
213 let s = serde_json::from_reader(reader)?;
214 Ok(s)
215 }
216
217 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
229 let path = path.as_ref();
230 let file = fs::File::create(path)?;
231 let mut writer = BufWriter::new(file);
232 serde_json::to_writer(&mut writer, self)?;
233 writer.flush()?;
234 Ok(())
235 }
236
237 pub fn canonicalize_rootfs<P: AsRef<Path>>(&mut self, bundle: P) -> Result<()> {
239 let root = self
240 .root
241 .as_ref()
242 .ok_or_else(|| oci_error("no root path provided for canonicalization"))?;
243 let path = Self::canonicalize_path(bundle, root.path())?;
244 self.root = Some(
245 RootBuilder::default()
246 .path(path)
247 .readonly(root.readonly().unwrap_or(false))
248 .build()
249 .map_err(|_| oci_error("failed to set canonicalized root"))?,
250 );
251 Ok(())
252 }
253
254 pub fn rootless(uid: u32, gid: u32) -> Self {
262 Self {
263 mounts: get_rootless_mounts().into(),
264 linux: Some(Linux::rootless(uid, gid)),
265 ..Default::default()
266 }
267 }
268
269 fn canonicalize_path<B, P>(bundle: B, path: P) -> Result<PathBuf>
270 where
271 B: AsRef<Path>,
272 P: AsRef<Path>,
273 {
274 Ok(if path.as_ref().is_absolute() {
275 fs::canonicalize(path.as_ref())?
276 } else {
277 let canonical_bundle_path = fs::canonicalize(&bundle)?;
278 fs::canonicalize(canonical_bundle_path.join(path.as_ref()))?
279 })
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_canonicalize_rootfs() {
289 let rootfs_name = "rootfs";
290 let bundle = tempfile::tempdir().expect("failed to create tmp test bundle dir");
291
292 let bundle = fs::canonicalize(bundle.path()).expect("failed to canonicalize bundle");
299
300 let rootfs_absolute_path = bundle.join(rootfs_name);
301 assert!(
302 rootfs_absolute_path.is_absolute(),
303 "rootfs path is not absolute path"
304 );
305 fs::create_dir_all(&rootfs_absolute_path).expect("failed to create the testing rootfs");
306 {
307 let mut spec = SpecBuilder::default()
309 .root(
310 RootBuilder::default()
311 .path(rootfs_absolute_path.clone())
312 .build()
313 .unwrap(),
314 )
315 .build()
316 .unwrap();
317
318 spec.canonicalize_rootfs(&bundle)
319 .expect("failed to canonicalize rootfs");
320
321 assert_eq!(
322 &rootfs_absolute_path,
323 spec.root.expect("no root in spec").path()
324 );
325 }
326 {
327 let mut spec = SpecBuilder::default()
329 .root(RootBuilder::default().path(rootfs_name).build().unwrap())
330 .build()
331 .unwrap();
332
333 spec.canonicalize_rootfs(&bundle)
334 .expect("failed to canonicalize rootfs");
335
336 assert_eq!(
337 &rootfs_absolute_path,
338 spec.root.expect("no root in spec").path()
339 );
340 }
341 }
342
343 #[test]
344 fn test_load_save() {
345 let spec = Spec {
346 ..Default::default()
347 };
348 let test_dir = tempfile::tempdir().expect("failed to create tmp test dir");
349 let spec_path = test_dir.keep().join("config.json");
350
351 spec.save(&spec_path).expect("failed to save spec");
354 let loaded_spec = Spec::load(&spec_path).expect("failed to load the saved spec.");
355 assert_eq!(
356 spec, loaded_spec,
357 "The saved spec is not the same as the loaded spec"
358 );
359 }
360
361 #[test]
362 fn test_rootless() {
363 const UID: u32 = 1000;
364 const GID: u32 = 1000;
365
366 let spec = Spec::default();
367 let spec_rootless = Spec::rootless(UID, GID);
368 assert!(
369 spec != spec_rootless,
370 "default spec and rootless spec should be different"
371 );
372
373 let linux = spec_rootless
375 .linux
376 .expect("linux object should not be empty");
377 let uid_mappings = linux
378 .uid_mappings()
379 .clone()
380 .expect("uid mappings should not be empty");
381 let gid_mappings = linux
382 .gid_mappings()
383 .clone()
384 .expect("gid mappings should not be empty");
385 let namespaces = linux
386 .namespaces()
387 .clone()
388 .expect("namespaces should not be empty");
389 assert_eq!(uid_mappings.len(), 1, "uid mappings length should be 1");
390 assert_eq!(
391 uid_mappings[0].host_id(),
392 UID,
393 "uid mapping host id should be as defined"
394 );
395 assert_eq!(gid_mappings.len(), 1, "gid mappings length should be 1");
396 assert_eq!(
397 gid_mappings[0].host_id(),
398 GID,
399 "gid mapping host id should be as defined"
400 );
401 assert!(
402 !namespaces
403 .iter()
404 .any(|ns| ns.typ() == LinuxNamespaceType::Network),
405 "rootless spec should not contain network namespace type"
406 );
407 assert!(
408 namespaces
409 .iter()
410 .any(|ns| ns.typ() == LinuxNamespaceType::User),
411 "rootless spec should contain user namespace type"
412 );
413 assert!(
414 linux.resources().is_none(),
415 "resources in rootless spec should be empty"
416 );
417
418 let mounts = spec_rootless.mounts.expect("mounts should not be empty");
420 assert!(
421 !mounts.iter().any(|m| {
422 if m.destination().to_string_lossy() == "/dev/pts" {
423 return m
424 .options()
425 .clone()
426 .expect("options should not be empty")
427 .iter()
428 .any(|o| o == "gid=5");
429 } else {
430 false
431 }
432 }),
433 "gid=5 in rootless should not be present"
434 );
435 let sys_mount = mounts
436 .iter()
437 .find(|m| m.destination().to_string_lossy() == "/sys")
438 .expect("sys mount should be present");
439 assert_eq!(
440 sys_mount.typ(),
441 &Some("none".to_string()),
442 "type should be changed in sys mount"
443 );
444 assert_eq!(
445 sys_mount
446 .source()
447 .clone()
448 .expect("source should not be empty in sys mount")
449 .to_string_lossy(),
450 "/sys",
451 "source should be changed in sys mount"
452 );
453 assert!(
454 sys_mount
455 .options()
456 .clone()
457 .expect("options should not be empty in sys mount")
458 .iter()
459 .any(|o| o == "rbind"),
460 "rbind option should be present in sys mount"
461 );
462
463 assert!(spec.process == spec_rootless.process);
465 assert!(spec.root == spec_rootless.root);
466 assert!(spec.hooks == spec_rootless.hooks);
467 assert!(spec.uid_mappings == spec_rootless.uid_mappings);
468 assert!(spec.gid_mappings == spec_rootless.gid_mappings);
469 }
470}