1- #![ allow( dead_code, unused_variables) ]
2-
3- use std:: io:: { Read , Seek , Write } ;
1+ use std:: { fs:: File , io:: Read , os:: fd:: AsRawFd } ;
42
53use anyhow:: { Context , Result } ;
6- use canon_json:: CanonJsonSerialize ;
7- use cap_std_ext:: cap_std:: {
8- ambient_authority,
9- fs:: { Dir , MetadataExt , OpenOptions } ,
10- } ;
4+ use cap_std_ext:: cap_std:: { ambient_authority, fs:: Dir } ;
115use composefs:: {
126 fsverity:: FsVerityHashValue ,
137 splitstream:: { SplitStreamData , SplitStreamReader } ,
148 tree:: { LeafContent , RegularFile } ,
159} ;
1610use composefs_oci:: tar:: TarItem ;
17- use openssl:: sha:: Sha256 ;
18- use ostree_ext:: oci_spec:: image:: { Descriptor , Digest , ImageConfiguration , MediaType } ;
11+ use ocidir:: { oci_spec:: image:: Platform , OciDir } ;
12+ use ostree_ext:: container:: skopeo;
13+ use ostree_ext:: { container:: Transport , oci_spec:: image:: ImageConfiguration } ;
1914use tar:: { EntryType , Header } ;
2015
2116use crate :: {
2217 bootc_composefs:: {
2318 status:: { get_composefs_status, get_imginfo} ,
2419 update:: str_to_sha256digest,
2520 } ,
21+ image:: IMAGE_DEFAULT ,
2622 store:: { BootedComposefs , Storage } ,
2723} ;
2824
@@ -58,10 +54,16 @@ fn get_entry_with_header<R: Read, ObjectID: FsVerityHashValue>(
5854 }
5955}
6056
61- pub async fn export_repo_to_oci ( storage : & Storage , booted_cfs : & BootedComposefs ) -> Result < ( ) > {
57+ /// Exports a composefs repository to a container image in containers-storage:
58+ pub async fn export_repo_to_image (
59+ storage : & Storage ,
60+ booted_cfs : & BootedComposefs ,
61+ source : Option < & str > ,
62+ target : Option < & str > ,
63+ ) -> Result < ( ) > {
6264 let host = get_composefs_status ( storage, booted_cfs) . await ?;
6365
64- let image = host
66+ let booted_image = host
6567 . status
6668 . booted
6769 . as_ref ( )
@@ -70,27 +72,61 @@ pub async fn export_repo_to_oci(storage: &Storage, booted_cfs: &BootedComposefs)
7072 . as_ref ( )
7173 . unwrap ( ) ;
7274
73- let imginfo = get_imginfo (
74- storage,
75- & booted_cfs. cmdline . digest ,
76- // TODO: Make this optional
77- & image. image ,
78- )
79- . await ?;
75+ // If the target isn't specified, push to containers-storage + our default image
76+ let dest_imgref = match target {
77+ Some ( target) => ostree_ext:: container:: ImageReference {
78+ transport : Transport :: ContainerStorage ,
79+ name : target. to_owned ( ) ,
80+ } ,
81+ None => ostree_ext:: container:: ImageReference {
82+ transport : Transport :: ContainerStorage ,
83+ name : IMAGE_DEFAULT . into ( ) ,
84+ } ,
85+ } ;
86+
87+ // If the source isn't specified, we use the booted image
88+ let source = match source {
89+ Some ( source) => ostree_ext:: container:: ImageReference :: try_from ( source)
90+ . context ( "Parsing source image" ) ?,
91+
92+ None => ostree_ext:: container:: ImageReference {
93+ transport : Transport :: try_from ( booted_image. image . transport . as_str ( ) ) . unwrap ( ) ,
94+ name : booted_image. image . image . clone ( ) ,
95+ } ,
96+ } ;
97+
98+ let mut depl_verity = None ;
99+
100+ for depl in host
101+ . status
102+ . booted
103+ . iter ( )
104+ . chain ( host. status . staged . iter ( ) )
105+ . chain ( host. status . rollback . iter ( ) )
106+ . chain ( host. status . other_deployments . iter ( ) )
107+ {
108+ let img = & depl. image . as_ref ( ) . unwrap ( ) . image ;
109+
110+ // Not checking transport here as we'll be pulling from the repo anyway
111+ // So, image name is all we need
112+ if img. image == source. name {
113+ depl_verity = Some ( depl. require_composefs ( ) ?. verity . clone ( ) ) ;
114+ break ;
115+ }
116+ }
117+
118+ let depl_verity = depl_verity. ok_or_else ( || anyhow:: anyhow!( "Image {source} not found" ) ) ?;
80119
81- let config_name = & image. image_digest ;
82- let config_name = str_to_sha256digest ( & config_name) ?;
120+ let imginfo = get_imginfo ( storage, & depl_verity, None ) . await ?;
121+
122+ let config_name = & imginfo. manifest . config ( ) . digest ( ) . digest ( ) ;
123+ let config_name = str_to_sha256digest ( config_name) ?;
83124
84125 let var_tmp =
85126 Dir :: open_ambient_dir ( "/var/tmp" , ambient_authority ( ) ) . context ( "Opening /var/tmp" ) ?;
86127
87- var_tmp
88- . create_dir_all ( & * booted_cfs. cmdline . digest )
89- . context ( "Creating image dir" ) ?;
90-
91- let image_dir = var_tmp
92- . open_dir ( & * booted_cfs. cmdline . digest )
93- . context ( "Opening image dir" ) ?;
128+ let tmpdir = cap_std_ext:: cap_tempfile:: tempdir_in ( & var_tmp) ?;
129+ let oci_dir = OciDir :: ensure ( tmpdir. try_clone ( ) ?) . context ( "Opening OCI" ) ?;
94130
95131 let mut config_stream = booted_cfs
96132 . repo
@@ -99,7 +135,8 @@ pub async fn export_repo_to_oci(storage: &Storage, booted_cfs: &BootedComposefs)
99135
100136 let config = ImageConfiguration :: from_reader ( & mut config_stream) ?;
101137
102- // We can't guarantee that we'll get the same tar as the container image
138+ // We can't guarantee that we'll get the same tar stream as the container image
139+ // So we create new config and manifest
103140 let mut new_config = config. clone ( ) ;
104141 if let Some ( history) = new_config. history_mut ( ) {
105142 history. clear ( ) ;
@@ -109,163 +146,113 @@ pub async fn export_repo_to_oci(storage: &Storage, booted_cfs: &BootedComposefs)
109146 let mut new_manifest = imginfo. manifest . clone ( ) ;
110147 new_manifest. layers_mut ( ) . clear ( ) ;
111148
112- let mut file_open_opts = OpenOptions :: new ( ) ;
113- file_open_opts. write ( true ) . create ( true ) ;
114-
115- for ( idx, diff_id) in config. rootfs ( ) . diff_ids ( ) . iter ( ) . enumerate ( ) {
116- let layer_sha256 = str_to_sha256digest ( diff_id) ?;
149+ for ( idx, old_diff_id) in config. rootfs ( ) . diff_ids ( ) . iter ( ) . enumerate ( ) {
150+ let layer_sha256 = str_to_sha256digest ( old_diff_id) ?;
117151 let layer_verity = config_stream. lookup ( & layer_sha256) ?;
118152
119153 let mut layer_stream = booted_cfs
120154 . repo
121155 . open_stream ( & hex:: encode ( layer_sha256) , Some ( layer_verity) ) ?;
122156
123- let mut file = image_dir. open_with ( hex:: encode ( layer_sha256) , & file_open_opts) ?;
124-
125- let mut builder = tar:: Builder :: new ( & mut file) ;
157+ let mut layer_writer = oci_dir. create_layer ( None ) ?;
158+ layer_writer. follow_symlinks ( false ) ;
126159
127160 while let Some ( ( header, entry) ) = get_entry_with_header ( & mut layer_stream) ? {
128161 let hsize = header. size ( ) ? as usize ;
129162 let mut v = vec ! [ 0 ; hsize] ;
130163
131164 match & entry {
132- TarItem :: Directory => {
133- assert_eq ! ( hsize, 0 ) ;
134- }
135-
136165 TarItem :: Leaf ( leaf_content) => {
137166 match & leaf_content {
138167 LeafContent :: Regular ( reg) => match reg {
139168 RegularFile :: Inline ( items) => {
140- assert_eq ! ( hsize, items. len( ) ) ;
141169 v[ ..hsize] . copy_from_slice ( items) ;
142170 }
143171
144- RegularFile :: External ( obj_id, size) => {
145- assert_eq ! ( * size as usize , hsize) ;
146-
147- let mut file =
148- std:: fs:: File :: from ( booted_cfs. repo . open_object ( obj_id) ?) ;
149-
172+ RegularFile :: External ( obj_id, ..) => {
173+ let mut file = File :: from ( booted_cfs. repo . open_object ( obj_id) ?) ;
150174 file. read_exact ( & mut v) ?;
151175 }
152176 } ,
153177
154- LeafContent :: BlockDevice ( _) => todo ! ( ) ,
155- LeafContent :: CharacterDevice ( _) => {
156- todo ! ( )
157- }
158- LeafContent :: Fifo => todo ! ( ) ,
159- LeafContent :: Socket => todo ! ( ) ,
160-
161- LeafContent :: Symlink ( ..) => {
162- // we don't need to write the data for symlinks as the
163- // target will be in the header itself
164- assert_eq ! ( hsize, 0 ) ;
165- }
178+ // we don't need to write the data for symlinks.
179+ // Same goes for devices, fifos and sockets
180+ _ => { }
166181 }
167182 }
168183
169- TarItem :: Hardlink ( ..) => {
170- // we don't need to write the data for hardlinks as the
171- // target will be in the header itself
172- assert_eq ! ( hsize, 0 ) ;
173- }
184+ // we don't need to write the data for hardlinks/dirs
185+ TarItem :: Directory | TarItem :: Hardlink ( ..) => { }
174186 } ;
175187
176- builder
188+ layer_writer
177189 . append ( & header, v. as_slice ( ) )
178190 . context ( "Failed to write entry" ) ?;
179191 }
180192
181- builder. finish ( ) . context ( "Finishing builder" ) ?;
182- drop ( builder) ;
183-
184- let mut new_diff_id = openssl:: hash:: Hasher :: new ( openssl:: hash:: MessageDigest :: sha256 ( ) ) ?;
185-
186- file. seek ( std:: io:: SeekFrom :: Start ( 0 ) )
187- . context ( "Seek failed" ) ?;
188- std:: io:: copy ( & mut file, & mut new_diff_id) . context ( "Failed to compute hash" ) ?;
189-
190- let final_sha = new_diff_id. finish ( ) ?;
191- let final_sha_str = hex:: encode ( final_sha) ;
192-
193- rustix:: fs:: renameat ( & image_dir, hex:: encode ( layer_sha256) , & image_dir, & final_sha_str)
194- . context ( "Renameat" ) ?;
195-
196- let digest = format ! ( "sha256:{}" , hex:: encode( final_sha) ) ;
197-
198- new_config. rootfs_mut ( ) . diff_ids_mut ( ) . push ( digest. clone ( ) ) ;
199-
200- // TODO: Gzip this for manifest
201- new_manifest. layers_mut ( ) . push ( Descriptor :: new (
202- MediaType :: ImageLayer ,
203- file. metadata ( ) ?. size ( ) ,
204- Digest :: try_from ( digest) ?,
205- ) ) ;
206-
207- if let Some ( old_history) = & config. history ( ) {
208- if idx >= old_history. len ( ) {
209- anyhow:: bail!( "Found more layers than history" ) ;
210- }
211-
212- let old_history = & old_history[ idx] ;
213-
214- let mut history = ostree_ext:: oci_spec:: image:: HistoryBuilder :: default ( ) ;
215-
216- if let Some ( old_created) = old_history. created ( ) {
217- history = history. created ( old_created) ;
218- }
219-
220- if let Some ( old_created_by) = old_history. created_by ( ) {
221- history = history. created_by ( old_created_by) ;
222- }
223-
224- if let Some ( comment) = old_history. comment ( ) {
225- history = history. comment ( comment) ;
226- }
227-
228- new_config
229- . history_mut ( )
230- . get_or_insert ( Vec :: new ( ) )
231- . push ( history. build ( ) . unwrap ( ) ) ;
232- }
233-
234- // TODO: Fsync
193+ layer_writer. finish ( ) ?;
194+
195+ let layer = layer_writer
196+ . into_inner ( )
197+ . context ( "Getting inner layer writer" ) ?
198+ . complete ( )
199+ . context ( "Writing layer to disk" ) ?;
200+
201+ tracing:: debug!( "Wrote layer: {}" , layer. uncompressed_sha256_as_digest( ) ) ;
202+
203+ let previous_annotations = imginfo
204+ . manifest
205+ . layers ( )
206+ . get ( idx)
207+ . and_then ( |l| l. annotations ( ) . as_ref ( ) )
208+ . cloned ( ) ;
209+
210+ let history = imginfo. config . history ( ) . as_ref ( ) ;
211+ let history_entry = history. and_then ( |v| v. get ( idx) ) ;
212+ let previous_description = history_entry
213+ . clone ( )
214+ . and_then ( |h| h. comment ( ) . as_deref ( ) )
215+ . unwrap_or_default ( ) ;
216+
217+ let previous_created = history_entry
218+ . and_then ( |h| h. created ( ) . as_deref ( ) )
219+ . and_then ( bootc_utils:: try_deserialize_timestamp)
220+ . unwrap_or_default ( ) ;
221+
222+ oci_dir. push_layer_full (
223+ & mut new_manifest,
224+ & mut new_config,
225+ layer,
226+ previous_annotations,
227+ previous_description,
228+ previous_created,
229+ ) ;
235230 }
236231
237- let config_json = new_config. to_canon_json_vec ( ) ?;
238-
239- // Hash the new config
240- let mut config_hash = Sha256 :: new ( ) ;
241- config_hash. update ( & config_json) ;
242- let config_hash = hex:: encode ( config_hash. finish ( ) ) ;
243-
244- // Write the config to Directory
245- let mut cfg_file = image_dir
246- . open_with ( & config_hash, & file_open_opts)
247- . context ( "Opening config file" ) ?;
248-
249- cfg_file
250- . write_all ( & config_json)
251- . context ( "Failed to write config" ) ?;
252-
253- // Write the manifest
254- let mut manifest_file = image_dir
255- . open_with ( "manifest.json" , & file_open_opts)
256- . context ( "Opening manifest file" ) ?;
257-
258- new_manifest. set_config ( Descriptor :: new (
259- MediaType :: ImageConfig ,
260- config_json. len ( ) as u64 ,
261- Digest :: try_from ( format ! ( "sha256:{config_hash}" ) ) ?,
262- ) ) ;
263-
264- manifest_file
265- . write_all ( & new_manifest. to_canon_json_vec ( ) ?)
266- . context ( "Failed to write manifest" ) ?;
267-
268- println ! ( "Image: {config_hash}" ) ;
232+ let descriptor = oci_dir. write_config ( new_config) . context ( "Writing config" ) ?;
233+
234+ new_manifest. set_config ( descriptor) ;
235+ oci_dir
236+ . insert_manifest ( new_manifest, None , Platform :: default ( ) )
237+ . context ( "Writing manifest" ) ?;
238+
239+ // Pass the temporary oci directory as the current working directory for the skopeo process
240+ let tempoci = ostree_ext:: container:: ImageReference {
241+ transport : Transport :: OciDir ,
242+ name : format ! ( "/proc/self/fd/{}" , tmpdir. as_raw_fd( ) ) ,
243+ } ;
244+
245+ skopeo:: copy (
246+ & tempoci,
247+ & dest_imgref,
248+ None ,
249+ Some ( (
250+ std:: sync:: Arc :: new ( tmpdir. try_clone ( ) ?. into ( ) ) ,
251+ tmpdir. as_raw_fd ( ) ,
252+ ) ) ,
253+ true ,
254+ )
255+ . await ?;
269256
270257 Ok ( ( ) )
271258}
0 commit comments