diff --git a/crates/containerd-shim-wasm/src/container/context.rs b/crates/containerd-shim-wasm/src/container/context.rs index 97fea259a..ae6c5cc47 100644 --- a/crates/containerd-shim-wasm/src/container/context.rs +++ b/crates/containerd-shim-wasm/src/container/context.rs @@ -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, + pub arg0: Option<&'a Path>, + pub source: Source<'a>, } pub(crate) struct WasiContext<'a> { @@ -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 } @@ -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::*; @@ -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(()) } @@ -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(()) } @@ -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(()) } diff --git a/crates/containerd-shim-wasm/src/container/engine.rs b/crates/containerd-shim-wasm/src/container/engine.rs index 8c9783117..77f10cc3f 100644 --- a/crates/containerd-shim-wasm/src/container/engine.rs +++ b/crates/containerd-shim-wasm/src/container/engine.rs @@ -3,6 +3,7 @@ use std::io::Read; use anyhow::{Context, Result}; +use super::Source; use crate::container::{PathResolve, RuntimeContext}; use crate::sandbox::Stdio; @@ -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")?; @@ -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"] + } } diff --git a/crates/containerd-shim-wasm/src/container/mod.rs b/crates/containerd-shim-wasm/src/container/mod.rs index ff2aca7f5..5554a47d3 100644 --- a/crates/containerd-shim-wasm/src/container/mod.rs +++ b/crates/containerd-shim-wasm/src/container/mod.rs @@ -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; diff --git a/crates/containerd-shim-wasm/src/sandbox/containerd.rs b/crates/containerd-shim-wasm/src/sandbox/containerd.rs index 3168c1dd7..8a5d2782b 100644 --- a/crates/containerd-shim-wasm/src/sandbox/containerd.rs +++ b/crates/containerd-shim-wasm/src/sandbox/containerd.rs @@ -119,6 +119,7 @@ impl Client { pub fn load_modules( &self, containerd_id: impl ToString, + supported_layer_types: &[&str], ) -> Result<(Vec, Platform)> { let image_name = self.get_image(containerd_id.to_string())?; let digest = self.get_image_content_sha(image_name)?; @@ -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(), @@ -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()) } diff --git a/crates/containerd-shim-wasm/src/sys/unix/container/executor.rs b/crates/containerd-shim-wasm/src/sys/unix/container/executor.rs index 287f74d3c..81e45933e 100644 --- a/crates/containerd-shim-wasm/src/sys/unix/container/executor.rs +++ b/crates/containerd-shim-wasm/src/sys/unix/container/executor.rs @@ -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)] @@ -88,10 +88,7 @@ impl Executor { 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 @@ -103,8 +100,13 @@ impl Executor { } 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 { diff --git a/crates/containerd-shim-wasm/src/sys/unix/container/instance.rs b/crates/containerd-shim-wasm/src/sys/unix/container/instance.rs index 55c6b111c..8b228066f 100644 --- a/crates/containerd-shim-wasm/src/sys/unix/container/instance.rs +++ b/crates/containerd-shim-wasm/src/sys/unix/container/instance.rs @@ -45,7 +45,7 @@ impl SandboxInstance for Instance { // 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()) diff --git a/crates/containerd-shim-wasmedge/src/instance.rs b/crates/containerd-shim-wasmedge/src/instance.rs index ce723d439..e8c33151e 100644 --- a/crates/containerd-shim-wasmedge/src/instance.rs +++ b/crates/containerd-shim-wasmedge/src/instance.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Result}; use containerd_shim_wasm::container::{ - Engine, Instance, PathResolve, RuntimeContext, Stdio, WasiEntrypoint, + Engine, Entrypoint, Instance, PathResolve, RuntimeContext, Source, Stdio, }; use log::debug; use wasmedge_sdk::config::{ConfigBuilder, HostRegistrationConfigOptions}; @@ -35,10 +35,12 @@ impl Engine for WasmEdgeEngine { fn run_wasi(&self, ctx: &impl RuntimeContext, stdio: Stdio) -> Result { let args = ctx.args(); let envs: Vec<_> = std::env::vars().map(|(k, v)| format!("{k}={v}")).collect(); - let WasiEntrypoint { - path: entrypoint_path, + let Entrypoint { + source, func, - } = ctx.wasi_entrypoint(); + arg0: _, + name, + } = ctx.entrypoint(); let mut vm = self.vm.clone(); vm.wasi_module_mut() @@ -49,18 +51,15 @@ impl Engine for WasmEdgeEngine { Some(vec!["/:/"]), ); - let mod_name = match entrypoint_path.file_stem() { - Some(name) => name.to_string_lossy().to_string(), - None => "main".to_string(), - }; + let mod_name = name.unwrap_or_else(|| "main".to_string()); PluginManager::load(None)?; let vm = vm.auto_detect_plugins()?; - let vm = match ctx.wasm_layers() { - [] => { - debug!("loading module from file"); - let path = entrypoint_path + let vm = match source { + Source::File(path) => { + debug!("loading module from file {path:?}"); + let path = path .resolve_in_path_or_cwd() .next() .context("module not found")?; @@ -68,17 +67,19 @@ impl Engine for WasmEdgeEngine { vm.register_module_from_file(&mod_name, path) .context("registering module")? } - [module] => { + Source::Oci([module]) => { log::info!("loading module from wasm OCI layers"); vm.register_module_from_bytes(&mod_name, &module.layer) .context("registering module")? } - [..] => bail!("only a single module is supported when using images with OCI layers"), + Source::Oci(_modules) => { + bail!("only a single module is supported when using images with OCI layers") + } }; stdio.redirect()?; - log::debug!("running {entrypoint_path:?} with method {func:?}"); + log::debug!("running with method {func:?}"); vm.run_func(Some(&mod_name), func, vec![])?; let status = vm diff --git a/crates/containerd-shim-wasmer/src/instance.rs b/crates/containerd-shim-wasmer/src/instance.rs index 7ca35a2ec..7f1702cd0 100644 --- a/crates/containerd-shim-wasmer/src/instance.rs +++ b/crates/containerd-shim-wasmer/src/instance.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Result}; use containerd_shim_wasm::container::{ - Engine, Instance, PathResolve, RuntimeContext, Stdio, WasiEntrypoint, + Engine, Entrypoint, Instance, PathResolve, RuntimeContext, Source, Stdio, }; use wasmer::{Module, Store}; use wasmer_wasix::virtual_fs::host_fs::FileSystem; @@ -21,18 +21,20 @@ impl Engine for WasmerEngine { fn run_wasi(&self, ctx: &impl RuntimeContext, stdio: Stdio) -> Result { let args = ctx.args(); let envs = std::env::vars(); - let WasiEntrypoint { path, func } = ctx.wasi_entrypoint(); + let Entrypoint { + source, + func, + arg0: _, + name, + } = ctx.entrypoint(); - let mod_name = match path.file_stem() { - Some(name) => name.to_string_lossy().to_string(), - None => "main".to_string(), - }; + let mod_name = name.unwrap_or_else(|| "main".to_string()); log::info!("Create a Store"); let mut store = Store::new(self.engine.clone()); - let module = match ctx.wasm_layers() { - [] => { + let module = match source { + Source::File(path) => { log::info!("loading module from file {path:?}"); let path = path .resolve_in_path_or_cwd() @@ -41,11 +43,14 @@ impl Engine for WasmerEngine { Module::from_file(&store, path)? } - [module] => { + Source::Oci([module]) => { + log::info!("loading module wasm OCI layers"); log::info!("loading module wasm OCI layers"); Module::from_binary(&store, &module.layer)? } - [..] => bail!("only a single module is supported when using images with OCI layers"), + Source::Oci(_modules) => { + bail!("only a single module is supported when using images with OCI layers") + } }; let runtime = tokio::runtime::Builder::new_multi_thread() diff --git a/crates/containerd-shim-wasmtime/src/instance.rs b/crates/containerd-shim-wasmtime/src/instance.rs index 3655a2773..9e0bd9bc9 100644 --- a/crates/containerd-shim-wasmtime/src/instance.rs +++ b/crates/containerd-shim-wasmtime/src/instance.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Result}; use containerd_shim_wasm::container::{ - Engine, Instance, PathResolve, RuntimeContext, Stdio, WasiEntrypoint, + Engine, Entrypoint, Instance, PathResolve, RuntimeContext, Source, Stdio, }; use wasi_common::I32Exit; use wasmtime::{Linker, Module, Store}; @@ -20,26 +20,31 @@ impl Engine for WasmtimeEngine { fn run_wasi(&self, ctx: &impl RuntimeContext, stdio: Stdio) -> Result { log::info!("setting up wasi"); - let path = Dir::from_std_file(std::fs::File::open("/")?); + let root_path = Dir::from_std_file(std::fs::File::open("/")?); let envs: Vec<_> = std::env::vars().collect(); let wasi_builder = WasiCtxBuilder::new() .args(ctx.args())? .envs(envs.as_slice())? .inherit_stdio() - .preopened_dir(path, "/")?; + .preopened_dir(root_path, "/")?; stdio.redirect()?; log::info!("building wasi context"); let wctx = wasi_builder.build(); - log::info!("wasi context ready"); - let WasiEntrypoint { path, func } = ctx.wasi_entrypoint(); + let Entrypoint { + source, + func, + arg0: _, + name: _, + } = ctx.entrypoint(); - let module = match ctx.wasm_layers() { - [] => { - log::info!("loading module from file"); + log::info!("wasi context ready"); + let module = match source { + Source::File(path) => { + log::info!("loading module from path {path:?}"); let path = path .resolve_in_path_or_cwd() .next() @@ -47,11 +52,13 @@ impl Engine for WasmtimeEngine { Module::from_file(&self.engine, path)? } - [module] => { + Source::Oci([module]) => { log::info!("loading module wasm OCI layers"); Module::from_binary(&self.engine, &module.layer)? } - [..] => bail!("only a single module is supported when using images with OCI layers"), + Source::Oci(_modules) => { + bail!("only a single module is supported when using images with OCI layers") + } }; let mut linker = Linker::new(&self.engine); @@ -67,7 +74,7 @@ impl Engine for WasmtimeEngine { .get_func(&mut store, &func) .context("module does not have a WASI start function")?; - log::debug!("running {path:?} with start function {func:?}"); + log::debug!("running with start function {func:?}"); let status = start_func.call(&mut store, &[], &mut []); let status = status.map(|_| 0).or_else(|err| {