From d987c2748e3be01d5491b7fffbc7493963f3d314 Mon Sep 17 00:00:00 2001 From: Jorge Prendes Date: Thu, 7 Sep 2023 13:39:01 +0100 Subject: [PATCH 1/2] fix restoring stdio in tests Signed-off-by: Jorge Prendes --- Cargo.lock | 4 +- .../containerd-shim-wasm/src/sandbox/stdio.rs | 68 ++++++++++++------ .../src/sys/unix/stdio.rs | 50 +++++++++++-- .../src/sys/windows/stdio.rs | 71 +++++++++++-------- crates/containerd-shim-wasmedge/Cargo.toml | 6 +- .../src/instance/instance_linux.rs | 40 +++++------ crates/containerd-shim-wasmtime/Cargo.toml | 3 - .../src/instance/instance_linux.rs | 32 ++------- 8 files changed, 162 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2fd1afa80..a0f59f22e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -622,10 +622,10 @@ dependencies = [ "containerd-shim", "containerd-shim-wasm", "dbus", + "env_logger", "libc", "libcontainer", "log", - "nix 0.26.4", "oci-spec", "serde_json", "serial_test", @@ -672,10 +672,8 @@ dependencies = [ "containerd-shim-wasm", "dbus", "env_logger", - "libc", "libcontainer", "log", - "nix 0.26.4", "oci-spec", "serde", "serde_json", diff --git a/crates/containerd-shim-wasm/src/sandbox/stdio.rs b/crates/containerd-shim-wasm/src/sandbox/stdio.rs index 9354f3375..7f95a2ab4 100644 --- a/crates/containerd-shim-wasm/src/sandbox/stdio.rs +++ b/crates/containerd-shim-wasm/src/sandbox/stdio.rs @@ -1,10 +1,7 @@ -use std::fs::File; use std::io::ErrorKind::NotFound; use std::io::{Error, Result}; use std::path::Path; -use std::sync::Arc; - -use crossbeam::atomic::AtomicCell; +use std::sync::{Arc, OnceLock}; use super::InstanceConfig; use crate::sys::stdio::*; @@ -16,6 +13,8 @@ pub struct Stdio { pub stderr: Stderr, } +static INITIAL_STDIO: OnceLock = OnceLock::new(); + impl Stdio { pub fn redirect(self) -> Result<()> { self.stdin.redirect()?; @@ -39,17 +38,39 @@ impl Stdio { stderr: cfg.get_stderr().try_into()?, }) } + + pub fn init_from_std() -> Self { + Self { + stdin: Stdin::try_from_std().unwrap_or_default(), + stdout: Stdout::try_from_std().unwrap_or_default(), + stderr: Stderr::try_from_std().unwrap_or_default(), + } + } + + pub fn guard(self) -> impl Drop { + StdioGuard(self) + } +} + +struct StdioGuard(Stdio); + +impl Drop for StdioGuard { + fn drop(&mut self) { + let _ = self.0.take().redirect(); + } } #[derive(Clone, Default)] -pub struct StdioStream(Arc>>); +pub struct StdioStream(Arc); impl StdioStream { pub fn redirect(self) -> Result<()> { - if let Some(f) = self.0.take() { - let f = try_into_fd(f)?; - let _ = unsafe { libc::dup(FD) }; - if unsafe { libc::dup2(f.as_raw_fd(), FD) } == -1 { + if let Some(fd) = self.0.as_raw_fd() { + // Before any redirection we try to keep a copy of the original stdio + // to make sure the streams stay open + INITIAL_STDIO.get_or_init(Stdio::init_from_std); + + if unsafe { libc::dup2(fd, FD) } == -1 { return Err(Error::last_os_error()); } } @@ -57,27 +78,34 @@ impl StdioStream { } pub fn take(&self) -> Self { - Self(Arc::new(AtomicCell::new(self.0.take()))) + Self(Arc::new(self.0.take())) + } + + pub fn try_from_std() -> Result { + let fd: i32 = unsafe { libc::dup(FD) }; + if fd == -1 { + return Err(Error::last_os_error()); + } + Ok(Self(Arc::new(unsafe { StdioOwnedFd::from_raw_fd(fd) }))) } } impl, const FD: StdioRawFd> TryFrom> for StdioStream { type Error = Error; fn try_from(path: Option

) -> Result { - let file = path + let fd = path .and_then(|path| match path.as_ref() { path if path.as_os_str().is_empty() => None, - path => Some(path.to_owned()), - }) - .map(|path| match open_file(path) { - Err(err) if err.kind() == NotFound => Ok(None), - Ok(f) => Ok(Some(f)), - Err(err) => Err(err), + path => Some(StdioOwnedFd::try_from_path(path)), }) - .transpose()? - .flatten(); + .transpose() + .or_else(|err| match err.kind() { + NotFound => Ok(None), + _ => Err(err), + })? + .unwrap_or_default(); - Ok(Self(Arc::new(AtomicCell::new(file)))) + Ok(Self(Arc::new(fd))) } } diff --git a/crates/containerd-shim-wasm/src/sys/unix/stdio.rs b/crates/containerd-shim-wasm/src/sys/unix/stdio.rs index 3bd0ae0f9..51f4e9d8d 100644 --- a/crates/containerd-shim-wasm/src/sys/unix/stdio.rs +++ b/crates/containerd-shim-wasm/src/sys/unix/stdio.rs @@ -1,15 +1,51 @@ -use std::fs::{File, OpenOptions}; +use std::fs::OpenOptions; use std::io::Result; -use std::os::fd::OwnedFd; -pub use std::os::fd::{AsRawFd as StdioAsRawFd, RawFd as StdioRawFd}; +use std::os::fd::{IntoRawFd, OwnedFd, RawFd}; use std::path::Path; +use crossbeam::atomic::AtomicCell; pub use libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; -pub fn try_into_fd(f: impl Into) -> Result { - Ok(f.into()) +pub type StdioRawFd = RawFd; + +pub struct StdioOwnedFd(AtomicCell); + +impl Drop for StdioOwnedFd { + fn drop(&mut self) { + let fd = self.0.swap(-1); + if fd >= 0 { + unsafe { libc::close(fd) }; + } + } +} + +impl Default for StdioOwnedFd { + fn default() -> Self { + Self(AtomicCell::new(-1)) + } } -pub fn open_file>(path: P) -> Result { - OpenOptions::new().read(true).write(true).open(path) +impl StdioOwnedFd { + pub fn try_from(f: impl Into) -> Result { + let fd = f.into().into_raw_fd(); + Ok(unsafe { Self::from_raw_fd(fd) }) + } + + pub unsafe fn from_raw_fd(fd: StdioRawFd) -> Self { + Self(AtomicCell::new(fd)) + } + + pub fn as_raw_fd(&self) -> Option { + let fd = self.0.load(); + (fd >= 0).then_some(fd) + } + + pub fn take(&self) -> Self { + let fd = self.0.swap(-1); + unsafe { Self::from_raw_fd(fd) } + } + + pub fn try_from_path(path: impl AsRef) -> Result { + Self::try_from(OpenOptions::new().read(true).write(true).open(path)?) + } } diff --git a/crates/containerd-shim-wasm/src/sys/windows/stdio.rs b/crates/containerd-shim-wasm/src/sys/windows/stdio.rs index 1c92d1c71..fb763b1b2 100644 --- a/crates/containerd-shim-wasm/src/sys/windows/stdio.rs +++ b/crates/containerd-shim-wasm/src/sys/windows/stdio.rs @@ -1,11 +1,12 @@ -use std::fs::{File, OpenOptions}; +use std::fs::OpenOptions; use std::io::ErrorKind::Other; use std::io::{Error, Result}; use std::os::windows::fs::OpenOptionsExt; use std::os::windows::prelude::{AsRawHandle, IntoRawHandle, OwnedHandle}; use std::path::Path; -use libc::{c_int, close, intptr_t, open_osfhandle, O_APPEND}; +use crossbeam::atomic::AtomicCell; +use libc::{intptr_t, open_osfhandle, O_APPEND}; use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OVERLAPPED; pub type StdioRawFd = libc::c_int; @@ -14,41 +15,55 @@ pub const STDIN_FILENO: StdioRawFd = 0; pub const STDOUT_FILENO: StdioRawFd = 1; pub const STDERR_FILENO: StdioRawFd = 2; -struct StdioOwnedFd(c_int); +pub struct StdioOwnedFd(AtomicCell); -pub fn try_into_fd(f: impl Into) -> Result { - let handle = f.into(); - let fd = unsafe { open_osfhandle(handle.as_raw_handle() as intptr_t, O_APPEND) }; - if fd == -1 { - return Err(Error::new(Other, "Failed to open file descriptor")); +impl Drop for StdioOwnedFd { + fn drop(&mut self) { + let fd = self.0.swap(-1); + if fd >= 0 { + unsafe { libc::close(fd) }; + } } - let _ = handle.into_raw_handle(); // drop ownership of the handle, it's managed by fd now - Ok(StdioOwnedFd(fd)) } -pub fn open_file>(path: P) -> Result { - // Containerd always passes a named pipe for stdin, stdout, and stderr so we can check if it is a pipe and open with overlapped IO - let mut options = OpenOptions::new(); - options.read(true).write(true); - if path.as_ref().starts_with("\\\\.\\pipe\\") { - options.custom_flags(FILE_FLAG_OVERLAPPED); +impl Default for StdioOwnedFd { + fn default() -> Self { + Self(AtomicCell::new(-1)) } - - options.open(path) } -pub trait StdioAsRawFd { - fn as_raw_fd(&self) -> c_int; -} +impl StdioOwnedFd { + pub fn try_from(f: impl Into) -> Result { + let handle = f.into(); + let fd = unsafe { open_osfhandle(handle.as_raw_handle() as intptr_t, O_APPEND) }; + if fd == -1 { + return Err(Error::new(Other, "Failed to open file descriptor")); + } + let _ = handle.into_raw_handle(); // drop ownership of the handle, it's managed by fd now + Ok(unsafe { Self::from_raw_fd(fd) }) + } -impl StdioAsRawFd for StdioOwnedFd { - fn as_raw_fd(&self) -> c_int { - self.0 + pub unsafe fn from_raw_fd(fd: StdioRawFd) -> Self { + Self(AtomicCell::new(fd)) } -} -impl Drop for StdioOwnedFd { - fn drop(&mut self) { - unsafe { close(self.0) }; + pub fn as_raw_fd(&self) -> Option { + let fd = self.0.load(); + (fd >= 0).then_some(fd) + } + + pub fn take(&self) -> Self { + let fd = self.0.swap(-1); + unsafe { Self::from_raw_fd(fd) } + } + + pub fn try_from_path(path: impl AsRef) -> Result { + // Containerd always passes a named pipe for stdin, stdout, and stderr so we can check if it is a pipe and open with overlapped IO + let mut options = OpenOptions::new(); + options.read(true).write(true); + if path.as_ref().starts_with(r"\\.\pipe\") { + options.custom_flags(FILE_FLAG_OVERLAPPED); + } + Self::try_from(options.open(path)?) } } diff --git a/crates/containerd-shim-wasmedge/Cargo.toml b/crates/containerd-shim-wasmedge/Cargo.toml index 740132d84..06c635a17 100644 --- a/crates/containerd-shim-wasmedge/Cargo.toml +++ b/crates/containerd-shim-wasmedge/Cargo.toml @@ -15,16 +15,16 @@ anyhow = { workspace = true } oci-spec = { workspace = true, features = ["runtime"] } thiserror = { workspace = true } serde_json = { workspace = true } -libc = { workspace = true } [target.'cfg(unix)'.dependencies] libcontainer = { workspace = true } dbus = { version = "*", optional = true } -nix = { workspace = true } [dev-dependencies] -tempfile = "3.7" +tempfile = "3.8" serial_test = "*" +env_logger = "0.10" +libc = { workspace = true } [features] default = ["standalone", "static"] diff --git a/crates/containerd-shim-wasmedge/src/instance/instance_linux.rs b/crates/containerd-shim-wasmedge/src/instance/instance_linux.rs index 9e4608eac..82a6790b9 100644 --- a/crates/containerd-shim-wasmedge/src/instance/instance_linux.rs +++ b/crates/containerd-shim-wasmedge/src/instance/instance_linux.rs @@ -77,38 +77,18 @@ impl LibcontainerInstance for Wasi { mod wasitest { use std::fs::read_to_string; - use std::os::unix::io::RawFd; 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; - use libc::{dup2, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; use serial_test::serial; use tempfile::tempdir; use wasmedge_sdk::wat2wasm; use super::*; - static mut STDIN_FD: Option = None; - static mut STDOUT_FD: Option = None; - static mut STDERR_FD: Option = None; - - fn reset_stdio() { - unsafe { - if let Some(stdin) = STDIN_FD { - let _ = dup2(stdin, STDIN_FILENO); - } - if let Some(stdout) = STDOUT_FD { - let _ = dup2(stdout, STDOUT_FILENO); - } - if let Some(stderr) = STDERR_FD { - let _ = dup2(stderr, STDERR_FILENO); - } - } - } - // 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 @@ -185,6 +165,13 @@ mod wasitest { 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 = wat2wasm(WASI_HELLO_WAT).unwrap(); @@ -196,7 +183,6 @@ mod wasitest { let output = read_to_string(path.join("stdout"))?; assert_eq!(output, "hello world\n"); - reset_stdio(); Ok(()) } @@ -208,6 +194,11 @@ mod wasitest { return run_test_with_sudo(function!()); } + // start logging + let _ = env_logger::try_init(); + + let _guard = Stdio::init_from_std().guard(); + let dir = tempdir()?; let wasm_bytes = wat2wasm(WASI_RETURN_ERROR).unwrap(); @@ -216,7 +207,6 @@ mod wasitest { // Expect error code from the run. assert_eq!(res.0, 137); - reset_stdio(); Ok(()) } @@ -228,6 +218,11 @@ mod wasitest { return run_test_with_sudo(function!()); } + // start logging + let _ = env_logger::try_init(); + + let _guard = Stdio::init_from_std().guard(); + let expected_exit_code: u32 = 42; let dir = tempdir()?; @@ -236,7 +231,6 @@ mod wasitest { assert_eq!(actual_exit_code, expected_exit_code); - reset_stdio(); Ok(()) } } diff --git a/crates/containerd-shim-wasmtime/Cargo.toml b/crates/containerd-shim-wasmtime/Cargo.toml index 199d0cb0c..d1b2bf815 100644 --- a/crates/containerd-shim-wasmtime/Cargo.toml +++ b/crates/containerd-shim-wasmtime/Cargo.toml @@ -33,16 +33,13 @@ oci-spec = { workspace = true, features = ["runtime"] } thiserror = { workspace = true } serde_json = { workspace = true } serde = { workspace = true } -libc = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true } libcontainer = { workspace = true } dbus = { version = "*", optional = true } [dev-dependencies] tempfile = "3.8" -libc = { workspace = true } serial_test = "*" env_logger = "0.10" diff --git a/crates/containerd-shim-wasmtime/src/instance/instance_linux.rs b/crates/containerd-shim-wasmtime/src/instance/instance_linux.rs index 4437b31a4..542b48ae2 100644 --- a/crates/containerd-shim-wasmtime/src/instance/instance_linux.rs +++ b/crates/containerd-shim-wasmtime/src/instance/instance_linux.rs @@ -82,38 +82,17 @@ impl LibcontainerInstance for Wasi { mod wasitest { use std::fs::read_to_string; - use std::os::fd::RawFd; 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; - use libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; - use nix::unistd::dup2; use serial_test::serial; use tempfile::tempdir; use super::*; - static mut STDIN_FD: Option = None; - static mut STDOUT_FD: Option = None; - static mut STDERR_FD: Option = None; - - fn reset_stdio() { - unsafe { - if let Some(stdin) = STDIN_FD { - let _ = dup2(stdin, STDIN_FILENO); - } - if let Some(stdout) = STDOUT_FD { - let _ = dup2(stdout, STDOUT_FILENO); - } - if let Some(stderr) = STDERR_FD { - let _ = dup2(stderr, STDERR_FILENO); - } - } - } - // 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"); @@ -174,7 +153,6 @@ mod wasitest { let i = Wasi::new("".to_string(), Some(&cfg)); i.delete()?; - reset_stdio(); Ok(()) } @@ -185,11 +163,14 @@ mod wasitest { 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); @@ -201,7 +182,6 @@ mod wasitest { let output = read_to_string(path.join("stdout"))?; assert_eq!(output, "hello world\n"); - reset_stdio(); Ok(()) } @@ -217,6 +197,8 @@ mod wasitest { // start logging 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(Some("foo")); @@ -228,7 +210,6 @@ mod wasitest { let output = read_to_string(path.join("stdout"))?; assert_eq!(output, "hello world\n"); - reset_stdio(); Ok(()) } @@ -243,6 +224,8 @@ mod wasitest { // start logging let _ = env_logger::try_init(); + let _guard = Stdio::init_from_std().guard(); + let expected_exit_code: u32 = 42; let dir = tempdir()?; @@ -251,7 +234,6 @@ mod wasitest { assert_eq!(actual_exit_code, expected_exit_code); - reset_stdio(); Ok(()) } } From 426b37ea4363fb402e312547f83e4b98022d144d Mon Sep 17 00:00:00 2001 From: Jorge Prendes Date: Fri, 18 Aug 2023 10:14:30 +0100 Subject: [PATCH 2/2] add simplified container instance api Signed-off-by: Jorge Prendes --- .github/workflows/action-fmt.yml | 6 - .github/workflows/action-test-k3s.yml | 6 + Cargo.lock | 2 + Cargo.toml | 1 + README.md | 49 +++- crates/containerd-shim-wasm/Cargo.toml | 1 + .../src/container/context.rs | 181 ++++++++++++ .../src/container/engine.rs | 39 +++ .../src/container/executor.rs | 110 ++++++++ .../src/container/instance.rs | 151 ++++++++++ .../containerd-shim-wasm/src/container/mod.rs | 24 ++ .../src/container/path.rs | 79 ++++++ crates/containerd-shim-wasm/src/lib.rs | 4 +- .../container_executor.rs | 117 -------- .../src/libcontainer_instance/instance.rs | 181 ------------ .../src/libcontainer_instance/mod.rs | 5 - .../containerd-shim-wasm/src/sandbox/error.rs | 4 + .../src/sandbox/instance.rs | 14 - .../src/sandbox/manager.rs | 13 +- .../containerd-shim-wasm/src/sandbox/mod.rs | 2 +- .../containerd-shim-wasm/src/sandbox/oci.rs | 171 ------------ .../containerd-shim-wasm/src/sandbox/shim.rs | 8 +- .../src/sandbox/testutil.rs | 1 - .../src/sys/unix/metrics.rs | 4 +- .../src/sys/unix/signals.rs | 2 +- .../src/sys/windows/metrics.rs | 4 +- .../bin/containerd-shim-wasmedge-v1/main.rs | 5 +- .../src/bin/containerd-wasmedged/main.rs | 9 +- crates/containerd-shim-wasmedge/src/error.rs | 13 - .../containerd-shim-wasmedge/src/executor.rs | 140 ---------- .../src/instance/instance_linux.rs | 236 ---------------- .../src/instance/instance_windows.rs | 40 --- .../src/instance_linux.rs | 78 ++++++ .../src/instance_windows.rs | 28 ++ crates/containerd-shim-wasmedge/src/lib.rs | 45 +-- .../containerd-shim-wasmedge/src/oci_utils.rs | 31 --- crates/containerd-shim-wasmedge/src/tests.rs | 218 +++++++++++++++ crates/containerd-shim-wasmer/build.rs | 24 ++ .../src/bin/containerd-shim-wasmer-v1/main.rs | 5 +- .../bin/containerd-shim-wasmerd-v1/main.rs | 2 + .../src/bin/containerd-wasmerd/main.rs | 4 +- crates/containerd-shim-wasmer/src/executor.rs | 137 --------- .../src/instance/instance_linux.rs | 262 ------------------ .../src/instance/instance_windows.rs | 38 --- .../src/instance_linux.rs | 67 +++++ .../src/instance_windows.rs | 28 ++ crates/containerd-shim-wasmer/src/lib.rs | 29 +- crates/containerd-shim-wasmer/src/tests.rs | 186 +++++++++++++ crates/containerd-shim-wasmtime/Cargo.toml | 5 +- .../bin/containerd-shim-wasmtime-v1/main.rs | 5 +- .../src/bin/containerd-wasmtimed/main.rs | 4 +- crates/containerd-shim-wasmtime/src/error.rs | 15 - .../containerd-shim-wasmtime/src/executor.rs | 143 ---------- .../src/instance/instance_linux.rs | 239 ---------------- .../src/instance/instance_windows.rs | 38 --- .../src/instance_linux.rs | 75 +++++ .../src/instance_windows.rs | 28 ++ crates/containerd-shim-wasmtime/src/lib.rs | 13 +- .../src/oci_wasmtime.rs | 78 ------ crates/containerd-shim-wasmtime/src/tests.rs | 186 +++++++++++++ 60 files changed, 1635 insertions(+), 1998 deletions(-) create mode 100644 crates/containerd-shim-wasm/src/container/context.rs create mode 100644 crates/containerd-shim-wasm/src/container/engine.rs create mode 100644 crates/containerd-shim-wasm/src/container/executor.rs create mode 100644 crates/containerd-shim-wasm/src/container/instance.rs create mode 100644 crates/containerd-shim-wasm/src/container/mod.rs create mode 100644 crates/containerd-shim-wasm/src/container/path.rs delete mode 100644 crates/containerd-shim-wasm/src/libcontainer_instance/container_executor.rs delete mode 100644 crates/containerd-shim-wasm/src/libcontainer_instance/instance.rs delete mode 100644 crates/containerd-shim-wasm/src/libcontainer_instance/mod.rs delete mode 100644 crates/containerd-shim-wasmedge/src/error.rs delete mode 100644 crates/containerd-shim-wasmedge/src/executor.rs delete mode 100644 crates/containerd-shim-wasmedge/src/instance/instance_linux.rs delete mode 100644 crates/containerd-shim-wasmedge/src/instance/instance_windows.rs create mode 100644 crates/containerd-shim-wasmedge/src/instance_linux.rs create mode 100644 crates/containerd-shim-wasmedge/src/instance_windows.rs delete mode 100644 crates/containerd-shim-wasmedge/src/oci_utils.rs create mode 100644 crates/containerd-shim-wasmedge/src/tests.rs create mode 100644 crates/containerd-shim-wasmer/build.rs delete mode 100644 crates/containerd-shim-wasmer/src/executor.rs delete mode 100644 crates/containerd-shim-wasmer/src/instance/instance_linux.rs delete mode 100644 crates/containerd-shim-wasmer/src/instance/instance_windows.rs create mode 100644 crates/containerd-shim-wasmer/src/instance_linux.rs create mode 100644 crates/containerd-shim-wasmer/src/instance_windows.rs create mode 100644 crates/containerd-shim-wasmer/src/tests.rs delete mode 100644 crates/containerd-shim-wasmtime/src/error.rs delete mode 100644 crates/containerd-shim-wasmtime/src/executor.rs delete mode 100644 crates/containerd-shim-wasmtime/src/instance/instance_linux.rs delete mode 100644 crates/containerd-shim-wasmtime/src/instance/instance_windows.rs create mode 100644 crates/containerd-shim-wasmtime/src/instance_linux.rs create mode 100644 crates/containerd-shim-wasmtime/src/instance_windows.rs delete mode 100644 crates/containerd-shim-wasmtime/src/oci_wasmtime.rs create mode 100644 crates/containerd-shim-wasmtime/src/tests.rs diff --git a/.github/workflows/action-fmt.yml b/.github/workflows/action-fmt.yml index bc507415c..803a1bef1 100644 --- a/.github/workflows/action-fmt.yml +++ b/.github/workflows/action-fmt.yml @@ -25,11 +25,5 @@ jobs: - run: # needed to run rustfmt in nightly toolchain rustup toolchain install nightly --component rustfmt - - name: Set environment variables for Windows - if: runner.os == 'Windows' - run: | - # required until standalong is implemented for windows (https://github.com/WasmEdge/wasmedge-rust-sdk/issues/54) - echo "WASMEDGE_LIB_DIR=C:\Program Files\WasmEdge\lib" >> $env:GITHUB_ENV - echo "WASMEDGE_INCLUDE_DIR=C:\Program Files\WasmEdge\include" >> $env:GITHUB_ENV - name: Run checks run: make check diff --git a/.github/workflows/action-test-k3s.yml b/.github/workflows/action-test-k3s.yml index 164bcbd89..7a5d53836 100644 --- a/.github/workflows/action-test-k3s.yml +++ b/.github/workflows/action-test-k3s.yml @@ -37,6 +37,12 @@ jobs: - name: run timeout-minutes: 5 run: make test/k3s-${{ inputs.runtime }} + # only runs when the previous step fails + - name: inspect failed pods + if: failure() + run: | + sudo bin/k3s kubectl get pods --all-namespaces + sudo bin/k3s kubectl describe pods --all-namespaces - name: cleanup if: always() run: make test/k3s/clean diff --git a/Cargo.lock b/Cargo.lock index a0f59f22e..c8b563227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,6 +610,7 @@ dependencies = [ "thiserror", "ttrpc", "ttrpc-codegen", + "wat", "windows-sys 0.48.0", ] @@ -684,6 +685,7 @@ dependencies = [ "wasi-common", "wasmtime", "wasmtime-wasi", + "wat", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f475cfbc1..554975247 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ sha256 = "1.4.0" libcontainer = { version = "0.2", default-features = false } windows-sys = { version = "0.48" } crossbeam = { version = "0.8.2", default-features = false } +wat = "*" # Use whatever version wasmtime will make us pull [profile.release] panic = "abort" diff --git a/README.md b/README.md index c5bc4ea83..368275fc2 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,19 @@ There are two modes of operation supported: 1. "Normal" mode where there is 1 shim process per container or k8s pod. 2. "Shared" mode where there is a single manager service running all shims in process. -In either case you need to implement the `Instance` trait: +In either case you need to implement a trait to teach runwasi how to use your wasm host. + +There are two ways to do this: +* implementing the `sandbox::Instance` trait +* or implementing the `container::Engine` trait + +The most flexible but complex is the `sandbox::Instance` trait: ```rust pub trait Instance { + /// The WASI engine type + type Engine: Send + Sync + Clone; + /// Create a new instance fn new(id: String, cfg: Option<&InstanceConfig>) -> Self; /// Start the instance @@ -48,6 +57,25 @@ pub trait Instance { } ``` +The `container::Engine` trait provides a simplified API: + +```rust +pub trait Engine: Clone + Send + Sync + 'static { + /// The name to use for this engine + fn name() -> &'static str; + /// Run a WebAssembly container + fn run_wasi(&self, ctx: &impl RuntimeContext, stdio: Stdio) -> Result; + /// Check that the runtime can run the container. + /// This checks runs after the container creation and before the container starts. + /// By it checks that the wasi_entrypoint is either: + /// * a file with the `wasm` filetype header + /// * a parsable `wat` file. + fn can_handle(&self, ctx: &impl RuntimeContext) -> Result<()> { /* default implementation*/ } +} +``` + +After implementing `container::Engine` you can use `container::Instance`, which implements the `sandbox::Instance` trait. + To use your implementation in "normal" mode, you'll need to create a binary which has a main that looks something like this: ```rust @@ -67,6 +95,25 @@ fn main() { } ``` +or when using the `container::Engine` trait, like this: + +```rust +use containerd_shim as shim; +use containerd_shim_wasm::{sandbox::ShimCli, container::{Instance, Engine}} + +struct MyEngine { + // ... +} + +impl Engine for MyEngine { + // ... +} + +fn main() { + shim::run::>>("io.containerd.myshim.v1", opts); +} +``` + Note you can implement your own ShimCli if you like and customize your wasm engine and other things. I encourage you to checkout how that is implemented. diff --git a/crates/containerd-shim-wasm/Cargo.toml b/crates/containerd-shim-wasm/Cargo.toml index 2b5fcc757..89eacc2c9 100644 --- a/crates/containerd-shim-wasm/Cargo.toml +++ b/crates/containerd-shim-wasm/Cargo.toml @@ -24,6 +24,7 @@ chrono = { workspace = true } log = { workspace = true } libc = { workspace = true } crossbeam = { workspace = true } +wat = { workspace = true } [target.'cfg(unix)'.dependencies] clone3 = "0.2" diff --git a/crates/containerd-shim-wasm/src/container/context.rs b/crates/containerd-shim-wasm/src/container/context.rs new file mode 100644 index 000000000..c0efc12e4 --- /dev/null +++ b/crates/containerd-shim-wasm/src/container/context.rs @@ -0,0 +1,181 @@ +use std::path::{Path, PathBuf}; + +use oci_spec::runtime::Spec; + +pub trait RuntimeContext { + // ctx.args() returns arguments from the runtime spec process field, including the + // 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. + // The girst argument in the spec is specified as `path#func` where `func` is optional + // and defaults to _start, e.g.: + // "/app/app.wasm#entry" -> { path: "/app/app.wasm", func: "entry" } + // "my_module.wat" -> { path: "my_module.wat", func: "_start" } + // "#init" -> { path: "", func: "init" } + fn wasi_entrypoint(&self) -> WasiEntrypoint; +} + +pub struct WasiEntrypoint { + pub path: PathBuf, + pub func: String, +} + +impl RuntimeContext for Spec { + fn args(&self) -> &[String] { + self.process() + .as_ref() + .and_then(|p| p.args().as_ref()) + .map(|a| a.as_slice()) + .unwrap_or_default() + } + + fn entrypoint(&self) -> Option<&Path> { + self.args().first().map(Path::new) + } + + fn wasi_entrypoint(&self) -> WasiEntrypoint { + let arg0 = self.args().first().map(String::as_str).unwrap_or(""); + let (path, func) = arg0.split_once('#').unwrap_or((arg0, "_start")); + WasiEntrypoint { + path: PathBuf::from(path), + func: func.to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use oci_spec::runtime::{ProcessBuilder, RootBuilder, SpecBuilder}; + + use super::*; + + #[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 spec = &spec; + + let args = spec.args(); + 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 spec = &spec; + + let args = spec.args(); + 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 spec = &spec; + + let args = spec.args(); + 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 spec = &spec; + + let path = spec.wasi_entrypoint().path; + assert!(path.as_os_str().is_empty()); + + 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 spec = &spec; + + let WasiEntrypoint { path, func } = spec.wasi_entrypoint(); + assert_eq!(path, Path::new("hello.wat")); + assert_eq!(func, "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![ + "/root/hello.wat".to_string(), + "echo".to_string(), + "hello".to_string(), + ]) + .build()?, + ) + .build()?; + let spec = &spec; + + let WasiEntrypoint { path, func } = spec.wasi_entrypoint(); + assert_eq!(path, Path::new("/root/hello.wat")); + assert_eq!(func, "_start"); + + Ok(()) + } +} diff --git a/crates/containerd-shim-wasm/src/container/engine.rs b/crates/containerd-shim-wasm/src/container/engine.rs new file mode 100644 index 000000000..8c9783117 --- /dev/null +++ b/crates/containerd-shim-wasm/src/container/engine.rs @@ -0,0 +1,39 @@ +use std::fs::File; +use std::io::Read; + +use anyhow::{Context, Result}; + +use crate::container::{PathResolve, RuntimeContext}; +use crate::sandbox::Stdio; + +pub trait Engine: Clone + Send + Sync + 'static { + /// The name to use for this engine + fn name() -> &'static str; + + /// Run a WebAssembly container + fn run_wasi(&self, ctx: &impl RuntimeContext, stdio: Stdio) -> Result; + + /// Check that the runtime can run the container. + /// This checks runs after the container creation and before the container starts. + /// By it checks that the wasi_entrypoint is either: + /// * a file with the `wasm` filetype header + /// * a parsable `wat` file. + fn can_handle(&self, ctx: &impl RuntimeContext) -> Result<()> { + let path = ctx + .wasi_entrypoint() + .path + .resolve_in_path_or_cwd() + .next() + .context("module not found")?; + + let mut buffer = [0; 4]; + File::open(&path)?.read_exact(&mut buffer)?; + + if buffer.as_slice() != b"\0asm" { + // Check if this is a `.wat` file + wat::parse_file(&path)?; + } + + Ok(()) + } +} diff --git a/crates/containerd-shim-wasm/src/container/executor.rs b/crates/containerd-shim-wasm/src/container/executor.rs new file mode 100644 index 000000000..54d9fcc29 --- /dev/null +++ b/crates/containerd-shim-wasm/src/container/executor.rs @@ -0,0 +1,110 @@ +use std::cell::OnceCell; +use std::fs::File; +use std::io::Read; +use std::os::unix::prelude::PermissionsExt; +use std::path::PathBuf; + +use anyhow::{bail, Context, Result}; +use libcontainer::workload::default::DefaultExecutor; +use libcontainer::workload::{ + Executor as LibcontainerExecutor, ExecutorError as LibcontainerExecutorError, + ExecutorValidationError, +}; +use oci_spec::runtime::Spec; + +use crate::container::context::RuntimeContext; +use crate::container::engine::Engine; +use crate::container::PathResolve; +use crate::sandbox::Stdio; + +#[derive(Clone)] +enum InnerExecutor { + Wasm, + Linux, + CantHandle, +} + +#[derive(Clone)] +pub(crate) struct Executor { + engine: E, + stdio: Stdio, + inner: OnceCell, +} + +impl LibcontainerExecutor for Executor { + fn validate(&self, spec: &Spec) -> Result<(), ExecutorValidationError> { + // We can handle linux container. We delegate wasm container to the engine. + match self.inner(spec) { + InnerExecutor::CantHandle => Err(ExecutorValidationError::CantHandle(E::name())), + _ => Ok(()), + } + } + + fn exec(&self, spec: &Spec) -> Result<(), LibcontainerExecutorError> { + // If it looks like a linux container, run it as a linux container. + // Otherwise, run it as a wasm container + match self.inner(spec) { + InnerExecutor::CantHandle => Err(LibcontainerExecutorError::CantHandle(E::name())), + InnerExecutor::Linux => { + log::info!("executing linux container"); + self.stdio.take().redirect().unwrap(); + DefaultExecutor {}.exec(spec) + } + InnerExecutor::Wasm => { + log::info!("calling start function"); + match self.engine.run_wasi(spec, self.stdio.take()) { + Ok(code) => std::process::exit(code), + Err(err) => { + log::info!("error running start function: {err}"); + std::process::exit(137) + } + }; + } + } + } +} + +impl Executor { + pub fn new(engine: E, stdio: Stdio) -> Self { + Self { + engine, + stdio, + inner: Default::default(), + } + } + + fn inner(&self, spec: &Spec) -> &InnerExecutor { + self.inner.get_or_init(|| { + if is_linux_container(spec).is_ok() { + InnerExecutor::Linux + } else if self.engine.can_handle(spec).is_ok() { + InnerExecutor::Wasm + } else { + InnerExecutor::CantHandle + } + }) + } +} + +fn is_linux_container(spec: &Spec) -> Result<()> { + let executable = spec + .entrypoint() + .context("no entrypoint provided")? + .resolve_in_path() + .find_map(|p| -> Option { + let mode = p.metadata().ok()?.permissions().mode(); + (mode & 0o001 != 0).then_some(p) + }) + .context("entrypoint not found")?; + + // check the shebang and ELF magic number + // https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header + let mut buffer = [0; 4]; + File::open(executable)?.read_exact(&mut buffer)?; + + match buffer { + [0x7f, 0x45, 0x4c, 0x46] => Ok(()), // ELF magic number + [0x23, 0x21, ..] => Ok(()), // shebang + _ => bail!("not a valid script or elf file"), + } +} diff --git a/crates/containerd-shim-wasm/src/container/instance.rs b/crates/containerd-shim-wasm/src/container/instance.rs new file mode 100644 index 000000000..e4c7e9490 --- /dev/null +++ b/crates/containerd-shim-wasm/src/container/instance.rs @@ -0,0 +1,151 @@ +use std::path::{Path, PathBuf}; +use std::thread; + +use anyhow::Context; +use chrono::Utc; +use libcontainer::container::builder::ContainerBuilder; +use libcontainer::container::Container; +use libcontainer::signal::Signal; +use libcontainer::syscall::syscall::SyscallType; +use nix::errno::Errno; +use nix::sys::wait::{waitid, Id as WaitID, WaitPidFlag, WaitStatus}; + +use crate::container::executor::Executor; +use crate::container::Engine; +use crate::sandbox::instance::{ExitCode, Wait}; +use crate::sandbox::instance_utils::{determine_rootdir, get_instance_root, instance_exists}; +use crate::sandbox::{Error as SandboxError, Instance as SandboxInstance, InstanceConfig, Stdio}; + +static DEFAULT_CONTAINER_ROOT_DIR: &str = "/run/containerd"; + +pub struct Instance { + engine: E, + exit_code: ExitCode, + stdio: Stdio, + bundle: PathBuf, + rootdir: PathBuf, + id: String, +} + +impl SandboxInstance for Instance { + type Engine = E; + + fn new(id: String, cfg: Option<&InstanceConfig>) -> Self { + let cfg = cfg.unwrap(); + let engine = cfg.get_engine(); + let bundle = cfg.get_bundle().unwrap_or_default().into(); + let namespace = cfg.get_namespace(); + let rootdir = Path::new(DEFAULT_CONTAINER_ROOT_DIR).join(E::name()); + let rootdir = determine_rootdir(&bundle, &namespace, &rootdir).unwrap(); + let stdio = Stdio::init_from_cfg(cfg).expect("failed to open stdio"); + Self { + id, + exit_code: ExitCode::default(), + engine, + stdio, + bundle, + rootdir, + } + } + + /// Start the instance + /// The returned value should be a unique ID (such as a PID) for the instance. + /// Nothing internally should be using this ID, but it is returned to containerd where a user may want to use it. + fn start(&self) -> Result { + log::info!("starting instance: {}", self.id); + let mut container = ContainerBuilder::new(self.id.clone(), SyscallType::Linux) + .with_executor(Executor::new(self.engine.clone(), self.stdio.take())) + .with_root_path(self.rootdir.clone())? + .as_init(&self.bundle) + .with_systemd(false) + .build()?; + + let pid = container.pid().context("failed to get pid")?; + + container.start().map_err(|err| { + SandboxError::Any(anyhow::anyhow!("failed to start container: {}", err)) + })?; + + let exit_code = self.exit_code.clone(); + thread::spawn(move || { + let (lock, cvar) = &*exit_code; + + let status = match waitid(WaitID::Pid(pid), WaitPidFlag::WEXITED) { + Ok(WaitStatus::Exited(_, status)) => status, + Ok(WaitStatus::Signaled(_, sig, _)) => sig as i32, + Ok(_) => 0, + Err(Errno::ECHILD) => { + log::info!("no child process"); + 0 + } + Err(e) => panic!("waitpid failed: {e}"), + } as u32; + let mut ec = lock.lock().unwrap(); + *ec = Some((status, Utc::now())); + drop(ec); + cvar.notify_all(); + }); + + Ok(pid.as_raw() as u32) + } + + /// Send a signal to the instance + fn kill(&self, signal: u32) -> Result<(), SandboxError> { + log::info!("sending signal {signal} to instance: {}", self.id); + let signal = Signal::try_from(signal as i32).map_err(|err| { + SandboxError::InvalidArgument(format!("invalid signal number: {}", err)) + })?; + let container_root = get_instance_root(&self.rootdir, &self.id)?; + let mut container = Container::load(container_root) + .with_context(|| format!("could not load state for container {}", self.id))?; + + container.kill(signal, true)?; + + Ok(()) + } + + /// Delete any reference to the instance + /// This is called after the instance has exited. + fn delete(&self) -> Result<(), SandboxError> { + log::info!("deleting instance: {}", self.id); + match instance_exists(&self.rootdir, &self.id) { + Ok(exists) => { + if !exists { + return Ok(()); + } + } + Err(err) => { + log::error!("could not find the container, skipping cleanup: {}", err); + return Ok(()); + } + } + let container_root = get_instance_root(&self.rootdir, &self.id)?; + let container = Container::load(container_root) + .with_context(|| format!("could not load state for container {}", self.id)); + match container { + Ok(mut container) => container.delete(true).map_err(|err| { + SandboxError::Any(anyhow::anyhow!( + "failed to delete container {}: {}", + &self.id, + err + )) + })?, + Err(err) => { + log::error!("could not find the container, skipping cleanup: {}", err); + return Ok(()); + } + } + Ok(()) + } + + /// Set up waiting for the instance to exit + /// The Wait struct is used to send the exit code and time back to the + /// caller. The recipient is expected to call function + /// set_up_exit_code_wait() implemented by Wait to set up exit code + /// processing. Note that the "wait" function doesn't block, but + /// it sets up the waiting channel. + fn wait(&self, waiter: &Wait) -> Result<(), SandboxError> { + log::info!("waiting for instance: {}", self.id); + waiter.set_up_exit_code_wait(self.exit_code.clone()) + } +} diff --git a/crates/containerd-shim-wasm/src/container/mod.rs b/crates/containerd-shim-wasm/src/container/mod.rs new file mode 100644 index 000000000..0c55ad0fe --- /dev/null +++ b/crates/containerd-shim-wasm/src/container/mod.rs @@ -0,0 +1,24 @@ +//! This module contains an API for building WebAssembly shims running on top of containers. +//! Unlike the `sandbox` module, this module delegates many of the actions to the container runtime. +//! +//! This has some advantages: +//! * Simplifies writing new shims, get you up and running quickly +//! * The complexity of the OCI spec is already taken care of +//! +//! But it also has some disadvantages: +//! * Runtime overhead in in setting up a container +//! * Less customizable +//! * Currently only works on Linux + +mod context; +mod engine; +mod executor; +mod instance; +mod path; + +pub use context::{RuntimeContext, WasiEntrypoint}; +pub use engine::Engine; +pub use instance::Instance; +pub use path::PathResolve; + +pub use crate::sandbox::stdio::Stdio; diff --git a/crates/containerd-shim-wasm/src/container/path.rs b/crates/containerd-shim-wasm/src/container/path.rs new file mode 100644 index 000000000..84a2ccf79 --- /dev/null +++ b/crates/containerd-shim-wasm/src/container/path.rs @@ -0,0 +1,79 @@ +use std::path::{Path, PathBuf}; + +pub trait PathResolve { + // TODO: Once RPITIT lands in stable, change the return types from + // `-> Box` to `-> impl Trait` + // See: https://rustc-dev-guide.rust-lang.org/return-position-impl-trait-in-trait.html + + // Resolve the path of a file give a set of directories as the `which` unix + // command would do with components of the `PATH` environment variable, and + // return an iterator over all candidates. + // Resulting candidates are files that exist, but no other constraing is + // imposed, in particular this function does not check for the executable bits. + // Further contraints can be added by calling filtering the returned iterator. + fn resolve_in_dirs<'a>( + &self, + dirs: impl IntoIterator> + 'a, + ) -> Box + 'a>; + fn resolve_in_path(&self) -> Box>; + fn resolve_in_path_or_cwd(&self) -> Box>; +} + +// Gets the content of the `PATH` environment variable as an +// iterator over its components +pub fn paths() -> impl Iterator { + std::env::var_os("PATH") + .as_ref() + .map(std::env::split_paths) + .into_iter() + .flatten() + .collect::>() + .into_iter() +} + +impl> PathResolve for T { + fn resolve_in_dirs<'a>( + &self, + dirs: impl IntoIterator> + 'a, + ) -> Box + 'a> { + let cwd = std::env::current_dir().ok(); + + let has_separator = self.as_ref().components().count() > 1; + + // The seemingly extra complexity here is because we can only have one concrete + // return type even if we return an `impl Iterator` + let (first, second) = if has_separator { + // file has a separator, we only need to rÂșesolve relative to `cwd`, we must ignore `PATH` + (cwd, None) + } else { + // file is just a binary name, we must not resolve relative to `cwd`, but relative to `PATH` components + let dirs = dirs.into_iter().filter_map(move |p| { + let path = cwd.as_ref()?.join(p.as_ref()).canonicalize().ok()?; + path.is_dir().then_some(path) + }); + (None, Some(dirs)) + }; + + let file = self.as_ref().to_owned(); + let it = first + .into_iter() + .chain(second.into_iter().flatten()) + .filter_map(move |p| { + // skip any paths that are not files + let path = p.join(&file).canonicalize().ok()?; + path.is_file().then_some(path) + }); + + Box::new(it) + } + + // Like `find_in_dirs`, but searches on the entries of `PATH`. + fn resolve_in_path(&self) -> Box> { + self.resolve_in_dirs(paths()) + } + + // Like `find_in_dirs`, but searches on the entries of `PATH`, and on `cwd`, in that order. + fn resolve_in_path_or_cwd(&self) -> Box> { + self.resolve_in_dirs(paths().chain(std::env::current_dir().ok())) + } +} diff --git a/crates/containerd-shim-wasm/src/lib.rs b/crates/containerd-shim-wasm/src/lib.rs index f2bbf7aba..f3280f911 100644 --- a/crates/containerd-shim-wasm/src/lib.rs +++ b/crates/containerd-shim-wasm/src/lib.rs @@ -9,5 +9,5 @@ pub mod services; #[cfg_attr(windows, path = "sys/windows/mod.rs")] pub(crate) mod sys; -#[cfg(all(feature = "libcontainer", not(target_os = "windows")))] -pub mod libcontainer_instance; +#[cfg(feature = "libcontainer")] +pub mod container; diff --git a/crates/containerd-shim-wasm/src/libcontainer_instance/container_executor.rs b/crates/containerd-shim-wasm/src/libcontainer_instance/container_executor.rs deleted file mode 100644 index 07d8a6642..000000000 --- a/crates/containerd-shim-wasm/src/libcontainer_instance/container_executor.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::fs::OpenOptions; -use std::io::Read; -use std::path::PathBuf; - -use libcontainer::workload::default::DefaultExecutor; -use libcontainer::workload::{Executor, ExecutorError, ExecutorValidationError}; -use oci_spec::runtime::Spec; - -use crate::sandbox::{oci, Stdio}; - -#[derive(Clone)] -pub struct LinuxContainerExecutor { - stdio: Stdio, - default_executor: DefaultExecutor, -} - -impl LinuxContainerExecutor { - pub fn new(stdio: Stdio) -> Self { - Self { - stdio, - default_executor: DefaultExecutor {}, - } - } -} - -impl Executor for LinuxContainerExecutor { - fn exec(&self, spec: &Spec) -> Result<(), ExecutorError> { - self.stdio.take().redirect().map_err(|err| { - log::error!("failed to redirect io: {}", err); - ExecutorError::Other(format!("failed to redirect io: {}", err)) - })?; - self.default_executor.exec(spec) - } - - fn validate(&self, spec: &Spec) -> Result<(), ExecutorValidationError> { - self.default_executor.validate(spec)?; - - can_handle(spec) - .then_some(()) - .ok_or(ExecutorValidationError::InvalidArg) - } -} - -fn can_handle(spec: &Spec) -> bool { - let args = oci::get_args(spec); - - if args.is_empty() { - return false; - } - - let executable = args[0].as_str(); - - // mostly follows youki's verify_binary implementation - // https://github.com/containers/youki/blob/2d6fd7650bb0f22a78fb5fa982b5628f61fe25af/crates/libcontainer/src/process/container_init_process.rs#L106 - let path = if executable.contains('/') { - PathBuf::from(executable) - } else { - let path = std::env::var("PATH").unwrap_or_default(); - // check each path in $PATH - let mut found = false; - let mut found_path = PathBuf::default(); - for p in path.split(':') { - let path = PathBuf::from(p).join(executable); - if path.exists() { - found = true; - found_path = path; - break; - } - } - if !found { - return false; - } - found_path - }; - - // check execute permission - use std::os::unix::fs::PermissionsExt; - let metadata = path.metadata(); - if metadata.is_err() { - log::info!("failed to get metadata of {:?}", path); - return false; - } - let metadata = metadata.unwrap(); - let permissions = metadata.permissions(); - if !metadata.is_file() || permissions.mode() & 0o001 == 0 { - log::info!("{} is not a file or has no execute permission", executable); - return false; - } - - // check the shebang and ELF magic number - // https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header - let mut buffer = [0; 4]; - - let file = OpenOptions::new().read(true).open(path); - if file.is_err() { - log::info!("failed to open {}", executable); - return false; - } - let mut file = file.unwrap(); - match file.read_exact(&mut buffer) { - Ok(_) => {} - Err(err) => { - log::info!("failed to read shebang of {}: {}", executable, err); - return false; - } - } - match buffer { - // ELF magic number - [0x7f, 0x45, 0x4c, 0x46] => true, - // shebang - [0x23, 0x21, ..] => true, - _ => { - log::info!("{} is not a valid script or elf file", executable); - false - } - } -} diff --git a/crates/containerd-shim-wasm/src/libcontainer_instance/instance.rs b/crates/containerd-shim-wasm/src/libcontainer_instance/instance.rs deleted file mode 100644 index 2510d9421..000000000 --- a/crates/containerd-shim-wasm/src/libcontainer_instance/instance.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! Abstractions for running/managing a wasm/wasi instance that uses youki's libcontainer library. - -use std::path::PathBuf; -use std::thread; - -use anyhow::Context; -use chrono::Utc; -use libcontainer::container::{Container, ContainerStatus}; -use libcontainer::signal::Signal; -use log::error; -use nix::errno::Errno; -use nix::sys::wait::{waitid, Id as WaitID, WaitPidFlag, WaitStatus}; - -use crate::sandbox::error::Error; -use crate::sandbox::instance::{ExitCode, Wait}; -use crate::sandbox::instance_utils::{get_instance_root, instance_exists}; -use crate::sandbox::{Instance, InstanceConfig}; -use crate::sys::signals::{SIGINT, SIGKILL}; - -/// LibcontainerInstance is a trait that gets implemented by a WASI runtime that -/// uses youki's libcontainer library as the container runtime. -/// It provides default implementations for some of the Instance trait methods. -/// The implementor of this trait is expected to implement the -/// * `new_libcontainer()` -/// * `get_exit_code()` -/// * `get_id()` -/// * `get_root_dir()` -/// * `build_container()` -/// methods. -pub trait LibcontainerInstance { - /// The WASI engine type - type Engine: Send + Sync + Clone; - - /// Create a new instance - fn new_libcontainer(id: String, cfg: Option<&InstanceConfig>) -> Self; - - /// Get the exit code of the instance - fn get_exit_code(&self) -> ExitCode; - - /// Get the ID of the instance - fn get_id(&self) -> String; - - /// Get the root directory of the instance - fn get_root_dir(&self) -> Result; - - /// Build the container - fn build_container(&self) -> Result; -} - -/// Default implementation of the Instance trait for YoukiInstance -/// This implementation uses the libcontainer library to create and start -/// the container. -impl Instance for T { - type Engine = T::Engine; - - fn new(id: String, cfg: Option<&InstanceConfig>) -> Self { - Self::new_libcontainer(id, cfg) - } - - /// Start the instance - /// The returned value should be a unique ID (such as a PID) for the instance. - /// Nothing internally should be using this ID, but it is returned to containerd where a user may want to use it. - fn start(&self) -> Result { - let id = self.get_id(); - log::info!("starting instance: {}", id); - - let mut container = self.build_container()?; - let code = self.get_exit_code(); - let pid = container.pid().context("failed to get pid")?; - - container - .start() - .map_err(|err| Error::Any(anyhow::anyhow!("failed to start container: {}", err)))?; - - thread::spawn(move || { - let (lock, cvar) = &*code; - - let status = match waitid(WaitID::Pid(pid), WaitPidFlag::WEXITED) { - Ok(WaitStatus::Exited(_, status)) => status, - Ok(WaitStatus::Signaled(_, sig, _)) => sig as i32, - Ok(_) => 0, - Err(e) => { - if e == Errno::ECHILD { - log::info!("no child process"); - 0 - } else { - panic!("waitpid failed: {}", e); - } - } - } as u32; - let mut ec = lock.lock().unwrap(); - *ec = Some((status, Utc::now())); - drop(ec); - cvar.notify_all(); - }); - - Ok(pid.as_raw() as u32) - } - - /// Send a signal to the instance - fn kill(&self, signal: u32) -> Result<(), Error> { - let id = self.get_id(); - let root_dir = self.get_root_dir()?; - log::info!("killing instance: {}", id.clone()); - if signal as i32 != SIGKILL && signal as i32 != SIGINT { - return Err(Error::InvalidArgument( - "only SIGKILL and SIGINT are supported".to_string(), - )); - } - let signal = Signal::try_from(signal as i32) - .map_err(|err| Error::InvalidArgument(format!("invalid signal number: {}", err)))?; - let container_root = get_instance_root(root_dir, id.as_str())?; - let mut container = Container::load(container_root).with_context(|| { - format!("could not load state for container {id}", id = id.as_str()) - })?; - - match container.kill(signal, true) { - Ok(_) => Ok(()), - Err(e) => { - if container.status() == ContainerStatus::Stopped { - return Err(Error::Others("container not running".into())); - } - Err(Error::Others(e.to_string())) - } - } - } - - /// Delete any reference to the instance - /// This is called after the instance has exited. - fn delete(&self) -> Result<(), Error> { - let id = self.get_id(); - let root_dir = self.get_root_dir()?; - log::info!("deleting instance: {}", id.clone()); - match instance_exists(&root_dir, id.as_str()) { - Ok(exists) => { - if !exists { - return Ok(()); - } - } - Err(err) => { - error!("could not find the container, skipping cleanup: {}", err); - return Ok(()); - } - } - let container_root = get_instance_root(&root_dir, id.as_str())?; - let container = Container::load(container_root).with_context(|| { - format!( - "could not load state for container {id}", - id = id.clone().as_str() - ) - }); - match container { - Ok(mut container) => container.delete(true).map_err(|err| { - Error::Any(anyhow::anyhow!( - "failed to delete container {}: {}", - id, - err - )) - })?, - Err(err) => { - error!("could not find the container, skipping cleanup: {}", err); - return Ok(()); - } - } - Ok(()) - } - - /// Set up waiting for the instance to exit - /// The Wait struct is used to send the exit code and time back to the - /// caller. The recipient is expected to call function - /// set_up_exit_code_wait() implemented by Wait to set up exit code - /// processing. Note that the "wait" function doesn't block, but - /// it sets up the waiting channel. - fn wait(&self, waiter: &Wait) -> Result<(), Error> { - let id = self.get_id(); - let exit_code = self.get_exit_code(); - log::info!("waiting for instance: {}", id); - let code = exit_code; - waiter.set_up_exit_code_wait(code) - } -} diff --git a/crates/containerd-shim-wasm/src/libcontainer_instance/mod.rs b/crates/containerd-shim-wasm/src/libcontainer_instance/mod.rs deleted file mode 100644 index 7d0b07faa..000000000 --- a/crates/containerd-shim-wasm/src/libcontainer_instance/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod container_executor; -pub mod instance; - -pub use container_executor::LinuxContainerExecutor; -pub use instance::LibcontainerInstance; diff --git a/crates/containerd-shim-wasm/src/sandbox/error.rs b/crates/containerd-shim-wasm/src/sandbox/error.rs index 3eecd1f6f..8fce49f23 100644 --- a/crates/containerd-shim-wasm/src/sandbox/error.rs +++ b/crates/containerd-shim-wasm/src/sandbox/error.rs @@ -42,6 +42,10 @@ pub enum Error { #[cfg(unix)] #[error("{0}")] Errno(#[from] nix::errno::Errno), + /// Errors from libcontainer + #[cfg(all(feature = "libcontainer", not(target_os = "windows")))] + #[error("{0}")] + Libcontainer(#[from] libcontainer::error::LibcontainerError), } pub type Result = ::std::result::Result; diff --git a/crates/containerd-shim-wasm/src/sandbox/instance.rs b/crates/containerd-shim-wasm/src/sandbox/instance.rs index 32cfbc4e7..d4d71ac5b 100644 --- a/crates/containerd-shim-wasm/src/sandbox/instance.rs +++ b/crates/containerd-shim-wasm/src/sandbox/instance.rs @@ -257,20 +257,6 @@ mod noptests { Ok(()) } - #[cfg(unix)] - #[test] - fn test_op_kill_other() -> Result<(), Error> { - let nop = Nop::new("".to_string(), None); - - let err = nop.kill(SIGHUP as u32).unwrap_err(); - match err { - Error::InvalidArgument(_) => {} - _ => panic!("unexpected error: {}", err), - } - - Ok(()) - } - #[test] fn test_nop_delete_after_create() { let nop = Nop::new("".to_string(), None); diff --git a/crates/containerd-shim-wasm/src/sandbox/manager.rs b/crates/containerd-shim-wasm/src/sandbox/manager.rs index 17cfeb853..a9c48f26f 100644 --- a/crates/containerd-shim-wasm/src/sandbox/manager.rs +++ b/crates/containerd-shim-wasm/src/sandbox/manager.rs @@ -104,21 +104,18 @@ impl Manager for Service { let id = &req.id; - match thread::Builder::new() + let _ = thread::Builder::new() .name(format!("{}-sandbox-create", id)) .spawn(move || { let r = start_sandbox(cfg, &mut server); tx.send(r).context("could not send sandbox result").unwrap(); - }) { - Ok(_) => {} - Err(e) => { - return Err(Error::Others(format!("failed to spawn sandbox thread: {}", e)).into()); - } - } + }) + .context("failed to spawn sandbox thread") + .map_err(Error::from)?; rx.recv() .context("could not receive sandbox result") - .map_err(|err| Error::Others(format!("{}", err)))??; + .map_err(Error::from)??; Ok(sandbox::CreateResponse { socket_path: sock, ..Default::default() diff --git a/crates/containerd-shim-wasm/src/sandbox/mod.rs b/crates/containerd-shim-wasm/src/sandbox/mod.rs index fd3802c87..5b2e56862 100644 --- a/crates/containerd-shim-wasm/src/sandbox/mod.rs +++ b/crates/containerd-shim-wasm/src/sandbox/mod.rs @@ -15,6 +15,6 @@ pub use manager::{Sandbox as SandboxService, Service as ManagerService}; pub use shim::{Cli as ShimCli, Local}; pub use stdio::Stdio; -pub mod oci; +pub(crate) mod oci; pub mod testutil; diff --git a/crates/containerd-shim-wasm/src/sandbox/oci.rs b/crates/containerd-shim-wasm/src/sandbox/oci.rs index 46b29b7b9..b10ba0c2a 100644 --- a/crates/containerd-shim-wasm/src/sandbox/oci.rs +++ b/crates/containerd-shim-wasm/src/sandbox/oci.rs @@ -4,7 +4,6 @@ use std::collections::HashMap; use std::io::{ErrorKind, Write}; #[cfg(unix)] use std::os::unix::process::CommandExt; -use std::path::PathBuf; use std::process; use anyhow::Context; @@ -12,53 +11,6 @@ pub use oci_spec::runtime::Spec; use super::error::Result; -pub fn get_root(spec: &Spec) -> &PathBuf { - let root = spec.root().as_ref().unwrap(); - root.path() -} - -pub 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(), - } -} - -// get_module returns the module name and exported function name to be called -// from the arguments on the runtime spec process field. The first argument will -// be the module name and the default function name is "_start". -// -// If there is a '#' in the argument it will split the string -// returning the first part as the module name and the second part as the function -// name. -// -// example: "module.wasm#function" will return (Some("module.wasm"), "function") -// -// If there are no arguments then it will return (None, "_start") -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()) -} - fn parse_env(envs: &[String]) -> HashMap { // make NAME=VALUE to HashMap. envs.iter() @@ -143,126 +95,3 @@ pub fn setup_prestart_hooks(hooks: &Option) -> Result< } Ok(()) } - -#[cfg(test)] -mod oci_tests { - use oci_spec::runtime::{ProcessBuilder, RootBuilder, SpecBuilder}; - - use super::*; - - #[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-wasm/src/sandbox/shim.rs b/crates/containerd-shim-wasm/src/sandbox/shim.rs index 7d29b2e0c..77be5703c 100644 --- a/crates/containerd-shim-wasm/src/sandbox/shim.rs +++ b/crates/containerd-shim-wasm/src/sandbox/shim.rs @@ -13,6 +13,7 @@ use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::{Arc, Condvar, Mutex, RwLock}; use std::thread; +use anyhow::Context as AnyhowContext; use chrono::{DateTime, Utc}; use containerd_shim::error::Error as ShimError; use containerd_shim::event::Event; @@ -1020,7 +1021,7 @@ impl Local { let id = req.id().to_string(); let state = i.state.clone(); - thread::Builder::new() + let _ = thread::Builder::new() .name(format!("{}-wait", req.id())) .spawn(move || { let ec = rx.recv().unwrap(); @@ -1055,9 +1056,8 @@ impl Local { error!("failed to send event for topic {}: {}", topic, err) }); }) - .map_err(|err| { - Error::Others(format!("could not spawn thread to wait exit: {}", err)) - })?; + .context("could not spawn thread to wait exit") + .map_err(Error::from)?; debug!("started: {:?}", req); diff --git a/crates/containerd-shim-wasm/src/sandbox/testutil.rs b/crates/containerd-shim-wasm/src/sandbox/testutil.rs index 1fcc64d9e..1f05b22d3 100644 --- a/crates/containerd-shim-wasm/src/sandbox/testutil.rs +++ b/crates/containerd-shim-wasm/src/sandbox/testutil.rs @@ -58,7 +58,6 @@ pub fn run_test_with_sudo(test: &str) -> Result<()> { }); ensure!(cmd.wait()?.success(), "running test with sudo failed"); - Ok(()) } diff --git a/crates/containerd-shim-wasm/src/sys/unix/metrics.rs b/crates/containerd-shim-wasm/src/sys/unix/metrics.rs index ba1b03e61..4efcb514c 100644 --- a/crates/containerd-shim-wasm/src/sys/unix/metrics.rs +++ b/crates/containerd-shim-wasm/src/sys/unix/metrics.rs @@ -4,11 +4,9 @@ use protobuf::well_known_types::any::Any; use shim::cgroup::collect_metrics; use shim::util::convert_to_any; -use crate::sandbox::Error; - pub fn get_metrics(pid: u32) -> Result { let metrics = collect_metrics(pid)?; - let metrics = convert_to_any(Box::new(metrics)).map_err(|e| Error::Others(e.to_string()))?; + let metrics = convert_to_any(Box::new(metrics))?; Ok(metrics) } diff --git a/crates/containerd-shim-wasm/src/sys/unix/signals.rs b/crates/containerd-shim-wasm/src/sys/unix/signals.rs index af4f5dfa0..07a7f3565 100644 --- a/crates/containerd-shim-wasm/src/sys/unix/signals.rs +++ b/crates/containerd-shim-wasm/src/sys/unix/signals.rs @@ -1 +1 @@ -pub use libc::{SIGHUP, SIGINT, SIGKILL, SIGTERM}; +pub use libc::{SIGINT, SIGKILL, SIGTERM}; diff --git a/crates/containerd-shim-wasm/src/sys/windows/metrics.rs b/crates/containerd-shim-wasm/src/sys/windows/metrics.rs index 2a8161b8b..73b581223 100644 --- a/crates/containerd-shim-wasm/src/sys/windows/metrics.rs +++ b/crates/containerd-shim-wasm/src/sys/windows/metrics.rs @@ -4,13 +4,11 @@ use oci_spec::runtime; use protobuf::well_known_types::any::Any; use shim::util::convert_to_any; -use crate::sandbox::Error; - pub fn get_metrics(pid: u32) -> Result { // Create empty message for now // https://github.com/containerd/rust-extensions/pull/178 let m = protobuf::well_known_types::any::Any::new(); - let metrics = convert_to_any(Box::new(m)).map_err(|e| Error::Others(e.to_string()))?; + let metrics = convert_to_any(Box::new(m))?; Ok(metrics) } diff --git a/crates/containerd-shim-wasmedge/src/bin/containerd-shim-wasmedge-v1/main.rs b/crates/containerd-shim-wasmedge/src/bin/containerd-shim-wasmedge-v1/main.rs index b86bf9c6c..2dc7e1a57 100644 --- a/crates/containerd-shim-wasmedge/src/bin/containerd-shim-wasmedge-v1/main.rs +++ b/crates/containerd-shim-wasmedge/src/bin/containerd-shim-wasmedge-v1/main.rs @@ -1,9 +1,8 @@ use containerd_shim as shim; use containerd_shim_wasm::sandbox::ShimCli; -use containerd_shim_wasmedge::instance::Wasi as WasiInstance; -use containerd_shim_wasmedge::parse_version; +use containerd_shim_wasmedge::{parse_version, WasmEdgeInstance}; fn main() { parse_version(); - shim::run::>("io.containerd.wasmedge.v1", None); + shim::run::>("io.containerd.wasmedge.v1", None); } diff --git a/crates/containerd-shim-wasmedge/src/bin/containerd-wasmedged/main.rs b/crates/containerd-shim-wasmedge/src/bin/containerd-wasmedged/main.rs index 89e52bcab..5cd152e87 100644 --- a/crates/containerd-shim-wasmedge/src/bin/containerd-wasmedged/main.rs +++ b/crates/containerd-shim-wasmedge/src/bin/containerd-wasmedged/main.rs @@ -2,13 +2,12 @@ use std::sync::Arc; use containerd_shim_wasm::sandbox::{Local, ManagerService}; use containerd_shim_wasm::services::sandbox_ttrpc::{create_manager, Manager}; -use containerd_shim_wasmedge::instance::Wasi as WasiInstance; -use log::info; +use containerd_shim_wasmedge::WasmEdgeInstance; use ttrpc::{self, Server}; fn main() { - info!("starting up!"); - let s: ManagerService> = Default::default(); + log::info!("starting up!"); + let s: ManagerService> = Default::default(); let s = Arc::new(Box::new(s) as Box); let service = create_manager(s); @@ -18,7 +17,7 @@ fn main() { .register_service(service); server.start().unwrap(); - info!("server started!"); + log::info!("server started!"); let (_tx, rx) = std::sync::mpsc::channel::<()>(); rx.recv().unwrap(); } diff --git a/crates/containerd-shim-wasmedge/src/error.rs b/crates/containerd-shim-wasmedge/src/error.rs deleted file mode 100644 index 657e5250b..000000000 --- a/crates/containerd-shim-wasmedge/src/error.rs +++ /dev/null @@ -1,13 +0,0 @@ -use anyhow; -use containerd_shim_wasm::sandbox::error; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum WasmRuntimeError { - #[error("{0}")] - Error(#[from] error::Error), - #[error("{0}")] - AnyError(#[from] anyhow::Error), - #[error("{0}")] - Wasmedge(#[from] Box), -} diff --git a/crates/containerd-shim-wasmedge/src/executor.rs b/crates/containerd-shim-wasmedge/src/executor.rs deleted file mode 100644 index 9a1a3e67b..000000000 --- a/crates/containerd-shim-wasmedge/src/executor.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::path::PathBuf; - -use anyhow::Result; -use containerd_shim_wasm::libcontainer_instance::LinuxContainerExecutor; -use containerd_shim_wasm::sandbox::{oci, Stdio}; -use libcontainer::workload::{Executor, ExecutorError, ExecutorValidationError}; -use log::debug; -use oci_spec::runtime::Spec; -use wasmedge_sdk::config::{CommonConfigOptions, ConfigBuilder, HostRegistrationConfigOptions}; -use wasmedge_sdk::{params, VmBuilder}; - -const EXECUTOR_NAME: &str = "wasmedge"; - -#[derive(Clone)] -pub struct WasmEdgeExecutor { - stdio: Stdio, -} - -impl WasmEdgeExecutor { - pub fn new(stdio: Stdio) -> Self { - Self { stdio } - } -} - -impl Executor for WasmEdgeExecutor { - fn exec(&self, spec: &Spec) -> Result<(), ExecutorError> { - match can_handle(spec) { - Ok(()) => { - // parse wasi parameters - let args = oci::get_args(spec); - if args.is_empty() { - return Err(ExecutorError::InvalidArg); - } - - let vm = self.prepare(args, spec).map_err(|err| { - ExecutorError::Other(format!("failed to prepare function: {}", err)) - })?; - - let (module_name, method) = oci::get_module(spec); - debug!("running {:?} with method {}", module_name, method); - if let Err(err) = vm.run_func(Some("main"), method, params!()) { - log::info!("failed to execute function: {err}"); - std::process::exit(137); - } - - let status = vm - .wasi_module() - .map(|module| module.exit_code()) - .unwrap_or(137); - std::process::exit(status as i32); - } - Err(ExecutorValidationError::CantHandle(_)) => { - LinuxContainerExecutor::new(self.stdio.clone()).exec(spec)?; - Ok(()) - } - Err(_) => Err(ExecutorError::InvalidArg), - } - } - - fn validate(&self, spec: &Spec) -> std::result::Result<(), ExecutorValidationError> { - match can_handle(spec) { - Ok(()) => Ok(()), - Err(ExecutorValidationError::CantHandle(_)) => { - LinuxContainerExecutor::new(self.stdio.clone()).validate(spec)?; - - Ok(()) - } - Err(err) => Err(err), - } - } -} - -impl WasmEdgeExecutor { - fn prepare(&self, args: &[String], spec: &Spec) -> anyhow::Result { - let envs = env_to_wasi(spec); - let config = ConfigBuilder::new(CommonConfigOptions::default()) - .with_host_registration_config(HostRegistrationConfigOptions::default().wasi(true)) - .build() - .map_err(|err| ExecutorError::Execution(err))?; - let mut vm = VmBuilder::new() - .with_config(config) - .build() - .map_err(|err| ExecutorError::Execution(err))?; - let wasi_module = vm - .wasi_module_mut() - .ok_or_else(|| anyhow::Error::msg("Not found wasi module")) - .map_err(|err| ExecutorError::Execution(err.into()))?; - wasi_module.initialize( - Some(args.iter().map(|s| s as &str).collect()), - Some(envs.iter().map(|s| s as &str).collect()), - Some(vec!["/:/"]), - ); - - 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", module_name) - .map_err(|err| ExecutorError::Execution(err))?; - - self.stdio.take().redirect()?; - - Ok(vm) - } -} - -fn env_to_wasi(spec: &Spec) -> Vec { - let default = vec![]; - let env = spec - .process() - .as_ref() - .unwrap() - .env() - .as_ref() - .unwrap_or(&default); - env.to_vec() -} - -fn can_handle(spec: &Spec) -> Result<(), ExecutorValidationError> { - // check if the entrypoint of the spec is a wasm binary. - let (module_name, _method) = oci::get_module(spec); - let module_name = match module_name { - Some(m) => m, - None => { - log::info!("WasmEdge cannot handle this workload, no arguments provided"); - return Err(ExecutorValidationError::CantHandle(EXECUTOR_NAME)); - } - }; - let path = PathBuf::from(module_name); - - path.extension() - .map(|ext| ext.to_ascii_lowercase()) - .is_some_and(|ext| ext == "wasm" || ext == "wat") - .then_some(()) - .ok_or(ExecutorValidationError::CantHandle(EXECUTOR_NAME))?; - - Ok(()) -} diff --git a/crates/containerd-shim-wasmedge/src/instance/instance_linux.rs b/crates/containerd-shim-wasmedge/src/instance/instance_linux.rs deleted file mode 100644 index 82a6790b9..000000000 --- a/crates/containerd-shim-wasmedge/src/instance/instance_linux.rs +++ /dev/null @@ -1,236 +0,0 @@ -use std::fs; -use std::path::PathBuf; -use std::sync::{Arc, Condvar, Mutex}; - -use containerd_shim_wasm::libcontainer_instance::LibcontainerInstance; -use containerd_shim_wasm::sandbox::error::Error; -use containerd_shim_wasm::sandbox::instance::ExitCode; -use containerd_shim_wasm::sandbox::instance_utils::determine_rootdir; -use containerd_shim_wasm::sandbox::{InstanceConfig, Stdio}; -use libcontainer::container::builder::ContainerBuilder; -use libcontainer::container::Container; -use libcontainer::syscall::syscall::SyscallType; - -use crate::executor::WasmEdgeExecutor; - -static DEFAULT_CONTAINER_ROOT_DIR: &str = "/run/containerd/wasmedge"; - -pub struct Wasi { - id: String, - exit_code: ExitCode, - stdio: Stdio, - bundle: String, - rootdir: PathBuf, -} - -impl LibcontainerInstance for Wasi { - type Engine = (); - - fn new_libcontainer(id: String, cfg: Option<&InstanceConfig>) -> Self { - let cfg = cfg.unwrap(); // TODO: handle error - let bundle = cfg.get_bundle().unwrap_or_default(); - let namespace = cfg.get_namespace(); - Wasi { - id, - rootdir: determine_rootdir( - bundle.as_str(), - namespace.as_str(), - DEFAULT_CONTAINER_ROOT_DIR, - ) - .unwrap(), - exit_code: Arc::new((Mutex::new(None), Condvar::new())), - stdio: Stdio::init_from_cfg(cfg).expect("failed to open stdio"), - bundle, - } - } - - fn get_exit_code(&self) -> ExitCode { - self.exit_code.clone() - } - - fn get_id(&self) -> String { - self.id.clone() - } - - fn get_root_dir(&self) -> std::result::Result { - Ok(self.rootdir.clone()) - } - - fn build_container(&self) -> std::result::Result { - fs::create_dir_all(&self.rootdir)?; - - let err_others = |err| Error::Others(format!("failed to create container: {}", err)); - let container = ContainerBuilder::new(self.id.clone(), SyscallType::Linux) - .with_executor(WasmEdgeExecutor::new(self.stdio.take())) - .with_root_path(self.rootdir.clone()) - .map_err(err_others)? - .as_init(&self.bundle) - .with_systemd(false) - .build() - .map_err(err_others)?; - - Ok(container) - } -} - -#[cfg(test)] -mod wasitest { - - use std::fs::read_to_string; - - 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; - use serial_test::serial; - use tempfile::tempdir; - use wasmedge_sdk::wat2wasm; - - 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_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 - ) - ) - "#.as_bytes(); - - 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() - } - - const WASI_RETURN_ERROR: &[u8] = r#"(module - (func $main (export "_start") - (unreachable) - ) - ) - "# - .as_bytes(); - - #[test] - #[serial] - fn test_delete_after_create() { - let i = Wasi::new( - "".to_string(), - Some(&InstanceConfig::new( - (), - "test_namespace".into(), - "/containerd/address".into(), - )), - ); - i.delete().unwrap(); - } - - #[test] - #[serial] - fn test_wasi() -> anyhow::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 = wat2wasm(WASI_HELLO_WAT).unwrap(); - - 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"); - - Ok(()) - } - - #[test] - #[serial] - fn test_wasi_error() -> anyhow::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 wasm_bytes = wat2wasm(WASI_RETURN_ERROR).unwrap(); - - let res = run_wasi_test::(&dir, wasm_bytes, None)?; - - // Expect error code from the run. - assert_eq!(res.0, 137); - - Ok(()) - } - - #[test] - #[serial] - fn test_wasi_exit_code() -> anyhow::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 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)?; - - assert_eq!(actual_exit_code, expected_exit_code); - - Ok(()) - } -} diff --git a/crates/containerd-shim-wasmedge/src/instance/instance_windows.rs b/crates/containerd-shim-wasmedge/src/instance/instance_windows.rs deleted file mode 100644 index daa4f839c..000000000 --- a/crates/containerd-shim-wasmedge/src/instance/instance_windows.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::path::PathBuf; - -use containerd_shim_wasm::sandbox::error::Error; -use containerd_shim_wasm::sandbox::instance::{ExitCode, Wait}; -use containerd_shim_wasm::sandbox::{Instance, InstanceConfig, Stdio}; - -pub struct Wasi { - id: String, - - exit_code: ExitCode, - - stdio: Stdio, - bundle: String, - - rootdir: PathBuf, -} - -impl Instance for Wasi { - type Engine = (); - - fn new(id: String, cfg: Option<&InstanceConfig>) -> Self { - todo!() - } - - fn start(&self) -> std::result::Result { - todo!() - } - - fn kill(&self, signal: u32) -> std::result::Result<(), Error> { - todo!() - } - - fn delete(&self) -> std::result::Result<(), Error> { - todo!() - } - - fn wait(&self, waiter: &Wait) -> std::result::Result<(), Error> { - todo!() - } -} diff --git a/crates/containerd-shim-wasmedge/src/instance_linux.rs b/crates/containerd-shim-wasmedge/src/instance_linux.rs new file mode 100644 index 000000000..e2757ac81 --- /dev/null +++ b/crates/containerd-shim-wasmedge/src/instance_linux.rs @@ -0,0 +1,78 @@ +use anyhow::{Context, Result}; +use containerd_shim_wasm::container::{ + Engine, Instance, PathResolve, RuntimeContext, Stdio, WasiEntrypoint, +}; +use wasmedge_sdk::config::{ConfigBuilder, HostRegistrationConfigOptions}; +use wasmedge_sdk::plugin::PluginManager; +use wasmedge_sdk::VmBuilder; + +pub type WasmEdgeInstance = Instance; + +#[derive(Clone)] +pub struct WasmEdgeEngine { + vm: wasmedge_sdk::Vm, +} + +impl Default for WasmEdgeEngine { + fn default() -> Self { + PluginManager::load(None).unwrap(); + + let host_options = HostRegistrationConfigOptions::default(); + let host_options = host_options.wasi(true); + #[cfg(all(target_os = "linux", feature = "wasi_nn", target_arch = "x86_64"))] + let host_options = host_options.wasi_nn(true); + + let config = ConfigBuilder::default() + .with_host_registration_config(host_options) + .build() + .unwrap(); + let vm = VmBuilder::new().with_config(config).build().unwrap(); + Self { vm } + } +} + +impl Engine for WasmEdgeEngine { + fn name() -> &'static str { + "wasmedge" + } + + fn run_wasi(&self, ctx: &impl RuntimeContext, stdio: Stdio) -> Result { + let args = ctx.args(); + let envs: Vec<_> = std::env::vars().map(|(k, v)| format!("{k}={v}")).collect(); + let WasiEntrypoint { path, func } = ctx.wasi_entrypoint(); + let path = path + .resolve_in_path_or_cwd() + .next() + .context("module not found")?; + + let mut vm = self.vm.clone(); + vm.wasi_module_mut() + .context("Not found wasi module")? + .initialize( + Some(args.iter().map(String::as_str).collect()), + Some(envs.iter().map(String::as_str).collect()), + Some(vec!["/:/"]), + ); + + let mod_name = match path.file_stem() { + Some(name) => name.to_string_lossy().to_string(), + None => "main".to_string(), + }; + + let vm = vm + .register_module_from_file(&mod_name, &path) + .context("registering module")?; + + stdio.redirect()?; + + log::debug!("running {path:?} with method {func:?}"); + vm.run_func(Some(&mod_name), func, vec![])?; + + let status = vm + .wasi_module() + .context("Not found wasi module")? + .exit_code(); + + Ok(status as i32) + } +} diff --git a/crates/containerd-shim-wasmedge/src/instance_windows.rs b/crates/containerd-shim-wasmedge/src/instance_windows.rs new file mode 100644 index 000000000..cf872b77d --- /dev/null +++ b/crates/containerd-shim-wasmedge/src/instance_windows.rs @@ -0,0 +1,28 @@ +use containerd_shim_wasm::sandbox::instance::Wait; +use containerd_shim_wasm::sandbox::{Instance, InstanceConfig, Result, Stdio}; + +pub struct WasmEdgeInstance {} + +impl Instance for WasmEdgeInstance { + type Engine = (); + + fn new(id: String, cfg: Option<&InstanceConfig>) -> Self { + todo!() + } + + fn start(&self) -> Result { + todo!() + } + + fn kill(&self, signal: u32) -> Result<()> { + todo!() + } + + fn delete(&self) -> Result<()> { + todo!() + } + + fn wait(&self, waiter: &Wait) -> Result<()> { + todo!() + } +} diff --git a/crates/containerd-shim-wasmedge/src/lib.rs b/crates/containerd-shim-wasmedge/src/lib.rs index a6aa4913d..1cbc33585 100644 --- a/crates/containerd-shim-wasmedge/src/lib.rs +++ b/crates/containerd-shim-wasmedge/src/lib.rs @@ -2,15 +2,11 @@ use std::env; use containerd_shim::parse; -pub mod error; - -#[cfg_attr(unix, path = "instance/instance_linux.rs")] -#[cfg_attr(windows, path = "instance/instance_windows.rs")] +#[cfg_attr(unix, path = "instance_linux.rs")] +#[cfg_attr(windows, path = "instance_windows.rs")] pub mod instance; -pub mod oci_utils; -#[cfg(unix)] -pub mod executor; +pub use instance::WasmEdgeInstance; pub fn parse_version() { let os_args: Vec<_> = env::args_os().collect(); @@ -27,37 +23,4 @@ pub fn parse_version() { #[cfg(unix)] #[cfg(test)] -mod test { - use std::os::unix::prelude::OsStrExt; - - // Get the path to binary where the `WasmEdge_VersionGet` C ffi symbol is defined. - // If wasmedge is dynamically linked, this will be the path to the `.so`. - // If wasmedge is statically linked, this will be the path to the current executable. - fn get_wasmedge_binary_path() -> Option { - 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 { - None - } else { - let fname = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) }; - let fname = std::ffi::OsStr::from_bytes(fname.to_bytes()); - Some(std::path::PathBuf::from(fname)) - } - } - - #[cfg(feature = "static")] - #[test] - fn check_static_linking() { - let wasmedge_path = get_wasmedge_binary_path().unwrap().canonicalize().unwrap(); - let current_exe = std::env::current_exe().unwrap().canonicalize().unwrap(); - assert!(wasmedge_path == current_exe); - } - - #[cfg(not(feature = "static"))] - #[test] - fn check_dynamic_linking() { - let wasmedge_path = get_wasmedge_binary_path().unwrap().canonicalize().unwrap(); - let current_exe = std::env::current_exe().unwrap().canonicalize().unwrap(); - assert!(wasmedge_path != current_exe); - } -} +mod tests; diff --git a/crates/containerd-shim-wasmedge/src/oci_utils.rs b/crates/containerd-shim-wasmedge/src/oci_utils.rs deleted file mode 100644 index 1630d1fb3..000000000 --- a/crates/containerd-shim-wasmedge/src/oci_utils.rs +++ /dev/null @@ -1,31 +0,0 @@ -use oci_spec::runtime::Spec; - -pub fn env_to_wasi(spec: &Spec) -> Vec { - let default = vec![]; - let env = spec - .process() - .as_ref() - .unwrap() - .env() - .as_ref() - .unwrap_or(&default); - env.to_vec() -} - -pub fn get_wasm_mounts(spec: &Spec) -> Vec<&str> { - let mounts: Vec<&str> = match spec.mounts() { - Some(mounts) => mounts - .iter() - .filter_map(|mount| { - if let Some(typ) = mount.typ() { - if typ == "bind" || typ == "tmpfs" { - return mount.destination().to_str(); - } - } - None - }) - .collect(), - _ => vec![], - }; - mounts -} diff --git a/crates/containerd-shim-wasmedge/src/tests.rs b/crates/containerd-shim-wasmedge/src/tests.rs new file mode 100644 index 000000000..a9add28fc --- /dev/null +++ b/crates/containerd-shim-wasmedge/src/tests.rs @@ -0,0 +1,218 @@ +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() +} + +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() +} + +const WASI_RETURN_ERROR: &[u8] = r#"(module + (func $main (export "_start") + (unreachable) + ) +) +"# +.as_bytes(); + +#[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()?; + + 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)?; + + assert_eq!(res.0, 0); + + let output = read_to_string(path.join("stdout"))?; + assert_eq!(output, "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(); + + let dir = tempdir()?; + let path = dir.path(); + let wasm_bytes = hello_world_module(Some("foo")); + + let res = run_wasi_test::(&dir, wasm_bytes, Some("foo"))?; + + assert_eq!(res.0, 0); + + let output = read_to_string(path.join("stdout"))?; + assert_eq!(output, "hello world\n"); + + 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)?; + + // Expect error code from the run. + assert_eq!(res.0, 137); + + 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!()); + } + + // start logging + let _ = env_logger::try_init(); + let _guard = Stdio::init_from_std().guard(); + + 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)?; + + assert_eq!(actual_exit_code, expected_exit_code); + + Ok(()) +} + +// Get the path to binary where the `WasmEdge_VersionGet` C ffi symbol is defined. +// If wasmedge is dynamically linked, this will be the path to the `.so`. +// 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 { + None + } else { + let fname = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) }; + let fname = std::ffi::OsStr::from_bytes(fname.to_bytes()); + Some(std::path::PathBuf::from(fname)) + } +} + +#[cfg(feature = "static")] +#[test] +fn check_static_linking() { + let wasmedge_path = get_wasmedge_binary_path().unwrap().canonicalize().unwrap(); + let current_exe = std::env::current_exe().unwrap().canonicalize().unwrap(); + assert!(wasmedge_path == current_exe); +} + +#[cfg(not(feature = "static"))] +#[test] +fn check_dynamic_linking() { + let wasmedge_path = get_wasmedge_binary_path().unwrap().canonicalize().unwrap(); + let current_exe = std::env::current_exe().unwrap().canonicalize().unwrap(); + assert!(wasmedge_path != current_exe); +} diff --git a/crates/containerd-shim-wasmer/build.rs b/crates/containerd-shim-wasmer/build.rs new file mode 100644 index 000000000..e5e6ae0b0 --- /dev/null +++ b/crates/containerd-shim-wasmer/build.rs @@ -0,0 +1,24 @@ +use std::process::Command; +use std::str::from_utf8; + +fn main() { + let output = match Command::new("git").arg("rev-parse").arg("HEAD").output() { + Ok(output) => output, + Err(_) => { + return; + } + }; + let mut hash = from_utf8(&output.stdout).unwrap().trim().to_string(); + + let output_dirty = match Command::new("git").arg("diff").arg("--exit-code").output() { + Ok(output) => output, + Err(_) => { + return; + } + }; + + if !output_dirty.status.success() { + hash.push_str(".m"); + } + println!("cargo:rustc-env=CARGO_GIT_HASH={}", hash); +} diff --git a/crates/containerd-shim-wasmer/src/bin/containerd-shim-wasmer-v1/main.rs b/crates/containerd-shim-wasmer/src/bin/containerd-shim-wasmer-v1/main.rs index acd35f89a..a3894960a 100644 --- a/crates/containerd-shim-wasmer/src/bin/containerd-shim-wasmer-v1/main.rs +++ b/crates/containerd-shim-wasmer/src/bin/containerd-shim-wasmer-v1/main.rs @@ -1,7 +1,8 @@ use containerd_shim as shim; use containerd_shim_wasm::sandbox::ShimCli; -use containerd_shim_wasmer::instance::Wasi as WasiInstance; +use containerd_shim_wasmer::{parse_version, WasmerInstance}; fn main() { - shim::run::>("io.containerd.wasmer.v1", None); + parse_version(); + shim::run::>("io.containerd.wasmer.v1", None); } diff --git a/crates/containerd-shim-wasmer/src/bin/containerd-shim-wasmerd-v1/main.rs b/crates/containerd-shim-wasmer/src/bin/containerd-shim-wasmerd-v1/main.rs index 6016a1d8e..c4392b3a1 100644 --- a/crates/containerd-shim-wasmer/src/bin/containerd-shim-wasmerd-v1/main.rs +++ b/crates/containerd-shim-wasmer/src/bin/containerd-shim-wasmerd-v1/main.rs @@ -1,6 +1,8 @@ use containerd_shim as shim; use containerd_shim_wasm::sandbox::manager::Shim; +use containerd_shim_wasmer::parse_version; fn main() { + parse_version(); shim::run::("containerd-shim-wasmerd-v1", None) } diff --git a/crates/containerd-shim-wasmer/src/bin/containerd-wasmerd/main.rs b/crates/containerd-shim-wasmer/src/bin/containerd-wasmerd/main.rs index ef2cdbafc..b693d654b 100644 --- a/crates/containerd-shim-wasmer/src/bin/containerd-wasmerd/main.rs +++ b/crates/containerd-shim-wasmer/src/bin/containerd-wasmerd/main.rs @@ -2,13 +2,13 @@ use std::sync::Arc; use containerd_shim_wasm::sandbox::{Local, ManagerService}; use containerd_shim_wasm::services::sandbox_ttrpc::{create_manager, Manager}; -use containerd_shim_wasmer::instance::Wasi as WasiInstance; +use containerd_shim_wasmer::WasmerInstance; use log::info; use ttrpc::{self, Server}; fn main() { info!("starting up!"); - let s: ManagerService> = ManagerService::default(); + let s: ManagerService> = ManagerService::default(); let s = Arc::new(Box::new(s) as Box); let service = create_manager(s); diff --git a/crates/containerd-shim-wasmer/src/executor.rs b/crates/containerd-shim-wasmer/src/executor.rs deleted file mode 100644 index 87535b809..000000000 --- a/crates/containerd-shim-wasmer/src/executor.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::path::PathBuf; - -use containerd_shim_wasm::libcontainer_instance::LinuxContainerExecutor; -use containerd_shim_wasm::sandbox::oci::{self, Spec}; -use containerd_shim_wasm::sandbox::Stdio; -use libcontainer::workload::{Executor, ExecutorError, ExecutorValidationError}; -use wasmer::{Cranelift, Module, Store}; -use wasmer_wasix::{WasiEnv, WasiError}; - -const EXECUTOR_NAME: &str = "wasmer"; - -#[derive(Clone)] -pub struct WasmerExecutor { - stdio: Stdio, - engine: Cranelift, -} - -impl WasmerExecutor { - pub fn new(stdio: Stdio, engine: Cranelift) -> Self { - Self { stdio, engine } - } -} - -impl Executor for WasmerExecutor { - fn exec(&self, spec: &Spec) -> Result<(), ExecutorError> { - match can_handle(spec) { - Ok(()) => { - let args = oci::get_args(spec); - if args.is_empty() { - return Err(ExecutorError::InvalidArg); - } - - let status = self - .start(spec, args) - .map_err(|err| ExecutorError::Other(format!("failed to prepare: {}", err)))?; - - std::process::exit(status) - } - Err(ExecutorValidationError::CantHandle(_)) => { - LinuxContainerExecutor::new(self.stdio.take()).exec(spec)?; - - Ok(()) - } - Err(_) => Err(ExecutorError::InvalidArg), - } - } - - fn validate(&self, spec: &Spec) -> std::result::Result<(), ExecutorValidationError> { - match can_handle(spec) { - Ok(()) => Ok(()), - Err(ExecutorValidationError::CantHandle(_)) => { - LinuxContainerExecutor::new(self.stdio.clone()).validate(spec)?; - - Ok(()) - } - Err(err) => Err(err), - } - } -} - -impl WasmerExecutor { - fn start(&self, spec: &Spec, args: &[String]) -> anyhow::Result { - log::info!("get envs from spec"); - let envs = std::env::vars(); - - log::info!("redirect stdio"); - self.stdio.take().redirect()?; - - log::info!("get module_name and method"); - 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!("Create a Store"); - let mut store = Store::new(self.engine.clone()); - - log::info!("loading module from file {} ", module_name); - let module = Module::from_file(&store, module_name)?; - - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build()?; - let _guard = runtime.enter(); - - log::info!("Creating `WasiEnv`...: args {:?}, envs: {:?}", args, envs); - let (instance, wasi_env) = WasiEnv::builder(EXECUTOR_NAME) - .args(&args[1..]) - .envs(envs) - .preopen_dir("/")? - .instantiate(module, &mut store)?; - - log::info!("Running {method:?}"); - let start = instance.exports.get_function(&method)?; - wasi_env.data(&store).thread.set_status_running(); - let status = start.call(&mut store, &[]).map(|_| 0).or_else(|err| { - match err.downcast_ref::() { - Some(WasiError::Exit(code)) => Ok(code.raw()), - _ => Err(err), - } - })?; - - Ok(status) - } -} - -fn can_handle(spec: &Spec) -> Result<(), ExecutorValidationError> { - // check if the entrypoint of the spec is a wasm binary. - let (module_name, _method) = oci::get_module(spec); - let module_name = match module_name { - Some(m) => m, - None => { - log::info!("Wasmtime cannot handle this workload, no arguments provided"); - return Err(ExecutorValidationError::CantHandle(EXECUTOR_NAME)); - } - }; - let path = PathBuf::from(module_name); - - // TODO: do we need to validate the wasm binary? - // ```rust - // let bytes = std::fs::read(path).unwrap(); - // wasmparser::validate(&bytes).is_ok() - // ``` - - path.extension() - .map(|ext| ext.to_ascii_lowercase()) - .is_some_and(|ext| ext == "wasm" || ext == "wat") - .then_some(()) - .ok_or(ExecutorValidationError::CantHandle(EXECUTOR_NAME))?; - - Ok(()) -} diff --git a/crates/containerd-shim-wasmer/src/instance/instance_linux.rs b/crates/containerd-shim-wasmer/src/instance/instance_linux.rs deleted file mode 100644 index 55ebefa10..000000000 --- a/crates/containerd-shim-wasmer/src/instance/instance_linux.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::path::PathBuf; -use std::sync::{Arc, Condvar, Mutex}; - -use anyhow::Result; -use containerd_shim_wasm::libcontainer_instance::LibcontainerInstance; -use containerd_shim_wasm::sandbox::instance::ExitCode; -use containerd_shim_wasm::sandbox::instance_utils::determine_rootdir; -use containerd_shim_wasm::sandbox::{Error, InstanceConfig, Stdio}; -use libcontainer::container::builder::ContainerBuilder; -use libcontainer::container::Container; -use libcontainer::syscall::syscall::SyscallType; -use serde::{Deserialize, Serialize}; - -use crate::executor::WasmerExecutor; - -static DEFAULT_CONTAINER_ROOT_DIR: &str = "/run/containerd/wasmer"; - -pub struct Wasi { - exit_code: ExitCode, - engine: wasmer::Cranelift, - stdio: Stdio, - bundle: String, - rootdir: PathBuf, - id: String, -} - -impl LibcontainerInstance for Wasi { - type Engine = wasmer::Cranelift; - - fn new_libcontainer(id: String, cfg: Option<&InstanceConfig>) -> Self { - // TODO: there are failure cases e.x. parsing cfg, loading spec, etc. - // thus should make `new` return `Result` instead of `Self` - log::info!("creating new instance: {}", id); - let cfg = cfg.unwrap(); - let bundle = cfg.get_bundle().unwrap_or_default(); - let rootdir = determine_rootdir( - bundle.as_str(), - &cfg.get_namespace(), - DEFAULT_CONTAINER_ROOT_DIR, - ) - .unwrap(); - Wasi { - id, - exit_code: Arc::new((Mutex::new(None), Condvar::new())), - engine: cfg.get_engine(), - stdio: Stdio::init_from_cfg(cfg).expect("failed to open stdio"), - bundle, - rootdir, - } - } - - fn build_container(&self) -> Result { - let engine = self.engine.clone(); - let err_others = |err| Error::Others(format!("failed to create container: {}", err)); - - let container = ContainerBuilder::new(self.id.clone(), SyscallType::Linux) - .with_executor(WasmerExecutor::new(self.stdio.take().clone(), engine)) - .with_root_path(self.rootdir.clone()) - .map_err(err_others)? - .as_init(&self.bundle) - .with_systemd(false) - .build() - .map_err(err_others)?; - - Ok(container) - } - - fn get_exit_code(&self) -> ExitCode { - self.exit_code.clone() - } - - fn get_id(&self) -> String { - self.id.clone() - } - - fn get_root_dir(&self) -> Result { - Ok(self.rootdir.clone()) - } -} - -#[derive(Serialize, Deserialize)] -struct Options { - root: Option, -} - -#[cfg(test)] -mod wasitest { - - use std::fs::read_to_string; - use std::os::fd::RawFd; - - 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; - use libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; - use nix::unistd::dup2; - use serial_test::serial; - use tempfile::tempdir; - - use super::*; - - static mut STDIN_FD: Option = None; - static mut STDOUT_FD: Option = None; - static mut STDERR_FD: Option = None; - - fn reset_stdio() { - unsafe { - if let Some(stdin) = STDIN_FD { - let _ = dup2(stdin, STDIN_FILENO); - } - if let Some(stdout) = STDOUT_FD { - let _ = dup2(stdout, STDOUT_FILENO); - } - if let Some(stderr) = STDERR_FD { - let _ = dup2(stderr, STDERR_FILENO); - } - } - } - - // 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() - } - - 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() - } - - #[test] - #[serial] - fn test_delete_after_create() -> anyhow::Result<()> { - let cfg = InstanceConfig::new( - Default::default(), - "test_namespace".into(), - "/containerd/address".into(), - ); - - let i = Wasi::new("".to_string(), Some(&cfg)); - i.delete()?; - reset_stdio(); - Ok(()) - } - - #[test] - #[serial] - fn test_wasi_entrypoint() -> anyhow::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 dir = tempdir()?; - let path = dir.path(); - let wasm_bytes = hello_world_module(None); - - 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"); - - reset_stdio(); - Ok(()) - } - - // ignore until https://github.com/containerd/runwasi/issues/194 is resolved - #[test] - #[serial] - fn test_wasi_custom_entrypoint() -> anyhow::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 dir = tempdir()?; - let path = dir.path(); - let wasm_bytes = hello_world_module(Some("foo")); - - let res = run_wasi_test::(&dir, wasm_bytes, Some("foo"))?; - - assert_eq!(res.0, 0); - - let output = read_to_string(path.join("stdout"))?; - assert_eq!(output, "hello world\n"); - - reset_stdio(); - Ok(()) - } - - #[test] - #[serial] - fn test_wasi_exit_code() -> anyhow::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 expected_exit_code: u32 = 42; - - let dir = tempdir()?; - let wasm_bytes = module_with_exit_code(expected_exit_code); - log::info!("{:?}", wasm_bytes); - let (actual_exit_code, _) = run_wasi_test::(&dir, wasm_bytes, None)?; - - assert_eq!(actual_exit_code, expected_exit_code); - - reset_stdio(); - Ok(()) - } -} diff --git a/crates/containerd-shim-wasmer/src/instance/instance_windows.rs b/crates/containerd-shim-wasmer/src/instance/instance_windows.rs deleted file mode 100644 index cb7a5ee96..000000000 --- a/crates/containerd-shim-wasmer/src/instance/instance_windows.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::path::PathBuf; - -use containerd_shim_wasm::sandbox::error::Error; -use containerd_shim_wasm::sandbox::instance::{ExitCode, Wait}; -use containerd_shim_wasm::sandbox::{Instance, InstanceConfig, Stdio}; - -pub struct Wasi { - id: String, - exit_code: ExitCode, - engine: wasmer::Cranelift, - stdio: Stdio, - bundle: String, - rootdir: PathBuf, -} - -impl Instance for Wasi { - type Engine = wasmer::Cranelift; - - fn new(id: String, cfg: Option<&InstanceConfig>) -> Self { - todo!() - } - - fn start(&self) -> std::result::Result { - todo!() - } - - fn kill(&self, signal: u32) -> std::result::Result<(), Error> { - todo!() - } - - fn delete(&self) -> std::result::Result<(), Error> { - todo!() - } - - fn wait(&self, waiter: &Wait) -> std::result::Result<(), Error> { - todo!() - } -} diff --git a/crates/containerd-shim-wasmer/src/instance_linux.rs b/crates/containerd-shim-wasmer/src/instance_linux.rs new file mode 100644 index 000000000..6161c46df --- /dev/null +++ b/crates/containerd-shim-wasmer/src/instance_linux.rs @@ -0,0 +1,67 @@ +use anyhow::{Context, Result}; +use containerd_shim_wasm::container::{ + Engine, Instance, PathResolve, RuntimeContext, Stdio, WasiEntrypoint, +}; +use wasmer::{Module, Store}; +use wasmer_wasix::{WasiEnv, WasiError}; + +pub type WasmerInstance = Instance; + +#[derive(Clone, Default)] +pub struct WasmerEngine { + engine: wasmer::Cranelift, +} + +impl Engine for WasmerEngine { + fn name() -> &'static str { + "wasmer" + } + + 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 path = path + .resolve_in_path_or_cwd() + .next() + .context("module not found")?; + + let mod_name = match path.file_stem() { + Some(name) => name.to_string_lossy().to_string(), + None => "main".to_string(), + }; + + log::info!("redirect stdio"); + stdio.redirect()?; + + log::info!("Create a Store"); + let mut store = Store::new(self.engine.clone()); + + log::info!("loading module from file {path:?}"); + let module = Module::from_file(&store, path)?; + + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + let _guard = runtime.enter(); + + log::info!("Creating `WasiEnv`...: args {:?}, envs: {:?}", args, envs); + let (instance, wasi_env) = WasiEnv::builder(mod_name) + .args(&args[1..]) + .envs(envs) + .preopen_dir("/")? + .instantiate(module, &mut store)?; + + log::info!("Running {func:?}"); + let start = instance.exports.get_function(&func)?; + wasi_env.data(&store).thread.set_status_running(); + let status = start.call(&mut store, &[]).map(|_| 0).or_else(|err| { + match err.downcast_ref::() { + Some(WasiError::Exit(code)) => Ok(code.raw()), + _ => Err(err), + } + })?; + + Ok(status) + } +} diff --git a/crates/containerd-shim-wasmer/src/instance_windows.rs b/crates/containerd-shim-wasmer/src/instance_windows.rs new file mode 100644 index 000000000..393a587d3 --- /dev/null +++ b/crates/containerd-shim-wasmer/src/instance_windows.rs @@ -0,0 +1,28 @@ +use containerd_shim_wasm::sandbox::instance::Wait; +use containerd_shim_wasm::sandbox::{Instance, InstanceConfig, Result, Stdio}; + +pub struct WasmerInstance {} + +impl Instance for WasmerInstance { + type Engine = (); + + fn new(id: String, cfg: Option<&InstanceConfig>) -> Self { + todo!() + } + + fn start(&self) -> Result { + todo!() + } + + fn kill(&self, signal: u32) -> Result<()> { + todo!() + } + + fn delete(&self) -> Result<()> { + todo!() + } + + fn wait(&self, waiter: &Wait) -> Result<()> { + todo!() + } +} diff --git a/crates/containerd-shim-wasmer/src/lib.rs b/crates/containerd-shim-wasmer/src/lib.rs index ecc0f9d36..5d14998f9 100644 --- a/crates/containerd-shim-wasmer/src/lib.rs +++ b/crates/containerd-shim-wasmer/src/lib.rs @@ -1,5 +1,26 @@ -#[cfg(unix)] -pub mod executor; -#[cfg_attr(unix, path = "instance/instance_linux.rs")] -#[cfg_attr(windows, path = "instance/instance_windows.rs")] +use std::env; + +use containerd_shim::parse; + +#[cfg_attr(unix, path = "instance_linux.rs")] +#[cfg_attr(windows, path = "instance_windows.rs")] pub mod instance; + +pub use instance::WasmerInstance; + +pub fn parse_version() { + let os_args: Vec<_> = env::args_os().collect(); + let flags = parse(&os_args[1..]).unwrap(); + if flags.version { + println!("{}:", os_args[0].to_string_lossy()); + println!(" Version: {}", env!("CARGO_PKG_VERSION")); + println!(" Revision: {}", env!("CARGO_GIT_HASH")); + println!(); + + std::process::exit(0); + } +} + +#[cfg(unix)] +#[cfg(test)] +mod tests; diff --git a/crates/containerd-shim-wasmer/src/tests.rs b/crates/containerd-shim-wasmer/src/tests.rs new file mode 100644 index 000000000..23e7c4aef --- /dev/null +++ b/crates/containerd-shim-wasmer/src/tests.rs @@ -0,0 +1,186 @@ +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() +} + +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() +} + +const WASI_RETURN_ERROR: &[u8] = r#"(module + (func $main (export "_start") + (unreachable) + ) +) +"# +.as_bytes(); + +#[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()?; + + 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)?; + + assert_eq!(res.0, 0); + + let output = read_to_string(path.join("stdout"))?; + assert_eq!(output, "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(); + + let dir = tempdir()?; + let path = dir.path(); + let wasm_bytes = hello_world_module(Some("foo")); + + let res = run_wasi_test::(&dir, wasm_bytes, Some("foo"))?; + + assert_eq!(res.0, 0); + + let output = read_to_string(path.join("stdout"))?; + assert_eq!(output, "hello world\n"); + + 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)?; + + // Expect error code from the run. + assert_eq!(res.0, 137); + + 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!()); + } + + // start logging + let _ = env_logger::try_init(); + let _guard = Stdio::init_from_std().guard(); + + 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)?; + + assert_eq!(actual_exit_code, expected_exit_code); + + Ok(()) +} diff --git a/crates/containerd-shim-wasmtime/Cargo.toml b/crates/containerd-shim-wasmtime/Cargo.toml index d1b2bf815..135a0986a 100644 --- a/crates/containerd-shim-wasmtime/Cargo.toml +++ b/crates/containerd-shim-wasmtime/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true [dependencies] containerd-shim = { workspace = true } -containerd-shim-wasm = { workspace = true, features = ["libcontainer_default"]} +containerd-shim-wasm = { workspace = true, features = ["libcontainer_default"] } log = { workspace = true } ttrpc = { workspace = true } @@ -22,8 +22,9 @@ wasmtime = { version = "11.0", default-features = false, features = [ 'cranelift', 'pooling-allocator', 'vtune', -]} +] } +wat = { workspace = true } wasmtime-wasi = { version = "11.0", features = ["exit"] } wasi-common = "11.0" chrono = { workspace = true } diff --git a/crates/containerd-shim-wasmtime/src/bin/containerd-shim-wasmtime-v1/main.rs b/crates/containerd-shim-wasmtime/src/bin/containerd-shim-wasmtime-v1/main.rs index cfbb8015d..f75be2169 100644 --- a/crates/containerd-shim-wasmtime/src/bin/containerd-shim-wasmtime-v1/main.rs +++ b/crates/containerd-shim-wasmtime/src/bin/containerd-shim-wasmtime-v1/main.rs @@ -1,9 +1,8 @@ use containerd_shim as shim; use containerd_shim_wasm::sandbox::ShimCli; -use containerd_shim_wasmtime::instance::Wasi as WasiInstance; -use containerd_shim_wasmtime::parse_version; +use containerd_shim_wasmtime::{parse_version, WasmtimeInstance}; fn main() { parse_version(); - shim::run::>("io.containerd.wasmtime.v1", None); + shim::run::>("io.containerd.wasmtime.v1", None); } diff --git a/crates/containerd-shim-wasmtime/src/bin/containerd-wasmtimed/main.rs b/crates/containerd-shim-wasmtime/src/bin/containerd-wasmtimed/main.rs index 677fff410..ed0480376 100644 --- a/crates/containerd-shim-wasmtime/src/bin/containerd-wasmtimed/main.rs +++ b/crates/containerd-shim-wasmtime/src/bin/containerd-wasmtimed/main.rs @@ -2,13 +2,13 @@ use std::sync::Arc; use containerd_shim_wasm::sandbox::{Local, ManagerService}; use containerd_shim_wasm::services::sandbox_ttrpc::{create_manager, Manager}; -use containerd_shim_wasmtime::instance::Wasi as WasiInstance; +use containerd_shim_wasmtime::WasmtimeInstance; use log::info; use ttrpc::{self, Server}; fn main() { info!("starting up!"); - let s: ManagerService> = ManagerService::default(); + let s: ManagerService> = ManagerService::default(); let s = Arc::new(Box::new(s) as Box); let service = create_manager(s); diff --git a/crates/containerd-shim-wasmtime/src/error.rs b/crates/containerd-shim-wasmtime/src/error.rs deleted file mode 100644 index e53e64f0c..000000000 --- a/crates/containerd-shim-wasmtime/src/error.rs +++ /dev/null @@ -1,15 +0,0 @@ -use anyhow; -use containerd_shim_wasm::sandbox::error; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum WasmtimeError { - #[error("{0}")] - Error(#[from] error::Error), - #[error("{0}")] - Wasi(#[from] wasmtime_wasi::Error), - #[error("{0}")] - WasiCommonStringArray(#[from] wasi_common::StringArrayError), - #[error("{0}")] - Other(#[from] anyhow::Error), -} diff --git a/crates/containerd-shim-wasmtime/src/executor.rs b/crates/containerd-shim-wasmtime/src/executor.rs deleted file mode 100644 index 09232a147..000000000 --- a/crates/containerd-shim-wasmtime/src/executor.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::fs::OpenOptions; -use std::path::PathBuf; - -use anyhow::{Context, Result}; -use containerd_shim_wasm::libcontainer_instance::LinuxContainerExecutor; -use containerd_shim_wasm::sandbox::{oci, Stdio}; -use libcontainer::workload::{Executor, ExecutorError, ExecutorValidationError}; -use oci_spec::runtime::Spec; -use wasmtime::{Engine, Linker, Module, Store}; -use wasmtime_wasi::{maybe_exit_on_error, WasiCtxBuilder}; - -use crate::oci_wasmtime::{self, wasi_dir}; - -const EXECUTOR_NAME: &str = "wasmtime"; - -#[derive(Clone)] -pub struct WasmtimeExecutor { - stdio: Stdio, - engine: Engine, -} - -impl WasmtimeExecutor { - pub fn new(stdio: Stdio, engine: Engine) -> Self { - Self { stdio, engine } - } -} - -impl Executor for WasmtimeExecutor { - fn exec(&self, spec: &Spec) -> Result<(), ExecutorError> { - match can_handle(spec) { - Ok(()) => { - let args = oci::get_args(spec); - if args.is_empty() { - return Err(ExecutorError::InvalidArg); - } - - let (mut store, f) = self.prepare(spec, args).map_err(|err| { - ExecutorError::Other(format!("failed to prepare function: {}", err)) - })?; - - log::info!("calling start function"); - - let status = f.call(&mut store, &[], &mut []); - let status = status - .map(|_| 0) - .map_err(maybe_exit_on_error) - .unwrap_or(137); - - std::process::exit(status); - } - Err(ExecutorValidationError::CantHandle(_)) => { - LinuxContainerExecutor::new(self.stdio.clone()).exec(spec)?; - - Ok(()) - } - Err(_) => Err(ExecutorError::InvalidArg), - } - } - - fn validate(&self, spec: &Spec) -> std::result::Result<(), ExecutorValidationError> { - match can_handle(spec) { - Ok(()) => Ok(()), - Err(ExecutorValidationError::CantHandle(_)) => { - LinuxContainerExecutor::new(self.stdio.clone()).validate(spec)?; - - Ok(()) - } - Err(err) => Err(err), - } - } -} - -impl WasmtimeExecutor { - fn prepare( - &self, - spec: &Spec, - args: &[String], - ) -> anyhow::Result<(Store, wasmtime::Func)> { - // already in the cgroup - let env = oci_wasmtime::env_to_wasi(spec); - log::info!("setting up wasi"); - - let path = wasi_dir("/", OpenOptions::new().read(true))?; - let wasi_builder = WasiCtxBuilder::new() - .args(args)? - .envs(env.as_slice())? - .inherit_stdio() - .preopened_dir(path, "/")?; - - self.stdio.take().redirect()?; - - log::info!("building wasi context"); - let wctx = wasi_builder.build(); - - log::info!("wasi context ready"); - let (module_name, method) = oci::get_module(spec); - let module_name = module_name - .context("no module provided, cannot load module from file within container")?; - - 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)?; - let mut store = Store::new(&self.engine, wctx); - - log::info!("instantiating instance"); - let instance = linker.instantiate(&mut store, &module)?; - - log::info!("getting start function"); - let start_func = instance - .get_func(&mut store, &method) - .context("module does not have a WASI start function")?; - Ok((store, start_func)) - } -} - -fn can_handle(spec: &Spec) -> Result<(), ExecutorValidationError> { - // check if the entrypoint of the spec is a wasm binary. - let (module_name, _method) = oci::get_module(spec); - let module_name = match module_name { - Some(m) => m, - None => { - log::info!("Wasmtime cannot handle this workload, no arguments provided"); - return Err(ExecutorValidationError::CantHandle(EXECUTOR_NAME)); - } - }; - let path = PathBuf::from(module_name); - - // TODO: do we need to validate the wasm binary? - // ```rust - // let bytes = std::fs::read(path).unwrap(); - // wasmparser::validate(&bytes).is_ok() - // ``` - - path.extension() - .map(|ext| ext.to_ascii_lowercase()) - .is_some_and(|ext| ext == "wasm" || ext == "wat") - .then_some(()) - .ok_or(ExecutorValidationError::CantHandle(EXECUTOR_NAME))?; - - Ok(()) -} diff --git a/crates/containerd-shim-wasmtime/src/instance/instance_linux.rs b/crates/containerd-shim-wasmtime/src/instance/instance_linux.rs deleted file mode 100644 index 542b48ae2..000000000 --- a/crates/containerd-shim-wasmtime/src/instance/instance_linux.rs +++ /dev/null @@ -1,239 +0,0 @@ -use std::path::PathBuf; -use std::sync::{Arc, Condvar, Mutex}; - -use containerd_shim_wasm::libcontainer_instance::LibcontainerInstance; -use containerd_shim_wasm::sandbox::error::Error; -use containerd_shim_wasm::sandbox::instance::ExitCode; -use containerd_shim_wasm::sandbox::instance_utils::determine_rootdir; -use containerd_shim_wasm::sandbox::stdio::Stdio; -use containerd_shim_wasm::sandbox::InstanceConfig; -use libcontainer::container::builder::ContainerBuilder; -use libcontainer::container::Container; -use libcontainer::syscall::syscall::SyscallType; - -use crate::executor::WasmtimeExecutor; - -static DEFAULT_CONTAINER_ROOT_DIR: &str = "/run/containerd/wasmtime"; - -pub struct Wasi { - exit_code: ExitCode, - engine: wasmtime::Engine, - stdio: Stdio, - bundle: String, - rootdir: PathBuf, - id: String, -} - -impl LibcontainerInstance for Wasi { - type Engine = wasmtime::Engine; - - fn new_libcontainer(id: String, cfg: Option<&InstanceConfig>) -> Self { - // TODO: there are failure cases e.x. parsing cfg, loading spec, etc. - // thus should make `new` return `Result` instead of `Self` - log::info!("creating new instance: {}", id); - let cfg = cfg.unwrap(); - let bundle = cfg.get_bundle().unwrap_or_default(); - let rootdir = determine_rootdir( - bundle.as_str(), - &cfg.get_namespace(), - DEFAULT_CONTAINER_ROOT_DIR, - ) - .unwrap(); - Wasi { - id, - exit_code: Arc::new((Mutex::new(None), Condvar::new())), - engine: cfg.get_engine(), - stdio: Stdio::init_from_cfg(cfg).expect("failed to open stdio"), - bundle, - rootdir, - } - } - - fn get_exit_code(&self) -> ExitCode { - self.exit_code.clone() - } - - fn get_id(&self) -> String { - self.id.clone() - } - - fn get_root_dir(&self) -> std::result::Result { - Ok(self.rootdir.clone()) - } - - fn build_container(&self) -> std::result::Result { - let engine = self.engine.clone(); - let err_others = |err| Error::Others(format!("failed to create container: {}", err)); - - let container = ContainerBuilder::new(self.id.clone(), SyscallType::Linux) - .with_executor(WasmtimeExecutor::new(self.stdio.take(), engine)) - .with_root_path(self.rootdir.clone()) - .map_err(err_others)? - .as_init(&self.bundle) - .with_systemd(false) - .build() - .map_err(err_others)?; - - Ok(container) - } -} - -#[cfg(test)] -mod wasitest { - - use std::fs::read_to_string; - - 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; - use serial_test::serial; - use tempfile::tempdir; - - use super::*; - - // 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() - } - - 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() - } - - #[test] - #[serial] - fn test_delete_after_create() -> anyhow::Result<()> { - let cfg = InstanceConfig::new( - Default::default(), - "test_namespace".into(), - "/containerd/address".into(), - ); - - let i = Wasi::new("".to_string(), Some(&cfg)); - i.delete()?; - Ok(()) - } - - #[test] - #[serial] - fn test_wasi_entrypoint() -> anyhow::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)?; - - assert_eq!(res.0, 0); - - let output = read_to_string(path.join("stdout"))?; - assert_eq!(output, "hello world\n"); - - Ok(()) - } - - // ignore until https://github.com/containerd/runwasi/issues/194 is resolved - #[test] - #[serial] - #[ignore] - fn test_wasi_custom_entrypoint() -> anyhow::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 path = dir.path(); - let wasm_bytes = hello_world_module(Some("foo")); - - let res = run_wasi_test::(&dir, wasm_bytes, Some("foo"))?; - - assert_eq!(res.0, 0); - - let output = read_to_string(path.join("stdout"))?; - assert_eq!(output, "hello world\n"); - - Ok(()) - } - - #[test] - #[serial] - fn test_wasi_exit_code() -> anyhow::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 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)?; - - assert_eq!(actual_exit_code, expected_exit_code); - - Ok(()) - } -} diff --git a/crates/containerd-shim-wasmtime/src/instance/instance_windows.rs b/crates/containerd-shim-wasmtime/src/instance/instance_windows.rs deleted file mode 100644 index 33c6c7aab..000000000 --- a/crates/containerd-shim-wasmtime/src/instance/instance_windows.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::path::PathBuf; - -use containerd_shim_wasm::sandbox::error::Error; -use containerd_shim_wasm::sandbox::instance::{ExitCode, Wait}; -use containerd_shim_wasm::sandbox::{Instance, InstanceConfig, Stdio}; - -pub struct Wasi { - id: String, - exit_code: ExitCode, - engine: wasmtime::Engine, - stdio: Stdio, - bundle: String, - rootdir: PathBuf, -} - -impl Instance for Wasi { - type Engine = wasmtime::Engine; - - fn new(id: String, cfg: Option<&InstanceConfig>) -> Self { - todo!() - } - - fn start(&self) -> std::result::Result { - todo!() - } - - fn kill(&self, signal: u32) -> std::result::Result<(), Error> { - todo!() - } - - fn delete(&self) -> std::result::Result<(), Error> { - todo!() - } - - fn wait(&self, waiter: &Wait) -> std::result::Result<(), Error> { - todo!() - } -} diff --git a/crates/containerd-shim-wasmtime/src/instance_linux.rs b/crates/containerd-shim-wasmtime/src/instance_linux.rs new file mode 100644 index 000000000..e627ddfb7 --- /dev/null +++ b/crates/containerd-shim-wasmtime/src/instance_linux.rs @@ -0,0 +1,75 @@ +use anyhow::{Context, Result}; +use containerd_shim_wasm::container::{ + Engine, Instance, PathResolve, RuntimeContext, Stdio, WasiEntrypoint, +}; +use wasi_common::I32Exit; +use wasmtime::{Linker, Module, Store}; +use wasmtime_wasi::{Dir, WasiCtxBuilder}; + +pub type WasmtimeInstance = Instance; + +#[derive(Clone, Default)] +pub struct WasmtimeEngine { + engine: wasmtime::Engine, +} + +impl Engine for WasmtimeEngine { + fn name() -> &'static str { + "wasmtime" + } + + fn run_wasi(&self, ctx: &impl RuntimeContext, stdio: Stdio) -> Result { + log::info!("setting up wasi"); + let path = Dir::from_std_file(std::fs::File::open("/")?); + let envs: Vec<_> = std::env::vars().collect(); + + let wasi_builder = WasiCtxBuilder::new() + .args(ctx.args())? + .envs(envs.as_slice())? + .inherit_stdio() + .preopened_dir(path, "/")?; + + stdio.redirect()?; + + log::info!("building wasi context"); + let wctx = wasi_builder.build(); + + log::info!("wasi context ready"); + let WasiEntrypoint { path, func } = ctx.wasi_entrypoint(); + let path = path + .resolve_in_path_or_cwd() + .next() + .context("module not found")?; + + log::info!("loading module from file {path:?}"); + let module = Module::from_file(&self.engine, &path)?; + let mut linker = Linker::new(&self.engine); + + wasmtime_wasi::add_to_linker(&mut linker, |s| s)?; + let mut store = Store::new(&self.engine, wctx); + + log::info!("instantiating instance"); + let instance: wasmtime::Instance = linker.instantiate(&mut store, &module)?; + + log::info!("getting start function"); + 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:?}"); + + let status = start_func.call(&mut store, &[], &mut []); + let status = status.map(|_| 0).or_else(|err| { + match err.downcast_ref::() { + // On Windows, exit status 3 indicates an abort (see below), + // so return 1 indicating a non-zero status to avoid ambiguity. + #[cfg(windows)] + Some(I32Exit(3..)) => Ok(1), + Some(I32Exit(status)) => Ok(*status), + _ => Err(err), + } + })?; + + Ok(status) + } +} diff --git a/crates/containerd-shim-wasmtime/src/instance_windows.rs b/crates/containerd-shim-wasmtime/src/instance_windows.rs new file mode 100644 index 000000000..4be744e0e --- /dev/null +++ b/crates/containerd-shim-wasmtime/src/instance_windows.rs @@ -0,0 +1,28 @@ +use containerd_shim_wasm::sandbox::instance::Wait; +use containerd_shim_wasm::sandbox::{Instance, InstanceConfig, Result, Stdio}; + +pub struct WasmtimeInstance {} + +impl Instance for WasmtimeInstance { + type Engine = (); + + fn new(id: String, cfg: Option<&InstanceConfig>) -> Self { + todo!() + } + + fn start(&self) -> Result { + todo!() + } + + fn kill(&self, signal: u32) -> Result<()> { + todo!() + } + + fn delete(&self) -> Result<()> { + todo!() + } + + fn wait(&self, waiter: &Wait) -> Result<()> { + todo!() + } +} diff --git a/crates/containerd-shim-wasmtime/src/lib.rs b/crates/containerd-shim-wasmtime/src/lib.rs index 1f0a707ce..26d39840d 100644 --- a/crates/containerd-shim-wasmtime/src/lib.rs +++ b/crates/containerd-shim-wasmtime/src/lib.rs @@ -2,14 +2,11 @@ use std::env; use containerd_shim::parse; -pub mod error; -#[cfg_attr(unix, path = "instance/instance_linux.rs")] -#[cfg_attr(windows, path = "instance/instance_windows.rs")] +#[cfg_attr(unix, path = "instance_linux.rs")] +#[cfg_attr(windows, path = "instance_windows.rs")] pub mod instance; -pub mod oci_wasmtime; -#[cfg(unix)] -pub mod executor; +pub use instance::WasmtimeInstance; pub fn parse_version() { let os_args: Vec<_> = env::args_os().collect(); @@ -23,3 +20,7 @@ pub fn parse_version() { std::process::exit(0); } } + +#[cfg(unix)] +#[cfg(test)] +mod tests; diff --git a/crates/containerd-shim-wasmtime/src/oci_wasmtime.rs b/crates/containerd-shim-wasmtime/src/oci_wasmtime.rs deleted file mode 100644 index 5fd5f2e30..000000000 --- a/crates/containerd-shim-wasmtime/src/oci_wasmtime.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::fs::OpenOptions; -use std::path::Path; - -use anyhow::Context; -use containerd_shim_wasm::sandbox::error::Error; -use containerd_shim_wasm::sandbox::oci; -use oci_spec::runtime::Spec; -use wasmtime_wasi::{Dir as WasiDir, WasiCtxBuilder}; - -pub fn get_rootfs(spec: &Spec) -> Result { - let path = oci::get_root(spec).to_str().unwrap(); - let rootfs = wasi_dir(path, OpenOptions::new().read(true))?; - Ok(rootfs) -} - -pub fn env_to_wasi(spec: &Spec) -> Vec<(String, String)> { - let default = vec![]; - let env = spec - .process() - .as_ref() - .unwrap() - .env() - .as_ref() - .unwrap_or(&default); - let mut vec: Vec<(String, String)> = Vec::with_capacity(env.len()); - - for v in env { - match v.split_once('=') { - None => vec.push((v.to_string(), "".to_string())), - Some(t) => vec.push((t.0.to_string(), t.1.to_string())), - }; - } - - vec -} - -pub fn spec_to_wasi>( - builder: WasiCtxBuilder, - bundle_path: P, - spec: &mut Spec, -) -> Result { - spec.canonicalize_rootfs(bundle_path)?; - let root = match spec.root() { - Some(r) => r.path().to_str().unwrap(), - None => return Err(Error::InvalidArgument("rootfs is not set".to_string())), - }; - - let rootfs = match wasi_dir(root, OpenOptions::new().read(true)) { - Ok(r) => r, - Err(e) => { - return Err(Error::InvalidArgument(format!( - "could not open rootfs: {0}", - e - ))); - } - }; - - let args = oci::get_args(spec); - if args.is_empty() { - return Err(Error::InvalidArgument("args is not set".to_string())); - } - - let env = env_to_wasi(spec); - let builder = builder - .preopened_dir(rootfs, "/") - .context("could not set rootfs")? - .envs(env.as_slice()) - .context("could not set envs")? - .args(args) - .context("could not set command args")?; - - Ok(builder) -} - -pub fn wasi_dir(path: &str, opts: &OpenOptions) -> Result { - let f = opts.open(path)?; - Ok(WasiDir::from_std_file(f)) -} diff --git a/crates/containerd-shim-wasmtime/src/tests.rs b/crates/containerd-shim-wasmtime/src/tests.rs new file mode 100644 index 000000000..7e4666ca2 --- /dev/null +++ b/crates/containerd-shim-wasmtime/src/tests.rs @@ -0,0 +1,186 @@ +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() +} + +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() +} + +const WASI_RETURN_ERROR: &[u8] = r#"(module + (func $main (export "_start") + (unreachable) + ) +) +"# +.as_bytes(); + +#[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()?; + + 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)?; + + assert_eq!(res.0, 0); + + let output = read_to_string(path.join("stdout"))?; + assert_eq!(output, "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(); + + let dir = tempdir()?; + let path = dir.path(); + let wasm_bytes = hello_world_module(Some("foo")); + + let res = run_wasi_test::(&dir, wasm_bytes, Some("foo"))?; + + assert_eq!(res.0, 0); + + let output = read_to_string(path.join("stdout"))?; + assert_eq!(output, "hello world\n"); + + 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)?; + + // Expect error code from the run. + assert_eq!(res.0, 137); + + 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!()); + } + + // start logging + let _ = env_logger::try_init(); + let _guard = Stdio::init_from_std().guard(); + + 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)?; + + assert_eq!(actual_exit_code, expected_exit_code); + + Ok(()) +}