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