use std::fs; use std::os::unix::fs::symlink; use std::os::unix::net::UnixListener; use alt_controller_image::archive::NativeTarWriter; use tar::Archive; use tempfile::tempdir; #[test] fn writes_deterministic_tar_with_files_directories_and_symlinks() { let fixture = tempdir().expect("fixture directory"); let rootfs = fixture.path().join("rootfs"); fs::create_dir_all(rootfs.join("etc/nested")).expect("create rootfs tree"); fs::write(rootfs.join("etc/nested/config"), "controller=true\n").expect("write config"); symlink("nested/config", rootfs.join("etc/config")).expect("create symlink"); fs::create_dir_all(rootfs.join(".host/private")).expect("create host metadata"); fs::write(rootfs.join(".host/private/key"), "excluded").expect("write host metadata"); fs::create_dir_all(rootfs.join("var/.fakedata")).expect("create fake metadata"); fs::write(rootfs.join("var/.fakedata/db"), "excluded").expect("write fake metadata"); let first = fixture.path().join("first.tar"); let second = fixture.path().join("second.tar"); NativeTarWriter::new() .write(&rootfs, &first) .expect("write first archive"); NativeTarWriter::new() .write(&rootfs, &second) .expect("write second archive"); assert_eq!( fs::read(&first).expect("read first"), fs::read(&second).expect("read second") ); let mut archive = Archive::new(fs::File::open(first).expect("open archive")); let entries = archive .entries() .expect("read archive entries") .map(|entry| { let entry = entry.expect("read entry"); let path = entry.path().expect("entry path").into_owned(); ( path, entry.header().entry_type(), entry.header().mtime().expect("mtime"), entry.header().uid().expect("uid"), entry.header().gid().expect("gid"), ) }) .collect::>(); assert_eq!( entries, vec![ ("etc".into(), tar::EntryType::Directory, 0, 0, 0), ("etc/config".into(), tar::EntryType::Symlink, 0, 0, 0), ("etc/nested".into(), tar::EntryType::Directory, 0, 0, 0), ("etc/nested/config".into(), tar::EntryType::Regular, 0, 0, 0), ("var".into(), tar::EntryType::Directory, 0, 0, 0), ] ); } #[test] fn preserves_hardlinks_in_the_archive() { use std::os::unix::fs::MetadataExt; let fixture = tempdir().expect("fixture directory"); let rootfs = fixture.path().join("rootfs"); fs::create_dir_all(rootfs.join("usr/bin")).expect("create rootfs tree"); let original = rootfs.join("usr/bin/controller"); let alias = rootfs.join("usr/bin/controller-link"); fs::write(&original, "controller\n").expect("write controller"); fs::hard_link(&original, &alias).expect("create hardlink"); assert_eq!( fs::metadata(&original).expect("stat original").ino(), fs::metadata(&alias).expect("stat alias").ino() ); let artifact = fixture.path().join("controller.tar"); NativeTarWriter::new() .write(&rootfs, &artifact) .expect("write archive"); let mut archive = Archive::new(fs::File::open(artifact).expect("open archive")); let entries = archive .entries() .expect("read archive entries") .map(|entry| { let entry = entry.expect("read entry"); ( entry.path().expect("entry path").into_owned(), entry.header().entry_type(), entry .link_name() .expect("entry link name") .map(|path| path.into_owned()), ) }) .collect::>(); assert_eq!( entries, vec![ ("usr".into(), tar::EntryType::Directory, None), ("usr/bin".into(), tar::EntryType::Directory, None), ("usr/bin/controller".into(), tar::EntryType::Regular, None), ( "usr/bin/controller-link".into(), tar::EntryType::Link, Some("usr/bin/controller".into()) ), ] ); } #[test] fn rejects_a_missing_rootfs() { let fixture = tempdir().expect("fixture directory"); let error = NativeTarWriter::new() .write( fixture.path().join("missing"), fixture.path().join("image.tar"), ) .expect_err("missing rootfs must fail"); assert!(error.to_string().contains("rootfs is not a directory")); } #[test] fn rejects_an_archive_output_inside_the_rootfs() { let fixture = tempdir().expect("fixture directory"); let rootfs = fixture.path().join("rootfs"); fs::create_dir_all(rootfs.join("etc")).expect("create rootfs"); fs::write(rootfs.join("etc/controller.conf"), "controller=true\n").expect("write rootfs file"); let artifact = rootfs.join("controller.tar"); let error = NativeTarWriter::new() .write(&rootfs, &artifact) .expect_err("archive output inside rootfs must be rejected"); assert!( error .to_string() .contains("archive output must not be inside rootfs") ); assert!(!artifact.exists()); assert!(!rootfs.join("controller.tar.partial").exists()); } #[test] fn refuses_to_replace_an_existing_archive() { let fixture = tempdir().expect("fixture directory"); let rootfs = fixture.path().join("rootfs"); fs::create_dir(&rootfs).expect("create rootfs"); fs::write(rootfs.join("controller.conf"), "controller=true\n").expect("write rootfs file"); let artifact = fixture.path().join("controller.tar"); fs::write(&artifact, "existing artifact").expect("write existing artifact"); let error = NativeTarWriter::new() .write(&rootfs, &artifact) .expect_err("an existing archive must not be replaced"); assert!(error.to_string().contains("archive output already exists")); assert_eq!( fs::read_to_string(&artifact).expect("read existing artifact"), "existing artifact" ); assert!(!fixture.path().join("controller.tar.partial").exists()); } #[test] fn removes_partial_output_when_an_unsupported_entry_stops_packaging() { let fixture = tempdir().expect("fixture directory"); let rootfs = fixture.path().join("rootfs"); fs::create_dir(&rootfs).expect("create rootfs"); let _socket = UnixListener::bind(rootfs.join("image-builder.sock")).expect("create socket"); let artifact = fixture.path().join("image.tar"); let error = NativeTarWriter::new() .write(&rootfs, &artifact) .expect_err("socket entries must be rejected"); assert!(error.to_string().contains("unsupported rootfs entry type")); assert!( !artifact.exists(), "failed packaging must not publish a partial archive" ); }