mod common;
use std::fs;
use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::path::Path;
use common::*;
use oci_image::{Digest, ImageLayer, LayerStore, Layout, Overlay, OverlayXattr, UnpackOptions};
fn opts() -> UnpackOptions {
UnpackOptions {
threads: 4,
layout: Layout::Overlay(OverlayXattr::User),
..UnpackOptions::default()
}
}
fn put(store: &LayerStore, entries: &[E]) -> ImageLayer {
let tar = tar_bytes(entries);
let gz = gzip(&tar);
let blob = Digest::of(&gz);
store.put_blob(&blob, &gz[..]).unwrap();
ImageLayer {
blob,
diff_id: Digest::of(&tar),
}
}
fn xattr(p: &Path, name: &str) -> Option<Vec<u8>> {
let c = std::ffi::CString::new(p.as_os_str().as_encoded_bytes()).unwrap();
let n = std::ffi::CString::new(name).unwrap();
let mut buf = [0u8; 16];
let r = unsafe { libc::lgetxattr(c.as_ptr(), n.as_ptr(), buf.as_mut_ptr().cast(), buf.len()) };
(r >= 0).then(|| buf[..r as usize].to_vec())
}
fn may_make_whiteouts(dir: &Path) -> bool {
let c = std::ffi::CString::new(dir.join("probe").as_os_str().as_encoded_bytes()).unwrap();
unsafe { libc::mknod(c.as_ptr(), libc::S_IFCHR | 0o600, 0) == 0 }
}
#[test]
fn whiteouts_become_overlay_whiteouts_and_opaque_dirs_get_the_xattr() {
let dir = tempfile::tempdir().unwrap();
if !may_make_whiteouts(dir.path()) {
use std::io::Write;
let _ = writeln!(
std::io::stderr(),
"SKIPPED whiteouts_become_overlay_whiteouts_and_opaque_dirs_get_the_xattr: this \
process may not mknod a 0/0 character device in {}; run it as root",
dir.path().display()
);
return;
}
let store = LayerStore::open(dir.path()).unwrap();
let base = put(
&store,
&[
E::Dir("etc/"),
E::File("etc/a", b"1"),
E::Dir("opt/"),
E::File("opt/x", b"x"),
],
);
let top = put(
&store,
&[
E::File("etc/.wh.a", b""),
E::File("opt/.wh..wh..opq", b""),
E::File("opt/y", b"y"),
],
);
let id = Digest::of(b"image");
let lowers = store
.add_image(&id, &[base.clone(), top.clone()], &opts())
.unwrap();
assert_eq!(lowers.len(), 2);
assert_eq!(lowers[0], store.layer_path(&base.diff_id));
assert_eq!(fs::read(lowers[0].join("etc/a")).unwrap(), b"1");
assert!(lowers[0].join("opt/x").exists());
let wh = fs::symlink_metadata(lowers[1].join("etc/a")).unwrap();
assert!(wh.file_type().is_char_device());
assert_eq!(wh.rdev(), 0);
assert_eq!(
xattr(&lowers[1].join("opt"), "user.overlay.opaque").as_deref(),
Some(&b"y"[..])
);
assert_eq!(fs::read(lowers[1].join("opt/y")).unwrap(), b"y");
assert!(!lowers[1].join("etc/.wh.a").exists());
assert!(!lowers[1].join("opt/.wh..wh..opq").exists());
}
#[test]
fn an_updated_image_unpacks_only_its_new_layers() {
let dir = tempfile::tempdir().unwrap();
let store = LayerStore::open(dir.path()).unwrap();
let l1 = put(&store, &[E::Dir("usr/"), E::File("usr/lib", b"base")]);
let l2 = put(&store, &[E::File("app", b"v1")]);
let l2b = put(&store, &[E::File("app", b"v2")]);
let (v1, v2) = (Digest::of(b"v1"), Digest::of(b"v2"));
store
.add_image(&v1, &[l1.clone(), l2.clone()], &opts())
.unwrap();
fs::write(store.layer_path(&l1.diff_id).join("marker"), b"").unwrap();
let missing = store.missing_layers(&[l1.clone(), l2b.clone()]);
assert_eq!(missing, vec![l2b.clone()]);
let lowers = store
.add_image(&v2, &[l1.clone(), l2b.clone()], &opts())
.unwrap();
assert!(
lowers[0].join("marker").exists(),
"the shared layer was not unpacked again"
);
assert_eq!(fs::read(lowers[1].join("app")).unwrap(), b"v2");
assert_eq!(store.refcount(&l1.diff_id).unwrap(), 2);
assert_eq!(store.refcount(&l2.diff_id).unwrap(), 1);
assert_eq!(store.image_layers(&v2).unwrap(), lowers);
}
#[test]
fn gc_removes_what_no_image_holds() {
let dir = tempfile::tempdir().unwrap();
let store = LayerStore::open(dir.path()).unwrap();
let l1 = put(&store, &[E::File("a", b"1")]);
let l2 = put(&store, &[E::File("b", b"2")]);
let l3 = put(&store, &[E::File("c", b"3")]);
let (v1, v2) = (Digest::of(b"v1"), Digest::of(b"v2"));
store
.add_image(&v1, &[l1.clone(), l2.clone()], &opts())
.unwrap();
store
.add_image(&v2, &[l1.clone(), l3.clone()], &opts())
.unwrap();
store.remove_image(&v1).unwrap();
assert_eq!(store.refcount(&l1.diff_id).unwrap(), 1);
let gc = store.gc().unwrap();
assert_eq!(gc.layers, 1);
assert!(!store.layer_path(&l2.diff_id).exists());
assert!(!store.has_blob(&l2.blob));
assert!(store.layer_path(&l1.diff_id).exists());
assert!(store.layer_path(&l3.diff_id).exists());
assert!(store.image_layers(&v1).is_err());
}
#[test]
fn a_layer_whose_content_is_not_its_diff_id_is_refused() {
let dir = tempfile::tempdir().unwrap();
let store = LayerStore::open(dir.path()).unwrap();
let mut l = put(&store, &[E::File("a", b"1")]);
l.diff_id = Digest::of(b"something else");
assert!(store
.add_image(&Digest::of(b"x"), &[l.clone()], &opts())
.is_err());
assert!(!store.layer_path(&l.diff_id).exists());
}
#[test]
fn overlays_stack_layers_top_first() {
let dir = tempfile::tempdir().unwrap();
let (a, b) = (dir.path().join("L1"), dir.path().join("L2"));
fs::create_dir_all(&a).unwrap();
fs::create_dir_all(&b).unwrap();
let ov = Overlay::for_layers(
&[a.clone(), b.clone()],
&dir.path().join("c"),
OverlayXattr::User,
)
.unwrap();
assert_eq!(ov.lower, vec![b.clone(), a.clone()]);
let data = ov.mount_data();
assert!(data.starts_with(&format!("lowerdir={}:{},", b.display(), a.display())));
assert!(data.ends_with(",userxattr"));
}
#[test]
fn a_layer_twice_in_one_image_is_stacked_once_at_its_top_position() {
let dir = tempfile::tempdir().unwrap();
let (e, a, b) = (
dir.path().join("E"),
dir.path().join("A"),
dir.path().join("B"),
);
for d in [&e, &a, &b] {
fs::create_dir_all(d).unwrap();
}
let layers = [a.clone(), e.clone(), b.clone(), e.clone()];
let ov = Overlay::for_layers(&layers, &dir.path().join("c"), OverlayXattr::Trusted).unwrap();
assert_eq!(ov.lower, vec![e, b, a]);
}
#[test]
fn a_pruned_blob_whose_layer_is_unpacked_is_still_held() {
let dir = tempfile::tempdir().unwrap();
let store = LayerStore::open(dir.path()).unwrap();
let base = put(&store, &[E::Dir("etc/"), E::File("etc/a", b"1")]);
let loose = put(&store, &[E::File("b", b"2")]);
assert!(store.holds_blob(&base.blob) && store.holds_blob(&loose.blob));
store
.add_image(&Digest::of(b"image"), std::slice::from_ref(&base), &opts())
.unwrap();
assert_eq!(store.prune_unpacked_blobs().unwrap(), 1);
assert!(!store.has_blob(&base.blob));
assert!(store.holds_blob(&base.blob), "the layer is unpacked");
assert!(store.holds_blob(&loose.blob), "the blob is there");
assert!(!store.holds_blob(&Digest::of(b"never seen")));
fs::remove_dir_all(store.layer_path(&base.diff_id)).unwrap();
assert!(!store.holds_blob(&base.blob));
}