---
# System prepended metadata

title: Cromwell メモ
tags: [memo, cloud]

---

###### tags: `cloud` `memo`
# Cromwell メモ
## Cromwell とは
- Cromwell は香川大学で研究開発されている（いた？） Rootless コンテナランタイム
- コンテナの勉強になるかと思いのソースコードを読んでみた

https://github.com/guni1192/cromwell

```
Rust Rootless Container Runntime

USAGE:
    cromwell [SUBCOMMAND]

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

SUBCOMMANDS:
    help    Prints this message or the help of the given subcommand(s)
    ps      show containers
    pull    pull oci image
    run     run cromwell container
```

## ソースコード
### 1. psコマンド
エントリポイントは`src/main.rs`内の以下の部分
```rust=
        ("ps", Some(sub_m)) => pids::show(&sub_m).expect("cannot get container processes"),
```
実体は`src/pids.rs`
```rust=
pub fn show(_sub_m: &ArgMatches) -> io::Result<()> {
    println!("|Container ID \t| PID \t|");

    let home = home_dir().expect("Could not get your home_dir");
    let home = home.to_str().expect("Could not PathBuf to str");
    let pids_path = format!("{}/.cromwell/pids", home);

    let pid_dir = Path::new(&pids_path);

    if pid_dir.is_dir() {
        for entry in fs::read_dir(pid_dir)? {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                continue;
            }
            let pidfile = Pidfile::read(&path).expect("Failed to read pidfile");
            println!("|{} \t| {} \t|", pidfile.container_id, pidfile.pid);
        }
    }
    Ok(())
}
```
`$HOME/.cromwell/pids` ディレクトリ以下のファイルを走査し、ファイルが見つかったら、 `Pidfile::read` にパスを渡す。`pids/` 以下にコンテナの情報がファイルに保存されている。

ファイルの内容は `Pidfile` 構造体で定義されている。
```rust=
pub struct Pidfile {
    pid: Pid,
    container_id: String, // as file_name
}

impl Pidfile {
    pub fn create(path: &Path, pid: Pid) -> io::Result<()> {
        fs::write(path, pid.to_string().as_bytes())
    }

    pub fn delete(path: &Path) -> io::Result<()> {
        fs::remove_file(path)
    }

    fn read(path: &Path) -> io::Result<Pidfile> {
        let pid: i32 = fs::read_to_string(path).unwrap().parse().unwrap();
        let pid = Pid::from_raw(pid);
        let container_id = path.file_stem().unwrap().to_str().unwrap();

        Ok(Pidfile {
            pid,
            container_id: container_id.to_string(),
        })
    }
}
```

ファイル名と中身はそれぞれ以下の通り
- ファイル名: コンテナID
- 中身: pid

### 2. pullコマンド
エントリポイントは`src/main.rs`内の
```rust=
        ("pull", Some(sub_m)) => runner::pull(&sub_m),
```

実体は`src/runner.rs`内の
```rust=
pub fn pull(sub_m: &ArgMatches) {
    let image_name = sub_m
        .value_of("image_name")
        .expect("invalied arguments about image name");

    let mut image = Image::new(image_name);

    image.pull().expect("Failed to image pull");
}
```

`image.pull`の定義は
```rust=
impl Image {
...
pub fn pull(&mut self) -> Result<(), reqwest::Error> {
        let auth_url = format!(
            "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{}:pull",
            self.name
        );
        let res_json: String = reqwest::get(auth_url.as_str())?.text()?;
        let body: Value = serde_json::from_str(res_json.as_str()).expect("parse json failed");

        let token = match &body["token"] {
            Value::String(t) => t,
            _ => panic!("unexpected data: body[\"token\"]"),
        };

        let manifests_url = format!(
            "https://registry.hub.docker.com/v2/{}/manifests/{}",
            self.name, self.tag
        );

        let res = reqwest::Client::new()
            .get(manifests_url.as_str())
            .bearer_auth(token)
            .send()?
            .text()?;

        let body: Value = serde_json::from_str(res.as_str()).expect("parse json failed");

        match &body["fsLayers"] {
            Value::Array(fs_layers) => {
                for fs_layer in fs_layers {
                    self.download(token, &fs_layer).expect("download failed");
                }
            }
            _ => eprintln!("unexpected type fsLayers"),
        }

        Ok(())
    }
...
}
```
URL から判断するに DockerHub からイメージを取得するっぽい。マニフェスト取得後に `self.download` を実行してイメージをダウンロードする。

```rust=
    fn download(&mut self, token: &str, fs_layer: &Value) -> std::io::Result<()> {
        if let Value::String(blob_sum) = &fs_layer["blobSum"] {
            let out_filename = format!("/tmp/{}.tar.gz", blob_sum.replace("sha256:", ""));
            self.fs_layers.push(out_filename.clone());

            if Path::new(out_filename.as_str()).exists() {
                return Ok(());
            }

            let url = format!(
                "https://registry.hub.docker.com/v2/{}/blobs/{}",
                self.name, blob_sum
            );

            let mut res = reqwest::Client::new()
                .get(url.as_str())
                .bearer_auth(token)
                .send()
                .expect("failed to send requwest");
            let mut out = File::create(&out_filename)?;

            io::copy(&mut res, &mut out)?;
        } else {
            return Err(Error::new(
                ErrorKind::Other,
                "blobSum not found from fsLayer",
            ));
        }
        Ok(())
    }
```
最終的に DockerHub からダウンロードしたイメージを `/tmp/{}.tar.gz` に保存する。ファイル名は blobSum という sha256 ハッシュ値。

blobSum は特定のファイルシステムイメージレイヤーのハッシュ値らしい。
https://docs.docker.com/registry/spec/manifest-v2-1/

### 3. runコマンド
```rust=
pub fn run(sub_m: &ArgMatches) {
    let command = match sub_m.value_of("exec_command") {
        Some(c) => c,
        None => "/bin/sh",
    };

    let image_name = sub_m.value_of("container_name");
    let image = match image_name {
        Some(name) => Some(Image::new(name)),
        None => None,
    };

    let container_path = sub_m.value_of("container_path");

    let become_daemon = sub_m.is_present("daemonize_flag");

    let mut container = container::Container::new(image, container_path);

    let home = home_dir().expect("Could not get your home_dir");
    let home = home.to_str().expect("Could not PathBuf to str");
    // TODO: parse config.json, get spec.Process.Cwd
    let container_dir = format!("{}/.cromwell/containers", home);

    // TODO: load config.json generated by `runc spec`
    // TODO: followr runtime-spec in spec-go.Process
    let process = Process::new(
        vec_cstr![command],
        format!("{}/{}/rootfs", container_dir, container.id),
        become_daemon,
        // Example environment
        vec_cstr![
            "PATH=/bin/:/usr/bin/:/usr/local/bin:/sbin:/usr/sbin",
            "TERM=xterm-256color",
            "LC_ALL=C"
        ],
    );

    if sub_m.is_present("del") {
        container
            .delete(&process)
            .expect("Failed to remove container: ");
    }

    container.prepare(&process);
    container.run(&process);
}
```

ます、コマンドラインから以下の情報を取得する。

- exec_command: 実行するコマンド
- container_name: コンテナ(イメージ)の名前
- container_path コンテナのパス
- daemonize_flag: デーモン化するか否か
- del: コンテナを削除するかどうか

ここで，以下の構造体にそれぞれ注目する．
- Image
- Container
- Process

`Image::new(name)`についてまず見ていく。
```rust=
pub struct Image {
    pub name: String,
    pub tag: String,
    pub fs_layers: Vec<String>,
}

impl Image {
    pub fn new(name_and_tag: &str) -> Image {
        let mut n: Vec<&str> = name_and_tag.split(':').collect();
        if n.len() < 2 {
            n.push("latest");
        }
        Image {
            name: n[0].to_string(),
            tag: n[1].to_string(),
            fs_layers: Vec::<String>::new(),
        }
    }
...
}
```

`<name>:<tag>` 形式の文字列を取り、それぞれ `Image` の `name` と `tag` に代入される。`<tag>` がない場合は `latest` をタグとする。ここらへんは Docker と同じ感じ。

次に、Container について見ていく。

```rust=
pub struct Container {
    pub id: String,
    pub image: Option<Image>,
    pub state: State,
}

// Container State
#[derive(Debug, PartialEq)]
pub enum State {
    Creating,
    Created,
    Running,
    Stopped,
}

impl Container {
    pub fn new(image: Option<Image>, path: Option<&str>) -> Container {
        let id: String = match path {
            Some(id) => id.to_string(),
            None => {
                let mut rng = thread_rng();
                iter::repeat(())
                    .map(|()| rng.sample(Alphanumeric))
                    .take(8)
                    .collect::<String>()
            }
        };

        Container {
            id,
            image,
            state: State::Stopped,
        }
    }
...
}
```

`path` が与えられたときはそれを `id` とする。なければ乱数を生成して `id` とする。その後、`Stopped` 状態の `Container` 構造体を返す。

次に見るのはこの部分
```rust=
let process = Process::new(
        vec_cstr![command],
        format!("{}/{}/rootfs", container_dir, container.id),
        become_daemon,
        // Example environment
        vec_cstr![
            "PATH=/bin/:/usr/bin/:/usr/local/bin:/sbin:/usr/sbin",
            "TERM=xterm-256color",
            "LC_ALL=C"
        ],
    );
```

`new` に渡すのは
- 実行するコマンド
- ファイルシステムのルートパス
- デーモンとして動作させるか
- 環境変数

最後に以下で、プロセスを準備して起動する。
```rust=
    container.prepare(&process);
    container.run(&process);
```

まずは準備の `prepare`
```rust=
pub fn prepare(&mut self, process: &Process) {
        // specify Image name
        if let Some(image) = &mut self.image {
            self.state = State::Creating;
            image.pull().expect("Failed to cromwell pull");
            image
                .build_from_tar(&process.cwd)
                .expect("Failed build image from fsLayer");

            let c_hosts = format!("{}/etc/hosts", process.cwd);
            let c_resolv = format!("{}/etc/resolv.conf", process.cwd);

            fs::copy("/etc/hosts", &c_hosts).expect("Failed copy /etc/hosts");
            info!("[Host] Copied /etc/hosts to {}", c_hosts);

            fs::copy("/etc/resolv.conf", &c_resolv).expect("Failed copy /etc/resolv.conf");
            info!("[Host] Copied /etc/resolv.conf {}", c_resolv);
        }

        // nochdir, close tty
        if process.become_daemon {
            daemon(true, false).expect("cannot become daemon");
        }

        unshare(
            CloneFlags::CLONE_NEWPID
                | CloneFlags::CLONE_NEWUTS
                | CloneFlags::CLONE_NEWNS
                | CloneFlags::CLONE_NEWUSER,
        )
        .expect("Can not unshare(2).");

        self.guid_map(&process)
            .expect("Failed to write /proc/self/gid_map|uid_map");
        self.state = State::Created;
    }
```

処理流れは大まかに以下の通り
1. コンテナイメージを pull して、内容をコンテナのルートディレクトリに展開
2. ホストの `/etc/hosts` と `/etc/resolv.conf` をコンテナのルートディレクトリ以下にコピー
3. `unshare` システムコールを実行して、親プロセスと名前空間を分離（新しい名前空間をつくる）
    a. CLONE_NEWPID: PID名前空間を分離（プロセスIDの分離）
    b. CLONE_NEWUTS: UTS名前空間を分離 (ホスト名とNISドメイン名を管理する名前空間)
    c. CLONE_NEWNS: マウント名前空間を分離
    d. CLONE_NEWUSER: ユーザ名前空間を分離（ユーザID/グループIDなどの分離）
4. uid と gid のマッピングを実行
    a. `/proc/<pid>/{uid,gid}_map` を通じてマッピング
    b. フォーマットは https://linuxjm.osdn.jp/html/LDP_man-pages/man7/user_namespaces.7.html の **ユーザー ID とグループ ID のマッピング: uid_map と gid_map** を参照
    c. 具体的には以下のようなフォーマット
    ```
    NS内ID        NS外ID        長さ
    ```
    d. Cromwell 内の `uid_map()` では以下のように、NS外(ホストのUID)をコンテナ内では0(root user)に割り当てている
    ```rust
    let mut uid_map_file = File::create("/proc/self/uid_map")?;
    let uid_map = format!("0 {} 1", uid);

    uid_map_file.write_all(uid_map.as_bytes())?;
    ```
    
最後の最後でやっと`run`

```rust
    pub fn run(&mut self, process: &Process) {
        match fork() {
            Ok(ForkResult::Parent { child, .. }) => {
                info!("[Host] PID: {}", getpid());
                info!("[Container] PID: {}", child);

                let home = home_dir().expect("Could not get your home_dir");
                let home = home.to_str().expect("Could not PathBuf to str");
                let pids_path = format!("{}/.cromwell/pids", home);
                fs::create_dir_all(&pids_path).expect("failed mkdir pids");

                let pidfile_path = format!("{}/{}.pid", pids_path, self.id);
                let pidfile_path = Path::new(&pidfile_path);

                Pidfile::create(&pidfile_path, child).expect("Failed to create pidfile");

                match waitpid(child, None).expect("waitpid faild") {
                    WaitStatus::Exited(_, _) => {
                        Pidfile::delete(&pidfile_path).expect("Failed to remove pidfile");
                        self.state = State::Stopped;
                    }
                    WaitStatus::Signaled(_, _, _) => {}
                    _ => eprintln!("Unexpected exit."),
                }
            }
            Ok(ForkResult::Child) => {
                self.state = State::Running;
                chroot(Path::new(&process.cwd)).expect("chroot failed.");
                chdir("/").expect("cd / failed.");

                sethostname(&self.id).expect("Could not set hostname");
                fs::create_dir_all("proc").unwrap_or_else(|why| {
                    eprintln!("{:?}", why.kind());
                });

                info!("[Container] Mount procfs ... ");
                mounts::mount_proc().expect("mount procfs failed");

                execve(&process.cmd[0], &process.cmd, &process.env).expect("execution failed.");
            }
            Err(e) => panic!("Fork failed: {}", e),
        }
    }
```

fork 後に子プロセスは以下を実行
- chroot でルートディレクトリを再設定
- chdir で `/` に移動
- sethostname でホスト名を設定
- `/proc` を作る
- `mounts::mount_proc` で何かをマウント？
- `execve` でコマンドを実行

親プロセスでは Pidfile を作る。（ps コマンドで参照できるようにするため）

## 参考サイト
- https://www.slideshare.net/masamiichikawa/linux-namespaces-53216942 
- https://kernhack.hatenablog.com/entry/2019/05/10/233527#UTS%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93-1