延續昨天的主題,我們來看看 containerd-shim-wasm 的實作。
在下面的檔案結構中,可以分成幾大類:
containerd-shim-wasm
├── build.rs
├── Cargo.toml
├── doc
│ ├── doc.md
│ └── header.md
├── protos
│ └── sandbox.proto
├── README.md
└── src
├── container
│ ├── context.rs
│ ├── engine.rs
│ ├── mod.rs
│ └── path.rs
├── lib.rs
├── sandbox
│ ├── cli.rs
│ ├── error.rs
│ ├── instance.rs
│ ├── instance_utils.rs
│ ├── manager.rs
│ ├── mod.rs
│ ├── oci.rs
│ ├── shim.rs
│ └── stdio.rs
├── services
│ ├── sandbox.rs
│ └── sandbox_ttrpc.rs
├── services.rs
└── sys
├── unix
│ ├── container
│ │ ├── executor.rs
│ │ ├── instance.rs
│ │ └── mod.rs
│ ├── metrics.rs
│ ├── mod.rs
│ ├── networking.rs
│ ├── signals.rs
│ └── stdio.rs
└── windows
├── container
│ ├── instance.rs
│ └── mod.rs
├── metrics.rs
├── mod.rs
├── networking.rs
├── signals.rs
└── stdio.rs
## 本日重點 - container
```txt
containerd-shim-wasm/src/container
├── context.rs
├── engine.rs
├── mod.rs
└── path.rs
mod.rs
定義了這個模組下有哪些功能:
//! This module contains an API for building WebAssembly shims running on top of containers.
//! 本模組包含一個用於在容器上運行 WebAssembly shims 的 API。
//! Unlike the `sandbox` module, this module delegates many of the actions to the container runtime.
//! 與`sandbox`模組不同,本模組將許多操作委託給容器(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
//! 這具有以下的優點:
//! * 簡化了新 shims 的撰寫,讓開發者能迅速開發與執行
//! * 將複雜的 OCI 規範處理掉,避免增加開發者負擔
//!
//! But it also has some disadvantages:
//! * Runtime overhead in in setting up a container
//! * Less customizable
//! * Currently only works on Linux
//! 但也有一些缺點:
//! * 當設置容器時會有運行時(runtime)的開銷
//! * 客製化程度較低
//! * 目前只適用於 Linux
mod context;
mod engine;
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;
use crate::sys::container::instance;
context.rs
use std::path::{Path, PathBuf};
use oci_spec::runtime::Spec;
// 定義了 RuntimeContext 應該具備什麼函式
pub trait RuntimeContext {
// ctx.args() returns arguments from the runtime spec process field, including the
// path to the entrypoint executable.
// ct.args 回傳來自 runtime 標準中的 process 欄位的參數,包含進入點 (`ENTRYPOINT`) 執行檔案的路徑。
fn args(&self) -> &[String];
// ctx.entrypoint() returns the entrypoint path from arguments on the runtime
// spec process field.
// ctx.entrypoint() 回傳將從 runtime 規範中的 process 欄位中的參數,回傳進入點的路徑。
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" }
// ctx.wasi_entrypoint() 回傳返回一個 `WasiEntrypoint`,包含了:
// 1. 作為進入點的模組路徑。
// 2. 將被呼叫的導出函式 (exported function) 的名字。
// 以上的資訊是從 OCI 標準的 process 欄位所取得。
// 在此標準中的第一個參數是 `path#func`,其中 `func` 是可選的,預設為 `_start`。
// 例如:
// "/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;
}
// `WasiEntrypoint` 的資料結構
// 裡面有兩個元素:
// 1. `path`: 進入點路徑
// 2. `func`: 進入點函式。在該進入點模組中準備呼叫的函式
pub struct WasiEntrypoint {
pub path: PathBuf,
pub func: String,
}
// 以下為 RuntimeContext 的實作
impl RuntimeContext for Spec {
fn args(&self) -> &[String] {
// 從 Spec 中讀取 `process` 欄位
// 並把內部的參數取出回傳
self.process()
.as_ref()
.and_then(|p| p.args().as_ref())
.map(|a| a.as_slice())
.unwrap_or_default()
}
fn entrypoint(&self) -> Option<&Path> {
// 將第一個參數取出作為 entrypoint
self.args().first().map(Path::new)
}
fn wasi_entrypoint(&self) -> WasiEntrypoint {
// 解析第一個參數,並建立 `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(())
}
}
engine.rs
use std::fs::File;
use std::io::Read;
use anyhow::{Context, Result};
use crate::container::{PathResolve, RuntimeContext};
use crate::sandbox::Stdio;
// 定義一個 Engine 應該具備什麼功能
pub trait Engine: Clone + Send + Sync + 'static {
/// The name to use for this engine
/// 此引擎的名字
fn name() -> &'static str;
/// Run a WebAssembly container
/// 執行一個 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.
/// 檢查 runtime 是否能夠執行該 container。
/// 此檢查在建立 container 後與 container 被啟動前執行。
/// 此檢查確保 wasi_entrypoint 必須為是以下其中一種:
/// * 一個具備有 `wasm` 檔案類型標頭的檔案,wasm 為 `\0asm`
/// * 一個可解析的 `wat` 檔案
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(())
}
}
path.rs
use std::path::{Path, PathBuf};
// 定義 `PathResolve` 需要實作的函式
pub trait PathResolve {
// TODO: Once RPITIT lands in stable, change the return types from
// `-> Box<dyn Trait>` to `-> impl Trait`
// See: https://rustc-dev-guide.rust-lang.org/return-position-impl-trait-in-trait.html
// 這個與 Rust 的語言標準有關,有興趣自行點入以上連結閱讀。
// 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.
// 根據給定的資料夾集合來解析檔案路徑,行為上類似於 `unix` 的 `which` 指令對 `PATH` 環境變數的操作。
// 將以迭代器(`Iterator`)形式回傳全部的候選者。
// 被回傳的候選者除了必須是存在的檔案,無其他的限制。
// 需特別注意,這個函式不檢查檔案的可執行檔位元。
// 可以透過對回傳的迭代器進行過濾來增加進一步的限制。
fn resolve_in_dirs<'a>(
&self,
dirs: impl IntoIterator<Item = impl AsRef<Path>> + 'a,
) -> Box<dyn Iterator<Item = PathBuf> + 'a>;
fn resolve_in_path(&self) -> Box<dyn Iterator<Item = PathBuf>>;
fn resolve_in_path_or_cwd(&self) -> Box<dyn Iterator<Item = PathBuf>>;
}
// Gets the content of the `PATH` environment variable as an
// iterator over its components
// 取得 `PATH` 環境變數的內容,並以迭代器的形式回傳對應的元件
pub fn paths() -> impl Iterator<Item = PathBuf> {
std::env::var_os("PATH")
.as_ref()
.map(std::env::split_paths)
.into_iter()
.flatten()
.collect::<Vec<_>>()
.into_iter()
}
impl<T: AsRef<Path>> PathResolve for T {
fn resolve_in_dirs<'a>(
&self,
dirs: impl IntoIterator<Item = impl AsRef<Path>> + 'a,
) -> Box<dyn Iterator<Item = PathBuf> + '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<Item = PathBuf>`
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<dyn Iterator<Item = PathBuf>> {
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<dyn Iterator<Item = PathBuf>> {
self.resolve_in_dirs(paths().chain(std::env::current_dir().ok()))
}
}