Skip to content

Commit

Permalink
Move away from oci artifact naming
Browse files Browse the repository at this point in the history
Signed-off-by: James Sturtevant <[email protected]>
  • Loading branch information
jsturtevant committed Oct 25, 2023
1 parent eff3e0a commit d1b21fb
Show file tree
Hide file tree
Showing 18 changed files with 158 additions and 119 deletions.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,11 @@ dist/img-oci.tar: target/wasm32-wasi/$(OPT_PROFILE)/img-oci.tar
load: dist/img.tar
sudo ctr -n $(CONTAINERD_NAMESPACE) image import --all-platforms $<

CTR_VERSION := $(shell sudo ctr version | sed -n -e '/Version/ {s/.*: *//p;q;}')
load/oci: dist/img-oci.tar
sudo ../containerd/bin/ctr -n $(CONTAINERD_NAMESPACE) image import --all-platforms $<
@echo $(CTR_VERSION)\\nv1.7.7 | sort -crV || (echo "containerd version must be 1.7.7+ was $(CTR_VERSION)" && exit 1)
@echo using containerd $(CTR_VERSION)
sudo ctr -n $(CONTAINERD_NAMESPACE) image import --all-platforms $<

.PHONY:
target/wasm32-wasi/$(OPT_PROFILE)/img-oci.tar: target/wasm32-wasi/$(OPT_PROFILE)/wasi-demo-app.wasm
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,22 +323,22 @@ To kill the process from demo 2, you can run in other session: `sudo ctr task ki

The test binary supports commands for different type of functionality, check [crates/wasi-demo-app/src/main.rs](crates/wasi-demo-app/src/main.rs) to try it out.

## Demo 3 using WASM OCI artifacts
## Demo 3 using OCI Images with custom WASM layers

The previous demos run with an OCI Container image containing the wasm module in the file system. Another option is to provide a cross-platform OCI artifact that that will not have the wasm module or components in the file system of the container that wraps the wasmtime/wasmedge process. This OCI artifact can be run across any platform and provides for de-duplication in the Containerd content store among other benefits.
The previous demos run with an OCI Container image containing the wasm module in the file system. Another option is to provide a cross-platform OCI Image that that will not have the wasm module or components in the file system of the container that wraps the wasmtime/wasmedge process. This OCI Image with custom WASM layers can be run across any platform and provides for de-duplication in the Containerd content store among other benefits.

To learn more about this approach checkout the [design document](https://docs.google.com/document/d/11shgC3l6gplBjWF1VJCWvN_9do51otscAm0hBDGSSAc/edit).

> **Note**: This requires containerd components based on https://github.com/containerd/containerd/pull/8699. Both CTR and containerd need to be build with that patch. If you do not have this patch for both `containerd` and `ctr` you will end up with an error message such as `mismatched image rootfs and manifest layers` at the import and run steps
> **Note**: This requires containerd 1.7.7+ and 1.6.25+ (not yet released). If you do not have these patches for both `containerd` and `ctr` you will end up with an error message such as `mismatched image rootfs and manifest layers` at the import and run steps.
Build and import the OCI artifact image:
Build and import the OCI image with WASM layers image:

```
make test-image/oci
make load/oci
```

Run the image with `sudo ctr run --rm --runtime=io.containerd.[ wasmedge | wasmtime | wasmer ].v1 --label application/vnd.bytecodealliance.wasm.module=oci.wasm ghcr.io/containerd/runwasi/wasi-demo-oci:latest testwasmoci`
Run the image with `sudo ctr run --rm --runtime=io.containerd.[ wasmedge | wasmtime | wasmer ].v1 ghcr.io/containerd/runwasi/wasi-demo-oci:latest testwasmoci`

```
sudo ctr run --rm --runtime=io.containerd.wasmtime.v1 ghcr.io/containerd/runwasi/wasi-demo-oci:latest testwasmoci wasi-demo-oci.wasm echo 'hello'
Expand Down
36 changes: 25 additions & 11 deletions crates/containerd-shim-wasm/src/container/context.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use std::path::{Path, PathBuf};

use oci_spec::image::Platform;
use oci_spec::runtime::Spec;

use crate::sandbox::oci::OciArtifact;
use crate::sandbox::oci::WasmLayer;

pub trait RuntimeContext {
// ctx.args() returns arguments from the runtime spec process field, including the
Expand All @@ -23,7 +24,9 @@ pub trait RuntimeContext {
// "#init" -> { path: "", func: "init" }
fn wasi_entrypoint(&self) -> WasiEntrypoint;

fn oci_artifacts(&self) -> &[OciArtifact];
fn wasm_layers(&self) -> &[WasmLayer];

fn platform(&self) -> &Platform;
}

pub struct WasiEntrypoint {
Expand All @@ -33,7 +36,8 @@ pub struct WasiEntrypoint {

pub(crate) struct WasiContext<'a> {
pub spec: &'a Spec,
pub oci_artifacts: &'a [OciArtifact],
pub wasm_layers: &'a [WasmLayer],
pub platform: &'a Platform,
}

impl RuntimeContext for WasiContext<'_> {
Expand All @@ -59,8 +63,12 @@ impl RuntimeContext for WasiContext<'_> {
}
}

fn oci_artifacts(&self) -> &[OciArtifact] {
self.oci_artifacts
fn wasm_layers(&self) -> &[WasmLayer] {
self.wasm_layers
}

fn platform(&self) -> &Platform {
self.platform
}
}

Expand All @@ -85,7 +93,8 @@ mod tests {

let ctx = WasiContext {
spec: &spec,
oci_artifacts: &[],
wasm_layers: &[],
platform: &Platform::default(),
};

let args = ctx.args();
Expand All @@ -104,7 +113,8 @@ mod tests {

let ctx = WasiContext {
spec: &spec,
oci_artifacts: &[],
wasm_layers: &[],
platform: &Platform::default(),
};

let args = ctx.args();
Expand All @@ -131,7 +141,8 @@ mod tests {

let ctx = WasiContext {
spec: &spec,
oci_artifacts: &[],
wasm_layers: &[],
platform: &Platform::default(),
};

let args = ctx.args();
Expand All @@ -152,7 +163,8 @@ mod tests {

let ctx = WasiContext {
spec: &spec,
oci_artifacts: &[],
wasm_layers: &[],
platform: &Platform::default(),
};

let path = ctx.wasi_entrypoint().path;
Expand All @@ -179,7 +191,8 @@ mod tests {

let ctx = WasiContext {
spec: &spec,
oci_artifacts: &[],
wasm_layers: &[],
platform: &Platform::default(),
};

let WasiEntrypoint { path, func } = ctx.wasi_entrypoint();
Expand Down Expand Up @@ -207,7 +220,8 @@ mod tests {

let ctx = WasiContext {
spec: &spec,
oci_artifacts: &[],
wasm_layers: &[],
platform: &Platform::default(),
};

let WasiEntrypoint { path, func } = ctx.wasi_entrypoint();
Expand Down
104 changes: 70 additions & 34 deletions crates/containerd-shim-wasm/src/sandbox/containerd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ use containerd_client::services::v1::{GetContainerRequest, GetImageRequest, Read
use containerd_client::tonic::transport::Channel;
use containerd_client::{tonic, with_namespace};
use futures::TryStreamExt;
use oci_spec::image::{ImageManifest, MediaType};
use oci_spec::image::{Arch, ImageManifest, MediaType, Platform};
use tokio::runtime::Runtime;
use tonic::Request;

use crate::sandbox::error::{Error as ShimError, Result};
use crate::sandbox::oci::{self, OciArtifact, COMPONENT_ARTIFACT_TYPE, MODULE_ARTIFACT_TYPE};
use crate::sandbox::oci::{self, WasmLayer};

pub struct Client {
pub(crate) struct Client {
inner: Channel,
rt: Runtime,
namespace: String,
Expand All @@ -33,7 +33,7 @@ impl Client {

let inner = rt
.block_on(containerd_client::connect(address))
.map_err(|err| ShimError::Others(err.to_string()))?;
.map_err(|err| ShimError::Containerd(err.to_string()))?;

Ok(Client {
inner,
Expand All @@ -53,12 +53,12 @@ impl Client {
ContentClient::new(self.inner.clone())
.read(req)
.await
.map_err(|err| ShimError::Others(err.to_string()))?
.map_err(|err| ShimError::Containerd(err.to_string()))?
.into_inner()
.map_ok(|msg| msg.data)
.try_concat()
.await
.map_err(|err| ShimError::Others(err.to_string()))
.map_err(|err| ShimError::Containerd(err.to_string()))
})
}

Expand All @@ -70,10 +70,22 @@ impl Client {
let digest = ImagesClient::new(self.inner.clone())
.get(req)
.await
.map_err(|err| ShimError::Others(err.to_string()))?
.map_err(|err| ShimError::Containerd(err.to_string()))?
.into_inner()
.image.ok_or(ShimError::Others(format!("failed to get image content sha for image {}", image_name.to_string())))?
.target.ok_or(ShimError::Others(format!("failed to get image content sha for image {}", image_name.to_string())))?
.image
.ok_or_else(|| {
ShimError::Containerd(format!(
"failed to get image content sha for image {}",
image_name.to_string()
))
})?
.target
.ok_or_else(|| {
ShimError::Containerd(format!(
"failed to get image content sha for image {}",
image_name.to_string()
))
})?
.digest;
Ok(digest)
})
Expand All @@ -87,50 +99,74 @@ impl Client {
let image = ContainersClient::new(self.inner.clone())
.get(req)
.await
.map_err(|err| ShimError::Others(err.to_string()))?
.map_err(|err| ShimError::Containerd(err.to_string()))?
.into_inner()
.container
.ok_or(ShimError::Others(format!("failed to get image for container {}", container_name.to_string())))?
.ok_or_else(|| {
ShimError::Containerd(format!(
"failed to get image for container {}",
container_name.to_string()
))
})?
.image;
Ok(image)
})
}

// load module will query the containerd store to find an image that has an ArtifactType of WASM OCI Artifact
// load module will query the containerd store to find an image that has an OS of type 'wasm'
// If found it continues to parse the manifest and return the layers that contains the WASM modules
// and possibly other configuration artifacts
pub fn load_modules(&self, containerd_id: impl ToString) -> Result<Vec<oci::OciArtifact>> {
// and possibly other configuration layers.
pub fn load_modules(
&self,
containerd_id: impl ToString,
) -> Result<(Vec<oci::WasmLayer>, Platform)> {
let image_name = self.get_image(containerd_id.to_string())?;
let digest = self.get_image_content_sha(image_name)?;
let manifest = self.read_content(digest)?;
let manifest = manifest.as_slice();
let manifest = ImageManifest::from_reader(manifest)?;

let artifact_type = manifest
.artifact_type()
.as_ref()
.ok_or(ShimError::Others("manifest is not an OCI Artifact".to_string()))?;
let image_config_descriptor = manifest.config();
let image_config = self.read_content(image_config_descriptor.digest())?;
let image_config = image_config.as_slice();

match artifact_type {
MediaType::Other(s) if s == COMPONENT_ARTIFACT_TYPE || s == MODULE_ARTIFACT_TYPE => {
log::info!("manifest with OCI Artifact of type {s}");
}
_ => {
log::info!("manifest is not a known OCI Artifact: {artifact_type}");
return Ok([].to_vec());
}
}
// the only part we care about here is the platform values
let platform: Platform = serde_json::from_slice(image_config)?;
let Arch::Wasm = platform.architecture() else {
log::info!("manifest is not in WASM OCI image format");
return Ok((vec![], platform));
};
log::info!("found manifest with WASM OCI image format.");

Ok(manifest
let layers = manifest
.layers()
.iter()
.filter(|x| !is_image_layer_type(x.media_type()))
.map(|config| {
self.read_content(config.digest())
.map(|module| OciArtifact {
config: config.clone(),
layer: module,
})
self.read_content(config.digest()).map(|module| WasmLayer {
config: config.clone(),
layer: module,
})
})
.collect::<Result<Vec<_>>>()?)
.collect::<Result<Vec<_>>>()?;
Ok((layers, platform))
}
}

fn is_image_layer_type(media_type: &MediaType) -> bool {
match media_type {
MediaType::ImageLayer
| MediaType::ImageLayerGzip
| MediaType::ImageLayerNonDistributable
| MediaType::ImageLayerNonDistributableGzip
| MediaType::ImageLayerNonDistributableZstd
| MediaType::ImageLayerZstd => true,
MediaType::Other(s)
if s.as_str()
.starts_with("application/vnd.docker.image.rootfs.") =>
{
true
}
_ => false,
}
}
2 changes: 2 additions & 0 deletions crates/containerd-shim-wasm/src/sandbox/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ pub enum Error {
#[cfg(unix)]
#[error("{0}")]
Libcontainer(#[from] libcontainer::error::LibcontainerError),
#[error("{0}")]
Containerd(String),
}

pub type Result<T> = ::std::result::Result<T, Error>;
Expand Down
5 changes: 1 addition & 4 deletions crates/containerd-shim-wasm/src/sandbox/oci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@ pub use oci_spec::runtime::Spec;

use super::error::Result;

pub const COMPONENT_ARTIFACT_TYPE: &str = "application/vnd.bytecodealliance.component.v1+wasm";
pub const MODULE_ARTIFACT_TYPE: &str = "application/vnd.bytecodealliance.module.v1+wasm";

#[derive(Clone)]
pub struct OciArtifact {
pub struct WasmLayer {
pub config: Descriptor,
pub layer: Vec<u8>,
}
Expand Down
19 changes: 12 additions & 7 deletions crates/containerd-shim-wasm/src/sys/unix/container/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ use libcontainer::workload::{
Executor as LibcontainerExecutor, ExecutorError as LibcontainerExecutorError,
ExecutorValidationError,
};
use oci_spec::image::Platform;
use oci_spec::runtime::Spec;

use crate::container::{Engine, PathResolve, RuntimeContext, Stdio, WasiContext};
use crate::sandbox::oci::OciArtifact;
use crate::sandbox::oci::WasmLayer;

#[derive(Clone)]
enum InnerExecutor {
Expand All @@ -27,7 +28,8 @@ pub(crate) struct Executor<E: Engine> {
engine: E,
stdio: Stdio,
inner: OnceCell<InnerExecutor>,
oci_artifacts: Vec<OciArtifact>,
wasm_layers: Vec<WasmLayer>,
platform: Platform,
}

impl<E: Engine> LibcontainerExecutor for Executor<E> {
Expand Down Expand Up @@ -64,27 +66,30 @@ impl<E: Engine> LibcontainerExecutor for Executor<E> {
}

impl<E: Engine> Executor<E> {
pub fn new(engine: E, stdio: Stdio, oci_artifacts: Vec<OciArtifact>) -> Self {
pub fn new(engine: E, stdio: Stdio, wasm_layers: Vec<WasmLayer>, platform: Platform) -> Self {
Self {
engine,
stdio,
inner: Default::default(),
oci_artifacts,
wasm_layers,
platform,
}
}

fn ctx<'a>(&'a self, spec: &'a Spec) -> WasiContext<'a> {
let oci_artifacts = &self.oci_artifacts;
let wasm_layers = &self.wasm_layers;
let platform = &self.platform;
WasiContext {
spec,
oci_artifacts,
wasm_layers,
platform,
}
}

fn inner(&self, spec: &Spec) -> &InnerExecutor {
self.inner.get_or_init(|| {
// if the spec has oci annotations we know it is wasm so short circuit checks
if !self.oci_artifacts.is_empty() {
if !self.wasm_layers.is_empty() {
InnerExecutor::Wasm
} else if is_linux_container(&self.ctx(spec)).is_ok() {
InnerExecutor::Linux
Expand Down
Loading

0 comments on commit d1b21fb

Please sign in to comment.