Skip to content

Commit 4ef1b66

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

File tree

3 files changed

+64
-25
lines changed

3 files changed

+64
-25
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: 2 additions & 2 deletions
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"
@@ -25,7 +25,7 @@ spin-manifest = { path = "../manifest" }
2525
spin-trigger = { path = "../trigger" }
2626
tempfile = "3.3"
2727
tokio = { version = "1", features = ["fs"] }
28-
tokio-util = "0.7.9"
28+
tokio-util = { version = "0.7.9", features = ["compat"] }
2929
tracing = { workspace = true }
3030
walkdir = "2.3"
3131

crates/oci/src/client.rs

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ use anyhow::{bail, Context, Result};
44
use docker_credential::DockerCredential;
55
use futures_util::future;
66
use futures_util::stream::{self, StreamExt, TryStreamExt};
7-
use oci_distribution::errors::OciDistributionError;
8-
use oci_distribution::token_cache::RegistryTokenType;
9-
use oci_distribution::RegistryOperation;
107
use 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
};
1615
use reqwest::Url;
1716
use spin_app::locked::{ContentPath, ContentRef, LockedApp};
@@ -23,11 +22,16 @@ use walkdir::WalkDir;
2322

2423
use 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

3236
const CONFIG_FILE: &str = "config.json";
3337
const 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

Comments
 (0)