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 4, 2023
1 parent d0a1a1c commit cd8026c
Show file tree
Hide file tree
Showing 39 changed files with 1,153 additions and 1,560 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
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
236 changes: 236 additions & 0 deletions crates/containerd-shim-wasm/src/container/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
use std::iter::{FilterMap, Map};
use std::path::{Path, PathBuf};
use std::slice::Iter;
use std::str::Split;

use oci_spec::runtime::Spec;

// Ideally this would be `impl Iterator<Item = (&str, &str)>`
// but we can't return `impl ...` in traits.
pub type EnvIterator<'a> = Map<Iter<'a, String>, fn(&String) -> (&str, &str)>;

// Ideally this would be `impl Iterator<Item = PathBuf>`
// but we can't return `impl ...` in traits.
pub type FindInPathIterator<'a> =
FilterMap<Split<'a, char>, Box<dyn FnMut(&str) -> Option<PathBuf>>>;

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.module() returns the module path and exported function name to be called
// as a (&Path, &str) tuple, obtained 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: "/app/module.wasm#function" will return the tuple
// (Path::new("/app/module.wasm"), "function")
//
// If there are no arguments then it will return (Path::new(""), "_start")
fn module(&self) -> (&Path, &str);

// ctx.envs() returns the environment variables from the runtime spec process field
// as an `impl Iterator<Item = (&str, &str)>` representing the variable name and value
// respectively.
fn envs(&self) -> EnvIterator;

// ctx.find_in_path("file.wasm") will try to find "file.wasm" using the process PATH
// environment variable for its resolution. It returns an `impl Iterator<Item = PathBuf>`
// with the canonical path of all found files. This function does not impose any
// requirement other than the file existing. Extra requirements, like executable mode,
// can be added by filtering the iterator.
fn find_in_path(&self, file: impl AsRef<Path>) -> FindInPathIterator;
}

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 module(&self) -> (&Path, &str) {
let arg0 = self.args().first().map(String::as_str).unwrap_or("");
let (module, method) = arg0.split_once('#').unwrap_or((arg0, "_start"));
(Path::new(module), method)
}

fn envs(&self) -> EnvIterator {
fn split_once(s: &String) -> (&str, &str) {
s.split_once('=').unwrap_or((s, ""))
}

self.process()
.as_ref()
.and_then(|p| p.env().as_ref())
.map(Vec::as_slice)
.unwrap_or_default()
.iter()
.map(split_once)
}

fn find_in_path(&self, file: impl AsRef<Path>) -> FindInPathIterator {
let executable = file.as_ref().to_owned();
let filter = Box::new(move |p: &str| -> Option<PathBuf> {
let path = Path::new(p).canonicalize().ok()?;
let path = path.is_dir().then_some(path)?.join(&executable);
let path = path.canonicalize().ok()?;
path.is_file().then_some(path)
});

let path_iter = if file.as_ref().components().count() > 1 {
"."
} else {
self.envs()
.find(|(key, _)| *key == "PATH")
.unwrap_or(("", "."))
.1
};

path_iter.split(':').filter_map(filter)
}
}

#[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 (module, _) = spec.module();
assert!(module.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 (module, function) = spec.module();
assert_eq!(module, Path::new("hello.wat"));
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![
"/root/hello.wat".to_string(),
"echo".to_string(),
"hello".to_string(),
])
.build()?,
)
.build()?;
let spec = &spec;

let (module, function) = spec.module();
assert_eq!(module, Path::new("/root/hello.wat"));
assert_eq!(function, "_start");

Ok(())
}
}
19 changes: 19 additions & 0 deletions crates/containerd-shim-wasm/src/container/engine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use anyhow::Result;

use crate::container::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(&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 default the check always succeeeds.
fn can_handle(&self, _ctx: impl RuntimeContext) -> Result<()> {
Ok(())
}
}
Loading

0 comments on commit cd8026c

Please sign in to comment.