@@ -93,6 +93,17 @@ pub(crate) async fn new_importer(
9393 Ok ( imp)
9494}
9595
96+ /// Wrapper for pulling a container image with a custom proxy config (e.g. for unified storage).
97+ pub ( crate ) async fn new_importer_with_config (
98+ repo : & ostree:: Repo ,
99+ imgref : & ostree_container:: OstreeImageReference ,
100+ config : ostree_ext:: containers_image_proxy:: ImageProxyConfig ,
101+ ) -> Result < ostree_container:: store:: ImageImporter > {
102+ let mut imp = ostree_container:: store:: ImageImporter :: new ( repo, imgref, config) . await ?;
103+ imp. require_bootable ( ) ;
104+ Ok ( imp)
105+ }
106+
96107pub ( crate ) fn check_bootc_label ( config : & ostree_ext:: oci_spec:: image:: ImageConfiguration ) {
97108 if let Some ( label) =
98109 labels_of_config ( config) . and_then ( |labels| labels. get ( crate :: metadata:: BOOTC_COMPAT_LABEL ) )
@@ -316,6 +327,16 @@ pub(crate) async fn prune_container_store(sysroot: &Storage) -> Result<()> {
316327 for deployment in deployments {
317328 let bound = crate :: boundimage:: query_bound_images_for_deployment ( ostree, & deployment) ?;
318329 all_bound_images. extend ( bound. into_iter ( ) ) ;
330+ // Also include the host image itself
331+ if let Some ( host_image) = crate :: status:: boot_entry_from_deployment ( ostree, & deployment) ?
332+ . image
333+ . map ( |i| i. image )
334+ {
335+ all_bound_images. push ( crate :: boundimage:: BoundImage {
336+ image : crate :: utils:: imageref_to_container_ref ( & host_image) ,
337+ auth_file : None ,
338+ } ) ;
339+ }
319340 }
320341 // Convert to a hashset of just the image names
321342 let image_names = HashSet :: from_iter ( all_bound_images. iter ( ) . map ( |img| img. image . as_str ( ) ) ) ;
@@ -381,6 +402,189 @@ pub(crate) async fn prepare_for_pull(
381402 Ok ( PreparedPullResult :: Ready ( Box :: new ( prepared_image) ) )
382403}
383404
405+ /// Check whether to use the unified storage path for pulling an image.
406+ ///
407+ /// If `explicit_flag` is Some(true), unified storage is always used.
408+ /// If `explicit_flag` is Some(false), unified storage is never used.
409+ /// If `explicit_flag` is None, auto-detect based on whether the image already exists
410+ /// in bootc's container storage.
411+ ///
412+ /// Returns true if unified storage should be used.
413+ pub ( crate ) async fn should_use_unified_storage (
414+ store : & Storage ,
415+ imgref : & ImageReference ,
416+ explicit_flag : Option < bool > ,
417+ ) -> bool {
418+ // Explicit flag takes precedence
419+ if let Some ( flag) = explicit_flag {
420+ return flag;
421+ }
422+
423+ // Auto-detect: check if image exists in bootc storage
424+ let imgstore = match store. get_ensure_imgstore ( ) {
425+ std:: result:: Result :: Ok ( s) => s,
426+ std:: result:: Result :: Err ( e) => {
427+ tracing:: warn!( "Failed to access bootc storage: {e}; falling back to standard pull" ) ;
428+ return false ;
429+ }
430+ } ;
431+
432+ let image_ref_str = crate :: utils:: imageref_to_container_ref ( imgref) ;
433+ match imgstore. exists ( & image_ref_str) . await {
434+ std:: result:: Result :: Ok ( v) => v,
435+ std:: result:: Result :: Err ( e) => {
436+ tracing:: warn!(
437+ "Failed to check bootc storage for image: {e}; falling back to standard pull"
438+ ) ;
439+ false
440+ }
441+ }
442+ }
443+
444+ /// Unified approach: Use bootc's CStorage to pull the image, then prepare from containers-storage.
445+ /// This reuses the same infrastructure as LBIs.
446+ ///
447+ /// The `sysroot_path` parameter specifies the path to the sysroot where bootc storage is located.
448+ /// During install, this should be the path to the target disk's mount point.
449+ /// During upgrade/switch on a running system, pass `None` to use the default `/sysroot`.
450+ pub ( crate ) async fn prepare_for_pull_unified (
451+ repo : & ostree:: Repo ,
452+ imgref : & ImageReference ,
453+ target_imgref : Option < & OstreeImageReference > ,
454+ store : & Storage ,
455+ sysroot_path : Option < & camino:: Utf8Path > ,
456+ ) -> Result < PreparedPullResult > {
457+ // Get or initialize the bootc container storage (same as used for LBIs)
458+ let imgstore = store. get_ensure_imgstore ( ) ?;
459+
460+ let image_ref_str = crate :: utils:: imageref_to_container_ref ( imgref) ;
461+
462+ // Always pull to ensure we have the latest image, whether from a remote
463+ // registry or a locally rebuilt image
464+ tracing:: info!(
465+ "Unified pull: pulling from transport '{}' to bootc storage" ,
466+ & imgref. transport
467+ ) ;
468+
469+ // Pull the image to bootc storage using the same method as LBIs
470+ // Show a spinner since podman pull can take a while and doesn't output progress
471+ let pull_msg = format ! ( "Pulling {} to bootc storage" , & image_ref_str) ;
472+ async_task_with_spinner ( & pull_msg, async move {
473+ imgstore
474+ . pull ( & image_ref_str, crate :: podstorage:: PullMode :: Always )
475+ . await
476+ } )
477+ . await ?;
478+
479+ // Now create a containers-storage reference to read from bootc storage
480+ tracing:: info!( "Unified pull: now importing from containers-storage transport" ) ;
481+ let containers_storage_imgref = ImageReference {
482+ transport : "containers-storage" . to_string ( ) ,
483+ image : imgref. image . clone ( ) ,
484+ signature : imgref. signature . clone ( ) ,
485+ } ;
486+ let ostree_imgref = OstreeImageReference :: from ( containers_storage_imgref) ;
487+
488+ // Configure the importer to use bootc storage as an additional image store
489+ use std:: process:: Command ;
490+ let mut config = ostree_ext:: containers_image_proxy:: ImageProxyConfig :: default ( ) ;
491+ let mut cmd = Command :: new ( "skopeo" ) ;
492+ // Use the actual physical path to bootc storage
493+ // During install, this is the target disk's mount point; otherwise default to /sysroot
494+ let sysroot_base = sysroot_path
495+ . map ( |p| p. to_string ( ) )
496+ . unwrap_or_else ( || "/sysroot" . to_string ( ) ) ;
497+ let storage_path = format ! (
498+ "{}/{}" ,
499+ sysroot_base,
500+ crate :: podstorage:: CStorage :: subpath( )
501+ ) ;
502+ crate :: podstorage:: set_additional_image_store ( & mut cmd, & storage_path) ;
503+ config. skopeo_cmd = Some ( cmd) ;
504+
505+ // Use the preparation flow with the custom config
506+ let mut imp = new_importer_with_config ( repo, & ostree_imgref, config) . await ?;
507+ if let Some ( target) = target_imgref {
508+ imp. set_target ( target) ;
509+ }
510+ let prep = match imp. prepare ( ) . await ? {
511+ PrepareResult :: AlreadyPresent ( c) => {
512+ println ! ( "No changes in {imgref:#} => {}" , c. manifest_digest) ;
513+ return Ok ( PreparedPullResult :: AlreadyPresent ( Box :: new ( ( * c) . into ( ) ) ) ) ;
514+ }
515+ PrepareResult :: Ready ( p) => p,
516+ } ;
517+ check_bootc_label ( & prep. config ) ;
518+ if let Some ( warning) = prep. deprecated_warning ( ) {
519+ ostree_ext:: cli:: print_deprecated_warning ( warning) . await ;
520+ }
521+ ostree_ext:: cli:: print_layer_status ( & prep) ;
522+ let layers_to_fetch = prep. layers_to_fetch ( ) . collect :: < Result < Vec < _ > > > ( ) ?;
523+
524+ // Log that we're importing a new image from containers-storage
525+ const PULLING_NEW_IMAGE_ID : & str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0" ;
526+ tracing:: info!(
527+ message_id = PULLING_NEW_IMAGE_ID ,
528+ bootc. image. reference = & imgref. image,
529+ bootc. image. transport = "containers-storage" ,
530+ bootc. original_transport = & imgref. transport,
531+ bootc. status = "importing_from_storage" ,
532+ "Importing image from bootc storage: {}" ,
533+ ostree_imgref
534+ ) ;
535+
536+ let prepared_image = PreparedImportMeta {
537+ imp,
538+ n_layers_to_fetch : layers_to_fetch. len ( ) ,
539+ layers_total : prep. all_layers ( ) . count ( ) ,
540+ bytes_to_fetch : layers_to_fetch. iter ( ) . map ( |( l, _) | l. layer . size ( ) ) . sum ( ) ,
541+ bytes_total : prep. all_layers ( ) . map ( |l| l. layer . size ( ) ) . sum ( ) ,
542+ digest : prep. manifest_digest . clone ( ) ,
543+ prep,
544+ } ;
545+
546+ Ok ( PreparedPullResult :: Ready ( Box :: new ( prepared_image) ) )
547+ }
548+
549+ /// Unified pull: Use podman to pull to containers-storage, then read from there
550+ ///
551+ /// The `sysroot_path` parameter specifies the path to the sysroot where bootc storage is located.
552+ /// For normal upgrade/switch operations, pass `None` to use the default `/sysroot`.
553+ pub ( crate ) async fn pull_unified (
554+ repo : & ostree:: Repo ,
555+ imgref : & ImageReference ,
556+ target_imgref : Option < & OstreeImageReference > ,
557+ quiet : bool ,
558+ prog : ProgressWriter ,
559+ store : & Storage ,
560+ sysroot_path : Option < & camino:: Utf8Path > ,
561+ ) -> Result < Box < ImageState > > {
562+ match prepare_for_pull_unified ( repo, imgref, target_imgref, store, sysroot_path) . await ? {
563+ PreparedPullResult :: AlreadyPresent ( existing) => {
564+ // Log that the image was already present (Debug level since it's not actionable)
565+ const IMAGE_ALREADY_PRESENT_ID : & str = "5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9" ;
566+ tracing:: debug!(
567+ message_id = IMAGE_ALREADY_PRESENT_ID ,
568+ bootc. image. reference = & imgref. image,
569+ bootc. image. transport = & imgref. transport,
570+ bootc. status = "already_present" ,
571+ "Image already present: {}" ,
572+ imgref
573+ ) ;
574+ Ok ( existing)
575+ }
576+ PreparedPullResult :: Ready ( prepared_image_meta) => {
577+ // To avoid duplicate success logs, pass a containers-storage imgref to the importer
578+ let cs_imgref = ImageReference {
579+ transport : "containers-storage" . to_string ( ) ,
580+ image : imgref. image . clone ( ) ,
581+ signature : imgref. signature . clone ( ) ,
582+ } ;
583+ pull_from_prepared ( & cs_imgref, quiet, prog, * prepared_image_meta) . await
584+ }
585+ }
586+ }
587+
384588#[ context( "Pulling" ) ]
385589pub ( crate ) async fn pull_from_prepared (
386590 imgref : & ImageReference ,
@@ -430,18 +634,21 @@ pub(crate) async fn pull_from_prepared(
430634 let imgref_canonicalized = imgref. clone ( ) . canonicalize ( ) ?;
431635 tracing:: debug!( "Canonicalized image reference: {imgref_canonicalized:#}" ) ;
432636
433- // Log successful import completion
434- const IMPORT_COMPLETE_JOURNAL_ID : & str = "4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8" ;
435-
436- tracing:: info!(
437- message_id = IMPORT_COMPLETE_JOURNAL_ID ,
438- bootc. image. reference = & imgref. image,
439- bootc. image. transport = & imgref. transport,
440- bootc. manifest_digest = import. manifest_digest. as_ref( ) ,
441- bootc. ostree_commit = & import. merge_commit,
442- "Successfully imported image: {}" ,
443- imgref
444- ) ;
637+ // Log successful import completion (skip if using unified storage to avoid double logging)
638+ let is_unified_path = imgref. transport == "containers-storage" ;
639+ if !is_unified_path {
640+ const IMPORT_COMPLETE_JOURNAL_ID : & str = "4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8" ;
641+
642+ tracing:: info!(
643+ message_id = IMPORT_COMPLETE_JOURNAL_ID ,
644+ bootc. image. reference = & imgref. image,
645+ bootc. image. transport = & imgref. transport,
646+ bootc. manifest_digest = import. manifest_digest. as_ref( ) ,
647+ bootc. ostree_commit = & import. merge_commit,
648+ "Successfully imported image: {}" ,
649+ imgref
650+ ) ;
651+ }
445652
446653 if let Some ( msg) =
447654 ostree_container:: store:: image_filtered_content_warning ( & import. filtered_files )
0 commit comments