From 8631d3a49e1e54849fe61afacf681c3538d318ab Mon Sep 17 00:00:00 2001 From: James Sturtevant Date: Fri, 21 Jul 2023 23:23:36 +0000 Subject: [PATCH] Refactor to put logic in one place Signed-off-by: James Sturtevant --- .../containerd-shim-wasm/src/sandbox/oci.rs | 141 +++++++++++++++ .../containerd-shim-wasmedge/src/executor.rs | 21 ++- .../containerd-shim-wasmedge/src/instance.rs | 8 +- .../containerd-shim-wasmtime/src/executor.rs | 28 ++- .../containerd-shim-wasmtime/src/instance.rs | 163 +++++++++++------- 5 files changed, 277 insertions(+), 84 deletions(-) diff --git a/crates/containerd-shim-wasm/src/sandbox/oci.rs b/crates/containerd-shim-wasm/src/sandbox/oci.rs index 909e9afc8..ceb5cb45a 100644 --- a/crates/containerd-shim-wasm/src/sandbox/oci.rs +++ b/crates/containerd-shim-wasm/src/sandbox/oci.rs @@ -36,6 +36,25 @@ pub fn get_args(spec: &Spec) -> &[String] { } } +pub fn get_module(spec: &Spec) -> (Option, String) { + let args = get_args(spec); + + if !args.is_empty() { + let start = args[0].clone(); + let mut iterator = start.split('#'); + let mut cmd = iterator.next().unwrap().to_string(); + + let stripped = cmd.strip_prefix(std::path::MAIN_SEPARATOR); + if let Some(strpd) = stripped { + cmd = strpd.to_string(); + } + let method = iterator.next().unwrap_or("_start"); + return (Some(cmd), method.to_string()); + } + + (None, "_start".to_string()) +} + pub fn spec_from_file>(path: P) -> Result { let file = File::open(path)?; let cfg: Spec = json::from_reader(file)?; @@ -160,3 +179,125 @@ pub fn setup_prestart_hooks(hooks: &Option) -> Result< } Ok(()) } + +#[cfg(test)] +mod oci_tests { + use super::*; + use oci_spec::runtime::{ProcessBuilder, RootBuilder, SpecBuilder}; + + #[test] + fn test_get_args() -> Result<()> { + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec!["hello.wat".to_string()]) + .build()?, + ) + .build()?; + + let args = get_args(&spec); + assert_eq!(args.len(), 1); + assert_eq!(args[0], "hello.wat"); + + Ok(()) + } + + #[test] + fn test_get_args_return_empty() -> Result<()> { + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process(ProcessBuilder::default().cwd("/").args(vec![]).build()?) + .build()?; + + let args = get_args(&spec); + assert_eq!(args.len(), 0); + + Ok(()) + } + + #[test] + fn test_get_args_returns_all() -> Result<()> { + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec![ + "hello.wat".to_string(), + "echo".to_string(), + "hello".to_string(), + ]) + .build()?, + ) + .build()?; + + let args = get_args(&spec); + assert_eq!(args.len(), 3); + assert_eq!(args[0], "hello.wat"); + assert_eq!(args[1], "echo"); + assert_eq!(args[2], "hello"); + + Ok(()) + } + + #[test] + fn test_get_module_returns_none_when_not_present() -> Result<()> { + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process(ProcessBuilder::default().cwd("/").args(vec![]).build()?) + .build()?; + + let (module, _) = get_module(&spec); + assert_eq!(module, None); + + Ok(()) + } + + #[test] + fn test_get_module_returns_function() -> Result<()> { + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec![ + "hello.wat#foo".to_string(), + "echo".to_string(), + "hello".to_string(), + ]) + .build()?, + ) + .build()?; + + let (module, function) = get_module(&spec); + assert_eq!(module, Some("hello.wat".to_string())); + assert_eq!(function, "foo"); + + Ok(()) + } + + #[test] + fn test_get_module_returns_start() -> Result<()> { + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec![ + "hello.wat".to_string(), + "echo".to_string(), + "hello".to_string(), + ]) + .build()?, + ) + .build()?; + + let (module, function) = get_module(&spec); + assert_eq!(module, Some("hello.wat".to_string())); + assert_eq!(function, "_start"); + + Ok(()) + } +} diff --git a/crates/containerd-shim-wasmedge/src/executor.rs b/crates/containerd-shim-wasmedge/src/executor.rs index 21ab3bf76..fb86d58a9 100644 --- a/crates/containerd-shim-wasmedge/src/executor.rs +++ b/crates/containerd-shim-wasmedge/src/executor.rs @@ -6,6 +6,7 @@ use oci_spec::runtime::Spec; use libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; use libcontainer::workload::{Executor, ExecutorError}; use std::os::unix::io::RawFd; +use log::debug; use wasmedge_sdk::{ config::{CommonConfigOptions, ConfigBuilder, HostRegistrationConfigOptions}, @@ -34,7 +35,9 @@ impl Executor for WasmEdgeExecutor { // TODO: How to get exit code? // This was relatively straight forward in go, but wasi and wasmtime are totally separate things in rust - match vm.run_func(Some("main"), "_start", params!()) { + let (module_name, method) = oci::get_module(spec); + debug!("running {:?} with method {}", module_name, method); + match vm.run_func(Some("main"), method, params!()) { Ok(_) => std::process::exit(0), Err(_) => std::process::exit(137), }; @@ -51,10 +54,6 @@ impl Executor for WasmEdgeExecutor { impl WasmEdgeExecutor { fn prepare(&self, args: &[String], spec: &Spec) -> anyhow::Result { - let mut cmd = args[0].clone(); - if let Some(stripped) = args[0].strip_prefix(std::path::MAIN_SEPARATOR) { - cmd = stripped.to_string(); - } let envs = env_to_wasi(spec); let config = ConfigBuilder::new(CommonConfigOptions::default()) .with_host_registration_config(HostRegistrationConfigOptions::default().wasi(true)) @@ -73,8 +72,18 @@ impl WasmEdgeExecutor { Some(envs.iter().map(|s| s as &str).collect()), None, ); + + let (module_name, _) = oci::get_module(spec); + let module_name = match module_name { + Some(m) => m, + None => { + return Err(anyhow::Error::msg( + "no module provided cannot load module", + )) + } + }; let vm = vm - .register_module_from_file("main", cmd) + .register_module_from_file("main", module_name) .map_err(|err| ExecutorError::Execution(err))?; if let Some(stdin) = self.stdin { dup(STDIN_FILENO)?; diff --git a/crates/containerd-shim-wasmedge/src/instance.rs b/crates/containerd-shim-wasmedge/src/instance.rs index 0906d15f0..29bf2c777 100644 --- a/crates/containerd-shim-wasmedge/src/instance.rs +++ b/crates/containerd-shim-wasmedge/src/instance.rs @@ -430,9 +430,9 @@ mod wasitest { let dir = tempdir()?; let path = dir.path(); - let wasmbytes = wat2wasm(WASI_HELLO_WAT).unwrap(); + let wasm_bytes = wat2wasm(WASI_HELLO_WAT).unwrap(); - let res = run_wasi_test(&dir, wasmbytes)?; + let res = run_wasi_test(&dir, wasm_bytes)?; assert_eq!(res.0, 0); @@ -452,9 +452,9 @@ mod wasitest { } let dir = tempdir()?; - let wasmbytes = wat2wasm(WASI_RETURN_ERROR).unwrap(); + let wasm_bytes = wat2wasm(WASI_RETURN_ERROR).unwrap(); - let res = run_wasi_test(&dir, wasmbytes)?; + let res = run_wasi_test(&dir, wasm_bytes)?; // Expect error code from the run. assert_eq!(res.0, 137); diff --git a/crates/containerd-shim-wasmtime/src/executor.rs b/crates/containerd-shim-wasmtime/src/executor.rs index df789c98f..e992ce5ee 100644 --- a/crates/containerd-shim-wasmtime/src/executor.rs +++ b/crates/containerd-shim-wasmtime/src/executor.rs @@ -1,7 +1,7 @@ use nix::unistd::{dup, dup2}; use std::{fs::OpenOptions, os::fd::RawFd}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use containerd_shim_wasm::sandbox::oci; use libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; use libcontainer::workload::{Executor, ExecutorError}; @@ -82,20 +82,18 @@ impl WasmtimeExecutor { let wctx = wasi_builder.build(); log::info!("wasi context ready"); - let mut iterator = args - .first() - .context("args must have at least one argument.")? - .split('#'); - let mut cmd = iterator.next().unwrap().to_string(); - let stripped = cmd.strip_prefix(std::path::MAIN_SEPARATOR); - if let Some(strpd) = stripped { - cmd = strpd.to_string(); - } - let method = iterator.next().unwrap_or("_start"); - let mod_path = cmd; + let (module_name, method) = oci::get_module(spec); + let module_name = match module_name { + Some(m) => m, + None => { + return Err(anyhow::format_err!( + "no module provided, cannot load module from file within container" + )) + } + }; - log::info!("loading module from file"); - let module = Module::from_file(&self.engine, mod_path)?; + log::info!("loading module from file {} ", module_name); + let module = Module::from_file(&self.engine, module_name)?; let mut linker = Linker::new(&self.engine); wasmtime_wasi::add_to_linker(&mut linker, |s| s)?; @@ -106,7 +104,7 @@ impl WasmtimeExecutor { log::info!("getting start function"); let start_func = instance - .get_func(&mut store, method) + .get_func(&mut store, &method) .ok_or_else(|| anyhow!("module does not have a WASI start function".to_string()))?; Ok((store, start_func)) } diff --git a/crates/containerd-shim-wasmtime/src/instance.rs b/crates/containerd-shim-wasmtime/src/instance.rs index 0385cb5bf..b354f1855 100644 --- a/crates/containerd-shim-wasmtime/src/instance.rs +++ b/crates/containerd-shim-wasmtime/src/instance.rs @@ -264,6 +264,7 @@ impl EngineGetter for Wasi { #[cfg(test)] mod wasitest { + use std::borrow::Cow; use std::fs::{create_dir, read_to_string, File, OpenOptions}; use std::io::prelude::*; use std::os::unix::prelude::OpenOptionsExt; @@ -280,39 +281,45 @@ mod wasitest { use super::*; // This is taken from https://github.com/bytecodealliance/wasmtime/blob/6a60e8363f50b936e4c4fc958cb9742314ff09f3/docs/WASI-tutorial.md?plain=1#L270-L298 - const WASI_HELLO_WAT: &[u8]= r#"(module - ;; Import the required fd_write WASI function which will write the given io vectors to stdout - ;; The function signature for fd_write is: - ;; (File Descriptor, *iovs, iovs_len, nwritten) -> Returns number of bytes written - (import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) - - (memory 1) - (export "memory" (memory 0)) - - ;; Write 'hello world\n' to memory at an offset of 8 bytes - ;; Note the trailing newline which is required for the text to appear - (data (i32.const 8) "hello world\n") - - (func $main (export "_start") - ;; Creating a new io vector within linear memory - (i32.store (i32.const 0) (i32.const 8)) ;; iov.iov_base - This is a pointer to the start of the 'hello world\n' string - (i32.store (i32.const 4) (i32.const 12)) ;; iov.iov_len - The length of the 'hello world\n' string - - (call $fd_write - (i32.const 1) ;; file_descriptor - 1 for stdout - (i32.const 0) ;; *iovs - The pointer to the iov array, which is stored at memory location 0 - (i32.const 1) ;; iovs_len - We're printing 1 string stored in an iov - so one. - (i32.const 20) ;; nwritten - A place in memory to store the number of bytes written + fn hello_world_module(start_fn: Option<&str>) -> Vec { + let start_fn = start_fn.unwrap_or("_start"); + format!(r#"(module + ;; Import the required fd_write WASI function which will write the given io vectors to stdout + ;; The function signature for fd_write is: + ;; (File Descriptor, *iovs, iovs_len, nwritten) -> Returns number of bytes written + (import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) + + (memory 1) + (export "memory" (memory 0)) + + ;; Write 'hello world\n' to memory at an offset of 8 bytes + ;; Note the trailing newline which is required for the text to appear + (data (i32.const 8) "hello world\n") + + (func $main (export "{start_fn}") + ;; Creating a new io vector within linear memory + (i32.store (i32.const 0) (i32.const 8)) ;; iov.iov_base - This is a pointer to the start of the 'hello world\n' string + (i32.store (i32.const 4) (i32.const 12)) ;; iov.iov_len - The length of the 'hello world\n' string + + (call $fd_write + (i32.const 1) ;; file_descriptor - 1 for stdout + (i32.const 0) ;; *iovs - The pointer to the iov array, which is stored at memory location 0 + (i32.const 1) ;; iovs_len - We're printing 1 string stored in an iov - so one. + (i32.const 20) ;; nwritten - A place in memory to store the number of bytes written + ) + drop ;; Discard the number of bytes written from the top of the stack ) - drop ;; Discard the number of bytes written from the top of the stack ) - ) - "#.as_bytes(); + "#).as_bytes().to_vec() + } #[test] fn test_delete_after_create() -> Result<()> { - let dir = tempdir()?; - let cfg = prepare_cfg(&dir)?; + let cfg = InstanceConfig::new( + Wasi::new_engine()?, + "test_namespace".into(), + "/containerd/address".into(), + ); let i = Wasi::new("".to_string(), Some(&cfg)); i.delete()?; @@ -321,51 +328,63 @@ mod wasitest { } #[test] - fn test_wasi() -> Result<(), Error> { + fn test_wasi_entrypoint() -> Result<(), Error> { if !has_cap_sys_admin() { println!("running test with sudo: {}", function!()); return run_test_with_sudo(function!()); } // start logging + // to enable logging run `export RUST_LOG=trace` and append cargo command with + // --show-output before running test let _ = env_logger::try_init(); let dir = tempdir()?; - let cfg = prepare_cfg(&dir)?; + let path = dir.path(); + let wasm_bytes = hello_world_module(None); - let wasi = Wasi::new("test".to_string(), Some(&cfg)); + let res = run_wasi_test(&dir, wasm_bytes.into(), None)?; - wasi.start()?; + assert_eq!(res.0, 0); - let (tx, rx) = channel(); - let waiter = Wait::new(tx); - wasi.wait(&waiter).unwrap(); + let output = read_to_string(path.join("stdout"))?; + assert_eq!(output, "hello world\n"); + + reset_stdio(); + Ok(()) + } + + // ignore until https://github.com/containerd/runwasi/issues/194 is resolved + #[test] + #[ignore] + fn test_wasi_custom_entrypoint() -> Result<(), Error> { + if !has_cap_sys_admin() { + println!("running test with sudo: {}", function!()); + return run_test_with_sudo(function!()); + } + // start logging + let _ = env_logger::try_init(); + + let dir = tempdir()?; + let path = dir.path(); + let wasm_bytes = hello_world_module(Some("foo")); + + let res = run_wasi_test(&dir, wasm_bytes.into(), Some("foo"))?; - let res = match rx.recv_timeout(Duration::from_secs(10)) { - Ok(res) => res, - Err(e) => { - wasi.kill(SIGKILL as u32).unwrap(); - return Err(Error::Others(format!( - "error waiting for module to finish: {0}", - e - ))); - } - }; assert_eq!(res.0, 0); - let output = read_to_string(dir.path().join("stdout"))?; + let output = read_to_string(path.join("stdout"))?; assert_eq!(output, "hello world\n"); - wasi.delete()?; - reset_stdio(); Ok(()) } - fn prepare_cfg(dir: &TempDir) -> Result> { + fn run_wasi_test(dir: &TempDir, wasmbytes: Cow<[u8]>, start_fn: Option<&str>) -> Result<(u32, DateTime), Error> { create_dir(dir.path().join("rootfs"))?; - + let rootdir = dir.path().join("runwasi"); + create_dir(&rootdir)?; let opts = Options { - root: Some(dir.path().join("runwasi")), + root: Some(rootdir), }; let opts_file = OpenOptions::new() .read(true) @@ -382,31 +401,57 @@ mod wasitest { .truncate(true) .mode(0o755) .open(wasm_path)?; - f.write_all(WASI_HELLO_WAT)?; + f.write_all(&wasmbytes)?; let stdout = File::create(dir.path().join("stdout"))?; - let stderr = File::create(dir.path().join("stderr"))?; drop(stdout); - drop(stderr); + + let entrypoint = match start_fn { + Some(s) => "./hello.wat#".to_string() + s, + None => "./hello.wat".to_string(), + }; let spec = SpecBuilder::default() .root(RootBuilder::default().path("rootfs").build()?) .process( ProcessBuilder::default() .cwd("/") - .args(vec!["./hello.wat".to_string()]) + .args(vec![entrypoint]) .build()?, ) .build()?; + spec.save(dir.path().join("config.json"))?; + let mut cfg = InstanceConfig::new( - Engine::default(), + Wasi::new_engine()?, "test_namespace".into(), "/containerd/address".into(), ); let cfg = cfg .set_bundle(dir.path().to_str().unwrap().to_string()) - .set_stdout(dir.path().join("stdout").to_str().unwrap().to_string()) - .set_stderr(dir.path().join("stderr").to_str().unwrap().to_string()); - Ok(cfg.to_owned()) + .set_stdout(dir.path().join("stdout").to_str().unwrap().to_string()); + + let wasi = Wasi::new("test".to_string(), Some(cfg)); + + wasi.start()?; + + let (tx, rx) = channel(); + let waiter = Wait::new(tx); + wasi.wait(&waiter).unwrap(); + + let res = match rx.recv_timeout(Duration::from_secs(10)) { + Ok(res) => Ok(res), + Err(e) => { + wasi.kill(SIGKILL as u32).unwrap(); + return Err(Error::Others(format!( + "error waiting for module to finish: {0}", + e + ))); + } + }; + wasi.delete()?; + res } + + }