//! Container image management for Microsandbox.
//!
//! This module provides functionality for managing container images from various
//! registries. It supports pulling images from Docker and Sandboxes.io registries,
//! handling image layers, and managing the local image cache.
use crate::{
management::db::{self, OCI_DB_MIGRATOR},
oci::{DockerRegistry, Ghcr, OciRegistryPull, Reference},
MicrosandboxError, MicrosandboxResult,
};
#[cfg(feature = "cli")]
use flate2::read::GzDecoder;
use futures::future;
#[cfg(feature = "cli")]
use indicatif::{ProgressBar, ProgressStyle};
#[cfg(feature = "cli")]
use microsandbox_utils::term::{self, MULTI_PROGRESS};
use microsandbox_utils::{env, EXTRACTED_LAYER_SUFFIX, LAYERS_SUBDIR, OCI_DB_FILENAME};
use sqlx::{Pool, Sqlite};
use std::ffi::CStr;
use std::io::Read;
#[cfg(feature = "cli")]
use std::io::Result as IoResult;
use std::path::{Path, PathBuf};
use tar::Archive;
use tempfile::tempdir;
use tokio::fs;
#[cfg(feature = "cli")]
use tokio::task::spawn_blocking;
//--------------------------------------------------------------------------------------------------
// Constants
//--------------------------------------------------------------------------------------------------
/// The domain name for the Docker registry.
const DOCKER_REGISTRY: &str = "docker.io";
/// The domain name for the Sandboxes registry.
const SANDBOXES_REGISTRY: &str = "sandboxes.io";
const GITHUB_CONTAINER_REGISTRY: &str = "ghcr.io";
#[cfg(feature = "cli")]
/// Spinner message used for extracting layers.
const EXTRACT_LAYERS_MSG: &str = "Extracting layers";
//--------------------------------------------------------------------------------------------------
// Functions
//--------------------------------------------------------------------------------------------------
/// Pulls an image or image group from a supported registry (Docker or Sandboxes.io).
///
/// This function handles pulling container images from different registries based on the provided
/// parameters. It supports both single image pulls and image group pulls (for Sandboxes.io registry only).
///
/// For Sandboxes.io registry:
/// - Library repository images are pulled from Docker registry for compatibility
/// - Other namespaces are also pulled from Docker registry with a warning about potential future changes
///
/// ## Arguments
///
/// * `name` - The reference to the image or image group to pull
/// * `image` - If true, indicates that a single image should be pulled
/// * `image_group` - If true, indicates that an image group should be pulled (Sandboxes.io only)
/// * `layer_path` - The path to store the layer files
///
/// ## Errors
///
/// Returns an error in the following cases:
/// * Both `image` and `image_group` are true (invalid combination)
/// * Image group pull is requested for a non-Sandboxes.io registry
/// * Unsupported registry is specified
/// * Registry-specific pull operations fail
///
/// # Examples
///
/// ```no_run
/// use microsandbox_core::management::image;
/// use microsandbox_core::oci::Reference;
/// use std::path::PathBuf;
///
/// # #[tokio::main]
/// # async fn main() -> anyhow::Result<()> {
/// // Pull a single image from Docker registry
/// image::pull("docker.io/library/ubuntu:latest".parse().unwrap(), true, false, None).await?;
///
/// // Pull an image from Sandboxes.io registry
/// image::pull("sandboxes.io/library/alpine:latest".parse().unwrap(), true, false, None).await?;
///
/// // Pull an image from the default registry (when no registry is specified in the reference)
/// image::pull("nginx:latest".parse().unwrap(), true, false, None).await?;
///
/// // You can set the OCI_REGISTRY_DOMAIN environment variable to specify your default registry
/// std::env::set_var("OCI_REGISTRY_DOMAIN", "docker.io");
/// image::pull("alpine:latest".parse().unwrap(), true, false, None).await?;
///
/// // Pull an image from Docker registry and store the layers in a custom directory
/// image::pull("docker.io/library/ubuntu:latest".parse().unwrap(), true, Some(PathBuf::from("/custom/path"))).await?;
/// # Ok(())
/// # }
/// ```
pub async fn pull(
name: Reference,
_image: bool,
layer_path: Option<PathBuf>,
) -> MicrosandboxResult<()> {
// Single image pull mode (default if both flags are false, or if image is true)
let registry = name.to_string().split('/').next().unwrap_or("").to_string();
let temp_download_dir = tempdir()?.into_path();
tracing::info!(
"temporary download directory: {}",
temp_download_dir.display()
);
if registry == DOCKER_REGISTRY {
pull_from_docker_registry(&name, &temp_download_dir, layer_path).await
} else if registry == SANDBOXES_REGISTRY {
pull_from_sandboxes_registry(&name, &temp_download_dir, layer_path).await
} else if registry == GITHUB_CONTAINER_REGISTRY {
pull_from_github_registry(&name, &temp_download_dir, layer_path).await
} else {
Err(MicrosandboxError::InvalidArgument(format!(
"Unsupported registry: {}",
registry
)))
}
}
/// Pulls a single image from the Github Container registry.
///
/// ## Arguments
///
/// * `image` - The reference to the Docker image to pull
/// * `download_dir` - The directory to download the image layers to
/// * `layer_path` - Optional custom path to store layers
///
/// ## Errors
///
/// Returns an error if:
/// * Failed to create temporary directories
/// * Failed to initialize Github Container registry client
/// * Failed to pull the image from Github Container registry
pub async fn pull_from_github_registry(
image: &Reference,
download_dir: impl AsRef<Path>,
layer_path: Option<PathBuf>,
) -> Result<(), MicrosandboxError> {
let download_dir = download_dir.as_ref();
let microsandbox_home_path = env::get_microsandbox_home_path();
let db_path = microsandbox_home_path.join(OCI_DB_FILENAME);
// Use custom layer_path if specified, otherwise use default microsandbox layers directory
let layers_dir = match layer_path {
Some(path) => path,
None => microsandbox_home_path.join(LAYERS_SUBDIR),
};
// Create layers directory if it doesn't exist
fs::create_dir_all(&layers_dir).await?;
let github_registry = Ghcr::new(download_dir, &db_path).await?;
// Get or create a connection pool to the database
let pool = db::get_or_create_pool(&db_path, &OCI_DB_MIGRATOR).await?;
// Check if we need to pull the image
if check_image_layers(&pool, image, &layers_dir).await? {
tracing::info!("image {} and all its layers exist, skipping pull", image);
return Ok(());
}
github_registry
.pull_image(image.get_repository(), image.get_selector().clone())
.await?;
// Find and extract layers in parallel
let layer_paths = collect_layer_files(download_dir).await?;
#[cfg(feature = "cli")]
let extract_layers_sp = term::create_spinner(
EXTRACT_LAYERS_MSG.to_string(),
None,
Some(layer_paths.len() as u64),
);
let extraction_futures: Vec<_> = layer_paths
.into_iter()
.map(|path| {
let layers_dir = layers_dir.clone();
#[cfg(feature = "cli")]
let extract_layers_sp = extract_layers_sp.clone();
async move {
let result = extract_layer(path, &layers_dir).await;
#[cfg(feature = "cli")]
extract_layers_sp.inc(1);
result
}
})
.collect();
// Wait for all extractions to complete
for result in future::join_all(extraction_futures).await {
result?;
}
#[cfg(feature = "cli")]
extract_layers_sp.finish();
Ok(())
}
/// Pulls a single image from the Docker registry.
///
/// ## Arguments
///
/// * `image` - The reference to the Docker image to pull
/// * `download_dir` - The directory to download the image layers to
/// * `layer_path` - Optional custom path to store layers
///
/// ## Errors
///
/// Returns an error if:
/// * Failed to create temporary directories
/// * Failed to initialize Docker registry client
/// * Failed to pull the image from Docker registry
pub async fn pull_from_docker_registry(
image: &Reference,
download_dir: impl AsRef<Path>,
layer_path: Option<PathBuf>,
) -> MicrosandboxResult<()> {
let download_dir = download_dir.as_ref();
let microsandbox_home_path = env::get_microsandbox_home_path();
let db_path = microsandbox_home_path.join(OCI_DB_FILENAME);
// Use custom layer_path if specified, otherwise use default microsandbox layers directory
let layers_dir = match layer_path {
Some(path) => path,
None => microsandbox_home_path.join(LAYERS_SUBDIR),
};
// Create layers directory if it doesn't exist
fs::create_dir_all(&layers_dir).await?;
let docker_registry = DockerRegistry::new(download_dir, &db_path).await?;
// Get or create a connection pool to the database
let pool = db::get_or_create_pool(&db_path, &OCI_DB_MIGRATOR).await?;
// Check if we need to pull the image
if check_image_layers(&pool, image, &layers_dir).await? {
tracing::info!("image {} and all its layers exist, skipping pull", image);
return Ok(());
}
docker_registry
.pull_image(image.get_repository(), image.get_selector().clone())
.await?;
// Find and extract layers in parallel
let layer_paths = collect_layer_files(download_dir).await?;
#[cfg(feature = "cli")]
let extract_layers_sp = term::create_spinner(
EXTRACT_LAYERS_MSG.to_string(),
None,
Some(layer_paths.len() as u64),
);
let extraction_futures: Vec<_> = layer_paths
.into_iter()
.map(|path| {
let layers_dir = layers_dir.clone();
#[cfg(feature = "cli")]
let extract_layers_sp = extract_layers_sp.clone();
async move {
let result = extract_layer(path, &layers_dir).await;
#[cfg(feature = "cli")]
extract_layers_sp.inc(1);
result
}
})
.collect();
// Wait for all extractions to complete
for result in future::join_all(extraction_futures).await {
result?;
}
#[cfg(feature = "cli")]
extract_layers_sp.finish();
Ok(())
}
/// Pulls a single image from the Sandboxes.io registry.
///
/// For library repository images, this function delegates to `pull_from_docker_registry` for compatibility.
/// For other namespaces, it also uses Docker registry but displays a warning about potential future changes.
///
/// ## Arguments
///
/// * `image` - The reference to the Sandboxes.io image to pull
/// * `download_dir` - The directory to download the image layers to
/// * `layer_path` - Optional custom path to store layers
///
/// ## Errors
///
/// Returns an error if the underlying Docker registry pull fails
pub async fn pull_from_sandboxes_registry(
image: &Reference,
download_dir: impl AsRef<Path>,
layer_path: Option<PathBuf>,
) -> MicrosandboxResult<()> {
// Check if this is a library repository image
let repository = image.get_repository();
// Create a Docker reference string using the original repository but with docker.io registry
// Format: docker.io/repository:tag
let docker_ref_str = format!(
"{}/{}",
DOCKER_REGISTRY,
image
.to_string()
.split('/')
.skip(1)
.collect::<Vec<&str>>()
.join("/")
);
let docker_reference: Reference = docker_ref_str.parse()?;
if repository.starts_with("library/") {
tracing::info!("pulling library image from Docker registry for compatibility");
} else {
tracing::warn!(
"Non-library namespace image requested from Sandboxes registry: {}",
repository
);
tracing::warn!(
"Currently using Docker registry for compatibility, but namespace mappings may change in the future"
);
tracing::info!(
"To ensure consistent behavior, consider setting OCI_REGISTRY_DOMAIN=docker.io if you want to use Docker registry consistently"
);
}
pull_from_docker_registry(&docker_reference, download_dir, layer_path).await
}
/// Pulls an image group from the Sandboxes.io registry.
///
/// ## Arguments
///
/// * `group` - The reference to the image group to pull
/// ## Errors
///
/// Returns an error if:
/// * Sandboxes registry image group pull is not implemented
pub async fn pull_group_from_sandboxes_registry(_group: &Reference) -> MicrosandboxResult<()> {
return Err(MicrosandboxError::NotImplemented(
"Sandboxes registry image group pull is not implemented".to_string(),
));
}
//--------------------------------------------------------------------------------------------------
// Functions: Helpers
//--------------------------------------------------------------------------------------------------
/// Checks if all layers for an image exist in both the database and the layers directory.
///
/// ## Arguments
///
/// * `pool` - The database connection pool
/// * `image` - The reference to the image to check
/// * `layers_dir` - The directory where layers should be stored
///
/// ## Returns
///
/// Returns Ok(true) if all layers exist and are valid, Ok(false) if any layers are missing
/// or invalid. Any errors during the check process will return Ok(false) with a warning log.
async fn check_image_layers(
pool: &Pool<Sqlite>,
image: &Reference,
layers_dir: impl AsRef<Path>,
) -> MicrosandboxResult<bool> {
let layers_dir = layers_dir.as_ref();
// Check if the image exists in the database
match db::image_exists(pool, &image.to_string()).await {
Ok(true) => {
// Image exists, get all layer digests for this image
match db::get_image_layer_digests(pool, &image.to_string()).await {
Ok(layer_digests) => {
tracing::info!("layer_digests: {:?}", layer_digests);
if layer_digests.is_empty() {
tracing::warn!("no layers found for image {}", image);
return Ok(false);
}
// Check if all layers exist in the layers directory
for digest in &layer_digests {
let layer_path =
layers_dir.join(format!("{}.{}", digest, EXTRACTED_LAYER_SUFFIX));
if !layer_path.exists() {
tracing::warn!("layer {} not found in layers directory", digest);
return Ok(false);
}
// Also check that the layer directory actually has content
let mut read_dir = fs::read_dir(&layer_path).await?;
let dir_empty = read_dir.next_entry().await?.is_none();
if dir_empty {
tracing::warn!("layer {} exists but is empty", digest);
}
tracing::info!("layer {} found in layers directory", digest);
}
// Get the layers from database to verify database records exist for all digests
let db_layers = db::get_layers_by_digest(pool, &layer_digests).await?;
if db_layers.len() < layer_digests.len() {
tracing::warn!(
"some layers for image {} exist on disk but missing in db",
image
);
return Ok(false);
}
tracing::info!("all layers for image {} exist and are valid", image);
Ok(true)
}
Err(e) => {
tracing::warn!("error checking layer digests: {}, will pull image", e);
Ok(false)
}
}
}
Ok(false) => {
tracing::warn!("image {} does not exist in db, will pull image", image);
Ok(false)
}
Err(e) => {
tracing::warn!("error checking image existence: {}, will pull image", e);
Ok(false)
}
}
}
/// Helper function to get full mode with file type bits
fn get_full_mode(entry_type: &tar::EntryType, permission_bits: u32) -> u32 {
let file_type_bits = if entry_type.is_file() {
libc::S_IFREG as u32
} else if entry_type.is_dir() {
libc::S_IFDIR as u32
} else if entry_type.is_symlink() {
libc::S_IFLNK as u32
} else if entry_type.is_block_special() {
libc::S_IFBLK as u32
} else if entry_type.is_character_special() {
libc::S_IFCHR as u32
} else if entry_type.is_fifo() {
libc::S_IFIFO as u32
} else {
0 // Unknown type
};
file_type_bits | permission_bits
}
/// Helper function to set xattr with stat information
fn set_stat_xattr(
path: &Path,
xattr_name: &CStr,
uid: u64,
gid: u64,
mode: u32,
) -> Result<(), MicrosandboxError> {
use std::ffi::CString;
let stat_data = format!("{}:{}:0{:o}", uid, gid, mode);
let path_cstring = CString::new(path.as_os_str().as_encoded_bytes())
.map_err(|e| MicrosandboxError::LayerExtraction(format!("Invalid path: {:?}", e)))?;
let result = unsafe {
#[cfg(target_os = "macos")]
{
libc::setxattr(
path_cstring.as_ptr(),
xattr_name.as_ptr(),
stat_data.as_ptr() as *const libc::c_void,
stat_data.len(),
0, // position parameter for macOS
0, // options
)
}
#[cfg(target_os = "linux")]
{
libc::setxattr(
path_cstring.as_ptr(),
xattr_name.as_ptr(),
stat_data.as_ptr() as *const libc::c_void,
stat_data.len(),
0, // flags
)
}
};
if result != 0 {
let errno = std::io::Error::last_os_error();
if errno.raw_os_error() == Some(libc::ENOTSUP) {
tracing::warn!(
"Filesystem does not support xattrs for {}, continuing without stat shadowing",
path.display()
);
} else {
return Err(MicrosandboxError::LayerExtraction(format!(
"Failed to set xattr on {}: {}",
path.display(),
errno
)));
}
}
Ok(())
}
/// Extracts a layer from the downloaded tar.gz file into an extracted directory.
/// The extracted directory will be named as <layer-name>.extracted
/// Custom extraction function that modifies file ownership during extraction
fn extract_tar_with_ownership_override<R: Read>(
archive: &mut Archive<R>,
extract_dir: &Path,
) -> MicrosandboxResult<()> {
use std::ffi::CString;
use std::os::unix::fs::PermissionsExt;
// Cache the xattr name to avoid repeated allocations
let xattr_name = CString::new("user.containers.override_stat")
.map_err(|e| MicrosandboxError::LayerExtraction(format!("Invalid attr name: {:?}", e)))?;
// Structure to store hard link information
struct HardLinkInfo {
link_path: PathBuf,
target_path: PathBuf,
uid: u64,
gid: u64,
mode: u32,
}
// Store hard links to process after all regular files are extracted
let mut hard_links = Vec::new();
for entry in archive.entries()? {
let mut entry =
entry.map_err(|e| MicrosandboxError::LayerExtraction(format!("{:?}", e)))?;
let path = entry
.path()
.map_err(|e| MicrosandboxError::LayerExtraction(format!("{:?}", e)))?;
let full_path = extract_dir.join(&path);
// Get the original metadata from the tar entry
let original_uid = entry.header().uid()?;
let original_gid = entry.header().gid()?;
let permission_bits = entry.header().mode()?;
// Check the entry type
let entry_type = entry.header().entry_type();
let is_symlink = entry_type.is_symlink();
let is_hard_link = entry_type.is_hard_link();
// Calculate the full mode with file type bits
let original_mode = get_full_mode(&entry_type, permission_bits);
// Handle hard links separately - collect them for processing after all files are extracted
if is_hard_link {
if let Ok(Some(link_name)) = entry.link_name() {
hard_links.push(HardLinkInfo {
link_path: full_path.clone(),
target_path: extract_dir.join(&link_name),
uid: original_uid,
gid: original_gid,
mode: original_mode,
});
}
continue; // Skip to next entry
}
// Extract the entry (regular files, directories, symlinks)
entry
.unpack(&full_path)
.map_err(|e| MicrosandboxError::LayerExtraction(format!("{:?}", e)))?;
// Skip all operations for symlinks
if is_symlink {
tracing::trace!(
"Extracted symlink {} with original uid:gid:mode {}:{}:{:o}",
full_path.display(),
original_uid,
original_gid,
original_mode
);
continue;
}
// For regular files and directories, handle permissions and xattrs
let metadata = std::fs::metadata(&full_path)?;
let is_dir = metadata.is_dir();
let current_mode = metadata.permissions().mode();
let current_permission_bits = current_mode & 0o7777; // Extract only permission bits
// Calculate the final desired permissions
let desired_permission_bits = if is_dir {
// For directories, ensure at least u+rwx (0o700)
current_permission_bits | 0o700
} else {
// For files, ensure at least u+rw (0o600)
current_permission_bits | 0o600
};
// If we need to modify permissions, do it once
if current_permission_bits != desired_permission_bits {
let mut permissions = metadata.permissions();
permissions.set_mode(desired_permission_bits);
std::fs::set_permissions(&full_path, permissions)?;
}
// Store original uid/gid/mode in xattrs
set_stat_xattr(
&full_path,
&xattr_name,
original_uid,
original_gid,
original_mode,
)?;
tracing::trace!(
"Extracted {} with original uid:gid:mode {}:{}:{:o}, stored in xattr",
full_path.display(),
original_uid,
original_gid,
original_mode
);
}
// Second pass: process hard links after all regular files are extracted
for link_info in hard_links {
// Create the hard link
match std::fs::hard_link(&link_info.target_path, &link_info.link_path) {
Ok(_) => {
// Hard link created successfully, now handle xattrs
// Get metadata and ensure proper permissions
let metadata = match std::fs::metadata(&link_info.link_path) {
Ok(m) => m,
Err(e) => {
tracing::warn!(
"Failed to get metadata for hard link {}: {}",
link_info.link_path.display(),
e
);
continue;
}
};
let current_mode = metadata.permissions().mode();
let current_permission_bits = current_mode & 0o7777; // Extract only permission bits
let desired_permission_bits = current_permission_bits | 0o600; // Ensure at least u+rw
// Set permissions if needed
if current_permission_bits != desired_permission_bits {
let mut permissions = metadata.permissions();
permissions.set_mode(desired_permission_bits);
if let Err(e) = std::fs::set_permissions(&link_info.link_path, permissions) {
tracing::warn!(
"Failed to set permissions for hard link {}: {}",
link_info.link_path.display(),
e
);
continue;
}
}
// Store original uid/gid/mode in xattrs
if let Err(e) = set_stat_xattr(
&link_info.link_path,
&xattr_name,
link_info.uid,
link_info.gid,
link_info.mode,
) {
// For hard links, we just warn on xattr errors instead of failing
tracing::warn!(
"Failed to set xattr on hard link {}: {}",
link_info.link_path.display(),
e
);
}
tracing::trace!(
"Created hard link {} -> {} with original uid:gid:mode {}:{}:{:o}",
link_info.link_path.display(),
link_info.target_path.display(),
link_info.uid,
link_info.gid,
link_info.mode
);
}
Err(e) => {
tracing::warn!(
"Failed to create hard link {} -> {}: {}",
link_info.link_path.display(),
link_info.target_path.display(),
e
);
}
}
}
Ok(())
}
async fn extract_layer(
layer_path: impl AsRef<Path>,
extract_base_dir: impl AsRef<Path>,
) -> MicrosandboxResult<()> {
let layer_path = layer_path.as_ref();
let file_name = layer_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| MicrosandboxError::LayerHandling {
source: std::io::Error::new(std::io::ErrorKind::NotFound, "invalid layer file name"),
layer: layer_path.display().to_string(),
})?;
// Create the extraction directory with name <layer-name>.extracted
let extract_dir = extract_base_dir
.as_ref()
.join(format!("{}.{}", file_name, EXTRACTED_LAYER_SUFFIX));
// Check if the layer is already extracted
if extract_dir.exists() {
// Check if the directory has content (not empty)
let mut read_dir =
fs::read_dir(&extract_dir)
.await
.map_err(|e| MicrosandboxError::LayerHandling {
source: e,
layer: file_name.to_string(),
})?;
if read_dir.next_entry().await?.is_some() {
tracing::info!(
"layer {} already extracted at {}, skipping extraction",
file_name,
extract_dir.display()
);
return Ok(());
}
}
fs::create_dir_all(&extract_dir)
.await
.map_err(|e| MicrosandboxError::LayerHandling {
source: e,
layer: file_name.to_string(),
})?;
tracing::info!(
"extracting layer {} to {}",
file_name,
extract_dir.display()
);
#[cfg(feature = "cli")]
struct ProgressReader<R> {
inner: R,
bar: ProgressBar,
}
#[cfg(feature = "cli")]
impl<R: Read> Read for ProgressReader<R> {
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
let n = self.inner.read(buf)?;
if n > 0 {
self.bar.inc(n as u64);
}
Ok(n)
}
}
#[cfg(feature = "cli")]
{
let total_bytes = fs::metadata(layer_path).await?.len();
let pb = MULTI_PROGRESS.add(ProgressBar::new(total_bytes));
pb.set_style(
ProgressStyle::with_template(
"{prefix:.bold.dim} {bar:40.green/green.dim} {bytes:.bold}/{total_bytes:.dim}",
)
.unwrap()
.progress_chars("=+-"),
);
let digest_short = if let Some(rest) = file_name.strip_prefix("sha256:") {
&rest[..8.min(rest.len())]
} else {
&file_name[..8.min(file_name.len())]
};
pb.set_prefix(format!("{}", digest_short));
let layer_path_clone = layer_path.to_path_buf();
let extract_dir_clone = extract_dir.clone();
let pb_clone = pb.clone();
spawn_blocking(move || -> MicrosandboxResult<()> {
let file = std::fs::File::open(&layer_path_clone)?;
let reader = ProgressReader {
inner: file,
bar: pb_clone.clone(),
};
let decoder = GzDecoder::new(reader);
let mut archive = Archive::new(decoder);
extract_tar_with_ownership_override(&mut archive, &extract_dir_clone)?;
Ok(())
})
.await
.map_err(|e| MicrosandboxError::LayerExtraction(format!("{:?}", e)))??;
pb.finish_and_clear();
}
#[cfg(not(feature = "cli"))]
{
use flate2::read::GzDecoder;
let file =
std::fs::File::open(layer_path).map_err(|e| MicrosandboxError::LayerHandling {
source: e,
layer: file_name.to_string(),
})?;
let decoder = GzDecoder::new(file);
let mut archive = Archive::new(decoder);
extract_tar_with_ownership_override(&mut archive, &extract_dir)?;
}
tracing::info!(
"successfully extracted layer {} to {}",
file_name,
extract_dir.display()
);
Ok(())
}
/// Collects all layer files in the given directory that start with "sha256:".
async fn collect_layer_files(dir: impl AsRef<Path>) -> MicrosandboxResult<Vec<PathBuf>> {
let mut layer_paths = Vec::new();
let mut read_dir = fs::read_dir(dir).await?;
while let Ok(Some(entry)) = read_dir.next_entry().await {
let path = entry.path();
if path.is_file() {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.starts_with("sha256:") {
layer_paths.push(path.clone());
}
}
}
}
tracing::info!("found {} layers to extract", layer_paths.len());
Ok(layer_paths)
}
//--------------------------------------------------------------------------------------------------
// Tests
//--------------------------------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test_log::test(tokio::test)]
#[ignore = "makes network requests to Docker registry to pull an image"]
async fn test_image_pull_from_docker_registry() -> MicrosandboxResult<()> {
// Create temporary directories for test
let temp_dir = TempDir::new()?;
let microsandbox_home = temp_dir.path().join("microsandbox_home");
let download_dir = temp_dir.path().join("download");
fs::create_dir_all(µsandbox_home).await?;
fs::create_dir_all(&download_dir).await?;
// Set up test environment
std::env::set_var("MICROSANDBOX_HOME", microsandbox_home.to_str().unwrap());
// Create test image reference (using a small image for faster tests)
let image_ref: Reference = "docker.io/library/nginx:stable-alpine".parse().unwrap();
// Call the function under test
pull_from_docker_registry(&image_ref, &download_dir, None).await?;
// Initialize database connection for verification
let db_path = microsandbox_home.join(OCI_DB_FILENAME);
let pool = db::get_or_create_pool(&db_path, &OCI_DB_MIGRATOR).await?;
// Verify image exists in database
let image_exists = db::image_exists(&pool, &image_ref.to_string()).await?;
assert!(image_exists, "Image should exist in database");
// Verify layers directory exists and contains extracted layers
let layers_dir = microsandbox_home.join(LAYERS_SUBDIR);
assert!(layers_dir.exists(), "Layers directory should exist");
// Verify extracted layer directories exist
let mut entries = fs::read_dir(&layers_dir).await?;
let mut found_extracted_layers = false;
while let Some(entry) = entries.next_entry().await? {
if entry
.file_name()
.to_string_lossy()
.ends_with(EXTRACTED_LAYER_SUFFIX)
{
found_extracted_layers = true;
assert!(
entry.path().is_dir(),
"Extracted layer path should be a directory"
);
}
}
assert!(
found_extracted_layers,
"Should have found extracted layer directories"
);
// Verify nginx files exist in the extracted layers
helper::verify_nginx_files(&layers_dir).await?;
Ok(())
}
}
#[cfg(test)]
mod helper {
use super::*;
/// Helper function to verify that all expected nginx files exist in the extracted layers
pub(super) async fn verify_nginx_files(layers_dir: impl AsRef<Path>) -> MicrosandboxResult<()> {
let mut found_nginx_conf = false;
let mut found_default_conf = false;
let mut found_nginx_binary = false;
// Check each extracted layer directory for nginx files
let mut entries = fs::read_dir(layers_dir).await?;
while let Some(entry) = entries.next_entry().await? {
if !entry
.file_name()
.to_string_lossy()
.ends_with(EXTRACTED_LAYER_SUFFIX)
{
continue;
}
let layer_path = entry.path();
tracing::info!("checking layer: {}", layer_path.display());
// Check for nginx.conf
let nginx_conf = layer_path.join("etc").join("nginx").join("nginx.conf");
if nginx_conf.exists() {
found_nginx_conf = true;
tracing::info!("found nginx.conf at {}", nginx_conf.display());
}
// Check for default.conf
let default_conf = layer_path
.join("etc")
.join("nginx")
.join("conf.d")
.join("default.conf");
if default_conf.exists() {
found_default_conf = true;
tracing::info!("found default.conf at {}", default_conf.display());
}
// Check for nginx binary
let nginx_binary = layer_path.join("usr").join("sbin").join("nginx");
if nginx_binary.exists() {
found_nginx_binary = true;
tracing::info!("found nginx binary at {}", nginx_binary.display());
}
// If we found all files, we can stop checking
if found_nginx_conf && found_default_conf && found_nginx_binary {
break;
}
}
// Assert that we found all the expected files
assert!(
found_nginx_conf,
"nginx.conf should exist in one of the layers"
);
assert!(
found_default_conf,
"default.conf should exist in one of the layers"
);
assert!(
found_nginx_binary,
"nginx binary should exist in one of the layers"
);
Ok(())
}
}