In this post, we will prepare the field for our implementation.
Every programmer is different and some of you may go directly into Linux syscalls to create an isolated box and so on ... I prefer to always create a clean ground on which lay off my implementations, which I find is much more practical to read/understand later on, and it's always a good thing to practice clean implementations.
That will also provide bonus tips and tools about topics that may interest some less experiences Rust users, like argument parsing, errors handling, logging, etc ...
I will assume here that you already have So I guess you've heard that Rust's mascot is Ferris, the little cutie crab. We'll create a Rust binary project called Crabcan, and the objective will be to separate the different parts of the projects as distincly as possible to allow us to search in the code, tweak it and understand it again after months of pause. Run the command In the folder Ok let's dive directly into our project. First of all let's get the arguments from the command line. The objective is to get configurations from text-written flags while calling our tool. Command example This command will call The structopt crate is a very useful tool to parse arguments from the commandline (using the A detailed use of structopt and all its power is available in its documentation. One thing worth noticing is that the We are going to create a new file In Basically we expect the Now let's implement that function So here we first import our necessary dependencies These arguments are defined with the macro attribute Finally, we create the After setting some placeholders for arguments validation and logging initialisation, we return the arguments. One last thing, add the dependencies we just imported inside the Let's test our code with And this is it, our argument parsing works ! Now if we try The code for this step is available on github litchipi/crabcan branch "step1". Now that we got from the user its input, let's set up a way to give him outputs. Simple text is enough, but we want to separate debug information from basic information and errors. For this, there's a lot of tools, but I chose the crates The log crate is a very used tool to perform logging. It provides a In Loggers have to be initialized with a level of verbosity. This will define wether to display debug messages, or only errors, or nothing at all. On our case, we want it to display normal information by default, and increase verbosity to debug messages when the Let's initialize our logger in Yeah, a function is not really needed, but it's more readable isn't it ? Ok, now let's actually initialize logging right after getting the arguments from the commandline, in the Now that everything is in place, let's actually log something in our terminal ! In the The After testing we get the output: The code for this step is available on github litchipi/crabcan branch "step2". As a general practise it's good to take care of handling errors. When it comes to Rust, this language is far too powerful concerning errors handling to ignore them and not exploit them. I am no-one to teach how to properly handle errors, but this part will give an example of how errors can be managed in a large Rust project, and use Rust specific tools to handle them more easily. Let's create a Each time we will add a new error type, we'll add a variant to this enum. The But we may want to display a more complete message for each variant, allowing us to not get lost in codes and different numbers around our project. For this, let's implement the Linux executable returns a number when they exit, which describe how everything went. A return code of 0 means that there was no errors, and any other number describe an error and what that error is (based on the return code value). You can find here a table of special return codes and their meaning. We do not seek to perform bash automation scripts here with our tool, but we'll just set up a way to return 0 if there was no errors, and 1 if there was an error. This function exit the process with a return code got from the When a piece of the code is not working properly, we can handle the error using a Let's see how we can set this up in the If something goes wrong during the execution, we can simply write: Okay, but now we need to do something different in our Here, in case the arguments parsing was successful, we log the args and call the In case there was an error, we log it (notice the One final step, we have to set After testing, we can get the following output: The code for this step is available on github litchipi/crabcan branch "step3". Before diving into the real work, let's validatet the arguments passed from the commandline. We will just check that the The condition checks if the path (a If it isn't, we return a And we can add in the The code for this step is available on github litchipi/crabcan branch "step4". ?#attribute-like-macrosrustc
and cargo
installed, if you don't, please follow the instructions on the BookCreate the project
Well, let's put Ferris in a container ! :Dcargo new --bin crabcan
to create the project.
This will generate a Cargo.toml
file in which we can describe our project, add dependencies and tweak configurations of the rust compiler, a handy file to avoid having to avoid having to create rustc
commands by hand in a Makefile. You can change the author name, e-mail and version of your project here, but we won't add any dependencies yet.src/
you will put, well, all your sources. For now there's only a main.rs
file with a Hello World!
code inside.Parse the arguments
crabcan --mount ./mountdir/ --uid 0 --debug --command "bash"
crabcan
with the folder mountdir
to mount as root for the container, the UID number 0
, will output all debug
messages, and will execute the command bash
inside the container.Introducing the
structopt
crateclap
crates as a backend). The method is very straightforward, by defining a struct containing all the arguments:
/// text
part above an argument defined in the struct will be used as a message inside the helper (when you type crabcan --help
for example).Creating our argument parsing
src/cli.rs
containing everything related to commandline. For it to be used inside our project, we have to include it as a module of the project.src/main.rs
we replace the content with the following:
src/cli.rs
file to provide a function parse_args
that will return the struct containing all our configuration defined by the user through the commandline.
Note that because args
is not used, you will get a compiler warning.parse_args
in src/cli.rs
:use PathBuf;
use StructOpt;
structopt
but also PathBuf
from the standard library.
Then we define our Args
struct, containing all the arguments and information to be used for argument parsing. Let's look what arguments we are expecting: - debug
: Will be used to display debug messages or just normal logs - command
: The command that will be executed inside the container (with arguments) - uid
: The user ID that will be created to run the software inside the container. - mount_dir
: The folder to use as a root /
directory inside the container.
Note that this argument will be passed as mount in the commandlinestructopt(short, long)
to automatically create a short and long commandline argument from the field name. (The field toto
will be defined as arguments -t
and --toto
).parse_args
in which we gather the arguments from the commandline with thefrom_args
function of the struct (which was generated thanks to the derive(StructOpt)
macro attribute).Cargo.toml
file:# ...
[dependencies]
structopt = "0.3.23"
Testing our code
cargo run
:error: The following required arguments were not provided:
--command <command>
--mount <mount-dir>
--uid <uid>
USAGE:
crabcan [FLAGS] --command <command> --mount <mount-dir> --uid <uid>
For more information try --help
cargo run -- --mount ./ --uid 0 --command "bash" --debug
, we don't get any errors. You can add a println!("{:?}", args)
in our src/main.rs
file to get a nice output:Args { debug: true, command: "bash", uid: 0, mount_dir: "./" }
Patch for this step
The raw patch to apply on a freshly created project using cargo new --bin
can be found hereSetup Logging
The logging crates
log
and env_logger
to perform this task.Log
trait (see the Book for traits explanation) which defines all the function a logger has to have, and lets any other crate implement these functions. I chose the env_logger crate to implement these.Cargo.toml
, we add the following dependencies:# ...
log = "0.4.14"
env_logger = "0.9.0"
Setting up logging
--debug
flag is passed through the commandline.src/cli.rs
:
If you are into Rust code optimisation, you may want to inline this function .parse_args
functions, let's replace the placeholders with this piece of code:if args.debug else
Logging
main
function of src/main.rs
, we can output the args gotten into a info
message. This is done using the log::info!
macro.!; info
log
crate allows us to use error!
, warn!
, info!
, debug!
or trace!
message levels.[2021-09-30T10:17:46Z INFO crabcan] Args { debug: true, command: "/bin/bash", uid: 0, mount_dir: "./mountdir/" }
Patch for this step
The raw patch to apply on the previous step can be found herePrepare errors handling
The Errcode enum
src/errors.rs
file in which we'll define the following enum:// Allows to display a variant with the format {:?}
// Contains all possible errors in our tool
derive(Debug)
allows the enum to be displayed using a {:?}
format.std::fmt::Display
trait, defining the behaviour of an object when attempting to display it in a regular {}
format.use fmt;
// trait Display, allows Errcode enum to be displayed by:
// println!("{}", error);
// in this case, it calls the function "fmt", which we define the behaviour below
The
unreachable_patterns
attribute ensure that we won't get any warning from the compiler if the match
statement describe all the variants.Linux return codes
In our src/errors.rs
file, let's define a exit_with_errcode
function:use exit;
// Get the result from a function, and exit the process with the correct error code
get_retcode
function implemented by our Errcode
enum. Let's implement it in the most easy and stupid way:Result in Rust
Result
in Rust (see the Book for detailed explanation). Result<T, U>
expects two types, one type T
to return if it's a success, one type U
to return if there's an error. In our case, we want to return an Errcode
if there's an error, and return whatever we want if everything goes well.parse_args
function:return Err;
The
Result
in Rust are very useful and powerful and it's generally a good idea to use it everywhere you want error handling as it's the standard Rust way to do.main
depending on how the function ended with an error or a success. Let's use a match
statement to define what to do in both cases:match parse_args;
exit_with_retcode
with an Ok(())
value (it will simply exit with the return code 0). That is where we're going to place our container starting point later.{}
format on our Errcode
that will call the fmt
function of the Display
trait we implemented earlier), and simply exit with the return code associated.src/errors.rs
as a module of our project, and import theexit_with_retcode
function in our src/main.rs
file.use exit_with_retcode;
[2021-09-30T13:47:45Z INFO crabcan] Args { debug: true, command: "/bin/bash", uid: 0, mount_dir: "./mountdir/" }
[2021-09-30T13:47:45Z DEBUG crabcan::errors] Exit without any error, returning 0
Patch for this step
The raw patch to apply on the previous step can be found hereThanks to
filtoid
for his PR fixing an error in the code of this stepValidate arguments
mount_dir
actually exists, but this part can be extended with additional checks, as we add more options, etc ...
Let's replace the placeholders in src/cli.rs
with the actual arguments validation:PathBuf
type as we defined in our Args
struct) exists and if it's a directory.Result::Err
with our Errcode
enum with a custom variantArgumentInvalid
, specifying that the fault was on argument mount
.
In src/errors.rs
, we will define this variant:match
statement of the fmt
function the following:match &self
Patch for this step
The raw patch to apply on the previous step can be found hereSpecial thanks to @kevinji who pointed out an error in the code of this step :D