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
@@ -35,6 +35,7 @@ Rules:
|
|||||||
- `guest` - the guest init script (`crates/ecrou/src/guest.rs`)
|
- `guest` - the guest init script (`crates/ecrou/src/guest.rs`)
|
||||||
- `rootfs` - cache-aware preparation and persistence
|
- `rootfs` - cache-aware preparation and persistence
|
||||||
(`crates/ecrou/src/rootfs.rs`)
|
(`crates/ecrou/src/rootfs.rs`)
|
||||||
|
- `pack` - rootfs tarball packing (`crates/ecrou/src/pack.rs`)
|
||||||
- `distro` - distro definitions and mirrors (`crates/ecrou/src/distro.rs`)
|
- `distro` - distro definitions and mirrors (`crates/ecrou/src/distro.rs`)
|
||||||
- `download` - image download (`crates/ecrou/src/download.rs`)
|
- `download` - image download (`crates/ecrou/src/download.rs`)
|
||||||
- `extract` - rootfs extraction (`crates/ecrou/src/extract.rs`)
|
- `extract` - rootfs extraction (`crates/ecrou/src/extract.rs`)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub mod error;
|
|||||||
pub mod extract;
|
pub mod extract;
|
||||||
pub mod guest;
|
pub mod guest;
|
||||||
pub mod krun;
|
pub mod krun;
|
||||||
|
mod pack;
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
mod registry;
|
mod registry;
|
||||||
pub mod rootfs;
|
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
|
/// layer lands under its own key while the prepared scratch still
|
||||||
/// carries the base's own cache path.
|
/// carries the base's own cache path.
|
||||||
pub fn persist_to(&self, cache_path: &Path) -> Result<()> {
|
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(|| {
|
std::fs::write(sidecar(cache_path, PROVISIONED_SUFFIX), b"").with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to write provisioned marker for {}",
|
"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
|
/// Generate a cache filename based on the image source
|
||||||
fn generate_cache_filename(source: &ImageSource, arch: &str) -> String {
|
fn generate_cache_filename(source: &ImageSource, arch: &str) -> String {
|
||||||
match source {
|
match source {
|
||||||
@@ -763,7 +669,7 @@ mod tests {
|
|||||||
|
|
||||||
let cache = tempfile::tempdir().unwrap();
|
let cache = tempfile::tempdir().unwrap();
|
||||||
let dest = cache.path().join(dest_name);
|
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();
|
let out = tempfile::tempdir().unwrap();
|
||||||
crate::extract::extract_tarball(&dest, out.path(), &NoProgress).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");
|
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]
|
#[test]
|
||||||
fn a_local_archive_prepares_without_touching_the_cache() {
|
fn a_local_archive_prepares_without_touching_the_cache() {
|
||||||
// A rootfs tree, packed next to itself as ./rootfs.tar.gz, the
|
// 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::create_dir_all(&tree).unwrap();
|
||||||
std::fs::write(tree.join("etc-marker"), b"local").unwrap();
|
std::fs::write(tree.join("etc-marker"), b"local").unwrap();
|
||||||
let tarball = work.path().join("rootfs.tar.gz");
|
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_dir = tempfile::tempdir().unwrap();
|
||||||
let cache = RootfsCache::new(cache_dir.path().to_path_buf());
|
let cache = RootfsCache::new(cache_dir.path().to_path_buf());
|
||||||
|
|||||||
Reference in new issue
Block a user