Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a simplified API when using libcontainer #293

Merged
merged 2 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .github/workflows/action-fmt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions .github/workflows/action-test-k3s.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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::E>>) -> Self;
/// Start the instance
Expand All @@ -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<i32>;
/// 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<impl container::Engine>`, 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
Expand All @@ -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::<ShimCli<Instance<Engine>>>("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.

Expand Down
1 change: 1 addition & 0 deletions crates/containerd-shim-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
181 changes: 181 additions & 0 deletions crates/containerd-shim-wasm/src/container/context.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}
39 changes: 39 additions & 0 deletions crates/containerd-shim-wasm/src/container/engine.rs
Original file line number Diff line number Diff line change
@@ -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<i32>;

/// 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(())
}
}
Loading