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