@@ -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,205 @@ 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+ // Check if image already exists in bootc storage - if so, skip the pull
463+ // This is important for localhost images which can't be pulled from a registry
464+ let image_exists = match imgstore. exists ( & image_ref_str) . await {
465+ std:: result:: Result :: Ok ( v) => v,
466+ std:: result:: Result :: Err ( e) => {
467+ tracing:: warn!( "Failed to check bootc storage for image: {e}; will attempt pull" ) ;
468+ false
469+ }
470+ } ;
471+
472+ if image_exists {
473+ tracing:: info!(
474+ "Unified pull: image '{}' already exists in bootc storage, skipping pull" ,
475+ & image_ref_str
476+ ) ;
477+ } else {
478+ // Log the original transport being used for the pull
479+ tracing:: info!(
480+ "Unified pull: pulling from transport '{}' to bootc storage" ,
481+ & imgref. transport
482+ ) ;
483+
484+ // Pull the image to bootc storage using the same method as LBIs
485+ // Show a spinner since podman pull can take a while and doesn't output progress
486+ let pull_msg = format ! ( "Pulling {} to bootc storage" , & image_ref_str) ;
487+ async_task_with_spinner ( & pull_msg, async move {
488+ imgstore
489+ . pull ( & image_ref_str, crate :: podstorage:: PullMode :: Always )
490+ . await
491+ } )
492+ . await ?;
493+ }
494+
495+ // Now create a containers-storage reference to read from bootc storage
496+ tracing:: info!( "Unified pull: now importing from containers-storage transport" ) ;
497+ let containers_storage_imgref = ImageReference {
498+ transport : "containers-storage" . to_string ( ) ,
499+ image : imgref. image . clone ( ) ,
500+ signature : imgref. signature . clone ( ) ,
501+ } ;
502+ let ostree_imgref = OstreeImageReference :: from ( containers_storage_imgref) ;
503+
504+ // Configure the importer to use bootc storage as an additional image store
505+ use std:: process:: Command ;
506+ let mut config = ostree_ext:: containers_image_proxy:: ImageProxyConfig :: default ( ) ;
507+ let mut cmd = Command :: new ( "skopeo" ) ;
508+ // Use the actual physical path to bootc storage
509+ // During install, this is the target disk's mount point; otherwise default to /sysroot
510+ let sysroot_base = sysroot_path
511+ . map ( |p| p. to_string ( ) )
512+ . unwrap_or_else ( || "/sysroot" . to_string ( ) ) ;
513+ let storage_path = format ! (
514+ "{}/{}" ,
515+ sysroot_base,
516+ crate :: podstorage:: CStorage :: subpath( )
517+ ) ;
518+ crate :: podstorage:: set_additional_image_store ( & mut cmd, & storage_path) ;
519+ config. skopeo_cmd = Some ( cmd) ;
520+
521+ // Use the preparation flow with the custom config
522+ let mut imp = new_importer_with_config ( repo, & ostree_imgref, config) . await ?;
523+ if let Some ( target) = target_imgref {
524+ imp. set_target ( target) ;
525+ }
526+ let prep = match imp. prepare ( ) . await ? {
527+ PrepareResult :: AlreadyPresent ( c) => {
528+ println ! ( "No changes in {imgref:#} => {}" , c. manifest_digest) ;
529+ return Ok ( PreparedPullResult :: AlreadyPresent ( Box :: new ( ( * c) . into ( ) ) ) ) ;
530+ }
531+ PrepareResult :: Ready ( p) => p,
532+ } ;
533+ check_bootc_label ( & prep. config ) ;
534+ if let Some ( warning) = prep. deprecated_warning ( ) {
535+ ostree_ext:: cli:: print_deprecated_warning ( warning) . await ;
536+ }
537+ ostree_ext:: cli:: print_layer_status ( & prep) ;
538+ let layers_to_fetch = prep. layers_to_fetch ( ) . collect :: < Result < Vec < _ > > > ( ) ?;
539+
540+ // Log that we're importing a new image from containers-storage
541+ const PULLING_NEW_IMAGE_ID : & str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0" ;
542+ tracing:: info!(
543+ message_id = PULLING_NEW_IMAGE_ID ,
544+ bootc. image. reference = & imgref. image,
545+ bootc. image. transport = "containers-storage" ,
546+ bootc. original_transport = & imgref. transport,
547+ bootc. status = "importing_from_storage" ,
548+ "Importing image from bootc storage: {}" ,
549+ ostree_imgref
550+ ) ;
551+
552+ let prepared_image = PreparedImportMeta {
553+ imp,
554+ n_layers_to_fetch : layers_to_fetch. len ( ) ,
555+ layers_total : prep. all_layers ( ) . count ( ) ,
556+ bytes_to_fetch : layers_to_fetch. iter ( ) . map ( |( l, _) | l. layer . size ( ) ) . sum ( ) ,
557+ bytes_total : prep. all_layers ( ) . map ( |l| l. layer . size ( ) ) . sum ( ) ,
558+ digest : prep. manifest_digest . clone ( ) ,
559+ prep,
560+ } ;
561+
562+ Ok ( PreparedPullResult :: Ready ( Box :: new ( prepared_image) ) )
563+ }
564+
565+ /// Unified pull: Use podman to pull to containers-storage, then read from there
566+ ///
567+ /// The `sysroot_path` parameter specifies the path to the sysroot where bootc storage is located.
568+ /// For normal upgrade/switch operations, pass `None` to use the default `/sysroot`.
569+ pub ( crate ) async fn pull_unified (
570+ repo : & ostree:: Repo ,
571+ imgref : & ImageReference ,
572+ target_imgref : Option < & OstreeImageReference > ,
573+ quiet : bool ,
574+ prog : ProgressWriter ,
575+ store : & Storage ,
576+ sysroot_path : Option < & camino:: Utf8Path > ,
577+ ) -> Result < Box < ImageState > > {
578+ match prepare_for_pull_unified ( repo, imgref, target_imgref, store, sysroot_path) . await ? {
579+ PreparedPullResult :: AlreadyPresent ( existing) => {
580+ // Log that the image was already present (Debug level since it's not actionable)
581+ const IMAGE_ALREADY_PRESENT_ID : & str = "5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9" ;
582+ tracing:: debug!(
583+ message_id = IMAGE_ALREADY_PRESENT_ID ,
584+ bootc. image. reference = & imgref. image,
585+ bootc. image. transport = & imgref. transport,
586+ bootc. status = "already_present" ,
587+ "Image already present: {}" ,
588+ imgref
589+ ) ;
590+ Ok ( existing)
591+ }
592+ PreparedPullResult :: Ready ( prepared_image_meta) => {
593+ // To avoid duplicate success logs, pass a containers-storage imgref to the importer
594+ let cs_imgref = ImageReference {
595+ transport : "containers-storage" . to_string ( ) ,
596+ image : imgref. image . clone ( ) ,
597+ signature : imgref. signature . clone ( ) ,
598+ } ;
599+ pull_from_prepared ( & cs_imgref, quiet, prog, * prepared_image_meta) . await
600+ }
601+ }
602+ }
603+
384604#[ context( "Pulling" ) ]
385605pub ( crate ) async fn pull_from_prepared (
386606 imgref : & ImageReference ,
@@ -430,18 +650,21 @@ pub(crate) async fn pull_from_prepared(
430650 let imgref_canonicalized = imgref. clone ( ) . canonicalize ( ) ?;
431651 tracing:: debug!( "Canonicalized image reference: {imgref_canonicalized:#}" ) ;
432652
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- ) ;
653+ // Log successful import completion (skip if using unified storage to avoid double logging)
654+ let is_unified_path = imgref. transport == "containers-storage" ;
655+ if !is_unified_path {
656+ const IMPORT_COMPLETE_JOURNAL_ID : & str = "4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8" ;
657+
658+ tracing:: info!(
659+ message_id = IMPORT_COMPLETE_JOURNAL_ID ,
660+ bootc. image. reference = & imgref. image,
661+ bootc. image. transport = & imgref. transport,
662+ bootc. manifest_digest = import. manifest_digest. as_ref( ) ,
663+ bootc. ostree_commit = & import. merge_commit,
664+ "Successfully imported image: {}" ,
665+ imgref
666+ ) ;
667+ }
445668
446669 if let Some ( msg) =
447670 ostree_container:: store:: image_filtered_content_warning ( & import. filtered_files )
@@ -490,6 +713,9 @@ pub(crate) async fn pull(
490713 }
491714}
492715
716+ /// Pull selecting unified vs standard path based on persistent storage config.
717+ // pull_auto was reverted per request; keep explicit callers branching.
718+
493719pub ( crate ) async fn wipe_ostree ( sysroot : Sysroot ) -> Result < ( ) > {
494720 tokio:: task:: spawn_blocking ( move || {
495721 sysroot
0 commit comments