Skip to content

Commit e3d6729

Browse files
committed
feat(oci): manifest/config updates to support containerd
Signed-off-by: Vaughn Dice <[email protected]>
1 parent 7daedf5 commit e3d6729

File tree

3 files changed

+87
-19
lines changed

3 files changed

+87
-19
lines changed

Cargo.lock

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oci/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dkregistry = { git = "https://github.com/camallo/dkregistry-rs", rev = "37acecb4
1414
docker_credential = "1.0"
1515
dirs = "4.0"
1616
futures-util = "0.3"
17-
oci-distribution = { git = "https://github.com/fermyon/oci-distribution", rev = "05022618d78feef9b99f20b5da8fd6def6bb80d2" }
17+
oci-distribution = { git = "https://github.com/fermyon/oci-distribution", rev = "639c907b7c0c4e74716356585410d4abe4aebf4d" }
1818
reqwest = "0.11"
1919
serde = { version = "1.0", features = ["derive"] }
2020
serde_json = "1.0"

crates/oci/src/client.rs

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ use anyhow::{bail, Context, Result};
66
use docker_credential::DockerCredential;
77
use futures_util::future;
88
use futures_util::stream::{self, StreamExt, TryStreamExt};
9-
use oci_distribution::token_cache::RegistryTokenType;
10-
use oci_distribution::RegistryOperation;
119
use oci_distribution::{
1210
client::{Config, ImageLayer},
13-
manifest::OciImageManifest,
11+
errors::OciDistributionError,
12+
manifest::{OciImageManifest, OCI_IMAGE_MEDIA_TYPE},
1413
secrets::RegistryAuth,
15-
Reference,
14+
token_cache::RegistryTokenType,
15+
Reference, RegistryOperation,
1616
};
1717
use reqwest::Url;
1818
use spin_common::sha256;
@@ -25,15 +25,19 @@ use walkdir::WalkDir;
2525

2626
use crate::auth::AuthConfig;
2727

28-
// TODO: the media types for application, wasm module, data and archive layer are not final.
28+
// TODO: the media types for application, data and archive layer are not final
2929
/// Media type for a layer representing a locked Spin application configuration
3030
pub 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";
3331
/// Media type for a layer representing a generic data file used by a Spin application
3432
pub const DATA_MEDIATYPE: &str = "application/vnd.wasm.content.layer.v1+data";
3533
/// Media type for a layer representing a compressed archive of one or more files used by a Spin application
3634
pub const ARCHIVE_MEDIATYPE: &str = "application/vnd.wasm.content.bundle.v1.tar+gzip";
35+
// Legacy wasm layer media type used by pre-2.0 versions of Spin
36+
const WASM_LAYER_MEDIA_TYPE_LEGACY: &str = "application/vnd.wasm.content.layer.v1+wasm";
37+
38+
// TODO: use canonical types defined upstream; see https://github.com/bytecodealliance/registry/pull/146
39+
const WASM_LAYER_MEDIA_TYPE: &str = "application/vnd.bytecodealliance.wasm.component.layer.v0+wasm";
40+
const COMPONENT_ARTIFACT_TYPE: &str = "application/vnd.bytecodealliance.component.v1+wasm";
3741

3842
const CONFIG_FILE: &str = "config.json";
3943
const LATEST_TAG: &str = "latest";
@@ -164,12 +168,24 @@ impl Client {
164168
locked.components = components;
165169
locked.metadata.remove("origin");
166170

171+
// Push layer for locked spin application config
172+
let locked_config_layer = ImageLayer::new(
173+
serde_json::to_vec(&locked).context("could not serialize locked config")?,
174+
SPIN_APPLICATION_MEDIA_TYPE.to_string(),
175+
None,
176+
);
177+
layers.push(locked_config_layer);
178+
167179
let oci_config = Config {
180+
// TODO: now that the locked config bytes are pushed as a layer, what should data here be?
181+
// Keeping as locked config bytes would make it feasible for older Spin clients to pull/run
182+
// apps published by newer Spin clients
168183
data: serde_json::to_vec(&locked)?,
169-
media_type: SPIN_APPLICATION_MEDIA_TYPE.to_string(),
184+
media_type: OCI_IMAGE_MEDIA_TYPE.to_string(),
170185
annotations: None,
171186
};
172-
let manifest = OciImageManifest::build(&layers, &oci_config, None);
187+
let mut manifest = OciImageManifest::build(&layers, &oci_config, None);
188+
manifest.artifact_type = Some(COMPONENT_ARTIFACT_TYPE.to_string());
173189
let response = self
174190
.oci
175191
.push(&reference, &layers, oci_config, &auth, Some(manifest))
@@ -275,16 +291,17 @@ impl Client {
275291
let m = self.manifest_path(&reference.to_string()).await?;
276292
fs::write(&m, &manifest_json).await?;
277293

294+
// Older published Spin apps feature the locked app config *as* the OCI manifest config layer,
295+
// while newer versions publish the locked app config as a generic layer alongside others.
296+
// Assume that these bytes may represent the locked app config and write it as such.
297+
// TODO: update this assumption if we change the data we write to the OCI manifest config layer.
278298
let mut cfg_bytes = Vec::new();
279299
self.oci
280300
.pull_blob(&reference, &manifest.config.digest, &mut cfg_bytes)
281301
.await?;
282-
let cfg = std::str::from_utf8(&cfg_bytes)?;
283-
tracing::debug!("Pulled config: {}", cfg);
284-
285-
// Write the config object in `<cache_root>/registry/oci/manifests/repository:<tag_or_latest>/config.json`
286-
let c = self.lockfile_path(&reference.to_string()).await?;
287-
fs::write(&c, &cfg).await?;
302+
self.write_locked_app_config(&reference.to_string(), &cfg_bytes)
303+
.await
304+
.context("unable to write locked app config to cache")?;
288305

289306
// If a layer is a Wasm module, write it in the Wasm directory.
290307
// Otherwise, write it in the data directory (after unpacking if archive layer)
@@ -298,6 +315,7 @@ impl Client {
298315
|| this.cache.data_file(&layer.digest).is_ok()
299316
{
300317
tracing::debug!("Layer {} already exists in cache", &layer.digest);
318+
<<<<<<< HEAD
301319
return anyhow::Ok(());
302320
}
303321

@@ -315,6 +333,44 @@ impl Client {
315333
}
316334
_ => {
317335
this.cache.write_data(&bytes, &layer.digest).await?;
336+
=======
337+
} else {
338+
tracing::debug!("Pulling layer {}", &layer.digest);
339+
let mut bytes = Vec::new();
340+
match this
341+
.oci
342+
.pull_blob(&reference, &layer.digest, &mut bytes)
343+
.await
344+
{
345+
Err(e) => return Err(e),
346+
_ => match layer.media_type.as_str() {
347+
// If the locked app config is present as a separate layer, this should take precedence
348+
SPIN_APPLICATION_MEDIA_TYPE => {
349+
if let Err(e) = this.write_locked_app_config(&reference.to_string(), &bytes)
350+
.await
351+
{
352+
return Err(OciDistributionError::GenericError(
353+
Some(format!("unable to write locked app config to cache: {}", e))
354+
));
355+
}
356+
}
357+
WASM_LAYER_MEDIA_TYPE | WASM_LAYER_MEDIA_TYPE_LEGACY => {
358+
let _ = this.cache.write_wasm(&bytes, &layer.digest).await;
359+
}
360+
ARCHIVE_MEDIATYPE => {
361+
if let Err(e) =
362+
this.unpack_archive_layer(&bytes, &layer.digest).await
363+
{
364+
return Err(OciDistributionError::GenericError(Some(
365+
format!("unable to unpack archive layer with digest {}: {}", &layer.digest, e),
366+
)));
367+
}
368+
}
369+
_ => {
370+
let _ = this.cache.write_data(&bytes, &layer.digest).await;
371+
}
372+
},
373+
>>>>>>> 942a1782 (feat(oci): manifest/config updates to support containerd)
318374
}
319375
}
320376
Ok(())
@@ -373,6 +429,19 @@ impl Client {
373429
Ok(p.join(CONFIG_FILE))
374430
}
375431

432+
/// Write the config object in `<cache_root>/registry/oci/manifests/repository:<tag_or_latest>/config.json`
433+
async fn write_locked_app_config(
434+
&self,
435+
reference: impl AsRef<str>,
436+
bytes: impl AsRef<[u8]>,
437+
) -> Result<()> {
438+
let cfg = std::str::from_utf8(bytes.as_ref())?;
439+
tracing::debug!("Pulled config: {}", cfg);
440+
441+
let c = self.lockfile_path(reference).await?;
442+
fs::write(&c, &cfg).await.map_err(anyhow::Error::from)
443+
}
444+
376445
/// Create a new wasm layer based on a file.
377446
async fn wasm_layer(file: &Path) -> Result<ImageLayer> {
378447
tracing::log::trace!("Reading wasm module from {:?}", file);

0 commit comments

Comments
 (0)