@@ -9,16 +9,17 @@ use cfsctl::composefs_oci;
99use composefs:: fsverity:: { FsVerityHashValue , Sha512HashValue } ;
1010use composefs_boot:: bootloader:: { BootEntry as ComposefsBootEntry , get_boot_resources} ;
1111use composefs_oci:: {
12- image:: create_filesystem as create_composefs_filesystem,
13- pull_image as composefs_oci_pull_image, skopeo:: PullResult , tag_image,
12+ PullOptions , PullResult , image:: create_filesystem as create_composefs_filesystem, tag_image,
1413} ;
1514
16- use ostree_ext:: container :: ImageReference as OstreeExtImgRef ;
15+ use ostree_ext:: containers_image_proxy ;
1716
1817use cap_std_ext:: cap_std:: { ambient_authority, fs:: Dir } ;
1918
2019use crate :: composefs_consts:: BOOTC_TAG_PREFIX ;
2120use crate :: install:: { RootSetup , State } ;
21+ use crate :: lsm;
22+ use crate :: podstorage:: CStorage ;
2223
2324/// Create a composefs OCI tag name for the given manifest digest.
2425///
@@ -69,24 +70,30 @@ pub(crate) async fn initialize_composefs_repository(
6970 repo. set_insecure ( ) ;
7071 }
7172
72- let OstreeExtImgRef {
73- name : image_name,
74- transport,
75- } = & state. source . imageref ;
73+ let imgref = get_imgref ( & transport. to_string ( ) , image_name) ?;
74+
75+ // On a composefs install, containers-storage lives physically under
76+ // composefs/bootc/storage with a compatibility symlink at
77+ // ostree/bootc -> ../composefs/bootc so the existing /usr/lib/bootc/storage
78+ // symlink (and all runtime code using ostree/bootc/storage) keeps working.
79+ crate :: store:: ensure_composefs_bootc_link ( rootfs_dir) ?;
7680
77- let mut config = crate :: deploy:: new_proxy_config ( ) ;
78- ostree_ext:: container:: merge_default_container_proxy_opts ( & mut config) ?;
81+ // Use the unified path: first into containers-storage on the target
82+ // rootfs, then cstor zero-copy into composefs. This ensures the image
83+ // is available for `podman run` from first boot.
84+ let sepolicy = state. load_policy ( ) ?;
85+ let run = Dir :: open_ambient_dir ( "/run" , ambient_authority ( ) ) ?;
86+ let imgstore = CStorage :: create ( rootfs_dir, & run, sepolicy. as_ref ( ) ) ?;
87+ let storage_path = root_setup. physical_root_path . join ( CStorage :: subpath ( ) ) ;
7988
80- // Pull without a reference tag; we tag explicitly afterward so we
81- // control the tag name format.
8289 let repo = Arc :: new ( repo) ;
83- let ( pull_result, _stats ) = composefs_oci_pull_image (
84- & repo,
85- & format ! ( "{transport}{image_name}" ) ,
86- None ,
87- Some ( config ) ,
88- )
89- . await ?;
90+ let pull_result =
91+ pull_composefs_unified ( & imgstore , storage_path . as_str ( ) , & repo, & imgref ) . await ? ;
92+
93+ // SELinux-label the containers-storage now that all pulls are done.
94+ imgstore
95+ . ensure_labeled ( )
96+ . context ( "SELinux labeling of containers-storage" ) ?;
9097
9198 // Tag the manifest as a bootc-owned GC root.
9299 let tag = bootc_tag_for_manifest ( & pull_result. manifest_digest . to_string ( ) ) ;
@@ -107,24 +114,26 @@ pub(crate) async fn initialize_composefs_repository(
107114 Ok ( pull_result)
108115}
109116
110- /// skopeo (in composefs-rs) doesn't understand "registry:"
111- /// This function will convert it to "docker://" and return the image ref
117+ /// Convert a transport string and image name into a `containers_image_proxy::ImageReference`.
112118///
113- /// Ex
114- /// docker://quay.io/some-image
115- /// containers-storage:some-image
116- /// docker-daemon:some-image-id
117- pub ( crate ) fn get_imgref ( transport : & str , image : & str ) -> String {
118- let img = image. strip_prefix ( ":" ) . unwrap_or ( & image) ;
119- let transport = transport. strip_suffix ( ":" ) . unwrap_or ( & transport) ;
120-
121- if transport == "registry" || transport == "docker://" {
122- format ! ( "docker://{img}" )
123- } else if transport == "docker-daemon" {
124- format ! ( "docker-daemon:{img}" )
125- } else {
126- format ! ( "{transport}:{img}" )
127- }
119+ /// The `spec::ImageReference` stores transport as a string (e.g. "registry:",
120+ /// "containers-storage:"). This parses that into a proper typed reference
121+ /// that renders correctly for skopeo (e.g. "docker://quay.io/some-image").
122+ pub ( crate ) fn get_imgref (
123+ transport : & str ,
124+ image : & str ,
125+ ) -> Result < containers_image_proxy:: ImageReference > {
126+ let img = image. strip_prefix ( ':' ) . unwrap_or ( image) ;
127+ // Normalize: strip trailing separator if present, then parse
128+ // via containers_image_proxy::Transport for proper typed handling.
129+ let transport_str = transport. strip_suffix ( ':' ) . unwrap_or ( transport) ;
130+ // Build a canonical imgref string so Transport::try_from can parse it.
131+ let imgref_str = format ! ( "{transport_str}:{img}" ) ;
132+ let transport: containers_image_proxy:: Transport = imgref_str
133+ . as_str ( )
134+ . try_into ( )
135+ . with_context ( || format ! ( "Parsing transport from '{imgref_str}'" ) ) ?;
136+ Ok ( containers_image_proxy:: ImageReference :: new ( transport, img) )
128137}
129138
130139/// Result of pulling a composefs repository, including the OCI manifest digest
@@ -137,25 +146,89 @@ pub(crate) struct PullRepoResult {
137146 pub ( crate ) manifest_digest : String ,
138147}
139148
140- /// Pulls the `image` from `transport` into a composefs repository at /sysroot
141- /// Checks for boot entries in the image and returns them
149+ /// Pull an image via unified storage: first into bootc-owned containers-storage,
150+ /// then from there into the composefs repository via cstor (zero-copy
151+ /// reflink/hardlink).
152+ ///
153+ /// The caller provides:
154+ /// - `imgstore`: the bootc-owned `CStorage` instance (may be on an arbitrary
155+ /// mount point during install, or under `/sysroot` during upgrade)
156+ /// - `storage_path`: the absolute filesystem path to that containers-storage
157+ /// directory, so cstor and skopeo can find it (e.g.
158+ /// `/mnt/sysroot/ostree/bootc/storage` during install, or
159+ /// `/sysroot/ostree/bootc/storage` during upgrade)
160+ ///
161+ /// This ensures the image is available in containers-storage for `podman run`
162+ /// while also populating the composefs repo for booting.
163+ async fn pull_composefs_unified (
164+ imgstore : & CStorage ,
165+ storage_path : & str ,
166+ repo : & Arc < crate :: store:: ComposefsRepository > ,
167+ imgref : & containers_image_proxy:: ImageReference ,
168+ ) -> Result < PullResult < Sha512HashValue > > {
169+ let image = & imgref. name ;
170+
171+ // Stage 1: get the image into bootc-owned containers-storage.
172+ if imgref. transport == containers_image_proxy:: Transport :: ContainerStorage {
173+ // The image is in the default containers-storage (/var/lib/containers/storage).
174+ // Copy it into bootc-owned storage.
175+ tracing:: info!( "Unified pull: copying {image} from host containers-storage" ) ;
176+ imgstore
177+ . pull_from_host_storage ( image)
178+ . await
179+ . context ( "Copying image from host containers-storage into bootc storage" ) ?;
180+ } else {
181+ // For registry (docker://), oci:, docker-daemon:, etc. — pull
182+ // via the native podman API with streaming progress display.
183+ let pull_ref = imgref. to_string ( ) ;
184+ tracing:: info!( "Unified pull: fetching {pull_ref} into containers-storage" ) ;
185+ imgstore
186+ . pull_with_progress ( & pull_ref)
187+ . await
188+ . context ( "Pulling image into bootc containers-storage" ) ?;
189+ }
190+
191+ // Stage 2: import full OCI structure (layers + config + manifest) from
192+ // containers-storage into composefs via cstor (zero-copy reflink/hardlink).
193+ let cstor_imgref_str = format ! ( "containers-storage:{image}" ) ;
194+ tracing:: info!( "Unified pull: importing from {cstor_imgref_str} (zero-copy)" ) ;
195+
196+ let storage = std:: path:: Path :: new ( storage_path) ;
197+ let pull_opts = PullOptions {
198+ additional_image_stores : & [ storage] ,
199+ ..Default :: default ( )
200+ } ;
201+ let pull_result = composefs_oci:: pull ( repo, & cstor_imgref_str, None , pull_opts)
202+ . await
203+ . context ( "Importing from containers-storage into composefs" ) ?;
204+
205+ Ok ( pull_result)
206+ }
207+
208+ /// Pulls the `image` from `transport` into a composefs repository at /sysroot.
209+ ///
210+ /// For registry transports, this uses the unified storage path: the image is
211+ /// first pulled into bootc-owned containers-storage (so it's available for
212+ /// `podman run`), then imported from there into the composefs repo.
213+ ///
214+ /// Checks for boot entries in the image and returns them.
142215#[ context( "Pulling composefs repository" ) ]
143216pub ( crate ) async fn pull_composefs_repo (
144- transport : & String ,
145- image : & String ,
217+ transport : & str ,
218+ image : & str ,
146219 allow_missing_fsverity : bool ,
147220) -> Result < PullRepoResult > {
148221 const COMPOSEFS_PULL_JOURNAL_ID : & str = "4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8" ;
149222
223+ let imgref = get_imgref ( transport, image) ?;
224+
150225 tracing:: info!(
151226 message_id = COMPOSEFS_PULL_JOURNAL_ID ,
152227 bootc. operation = "pull" ,
153228 bootc. source_image = image,
154- bootc. transport = transport,
229+ bootc. transport = %imgref . transport,
155230 bootc. allow_missing_fsverity = allow_missing_fsverity,
156- "Pulling composefs image {}:{}" ,
157- transport,
158- image
231+ "Pulling composefs image {imgref}" ,
159232 ) ;
160233
161234 let rootfs_dir = Dir :: open_ambient_dir ( "/sysroot" , ambient_authority ( ) ) ?;
@@ -165,17 +238,18 @@ pub(crate) async fn pull_composefs_repo(
165238 repo. set_insecure ( ) ;
166239 }
167240
168- let final_imgref = get_imgref ( transport , image ) ;
241+ let repo = Arc :: new ( repo ) ;
169242
170- tracing:: debug!( "Image to pull {final_imgref}" ) ;
243+ // Create bootc-owned containers-storage on the rootfs.
244+ // Load SELinux policy from the running system so newly pulled layers
245+ // get the correct container_var_lib_t labels.
246+ let root = Dir :: open_ambient_dir ( "/" , ambient_authority ( ) ) ?;
247+ let sepolicy = lsm:: new_sepolicy_at ( & root) ?;
248+ let run = Dir :: open_ambient_dir ( "/run" , ambient_authority ( ) ) ?;
249+ let imgstore = CStorage :: create ( & rootfs_dir, & run, sepolicy. as_ref ( ) ) ?;
250+ let storage_path = format ! ( "/sysroot/{}" , CStorage :: subpath( ) ) ;
171251
172- let mut config = crate :: deploy:: new_proxy_config ( ) ;
173- ostree_ext:: container:: merge_default_container_proxy_opts ( & mut config) ?;
174-
175- 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" ) ?;
252+ let pull_result = pull_composefs_unified ( & imgstore, & storage_path, & repo, & imgref) . await ?;
179253
180254 // Tag the manifest as a bootc-owned GC root.
181255 let tag = bootc_tag_for_manifest ( & pull_result. manifest_digest . to_string ( ) ) ;
@@ -227,39 +301,41 @@ mod tests {
227301
228302 #[ test]
229303 fn test_get_imgref_registry_transport ( ) {
230- assert_eq ! (
231- get_imgref ( "registry:" , IMAGE_NAME ) ,
232- format! ( "docker://{ IMAGE_NAME}" )
233- ) ;
304+ let r = get_imgref ( "registry:" , IMAGE_NAME ) . unwrap ( ) ;
305+ assert_eq ! ( r . transport , containers_image_proxy :: Transport :: Registry ) ;
306+ assert_eq ! ( r . name , IMAGE_NAME ) ;
307+ assert_eq ! ( r . to_string ( ) , format! ( "docker://{IMAGE_NAME}" ) ) ;
234308 }
235309
236310 #[ test]
237311 fn test_get_imgref_containers_storage ( ) {
312+ let r = get_imgref ( "containers-storage" , IMAGE_NAME ) . unwrap ( ) ;
238313 assert_eq ! (
239- get_imgref ( "containers-storage" , IMAGE_NAME ) ,
240- format! ( "containers-storage:{IMAGE_NAME}" )
314+ r . transport ,
315+ containers_image_proxy :: Transport :: ContainerStorage
241316 ) ;
317+ assert_eq ! ( r. name, IMAGE_NAME ) ;
242318
319+ let r = get_imgref ( "containers-storage:" , IMAGE_NAME ) . unwrap ( ) ;
243320 assert_eq ! (
244- get_imgref ( "containers-storage:" , IMAGE_NAME ) ,
245- format! ( "containers-storage:{IMAGE_NAME}" )
321+ r . transport ,
322+ containers_image_proxy :: Transport :: ContainerStorage
246323 ) ;
324+ assert_eq ! ( r. name, IMAGE_NAME ) ;
247325 }
248326
249327 #[ test]
250328 fn test_get_imgref_edge_cases ( ) {
251- assert_eq ! (
252- get_imgref( "registry" , IMAGE_NAME ) ,
253- format!( "docker://{IMAGE_NAME}" )
254- ) ;
329+ let r = get_imgref ( "registry" , IMAGE_NAME ) . unwrap ( ) ;
330+ assert_eq ! ( r. transport, containers_image_proxy:: Transport :: Registry ) ;
331+ assert_eq ! ( r. to_string( ) , format!( "docker://{IMAGE_NAME}" ) ) ;
255332 }
256333
257334 #[ test]
258335 fn test_get_imgref_docker_daemon_transport ( ) {
259- assert_eq ! (
260- get_imgref( "docker-daemon" , IMAGE_NAME ) ,
261- format!( "docker-daemon:{IMAGE_NAME}" )
262- ) ;
336+ let r = get_imgref ( "docker-daemon" , IMAGE_NAME ) . unwrap ( ) ;
337+ assert_eq ! ( r. transport, containers_image_proxy:: Transport :: DockerDaemon ) ;
338+ assert_eq ! ( r. name, IMAGE_NAME ) ;
263339 }
264340
265341 #[ test]
0 commit comments