@@ -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,15 @@ enum AssemblyMode {
6768 Archive ,
6869}
6970
71+ /// Indicates whether to compose the components of a Spin application when pushing an image.
72+ #[ derive( Copy , Clone ) ]
73+ pub enum ComposeMode {
74+ /// Compose components before pushing the image.
75+ All ,
76+ /// Skip composing components before pushing the image.
77+ Skip ,
78+ }
79+
7080/// Client for interacting with an OCI registry for Spin applications.
7181pub struct Client {
7282 /// Global cache for the metadata, Wasm modules, and static assets pulled from OCI registries.
@@ -119,6 +129,7 @@ impl Client {
119129 reference : impl AsRef < str > ,
120130 annotations : Option < BTreeMap < String , String > > ,
121131 infer_annotations : InferPredefinedAnnotations ,
132+ compose_mode : ComposeMode ,
122133 ) -> Result < Option < String > > {
123134 let reference: Reference = reference
124135 . as_ref ( )
@@ -137,8 +148,15 @@ impl Client {
137148 )
138149 . await ?;
139150
140- self . push_locked_core ( locked, auth, reference, annotations, infer_annotations)
141- . await
151+ self . push_locked_core (
152+ locked,
153+ auth,
154+ reference,
155+ annotations,
156+ infer_annotations,
157+ compose_mode,
158+ )
159+ . await
142160 }
143161
144162 /// Push a Spin application to an OCI registry and return the digest (or None
@@ -149,15 +167,23 @@ impl Client {
149167 reference : impl AsRef < str > ,
150168 annotations : Option < BTreeMap < String , String > > ,
151169 infer_annotations : InferPredefinedAnnotations ,
170+ compose_mode : ComposeMode ,
152171 ) -> Result < Option < String > > {
153172 let reference: Reference = reference
154173 . as_ref ( )
155174 . parse ( )
156175 . with_context ( || format ! ( "cannot parse reference {}" , reference. as_ref( ) ) ) ?;
157176 let auth = Self :: auth ( & reference) . await ?;
158177
159- self . push_locked_core ( locked, auth, reference, annotations, infer_annotations)
160- . await
178+ self . push_locked_core (
179+ locked,
180+ auth,
181+ reference,
182+ annotations,
183+ infer_annotations,
184+ compose_mode,
185+ )
186+ . await
161187 }
162188
163189 /// Push a Spin application to an OCI registry and return the digest (or None
@@ -169,10 +195,11 @@ impl Client {
169195 reference : Reference ,
170196 annotations : Option < BTreeMap < String , String > > ,
171197 infer_annotations : InferPredefinedAnnotations ,
198+ compose_mode : ComposeMode ,
172199 ) -> Result < Option < String > > {
173200 let mut locked_app = locked. clone ( ) ;
174201 let mut layers = self
175- . assemble_layers ( & mut locked_app, AssemblyMode :: Simple )
202+ . assemble_layers ( & mut locked_app, AssemblyMode :: Simple , compose_mode )
176203 . await
177204 . context ( "could not assemble layers for locked application" ) ?;
178205
@@ -183,7 +210,7 @@ impl Client {
183210 {
184211 locked_app = locked. clone ( ) ;
185212 layers = self
186- . assemble_layers ( & mut locked_app, AssemblyMode :: Archive )
213+ . assemble_layers ( & mut locked_app, AssemblyMode :: Archive , compose_mode )
187214 . await
188215 . context ( "could not assemble archive layers for locked application" ) ?;
189216 }
@@ -246,43 +273,59 @@ impl Client {
246273 & mut self ,
247274 locked : & mut LockedApp ,
248275 assembly_mode : AssemblyMode ,
276+ compose_mode : ComposeMode ,
249277 ) -> Result < Vec < ImageLayer > > {
250278 let mut layers = Vec :: new ( ) ;
251279 let mut components = Vec :: new ( ) ;
252280 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" ) ?;
281+ match compose_mode {
282+ ComposeMode :: All => {
283+ let composed = spin_compose:: compose ( & ComponentSourceLoader , & c)
284+ . await
285+ . with_context ( || {
286+ format ! ( "failed to resolve dependencies for component {:?}" , c. id)
287+ } ) ?;
288+
289+ let layer = ImageLayer :: new ( composed, WASM_LAYER_MEDIA_TYPE . to_string ( ) , None ) ;
290+ c. source . content = self . content_ref_for_layer ( & layer) ;
291+ c. dependencies . clear ( ) ;
292+ layers. push ( layer) ;
293+ }
294+ ComposeMode :: Skip => {
295+ // Add the wasm module for the component as layers.
296+ let source = c
297+ . clone ( )
298+ . source
299+ . content
300+ . source
301+ . context ( "component loaded from disk should contain a file source" ) ?;
260302
261- let source = parse_file_url ( source. as_str ( ) ) ?;
262- let layer = Self :: wasm_layer ( & source) . await ?;
303+ let source = parse_file_url ( source. as_str ( ) ) ?;
304+ let layer = Self :: wasm_layer ( & source) . await ?;
263305
264- // Update the module source with the content ref of the layer.
265- c. source . content = self . content_ref_for_layer ( & layer) ;
306+ // Update the module source with the content ref of the layer.
307+ c. source . content = self . content_ref_for_layer ( & layer) ;
266308
267- layers. push ( layer) ;
309+ layers. push ( layer) ;
268310
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 ( ) ) ?;
311+ let mut deps = BTreeMap :: default ( ) ;
312+ for ( dep_name, mut dep) in c. dependencies {
313+ let source =
314+ dep. source . content . source . context (
315+ "dependency loaded from disk should contain a file source" ,
316+ ) ?;
317+ let source = parse_file_url ( source. as_str ( ) ) ?;
277318
278- let layer = Self :: wasm_layer ( & source) . await ?;
319+ let layer = Self :: wasm_layer ( & source) . await ?;
279320
280- dep. source . content = self . content_ref_for_layer ( & layer) ;
281- deps. insert ( dep_name, dep) ;
321+ dep. source . content = self . content_ref_for_layer ( & layer) ;
322+ deps. insert ( dep_name, dep) ;
282323
283- layers. push ( layer) ;
324+ layers. push ( layer) ;
325+ }
326+ c. dependencies = deps;
327+ }
284328 }
285- c. dependencies = deps;
286329
287330 let mut files = Vec :: new ( ) ;
288331 for f in c. files {
@@ -669,6 +712,32 @@ impl Client {
669712 }
670713}
671714
715+ struct ComponentSourceLoader ;
716+
717+ #[ async_trait]
718+ impl spin_compose:: ComponentSourceLoader for ComponentSourceLoader {
719+ async fn load_component_source (
720+ & self ,
721+ source : & LockedComponentSource ,
722+ ) -> anyhow:: Result < Vec < u8 > > {
723+ let source = source
724+ . content
725+ . source
726+ . as_ref ( )
727+ . context ( "component loaded from disk should contain a file source" ) ?;
728+
729+ let source = parse_file_url ( source. as_str ( ) ) ?;
730+
731+ let bytes = fs:: read ( & source)
732+ . await
733+ . with_context ( || format ! ( "cannot read wasm module {}" , quoted_path( source) ) ) ?;
734+
735+ let component = spin_componentize:: componentize_if_necessary ( & bytes) ?;
736+
737+ Ok ( component. into ( ) )
738+ }
739+ }
740+
672741/// Unpack contents of the provided archive layer, represented by bytes and its
673742/// corresponding digest, into the provided cache.
674743/// A temporary staging directory is created via tempfile::tempdir() to store
@@ -946,6 +1015,7 @@ mod test {
9461015 locked_components : Vec < LockedComponent > ,
9471016 expected_layer_count : usize ,
9481017 expected_error : Option < & ' static str > ,
1018+ compose_mode : ComposeMode ,
9491019 }
9501020
9511021 let tests: Vec < TestCase > = [
@@ -968,6 +1038,7 @@ mod test {
9681038 } } ] ) ,
9691039 expected_layer_count : 2 ,
9701040 expected_error : None ,
1041+ compose_mode : ComposeMode :: Skip ,
9711042 } ,
9721043 TestCase {
9731044 name : "One component layer and two file layers" ,
@@ -992,6 +1063,7 @@ mod test {
9921063 } ] ) ,
9931064 expected_layer_count : 3 ,
9941065 expected_error : None ,
1066+ compose_mode : ComposeMode :: Skip ,
9951067 } ,
9961068 TestCase {
9971069 name : "One component layer and one file with inlined content" ,
@@ -1012,6 +1084,7 @@ mod test {
10121084 } ] ) ,
10131085 expected_layer_count : 1 ,
10141086 expected_error : None ,
1087+ compose_mode : ComposeMode :: Skip ,
10151088 } ,
10161089 TestCase {
10171090 name : "One component layer and one dependency component layer" ,
@@ -1036,6 +1109,7 @@ mod test {
10361109 } ] ) ,
10371110 expected_layer_count : 2 ,
10381111 expected_error : None ,
1112+ compose_mode : ComposeMode :: Skip ,
10391113 } ,
10401114 TestCase {
10411115 name : "Component has no source" ,
@@ -1050,6 +1124,7 @@ mod test {
10501124 } ] ) ,
10511125 expected_layer_count : 0 ,
10521126 expected_error : Some ( "Invalid URL: \" \" " ) ,
1127+ compose_mode : ComposeMode :: Skip ,
10531128 } ,
10541129 TestCase {
10551130 name : "Duplicate component sources" ,
@@ -1070,6 +1145,7 @@ mod test {
10701145 } } ] ) ,
10711146 expected_layer_count : 1 ,
10721147 expected_error : None ,
1148+ compose_mode : ComposeMode :: Skip ,
10731149 } ,
10741150 TestCase {
10751151 name : "Duplicate file paths" ,
@@ -1107,6 +1183,7 @@ mod test {
11071183 } ] ) ,
11081184 expected_layer_count : 4 ,
11091185 expected_error : None ,
1186+ compose_mode : ComposeMode :: Skip ,
11101187 } ,
11111188 ]
11121189 . to_vec ( ) ;
@@ -1137,7 +1214,7 @@ mod test {
11371214 assert_eq ! (
11381215 e,
11391216 client
1140- . assemble_layers( & mut locked, AssemblyMode :: Simple )
1217+ . assemble_layers( & mut locked, AssemblyMode :: Simple , tc . compose_mode )
11411218 . await
11421219 . unwrap_err( )
11431220 . to_string( ) ,
@@ -1149,7 +1226,7 @@ mod test {
11491226 assert_eq ! (
11501227 tc. expected_layer_count,
11511228 client
1152- . assemble_layers( & mut locked, AssemblyMode :: Simple )
1229+ . assemble_layers( & mut locked, AssemblyMode :: Simple , tc . compose_mode )
11531230 . await
11541231 . unwrap( )
11551232 . len( ) ,
0 commit comments