mirror of
https://github.com/vhaudiquet/ecrou.git
synced 2026-11-12 22:22:44 +00:00
refactor(rootfs): split the tarball packer into pack.rs
rootfs.rs mixed the cache lifecycle with the tree-to-tarball writer that persisting relies on. The packer moves to pack.rs beside extract.rs: pack_rootfs becomes crate-internal, its partial-cleanup test moves with it. No behavior change.
This commit is contained in:
1 parent
aa95d05f41
commit
3e839cb5ca
4 files changed
+130
-116
No files matched your search
@@ -37,6 +37,7 @@ pub mod error;
|
||||
pub mod extract;
|
||||
pub mod guest;
|
||||
pub mod krun;
|
||||
mod pack;
|
||||
pub mod progress;
|
||||
mod registry;
|
||||
pub mod rootfs;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//! Packing a rootfs tree into the compressed tarball a cache entry
|
||||
//! stores, so a provisioned tree persists and later boots skip the
|
||||
//! download. The write lands in a unique partial file and renames
|
||||
//! atomically; compression follows the destination's extension.
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use std::path::Path;
|
||||
|
||||
/// Write a tarball of `rootfs` at `dest`, compressed according to dest's
|
||||
/// extension (.tar.gz, .tar.xz, .tar.zst, plain .tar), atomically via a
|
||||
/// unique partial file ([`crate::utils::partial_path`]).
|
||||
pub(crate) fn pack_rootfs(rootfs: &Path, dest: &Path) -> anyhow::Result<()> {
|
||||
// Packing the tree into a tarball placed inside it would recurse
|
||||
// forever: the walker keeps seeing the archive grow.
|
||||
let rootfs_canonical = rootfs
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Failed to resolve rootfs directory {}", rootfs.display()))?;
|
||||
if let Some(parent) = dest.parent() {
|
||||
if let Ok(parent_canonical) = parent.canonicalize() {
|
||||
if parent_canonical.starts_with(&rootfs_canonical) {
|
||||
return Err(anyhow!(
|
||||
"Cache entry {} must live outside the rootfs",
|
||||
dest.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let filename = dest.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
let partial = crate::utils::partial_path(dest);
|
||||
|
||||
let file = std::fs::File::create(&partial)
|
||||
.with_context(|| format!("Failed to create {}", partial.display()))?;
|
||||
|
||||
let result: anyhow::Result<()> = (|| {
|
||||
if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
|
||||
let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
|
||||
let encoder = write_rootfs_tar(rootfs, encoder)?;
|
||||
encoder.finish().context("Failed to finalise gzip stream")?;
|
||||
} else if filename.ends_with(".tar.xz") || filename.ends_with(".txz") {
|
||||
let encoder = xz2::write::XzEncoder::new(file, 6);
|
||||
let encoder = write_rootfs_tar(rootfs, encoder)?;
|
||||
encoder.finish().context("Failed to finalise xz stream")?;
|
||||
} else if filename.ends_with(".tar.zst") || filename.ends_with(".tar.zstd") {
|
||||
let encoder = zstd::stream::write::Encoder::new(file, 0)
|
||||
.context("Failed to create zstd encoder")?;
|
||||
let encoder = write_rootfs_tar(rootfs, encoder)?;
|
||||
encoder.finish().context("Failed to finalise zstd stream")?;
|
||||
} else {
|
||||
write_rootfs_tar(rootfs, file)?;
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
if result.is_err() {
|
||||
// Best-effort cleanup; ignore errors (file may not exist if creation failed).
|
||||
let _ = std::fs::remove_file(&partial);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::fs::rename(&partial, dest)
|
||||
.with_context(|| format!("Failed to move packed rootfs to {}", dest.display()))
|
||||
}
|
||||
|
||||
/// Write the rootfs tree into a tar archive streamed to `writer`, returning
|
||||
/// the writer so the caller can finalise the compression stream.
|
||||
fn write_rootfs_tar<W: std::io::Write>(rootfs: &Path, writer: W) -> anyhow::Result<W> {
|
||||
let mut builder = tar::Builder::new(writer);
|
||||
// Store symlinks as symlinks; following them would duplicate the target's
|
||||
// contents into the archive (and recurse into symlinked directories).
|
||||
builder.follow_symlinks(false);
|
||||
|
||||
let entries = std::fs::read_dir(rootfs)
|
||||
.with_context(|| format!("Failed to read {}", rootfs.display()))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry =
|
||||
entry.with_context(|| format!("Failed to read entry in {}", rootfs.display()))?;
|
||||
let path = entry.path();
|
||||
let name = entry.file_name();
|
||||
|
||||
// symlink_metadata: never follow symlinks — a top-level symlink to a
|
||||
// directory must be stored as a link, not recursed into.
|
||||
let meta = std::fs::symlink_metadata(&path)
|
||||
.with_context(|| format!("Failed to stat {}", path.display()))?;
|
||||
|
||||
if meta.is_dir() {
|
||||
builder
|
||||
.append_dir_all(&name, &path)
|
||||
.with_context(|| format!("Failed to archive {}", path.display()))?;
|
||||
} else {
|
||||
builder
|
||||
.append_path_with_name(&path, &name)
|
||||
.with_context(|| format!("Failed to archive {}", path.display()))?;
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
.into_inner()
|
||||
.context("Failed to finalise tar archive")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pack_failure_leaves_no_partial_file() {
|
||||
// A nonexistent rootfs must fail cleanly and clean up its
|
||||
// partial file, whatever unique name it carried.
|
||||
let cache = tempfile::tempdir().unwrap();
|
||||
let dest = cache.path().join("x.tar.gz");
|
||||
assert!(pack_rootfs(Path::new("/nonexistent-rootfs"), &dest).is_err());
|
||||
assert!(!dest.exists());
|
||||
let leftovers: Vec<String> = std::fs::read_dir(cache.path())
|
||||
.unwrap()
|
||||
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
|
||||
.filter(|name| name.contains("partial"))
|
||||
.collect();
|
||||
assert!(
|
||||
leftovers.is_empty(),
|
||||
"partial files left behind: {leftovers:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-116
@@ -385,7 +385,7 @@ impl PreparedRootfs {
|
||||
/// layer lands under its own key while the prepared scratch still
|
||||
/// carries the base's own cache path.
|
||||
pub fn persist_to(&self, cache_path: &Path) -> Result<()> {
|
||||
pack_rootfs(self.dir(), cache_path)?;
|
||||
crate::pack::pack_rootfs(self.dir(), cache_path)?;
|
||||
std::fs::write(sidecar(cache_path, PROVISIONED_SUFFIX), b"").with_context(|| {
|
||||
format!(
|
||||
"Failed to write provisioned marker for {}",
|
||||
@@ -501,100 +501,6 @@ fn drop_loopback_nameservers(config: &str) -> String {
|
||||
})
|
||||
}
|
||||
|
||||
/// Write a tarball of `rootfs` at `dest`, compressed according to dest's
|
||||
/// extension (.tar.gz, .tar.xz, .tar.zst, plain .tar), atomically via a
|
||||
/// unique partial file ([`crate::utils::partial_path`]).
|
||||
fn pack_rootfs(rootfs: &Path, dest: &Path) -> anyhow::Result<()> {
|
||||
// Packing the tree into a tarball placed inside it would recurse
|
||||
// forever: the walker keeps seeing the archive grow.
|
||||
let rootfs_canonical = rootfs
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Failed to resolve rootfs directory {}", rootfs.display()))?;
|
||||
if let Some(parent) = dest.parent() {
|
||||
if let Ok(parent_canonical) = parent.canonicalize() {
|
||||
if parent_canonical.starts_with(&rootfs_canonical) {
|
||||
return Err(anyhow!(
|
||||
"Cache entry {} must live outside the rootfs",
|
||||
dest.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let filename = dest.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
let partial = crate::utils::partial_path(dest);
|
||||
|
||||
let file = std::fs::File::create(&partial)
|
||||
.with_context(|| format!("Failed to create {}", partial.display()))?;
|
||||
|
||||
let result: anyhow::Result<()> = (|| {
|
||||
if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
|
||||
let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
|
||||
let encoder = write_rootfs_tar(rootfs, encoder)?;
|
||||
encoder.finish().context("Failed to finalise gzip stream")?;
|
||||
} else if filename.ends_with(".tar.xz") || filename.ends_with(".txz") {
|
||||
let encoder = xz2::write::XzEncoder::new(file, 6);
|
||||
let encoder = write_rootfs_tar(rootfs, encoder)?;
|
||||
encoder.finish().context("Failed to finalise xz stream")?;
|
||||
} else if filename.ends_with(".tar.zst") || filename.ends_with(".tar.zstd") {
|
||||
let encoder = zstd::stream::write::Encoder::new(file, 0)
|
||||
.context("Failed to create zstd encoder")?;
|
||||
let encoder = write_rootfs_tar(rootfs, encoder)?;
|
||||
encoder.finish().context("Failed to finalise zstd stream")?;
|
||||
} else {
|
||||
write_rootfs_tar(rootfs, file)?;
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
if result.is_err() {
|
||||
// Best-effort cleanup; ignore errors (file may not exist if creation failed).
|
||||
let _ = std::fs::remove_file(&partial);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::fs::rename(&partial, dest)
|
||||
.with_context(|| format!("Failed to move packed rootfs to {}", dest.display()))
|
||||
}
|
||||
|
||||
/// Write the rootfs tree into a tar archive streamed to `writer`, returning
|
||||
/// the writer so the caller can finalise the compression stream.
|
||||
fn write_rootfs_tar<W: std::io::Write>(rootfs: &Path, writer: W) -> anyhow::Result<W> {
|
||||
let mut builder = tar::Builder::new(writer);
|
||||
// Store symlinks as symlinks; following them would duplicate the target's
|
||||
// contents into the archive (and recurse into symlinked directories).
|
||||
builder.follow_symlinks(false);
|
||||
|
||||
let entries = std::fs::read_dir(rootfs)
|
||||
.with_context(|| format!("Failed to read {}", rootfs.display()))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry =
|
||||
entry.with_context(|| format!("Failed to read entry in {}", rootfs.display()))?;
|
||||
let path = entry.path();
|
||||
let name = entry.file_name();
|
||||
|
||||
// symlink_metadata: never follow symlinks — a top-level symlink to a
|
||||
// directory must be stored as a link, not recursed into.
|
||||
let meta = std::fs::symlink_metadata(&path)
|
||||
.with_context(|| format!("Failed to stat {}", path.display()))?;
|
||||
|
||||
if meta.is_dir() {
|
||||
builder
|
||||
.append_dir_all(&name, &path)
|
||||
.with_context(|| format!("Failed to archive {}", path.display()))?;
|
||||
} else {
|
||||
builder
|
||||
.append_path_with_name(&path, &name)
|
||||
.with_context(|| format!("Failed to archive {}", path.display()))?;
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
.into_inner()
|
||||
.context("Failed to finalise tar archive")
|
||||
}
|
||||
|
||||
/// Generate a cache filename based on the image source
|
||||
fn generate_cache_filename(source: &ImageSource, arch: &str) -> String {
|
||||
match source {
|
||||
@@ -763,7 +669,7 @@ mod tests {
|
||||
|
||||
let cache = tempfile::tempdir().unwrap();
|
||||
let dest = cache.path().join(dest_name);
|
||||
pack_rootfs(src.path(), &dest).unwrap();
|
||||
crate::pack::pack_rootfs(src.path(), &dest).unwrap();
|
||||
|
||||
let out = tempfile::tempdir().unwrap();
|
||||
crate::extract::extract_tarball(&dest, out.path(), &NoProgress).unwrap();
|
||||
@@ -826,25 +732,6 @@ mod tests {
|
||||
assert_eq!(std::fs::read(out.path().join("file.txt")).unwrap(), b"data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_failure_leaves_no_partial_file() {
|
||||
// A nonexistent rootfs must fail cleanly and clean up its
|
||||
// partial file, whatever unique name it carried.
|
||||
let cache = tempfile::tempdir().unwrap();
|
||||
let dest = cache.path().join("x.tar.gz");
|
||||
assert!(pack_rootfs(Path::new("/nonexistent-rootfs"), &dest).is_err());
|
||||
assert!(!dest.exists());
|
||||
let leftovers: Vec<String> = std::fs::read_dir(cache.path())
|
||||
.unwrap()
|
||||
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
|
||||
.filter(|name| name.contains("partial"))
|
||||
.collect();
|
||||
assert!(
|
||||
leftovers.is_empty(),
|
||||
"partial files left behind: {leftovers:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_local_archive_prepares_without_touching_the_cache() {
|
||||
// A rootfs tree, packed next to itself as ./rootfs.tar.gz, the
|
||||
@@ -854,7 +741,7 @@ mod tests {
|
||||
std::fs::create_dir_all(&tree).unwrap();
|
||||
std::fs::write(tree.join("etc-marker"), b"local").unwrap();
|
||||
let tarball = work.path().join("rootfs.tar.gz");
|
||||
pack_rootfs(&tree, &tarball).unwrap();
|
||||
crate::pack::pack_rootfs(&tree, &tarball).unwrap();
|
||||
|
||||
let cache_dir = tempfile::tempdir().unwrap();
|
||||
let cache = RootfsCache::new(cache_dir.path().to_path_buf());
|
||||
|
||||
Reference in new issue
Block a user