@@ -4,14 +4,13 @@ use anyhow::{bail, Context, Result};
44use docker_credential:: DockerCredential ;
55use futures_util:: future;
66use futures_util:: stream:: { self , StreamExt , TryStreamExt } ;
7- use oci_distribution:: errors:: OciDistributionError ;
8- use oci_distribution:: token_cache:: RegistryTokenType ;
9- use oci_distribution:: RegistryOperation ;
107use oci_distribution:: {
118 client:: { Config , ImageLayer } ,
12- manifest:: OciImageManifest ,
9+ errors:: OciDistributionError ,
10+ manifest:: { OciImageManifest , OCI_IMAGE_MEDIA_TYPE } ,
1311 secrets:: RegistryAuth ,
14- Reference ,
12+ token_cache:: RegistryTokenType ,
13+ Reference , RegistryOperation ,
1514} ;
1615use reqwest:: Url ;
1716use spin_app:: locked:: { ContentPath , ContentRef , LockedApp } ;
@@ -23,11 +22,16 @@ use walkdir::WalkDir;
2322
2423use crate :: auth:: AuthConfig ;
2524
26- // TODO: the media types for application, wasm module, data and archive layer are not final.
27- const SPIN_APPLICATION_MEDIA_TYPE : & str = "application/vnd.fermyon.spin.application.v1+config" ;
28- const WASM_LAYER_MEDIA_TYPE : & str = "application/vnd.wasm.content.layer.v1+wasm" ;
29- const DATA_MEDIATYPE : & str = "application/vnd.wasm.content.layer.v1+data" ;
30- const ARCHIVE_MEDIATYPE : & str = "application/vnd.wasm.content.bundle.v1.tar+gzip" ;
25+ // TODO: the media types for application, data and archive layer are not final
26+ pub const SPIN_APPLICATION_MEDIA_TYPE : & str = "application/vnd.fermyon.spin.application.v1+config" ;
27+ pub const DATA_MEDIATYPE : & str = "application/vnd.wasm.content.layer.v1+data" ;
28+ pub const ARCHIVE_MEDIATYPE : & str = "application/vnd.wasm.content.bundle.v1.tar+gzip" ;
29+ // Legacy wasm layer media type used by pre-2.0 versions of Spin
30+ const WASM_LAYER_MEDIA_TYPE_LEGACY : & str = "application/vnd.wasm.content.layer.v1+wasm" ;
31+
32+ // TODO: use canonical types defined upstream; see https://github.com/bytecodealliance/registry/pull/146
33+ const WASM_LAYER_MEDIA_TYPE : & str = "application/vnd.bytecodealliance.wasm.component.layer.v0+wasm" ;
34+ const COMPONENT_ARTIFACT_TYPE : & str = "application/vnd.bytecodealliance.component.v1+wasm" ;
3135
3236const CONFIG_FILE : & str = "config.json" ;
3337const LATEST_TAG : & str = "latest" ;
@@ -156,12 +160,24 @@ impl Client {
156160 locked. components = components;
157161 locked. metadata . remove ( "origin" ) ;
158162
163+ // Push layer for locked spin application config
164+ let locked_config_layer = ImageLayer :: new (
165+ serde_json:: to_vec ( & locked) . context ( "could not serialize locked config" ) ?,
166+ SPIN_APPLICATION_MEDIA_TYPE . to_string ( ) ,
167+ None ,
168+ ) ;
169+ layers. push ( locked_config_layer) ;
170+
159171 let oci_config = Config {
172+ // TODO: now that the locked config bytes are pushed as a layer, what should data here be?
173+ // Keeping as locked config bytes would make it feasible for older Spin clients to pull/run
174+ // apps published by newer Spin clients
160175 data : serde_json:: to_vec ( & locked) ?,
161- media_type : SPIN_APPLICATION_MEDIA_TYPE . to_string ( ) ,
176+ media_type : OCI_IMAGE_MEDIA_TYPE . to_string ( ) ,
162177 annotations : None ,
163178 } ;
164- let manifest = OciImageManifest :: build ( & layers, & oci_config, None ) ;
179+ let mut manifest = OciImageManifest :: build ( & layers, & oci_config, None ) ;
180+ manifest. artifact_type = Some ( COMPONENT_ARTIFACT_TYPE . to_string ( ) ) ;
165181 let response = self
166182 . oci
167183 . push ( & reference, & layers, oci_config, & auth, Some ( manifest) )
@@ -267,16 +283,17 @@ impl Client {
267283 let m = self . manifest_path ( & reference. to_string ( ) ) . await ?;
268284 fs:: write ( & m, & manifest_json) . await ?;
269285
286+ // Older published Spin apps feature the locked app config *as* the OCI manifest config layer,
287+ // while newer versions publish the locked app config as a generic layer alongside others.
288+ // Assume that these bytes may represent the locked app config and write it as such.
289+ // TODO: update this assumption if we change the data we write to the OCI manifest config layer.
270290 let mut cfg_bytes = Vec :: new ( ) ;
271291 self . oci
272292 . pull_blob ( & reference, & manifest. config . digest , & mut cfg_bytes)
273293 . await ?;
274- let cfg = std:: str:: from_utf8 ( & cfg_bytes) ?;
275- tracing:: debug!( "Pulled config: {}" , cfg) ;
276-
277- // Write the config object in `<cache_root>/registry/oci/manifests/repository:<tag_or_latest>/config.json`
278- let c = self . lockfile_path ( & reference. to_string ( ) ) . await ?;
279- fs:: write ( & c, & cfg) . await ?;
294+ self . write_locked_app_config ( & reference. to_string ( ) , & cfg_bytes)
295+ . await
296+ . context ( "unable to write locked app config to cache" ) ?;
280297
281298 // If a layer is a Wasm module, write it in the Wasm directory.
282299 // Otherwise, write it in the data directory (after unpacking if archive layer)
@@ -300,15 +317,25 @@ impl Client {
300317 {
301318 Err ( e) => return Err ( e) ,
302319 _ => match layer. media_type . as_str ( ) {
303- WASM_LAYER_MEDIA_TYPE => {
320+ // If the locked app config is present as a separate layer, this should take precedence
321+ SPIN_APPLICATION_MEDIA_TYPE => {
322+ if let Err ( e) = this. write_locked_app_config ( & reference. to_string ( ) , & bytes)
323+ . await
324+ {
325+ return Err ( OciDistributionError :: GenericError (
326+ Some ( format ! ( "unable to write locked app config to cache: {}" , e) )
327+ ) ) ;
328+ }
329+ }
330+ WASM_LAYER_MEDIA_TYPE | WASM_LAYER_MEDIA_TYPE_LEGACY => {
304331 let _ = this. cache . write_wasm ( & bytes, & layer. digest ) . await ;
305332 }
306333 ARCHIVE_MEDIATYPE => {
307334 if let Err ( e) =
308335 this. unpack_archive_layer ( & bytes, & layer. digest ) . await
309336 {
310337 return Err ( OciDistributionError :: GenericError ( Some (
311- e . to_string ( ) ,
338+ format ! ( "unable to unpack archive layer with digest {}: {}" , & layer . digest , e ) ,
312339 ) ) ) ;
313340 }
314341 }
@@ -374,6 +401,19 @@ impl Client {
374401 Ok ( p. join ( CONFIG_FILE ) )
375402 }
376403
404+ /// Write the config object in `<cache_root>/registry/oci/manifests/repository:<tag_or_latest>/config.json`
405+ async fn write_locked_app_config (
406+ & self ,
407+ reference : impl AsRef < str > ,
408+ bytes : impl AsRef < [ u8 ] > ,
409+ ) -> Result < ( ) > {
410+ let cfg = std:: str:: from_utf8 ( bytes. as_ref ( ) ) ?;
411+ tracing:: debug!( "Pulled config: {}" , cfg) ;
412+
413+ let c = self . lockfile_path ( reference) . await ?;
414+ fs:: write ( & c, & cfg) . await . map_err ( anyhow:: Error :: from)
415+ }
416+
377417 /// Create a new wasm layer based on a file.
378418 async fn wasm_layer ( file : & Path ) -> Result < ImageLayer > {
379419 tracing:: log:: trace!( "Reading wasm module from {:?}" , file) ;
0 commit comments