Skip to content

Commit

Permalink
add simplified container instance api
Browse files Browse the repository at this point in the history
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
  • Loading branch information
jprendes committed Sep 6, 2023
1 parent 0931abc commit e0d7179
Show file tree
Hide file tree
Showing 50 changed files with 1,326 additions and 1,577 deletions.
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
# 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
3 changes: 3 additions & 0 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 @@ -36,6 +36,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"
47 changes: 46 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,23 @@ 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(&self, ctx: impl RuntimeContext, stdio: Stdio) -> Result<i32>;
/// Check that the runtime can run the container.
/// These checks run after the container creation and before the container start.
/// By default it checks that the entrypoint is an existing `.wasm` or `.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 +93,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

0 comments on commit e0d7179

Please sign in to comment.