###### 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