docker_registry/
render.rs

1//! Render a docker image.
2
3// Docker image format is specified at
4// https://github.com/moby/moby/blob/v17.05.0-ce/image/spec/v1.md
5
6use std::{fs, io, path};
7
8use libflate::gzip;
9
10#[derive(Debug, thiserror::Error)]
11pub enum RenderError {
12  #[error("wrong target path {}: must be absolute path to existing directory", _0.display())]
13  WrongTargetPath(path::PathBuf),
14  #[error("io error")]
15  Io(#[from] std::io::Error),
16}
17
18/// Unpack an ordered list of layers to a target directory.
19///
20/// Layers must be provided as gzip-compressed tar archives, with lower layers
21/// coming first. Target directory must be an existing absolute path.
22pub fn unpack(layers: &[Vec<u8>], target_dir: &path::Path) -> Result<(), RenderError> {
23  _unpack(layers, target_dir, |mut archive, target_dir| {
24    Ok(archive.unpack(target_dir)?)
25  })
26}
27
28/// Unpack an ordered list of layers to a target directory, filtering
29/// file entries by path.
30///
31/// Layers must be provided as gzip-compressed tar archives, with lower layers
32/// coming first. Target directory must be an existing absolute path.
33pub fn filter_unpack<P>(layers: &[Vec<u8>], target_dir: &path::Path, predicate: P) -> Result<(), RenderError>
34where
35  P: Fn(&path::Path) -> bool,
36{
37  _unpack(layers, target_dir, |mut archive, target_dir| {
38    for entry in archive.entries()? {
39      let mut entry = entry?;
40      let path = entry.path()?;
41
42      if predicate(&path) {
43        entry.unpack_in(target_dir)?;
44      }
45    }
46
47    Ok(())
48  })
49}
50
51fn _unpack<U>(layers: &[Vec<u8>], target_dir: &path::Path, unpacker: U) -> Result<(), RenderError>
52where
53  U: Fn(tar::Archive<gzip::Decoder<&[u8]>>, &path::Path) -> Result<(), RenderError>,
54{
55  if !target_dir.is_absolute() || !target_dir.exists() || !target_dir.is_dir() {
56    return Err(RenderError::WrongTargetPath(target_dir.to_path_buf()));
57  }
58  for l in layers {
59    // Unpack layers
60    let gz_dec = gzip::Decoder::new(l.as_slice())?;
61    let mut archive = tar::Archive::new(gz_dec);
62    archive.set_preserve_permissions(true);
63    archive.set_unpack_xattrs(true);
64    unpacker(archive, target_dir)?;
65
66    // Clean whiteouts
67    let gz_dec = gzip::Decoder::new(l.as_slice())?;
68    let mut archive = tar::Archive::new(gz_dec);
69    for entry in archive.entries()? {
70      let file = entry?;
71      let path = file.path()?;
72      let parent = path.parent().unwrap_or_else(|| path::Path::new("/"));
73      if let Some(fname) = path.file_name() {
74        let wh_name = fname.to_string_lossy();
75        if wh_name == ".wh..wh..opq" {
76          //TODO(lucab): opaque whiteout, dir removal
77        } else if wh_name.starts_with(".wh.") {
78          let rel_parent = path::PathBuf::from("./".to_string() + &parent.to_string_lossy());
79
80          // Remove real file behind whiteout
81          let real_name = wh_name.trim_start_matches(".wh.");
82          let abs_real_path = target_dir.join(&rel_parent).join(real_name);
83          remove_whiteout(abs_real_path)?;
84
85          // Remove whiteout place-holder
86          let abs_wh_path = target_dir.join(&rel_parent).join(fname);
87          remove_whiteout(abs_wh_path)?;
88        };
89      }
90    }
91  }
92  Ok(())
93}
94
95// Whiteout files in archive may not exist on filesystem if they were
96// filtered out via filter_unpack.  If not found, that's ok and the
97// error is non-fatal.  Otherwise still return error for other
98// failures.
99fn remove_whiteout(path: path::PathBuf) -> io::Result<()> {
100  let res = fs::remove_dir_all(path);
101
102  match res {
103    Ok(_) => res,
104    Err(ref e) => match e.kind() {
105      io::ErrorKind::NotFound => Ok(()),
106      _ => res,
107    },
108  }
109}