@@ -6,14 +6,13 @@ use anyhow::{bail, Context, Result};
66use docker_credential:: DockerCredential ;
77use futures_util:: future;
88use futures_util:: stream:: { self , StreamExt , TryStreamExt } ;
9- use oci_distribution:: errors:: OciDistributionError ;
10- use oci_distribution:: token_cache:: RegistryTokenType ;
11- use oci_distribution:: RegistryOperation ;
129use oci_distribution:: {
1310 client:: { Config , ImageLayer } ,
14- manifest:: OciImageManifest ,
11+ errors:: OciDistributionError ,
12+ manifest:: { OciImageManifest , OCI_IMAGE_MEDIA_TYPE } ,
1513 secrets:: RegistryAuth ,
16- Reference ,
14+ token_cache:: RegistryTokenType ,
15+ Reference , RegistryOperation ,
1716} ;
1817use reqwest:: Url ;
1918use spin_app:: locked:: { ContentPath , ContentRef , LockedApp } ;
@@ -25,15 +24,19 @@ use walkdir::WalkDir;
2524
2625use crate :: auth:: AuthConfig ;
2726
28- // TODO: the media types for application, wasm module, data and archive layer are not final.
27+ // TODO: the media types for application, data and archive layer are not final
2928/// Media type for a layer representing a locked Spin application configuration
3029pub const SPIN_APPLICATION_MEDIA_TYPE : & str = "application/vnd.fermyon.spin.application.v1+config" ;
31- // Note: we hope to use a canonical value defined upstream for this media type
32- const WASM_LAYER_MEDIA_TYPE : & str = "application/vnd.wasm.content.layer.v1+wasm" ;
3330/// Media type for a layer representing a generic data file used by a Spin application
3431pub const DATA_MEDIATYPE : & str = "application/vnd.wasm.content.layer.v1+data" ;
3532/// Media type for a layer representing a compressed archive of one or more files used by a Spin application
3633pub const ARCHIVE_MEDIATYPE : & str = "application/vnd.wasm.content.bundle.v1.tar+gzip" ;
34+ // Legacy wasm layer media type used by pre-2.0 versions of Spin
35+ const WASM_LAYER_MEDIA_TYPE_LEGACY : & str = "application/vnd.wasm.content.layer.v1+wasm" ;
36+
37+ // TODO: use canonical types defined upstream; see https://github.com/bytecodealliance/registry/pull/146
38+ const WASM_LAYER_MEDIA_TYPE : & str = "application/vnd.bytecodealliance.wasm.component.layer.v0+wasm" ;
39+ const COMPONENT_ARTIFACT_TYPE : & str = "application/vnd.bytecodealliance.component.v1+wasm" ;
3740
3841const CONFIG_FILE : & str = "config.json" ;
3942const LATEST_TAG : & str = "latest" ;
@@ -162,12 +165,24 @@ impl Client {
162165 locked. components = components;
163166 locked. metadata . remove ( "origin" ) ;
164167
168+ // Push layer for locked spin application config
169+ let locked_config_layer = ImageLayer :: new (
170+ serde_json:: to_vec ( & locked) . context ( "could not serialize locked config" ) ?,
171+ SPIN_APPLICATION_MEDIA_TYPE . to_string ( ) ,
172+ None ,
173+ ) ;
174+ layers. push ( locked_config_layer) ;
175+
165176 let oci_config = Config {
177+ // TODO: now that the locked config bytes are pushed as a layer, what should data here be?
178+ // Keeping as locked config bytes would make it feasible for older Spin clients to pull/run
179+ // apps published by newer Spin clients
166180 data : serde_json:: to_vec ( & locked) ?,
167- media_type : SPIN_APPLICATION_MEDIA_TYPE . to_string ( ) ,
181+ media_type : OCI_IMAGE_MEDIA_TYPE . to_string ( ) ,
168182 annotations : None ,
169183 } ;
170- let manifest = OciImageManifest :: build ( & layers, & oci_config, None ) ;
184+ let mut manifest = OciImageManifest :: build ( & layers, & oci_config, None ) ;
185+ manifest. artifact_type = Some ( COMPONENT_ARTIFACT_TYPE . to_string ( ) ) ;
171186 let response = self
172187 . oci
173188 . push ( & reference, & layers, oci_config, & auth, Some ( manifest) )
@@ -273,16 +288,17 @@ impl Client {
273288 let m = self . manifest_path ( & reference. to_string ( ) ) . await ?;
274289 fs:: write ( & m, & manifest_json) . await ?;
275290
291+ // Older published Spin apps feature the locked app config *as* the OCI manifest config layer,
292+ // while newer versions publish the locked app config as a generic layer alongside others.
293+ // Assume that these bytes may represent the locked app config and write it as such.
294+ // TODO: update this assumption if we change the data we write to the OCI manifest config layer.
276295 let mut cfg_bytes = Vec :: new ( ) ;
277296 self . oci
278297 . pull_blob ( & reference, & manifest. config . digest , & mut cfg_bytes)
279298 . await ?;
280- let cfg = std:: str:: from_utf8 ( & cfg_bytes) ?;
281- tracing:: debug!( "Pulled config: {}" , cfg) ;
282-
283- // Write the config object in `<cache_root>/registry/oci/manifests/repository:<tag_or_latest>/config.json`
284- let c = self . lockfile_path ( & reference. to_string ( ) ) . await ?;
285- fs:: write ( & c, & cfg) . await ?;
299+ self . write_locked_app_config ( & reference. to_string ( ) , & cfg_bytes)
300+ . await
301+ . context ( "unable to write locked app config to cache" ) ?;
286302
287303 // If a layer is a Wasm module, write it in the Wasm directory.
288304 // Otherwise, write it in the data directory (after unpacking if archive layer)
@@ -306,15 +322,25 @@ impl Client {
306322 {
307323 Err ( e) => return Err ( e) ,
308324 _ => match layer. media_type . as_str ( ) {
309- WASM_LAYER_MEDIA_TYPE => {
325+ // If the locked app config is present as a separate layer, this should take precedence
326+ SPIN_APPLICATION_MEDIA_TYPE => {
327+ if let Err ( e) = this. write_locked_app_config ( & reference. to_string ( ) , & bytes)
328+ . await
329+ {
330+ return Err ( OciDistributionError :: GenericError (
331+ Some ( format ! ( "unable to write locked app config to cache: {}" , e) )
332+ ) ) ;
333+ }
334+ }
335+ WASM_LAYER_MEDIA_TYPE | WASM_LAYER_MEDIA_TYPE_LEGACY => {
310336 let _ = this. cache . write_wasm ( & bytes, & layer. digest ) . await ;
311337 }
312338 ARCHIVE_MEDIATYPE => {
313339 if let Err ( e) =
314340 this. unpack_archive_layer ( & bytes, & layer. digest ) . await
315341 {
316342 return Err ( OciDistributionError :: GenericError ( Some (
317- e . to_string ( ) ,
343+ format ! ( "unable to unpack archive layer with digest {}: {}" , & layer . digest , e ) ,
318344 ) ) ) ;
319345 }
320346 }
@@ -380,6 +406,19 @@ impl Client {
380406 Ok ( p. join ( CONFIG_FILE ) )
381407 }
382408
409+ /// Write the config object in `<cache_root>/registry/oci/manifests/repository:<tag_or_latest>/config.json`
410+ async fn write_locked_app_config (
411+ & self ,
412+ reference : impl AsRef < str > ,
413+ bytes : impl AsRef < [ u8 ] > ,
414+ ) -> Result < ( ) > {
415+ let cfg = std:: str:: from_utf8 ( bytes. as_ref ( ) ) ?;
416+ tracing:: debug!( "Pulled config: {}" , cfg) ;
417+
418+ let c = self . lockfile_path ( reference) . await ?;
419+ fs:: write ( & c, & cfg) . await . map_err ( anyhow:: Error :: from)
420+ }
421+
383422 /// Create a new wasm layer based on a file.
384423 async fn wasm_layer ( file : & Path ) -> Result < ImageLayer > {
385424 tracing:: log:: trace!( "Reading wasm module from {:?}" , file) ;
0 commit comments