1use 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
27pub 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#[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 version: String,
64
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 root: Option<Root>,
72
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 mounts: Option<Vec<Mount>>,
82
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 process: Option<Process>,
88
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 hostname: Option<String>,
97
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 domainname: Option<String>,
106
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 hooks: Option<Hooks>,
114
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 annotations: Option<HashMap<String, String>>,
130
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 linux: Option<Linux>,
134
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 solaris: Option<Solaris>,
139
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 windows: Option<Windows>,
144
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 vm: Option<VM>,
148
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 uid_mappings: Option<Vec<LinuxIdMapping>>,
153
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 gid_mappings: Option<Vec<LinuxIdMapping>>,
158}
159
160impl Default for Spec {
165 fn default() -> Self {
166 Spec {
167 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 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 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 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 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 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 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 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 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 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 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 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}