From 3a96d6503fb7ee53ef996f1a88ed0f7ec0fbc5cd Mon Sep 17 00:00:00 2001 From: James Sturtevant Date: Thu, 16 Nov 2023 00:01:32 +0000 Subject: [PATCH] Improve the API for loading shim from file or OCI Signed-off-by: James Sturtevant --- .../src/container/context.rs | 110 +++++++++++++++--- .../src/container/engine.rs | 6 +- .../containerd-shim-wasm/src/container/mod.rs | 2 +- .../src/sandbox/containerd.rs | 21 +--- .../src/sys/unix/container/executor.rs | 1 + .../src/sys/unix/container/instance.rs | 2 +- .../containerd-shim-wasmedge/src/instance.rs | 21 ++-- crates/containerd-shim-wasmer/src/instance.rs | 19 ++- .../containerd-shim-wasmtime/src/instance.rs | 19 +-- 9 files changed, 139 insertions(+), 62 deletions(-) diff --git a/crates/containerd-shim-wasm/src/container/context.rs b/crates/containerd-shim-wasm/src/container/context.rs index 97fea259a..5eba36b16 100644 --- a/crates/containerd-shim-wasm/src/container/context.rs +++ b/crates/containerd-shim-wasm/src/container/context.rs @@ -10,10 +10,6 @@ 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 // arguments on process OCI spec. @@ -22,16 +18,22 @@ pub trait RuntimeContext { // "/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 entrypoint(&self) -> WasiEntrypoint; - fn wasm_layers(&self) -> &[WasmLayer]; + fn wasi_loading_strategy(&self) -> WasiLoadingStrategy; fn platform(&self) -> &Platform; } -pub struct WasiEntrypoint { +pub enum WasiLoadingStrategy<'a> { + File(PathBuf), + Oci(&'a [WasmLayer]), +} + +pub struct WasiEntrypoint<'a> { pub path: PathBuf, pub func: String, + pub arg0: Option<&'a Path>, } pub(crate) struct WasiContext<'a> { @@ -50,21 +52,26 @@ impl RuntimeContext for WasiContext<'_> { .unwrap_or_default() } - fn entrypoint(&self) -> Option<&Path> { - self.args().first().map(Path::new) - } + fn entrypoint(&self) -> WasiEntrypoint { + let arg0 = self.args().first(); - 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")); + let entry_point = arg0.map(String::as_str).unwrap_or(""); + let (path, func) = entry_point + .split_once('#') + .unwrap_or((entry_point, "_start")); WasiEntrypoint { path: PathBuf::from(path), func: func.to_string(), + arg0: arg0.map(Path::new), } } - fn wasm_layers(&self) -> &[WasmLayer] { - self.wasm_layers + fn wasi_loading_strategy(&self) -> WasiLoadingStrategy { + if self.wasm_layers.is_empty() { + WasiLoadingStrategy::File(self.entrypoint().path.clone()) + } else { + WasiLoadingStrategy::Oci(self.wasm_layers) + } } fn platform(&self) -> &Platform { @@ -75,6 +82,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,7 +175,7 @@ mod tests { platform: &Platform::default(), }; - let path = ctx.wasi_entrypoint().path; + let path = ctx.entrypoint().path; assert!(path.as_os_str().is_empty()); Ok(()) @@ -195,9 +203,10 @@ mod tests { platform: &Platform::default(), }; - let WasiEntrypoint { path, func } = ctx.wasi_entrypoint(); + let WasiEntrypoint { path, func, arg0 } = ctx.entrypoint(); assert_eq!(path, Path::new("hello.wat")); assert_eq!(func, "foo"); + assert_eq!(arg0, Some(Path::new("hello.wat#foo"))); Ok(()) } @@ -224,9 +233,74 @@ mod tests { platform: &Platform::default(), }; - let WasiEntrypoint { path, func } = ctx.wasi_entrypoint(); + let WasiEntrypoint { path, func, arg0 } = ctx.entrypoint(); assert_eq!(path, Path::new("/root/hello.wat")); assert_eq!(func, "_start"); + assert_eq!(arg0, Some(Path::new("/root/hello.wat"))); + + 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.wasi_loading_strategy(), + WasiLoadingStrategy::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.wasi_loading_strategy(), + WasiLoadingStrategy::Oci(_) + )); Ok(()) } diff --git a/crates/containerd-shim-wasm/src/container/engine.rs b/crates/containerd-shim-wasm/src/container/engine.rs index 8c9783117..28d0f08ab 100644 --- a/crates/containerd-shim-wasm/src/container/engine.rs +++ b/crates/containerd-shim-wasm/src/container/engine.rs @@ -20,7 +20,7 @@ pub trait Engine: Clone + Send + Sync + 'static { /// * a parsable `wat` file. fn can_handle(&self, ctx: &impl RuntimeContext) -> Result<()> { let path = ctx - .wasi_entrypoint() + .entrypoint() .path .resolve_in_path_or_cwd() .next() @@ -36,4 +36,8 @@ pub trait Engine: Clone + Send + Sync + 'static { Ok(()) } + + 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..8b8b9165f 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::{RuntimeContext, WasiEntrypoint, WasiLoadingStrategy}; 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..450a529e9 100644 --- a/crates/containerd-shim-wasm/src/sys/unix/container/executor.rs +++ b/crates/containerd-shim-wasm/src/sys/unix/container/executor.rs @@ -105,6 +105,7 @@ impl Executor { fn is_linux_container(ctx: &impl RuntimeContext) -> Result<()> { 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..5f5b09ebd 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, Instance, PathResolve, RuntimeContext, Stdio, WasiEntrypoint, WasiLoadingStrategy, }; use log::debug; use wasmedge_sdk::config::{ConfigBuilder, HostRegistrationConfigOptions}; @@ -38,7 +38,8 @@ impl Engine for WasmEdgeEngine { let WasiEntrypoint { path: entrypoint_path, func, - } = ctx.wasi_entrypoint(); + arg0: _, + } = ctx.entrypoint(); let mut vm = self.vm.clone(); vm.wasi_module_mut() @@ -57,10 +58,10 @@ impl Engine for WasmEdgeEngine { 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 ctx.wasi_loading_strategy() { + WasiLoadingStrategy::File(path) => { + debug!("loading module from file {path:?}"); + let path = path .resolve_in_path_or_cwd() .next() .context("module not found")?; @@ -68,17 +69,19 @@ impl Engine for WasmEdgeEngine { vm.register_module_from_file(&mod_name, path) .context("registering module")? } - [module] => { + WasiLoadingStrategy::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"), + WasiLoadingStrategy::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..d5344da3b 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, Instance, PathResolve, RuntimeContext, Stdio, WasiEntrypoint, WasiLoadingStrategy, }; use wasmer::{Module, Store}; use wasmer_wasix::virtual_fs::host_fs::FileSystem; @@ -21,7 +21,11 @@ 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 WasiEntrypoint { + path, + func, + arg0: _, + } = ctx.entrypoint(); let mod_name = match path.file_stem() { Some(name) => name.to_string_lossy().to_string(), @@ -31,8 +35,8 @@ impl Engine for WasmerEngine { log::info!("Create a Store"); let mut store = Store::new(self.engine.clone()); - let module = match ctx.wasm_layers() { - [] => { + let module = match ctx.wasi_loading_strategy() { + WasiLoadingStrategy::File(path) => { log::info!("loading module from file {path:?}"); let path = path .resolve_in_path_or_cwd() @@ -41,11 +45,14 @@ impl Engine for WasmerEngine { Module::from_file(&store, path)? } - [module] => { + WasiLoadingStrategy::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"), + WasiLoadingStrategy::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..22ff8bad2 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, Instance, PathResolve, RuntimeContext, Stdio, WasiLoadingStrategy, }; use wasi_common::I32Exit; use wasmtime::{Linker, Module, Store}; @@ -35,11 +35,9 @@ impl Engine for WasmtimeEngine { let wctx = wasi_builder.build(); log::info!("wasi context ready"); - let WasiEntrypoint { path, func } = ctx.wasi_entrypoint(); - - let module = match ctx.wasm_layers() { - [] => { - log::info!("loading module from file"); + let module = match ctx.wasi_loading_strategy() { + WasiLoadingStrategy::File(path) => { + log::info!("loading module from path {path:?}"); let path = path .resolve_in_path_or_cwd() .next() @@ -47,11 +45,13 @@ impl Engine for WasmtimeEngine { Module::from_file(&self.engine, path)? } - [module] => { + WasiLoadingStrategy::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"), + WasiLoadingStrategy::Oci(_modules) => { + bail!("only a single module is supported when using images with OCI layers") + } }; let mut linker = Linker::new(&self.engine); @@ -63,11 +63,12 @@ impl Engine for WasmtimeEngine { let instance: wasmtime::Instance = linker.instantiate(&mut store, &module)?; log::info!("getting start function"); + let func = ctx.entrypoint().func; let start_func = instance .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| {