本文章为对 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库是一个非常流行的日志工具。 它提供了一个Logtrait(请参阅此处来获取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指出的此步骤中的错误