@@ -4,6 +4,7 @@ use std::collections::{BTreeMap, HashMap};
44use std:: path:: { Path , PathBuf } ;
55
66use anyhow:: { bail, Context , Result } ;
7+ use async_trait:: async_trait;
78use docker_credential:: DockerCredential ;
89use futures_util:: future;
910use futures_util:: stream:: { self , StreamExt , TryStreamExt } ;
@@ -18,7 +19,7 @@ use spin_common::ui::quoted_path;
1819use spin_common:: url:: parse_file_url;
1920use spin_loader:: cache:: Cache ;
2021use spin_loader:: FilesMountStrategy ;
21- use spin_locked_app:: locked:: { ContentPath , ContentRef , LockedApp } ;
22+ use spin_locked_app:: locked:: { ContentPath , ContentRef , LockedApp , LockedComponentSource } ;
2223use tokio:: fs;
2324use walkdir:: WalkDir ;
2425
@@ -67,6 +68,16 @@ enum AssemblyMode {
6768 Archive ,
6869}
6970
71+ /// Indicates whether to compose the components of a Spin application when pushing an image.
72+ #[ derive( Default ) ]
73+ enum ComposeMode {
74+ /// Compose components before pushing the image.
75+ #[ default]
76+ All ,
77+ /// Skip composing components before pushing the image.
78+ Skip ,
79+ }
80+
7081/// Client for interacting with an OCI registry for Spin applications.
7182pub struct Client {
7283 /// Global cache for the metadata, Wasm modules, and static assets pulled from OCI registries.
@@ -119,6 +130,7 @@ impl Client {
119130 reference : impl AsRef < str > ,
120131 annotations : Option < BTreeMap < String , String > > ,
121132 infer_annotations : InferPredefinedAnnotations ,
133+ compose_mode : ComposeMode ,
122134 ) -> Result < Option < String > > {
123135 let reference: Reference = reference
124136 . as_ref ( )
@@ -137,8 +149,15 @@ impl Client {
137149 )
138150 . await ?;
139151
140- self . push_locked_core ( locked, auth, reference, annotations, infer_annotations)
141- . await
152+ self . push_locked_core (
153+ locked,
154+ auth,
155+ reference,
156+ annotations,
157+ infer_annotations,
158+ compose_mode,
159+ )
160+ . await
142161 }
143162
144163 /// Push a Spin application to an OCI registry and return the digest (or None
@@ -149,15 +168,23 @@ impl Client {
149168 reference : impl AsRef < str > ,
150169 annotations : Option < BTreeMap < String , String > > ,
151170 infer_annotations : InferPredefinedAnnotations ,
171+ compose_mode : ComposeMode ,
152172 ) -> Result < Option < String > > {
153173 let reference: Reference = reference
154174 . as_ref ( )
155175 . parse ( )
156176 . with_context ( || format ! ( "cannot parse reference {}" , reference. as_ref( ) ) ) ?;
157177 let auth = Self :: auth ( & reference) . await ?;
158178
159- self . push_locked_core ( locked, auth, reference, annotations, infer_annotations)
160- . await
179+ self . push_locked_core (
180+ locked,
181+ auth,
182+ reference,
183+ annotations,
184+ infer_annotations,
185+ compose_mode,
186+ )
187+ . await
161188 }
162189
163190 /// Push a Spin application to an OCI registry and return the digest (or None
@@ -169,10 +196,11 @@ impl Client {
169196 reference : Reference ,
170197 annotations : Option < BTreeMap < String , String > > ,
171198 infer_annotations : InferPredefinedAnnotations ,
199+ compose_mode : ComposeMode ,
172200 ) -> Result < Option < String > > {
173201 let mut locked_app = locked. clone ( ) ;
174202 let mut layers = self
175- . assemble_layers ( & mut locked_app, AssemblyMode :: Simple )
203+ . assemble_layers ( & mut locked_app, AssemblyMode :: Simple , compose_mode )
176204 . await
177205 . context ( "could not assemble layers for locked application" ) ?;
178206
@@ -183,7 +211,7 @@ impl Client {
183211 {
184212 locked_app = locked. clone ( ) ;
185213 layers = self
186- . assemble_layers ( & mut locked_app, AssemblyMode :: Archive )
214+ . assemble_layers ( & mut locked_app, AssemblyMode :: Archive , compose_mode )
187215 . await
188216 . context ( "could not assemble archive layers for locked application" ) ?;
189217 }
@@ -246,43 +274,59 @@ impl Client {
246274 & mut self ,
247275 locked : & mut LockedApp ,
248276 assembly_mode : AssemblyMode ,
277+ compose_mode : ComposeMode ,
249278 ) -> Result < Vec < ImageLayer > > {
250279 let mut layers = Vec :: new ( ) ;
251280 let mut components = Vec :: new ( ) ;
252281 for mut c in locked. clone ( ) . components {
253- // Add the wasm module for the component as layers.
254- let source = c
255- . clone ( )
256- . source
257- . content
258- . source
259- . context ( "component loaded from disk should contain a file source" ) ?;
282+ match compose_mode {
283+ ComposeMode :: All => {
284+ let composed = spin_compose:: compose ( & ComponentSourceLoader , & c)
285+ . await
286+ . with_context ( || {
287+ format ! ( "failed to resolve dependencies for component {:?}" , c. id)
288+ } ) ?;
289+
290+ let layer = ImageLayer :: new ( composed, WASM_LAYER_MEDIA_TYPE . to_string ( ) , None ) ;
291+ c. source . content = self . content_ref_for_layer ( & layer) ;
292+ c. dependencies . clear ( ) ;
293+ layers. push ( layer) ;
294+ }
295+ ComposeMode :: Skip => {
296+ // Add the wasm module for the component as layers.
297+ let source = c
298+ . clone ( )
299+ . source
300+ . content
301+ . source
302+ . context ( "component loaded from disk should contain a file source" ) ?;
260303
261- let source = parse_file_url ( source. as_str ( ) ) ?;
262- let layer = Self :: wasm_layer ( & source) . await ?;
304+ let source = parse_file_url ( source. as_str ( ) ) ?;
305+ let layer = Self :: wasm_layer ( & source) . await ?;
263306
264- // Update the module source with the content ref of the layer.
265- c. source . content = self . content_ref_for_layer ( & layer) ;
307+ // Update the module source with the content ref of the layer.
308+ c. source . content = self . content_ref_for_layer ( & layer) ;
266309
267- layers. push ( layer) ;
310+ layers. push ( layer) ;
268311
269- let mut deps = BTreeMap :: default ( ) ;
270- for ( dep_name, mut dep) in c. dependencies {
271- let source = dep
272- . source
273- . content
274- . source
275- . context ( "dependency loaded from disk should contain a file source" ) ?;
276- let source = parse_file_url ( source. as_str ( ) ) ?;
312+ let mut deps = BTreeMap :: default ( ) ;
313+ for ( dep_name, mut dep) in c. dependencies {
314+ let source =
315+ dep. source . content . source . context (
316+ "dependency loaded from disk should contain a file source" ,
317+ ) ?;
318+ let source = parse_file_url ( source. as_str ( ) ) ?;
277319
278- let layer = Self :: wasm_layer ( & source) . await ?;
320+ let layer = Self :: wasm_layer ( & source) . await ?;
279321
280- dep. source . content = self . content_ref_for_layer ( & layer) ;
281- deps. insert ( dep_name, dep) ;
322+ dep. source . content = self . content_ref_for_layer ( & layer) ;
323+ deps. insert ( dep_name, dep) ;
282324
283- layers. push ( layer) ;
325+ layers. push ( layer) ;
326+ }
327+ c. dependencies = deps;
328+ }
284329 }
285- c. dependencies = deps;
286330
287331 let mut files = Vec :: new ( ) ;
288332 for f in c. files {
@@ -669,6 +713,32 @@ impl Client {
669713 }
670714}
671715
716+ struct ComponentSourceLoader ;
717+
718+ #[ async_trait]
719+ impl spin_compose:: ComponentSourceLoader for ComponentSourceLoader {
720+ async fn load_component_source (
721+ & self ,
722+ source : & LockedComponentSource ,
723+ ) -> anyhow:: Result < Vec < u8 > > {
724+ let source = source
725+ . content
726+ . source
727+ . as_ref ( )
728+ . context ( "component loaded from disk should contain a file source" ) ?;
729+
730+ let source = parse_file_url ( source. as_str ( ) ) ?;
731+
732+ let bytes = fs:: read ( & source)
733+ . await
734+ . with_context ( || format ! ( "cannot read wasm module {}" , quoted_path( source) ) ) ?;
735+
736+ let component = spin_componentize:: componentize_if_necessary ( & bytes) ?;
737+
738+ Ok ( component. into ( ) )
739+ }
740+ }
741+
672742/// Unpack contents of the provided archive layer, represented by bytes and its
673743/// corresponding digest, into the provided cache.
674744/// A temporary staging directory is created via tempfile::tempdir() to store
@@ -946,6 +1016,7 @@ mod test {
9461016 locked_components : Vec < LockedComponent > ,
9471017 expected_layer_count : usize ,
9481018 expected_error : Option < & ' static str > ,
1019+ compose_mode : ComposeMode ,
9491020 }
9501021
9511022 let tests: Vec < TestCase > = [
@@ -968,6 +1039,7 @@ mod test {
9681039 } } ] ) ,
9691040 expected_layer_count : 2 ,
9701041 expected_error : None ,
1042+ compose_mode : ComposeMode :: Skip ,
9711043 } ,
9721044 TestCase {
9731045 name : "One component layer and two file layers" ,
@@ -992,6 +1064,7 @@ mod test {
9921064 } ] ) ,
9931065 expected_layer_count : 3 ,
9941066 expected_error : None ,
1067+ compose_mode : ComposeMode :: Skip ,
9951068 } ,
9961069 TestCase {
9971070 name : "One component layer and one file with inlined content" ,
@@ -1012,6 +1085,7 @@ mod test {
10121085 } ] ) ,
10131086 expected_layer_count : 1 ,
10141087 expected_error : None ,
1088+ compose_mode : ComposeMode :: Skip ,
10151089 } ,
10161090 TestCase {
10171091 name : "One component layer and one dependency component layer" ,
@@ -1036,6 +1110,7 @@ mod test {
10361110 } ] ) ,
10371111 expected_layer_count : 2 ,
10381112 expected_error : None ,
1113+ compose_mode : ComposeMode :: Skip ,
10391114 } ,
10401115 TestCase {
10411116 name : "Component has no source" ,
@@ -1050,6 +1125,7 @@ mod test {
10501125 } ] ) ,
10511126 expected_layer_count : 0 ,
10521127 expected_error : Some ( "Invalid URL: \" \" " ) ,
1128+ compose_mode : ComposeMode :: Skip ,
10531129 } ,
10541130 TestCase {
10551131 name : "Duplicate component sources" ,
@@ -1070,6 +1146,7 @@ mod test {
10701146 } } ] ) ,
10711147 expected_layer_count : 1 ,
10721148 expected_error : None ,
1149+ compose_mode : ComposeMode :: Skip ,
10731150 } ,
10741151 TestCase {
10751152 name : "Duplicate file paths" ,
@@ -1107,6 +1184,7 @@ mod test {
11071184 } ] ) ,
11081185 expected_layer_count : 4 ,
11091186 expected_error : None ,
1187+ compose_mode : ComposeMode :: Skip ,
11101188 } ,
11111189 ]
11121190 . to_vec ( ) ;
@@ -1137,7 +1215,7 @@ mod test {
11371215 assert_eq ! (
11381216 e,
11391217 client
1140- . assemble_layers( & mut locked, AssemblyMode :: Simple )
1218+ . assemble_layers( & mut locked, AssemblyMode :: Simple , tc . compose_mode )
11411219 . await
11421220 . unwrap_err( )
11431221 . to_string( ) ,
@@ -1149,7 +1227,7 @@ mod test {
11491227 assert_eq ! (
11501228 tc. expected_layer_count,
11511229 client
1152- . assemble_layers( & mut locked, AssemblyMode :: Simple )
1230+ . assemble_layers( & mut locked, AssemblyMode :: Simple , tc . compose_mode )
11531231 . await
11541232 . unwrap( )
11551233 . len( ) ,
0 commit comments