iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0
Cloud Native

關於 WebAssembly 也能變成 Container 的這檔事系列 第 9

Wasm+containerd-shim-wasm+container

  • 分享至 

  • xImage
  •  

Wasm+containerd-shim-wasm+container

containerd-shim-wasm

延續昨天的主題,我們來看看 containerd-shim-wasm 的實作。

檔案結構

在下面的檔案結構中,可以分成幾大類:

  1. doc: 文件,基本上等於沒寫,看程式碼比較快
  2. protos: protobuf 定義檔,可以知道 sandbox 具備哪些功能,如:Create, Connect, Delete
  3. src: 包含了 container, sandbox, services, sys 等主要程式碼的分類。

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

上一篇
Wasm+Runwasi
下一篇
Wasm+containerd-shim-wasm+sandbox - part 1
系列文
關於 WebAssembly 也能變成 Container 的這檔事15
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言