This commit is contained in:
2026-06-17 17:42:56 +02:00
parent 49343e5811
commit 1d2031b3ca
5 changed files with 123 additions and 87 deletions
+23 -11
View File
@@ -137,7 +137,11 @@ fn extract_oci_layer(layer_path: &Path, dest: &Path) -> Result<()> {
"Extracting OCI layer", "Extracting OCI layer",
) )
} else if layer_name.ends_with(".tar.xz") || layer_name.ends_with(".txz") { } else if layer_name.ends_with(".tar.xz") || layer_name.ends_with(".txz") {
extract_with_progress(tar::Archive::new(xz2::read::XzDecoder::new(reader)), dest, "Extracting OCI layer") extract_with_progress(
tar::Archive::new(xz2::read::XzDecoder::new(reader)),
dest,
"Extracting OCI layer",
)
} else if layer_name.ends_with(".tar.zst") || layer_name.ends_with(".tar.zstd") { } else if layer_name.ends_with(".tar.zst") || layer_name.ends_with(".tar.zstd") {
extract_with_progress( extract_with_progress(
tar::Archive::new(zstd::stream::read::Decoder::new(reader)?), tar::Archive::new(zstd::stream::read::Decoder::new(reader)?),
@@ -217,26 +221,33 @@ fn extract_tar<R: std::io::Read>(reader: R, dest: &Path) -> Result<()> {
} }
/// Extract tar archive with progress bar, handling whiteout files for OCI layers /// Extract tar archive with progress bar, handling whiteout files for OCI layers
fn extract_with_progress<R: std::io::Read>(mut archive: tar::Archive<R>, dest: &Path, msg: &str) -> Result<()> { fn extract_with_progress<R: std::io::Read>(
mut archive: tar::Archive<R>,
dest: &Path,
msg: &str,
) -> Result<()> {
let pb = ProgressBar::new_spinner(); let pb = ProgressBar::new_spinner();
pb.set_style(ProgressStyle::default_spinner() pb.set_style(
.template("{spinner:.green} {msg} ({pos} files)") ProgressStyle::default_spinner()
.unwrap()); .template("{spinner:.green} {msg} ({pos} files)")
.unwrap(),
);
pb.set_message(msg.to_string()); pb.set_message(msg.to_string());
archive.set_preserve_permissions(true); archive.set_preserve_permissions(true);
archive.set_preserve_ownerships(false); archive.set_preserve_ownerships(false);
archive.set_unpack_xattrs(false); archive.set_unpack_xattrs(false);
let entries = archive.entries() let entries = archive
.entries()
.context("Failed to read archive entries")?; .context("Failed to read archive entries")?;
for entry in entries { for entry in entries {
let mut entry = entry.context("Failed to read archive entry")?; let mut entry = entry.context("Failed to read archive entry")?;
// Clone the path before any mutable borrow of entry // Clone the path before any mutable borrow of entry
let path = entry.path().context("Invalid tar entry path")?.into_owned(); let path = entry.path().context("Invalid tar entry path")?.into_owned();
let filename = path let filename = path
.file_name() .file_name()
.map(|n| n.to_string_lossy().into_owned()) .map(|n| n.to_string_lossy().into_owned())
@@ -269,10 +280,11 @@ fn extract_with_progress<R: std::io::Read>(mut archive: tar::Archive<R>, dest: &
} }
// Do not extract the .wh.* marker itself // Do not extract the .wh.* marker itself
} else { } else {
entry.unpack_in(dest) entry
.unpack_in(dest)
.with_context(|| format!("Failed to extract {}", path.display()))?; .with_context(|| format!("Failed to extract {}", path.display()))?;
} }
pb.inc(1); pb.inc(1);
} }
+2 -6
View File
@@ -1,5 +1,4 @@
mod chroot; mod chroot;
mod utils;
mod cli; mod cli;
mod config; mod config;
mod distro; mod distro;
@@ -9,6 +8,7 @@ mod mount;
mod namespace; mod namespace;
mod qemu; mod qemu;
mod qemu_vm; mod qemu_vm;
mod utils;
mod verbose; mod verbose;
/// Print to stderr only when --verbose / -v is active. /// Print to stderr only when --verbose / -v is active.
@@ -160,11 +160,7 @@ fn main() -> Result<()> {
} }
/// Run in namespace/chroot mode /// Run in namespace/chroot mode
fn namespace_mode( fn namespace_mode(args: Args, rootfs: std::path::PathBuf, config: Config) -> Result<()> {
args: Args,
rootfs: std::path::PathBuf,
config: Config,
) -> Result<()> {
// Check user namespace availability // Check user namespace availability
namespace::check_user_namespace()?; namespace::check_user_namespace()?;
+14 -8
View File
@@ -435,7 +435,7 @@ mod tests {
fn test_hostname_format() { fn test_hostname_format() {
// Test that hostname generation produces valid format // Test that hostname generation produces valid format
use std::collections::HashSet; use std::collections::HashSet;
let mut hostnames = HashSet::new(); let mut hostnames = HashSet::new();
for _ in 0..100 { for _ in 0..100 {
let mut hasher = std::collections::hash_map::DefaultHasher::new(); let mut hasher = std::collections::hash_map::DefaultHasher::new();
@@ -455,24 +455,30 @@ mod tests {
.collect(); .collect();
let hostname = format!("ecr-test-{}", random_suffix); let hostname = format!("ecr-test-{}", random_suffix);
// Verify hostname format // Verify hostname format
assert!(hostname.starts_with("ecr-test-")); assert!(hostname.starts_with("ecr-test-"));
assert!(hostname.len() > 9); // "ecr-test-" + at least 1 char assert!(hostname.len() > 9); // "ecr-test-" + at least 1 char
assert!(hostname.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')); assert!(hostname
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'));
hostnames.insert(hostname); hostnames.insert(hostname);
} }
// With 100 iterations and good entropy, we should get many unique hostnames // With 100 iterations and good entropy, we should get many unique hostnames
assert!(hostnames.len() > 50, "Expected many unique hostnames, got {}", hostnames.len()); assert!(
hostnames.len() > 50,
"Expected many unique hostnames, got {}",
hostnames.len()
);
} }
#[test] #[test]
fn test_set_hostname_uniqueness() { fn test_set_hostname_uniqueness() {
// Verify that rapid consecutive calls produce different hostnames // Verify that rapid consecutive calls produce different hostnames
use std::collections::HashSet; use std::collections::HashSet;
let mut hostnames = Vec::new(); let mut hostnames = Vec::new();
for _ in 0..10 { for _ in 0..10 {
// Simulate the hostname generation logic // Simulate the hostname generation logic
@@ -494,7 +500,7 @@ mod tests {
hostnames.push(format!("ecr-test-{}", random_suffix)); hostnames.push(format!("ecr-test-{}", random_suffix));
} }
let unique: HashSet<_> = hostnames.iter().collect(); let unique: HashSet<_> = hostnames.iter().collect();
// Most hostnames should be unique (high entropy) // Most hostnames should be unique (high entropy)
assert!(unique.len() >= 8, "Expected mostly unique hostnames"); assert!(unique.len() >= 8, "Expected mostly unique hostnames");
+83 -57
View File
@@ -55,7 +55,10 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
Ubuntu/Debian: sudo apt install qemu-system-{}\n\ Ubuntu/Debian: sudo apt install qemu-system-{}\n\
Arch: sudo pacman -S qemu-system-{}\n\ Arch: sudo pacman -S qemu-system-{}\n\
Alpine: sudo apk add qemu-system-{}", Alpine: sudo apk add qemu-system-{}",
qemu_bin, get_arch_package_suffix(&config.arch), get_arch_package_suffix(&config.arch), get_arch_package_suffix(&config.arch) qemu_bin,
get_arch_package_suffix(&config.arch),
get_arch_package_suffix(&config.arch),
get_arch_package_suffix(&config.arch)
))?; ))?;
// Check if we can use KVM acceleration // Check if we can use KVM acceleration
@@ -68,16 +71,20 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
// Detect the best available shell in the rootfs // Detect the best available shell in the rootfs
let shell = crate::utils::detect_shell(&config.rootfs_path); let shell = crate::utils::detect_shell(&config.rootfs_path);
// Generate a unique hostname like "ecr-vm-a1b2c3" // Generate a unique hostname like "ecr-vm-a1b2c3"
// Use VM_HOSTNAME_SUFFIX_BITS constant for entropy // Use VM_HOSTNAME_SUFFIX_BITS constant for entropy
let hostname_suffix = format!("{:x}", (std::process::id() as u64) let hostname_suffix = format!(
.wrapping_mul(std::time::SystemTime::now() "{:x}",
.duration_since(std::time::UNIX_EPOCH) (std::process::id() as u64).wrapping_mul(
.unwrap_or_default() std::time::SystemTime::now()
.as_nanos() as u64) % crate::utils::VM_HOSTNAME_SUFFIX_BITS); .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64
) % crate::utils::VM_HOSTNAME_SUFFIX_BITS
);
let hostname = format!("ecr-vm-{}", hostname_suffix); let hostname = format!("ecr-vm-{}", hostname_suffix);
// Build kernel command line // Build kernel command line
// For initramfs boot, use rdinit= instead of init= // For initramfs boot, use rdinit= instead of init=
// No root= needed as initramfs becomes the rootfs // No root= needed as initramfs becomes the rootfs
@@ -168,15 +175,15 @@ fn get_arch_package_suffix(arch: &str) -> &'static str {
/// Check if KVM acceleration can be used for the target architecture /// Check if KVM acceleration can be used for the target architecture
fn can_use_kvm(target_arch: &str) -> bool { fn can_use_kvm(target_arch: &str) -> bool {
use crate::utils::Arch; use crate::utils::Arch;
// Normalize both to canonical form (uname -m style) and compare // Normalize both to canonical form (uname -m style) and compare
let host_arch = crate::utils::get_host_arch(); let host_arch = crate::utils::get_host_arch();
let target_enum = Arch::from_str(target_arch); let target_enum = Arch::from_str(target_arch);
if host_arch != target_enum { if host_arch != target_enum {
return false; return false;
} }
// Check if /dev/kvm exists and is accessible (read+write required for VM execution) // Check if /dev/kvm exists and is accessible (read+write required for VM execution)
match std::fs::OpenOptions::new() match std::fs::OpenOptions::new()
.read(true) .read(true)
@@ -195,7 +202,7 @@ fn can_use_kvm(target_arch: &str) -> bool {
/// Check if KVM capabilities are actually functional /// Check if KVM capabilities are actually functional
fn check_kvm_capabilities() -> bool { fn check_kvm_capabilities() -> bool {
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
// Try to open /dev/kvm and check KVM_GET_API_VERSION // Try to open /dev/kvm and check KVM_GET_API_VERSION
match std::fs::OpenOptions::new() match std::fs::OpenOptions::new()
.read(true) .read(true)
@@ -227,9 +234,11 @@ fn create_initramfs(rootfs: &Path) -> Result<PathBuf> {
// Create progress bar // Create progress bar
let pb = ProgressBar::new_spinner(); let pb = ProgressBar::new_spinner();
pb.set_style(ProgressStyle::default_spinner() pb.set_style(
.template("{spinner:.green} {msg} ({pos} files)") ProgressStyle::default_spinner()
.unwrap()); .template("{spinner:.green} {msg} ({pos} files)")
.unwrap(),
);
pb.set_message("Creating initramfs..."); pb.set_message("Creating initramfs...");
// Create the cpio archive with progress // Create the cpio archive with progress
@@ -238,12 +247,15 @@ fn create_initramfs(rootfs: &Path) -> Result<PathBuf> {
let file_count = pb.position(); let file_count = pb.position();
// Write directly to file (no compression) // Write directly to file (no compression)
std::fs::write(&initramfs_path, &cpio_data) std::fs::write(&initramfs_path, &cpio_data).context("Failed to write initramfs file")?;
.context("Failed to write initramfs file")?;
// Finish progress bar // Finish progress bar
pb.finish_and_clear(); pb.finish_and_clear();
veprintln!("Initramfs created: {} bytes, {} files", total_bytes, file_count); veprintln!(
"Initramfs created: {} bytes, {} files",
total_bytes,
file_count
);
Ok(initramfs_path) Ok(initramfs_path)
} }
@@ -251,41 +263,41 @@ fn create_initramfs(rootfs: &Path) -> Result<PathBuf> {
/// Create a newc-format cpio archive from a directory using the cpio crate /// Create a newc-format cpio archive from a directory using the cpio crate
fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result<Vec<u8>> { fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result<Vec<u8>> {
let mut archive = Vec::new(); let mut archive = Vec::new();
// Track seen inodes to handle hard links properly // Track seen inodes to handle hard links properly
let mut seen_inodes = std::collections::HashMap::new(); let mut seen_inodes = std::collections::HashMap::new();
// Collect all entries with their data // Collect all entries with their data
let entries = collect_entries(rootfs, rootfs, pb, &mut seen_inodes)?; let entries = collect_entries(rootfs, rootfs, pb, &mut seen_inodes)?;
// Collect entry names for checking existence later // Collect entry names for checking existence later
let entry_names: Vec<&str> = entries.iter().map(|(n, _, _, _, _)| n.as_str()).collect(); let entry_names: Vec<&str> = entries.iter().map(|(n, _, _, _, _)| n.as_str()).collect();
pb.set_message("Writing initramfs..."); pb.set_message("Writing initramfs...");
// Write each entry using the cpio crate // Write each entry using the cpio crate
for (name, mode, mtime, nlink, data) in &entries { for (name, mode, mtime, nlink, data) in &entries {
let file_size = data.len() as u32; let file_size = data.len() as u32;
let builder = NewcBuilder::new(name) let builder = NewcBuilder::new(name)
.mode(*mode) .mode(*mode)
.uid(0) .uid(0)
.gid(0) .gid(0)
.nlink(*nlink) .nlink(*nlink)
.mtime(*mtime); .mtime(*mtime);
// Write header and get a writer // Write header and get a writer
let mut writer = builder.write(&mut archive, file_size); let mut writer = builder.write(&mut archive, file_size);
// Write the file content // Write the file content
writer.write_all(data) writer
.write_all(data)
.context("Failed to write file content to cpio archive")?; .context("Failed to write file content to cpio archive")?;
// Finish this entry (returns the underlying writer) // Finish this entry (returns the underlying writer)
writer.finish() writer.finish().context("Failed to finish cpio entry")?;
.context("Failed to finish cpio entry")?;
} }
// Add essential device nodes for serial console // Add essential device nodes for serial console
// These are character devices (mode 0o020xxx) // These are character devices (mode 0o020xxx)
let device_nodes = [ let device_nodes = [
@@ -296,13 +308,13 @@ fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result<Vec<u8>> {
// /dev/tty - controlling terminal (major 5, minor 0) // /dev/tty - controlling terminal (major 5, minor 0)
("dev/tty", 0o020666, 5, 0), ("dev/tty", 0o020666, 5, 0),
]; ];
for (name, mode, major, minor) in device_nodes { for (name, mode, major, minor) in device_nodes {
// Check if this device node already exists in the archive // Check if this device node already exists in the archive
if entry_names.contains(&name) { if entry_names.contains(&name) {
continue; continue;
} }
let builder = NewcBuilder::new(name) let builder = NewcBuilder::new(name)
.mode(mode) .mode(mode)
.uid(0) .uid(0)
@@ -311,13 +323,14 @@ fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result<Vec<u8>> {
.mtime(0) .mtime(0)
.rdev_major(major) .rdev_major(major)
.rdev_minor(minor); .rdev_minor(minor);
// Device nodes have zero size // Device nodes have zero size
let writer = builder.write(&mut archive, 0); let writer = builder.write(&mut archive, 0);
writer.finish() writer
.finish()
.context("Failed to finish device node entry")?; .context("Failed to finish device node entry")?;
} }
// Add the /init script that will be run as PID 1 // Add the /init script that will be run as PID 1
// This script handles hostname setup, shell execution, and poweroff on exit // This script handles hostname setup, shell execution, and poweroff on exit
// Uses /proc/sysrq-trigger for poweroff since poweroff command may not be available // Uses /proc/sysrq-trigger for poweroff since poweroff command may not be available
@@ -369,16 +382,17 @@ fi
.gid(0) .gid(0)
.nlink(1) .nlink(1)
.mtime(0); .mtime(0);
let mut init_writer = init_builder.write(&mut archive, init_data.len() as u32); let mut init_writer = init_builder.write(&mut archive, init_data.len() as u32);
init_writer.write_all(init_data) init_writer
.write_all(init_data)
.context("Failed to write init script to cpio archive")?; .context("Failed to write init script to cpio archive")?;
init_writer.finish() init_writer
.finish()
.context("Failed to finish init script entry")?; .context("Failed to finish init script entry")?;
// Write the trailer (takes ownership and returns the writer) // Write the trailer (takes ownership and returns the writer)
archive = newc::trailer(archive) archive = newc::trailer(archive).context("Failed to write cpio trailer")?;
.context("Failed to write cpio trailer")?;
Ok(archive) Ok(archive)
} }
@@ -393,33 +407,41 @@ fn collect_entries(
) -> Result<Vec<CpioEntry>> { ) -> Result<Vec<CpioEntry>> {
let mut entries = Vec::new(); let mut entries = Vec::new();
let mut total_data: u64 = 0; let mut total_data: u64 = 0;
// Read directory entries // Read directory entries
let dir_entries: Vec<_> = match std::fs::read_dir(current) { let dir_entries: Vec<_> = match std::fs::read_dir(current) {
Ok(entries) => entries.collect::<std::result::Result<_, _>>()?, Ok(entries) => entries.collect::<std::result::Result<_, _>>()?,
Err(e) => { Err(e) => {
veprintln!("Warning: cannot read directory {}: {}", current.display(), e); veprintln!(
"Warning: cannot read directory {}: {}",
current.display(),
e
);
return Ok(entries); return Ok(entries);
} }
}; };
for entry in dir_entries { for entry in dir_entries {
let path = entry.path(); let path = entry.path();
// Get metadata // Get metadata
let metadata = match std::fs::symlink_metadata(&path) { let metadata = match std::fs::symlink_metadata(&path) {
Ok(m) => m, Ok(m) => m,
Err(e) => { Err(e) => {
veprintln!("Warning: skipping {} due to metadata error: {}", path.display(), e); veprintln!(
"Warning: skipping {} due to metadata error: {}",
path.display(),
e
);
continue; continue;
} }
}; };
// Increment progress counter // Increment progress counter
pb.inc(1); pb.inc(1);
let file_type = metadata.file_type(); let file_type = metadata.file_type();
// Determine mode (file type + permissions from filesystem) // Determine mode (file type + permissions from filesystem)
let mode = if file_type.is_dir() { let mode = if file_type.is_dir() {
// Directory: preserve permissions, ensure at least rwx for owner // Directory: preserve permissions, ensure at least rwx for owner
@@ -432,16 +454,16 @@ fn collect_entries(
} else { } else {
continue; // Skip other types (sockets, fifos, etc.) continue; // Skip other types (sockets, fifos, etc.)
}; };
// Build the entry name (relative path from base) // Build the entry name (relative path from base)
let relative = path.strip_prefix(base).unwrap(); let relative = path.strip_prefix(base).unwrap();
let entry_name = relative.to_string_lossy().into_owned(); let entry_name = relative.to_string_lossy().into_owned();
// Handle hard links: only store data for first occurrence // Handle hard links: only store data for first occurrence
let (data, nlink) = if file_type.is_file() && metadata.nlink() > 1 { let (data, nlink) = if file_type.is_file() && metadata.nlink() > 1 {
// This file has multiple hard links - check if we've seen it before // This file has multiple hard links - check if we've seen it before
let inode_key = (metadata.dev(), metadata.ino()); let inode_key = (metadata.dev(), metadata.ino());
if let Some(_first_path) = seen_inodes.get(&inode_key) { if let Some(_first_path) = seen_inodes.get(&inode_key) {
// We've seen this inode before - create a hard link entry with no data // We've seen this inode before - create a hard link entry with no data
(Vec::new(), metadata.nlink() as u32) (Vec::new(), metadata.nlink() as u32)
@@ -479,10 +501,10 @@ fn collect_entries(
// Directory // Directory
(Vec::new(), 2) (Vec::new(), 2)
}; };
total_data += data.len() as u64; total_data += data.len() as u64;
entries.push((entry_name, mode, metadata.mtime() as u32, nlink, data)); entries.push((entry_name, mode, metadata.mtime() as u32, nlink, data));
// Recurse into directories // Recurse into directories
if file_type.is_dir() { if file_type.is_dir() {
let mut sub_entries = collect_entries(base, &path, pb, seen_inodes)?; let mut sub_entries = collect_entries(base, &path, pb, seen_inodes)?;
@@ -492,10 +514,14 @@ fn collect_entries(
entries.append(&mut sub_entries); entries.append(&mut sub_entries);
} }
} }
// Only print summary for the root directory // Only print summary for the root directory
if current == base { if current == base {
veprintln!("Collected {} entries, {} bytes total data", entries.len(), total_data); veprintln!(
"Collected {} entries, {} bytes total data",
entries.len(),
total_data
);
} }
Ok(entries) Ok(entries)
} }
+1 -5
View File
@@ -196,11 +196,7 @@ pub fn validate_memory_string(s: &str) -> Result<()> {
let suffix = s.chars().last().unwrap(); let suffix = s.chars().last().unwrap();
let has_suffix = suffix.is_ascii_alphabetic(); let has_suffix = suffix.is_ascii_alphabetic();
let numeric_part = if has_suffix { let numeric_part = if has_suffix { &s[..s.len() - 1] } else { s };
&s[..s.len() - 1]
} else {
s
};
// Check for negative numbers // Check for negative numbers
if numeric_part.starts_with('-') { if numeric_part.starts_with('-') {