11use fn_error_context:: context;
2+ use std:: process:: Command ;
23use std:: sync:: Arc ;
34
45use anyhow:: { Context , Result } ;
@@ -19,6 +20,7 @@ use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
1920
2021use crate :: composefs_consts:: BOOTC_TAG_PREFIX ;
2122use crate :: install:: { RootSetup , State } ;
23+ use crate :: podstorage:: { CStorage , PullMode } ;
2224
2325/// Create a composefs OCI tag name for the given manifest digest.
2426///
@@ -137,8 +139,81 @@ pub(crate) struct PullRepoResult {
137139 pub ( crate ) manifest_digest : String ,
138140}
139141
140- /// Pulls the `image` from `transport` into a composefs repository at /sysroot
141- /// Checks for boot entries in the image and returns them
142+ /// Pull an image via unified storage: first into bootc-owned containers-storage,
143+ /// then from there into the composefs repository via cstor (zero-copy
144+ /// reflink/hardlink).
145+ ///
146+ /// This ensures the image is available in containers-storage for `podman run`
147+ /// while also populating the composefs repo for booting.
148+ async fn pull_composefs_unified (
149+ rootfs_dir : & Dir ,
150+ repo : & Arc < crate :: store:: ComposefsRepository > ,
151+ transport : & str ,
152+ image : & str ,
153+ ) -> Result < PullResult < Sha512HashValue > > {
154+ // Open/create bootc-owned containers-storage.
155+ let run = Dir :: open_ambient_dir ( "/run" , ambient_authority ( ) ) ?;
156+ let imgstore = CStorage :: create ( rootfs_dir, & run, None ) ?;
157+
158+ // Stage 1: get the image into bootc-owned containers-storage.
159+ let t = transport. strip_suffix ( ':' ) . unwrap_or ( transport) ;
160+ if t == "containers-storage" {
161+ // The image is in the default containers-storage (/var/lib/containers/storage).
162+ // Copy it into bootc-owned storage.
163+ tracing:: info!( "Unified pull: copying {image} from host containers-storage" ) ;
164+ imgstore
165+ . pull_from_host_storage ( image)
166+ . await
167+ . context ( "Copying image from host containers-storage into bootc storage" ) ?;
168+ } else {
169+ // For registry (docker://), oci:, docker-daemon:, etc. — podman
170+ // can pull directly into bootc-owned storage.
171+ let pull_ref = get_imgref ( transport, image) ;
172+ tracing:: info!( "Unified pull: fetching {pull_ref} into containers-storage" ) ;
173+ imgstore
174+ . pull ( & pull_ref, PullMode :: Always )
175+ . await
176+ . context ( "Pulling image into bootc containers-storage" ) ?;
177+ }
178+
179+ // Stage 2: import layers+config from containers-storage into composefs
180+ // via cstor (zero-copy reflink/hardlink from overlay diff/).
181+ let cstor_imgref = format ! ( "containers-storage:{image}" ) ;
182+ tracing:: info!( "Unified pull: importing layers from {cstor_imgref} (zero-copy)" ) ;
183+
184+ let cstor_result = composefs_oci:: pull ( repo, & cstor_imgref, None , None , false )
185+ . await
186+ . context ( "Importing layers from containers-storage into composefs" ) ?;
187+
188+ tracing:: info!(
189+ "Unified pull: cstor import complete (config {}), importing manifest" ,
190+ cstor_result. config_digest
191+ ) ;
192+
193+ // Stage 3: fetch and store the OCI manifest via skopeo.
194+ // The layers+config are already in composefs (imported above), so skopeo
195+ // will see them as AlreadyPresent and only create the manifest splitstream.
196+ let storage_path = format ! ( "/sysroot/{}" , CStorage :: subpath( ) ) ;
197+ let mut config = crate :: deploy:: new_proxy_config ( ) ;
198+ ostree_ext:: container:: merge_default_container_proxy_opts ( & mut config) ?;
199+ let mut cmd = Command :: new ( "skopeo" ) ;
200+ crate :: podstorage:: set_additional_image_store ( & mut cmd, & storage_path) ;
201+ config. skopeo_cmd = Some ( cmd) ;
202+
203+ let ( pull_result, _stats) = composefs_oci_pull_image ( repo, & cstor_imgref, None , Some ( config) )
204+ . await
205+ . context ( "Storing manifest from containers-storage" ) ?;
206+
207+ Ok ( pull_result)
208+ }
209+
210+ /// Pulls the `image` from `transport` into a composefs repository at /sysroot.
211+ ///
212+ /// For registry transports, this uses the unified storage path: the image is
213+ /// first pulled into bootc-owned containers-storage (so it's available for
214+ /// `podman run`), then imported from there into the composefs repo.
215+ ///
216+ /// Checks for boot entries in the image and returns them.
142217#[ context( "Pulling composefs repository" ) ]
143218pub ( crate ) async fn pull_composefs_repo (
144219 transport : & String ,
@@ -165,17 +240,9 @@ pub(crate) async fn pull_composefs_repo(
165240 repo. set_insecure ( ) ;
166241 }
167242
168- let final_imgref = get_imgref ( transport, image) ;
169-
170- tracing:: debug!( "Image to pull {final_imgref}" ) ;
171-
172- let mut config = crate :: deploy:: new_proxy_config ( ) ;
173- ostree_ext:: container:: merge_default_container_proxy_opts ( & mut config) ?;
174-
175243 let repo = Arc :: new ( repo) ;
176- let ( pull_result, _stats) = composefs_oci_pull_image ( & repo, & final_imgref, None , Some ( config) )
177- . await
178- . context ( "Pulling composefs repo" ) ?;
244+
245+ let pull_result = pull_composefs_unified ( & rootfs_dir, & repo, transport, image) . await ?;
179246
180247 // Tag the manifest as a bootc-owned GC root.
181248 let tag = bootc_tag_for_manifest ( & pull_result. manifest_digest . to_string ( ) ) ;
0 commit comments