Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consolidate the RuntimeContext API to use one EntryPoint and expose Source for WASI files #398

Merged
merged 3 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 148 additions & 33 deletions crates/containerd-shim-wasm/src/container/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,47 @@ pub trait RuntimeContext {
// path to the entrypoint executable.
fn args(&self) -> &[String];

// ctx.entrypoint() returns the entrypoint path from arguments on the runtime
// spec process field.
fn entrypoint(&self) -> Option<&Path>;

// ctx.wasi_entrypoint() returns a `WasiEntrypoint` with the path to the module to use
// as an entrypoint and the name of the exported function to call, obtained from the
// ctx.entrypoint() returns a `Entrypoint` with the following fields obtained from the first argument in the OCI spec for entrypoint:
// - `arg0` - raw entrypoint from the OCI spec
// - `name` - provided as the file name of the module in the entrypoint without the extension
// - `func` - name of the exported function to call, obtained from the
// arguments on process OCI spec.
// The girst argument in the spec is specified as `path#func` where `func` is optional
// - `Source` - either a `File(PathBuf)` or `Oci(WasmLayer)`. When a `File` source the `PathBuf`` is provided by entrypoint in OCI spec.
// If the image contains custom OCI Wasm layers, the source is provided as an array of `WasmLayer` structs.
//
// The first argument in the OCI spec for entrypoint is specified as `path#func` where `func` is optional
// and defaults to _start, e.g.:
// "/app/app.wasm#entry" -> { path: "/app/app.wasm", func: "entry" }
// "my_module.wat" -> { path: "my_module.wat", func: "_start" }
// "#init" -> { path: "", func: "init" }
fn wasi_entrypoint(&self) -> WasiEntrypoint;

fn wasm_layers(&self) -> &[WasmLayer];
// "/app/app.wasm#entry" -> { source: File("/app/app.wasm"), func: "entry", name: "Some(app)", arg0: "/app/app.wasm#entry" }
// "my_module.wat" -> { source: File("my_module.wat"), func: "_start", name: "Some(my_module)", arg0: "my_module.wat" }
// "#init" -> { source: File(""), func: "init", name: None, arg0: "#init" }
fn entrypoint(&self) -> Entrypoint;

// the platform for the container using the struct defined on the OCI spec definition
// https://github.com/opencontainers/image-spec/blob/v1.1.0-rc5/image-index.md
fn platform(&self) -> &Platform;
}

pub struct WasiEntrypoint {
pub path: PathBuf,
/// The source for a WASI module / components.
pub enum Source<'a> {
// The WASI module is a file in the file system.
File(PathBuf),
// The WASI module / component is provided as a layer in the OCI spec.
// For a WASI preview 1 module this is usually a single element array.
// For a WASI preview 2 component this is an array of one or more
// elements, where each element is a component.
// Runtimes can additionally provide a list of layer types they support,
// and they will be included in this array, e.g., a `toml` file with the
// runtime configuration.
Oci(&'a [WasmLayer]),
}

/// The entrypoint for a WASI module / component.
///
pub struct Entrypoint<'a> {
pub func: String,
pub name: Option<String>,
pub arg0: Option<&'a Path>,
jsturtevant marked this conversation as resolved.
Show resolved Hide resolved
pub source: Source<'a>,
}

pub(crate) struct WasiContext<'a> {
Expand All @@ -50,23 +69,32 @@ impl RuntimeContext for WasiContext<'_> {
.unwrap_or_default()
}

fn entrypoint(&self) -> Option<&Path> {
self.args().first().map(Path::new)
}
fn entrypoint(&self) -> Entrypoint {
let arg0 = self.args().first();

let entry_point = arg0.map(String::as_str).unwrap_or("");
let (path, func) = entry_point
.split_once('#')
.unwrap_or((entry_point, "_start"));

let source = if self.wasm_layers.is_empty() {
Source::File(PathBuf::from(path))
} else {
Source::Oci(self.wasm_layers)
};

fn wasi_entrypoint(&self) -> WasiEntrypoint {
let arg0 = self.args().first().map(String::as_str).unwrap_or("");
let (path, func) = arg0.split_once('#').unwrap_or((arg0, "_start"));
WasiEntrypoint {
path: PathBuf::from(path),
let module_name = PathBuf::from(path)
.file_stem()
.map(|name| name.to_string_lossy().to_string());

Entrypoint {
func: func.to_string(),
arg0: arg0.map(Path::new),
source,
name: module_name,
}
}

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

fn platform(&self) -> &Platform {
self.platform
}
Expand All @@ -75,6 +103,7 @@ impl RuntimeContext for WasiContext<'_> {
#[cfg(test)]
mod tests {
use anyhow::Result;
use oci_spec::image::Descriptor;
use oci_spec::runtime::{ProcessBuilder, RootBuilder, SpecBuilder};

use super::*;
Expand Down Expand Up @@ -167,8 +196,11 @@ mod tests {
platform: &Platform::default(),
};

let path = ctx.wasi_entrypoint().path;
assert!(path.as_os_str().is_empty());
let path = ctx.entrypoint().source;
assert!(matches!(
path,
Source::File(p) if p.as_os_str().is_empty()
));

Ok(())
}
Expand All @@ -195,9 +227,20 @@ mod tests {
platform: &Platform::default(),
};

let WasiEntrypoint { path, func } = ctx.wasi_entrypoint();
assert_eq!(path, Path::new("hello.wat"));
let expected_path = PathBuf::from("hello.wat");
let Entrypoint {
name,
func,
arg0,
source,
} = ctx.entrypoint();
assert_eq!(name, Some("hello".to_string()));
assert_eq!(func, "foo");
assert_eq!(arg0, Some(Path::new("hello.wat#foo")));
assert!(matches!(
source,
Source::File(p) if p == expected_path
));

Ok(())
}
Expand All @@ -224,9 +267,81 @@ mod tests {
platform: &Platform::default(),
};

let WasiEntrypoint { path, func } = ctx.wasi_entrypoint();
assert_eq!(path, Path::new("/root/hello.wat"));
let expected_path = PathBuf::from("/root/hello.wat");
let Entrypoint {
name,
func,
arg0,
source,
} = ctx.entrypoint();
assert_eq!(name, Some("hello".to_string()));
assert_eq!(func, "_start");
assert_eq!(arg0, Some(Path::new("/root/hello.wat")));
assert!(matches!(
source,
Source::File(p) if p == expected_path
));

Ok(())
}

#[test]
fn test_loading_strategy_is_file_when_no_layers() -> Result<()> {
let spec = SpecBuilder::default()
.root(RootBuilder::default().path("rootfs").build()?)
.process(
ProcessBuilder::default()
.cwd("/")
.args(vec![
"/root/hello.wat#foo".to_string(),
"echo".to_string(),
"hello".to_string(),
])
.build()?,
)
.build()?;

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

let expected_path = PathBuf::from("/root/hello.wat");
assert!(matches!(
ctx.entrypoint().source,
Source::File(p) if p == expected_path
));

Ok(())
}

#[test]
fn test_loading_strategy_is_oci_when_layers_present() -> Result<()> {
let spec = SpecBuilder::default()
.root(RootBuilder::default().path("rootfs").build()?)
.process(
ProcessBuilder::default()
.cwd("/")
.args(vec![
"/root/hello.wat".to_string(),
"echo".to_string(),
"hello".to_string(),
])
.build()?,
)
.build()?;

let ctx = WasiContext {
spec: &spec,
wasm_layers: &[WasmLayer {
layer: vec![],
config: Descriptor::new(oci_spec::image::MediaType::Other("".to_string()), 10, ""),
}],
platform: &Platform::default(),
};

assert!(matches!(ctx.entrypoint().source, Source::Oci(_)));

Ok(())
}
Expand Down
24 changes: 20 additions & 4 deletions crates/containerd-shim-wasm/src/container/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::io::Read;

use anyhow::{Context, Result};

use super::Source;
use crate::container::{PathResolve, RuntimeContext};
use crate::sandbox::Stdio;

Expand All @@ -16,13 +17,18 @@ pub trait Engine: Clone + Send + Sync + 'static {
/// Check that the runtime can run the container.
/// This checks runs after the container creation and before the container starts.
/// By it checks that the wasi_entrypoint is either:
/// * a OCI image with wasm layers
/// * a file with the `wasm` filetype header
/// * a parsable `wat` file.
fn can_handle(&self, ctx: &impl RuntimeContext) -> Result<()> {
let path = ctx
.wasi_entrypoint()
.path
.resolve_in_path_or_cwd()
let source = ctx.entrypoint().source;

let path = match source {
Source::File(path) => path,
Source::Oci(_) => return Ok(()),
};

path.resolve_in_path_or_cwd()
.next()
.context("module not found")?;

Expand All @@ -36,4 +42,14 @@ pub trait Engine: Clone + Send + Sync + 'static {

Ok(())
}

/// Return the supported OCI layer types
/// This is used to filter only layers that are supported by the runtime.
/// The default implementation returns the OCI layer type 'application/vnd.bytecodealliance.wasm.component.layer.v0+wasm'
/// for WASM modules which can be contain with wasip1 or wasip2 components.
/// Runtimes can override this to support other layer types
/// such as lays that contain runtime specific configuration
fn supported_layers_types() -> &'static [&'static str] {
&["application/vnd.bytecodealliance.wasm.component.layer.v0+wasm"]
jsturtevant marked this conversation as resolved.
Show resolved Hide resolved
}
}
2 changes: 1 addition & 1 deletion crates/containerd-shim-wasm/src/container/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod engine;
mod path;

pub(crate) use context::WasiContext;
pub use context::{RuntimeContext, WasiEntrypoint};
pub use context::{Entrypoint, RuntimeContext, Source};
pub use engine::Engine;
pub use instance::Instance;
pub use path::PathResolve;
Expand Down
21 changes: 4 additions & 17 deletions crates/containerd-shim-wasm/src/sandbox/containerd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ impl Client {
pub fn load_modules(
&self,
containerd_id: impl ToString,
supported_layer_types: &[&str],
) -> Result<(Vec<oci::WasmLayer>, Platform)> {
let image_name = self.get_image(containerd_id.to_string())?;
let digest = self.get_image_content_sha(image_name)?;
Expand All @@ -141,7 +142,7 @@ impl Client {
let layers = manifest
.layers()
.iter()
.filter(|x| !is_image_layer_type(x.media_type()))
.filter(|x| is_wasm_layer(x.media_type(), supported_layer_types))
.map(|config| {
self.read_content(config.digest()).map(|module| WasmLayer {
config: config.clone(),
Expand All @@ -153,20 +154,6 @@ impl Client {
}
}

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,
}
fn is_wasm_layer(media_type: &MediaType, supported_layer_types: &[&str]) -> bool {
supported_layer_types.contains(&media_type.to_string().as_str())
}
12 changes: 7 additions & 5 deletions crates/containerd-shim-wasm/src/sys/unix/container/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use libcontainer::workload::{
use oci_spec::image::Platform;
use oci_spec::runtime::Spec;

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

#[derive(Clone)]
Expand Down Expand Up @@ -88,10 +88,7 @@ impl<E: Engine> Executor<E> {

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.wasm_layers.is_empty() {
InnerExecutor::Wasm
} else if is_linux_container(&self.ctx(spec)).is_ok() {
if is_linux_container(&self.ctx(spec)).is_ok() {
InnerExecutor::Linux
} else if self.engine.can_handle(&self.ctx(spec)).is_ok() {
InnerExecutor::Wasm
Expand All @@ -103,8 +100,13 @@ impl<E: Engine> Executor<E> {
}

fn is_linux_container(ctx: &impl RuntimeContext) -> Result<()> {
if let Source::Oci(_) = ctx.entrypoint().source {
bail!("the entry point contains wasm layers")
};

let executable = ctx
.entrypoint()
.arg0
.context("no entrypoint provided")?
.resolve_in_path()
.find_map(|p| -> Option<PathBuf> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl<E: Engine> SandboxInstance for Instance<E> {

// check if container is OCI image with wasm layers and attempt to read the module
let (modules, platform) = containerd::Client::connect(cfg.get_containerd_address(), &namespace)?
.load_modules(&id)
.load_modules(&id, E::supported_layers_types())
.unwrap_or_else(|e| {
log::warn!("Error obtaining wasm layers for container {id}. Will attempt to use files inside container image. Error: {e}");
(vec![], Platform::default())
Expand Down
Loading