本文章为对 Litchi Pi
的《Writing a container in Rust》的翻译转载,不享受任何著作权利,不用于任何商业目的,不以任何许可证进行授权,不对任何转载行为尤其是商业转载行为负责。一切权利均由原作者 Litchi Pi 保有。个人翻译能力有限,如有疑问可查看原文。
开始项目
在这篇文章中,我们将为我们的实现做好准备。
每个程序员都各自有不同的习惯,一些人可能会直接通过Linux的系统调用来创建一个隔离的box。而我则更喜欢创建一个干净的基础环境来做我自己的实现,我发现这么做更有利于以后的阅读和理解,并且做干净的实现总是好事。
同样的,这也能提供一些rust新手感兴趣的额外的一些tips和工具,比如参数解析、错误处理和日志记录等等.
在阅读这篇文章之前,我假定你们已经安装好了rustc
以及cargo
,如果你没有安装这两个工具,请按照本书说明进行操作。
创建项目
我猜你已经听说过Rust的吉祥物是Ferris这只又小又可爱的螃蟹了。
好,让我们吧Ferris放进容器里吧!:D

我们将创建一个名为Crabcan的Rust binary项目,目标是尽可能对这个项目的不同部分进行拆分,以便于我们在代码中进行搜索和调整,以及在数月的暂停后也能对项目很好的重新理解。
首先我们运行cargo new --bin crabcan
来创建这个项目。
这个命令会生成一个Cargo.toml
文件,我们可以通过这个文件来为项目添加描述、添加依赖项、调整Rust编译器配置。它能避免我们手动在Makefile中创建rustc命令,是一个很方便的文件。你现在可以在这个文件中修改作者的名字和邮箱、版本等,不过目前我们还不会在这个文件中添加任何依赖项。
在文件夹src/
中,您将放置所有源代码。不过目前只有一个写了Hello World!
代码的main.rs
文件在这个文件夹里。
解析参数
OK,现在我们直接打开我们的项目。首先我们要从命令行获取参数。
目标是在调用我们的工具时从给出的文本形式的的flag中获取配置。
命令执行例子
1 | crabcan --mount ./mountdir/ --uid 0 --debug --command "bash" |
这个命令会调用crabcan
,以root身份将mountdir
挂载到容器根目录,UID为0
,输出所有debug
消息,并将在容器内执行命令bash
。
structopt
库介绍
structopt库是一个用于解析来自命令行的参数非常有用的工具(底层使用了clap库)。 使用方法非常简单,我们只需要定义一个包含所有参数的结构体:
1 |
|
structopt的详细用法和所有功能可在其文档中找到。值得注意的是,结构体中定义的参数上方的/// text
部分将用作帮助信息(例:当输入crabcan --help
时出现的信息)。
创建我们自己的参数解析
我们需要先创建一个新文件src/cli.rs
,这个文件中包含了与命令行相关的全部内容。为了在我们的项目中使用它,我们必须先将他作为一个模块来引入到我们的项目中。
我们先在src/main.rs
中把文本替换为以下内容:
1 | mod cli; |
我们期望src/cli.rs
能提供一个基础的parse_args
函数,这个函数会返回包含所有我们通过命令行定义的配置的结构体。
注意:由于args
并没有被使用,你会看见编译器出现的对应warning
现在让我们在src/cli.rs
中实现这个parse_args
函数:
1 | use std::path::PathBuf; |
在这里 我们必须导入我们必需的依赖库structopt
以及在标准库中的PathBuf
接下来我们定义Args
结构体,它包含了所有参数以及用于解析参数的信息,让我们先来看看我们需要哪些参数:
debug
: 用于显示调试消息或仅显示正常日志command
: 在容器内执行的命令(带参数)uid
: 在容器内作为应用运行用户的userIDmount_dir
: 用作容器内根目录/
的文件夹。
注意:这个参数会以命令行中mount的形式来传递
这些加上了宏属性(macro attribute)structopt(short, long)
定义的参数,会根据字段名称来自动创建对应的short命令行参数和long命令行参数。(如字段 toto 将被定义为参数 -t 和 –toto)。
终于,我们创建了parse_args
函数,它通过结构体的from_args
函数(由derive(StructOpt)
宏生成)中来获取命令行参数。
在为参数验证和日志初始化设置一些占位符后,我们将参数返回。
最后我们把刚刚引入的包在Cargo.toml
文件中作为依赖引入:
1 | # ... |
测试我们的代码
让我们使用cargo run
来运行我们的代码
1 | error: The following required arguments were not provided: |
就是这样,我们的参数解析功能运行成功了!现在如果我们尝试运行cargo run -- --mount ./ --uid 0 --command "bash" --debug
的话,不会出现任何错误,你也可以在我们的src/main.rs
中添加println!("{:?}", args)
来得到还不错的输出:
1 | Args { debug: true, command: "bash", uid: 0, mount_dir: "./" } |
Patch for this step
这一步的代码可以在github litchipi/crabcan branch “step1”中找到.
应用于cargo new --bin
创建的新项目的原始补丁可以在此处找到
添加日志记录
现在我们已经成功获得了用户的输入信息,让我们用某些方式来为他提供输出。其实需要简单的文本就足够满足需求,但是我希望能将调试信息与基础的输出信息以及错误信息分开。因此,尽管有很多工具可供选择,但我选择了log
库和env_logger
库来实现这个功能。
log库是一个非常流行的日志工具。 它提供了一个Log
trait(请参阅此处来获取trait的解释),它定义了日志记录工具必须具有的所有功能,并允许任何其他crate实现这些功能。我选择了env_logger库来实现这些功能。
我们在Cargo.toml
添加下面的依赖:
1 | # ... |
添加日志记录功能
日志记录工具必须设置一个详细程度等级来进行初始化,这个等级定义了是否显示调试信息、仅显示错误信息或者是完全不显示日志信息。在我们的例子中,我们希望它在默认的情况下显示普通信息,并在我们通过命令行传递了--debug
标志的情况下提高debug信息的详细程度。
让我们在src/cli.rs
中初始化我们的日志记录器:
1 | pub fn setup_log(level: log::LevelFilter){ |
诚然,将它单独作为一个函数可能并不是很有必要,但是这么做的话代码会更有可读性,不是吗?
如果你对Rust代码优化感兴趣的话,你可能想内联这个函数。
关于内联函数,也可查看这篇中文翻译[Rust 中的内联](https://nihil.cc/posts/translate_rust_inline/
OK,现在我们需要在从命令行获取到参数后立刻初始化日志记录,让我们在parse_args
函数中 用这段代码替换之前我们保留的占位符:
1 | if args.debug{ |
记录日志信息
现在一切都就绪l,让我们在终端中记录一些内容!在src/main.rs
的main
函数中,我们可以将获取的参数输出到info
信息中。这可以通过log::info!
宏来实现
1 | log::info!("{:?}", args); |
log
库给我们提供了error!
、warn!
、info!
、debug!
、trace!
五个级别。
测试后我们将得到输出:
1 | [2021-09-30T10:17:46Z INFO crabcan] Args { debug: true, command: "/bin/bash", uid: 0, mount_dir: "./mountdir/" } |
Patch for this step
这一步的代码可以在github litchipi/crabcan branch “step2”中找到.
从前一步到这一步的原始补丁可以在此处找到。
准备错误处理
作为通常的练习,注意错误处理会是一个好习惯。Rust这门语言在错误处理方面非常强大,以至于我们无法在错误处理上忽略或者遗漏这个特点。
Errcode 枚举
让我们创建一个定义了如下enum(枚举)的src/errors.rs
文件:
1 | // Allows to display a variant with the format {:?} |
每次我们添加新的错误类型时,我们都会向该enum添加一个可变体(variant)。derive(Debug)
允许使用{:?}
来格式化显示enum。
我们可能希望为每个可变体显示更完整的消息,以防我们在项目中的代码和数字使我们晕头转向。为此,让我们来实现std::fmt::Display
特性,定义该对象尝试在常规{}
格式中显示时的行为。
1 | use std::fmt; |
unreachable_patterns
属性确保在match
语句描述了所有可变体时,不会收到编译器的任何警告信息。
Linux返回码
Linux可执行文件在退出时会返回一个数字,它描述了一切的进展情况。 返回代码 0 表示没有错误,任何其他数字都用于描述错误以及该错误的详情(基于返回代码值)。 您可以在此处找到有关特殊返回代码及其含义的表。
我们并不追求通过我们的工具来执行bash自动化脚本,只要设置一个如果有错误则返回1,没有错误则返回0的方式就可以了。
让我们在src/errors.rs
文件中定义一个exit_with_errcode
函数:
1 | use std::process::exit; |
这个函数会使用我们在Errcode
这个enum中实现的get_retcode
函数提供的返回状态码来退出进程,让我们先用最简单夜色最笨的办法来实现它:
1 | impl Errcode{ |
Rust中的Result
当一段代码无法正常工作时,我们可以通过Rust中的Result来进行错误处理(详情请参阅此处)Result<T, U>
需要两种类型,一种是成功时返回的T
类型,一种是发生错误时返回的U
类型。在我们的例子中,如果出现错误,我们希望返回Errcode
,如果一切顺利,则返回我们想要的任何内容。
让我们看看我们怎么在parse_args
函数中进行设置:
1 | pub fn parse_args() -> Result<Args, Errcode> { |
如果在程序运行过程中出现了某种错误,我们只要简单写上:
1 | return Err(Errcode::MyErrorType); |
Rust中的
Result
非常有用而且强大,一般来说。通常在任何你想要错误处理的地方使用都是个好主意,因为这是Rust中错误处理的标准方式。
OK,现在我们需要在main
函数中对函数成功或者失败两种状态分别做不同的处理,让我们使用match
语句来定义这两种情况下要做哪些事:
1 | match cli::parse_args(){ |
在这段代码中,如果参数解析成功,那么我们在日志中打印参数并使用OK()
作为参数来调用exit_with_retcode
(这个函数会简单的返回0),我们稍后会将这里作为我们放置容器的起点。
如果出现了错误,我们会在日志中打印它(注意Errcode
上的{}
格式化方式 ,它将调用我们之前实现的Display
trait中的 fmt 函数)然后简单地退出并返回相关的返回代码。
最后一步,我们必须将src/errors.rs
设置本为项目的一个模块,并在src/main.rs
文件中导入exit_with_retcode
函数。
1 | mod errors; |
在测试完成后,我们可以得到如下输出:
1 | [2021-09-30T13:47:45Z INFO crabcan] Args { debug: true, command: "/bin/bash", uid: 0, mount_dir: "./mountdir/" } |
Patch for this step
这一步的代码可以在github litchipi/crabcan branch “step3”中找到.
前一步到这一步的原始补丁可以在此处找到
感谢
filtoid
为修复此步骤中出现的错误所提的PR
校验参数
在我们深入实际工作之前,我们先校验一下从命令行传入的参数,此处我们仅仅检查一下mount_dir
是否存在,不过如果有我们添加了更多选项等情况,可以通过额外的检查来对这一步进行拓展。
让我们用的实际参数验证代码替换src/cli.rs
中的占位符:
1 | pub fn parse_args() -> Result<Args, Errcode> { |
这个条件语句检验了路径(我们在Args
结构体中定义的PathBuf
类型)是否存在以及这个路径是否为目录。
如果不满足这个条件,我们会返回Result::Err
以及带有自定义可变体ArgumentInvalid
的Errcode
enum ,指明错误发生在参数mount
部分。
我们先在src/errors.rs
中定义这个可变体:
1 | pub enum Errcode{ |
接下来我们可以在fmt
函数的match
语句下添加以下内容:
1 | match &self{ |
Patch for this step
这一步的代码可以在github litchipi/crabcan branch “step4”中找到.
前一步到这一步的原始补丁可以在此处找到
特别感谢 @kevinji指出的此步骤中的错误