diff --git a/.cargo/config.toml b/.cargo/config.toml index ac4d9de29..48a01fb64 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,3 +3,6 @@ rustflags = [ "-Adead_code", "-Awarnings", ] + +[target.'cfg(unix)'] +runner = 'scripts/test-runner.sh' diff --git a/Cargo.lock b/Cargo.lock index 43b12627b..be8700558 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,9 +457,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.2" +version = "4.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" dependencies = [ "clap_builder", "clap_derive", @@ -602,6 +602,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "containerd-shim-wasm-test" +version = "0.1.1" +dependencies = [ + "anyhow", + "containerd-shim-wasm", + "env_logger", + "lazy_static", + "libc", + "log", + "oci-spec", + "serde_json", + "tempfile", + "wat", +] + [[package]] name = "containerd-shim-wasmedge" version = "0.1.1" @@ -609,13 +625,12 @@ dependencies = [ "anyhow", "containerd-shim", "containerd-shim-wasm", - "env_logger", + "containerd-shim-wasm-test", "libc", "libcontainer", "log", "oci-spec", "serial_test", - "tempfile", "ttrpc", "wasmedge-sdk", "wasmedge-sys", @@ -628,12 +643,11 @@ dependencies = [ "anyhow", "containerd-shim", "containerd-shim-wasm", - "env_logger", + "containerd-shim-wasm-test", "libcontainer", "log", "oci-spec", "serial_test", - "tempfile", "tokio", "ttrpc", "wasmer", @@ -648,12 +662,11 @@ dependencies = [ "anyhow", "containerd-shim", "containerd-shim-wasm", - "env_logger", + "containerd-shim-wasm-test", "libcontainer", "log", "oci-spec", "serial_test", - "tempfile", "ttrpc", "wasi-common", "wasmtime", @@ -3315,9 +3328,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.4" +version = "0.101.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" +checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" dependencies = [ "ring", "untrusted", diff --git a/Cargo.toml b/Cargo.toml index 554975247..32d18b375 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/containerd-shim-wasm", + "crates/containerd-shim-wasm-test", "crates/wasi-demo-app", "crates/oci-tar-builder", "crates/containerd-shim-wasmedge", @@ -20,6 +21,7 @@ homepage = "https://github.com/containerd/runwasi" [workspace.dependencies] anyhow = "1.0" containerd-shim-wasm = { path = "crates/containerd-shim-wasm" } +containerd-shim-wasm-test = { path = "crates/containerd-shim-wasm-test" } serde = "1.0" serde_json = "1.0" env_logger = "0.10" diff --git a/Makefile b/Makefile index a93f69d76..605b08a2a 100644 --- a/Makefile +++ b/Makefile @@ -42,8 +42,8 @@ check: check-wasm $(RUNTIMES:%=check-%); check-common: check-wasm; check-wasm: - cargo +nightly fmt -p oci-tar-builder -p wasi-demo-app -p containerd-shim-wasm -- --check - cargo clippy $(FEATURES_wasm) -p oci-tar-builder -p wasi-demo-app -p containerd-shim-wasm -- $(WARNINGS) + cargo +nightly fmt -p oci-tar-builder -p wasi-demo-app -p containerd-shim-wasm -p containerd-shim-wasm-test -- --check + cargo clippy $(FEATURES_wasm) -p oci-tar-builder -p wasi-demo-app -p containerd-shim-wasm -p containerd-shim-wasm-test -- $(WARNINGS) check-%: cargo +nightly fmt -p containerd-shim-$* -- --check @@ -54,8 +54,8 @@ fix: fix-wasm $(RUNTIMES:%=fix-%); fix-common: fix-wasm; fix-wasm: - cargo +nightly fmt -p oci-tar-builder -p wasi-demo-app -p containerd-shim-wasm - cargo clippy $(FEATURES_wasm) --fix -p oci-tar-builder -p wasi-demo-app -p containerd-shim-wasm -- $(WARNINGS) + cargo +nightly fmt -p oci-tar-builder -p wasi-demo-app -p containerd-shim-wasm -p containerd-shim-wasm-test + cargo clippy $(FEATURES_wasm) --fix -p oci-tar-builder -p wasi-demo-app -p containerd-shim-wasm -p containerd-shim-wasm-test -- $(WARNINGS) fix-%: cargo +nightly fmt -p containerd-shim-$* diff --git a/crates/containerd-shim-wasm-test/Cargo.toml b/crates/containerd-shim-wasm-test/Cargo.toml new file mode 100644 index 000000000..dec58651e --- /dev/null +++ b/crates/containerd-shim-wasm-test/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "containerd-shim-wasm-test" +description = "Library for testing containerd shims for wasm" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow = { workspace = true } +containerd-shim-wasm = { workspace = true } +env_logger = "0.10" +libc = { workspace = true } +log = { workspace = true } +oci-spec = { workspace = true } +serde_json = { workspace = true } +tempfile = "3.8" + +[build-dependencies] +anyhow = { workspace = true } +lazy_static = { version = "1.4.0" } +wat = { version = "1.0.46" } diff --git a/crates/containerd-shim-wasm-test/build.rs b/crates/containerd-shim-wasm-test/build.rs new file mode 100644 index 000000000..6c7170e16 --- /dev/null +++ b/crates/containerd-shim-wasm-test/build.rs @@ -0,0 +1,108 @@ +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use lazy_static::lazy_static; + +fn env_path(key: impl AsRef) -> Result { + std::env::var_os(key.as_ref()) + .map(Into::into) + .with_context(|| format!("failed to read env-var {}", key.as_ref())) +} + +lazy_static! { + static ref OUT_DIR: PathBuf = env_path("OUT_DIR").unwrap(); + static ref PKG_DIR: PathBuf = env_path("CARGO_MANIFEST_DIR").unwrap(); +} + +fn main() -> Result<()> { + let modules_file = OUT_DIR.join("modules.rs"); + let modules_dir = PKG_DIR.join("src").join("modules"); + + let mut writer = std::fs::File::create(modules_file)?; + + let paths = std::fs::read_dir(modules_dir)?; + for entry in paths.flatten() { + let src = entry.path(); + let name = path_to_ident(&src)?.to_ascii_uppercase(); + + println!("cargo:rerun-if-changed={}", src.to_string_lossy()); + + let dst = match src + .extension() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + { + "rs" => compile_rust(&src)?, + "wat" => compile_wat(&src)?, + _ => bail!("unrecognized file format for source file {src:?}"), + }; + + writeln!(writer, "pub const {name}: TestModule = TestModule {{")?; + writeln!(writer, " source: include_str!({src:?}),")?; + writeln!(writer, " bytes: include_bytes!({dst:?}),")?; + writeln!(writer, "}};")?; + } + + Ok(()) +} + +fn compile_rust(src: impl AsRef) -> Result { + let rustc = std::env::var_os("RUSTC").context("reading RUSTC")?; + let src = src.as_ref(); + let dst = output_for(src)?; + + Command::new(rustc) + .arg("--target=wasm32-wasi") + .arg("-Copt-level=z") + .arg("-Cstrip=symbols") + .arg("-o") + .arg(&dst) + .arg(src) + .spawn()? + .wait()? + .success() + .then_some(dst) + .context("running rustc") +} + +fn compile_wat(src: impl AsRef) -> Result { + let src = src.as_ref(); + let dst = output_for(src)?; + + let bytes = wat::parse_file(src)?; + std::fs::write(&dst, bytes)?; + + Ok(dst) +} + +fn output_for(src: impl AsRef) -> Result { + let src = src.as_ref(); + let filename = src + .file_name() + .with_context(|| format!("getting filename of {src:?}"))?; + Ok(OUT_DIR.join(filename).with_extension("wasm")) +} + +fn path_to_ident(path: impl AsRef) -> Result { + let path = path.as_ref(); + let ident: String = path + .file_stem() + .with_context(|| format!("getting filename of {path:?}"))? + .to_str() + .context("converting filename to string")? + .chars() + .map(|c| match c { + 'A'..='Z' | 'a'..='z' | '0'..='9' => c, + _ => '_', + }) + .collect(); + + if !ident.starts_with(char::is_alphabetic) { + bail!("please start the filename with [a-zA-Z]") + } + + Ok(ident) +} diff --git a/crates/containerd-shim-wasm-test/src/lib.rs b/crates/containerd-shim-wasm-test/src/lib.rs new file mode 100644 index 000000000..53b693a95 --- /dev/null +++ b/crates/containerd-shim-wasm-test/src/lib.rs @@ -0,0 +1,195 @@ +//! Testing utilities used across different modules + +use std::collections::HashMap; +use std::fs::{create_dir, read_to_string, write, File}; +use std::marker::PhantomData; +use std::ops::Add; +use std::sync::mpsc::channel; +use std::time::Duration; + +use anyhow::{bail, Result}; +#[cfg(unix)] +use libc::SIGKILL; + +#[cfg(windows)] +const SIGKILL: i32 = 9; + +use containerd_shim_wasm::sandbox::instance::Wait; +use containerd_shim_wasm::sandbox::{Instance, InstanceConfig}; +use oci_spec::runtime::{ProcessBuilder, RootBuilder, SpecBuilder}; + +pub mod modules; + +pub struct WasiTestBuilder +where + WasiInstance::Engine: Default + Send + Sync + Clone, +{ + tempdir: tempfile::TempDir, + _phantom: PhantomData, +} + +pub struct WasiTest +where + WasiInstance::Engine: Default + Send + Sync + Clone, +{ + instance: WasiInstance, + tempdir: tempfile::TempDir, +} + +impl WasiTestBuilder +where + WasiInstance::Engine: Default + Send + Sync + Clone, +{ + pub fn new() -> Result { + // 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(); + + log::info!("creating new wasi test"); + + let tempdir = tempfile::tempdir()?; + let dir = tempdir.path(); + + create_dir(dir.join("rootfs"))?; + let rootdir = dir.join("runwasi"); + create_dir(&rootdir)?; + let opts = HashMap::from([("root", rootdir)]); + let opts_file = File::create(dir.join("options.json"))?; + serde_json::to_writer(opts_file, &opts)?; + + write(dir.join("stdout"), "")?; + write(dir.join("stderr"), "")?; + + let builder = Self { + tempdir, + _phantom: Default::default(), + } + .with_wasm([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00])? + .with_start_fn("")? + .with_stdin("")?; + + Ok(builder) + } + + pub fn with_start_fn(self, start_fn: impl AsRef) -> Result { + let dir = self.tempdir.path(); + let start_fn = start_fn.as_ref(); + + log::info!("setting wasi test start_fn to {start_fn:?}"); + + let entrypoint = match start_fn { + "" => "/hello.wasm".to_string(), + s => "/hello.wasm#".to_string().add(s), + }; + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args([entrypoint]) + .build()?, + ) + .build()?; + + spec.save(dir.join("config.json"))?; + + Ok(self) + } + + pub fn with_wasm(self, wasmbytes: impl AsRef<[u8]>) -> Result { + let dir = self.tempdir.path(); + + log::info!( + "setting wasi test wasm file [u8; {}]", + wasmbytes.as_ref().len() + ); + + let wasm_path = dir.join("rootfs").join("hello.wasm"); + write(wasm_path, wasmbytes)?; + + Ok(self) + } + + pub fn with_stdin(self, stdin: impl AsRef<[u8]>) -> Result { + let dir = self.tempdir.path(); + + log::info!("setting wasi test stdin to [u8; {}]", stdin.as_ref().len()); + + write(dir.join("stdin"), stdin)?; + + Ok(self) + } + + pub fn build(self) -> Result> { + let tempdir = self.tempdir; + let dir = tempdir.path(); + + log::info!("building wasi test"); + + let mut cfg = InstanceConfig::new( + WasiInstance::Engine::default(), + "test_namespace".into(), + "/containerd/address".into(), + ); + cfg.set_bundle(dir.to_string_lossy().to_string()) + .set_stdout(dir.join("stdout").to_string_lossy().to_string()) + .set_stderr(dir.join("stderr").to_string_lossy().to_string()) + .set_stdin(dir.join("stdin").to_string_lossy().to_string()); + + let instance = WasiInstance::new("test".to_string(), Some(&cfg)); + Ok(WasiTest { instance, tempdir }) + } +} + +impl WasiTest +where + WasiInstance::Engine: Default + Send + Sync + Clone, +{ + pub fn builder() -> Result> { + WasiTestBuilder::new() + } + + pub fn instance(&self) -> &WasiInstance { + &self.instance + } + + pub fn start(&self) -> Result<&Self> { + log::info!("starting wasi test"); + self.instance.start()?; + Ok(self) + } + + pub fn delete(&self) -> Result<&Self> { + log::info!("deleting wasi test"); + self.instance.delete()?; + Ok(self) + } + + pub fn wait(&self, timeout: Duration) -> Result<(u32, String, String)> { + let dir = self.tempdir.path(); + + log::info!("waiting wasi test"); + + let (tx, rx) = channel(); + let waiter = Wait::new(tx); + self.instance.wait(&waiter).unwrap(); + + let (status, _) = match rx.recv_timeout(timeout) { + Ok(res) => res, + Err(e) => { + self.instance.kill(SIGKILL as u32)?; + bail!("error waiting for module to finish: {e}"); + } + }; + + let stdout = read_to_string(dir.join("stdout"))?; + let stderr = read_to_string(dir.join("stderr"))?; + + self.instance.delete()?; + + log::info!("wasi test status is {status}"); + + Ok((status, stdout, stderr)) + } +} diff --git a/crates/containerd-shim-wasm-test/src/modules.rs b/crates/containerd-shim-wasm-test/src/modules.rs new file mode 100644 index 000000000..cff350478 --- /dev/null +++ b/crates/containerd-shim-wasm-test/src/modules.rs @@ -0,0 +1,12 @@ +pub struct TestModule { + pub source: &'static str, + pub bytes: &'static [u8], +} + +impl AsRef<[u8]> for TestModule { + fn as_ref(&self) -> &[u8] { + self.bytes + } +} + +include!(concat!(env!("OUT_DIR"), "/modules.rs")); diff --git a/crates/containerd-shim-wasm-test/src/modules/custom_entrypoint.wat b/crates/containerd-shim-wasm-test/src/modules/custom_entrypoint.wat new file mode 100644 index 000000000..35d3194ef --- /dev/null +++ b/crates/containerd-shim-wasm-test/src/modules/custom_entrypoint.wat @@ -0,0 +1,27 @@ +(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_snapshot_preview1" "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 "foo") + ;; 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 + ) +) diff --git a/crates/containerd-shim-wasm-test/src/modules/exit_code.wat b/crates/containerd-shim-wasm-test/src/modules/exit_code.wat new file mode 100644 index 000000000..8b82f3c08 --- /dev/null +++ b/crates/containerd-shim-wasm-test/src/modules/exit_code.wat @@ -0,0 +1,12 @@ +(module + ;; Import the required proc_exit WASI function which terminates the program with an exit code. + ;; The function signature for proc_exit is: + ;; (exit_code: i32) -> ! + (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32))) + (memory 1) + (export "memory" (memory 0)) + (func $main (export "_start") + (call $proc_exit (i32.const 42)) + unreachable + ) +) diff --git a/crates/containerd-shim-wasm-test/src/modules/has_default_devices.rs b/crates/containerd-shim-wasm-test/src/modules/has_default_devices.rs new file mode 100644 index 000000000..d7be5478a --- /dev/null +++ b/crates/containerd-shim-wasm-test/src/modules/has_default_devices.rs @@ -0,0 +1,21 @@ +use std::path::Path; + +fn main() { + // Runtime must supply at least the following files regardless of OCI devices setting: + let devices = [ + "/dev/null", + "/dev/zero", + "/dev/full", + "/dev/random", + "/dev/urandom", + "/dev/tty", + ]; + + for device in devices.iter() { + if Path::new(device).exists() { + println!("{device} found"); + } else { + panic!("{device} not found"); + } + } +} diff --git a/crates/containerd-shim-wasm-test/src/modules/hello_world.wat b/crates/containerd-shim-wasm-test/src/modules/hello_world.wat new file mode 100644 index 000000000..ee1e35bdf --- /dev/null +++ b/crates/containerd-shim-wasm-test/src/modules/hello_world.wat @@ -0,0 +1,27 @@ +(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_snapshot_preview1" "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 + ) + drop ;; Discard the number of bytes written from the top of the stack + ) +) diff --git a/crates/containerd-shim-wasm-test/src/modules/seccomp.rs b/crates/containerd-shim-wasm-test/src/modules/seccomp.rs new file mode 100644 index 000000000..962fe8373 --- /dev/null +++ b/crates/containerd-shim-wasm-test/src/modules/seccomp.rs @@ -0,0 +1,10 @@ +fn main() { + // Add current working dir request so that we have some known system call to + // test seccomp with. + let cwd = std::env::current_dir().unwrap(); + + println!( + "current working dir: {}", + cwd.to_string_lossy() + ); +} diff --git a/crates/containerd-shim-wasm-test/src/modules/unreachable.wat b/crates/containerd-shim-wasm-test/src/modules/unreachable.wat new file mode 100644 index 000000000..c37be6659 --- /dev/null +++ b/crates/containerd-shim-wasm-test/src/modules/unreachable.wat @@ -0,0 +1,3 @@ +(func $main (export "_start") + (unreachable) +) diff --git a/crates/containerd-shim-wasm/src/sandbox/mod.rs b/crates/containerd-shim-wasm/src/sandbox/mod.rs index 5b2e56862..33d00518d 100644 --- a/crates/containerd-shim-wasm/src/sandbox/mod.rs +++ b/crates/containerd-shim-wasm/src/sandbox/mod.rs @@ -16,5 +16,3 @@ pub use shim::{Cli as ShimCli, Local}; pub use stdio::Stdio; pub(crate) mod oci; - -pub mod testutil; diff --git a/crates/containerd-shim-wasm/src/sandbox/testutil.rs b/crates/containerd-shim-wasm/src/sandbox/testutil.rs deleted file mode 100644 index 1f05b22d3..000000000 --- a/crates/containerd-shim-wasm/src/sandbox/testutil.rs +++ /dev/null @@ -1,175 +0,0 @@ -//! Testing utilities used across different modules - -use std::collections::HashMap; -use std::fs::{create_dir, File}; -use std::path::Path; -use std::process::{Command, Stdio}; -use std::sync::mpsc::channel; -use std::time::Duration; - -use anyhow::{bail, ensure, Result}; - -use crate::sys::signals::SIGKILL; - -fn normalize_test_name(test: &str) -> Result<&str> { - let closure_removed = test.trim_end_matches("::{{closure}}"); - - // More tests and validation here if needed. - - Ok(closure_removed) -} - -/// Re-execs the current process with sudo and runs the given test. -/// Unless this is run in a CI environment, this may prompt the user for a password. -/// This is significantly faster than expecting the user to run the tests with sudo due to build and crate caching. -pub fn run_test_with_sudo(test: &str) -> Result<()> { - // This uses piped stdout/stderr. - // This makes it so cargo doesn't mess up the caller's TTY. - // This also explicitly sets LD_LIBRARY_PATH, which sudo usually removes. - // This might be needed when dynamically linking libwasmedge. - - let normalized_test = normalize_test_name(test)?; - let ld_library_path = std::env::var_os("LD_LIBRARY_PATH") - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - let mut cmd = Command::new("sudo") - .arg("-E") - .arg("env") - .arg(format!("LD_LIBRARY_PATH={ld_library_path}")) - .arg(std::env::current_exe().unwrap()) - .arg("--") - .arg(normalized_test) - .arg("--exact") - .stdin(Stdio::inherit()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - - let mut stdout = cmd.stdout.take().unwrap(); - let mut stderr = cmd.stderr.take().unwrap(); - - std::thread::spawn(move || { - std::io::copy(&mut stdout, &mut std::io::stdout()).unwrap(); - }); - std::thread::spawn(move || { - std::io::copy(&mut stderr, &mut std::io::stderr()).unwrap(); - }); - - ensure!(cmd.wait()?.success(), "running test with sudo failed"); - Ok(()) -} - -#[macro_export] -macro_rules! function { - () => {{ - fn f() {} - fn type_name_of(_: T) -> &'static str { - std::any::type_name::() - } - let name = type_name_of(f); - let name = &name[..name.len() - 3][env!("CARGO_PKG_NAME").len() + 2..]; - name - }}; -} - -#[cfg(unix)] -use caps::{CapSet, Capability}; -use chrono::{DateTime, Utc}; -pub use function; -use oci_spec::runtime::{ProcessBuilder, RootBuilder, SpecBuilder}; - -use super::instance::Wait; -use super::{Instance, InstanceConfig}; - -/// Determines if the current process has the CAP_SYS_ADMIN capability in its effective set. -pub fn has_cap_sys_admin() -> bool { - #[cfg(unix)] - { - let caps = caps::read(None, CapSet::Effective).unwrap(); - caps.contains(&Capability::CAP_SYS_ADMIN) - } - - #[cfg(windows)] - { - false - } -} - -pub fn run_wasi_test( - dir: impl AsRef, - wasmbytes: impl AsRef<[u8]>, - start_fn: Option<&str>, -) -> Result<(u32, DateTime)> -where - WasmInstance::Engine: Default, -{ - create_dir(dir.as_ref().join("rootfs"))?; - let rootdir = dir.as_ref().join("runwasi"); - create_dir(&rootdir)?; - let opts = HashMap::from([("root", rootdir)]); - let opts_file = std::fs::File::create(dir.as_ref().join("options.json"))?; - serde_json::to_writer(opts_file, &opts)?; - - let filename = if wasmbytes.as_ref().starts_with(b"\0asm") { - "hello.wasm" - } else { - "hello.wat" - }; - - let wasm_path = dir.as_ref().join("rootfs").join(filename); - std::fs::write(&wasm_path, wasmbytes)?; - - #[cfg(unix)] - std::fs::set_permissions( - &wasm_path, - std::os::unix::prelude::PermissionsExt::from_mode(0o755), - )?; - - let stdout = File::create(dir.as_ref().join("stdout"))?; - drop(stdout); - - let entrypoint = match start_fn { - Some(s) => format!("./{filename}#{s}"), - None => format!("./{filename}"), - }; - let spec = SpecBuilder::default() - .root(RootBuilder::default().path("rootfs").build()?) - .process( - ProcessBuilder::default() - .cwd("/") - .args(vec![entrypoint]) - .build()?, - ) - .build()?; - - spec.save(dir.as_ref().join("config.json"))?; - - let mut cfg = InstanceConfig::new( - WasmInstance::Engine::default(), - "test_namespace".into(), - "/containerd/address".into(), - ); - let cfg = cfg - .set_bundle(dir.as_ref().to_str().unwrap().to_string()) - .set_stdout(dir.as_ref().join("stdout").to_str().unwrap().to_string()); - - let wasi = WasmInstance::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(); - bail!("error waiting for module to finish: {e}"); - } - }; - wasi.delete()?; - res -} diff --git a/crates/containerd-shim-wasmedge/Cargo.toml b/crates/containerd-shim-wasmedge/Cargo.toml index f297353e0..84b52be5b 100644 --- a/crates/containerd-shim-wasmedge/Cargo.toml +++ b/crates/containerd-shim-wasmedge/Cargo.toml @@ -18,10 +18,9 @@ wasmedge-sys = "*" libcontainer = { workspace = true } [dev-dependencies] -env_logger = { workspace = true } +containerd-shim-wasm-test = { workspace = true } libc = { workspace = true } serial_test = "*" -tempfile = "3.8" [features] default = ["standalone", "static"] diff --git a/crates/containerd-shim-wasmedge/src/lib.rs b/crates/containerd-shim-wasmedge/src/lib.rs index 1cbc33585..644c43860 100644 --- a/crates/containerd-shim-wasmedge/src/lib.rs +++ b/crates/containerd-shim-wasmedge/src/lib.rs @@ -23,4 +23,5 @@ pub fn parse_version() { #[cfg(unix)] #[cfg(test)] -mod tests; +#[path = "tests.rs"] +mod wasmedge_tests; diff --git a/crates/containerd-shim-wasmedge/src/tests.rs b/crates/containerd-shim-wasmedge/src/tests.rs index a9add28fc..a290e95ae 100644 --- a/crates/containerd-shim-wasmedge/src/tests.rs +++ b/crates/containerd-shim-wasmedge/src/tests.rs @@ -1,186 +1,103 @@ -use std::fs::read_to_string; - -use anyhow::Result; -use containerd_shim_wasm::function; -use containerd_shim_wasm::sandbox::testutil::{ - has_cap_sys_admin, run_test_with_sudo, run_wasi_test, -}; -use containerd_shim_wasm::sandbox::{Instance as SandboxInstance, InstanceConfig, Stdio}; -use serial_test::serial; -use tempfile::tempdir; - -use crate::WasmEdgeInstance as Instance; - -// This is taken from https://github.com/bytecodealliance/wasmtime/blob/6a60e8363f50b936e4c4fc958cb9742314ff09f3/docs/WASI-tutorial.md?plain=1#L270-L298 -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_snapshot_preview1" "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 - ) - ) - "#).as_bytes().to_vec() -} +use std::time::Duration; -fn module_with_exit_code(exit_code: u32) -> Vec { - format!(r#"(module - ;; Import the required proc_exit WASI function which terminates the program with an exit code. - ;; The function signature for proc_exit is: - ;; (exit_code: i32) -> ! - (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32))) - (memory 1) - (export "memory" (memory 0)) - (func $main (export "_start") - (call $proc_exit (i32.const {exit_code})) - unreachable - ) - ) - "#).as_bytes().to_vec() -} +//use containerd_shim_wasm::sandbox::Instance; +use containerd_shim_wasm_test::modules::*; +use containerd_shim_wasm_test::WasiTest; +use serial_test::serial; -const WASI_RETURN_ERROR: &[u8] = r#"(module - (func $main (export "_start") - (unreachable) - ) -) -"# -.as_bytes(); +use crate::instance::WasmEdgeInstance as WasiInstance; #[test] #[serial] -fn test_delete_after_create() -> Result<()> { - // start logging - let _ = env_logger::try_init(); - let _guard = Stdio::init_from_std().guard(); - - let cfg = InstanceConfig::new( - Default::default(), - "test_namespace".into(), - "/containerd/address".into(), - ); - - let i = Instance::new("".to_string(), Some(&cfg)); - i.delete()?; - +fn test_delete_after_create() -> anyhow::Result<()> { + WasiTest::::builder()?.build()?.delete()?; Ok(()) } #[test] #[serial] -fn test_wasi_entrypoint() -> Result<()> { - 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 _guard = Stdio::init_from_std().guard(); - - let dir = tempdir()?; - let path = dir.path(); - let wasm_bytes = hello_world_module(None); - - let res = run_wasi_test::(&dir, wasm_bytes, None)?; +fn test_hello_world() -> anyhow::Result<()> { + let (exit_code, stdout, _) = WasiTest::::builder()? + .with_wasm(HELLO_WORLD)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - assert_eq!(res.0, 0); - - let output = read_to_string(path.join("stdout"))?; - assert_eq!(output, "hello world\n"); + assert_eq!(exit_code, 0); + assert_eq!(stdout, "hello world\n"); Ok(()) } #[test] #[serial] -fn test_wasi_custom_entrypoint() -> Result<()> { - 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 _guard = Stdio::init_from_std().guard(); +fn test_custom_entrypoint() -> anyhow::Result<()> { + let (exit_code, stdout, _) = WasiTest::::builder()? + .with_start_fn("foo")? + .with_wasm(CUSTOM_ENTRYPOINT)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - let dir = tempdir()?; - let path = dir.path(); - let wasm_bytes = hello_world_module(Some("foo")); + assert_eq!(exit_code, 0); + assert_eq!(stdout, "hello world\n"); - let res = run_wasi_test::(&dir, wasm_bytes, Some("foo"))?; + Ok(()) +} - assert_eq!(res.0, 0); +#[test] +#[serial] +fn test_unreachable() -> anyhow::Result<()> { + let (exit_code, _, _) = WasiTest::::builder()? + .with_wasm(UNREACHABLE)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - let output = read_to_string(path.join("stdout"))?; - assert_eq!(output, "hello world\n"); + assert_ne!(exit_code, 0); Ok(()) } #[test] #[serial] -fn test_wasi_error() -> Result<()> { - 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 _guard = Stdio::init_from_std().guard(); - - let dir = tempdir()?; - let res = run_wasi_test::(&dir, WASI_RETURN_ERROR, None)?; +fn test_exit_code() -> anyhow::Result<()> { + let (exit_code, _, _) = WasiTest::::builder()? + .with_wasm(EXIT_CODE)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - // Expect error code from the run. - assert_eq!(res.0, 137); + assert_eq!(exit_code, 42); Ok(()) } #[test] #[serial] -fn test_wasi_exit_code() -> Result<()> { - if !has_cap_sys_admin() { - println!("running test with sudo: {}", function!()); - return run_test_with_sudo(function!()); - } +fn test_seccomp() -> anyhow::Result<()> { + let (exit_code, stdout, _) = WasiTest::::builder()? + .with_wasm(SECCOMP)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - // start logging - let _ = env_logger::try_init(); - let _guard = Stdio::init_from_std().guard(); + assert_eq!(exit_code, 0); + assert_eq!(stdout.trim(), "current working dir: /"); - let expected_exit_code: u32 = 42; + Ok(()) +} - let dir = tempdir()?; - let wasm_bytes = module_with_exit_code(expected_exit_code); - let (actual_exit_code, _) = run_wasi_test::(&dir, wasm_bytes, None)?; +#[test] +#[serial] +fn test_has_default_devices() -> anyhow::Result<()> { + let (exit_code, _, _) = WasiTest::::builder()? + .with_wasm(HAS_DEFAULT_DEVICES)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - assert_eq!(actual_exit_code, expected_exit_code); + assert_eq!(exit_code, 0); Ok(()) } @@ -190,6 +107,7 @@ fn test_wasi_exit_code() -> Result<()> { // If wasmedge is statically linked, this will be the path to the current executable. fn get_wasmedge_binary_path() -> Option { use std::os::unix::prelude::OsStrExt; + let f = wasmedge_sys::ffi::WasmEdge_VersionGet; let mut info = unsafe { std::mem::zeroed() }; if unsafe { libc::dladdr(f as *const libc::c_void, &mut info) } == 0 { diff --git a/crates/containerd-shim-wasmer/Cargo.toml b/crates/containerd-shim-wasmer/Cargo.toml index e8b6c72d7..6e14281ed 100644 --- a/crates/containerd-shim-wasmer/Cargo.toml +++ b/crates/containerd-shim-wasmer/Cargo.toml @@ -20,9 +20,8 @@ wasmer-wasix = { version = "0.12.0" } libcontainer = { workspace = true } [dev-dependencies] -env_logger = { workspace = true } +containerd-shim-wasm-test = { workspace = true } serial_test = "*" -tempfile = "3.8" [[bin]] name = "containerd-shim-wasmer-v1" diff --git a/crates/containerd-shim-wasmer/src/instance_linux.rs b/crates/containerd-shim-wasmer/src/instance_linux.rs index 6161c46df..106387440 100644 --- a/crates/containerd-shim-wasmer/src/instance_linux.rs +++ b/crates/containerd-shim-wasmer/src/instance_linux.rs @@ -3,6 +3,7 @@ use containerd_shim_wasm::container::{ Engine, Instance, PathResolve, RuntimeContext, Stdio, WasiEntrypoint, }; use wasmer::{Module, Store}; +use wasmer_wasix::virtual_fs::host_fs::FileSystem; use wasmer_wasix::{WasiEnv, WasiError}; pub type WasmerInstance = Instance; @@ -45,10 +46,11 @@ impl Engine for WasmerEngine { .build()?; let _guard = runtime.enter(); - log::info!("Creating `WasiEnv`...: args {:?}, envs: {:?}", args, envs); + log::info!("Creating `WasiEnv`...: args {args:?}, envs: {envs:?}"); let (instance, wasi_env) = WasiEnv::builder(mod_name) .args(&args[1..]) .envs(envs) + .fs(Box::::default()) .preopen_dir("/")? .instantiate(module, &mut store)?; diff --git a/crates/containerd-shim-wasmer/src/lib.rs b/crates/containerd-shim-wasmer/src/lib.rs index 5d14998f9..e641fb9ed 100644 --- a/crates/containerd-shim-wasmer/src/lib.rs +++ b/crates/containerd-shim-wasmer/src/lib.rs @@ -23,4 +23,5 @@ pub fn parse_version() { #[cfg(unix)] #[cfg(test)] -mod tests; +#[path = "tests.rs"] +mod wasmer_tests; diff --git a/crates/containerd-shim-wasmer/src/tests.rs b/crates/containerd-shim-wasmer/src/tests.rs index 23e7c4aef..c3ce8d2e4 100644 --- a/crates/containerd-shim-wasmer/src/tests.rs +++ b/crates/containerd-shim-wasmer/src/tests.rs @@ -1,186 +1,103 @@ -use std::fs::read_to_string; - -use anyhow::Result; -use containerd_shim_wasm::function; -use containerd_shim_wasm::sandbox::testutil::{ - has_cap_sys_admin, run_test_with_sudo, run_wasi_test, -}; -use containerd_shim_wasm::sandbox::{Instance as SandboxInstance, InstanceConfig, Stdio}; -use serial_test::serial; -use tempfile::tempdir; - -use crate::WasmerInstance as Instance; - -// This is taken from https://github.com/bytecodealliance/wasmtime/blob/6a60e8363f50b936e4c4fc958cb9742314ff09f3/docs/WASI-tutorial.md?plain=1#L270-L298 -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_snapshot_preview1" "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 - ) - ) - "#).as_bytes().to_vec() -} +use std::time::Duration; -fn module_with_exit_code(exit_code: u32) -> Vec { - format!(r#"(module - ;; Import the required proc_exit WASI function which terminates the program with an exit code. - ;; The function signature for proc_exit is: - ;; (exit_code: i32) -> ! - (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32))) - (memory 1) - (export "memory" (memory 0)) - (func $main (export "_start") - (call $proc_exit (i32.const {exit_code})) - unreachable - ) - ) - "#).as_bytes().to_vec() -} +//use containerd_shim_wasm::sandbox::Instance; +use containerd_shim_wasm_test::modules::*; +use containerd_shim_wasm_test::WasiTest; +use serial_test::serial; -const WASI_RETURN_ERROR: &[u8] = r#"(module - (func $main (export "_start") - (unreachable) - ) -) -"# -.as_bytes(); +use crate::instance::WasmerInstance as WasiInstance; #[test] #[serial] -fn test_delete_after_create() -> Result<()> { - // start logging - let _ = env_logger::try_init(); - let _guard = Stdio::init_from_std().guard(); - - let cfg = InstanceConfig::new( - Default::default(), - "test_namespace".into(), - "/containerd/address".into(), - ); - - let i = Instance::new("".to_string(), Some(&cfg)); - i.delete()?; - +fn test_delete_after_create() -> anyhow::Result<()> { + WasiTest::::builder()?.build()?.delete()?; Ok(()) } #[test] #[serial] -fn test_wasi_entrypoint() -> Result<()> { - 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 _guard = Stdio::init_from_std().guard(); - - let dir = tempdir()?; - let path = dir.path(); - let wasm_bytes = hello_world_module(None); +fn test_hello_world() -> anyhow::Result<()> { + let (exit_code, stdout, _) = WasiTest::::builder()? + .with_wasm(HELLO_WORLD)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - let res = run_wasi_test::(&dir, wasm_bytes, None)?; - - assert_eq!(res.0, 0); - - let output = read_to_string(path.join("stdout"))?; - assert_eq!(output, "hello world\n"); + assert_eq!(exit_code, 0); + assert_eq!(stdout, "hello world\n"); Ok(()) } #[test] #[serial] -fn test_wasi_custom_entrypoint() -> Result<()> { - 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 _guard = Stdio::init_from_std().guard(); +fn test_custom_entrypoint() -> anyhow::Result<()> { + let (exit_code, stdout, _) = WasiTest::::builder()? + .with_start_fn("foo")? + .with_wasm(CUSTOM_ENTRYPOINT)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - let dir = tempdir()?; - let path = dir.path(); - let wasm_bytes = hello_world_module(Some("foo")); + assert_eq!(exit_code, 0); + assert_eq!(stdout, "hello world\n"); - let res = run_wasi_test::(&dir, wasm_bytes, Some("foo"))?; + Ok(()) +} - assert_eq!(res.0, 0); +#[test] +#[serial] +fn test_unreachable() -> anyhow::Result<()> { + let (exit_code, _, _) = WasiTest::::builder()? + .with_wasm(UNREACHABLE)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - let output = read_to_string(path.join("stdout"))?; - assert_eq!(output, "hello world\n"); + assert_ne!(exit_code, 0); Ok(()) } #[test] #[serial] -fn test_wasi_error() -> Result<()> { - 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 _guard = Stdio::init_from_std().guard(); - - let dir = tempdir()?; - let res = run_wasi_test::(&dir, WASI_RETURN_ERROR, None)?; +fn test_exit_code() -> anyhow::Result<()> { + let (exit_code, _, _) = WasiTest::::builder()? + .with_wasm(EXIT_CODE)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - // Expect error code from the run. - assert_eq!(res.0, 137); + assert_eq!(exit_code, 42); Ok(()) } #[test] #[serial] -fn test_wasi_exit_code() -> Result<()> { - if !has_cap_sys_admin() { - println!("running test with sudo: {}", function!()); - return run_test_with_sudo(function!()); - } +fn test_seccomp() -> anyhow::Result<()> { + let (exit_code, stdout, _) = WasiTest::::builder()? + .with_wasm(SECCOMP)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - // start logging - let _ = env_logger::try_init(); - let _guard = Stdio::init_from_std().guard(); + assert_eq!(exit_code, 0); + assert_eq!(stdout.trim(), "current working dir: /"); - let expected_exit_code: u32 = 42; - - let dir = tempdir()?; - let wasm_bytes = module_with_exit_code(expected_exit_code); - let (actual_exit_code, _) = run_wasi_test::(&dir, wasm_bytes, None)?; + Ok(()) +} - assert_eq!(actual_exit_code, expected_exit_code); +#[test] +#[serial] +fn test_has_default_devices() -> anyhow::Result<()> { + let (exit_code, _, _) = WasiTest::::builder()? + .with_wasm(HAS_DEFAULT_DEVICES)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; + + assert_eq!(exit_code, 0); Ok(()) } diff --git a/crates/containerd-shim-wasmtime/Cargo.toml b/crates/containerd-shim-wasmtime/Cargo.toml index 955d840af..761802b26 100644 --- a/crates/containerd-shim-wasmtime/Cargo.toml +++ b/crates/containerd-shim-wasmtime/Cargo.toml @@ -32,9 +32,8 @@ wasi-common = "11.0" libcontainer = { workspace = true } [dev-dependencies] -env_logger = { workspace = true } +containerd-shim-wasm-test = { workspace = true } serial_test = "*" -tempfile = "3.8" [[bin]] name = "containerd-shim-wasmtime-v1" diff --git a/crates/containerd-shim-wasmtime/src/lib.rs b/crates/containerd-shim-wasmtime/src/lib.rs index 26d39840d..00a08d30a 100644 --- a/crates/containerd-shim-wasmtime/src/lib.rs +++ b/crates/containerd-shim-wasmtime/src/lib.rs @@ -23,4 +23,5 @@ pub fn parse_version() { #[cfg(unix)] #[cfg(test)] -mod tests; +#[path = "tests.rs"] +mod wasmtime_tests; diff --git a/crates/containerd-shim-wasmtime/src/tests.rs b/crates/containerd-shim-wasmtime/src/tests.rs index 7e4666ca2..27dbd70c8 100644 --- a/crates/containerd-shim-wasmtime/src/tests.rs +++ b/crates/containerd-shim-wasmtime/src/tests.rs @@ -1,186 +1,103 @@ -use std::fs::read_to_string; - -use anyhow::Result; -use containerd_shim_wasm::function; -use containerd_shim_wasm::sandbox::testutil::{ - has_cap_sys_admin, run_test_with_sudo, run_wasi_test, -}; -use containerd_shim_wasm::sandbox::{Instance as SandboxInstance, InstanceConfig, Stdio}; -use serial_test::serial; -use tempfile::tempdir; - -use crate::WasmtimeInstance as Instance; - -// This is taken from https://github.com/bytecodealliance/wasmtime/blob/6a60e8363f50b936e4c4fc958cb9742314ff09f3/docs/WASI-tutorial.md?plain=1#L270-L298 -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_snapshot_preview1" "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 - ) - ) - "#).as_bytes().to_vec() -} +use std::time::Duration; -fn module_with_exit_code(exit_code: u32) -> Vec { - format!(r#"(module - ;; Import the required proc_exit WASI function which terminates the program with an exit code. - ;; The function signature for proc_exit is: - ;; (exit_code: i32) -> ! - (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32))) - (memory 1) - (export "memory" (memory 0)) - (func $main (export "_start") - (call $proc_exit (i32.const {exit_code})) - unreachable - ) - ) - "#).as_bytes().to_vec() -} +//use containerd_shim_wasm::sandbox::Instance; +use containerd_shim_wasm_test::modules::*; +use containerd_shim_wasm_test::WasiTest; +use serial_test::serial; -const WASI_RETURN_ERROR: &[u8] = r#"(module - (func $main (export "_start") - (unreachable) - ) -) -"# -.as_bytes(); +use crate::instance::WasmtimeInstance as WasiInstance; #[test] #[serial] -fn test_delete_after_create() -> Result<()> { - // start logging - let _ = env_logger::try_init(); - let _guard = Stdio::init_from_std().guard(); - - let cfg = InstanceConfig::new( - Default::default(), - "test_namespace".into(), - "/containerd/address".into(), - ); - - let i = Instance::new("".to_string(), Some(&cfg)); - i.delete()?; - +fn test_delete_after_create() -> anyhow::Result<()> { + WasiTest::::builder()?.build()?.delete()?; Ok(()) } #[test] #[serial] -fn test_wasi_entrypoint() -> Result<()> { - 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 _guard = Stdio::init_from_std().guard(); - - let dir = tempdir()?; - let path = dir.path(); - let wasm_bytes = hello_world_module(None); +fn test_hello_world() -> anyhow::Result<()> { + let (exit_code, stdout, _) = WasiTest::::builder()? + .with_wasm(HELLO_WORLD)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - let res = run_wasi_test::(&dir, wasm_bytes, None)?; - - assert_eq!(res.0, 0); - - let output = read_to_string(path.join("stdout"))?; - assert_eq!(output, "hello world\n"); + assert_eq!(exit_code, 0); + assert_eq!(stdout, "hello world\n"); Ok(()) } #[test] #[serial] -fn test_wasi_custom_entrypoint() -> Result<()> { - 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 _guard = Stdio::init_from_std().guard(); +fn test_custom_entrypoint() -> anyhow::Result<()> { + let (exit_code, stdout, _) = WasiTest::::builder()? + .with_start_fn("foo")? + .with_wasm(CUSTOM_ENTRYPOINT)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - let dir = tempdir()?; - let path = dir.path(); - let wasm_bytes = hello_world_module(Some("foo")); + assert_eq!(exit_code, 0); + assert_eq!(stdout, "hello world\n"); - let res = run_wasi_test::(&dir, wasm_bytes, Some("foo"))?; + Ok(()) +} - assert_eq!(res.0, 0); +#[test] +#[serial] +fn test_unreachable() -> anyhow::Result<()> { + let (exit_code, _, _) = WasiTest::::builder()? + .with_wasm(UNREACHABLE)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - let output = read_to_string(path.join("stdout"))?; - assert_eq!(output, "hello world\n"); + assert_ne!(exit_code, 0); Ok(()) } #[test] #[serial] -fn test_wasi_error() -> Result<()> { - 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 _guard = Stdio::init_from_std().guard(); - - let dir = tempdir()?; - let res = run_wasi_test::(&dir, WASI_RETURN_ERROR, None)?; +fn test_exit_code() -> anyhow::Result<()> { + let (exit_code, _, _) = WasiTest::::builder()? + .with_wasm(EXIT_CODE)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - // Expect error code from the run. - assert_eq!(res.0, 137); + assert_eq!(exit_code, 42); Ok(()) } #[test] #[serial] -fn test_wasi_exit_code() -> Result<()> { - if !has_cap_sys_admin() { - println!("running test with sudo: {}", function!()); - return run_test_with_sudo(function!()); - } +fn test_seccomp() -> anyhow::Result<()> { + let (exit_code, stdout, _) = WasiTest::::builder()? + .with_wasm(SECCOMP)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; - // start logging - let _ = env_logger::try_init(); - let _guard = Stdio::init_from_std().guard(); + assert_eq!(exit_code, 0); + assert_eq!(stdout.trim(), "current working dir: /"); - let expected_exit_code: u32 = 42; - - let dir = tempdir()?; - let wasm_bytes = module_with_exit_code(expected_exit_code); - let (actual_exit_code, _) = run_wasi_test::(&dir, wasm_bytes, None)?; + Ok(()) +} - assert_eq!(actual_exit_code, expected_exit_code); +#[test] +#[serial] +fn test_has_default_devices() -> anyhow::Result<()> { + let (exit_code, _, _) = WasiTest::::builder()? + .with_wasm(HAS_DEFAULT_DEVICES)? + .build()? + .start()? + .wait(Duration::from_secs(10))?; + + assert_eq!(exit_code, 0); Ok(()) } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index db0dcef22..4d5b147f4 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -2,3 +2,4 @@ # Make sure to this in sync with RUST_VERSION in the Dockerfile channel="1.72.0" profile="default" +targets = ["wasm32-wasi"] \ No newline at end of file diff --git a/scripts/test-runner.sh b/scripts/test-runner.sh new file mode 100755 index 000000000..f2cc84512 --- /dev/null +++ b/scripts/test-runner.sh @@ -0,0 +1,2 @@ +#!/bin/sh +sudo -E env LD_LIBRARY_PATH=${LD_LIBRARY_PATH} "$@" \ No newline at end of file