diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 25d4db2df..721bb6c2c 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -1047,7 +1047,7 @@ async fn upgrade( crate::deploy::pull_unified(repo, imgref, None, opts.quiet, prog.clone(), storage) .await? } else { - crate::deploy::pull(repo, imgref, None, opts.quiet, prog.clone()).await? + crate::deploy::pull(repo, imgref, None, opts.quiet, prog.clone(), Some(storage)).await? }; let staged_digest = staged_image.map(|s| s.digest().expect("valid digest in status")); let fetched_digest = &fetched.manifest_digest; @@ -1210,7 +1210,7 @@ async fn switch_ostree( let fetched = if use_unified { crate::deploy::pull_unified(repo, &target, None, opts.quiet, prog.clone(), storage).await? } else { - crate::deploy::pull(repo, &target, None, opts.quiet, prog.clone()).await? + crate::deploy::pull(repo, &target, None, opts.quiet, prog.clone(), Some(storage)).await? }; if !opts.retain { @@ -1347,7 +1347,15 @@ async fn edit_ostree( return crate::deploy::rollback(storage).await; } - let fetched = crate::deploy::pull(repo, new_spec.image, None, opts.quiet, prog.clone()).await?; + let fetched = crate::deploy::pull( + repo, + new_spec.image, + None, + opts.quiet, + prog.clone(), + Some(storage), + ) + .await?; // TODO gc old layers here diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index 766f7acae..c3588941b 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -380,6 +380,7 @@ pub(crate) async fn prepare_for_pull( repo: &ostree::Repo, imgref: &ImageReference, target_imgref: Option<&OstreeImageReference>, + _store: Option<&Storage>, ) -> Result { let imgref_canonicalized = imgref.clone().canonicalize()?; tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}"); @@ -388,6 +389,13 @@ pub(crate) async fn prepare_for_pull( if let Some(target) = target_imgref { imp.set_target(target); } + + // Set storage root for direct access to the layer content when using containers-storage. + // For regular pulls, images come from the host's default container storage. + if ostree_imgref.imgref.transport == ostree_container::Transport::ContainerStorage { + let storage_path = format!("{}/storage", crate::podman::CONTAINER_STORAGE); + imp.set_storage_root(storage_path); + } let prep = match imp.prepare().await? { PrepareResult::AlreadyPresent(c) => { println!("No changes in {imgref:#} => {}", c.manifest_digest); @@ -484,6 +492,8 @@ pub(crate) async fn prepare_for_pull_unified( // Use the preparation flow with the custom config let mut imp = new_importer_with_config(repo, &ostree_imgref, config).await?; + // Set storage root for direct access to the layer content + imp.set_storage_root(&storage_path); if let Some(target) = target_imgref { imp.set_target(target); } @@ -642,8 +652,9 @@ pub(crate) async fn pull( target_imgref: Option<&OstreeImageReference>, quiet: bool, prog: ProgressWriter, + store: Option<&Storage>, ) -> Result> { - match prepare_for_pull(repo, imgref, target_imgref).await? { + match prepare_for_pull(repo, imgref, target_imgref, store).await? { PreparedPullResult::AlreadyPresent(existing) => { // Log that the image was already present (Debug level since it's not actionable) const IMAGE_ALREADY_PRESENT_ID: &str = "5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9"; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 600371e63..ddbfeecac 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -969,7 +969,13 @@ async fn install_container( ) .await? } else { - prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref)).await? + prepare_for_pull( + repo, + &spec_imgref, + Some(&state.target_imgref), + Some(storage), + ) + .await? }; let pulled_image = match prepared { @@ -2513,6 +2519,7 @@ pub(crate) async fn install_reset(opts: InstallResetOpts) -> Result<()> { None, opts.quiet, prog.clone(), + Some(sysroot), ) .await?; (fetched, new_spec) diff --git a/crates/ostree-ext/src/container/store.rs b/crates/ostree-ext/src/container/store.rs index 0ef4d9803..39aa4a81a 100644 --- a/crates/ostree-ext/src/container/store.rs +++ b/crates/ostree-ext/src/container/store.rs @@ -187,6 +187,8 @@ pub struct ImageImporter { offline: bool, /// If true, we have ostree v2024.3 or newer. ostree_v2024_3: bool, + /// Optional containers-storage root path for direct access to the layer content + storage_root: Option, layer_progress: Option>, layer_byte_progress: Option>>, @@ -530,6 +532,7 @@ impl ImageImporter { disable_gc: false, require_bootable: false, offline: false, + storage_root: None, imgref: imgref.clone(), layer_progress: None, layer_byte_progress: None, @@ -568,6 +571,13 @@ impl ImageImporter { self.disable_gc = true; } + /// Set the containers-storage root path for direct access to the layer content. + /// When set, layers will be imported directly from the layer diff directory + /// instead of being streamed through the proxy. + pub fn set_storage_root(&mut self, path: impl Into) { + self.storage_root = Some(path.into()); + } + /// Determine if there is a new manifest, and if so return its digest. /// This will also serialize the new manifest and configuration into /// metadata associated with the image, so that invocations of `[query_cached]` @@ -1120,16 +1130,6 @@ impl ImageImporter { p.send(ImportProgress::DerivedLayerStarted(layer.layer.clone())) .await?; } - let (blob, driver, media_type) = super::unencapsulate::fetch_layer( - &proxy, - &import.proxy_img, - &import.manifest, - &layer.layer, - self.layer_byte_progress.as_ref(), - des_layers.as_ref(), - self.imgref.imgref.transport, - ) - .await?; // An important aspect of this is that we SELinux label the derived layers using // the base policy. let opts = crate::tar::WriteTarOptions { @@ -1138,16 +1138,61 @@ impl ImageImporter { allow_nonusr: root_is_transient, retain_var: self.ostree_v2024_3, }; - let r = crate::tar::write_tar( - &self.repo, - blob, - media_type, - layer.ostree_ref.as_str(), - Some(opts), + + let layer_index = import + .manifest + .layers() + .iter() + .position(|x| x == &layer.layer) + .ok_or_else(|| { + anyhow!("Layer {} not found in manifest", layer.layer.digest()) + })?; + tracing::debug!( + "Processing layer {}: digest={}, ostree_ref={}, transport={:?}", + layer_index, + layer.layer.digest(), + layer.ostree_ref, + self.imgref.imgref.transport ); - let r = super::unencapsulate::join_fetch(r, driver) - .await - .with_context(|| format!("Parsing layer blob {}", layer.layer.digest()))?; + let layer_diff_path = super::unencapsulate::try_get_layer_diff_path( + self.storage_root.as_deref(), + des_layers.as_ref(), + layer_index, + self.imgref.imgref.transport, + )?; + + let r = if let Some(ref path) = layer_diff_path { + tracing::info!("Importing layer {} from filesystem: {}", layer_index, path); + Self::import_layer_from_filesystem(&self.repo, path, &layer.ostree_ref, &opts) + .await? + } else { + // Fall back to blob access + let (blob, driver, media_type) = super::unencapsulate::fetch_layer( + &proxy, + &import.proxy_img, + &import.manifest, + &layer.layer, + self.layer_byte_progress.as_ref(), + des_layers.as_ref(), + self.imgref.imgref.transport, + ) + .await?; + tracing::debug!( + "Importing layer {} from tar stream, media_type={:?}", + layer_index, + media_type + ); + let r = crate::tar::write_tar( + &self.repo, + blob, + media_type, + layer.ostree_ref.as_str(), + Some(opts), + ); + super::unencapsulate::join_fetch(r, driver) + .await + .with_context(|| format!("Parsing layer blob {}", layer.layer.digest()))? + }; tracing::debug!("Imported layer: {}", r.commit.as_str()); layer_commits.push(r.commit); let filtered_owned = HashMap::from_iter(r.filtered.clone()); @@ -1233,6 +1278,55 @@ impl ImageImporter { state.filtered_files = layer_filtered_content; Ok(state) } + + /// Import a layer directly from filesystem path instead of tar stream. + /// This directly walks the filesystem and writes content objects to OSTree, + /// applying path transformations (e.g., /etc -> /usr/etc). + async fn import_layer_from_filesystem( + repo: &ostree::Repo, + layer_path: &Utf8Path, + target_ref: &str, + options: &crate::tar::WriteTarOptions, + ) -> Result { + tracing::info!( + "import_layer_from_filesystem: layer_path={}, target_ref={}", + layer_path, + target_ref + ); + + let config = crate::filesystem::FilesystemFilterConfig::from_tar_options(options); + let repo = repo.clone(); + let layer_path = layer_path.to_owned(); + let target_ref = target_ref.to_string(); + + // Run the synchronous import in a blocking task + let result = { + let repo = repo.clone(); + crate::tokio_util::spawn_blocking_flatten(move || { + crate::filesystem::import_filesystem_to_ostree(&repo, &layer_path, &config) + }) + .await? + }; + + // Cache the layer by setting the ref (matching write_tar behavior) + { + let target_ref = target_ref.clone(); + let commit = result.commit.clone(); + crate::tokio_util::spawn_blocking_flatten(move || { + repo.set_ref_immediate(None, &target_ref, Some(&commit), gio::Cancellable::NONE)?; + Ok::<_, anyhow::Error>(()) + }) + .await?; + } + + tracing::info!( + "Successfully imported layer from filesystem: commit={}, ref={}", + result.commit, + target_ref + ); + + Ok(result) + } } /// List all images stored diff --git a/crates/ostree-ext/src/container/unencapsulate.rs b/crates/ostree-ext/src/container/unencapsulate.rs index c386576d0..29494dff5 100644 --- a/crates/ostree-ext/src/container/unencapsulate.rs +++ b/crates/ostree-ext/src/container/unencapsulate.rs @@ -34,6 +34,8 @@ use crate::container::store::LayerProgress; use super::*; +use anyhow::Context as _; +use camino::{Utf8Path, Utf8PathBuf}; use containers_image_proxy::{ImageProxy, OpenedImage}; use fn_error_context::context; use futures_util::{Future, FutureExt}; @@ -185,6 +187,108 @@ pub async fn unencapsulate(repo: &ostree::Repo, imgref: &OstreeImageReference) - importer.unencapsulate().await } +/// Try to get the diff path for a layer in containers-storage. +/// Returns Some(path) if the layer diff directory is available, None to use blob access. +/// +/// The `storage_root` parameter specifies the containers-storage root directory. +/// If None, direct filesystem access is not attempted. +pub(crate) fn try_get_layer_diff_path( + storage_root: Option<&Utf8Path>, + layer_info: Option<&Vec>, + layer_index: usize, + transport_src: Transport, +) -> Result> { + match (transport_src, storage_root) { + (Transport::ContainerStorage, Some(storage_root)) => { + get_layer_diff_path(storage_root, layer_info, layer_index) + } + _ => Ok(None), + } +} + +/// Entry in the overlay-layers/layers.json file +#[derive(serde::Deserialize, Debug)] +struct OverlayLayerEntry { + /// The directory name under overlay/ + id: String, + /// Uncompressed diff digest (e.g., "sha256:...") + #[serde(rename = "diff-digest")] + diff_digest: Option, + /// Compressed diff digest (e.g., "sha256:...") + #[serde(rename = "compressed-diff-digest")] + compressed_diff_digest: Option, +} + +/// Look up the overlay directory ID from layers.json given a digest +fn lookup_layer_id_from_digest(storage_root: &Utf8Path, digest: &str) -> Result> { + let layers_json_path = storage_root.join("overlay-layers/layers.json"); + + let file = match std::fs::File::open(&layers_json_path) { + Ok(f) => f, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Ok(None); + } + Err(e) => return Err(anyhow::Error::new(e).context("Failed to open layers.json")), + }; + + let reader = std::io::BufReader::new(file); + let layers: Vec = + serde_json::from_reader(reader).context("Failed to parse layers.json")?; + + // Search for matching digest in both diff-digest and compressed-diff-digest + for layer in &layers { + let matches = layer.diff_digest.as_deref() == Some(digest) + || layer.compressed_diff_digest.as_deref() == Some(digest); + if matches { + return Ok(Some(layer.id.clone())); + } + } + + Ok(None) +} + +/// Get the diff directory path for a layer in containers-storage +fn get_layer_diff_path( + storage_root: &Utf8Path, + layer_info: Option<&Vec>, + layer_index: usize, +) -> Result> { + let Some(info) = layer_info else { + return Ok(None); + }; + + let Some(layer) = info.get(layer_index) else { + return Ok(None); + }; + + // Get the digest string (includes "sha256:" prefix) + let digest_str = layer.digest.to_string(); + + // Look up the layer ID from layers.json + let Some(layer_id) = lookup_layer_id_from_digest(storage_root, &digest_str)? else { + return Ok(None); + }; + + // Check if this is a composefs layer (not supported yet) + let composefs_blob_path = storage_root.join(format!( + "overlay/{}/composefs-data/composefs.blob", + layer_id + )); + if composefs_blob_path.exists() { + return Ok(None); + } + + // Construct the layer diff path: $STORAGE/overlay/$LAYER_ID/diff + let layer_diff_path = storage_root.join(format!("overlay/{}/diff", layer_id)); + + // Check if the layer diff directory exists and is a directory + if !layer_diff_path.is_dir() { + return Ok(None); + } + + Ok(Some(layer_diff_path)) +} + /// A wrapper for [`get_blob`] which fetches a layer and decompresses it. pub(crate) async fn fetch_layer<'a>( proxy: &'a ImageProxy, diff --git a/crates/ostree-ext/src/filesystem/filter.rs b/crates/ostree-ext/src/filesystem/filter.rs new file mode 100644 index 000000000..c9b57cae1 --- /dev/null +++ b/crates/ostree-ext/src/filesystem/filter.rs @@ -0,0 +1,313 @@ +use anyhow::{Context, Result, anyhow}; +use camino::Utf8Path; +use ostree::gio; +use ostree::glib; +use ostree::prelude::*; +use std::collections::BTreeMap; +use std::os::fd::AsRawFd; +use std::os::unix::fs::{FileTypeExt, MetadataExt}; +use std::path::Path; + +use crate::tar::EXCLUDED_TOPLEVEL_PATHS; + +/// Configuration for filesystem filtering operations +#[derive(Debug, Clone, Default)] +pub struct FilesystemFilterConfig { + /// Allow content outside /usr (for transient rootfs with overlayfs) + pub allow_nonusr: bool, + /// Remap /var to /usr/share/factory/var + pub remap_factory_var: bool, + /// Base commit to get SELinux policy from (unused for layer import - + /// SELinux labeling happens during the merge step) + pub base: Option, +} + +impl FilesystemFilterConfig { + /// Create from WriteTarOptions + pub fn from_tar_options(opts: &crate::tar::WriteTarOptions) -> Self { + Self { + allow_nonusr: opts.allow_nonusr, + remap_factory_var: !opts.retain_var, + base: opts.base.clone(), + } + } +} + +/// Describes how a toplevel directory should be handled during import +enum ToplevelAction { + /// Import directly using write_dfd_to_mtree at the given destination path + Import { dest_path: &'static [&'static str] }, + /// Skip this directory entirely + Skip, +} + +/// Determine how to handle a toplevel directory entry +fn classify_toplevel(name: &str, config: &FilesystemFilterConfig) -> ToplevelAction { + match name { + "usr" => ToplevelAction::Import { + dest_path: &["usr"], + }, + "etc" => ToplevelAction::Import { + dest_path: &["usr", "etc"], + }, + "var" if config.remap_factory_var => ToplevelAction::Import { + dest_path: &["usr", "share", "factory", "var"], + }, + "var" => ToplevelAction::Import { + dest_path: &["var"], + }, + name if EXCLUDED_TOPLEVEL_PATHS.contains(&name) => ToplevelAction::Skip, + _ if config.allow_nonusr => ToplevelAction::Import { + // For non-usr paths when allowed, we need to handle dynamically + // This case requires special handling since we can't return a static slice + dest_path: &[], + }, + _ => ToplevelAction::Skip, + } +} + +/// Check if a file is an overlay whiteout (character device with major/minor 0/0) +fn is_overlay_whiteout(metadata: &std::fs::Metadata) -> bool { + let file_type = metadata.file_type(); + if !file_type.is_char_device() { + return false; + } + // Check if rdev is 0 (major 0, minor 0) + metadata.rdev() == 0 +} + +/// Import a filesystem directory directly to OSTree with path transformations. +/// +/// Uses `write_dfd_to_mtree` for each file individually, enabling reflinks while +/// handling overlay whiteouts by converting them to OCI format. +/// +/// Overlay whiteouts (char device 0/0) are converted to OCI-format whiteout files +/// (`.wh.`) to match the behavior of the tar import path. +/// +/// Note: SELinux labeling is NOT performed here. It happens during the merge step +/// in the container import flow, where all layers are combined and labeled together +/// with the correct destination paths. +/// +/// Returns the commit checksum. The caller is responsible for setting the ref +/// within a transaction. +pub fn import_filesystem_to_ostree( + repo: &ostree::Repo, + src_path: &Utf8Path, + config: &FilesystemFilterConfig, +) -> Result { + let cancellable = gio::Cancellable::NONE; + + // Create the root MutableTree + let root_mtree = ostree::MutableTree::new(); + let mut filtered_stats = BTreeMap::new(); + + // Create a commit modifier - no SELinux here, it's applied during merge + let modifier = ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::CONSUME, None); + + // Create default dirmeta for directories we create + let dirmeta_checksum = create_default_dirmeta(repo)?; + + // Set the root tree's metadata checksum - required before write_mtree + root_mtree.set_metadata_checksum(&dirmeta_checksum); + + // Read toplevel entries and process each according to its classification + let entries = + std::fs::read_dir(src_path).with_context(|| format!("Reading directory: {}", src_path))?; + + for entry in entries { + let entry = entry.context("Reading directory entry")?; + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + let file_type = entry.file_type().context("Getting file type")?; + + // Only process directories at toplevel + if !file_type.is_dir() { + continue; + } + + let action = classify_toplevel(&name_str, config); + + match action { + ToplevelAction::Import { dest_path } => { + // Handle the special case of dynamic non-usr paths + let dest_parts: Vec<&str> = if dest_path.is_empty() { + vec![&name_str] + } else { + dest_path.to_vec() + }; + + // Ensure the destination directory exists in the MutableTree + let dest_mtree = if dest_parts.len() == 1 { + root_mtree + .ensure_dir(dest_parts[0]) + .with_context(|| format!("Creating directory: {}", dest_parts[0]))? + } else { + // For nested paths like usr/etc or usr/share/factory/var, + // we need to ensure all parent directories exist + root_mtree + .ensure_parent_dirs(&dest_parts, &dirmeta_checksum) + .with_context(|| { + format!("Creating directory path: {}", dest_parts.join("/")) + })? + }; + + // Set metadata checksum on the destination directory + dest_mtree.set_metadata_checksum(&dirmeta_checksum); + + let src_dir_path = src_path.join(&*name_str); + + // Import the directory recursively, handling whiteouts + import_directory_recursive( + repo, + src_dir_path.as_std_path(), + &dest_mtree, + &modifier, + &dirmeta_checksum, + cancellable, + ) + .with_context(|| { + format!( + "Importing directory '{}' to '{}'", + name_str, + dest_parts.join("/") + ) + })?; + } + ToplevelAction::Skip => { + *filtered_stats.entry(name_str.to_string()).or_insert(0) += 1; + } + } + } + + // Convert MutableTree to a RepoFile + let root = repo + .write_mtree(&root_mtree, cancellable) + .context("Writing mtree")?; + let root = root + .downcast::() + .map_err(|_| anyhow!("Expected RepoFile"))?; + + // Create commit metadata + let metadata = glib::VariantDict::new(None); + metadata.insert( + "ostree.importer.version", + env!("CARGO_PKG_VERSION").to_variant(), + ); + let metadata = metadata.to_variant(); + + // Write the commit + let commit = repo + .write_commit(None, None, None, Some(&metadata), &root, cancellable) + .context("Writing commit")?; + + Ok(crate::tar::WriteTarResult { + commit: commit.to_string(), + filtered: filtered_stats, + }) +} + +/// Recursively import a directory, using write_dfd_to_mtree for each file +/// and converting overlay whiteouts to OCI format. +fn import_directory_recursive( + repo: &ostree::Repo, + src_dir: &Path, + mtree: &ostree::MutableTree, + modifier: &ostree::RepoCommitModifier, + dirmeta_checksum: &str, + cancellable: Option<&gio::Cancellable>, +) -> Result<()> { + // Open the source directory for use with write_dfd_to_mtree + let src_fd = rustix::fs::openat( + rustix::fs::CWD, + src_dir, + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::DIRECTORY, + rustix::fs::Mode::empty(), + ) + .with_context(|| format!("Opening directory: {}", src_dir.display()))?; + + let entries = std::fs::read_dir(src_dir) + .with_context(|| format!("Reading directory: {}", src_dir.display()))?; + + for entry in entries { + let entry = entry.context("Reading directory entry")?; + let file_name = entry.file_name(); + let file_name_str = file_name.to_string_lossy(); + let file_path = entry.path(); + + let metadata = std::fs::symlink_metadata(&file_path) + .with_context(|| format!("Getting metadata for: {}", file_path.display()))?; + let file_type = metadata.file_type(); + + if file_type.is_dir() { + // Create subdirectory in mtree and recurse + let sub_mtree = mtree + .ensure_dir(&file_name_str) + .with_context(|| format!("Creating directory: {}", file_name_str))?; + sub_mtree.set_metadata_checksum(dirmeta_checksum); + + import_directory_recursive( + repo, + &file_path, + &sub_mtree, + modifier, + dirmeta_checksum, + cancellable, + )?; + } else if file_type.is_file() || file_type.is_symlink() { + // Use write_dfd_to_mtree for the individual file - this enables reflinks + repo.write_dfd_to_mtree( + src_fd.as_raw_fd(), + &file_name_str, + mtree, + Some(modifier), + cancellable, + ) + .with_context(|| format!("Importing file: {}", file_path.display()))?; + } else if is_overlay_whiteout(&metadata) { + // Convert overlay whiteout to OCI format (.wh.) + let whiteout_name = format!(".wh.{}", file_name_str); + let checksum = create_empty_file(repo)?; + mtree + .replace_file(&whiteout_name, &checksum) + .with_context(|| format!("Adding whiteout file: {}", whiteout_name))?; + tracing::debug!( + src = %file_path.display(), + dest = %whiteout_name, + "Converted overlay whiteout to OCI format" + ); + } else { + // Skip other special files (sockets, fifos, block devices) + tracing::debug!(path = %file_path.display(), "Skipping special file"); + } + } + + Ok(()) +} + +/// Create default directory metadata and return its checksum +fn create_default_dirmeta(repo: &ostree::Repo) -> Result { + let cancellable = gio::Cancellable::NONE; + + let finfo = gio::FileInfo::new(); + finfo.set_file_type(gio::FileType::Directory); + finfo.set_attribute_uint32("unix::uid", 0); + finfo.set_attribute_uint32("unix::gid", 0); + finfo.set_attribute_uint32("unix::mode", libc::S_IFDIR | 0o755); + + let dirmeta = ostree::create_directory_metadata(&finfo, None); + let checksum = repo + .write_metadata(ostree::ObjectType::DirMeta, None, &dirmeta, cancellable)? + .to_hex(); + + Ok(checksum) +} + +/// Create an empty file and return its checksum (for whiteout files) +fn create_empty_file(repo: &ostree::Repo) -> Result { + let cancellable = gio::Cancellable::NONE; + + let checksum = + repo.write_regfile_inline(None, 0, 0, libc::S_IFREG | 0o644, None, &[], cancellable)?; + + Ok(checksum.to_string()) +} diff --git a/crates/ostree-ext/src/filesystem/mod.rs b/crates/ostree-ext/src/filesystem/mod.rs new file mode 100644 index 000000000..ee005a8df --- /dev/null +++ b/crates/ostree-ext/src/filesystem/mod.rs @@ -0,0 +1,42 @@ +//! Filesystem-based filtering and processing +//! +//! This module provides functionality to import filesystem directories directly +//! to OSTree, applying path transformations without creating temporary files +//! on disk. + +mod filter; + +pub use filter::{FilesystemFilterConfig, import_filesystem_to_ostree}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_config_from_tar_options() { + let tar_opts = crate::tar::WriteTarOptions { + base: None, + selinux: false, + allow_nonusr: true, + retain_var: false, + }; + + let fs_config = FilesystemFilterConfig::from_tar_options(&tar_opts); + assert!(fs_config.allow_nonusr); + assert!(fs_config.remap_factory_var); // !retain_var + } + + #[test] + fn test_config_from_tar_options_retain_var() { + let tar_opts = crate::tar::WriteTarOptions { + base: None, + selinux: false, + allow_nonusr: false, + retain_var: true, + }; + + let fs_config = FilesystemFilterConfig::from_tar_options(&tar_opts); + assert!(!fs_config.allow_nonusr); + assert!(!fs_config.remap_factory_var); // !retain_var + } +} diff --git a/crates/ostree-ext/src/lib.rs b/crates/ostree-ext/src/lib.rs index 5de91010f..1625714ec 100644 --- a/crates/ostree-ext/src/lib.rs +++ b/crates/ostree-ext/src/lib.rs @@ -39,6 +39,7 @@ pub mod cli; pub mod container; pub mod container_utils; pub mod diff; +pub mod filesystem; pub(crate) mod generic_decompress; pub mod ima; pub mod keyfileext; diff --git a/crates/ostree-ext/src/tar/write.rs b/crates/ostree-ext/src/tar/write.rs index c1ca69666..54b5a63a1 100644 --- a/crates/ostree-ext/src/tar/write.rs +++ b/crates/ostree-ext/src/tar/write.rs @@ -29,9 +29,9 @@ use std::sync::Arc; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite}; use tracing::instrument; -// Exclude things in https://www.freedesktop.org/wiki/Software/systemd/APIFileSystems/ -// from being placed in the rootfs. -const EXCLUDED_TOPLEVEL_PATHS: &[&str] = &["run", "tmp", "proc", "sys", "dev"]; +/// Exclude things in https://www.freedesktop.org/wiki/Software/systemd/APIFileSystems/ +/// from being placed in the rootfs. +pub(crate) const EXCLUDED_TOPLEVEL_PATHS: &[&str] = &["run", "tmp", "proc", "sys", "dev"]; /// Copy a tar entry to a new tar archive, optionally using a different filesystem path. #[context("Copying entry")]