-
Notifications
You must be signed in to change notification settings - Fork 159
lib: Add experimental unified storage support for install #1601
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,8 +4,8 @@ | |
|
|
||
| use std::collections::HashSet; | ||
| use std::io::{BufRead, Write}; | ||
| use std::process::Command; | ||
|
|
||
| use anyhow::Ok; | ||
| use anyhow::{anyhow, Context, Result}; | ||
| use bootc_kernel_cmdline::utf8::CmdlineOwned; | ||
| use cap_std::fs::{Dir, MetadataExt}; | ||
|
|
@@ -93,6 +93,17 @@ pub(crate) async fn new_importer( | |
| Ok(imp) | ||
| } | ||
|
|
||
| /// Wrapper for pulling a container image with a custom proxy config (e.g. for unified storage). | ||
| pub(crate) async fn new_importer_with_config( | ||
| repo: &ostree::Repo, | ||
| imgref: &ostree_container::OstreeImageReference, | ||
| config: ostree_ext::containers_image_proxy::ImageProxyConfig, | ||
| ) -> Result<ostree_container::store::ImageImporter> { | ||
| let mut imp = ostree_container::store::ImageImporter::new(repo, imgref, config).await?; | ||
| imp.require_bootable(); | ||
| Ok(imp) | ||
| } | ||
|
|
||
| pub(crate) fn check_bootc_label(config: &ostree_ext::oci_spec::image::ImageConfiguration) { | ||
| if let Some(label) = | ||
| labels_of_config(config).and_then(|labels| labels.get(crate::metadata::BOOTC_COMPAT_LABEL)) | ||
|
|
@@ -316,6 +327,18 @@ pub(crate) async fn prune_container_store(sysroot: &Storage) -> Result<()> { | |
| for deployment in deployments { | ||
| let bound = crate::boundimage::query_bound_images_for_deployment(ostree, &deployment)?; | ||
| all_bound_images.extend(bound.into_iter()); | ||
| // Also include the host image itself | ||
| // Note: Use just the image name (not the full transport:image format) because | ||
| // podman's image names don't include the transport prefix. | ||
| if let Some(host_image) = crate::status::boot_entry_from_deployment(ostree, &deployment)? | ||
| .image | ||
| .map(|i| i.image) | ||
| { | ||
| all_bound_images.push(crate::boundimage::BoundImage { | ||
| image: host_image.image.clone(), | ||
| auth_file: None, | ||
| }); | ||
| } | ||
| } | ||
| // Convert to a hashset of just the image names | ||
| let image_names = HashSet::from_iter(all_bound_images.iter().map(|img| img.image.as_str())); | ||
|
|
@@ -381,6 +404,152 @@ pub(crate) async fn prepare_for_pull( | |
| Ok(PreparedPullResult::Ready(Box::new(prepared_image))) | ||
| } | ||
|
|
||
| /// Check whether the image exists in bootc's unified container storage. | ||
| /// | ||
| /// This is used for auto-detection: if the image already exists in bootc storage | ||
| /// (e.g., from a previous `bootc image set-unified` or LBI pull), we can use | ||
| /// the unified storage path for faster imports. | ||
| /// | ||
| /// Returns true if the image exists in bootc storage. | ||
| pub(crate) async fn image_exists_in_unified_storage( | ||
| store: &Storage, | ||
| imgref: &ImageReference, | ||
| ) -> Result<bool> { | ||
| let imgstore = store.get_ensure_imgstore()?; | ||
| let image_ref_str = imgref.to_transport_image()?; | ||
| imgstore.exists(&image_ref_str).await | ||
| } | ||
|
|
||
| /// Unified approach: Use bootc's CStorage to pull the image, then prepare from containers-storage. | ||
| /// This reuses the same infrastructure as LBIs. | ||
| pub(crate) async fn prepare_for_pull_unified( | ||
| repo: &ostree::Repo, | ||
| imgref: &ImageReference, | ||
| target_imgref: Option<&OstreeImageReference>, | ||
| store: &Storage, | ||
| ) -> Result<PreparedPullResult> { | ||
| // Get or initialize the bootc container storage (same as used for LBIs) | ||
| let imgstore = store.get_ensure_imgstore()?; | ||
|
|
||
| let image_ref_str = imgref.to_transport_image()?; | ||
|
|
||
| // Always pull to ensure we have the latest image, whether from a remote | ||
| // registry or a locally rebuilt image | ||
| tracing::info!( | ||
| "Unified pull: pulling from transport '{}' to bootc storage", | ||
| &imgref.transport | ||
| ); | ||
|
|
||
| // Pull the image to bootc storage using the same method as LBIs | ||
| // Show a spinner since podman pull can take a while and doesn't output progress | ||
| let pull_msg = format!("Pulling {} to bootc storage", &image_ref_str); | ||
| async_task_with_spinner(&pull_msg, async move { | ||
| imgstore | ||
| .pull(&image_ref_str, crate::podstorage::PullMode::Always) | ||
jmarrero marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .await | ||
| }) | ||
| .await?; | ||
|
|
||
| // Now create a containers-storage reference to read from bootc storage | ||
| tracing::info!("Unified pull: now importing from containers-storage transport"); | ||
| let containers_storage_imgref = ImageReference { | ||
| transport: "containers-storage".to_string(), | ||
| image: imgref.image.clone(), | ||
| signature: imgref.signature.clone(), | ||
| }; | ||
| let ostree_imgref = OstreeImageReference::from(containers_storage_imgref); | ||
|
|
||
| // Configure the importer to use bootc storage as an additional image store | ||
| let mut config = ostree_ext::containers_image_proxy::ImageProxyConfig::default(); | ||
| let mut cmd = Command::new("skopeo"); | ||
| // Use the physical path to bootc storage from the Storage struct | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is OK but I've been trying to ensure that bootc uses file descriptors and not file paths as much as possible. The problem here is the case where we have a fd but need to pass that state down to a subprocess which expects a file path. podstorage.rs goes to quite some effort to do this for the primary storage, see |
||
| let storage_path = format!( | ||
| "{}/{}", | ||
| store.physical_root_path, | ||
| crate::podstorage::CStorage::subpath() | ||
| ); | ||
| crate::podstorage::set_additional_image_store(&mut cmd, &storage_path); | ||
| config.skopeo_cmd = Some(cmd); | ||
|
|
||
| // Use the preparation flow with the custom config | ||
| let mut imp = new_importer_with_config(repo, &ostree_imgref, config).await?; | ||
| if let Some(target) = target_imgref { | ||
| imp.set_target(target); | ||
| } | ||
| let prep = match imp.prepare().await? { | ||
| PrepareResult::AlreadyPresent(c) => { | ||
| println!("No changes in {imgref:#} => {}", c.manifest_digest); | ||
| return Ok(PreparedPullResult::AlreadyPresent(Box::new((*c).into()))); | ||
| } | ||
| PrepareResult::Ready(p) => p, | ||
| }; | ||
| check_bootc_label(&prep.config); | ||
| if let Some(warning) = prep.deprecated_warning() { | ||
| ostree_ext::cli::print_deprecated_warning(warning).await; | ||
| } | ||
| ostree_ext::cli::print_layer_status(&prep); | ||
| let layers_to_fetch = prep.layers_to_fetch().collect::<Result<Vec<_>>>()?; | ||
|
|
||
| // Log that we're importing a new image from containers-storage | ||
| const PULLING_NEW_IMAGE_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0"; | ||
| tracing::info!( | ||
| message_id = PULLING_NEW_IMAGE_ID, | ||
| bootc.image.reference = &imgref.image, | ||
| bootc.image.transport = "containers-storage", | ||
| bootc.original_transport = &imgref.transport, | ||
| bootc.status = "importing_from_storage", | ||
| "Importing image from bootc storage: {}", | ||
| ostree_imgref | ||
| ); | ||
|
|
||
| let prepared_image = PreparedImportMeta { | ||
| imp, | ||
| n_layers_to_fetch: layers_to_fetch.len(), | ||
| layers_total: prep.all_layers().count(), | ||
| bytes_to_fetch: layers_to_fetch.iter().map(|(l, _)| l.layer.size()).sum(), | ||
| bytes_total: prep.all_layers().map(|l| l.layer.size()).sum(), | ||
| digest: prep.manifest_digest.clone(), | ||
| prep, | ||
| }; | ||
|
|
||
| Ok(PreparedPullResult::Ready(Box::new(prepared_image))) | ||
| } | ||
|
|
||
| /// Unified pull: Use podman to pull to containers-storage, then read from there | ||
| pub(crate) async fn pull_unified( | ||
| repo: &ostree::Repo, | ||
| imgref: &ImageReference, | ||
| target_imgref: Option<&OstreeImageReference>, | ||
| quiet: bool, | ||
| prog: ProgressWriter, | ||
| store: &Storage, | ||
| ) -> Result<Box<ImageState>> { | ||
| match prepare_for_pull_unified(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"; | ||
jmarrero marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| tracing::debug!( | ||
| message_id = IMAGE_ALREADY_PRESENT_ID, | ||
| bootc.image.reference = &imgref.image, | ||
| bootc.image.transport = &imgref.transport, | ||
| bootc.status = "already_present", | ||
| "Image already present: {}", | ||
| imgref | ||
| ); | ||
| Ok(existing) | ||
| } | ||
| PreparedPullResult::Ready(prepared_image_meta) => { | ||
| // To avoid duplicate success logs, pass a containers-storage imgref to the importer | ||
| let cs_imgref = ImageReference { | ||
| transport: "containers-storage".to_string(), | ||
| image: imgref.image.clone(), | ||
| signature: imgref.signature.clone(), | ||
| }; | ||
| pull_from_prepared(&cs_imgref, quiet, prog, *prepared_image_meta).await | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[context("Pulling")] | ||
| pub(crate) async fn pull_from_prepared( | ||
| imgref: &ImageReference, | ||
|
|
@@ -430,18 +599,21 @@ pub(crate) async fn pull_from_prepared( | |
| let imgref_canonicalized = imgref.clone().canonicalize()?; | ||
| tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}"); | ||
|
|
||
| // Log successful import completion | ||
| const IMPORT_COMPLETE_JOURNAL_ID: &str = "4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8"; | ||
|
|
||
| tracing::info!( | ||
| message_id = IMPORT_COMPLETE_JOURNAL_ID, | ||
| bootc.image.reference = &imgref.image, | ||
| bootc.image.transport = &imgref.transport, | ||
| bootc.manifest_digest = import.manifest_digest.as_ref(), | ||
| bootc.ostree_commit = &import.merge_commit, | ||
| "Successfully imported image: {}", | ||
| imgref | ||
| ); | ||
| // Log successful import completion (skip if using unified storage to avoid double logging) | ||
| let is_unified_path = imgref.transport == "containers-storage"; | ||
| if !is_unified_path { | ||
| const IMPORT_COMPLETE_JOURNAL_ID: &str = "4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8"; | ||
|
|
||
| tracing::info!( | ||
| message_id = IMPORT_COMPLETE_JOURNAL_ID, | ||
| bootc.image.reference = &imgref.image, | ||
| bootc.image.transport = &imgref.transport, | ||
| bootc.manifest_digest = import.manifest_digest.as_ref(), | ||
| bootc.ostree_commit = &import.merge_commit, | ||
| "Successfully imported image: {}", | ||
| imgref | ||
| ); | ||
| } | ||
|
|
||
| if let Some(msg) = | ||
| ostree_container::store::image_filtered_content_warning(&import.filtered_files) | ||
|
|
@@ -1051,7 +1223,7 @@ pub(crate) fn fixup_etc_fstab(root: &Dir) -> Result<()> { | |
| } | ||
|
|
||
| // Read the input, and atomically write a modified version | ||
| root.atomic_replace_with(fstab_path, move |mut w| { | ||
| root.atomic_replace_with(fstab_path, move |mut w| -> Result<()> { | ||
| for line in fd.lines() { | ||
| let line = line?; | ||
| if !edit_fstab_line(&line, &mut w)? { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.