diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 160ba2b52..fb3949e2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,4 +109,7 @@ jobs: make test/k3s - name: cleanup if: always() - run: make test/k3s/clean + run: | + sudo bin/k3s kubectl logs deployments/wasi-demo + sudo bin/k3s kubectl get pods -o wide + make test/k3s/clean diff --git a/.gitignore b/.gitignore index c99b23042..ab01ec003 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ test/out/img.tar !crates/wasmtime/src/bin/ test/k8s/_out release/ +.vscode/settings.json diff --git a/Makefile b/Makefile index c78c10c08..52e08462b 100644 --- a/Makefile +++ b/Makefile @@ -120,6 +120,7 @@ test/k3s: target/wasm32-wasi/$(TARGET)/img.tar bin/wasmedge bin/k3s sudo bin/k3s kubectl apply -f test/k8s/deploy.yaml sudo bin/k3s kubectl wait deployment wasi-demo --for condition=Available=True --timeout=90s && \ sudo bin/k3s kubectl get pods -o wide + sudo bin/k3s kubectl logs deployments/wasi-demo .PHONY: test/k3s/clean test/k3s/clean: bin/wasmedge/clean bin/k3s/clean diff --git a/crates/containerd-shim-wasm/src/sandbox/oci.rs b/crates/containerd-shim-wasm/src/sandbox/oci.rs index 909e9afc8..e39e650fd 100644 --- a/crates/containerd-shim-wasm/src/sandbox/oci.rs +++ b/crates/containerd-shim-wasm/src/sandbox/oci.rs @@ -36,6 +36,53 @@ pub fn get_args(spec: &Spec) -> &[String] { } } +pub fn get_module_args(spec: &Spec) -> &[String] { + let args = get_args(spec); + + match spec.annotations().clone() { + Some(annotations) + if annotations.contains_key("application/vnd.w3c.wasm.module.v1+wasm") => + { + args + } + _ => { + if args.len() > 1 { + &args[1..] + } else { + &[] + } + } + } +} + +pub fn get_module(spec: &Spec) -> (Option, String) { + let args = get_args(spec); + + match spec.annotations().clone() { + Some(annotations) + if annotations.contains_key("application/vnd.w3c.wasm.module.v1+wasm") => + { + (None, "_start".to_string()) + } + _ => { + 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"); + (Some(cmd), method.to_string()) + } else { + (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 +207,204 @@ 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_module_args_returns_module_args_only() -> 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_module_args(&spec); + assert_eq!(args.len(), 2); + assert_eq!(args[0], "echo"); + assert_eq!(args[1], "hello"); + + Ok(()) + } + + #[test] + fn test_module_args_returns_empty_when_just_module() -> 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_module_args(&spec); + assert_eq!(args.len(), 0); + + Ok(()) + } + + #[test] + fn test_module_args_returns_module_args_only_when_oci_artifact() -> Result<()> { + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec!["echo".to_string(), "hello".to_string()]) + .build()?, + ) + .annotations(HashMap::from([( + "application/vnd.w3c.wasm.module.v1+wasm".to_string(), + "sha256:1234".to_string(), + )])) + .build()?; + + let args = get_module_args(&spec); + assert_eq!(args.len(), 2); + assert_eq!(args[0], "echo"); + assert_eq!(args[1], "hello"); + + Ok(()) + } + + #[test] + fn test_module_args_returns_empty_args_when_oci_artifact() -> Result<()> { + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process(ProcessBuilder::default().cwd("/").args(vec![]).build()?) + .annotations(HashMap::from([( + "application/vnd.w3c.wasm.module.v1+wasm".to_string(), + "sha256:1234".to_string(), + )])) + .build()?; + + let args = get_module_args(&spec); + assert_eq!(args.len(), 0); + + Ok(()) + } + + #[test] + fn test_get_module_returns_empty_args_when_oci_artifact() -> Result<()> { + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec!["echo".to_string(), "hello".to_string()]) + .build()?, + ) + .annotations(HashMap::from([( + "application/vnd.w3c.wasm.module.v1+wasm".to_string(), + "sha256:1234".to_string(), + )])) + .build()?; + + let (module, method) = get_module(&spec); + assert_eq!(module, None); + assert_eq!(method, "_start"); + + Ok(()) + } + + #[test] + fn test_get_module_returns_module() -> Result<()> { + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec!["hello.wat".to_string()]) + .build()?, + ) + .build()?; + + let (module, method) = get_module(&spec); + assert_eq!(module.unwrap(), "hello.wat"); + assert_eq!(method, "_start"); + + 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(()) + } +} diff --git a/crates/containerd-shim-wasmedge/src/executor.rs b/crates/containerd-shim-wasmedge/src/executor.rs index 1158271bc..ce38d62f8 100644 --- a/crates/containerd-shim-wasmedge/src/executor.rs +++ b/crates/containerd-shim-wasmedge/src/executor.rs @@ -1,11 +1,12 @@ use anyhow::Result; +use containerd_shim_wasm::sandbox::oci; use nix::unistd::{dup, dup2}; use oci_spec::runtime::Spec; use libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; use libcontainer::workload::{Executor, ExecutorError}; +use log::debug; use std::os::unix::io::RawFd; - use wasmedge_sdk::{ config::{CommonConfigOptions, ConfigBuilder, HostRegistrationConfigOptions}, params, VmBuilder, @@ -21,16 +22,6 @@ pub struct WasmEdgeExecutor { impl Executor for WasmEdgeExecutor { fn exec(&self, spec: &Spec) -> Result<(), ExecutorError> { - // parse wasi parameters - let args = get_args(spec); - if args.is_empty() { - return Err(ExecutorError::InvalidArg); - } - - 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); // create configuration with `wasi` option enabled @@ -51,14 +42,34 @@ impl Executor for WasmEdgeExecutor { .ok_or_else(|| anyhow::Error::msg("Not found wasi module")) .map_err(|err| ExecutorError::Execution(err.into()))?; + let args = oci::get_module_args(spec); + let mut module_args = None; + if !args.is_empty() { + module_args = Some(args.iter().map(|s| s as &str).collect()) + } + + debug!("module args: {:?}", module_args); wasi_module.initialize( - Some(args.iter().map(|s| s as &str).collect()), + module_args, Some(envs.iter().map(|s| s as &str).collect()), None, ); + let (module_name, method) = oci::get_module(spec); + let module_name = match module_name { + Some(m) => m, + None => { + return Err(ExecutorError::Execution( + anyhow::Error::msg( + "no module provided, cannot load module from file within container", + ) + .into(), + )) + } + }; + let vm = vm - .register_module_from_file("main", cmd) + .register_module_from_file("main", module_name.clone()) .map_err(|err| ExecutorError::Execution(err))?; if let Some(stdin) = self.stdin { @@ -74,9 +85,8 @@ impl Executor for WasmEdgeExecutor { let _ = dup2(stderr, STDERR_FILENO); } - // 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!()) { + 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), }; @@ -91,18 +101,6 @@ impl Executor for WasmEdgeExecutor { } } -fn get_args(spec: &Spec) -> &[String] { - let p = match spec.process() { - None => return &[], - Some(p) => p, - }; - - match p.args() { - None => &[], - Some(args) => args.as_slice(), - } -} - fn env_to_wasi(spec: &Spec) -> Vec { let default = vec![]; let env = spec diff --git a/crates/containerd-shim-wasmtime/src/instance.rs b/crates/containerd-shim-wasmtime/src/instance.rs index 4fbaa7c9a..29b63530e 100644 --- a/crates/containerd-shim-wasmtime/src/instance.rs +++ b/crates/containerd-shim-wasmtime/src/instance.rs @@ -90,7 +90,7 @@ pub fn prepare_module( ) -> Result<(WasiCtx, Module, String), WasmtimeError> { debug!("opening rootfs"); let rootfs = oci_wasmtime::get_rootfs(spec)?; - let args = oci::get_args(spec); + let args = oci::get_module_args(spec); let env = oci_wasmtime::env_to_wasi(spec); debug!("setting up wasi"); @@ -121,22 +121,25 @@ pub fn prepare_module( let wctx = wasi_builder.build(); debug!("wasi context ready"); - 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"); + let (module_name, method) = oci::get_module(spec); + let module_name = match module_name { + Some(m) => m, + None => { + return Err(WasmtimeError::Other(anyhow::format_err!( + "no module provided, cannot load module from file within container" + ))) + } + }; - let mod_path = oci::get_root(spec).join(cmd); - debug!("loading module from file"); + let mod_path = oci::get_root(spec).join(module_name.clone()); + debug!( + "loading module from file {} with method {}", + module_name, method + ); let module = Module::from_file(&engine, mod_path) .map_err(|err| Error::Others(format!("could not load module from file: {}", err)))?; - Ok((wctx, module, method.to_string())) + Ok((wctx, module, method)) } impl Instance for Wasi { @@ -279,34 +282,36 @@ 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(startfn: String) -> Vec { + 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 "{startfn}") + ;; 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() { @@ -322,40 +327,58 @@ mod wasitest { #[test] fn test_wasi() -> Result<(), Error> { - let dir = tempdir()?; - create_dir(dir.path().join("rootfs"))?; + let module = hello_world_module("_start".to_string()); + let dir = create_module_dir(module)?; - let mut f = File::create(dir.path().join("rootfs/hello.wat"))?; - f.write_all(WASI_HELLO_WAT)?; + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec!["hello.wat".to_string()]) + .build()?, + ) + .build()?; - let stdout = File::create(dir.path().join("stdout"))?; - drop(stdout); + spec.save(dir.path().join("config.json"))?; + + run_module(dir)?; + + Ok(()) + } + + #[test] + fn test_wasi_entrypoint() -> Result<(), Error> { + let module = hello_world_module("foo".to_string()); + let dir = create_module_dir(module)?; let spec = SpecBuilder::default() .root(RootBuilder::default().path("rootfs").build()?) .process( ProcessBuilder::default() .cwd("/") - .args(vec!["hello.wat".to_string()]) + .args(vec!["hello.wat#foo".to_string()]) .build()?, ) .build()?; spec.save(dir.path().join("config.json"))?; + run_module(dir)?; + + Ok(()) + } + + fn run_module(dir: tempfile::TempDir) -> Result<(), Error> { let mut cfg = InstanceConfig::new(Engine::default(), "test_namespace".into()); let cfg = cfg .set_bundle(dir.path().to_str().unwrap().to_string()) .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) => res, Err(e) => { @@ -367,14 +390,22 @@ mod wasitest { } }; assert_eq!(res.0, 0); - let output = read_to_string(dir.path().join("stdout"))?; assert_eq!(output, "hello world\n"); - wasi.delete()?; - Ok(()) } + + fn create_module_dir(module: Vec) -> Result { + let dir = tempdir()?; + println!("{}", dir.path().to_str().unwrap()); + create_dir(dir.path().join("rootfs"))?; + let mut f = File::create(dir.path().join("rootfs/hello.wat"))?; + f.write_all(&module)?; + let stdout = File::create(dir.path().join("stdout"))?; + drop(stdout); + Ok(dir) + } } impl EngineGetter for Wasi { diff --git a/crates/wasi-demo-app/src/main.rs b/crates/wasi-demo-app/src/main.rs index f2b4743f2..6520ed4f5 100644 --- a/crates/wasi-demo-app/src/main.rs +++ b/crates/wasi-demo-app/src/main.rs @@ -3,24 +3,28 @@ use std::{env, fs::File, io::prelude::*, process, thread::sleep, time::Duration} fn main() { let args: Vec<_> = env::args().collect(); let mut cmd = "daemon"; - if args.len() >= 2 { - cmd = &args[1]; + if !args.is_empty() { + // temporary work around for wasmedge bug + // https://github.com/WasmEdge/wasmedge-rust-sdk/issues/10 + if !(args.len() == 1 && args[0].is_empty()) { + cmd = &args[0]; + } } match cmd { - "echo" => println!("{}", &args[2..].join(" ")), - "sleep" => sleep(Duration::from_secs_f64(args[2].parse::().unwrap())), - "exit" => process::exit(args[2].parse::().unwrap()), + "echo" => println!("{}", &args[1..].join(" ")), + "sleep" => sleep(Duration::from_secs_f64(args[1].parse::().unwrap())), + "exit" => process::exit(args[1].parse::().unwrap()), "write" => { - let mut file = File::create(&args[2]).unwrap(); - file.write_all(args[3..].join(" ").as_bytes()).unwrap(); + let mut file = File::create(&args[1]).unwrap(); + file.write_all(args[2..].join(" ").as_bytes()).unwrap(); } "daemon" => loop { println!("This is a song that never ends.\nYes, it goes on and on my friends.\nSome people started singing it not knowing what it was,\nSo they'll continue singing it forever just because...\n"); sleep(Duration::from_secs(1)); }, _ => { - eprintln!("unknown command: {0}", args[1]); + eprintln!("unknown command: {0}", args[0]); process::exit(1); } }