In this exercise, we will create our first “container”, using filesystem namespacing.
We have provided a new Vagrantfile that includes some extra packages and avoids most of the issues from the first week. Rather than using the VirtualBox user interface, you can interact with the virtual machine by running vagrant ssh
on the host, from the same directory as the Vagrantfile
:
$ vagrant up --provision
$ vagrant ssh
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-70-generic x86_64)
[...]
vagrant@ubuntu-focal:~$
As before, the directory containing the Vagrantfile
will be shared with the virtual machine at /home/vagrant/exercises/
.
First, you will write a (very) basic Dockerfile containing the application that you wrote last week.
Since we built the program using musl-gcc -static
, rather than the usual gcc
or clang
, it uses the standard library musl, which is incorporated into the binary itself, so that it does not require any libraries from the operating system.
You can then start your Dockerfile with
FROM alpine:latest
Fill in the rest based on the Dockerfile reference, so that you have an image containing the fib
binary in /usr/bin
. Hint: you will need the ADD
and CMD
commands.
Then, build your image with
$ docker build -t fib .
and run it as follows:
$ docker run -it fib
Fibonacci numbers:
1
1
2
3
5
8
13
21
34
55
Now, rather than using Docker, you will manually create a filesystem image for your container using overlayfs
.
The following instructions will not work inside a shared folder, and so you must create the directories outside of /home/vagrant/exercises
.
First, create a directory rootfs/
, and inside it, extract the Alpine Linux “mini root filesystem”, which you can download here.
Next, you will add your application to the image. Create four new directories with mkdir
:
mountpoint/
app/
writedir/
overlay-work/
Then, mount the filesystem (note: all one line):
$ sudo mount -t overlay overlay -olowerdir=rootfs,upperdir=app,workdir=overlay-work mountpoint/
Look at the filesystem under mountpoint/
; it should look like rootfs/
. Now copy the fib
binary to mountpoint/usr/bin
, like in Part 1; where does it get stored? After you are done, unmount the filesystem with
$ sudo umount mountpoint/
Finally, you will “containerise” your program by creating a new filesystem namespace for it, using the root filesystem that you set up in Part 2.
You will write a program container-fs
that will accept a list of layers, and then run the program in a new mount-point namespace, whose root filesystem is built up from the provided layers. It will run like the following:
$ sudo ./container-fs rootfs app writedir -- sh
[...some output here...]
/ #
First, take container-template.c
and fill in the following details (you can find where to add it by searching for YOUR CODE HERE <number>
, e.g. YOUR CODE HERE 4
).
Parse the command-line arguments. C programs start by calling the entry point int main(int argc, char*** argv)
. The argc
parameter contains the number of command-line arguments (including the name of the program), and argv
contains an array of (pointers to) null-terminated strings. Your first job is to make sure that the command-line arguments really do take the form above, and identify how many layers have been provided. Though you can’t know in advance how many layers there will be, you should make sure that there are at least two.
Next, you need to work out what are the values of argc
and argv
for the containerised process (in the example above, 1
and { "sh" }
respectively). These are stored in an args_t
structure to be passed to the new process created by clone(2)
. Pointer arithmetic will be helpful here: argv + n
, where n
is an integer, points to the nth element of argv
.
Set up the overlay filesystem. You saw in Part 1 how to mount an overlay filesystem from the command line; here you need to do the same thing using the mount(2)
system call. You will find documentation on overlayfs’s option string format here. To save you some time and effort when constructing the option string, we have provided a function append_string
in utils.c
that will let you progressively build a string up from small components. We have also provided a function escape_directory
that will add the appropriate backslashes to a path so that the overlay filesystem won’t misinterpret paths containing special characters such as commas.
Set up the environment for the containerised program. The containerised process needs more than just a root filesystem: there are other filesystems mounted e.g. at /proc
that are used to communicate with the kernel. Mount these filesystems too.
Run the program. Now that the environment is mostly set up, we can run the program. Use the execvp(3)
library function to run the command line passed to clone_main
. The execvp(3)
function should never return, so if it does then make sure to report the error.
Hint: make sure to read the execvp(3)
man page carefully, as you cannot pass arg->argv
directly to execvp(3)
. The calloc(3)
function and the sizeof
operator will be helpful.
Wait for the containerised program to complete. You need to wait for the containerized application to complete before unmounting its filesystem. This can be done using the waitpid(2) system call.
You should now be able to compile and run (as root) the application with
$ gcc -o container-fs container-template.c utils.c
$ sudo ./container-fs /path/to/the/previous/rootfs /path/to/the/previous/app /path/to/the/previous/writedir -- sh
[...some output here...]
/ # fib
Fibonacci numbers:
1
1
2
3
5
8
13
21
34
55