使用 Rust 来实现自己的 Docker

Last Edited Time
May 29, 2024 07:05 AM
date
Mar 1, 2024
slug
build-your-own-docker-using-rust
status
Published
tags
Rust
Docker
summary
使用 Rust 来构建自己的 Docker
type
Post

背景

最近刚刚学完 Rust 的语法,所以想做一个简单的《Build your own docker》,看到 CodeCrafters 有个的教程,所以赶紧来试一下。

开搞

项目搭建

教程中的项目有提供最基础的项目代码,在编写完成后提交代码会在云端进行测试验证,但是我想在本地直接进行验证(其实是后面的章节收费了,大雾),所以项目由以下两部分组成:
docker:完整的 my-docker 项目源码,执行 your_docker.sh 的时候会通过 cargo run 来执行代码
notion image
tester:本地测试命令的项目,这部分代码在《docker-tester》,不过这部分是用 go 写的,所以要运行在 docker 项目里
notion image
项目根目录会提供两个脚本方便测试
  • test_your_docker.sh:在 docker 中执行 docker-tester 来验证 cli 的逻辑
    • docker system prune --force
      docker build -t docker-tester-dev . && docker run --cap-add "SYS_ADMIN" -e "TERM=xterm-256color" docker-tester-dev make test
  • test_shell_output.sh:判断命令输出是否符合要求
    • #!/bin/bash
      
      # 执行 shell 命令
      # bash -c "xxxx"
      # bash -c "ls non_existing_file.txt"
      bash -c "ls"
      
      # 获取退出代码
      exit_code=$?
      
      # 根据退出代码进行错误判断
      if [[ $exit_code -eq 0 ]]; then
        echo "命令执行成功"
      else
        echo "命令执行失败,退出代码:$exit_code"
        
        # 根据具体的退出代码值进行错误处理
        if [[ $exit_code -eq 1 ]]; then
          echo "General Error: 1"
        elif [[ $exit_code -eq 2 ]]; then
          echo "Misuse of Shell Built-in: 2"
        elif [[ $exit_code -eq 126 ]]; then
          echo "Cannot Execute: 126"
        elif [[ $exit_code -eq 127 ]]; then
          echo "Command Not Found: 127"
        fi
      fi
       
如果想直接运行 mydocker,可以直接添加以下 alias
mydocker='docker container prune -f && docker build -t mydocker ./docker && docker run --cap-add="SYS_ADMIN" mydocker'

功能实现

1. 命令执行并输出

根据用户传入的命令,在本地电脑执行对应的命令,并且处理好对应的输入输出和退出逻辑,关键代码如下
let args: Vec<_> = std::env::args().collect();
let command = &args[3];
let command_args = &args[4..];
let output = Command::new(command)
    .args(command_args)
    // .stdout(Stdio::piped())
    // .stderr(Stdio::piped())
    .output()
    .with_context(|| {
        format!(
            "Tried to run '{}' with arguments {:?}",
            command, command_args
        )
    })?;

let std_out = std::str::from_utf8(&output.stdout)?;
print!("{}", std_out);
let std_err = std::str::from_utf8(&output.stderr)?;
eprint!("{}", std_err);

2. 处理 exit 逻辑

在执行命令之后判断命令是否执行成功,如果没有则返回相应的退出码
if !output.status.success() {
    exit(output.status.code().unwrap_or(1));
}

3. 文件系统隔离

文件系统是借用的宿主的 chroot 能力,该能力可以改变当前进程及其子进程根目录的操作,从而完成文件系统的隔离
fn setup_filesystem_isolation(command: &str) -> Result<()> {
    // container_path: /tmp/your_docker_container/
    let container_path = Path::new("/tmp/your_docker_container");
    std::fs::create_dir_all(container_path.join("dev/null"))?;

    let command_path = PathBuf::from(command);
    let command_file_name = command_path.file_name().unwrap();
    let dest_command_path = container_path.join(command_path.parent().unwrap().strip_prefix("/")?);
    if DEBUG == true {
        println!(
            "command_path: {}; command_path: {}",
            command_path.display(),
            dest_command_path.display()
        );
    }

    std::fs::create_dir_all(dest_command_path.clone())?;
    std::fs::copy(
        command_path.clone(),
        dest_command_path.join(command_file_name),
    )?;

    chroot(container_path)?;
    std::env::set_current_dir("/")?;

    Ok(())
}

4. 进程隔离

通过使用 PID namespaces 的方式来常见一个新的进程 ID 命名空间,来进行进程隔离
fn setup_process_isolation() -> () {
    unsafe {
        libc::unshare(libc::CLONE_NEWPID);
    }
}

5. 其他

到此为止已对项目中的核心功能进行了复现,其他的部分以后再来探索吧:
  • 登录到 docker hub
  • 下载 docker 镜像
  • 运行镜像时使用 docker layer

总结

总的来说还是很有意思的,在熟悉了 rust 的同时也了解了 Linux 的 chrootPID namespace 的能力,并且对 docker 的基本运行原理有了更深一步的了解,Very nice!

Reference