Execute a program
Your task is to implement a very basic version of docker run
. It will be executed similar to docker run
:
mydocker run alpine:latest /usr/local/bin/docker-explorer echo hey
docker-explorer is a custom test program that exposes commands like echo
and ls
.
For now, don’t worry about pulling the alpine:latest
image. We will just execute a local program for this stage and print its output. You’ll work on pulling images from Docker Hub in stage 6.
Many languages hide this behind a nice API, but creating a new process on UNIX systems involves two steps:
fork
, which duplicates the processexec
, which replaces the duplicated process with another one
There’s some history behind this explained in this superuser post, and this Stackoverflow post.
package main
import (
"fmt"
"os"
"os/exec"
)
// Usage: your_docker.sh run <image> <command> <arg1> <arg2> ...
func main() {
command := os.Args[3]
args := os.Args[4:len(os.Args)]
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
fmt.Printf("Err: %v", err)
os.Exit(1)
}
}
package main
import (
"fmt"
// Uncomment this block to pass the first stage!
"os"
"os/exec"
)
// Usage: your_docker.sh run <image> <command> <arg1> <arg2> ...
func main() {
// You can use print statements as follows for debugging, they'll be visible when running tests.
// fmt.Println("Logs from your program will appear here!")
// Uncomment this block to pass the first stage!
//
command := os.Args[3]
args := os.Args[4:len(os.Args)]
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Err: %v", err)
os.Exit(1)
}
os.Exit(cmd.ProcessState.ExitCode())
}
Filesystem isolation
In the previous stage, we executed a program that existed locally on our machine. This program had write access to the whole filesystem, which means that it could do dangerous things!
In this stage, you’ll use chroot to ensure that the program you execute doesn’t have access to any files on the host machine. Create an empty temporary directory and chroot
into it when executing the command. You’ll need to copy the binary being executed too.
When executing your program within the chroot directory, you might run into an error that says open /dev/null: no such file or directory
. This is because Cmd.Run() and its siblings expect /dev/null
to be present. You can work around this by either creating an empty /dev/null
file inside the chroot directory, or by ensuring that Cmd.Stdout
, Cmd.Stderr
and Cmd.Stdin
are not nil
. More details about this here.
Just like the previous stage, the tester will run your program like this:
mydocker run alpine:latest /usr/local/bin/docker-explorer ls /some_dir
The official Docker implementation now uses pivot_root
instead of chroot
, for reasons listed here.
Note that in order to use pivot_root in Rust you’d need (as far as I know) to include nix as a dependency. Of course one can use libc instead, but just saying :-)
If you have trouble running executables via chroot
locally (say on a Mac with Apple Silicon), you can rely on codecrafters test
CLI to test your code (it runs tests on a remote machine)
On macOS with Apple Silicon Macs, mounting /proc
file system inside chroot
ed directory is mandatory. Otherwise running /usr/local/bin/docker-explorer
binary will result in following error:
rosetta error: Unable to open /proc/self/exe: 2
package main
import (
"fmt"
"os"
"os/exec"
// Uncomment this block to pass the first stage!
// "os"
// "os/exec"
)
func createChroot() string {
dir, err := os.MkdirTemp("", "chroot")
if err != nil {
panic(err)
}
cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("mkdir -p %s/usr/local/bin && cp /usr/local/bin/docker-explorer %s/usr/local/bin/docker-explorer", dir, dir))
if err := cmd.Run(); err != nil {
panic(err)
}
return dir
}
// Usage: your_docker.sh run <image> <command> <arg1> <arg2> ...
func main() {
switch command := os.Args[1]; command {
case "run":
dir := createChroot()
defer os.RemoveAll(dir)
cmd := exec.Command("chroot", append([]string{dir}, os.Args[3:]...)...)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("%v\n", err)
}
os.Exit(cmd.ProcessState.ExitCode())
default:
panic("mydocker: '" + command + "' is not a mydocker command.")
}
}
Process isolation
In the previous stage, we guarded against malicious activity by restricting an executable’s access to the filesystem.
There’s another resource that needs to be guarded: the process tree. The process you’re executing is currently capable of viewing all other processes running on the host system, and sending signals to them.
In this stage, you’ll use PID namespaces to ensure that the program you execute has its own isolated process tree. The process being executed must see itself as PID 1.
Just like the previous stage, the tester will run your program like this:
mydocker run alpine:latest /usr/local/bin/docker-explorer mypid
In the previous stage, we guarded against malicious activity by restricting an executable’s access to the filesystem.
There’s another resource that needs to be guarded: the process tree. The process you’re executing is currently capable of viewing all other processes running on the host system, and sending signals to them.
In this stage, you’ll use PID namespaces to ensure that the program you execute has its own isolated process tree. The process being executed must see itself as PID 1.
Just like the previous stage, the tester will run your program like this:
mydocker run alpine:latest /usr/local/bin/docker-explorer mypid
Needs a note here that on a mac, Cloneflags is not going to be available
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID,
}
I had to add this to the top of main.go
//go:build linux
// +build linux
and then it’ll be fine
Here’s a talk from a Docker employee that goes over how Docker uses cgroups, namespaces and more: https://www.youtube.com/watch?v=sK5i-N34im8.
This was a really helpful read for this step ⇒ https://itnext.io/container-runtime-in-rust-part-0-7af709415cda
package main
import (
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"syscall"
)
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
// Usage: your_docker.sh run <image> <command> <arg1> <arg2> ...
func main() {
command := os.Args[3]
args := os.Args[4:len(os.Args)]
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWPID}
dtmp, err := os.MkdirTemp("/tmp", "mydocker")
check(err)
defer os.RemoveAll(dtmp)
err = os.MkdirAll(dtmp+filepath.Dir(command), 0644)
check(err)
data, err := ioutil.ReadFile(command)
check(err)
err = ioutil.WriteFile(dtmp+command, data, 0777)
check(err)
err = syscall.Chroot(dtmp)
check(err)
err = cmd.Run()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
os.Exit(exitError.ExitCode())
}
}
}
// Step 1
// ./your_docker.sh ubuntu:latest /usr/local/bin/docker-explorer echo hey
// Step 2
// systemctl start docker
// gpasswd -a $USER docker
// alias mydocker='docker build -t mydocker . && docker run --cap-add="SYS_ADMIN" mydocker'
// ./your_docker.sh run <some_image> /usr/local/bin/docker-explorer echo 5144
Fetch an image from the Docker Registry
Your docker implementation can now execute a program with a fair degree of isolation - it can’t modify files or interact with processes running on the host.
In this stage, you’ll use the Docker registry API to fetch the contents of a public image on Docker Hub and then execute a command within it.
You’ll need to:
- Do a small authentication dance
- Fetch the image manifest
- Pull layers of an image and extract them to the chroot directory
The base URL for Docker Hub’s public registry is registry.hub.docker.com
.
The tester will run your program like this:
mydocker run alpine:latest /bin/echo hey
The image used will be an official image from Docker Hub. For example: alpine:latest
, alpine:latest
, busybox:latest
. When interacting with the Registry API, you’ll need to prepend library/
to the image names.
Since Go doesn’t have an archive extraction utility in its stdlib, you might want to shell out and use tar
.
Docker API can be very confusing:
- You need to get an auth token, but you don’t need a username/password
Say your image isbusybox/latest
, you would make a GET request to this URL:https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/busybox:pull
You’ll send the token you receive in following API calls as a header:"Authorization = Bearer <token>"
- Next step is to get the manifest. The URL to get the manifest will be:
https://registry.hub.docker.com/v2/library/busybox/manifests/latest
I’d recommend using"Accept = application/vnd.docker.distribution.manifest.v2+json"
header to get v2 of this response, which tells the media type for each layer - The last step is to download the layer and extract the files. The URL for the API call will be:
https://registry.hub.docker.com/v2/library/busybox/blobs/<sha256:xxxx>
Updated links for Docker registry:
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"strings"
"syscall"
)
type TokenResponse struct {
Token string `json:"token"`
AccessToken string `json:"access_token"`
Expires int `json:"expires_in"`
IssuedAt string `json:"issued_at"`
}
type ManiFest struct {
Name string `json:"name"`
Tag string `json:"tag"`
FSLayers []fsLayers `json:"fsLayers"`
}
type fsLayers struct {
BlobSum string `json:"blobSum"`
}
// Usage: your_docker.sh run <image> <command> <arg1> <arg2> ...
func main() {
img := os.Args[2]
split := strings.Split(img, ":")
repo := "library"
image := split[0]
tag := "latest"
if len(split) == 2 {
tag = split[1]
}
request, err := http.NewRequest("GET", fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", repo+"/"+image), nil)
if err != nil {
fmt.Printf("ERR!! %+v", err)
}
request.Header.Add("Accept", "application/json")
request.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(request)
var result TokenResponse
json.NewDecoder(resp.Body).Decode(&result)
// fmt.Printf("\n\nTOKEN => %+v\n\n", result.Token)
manifestReq, err := http.NewRequest("GET", fmt.Sprintf("https://registry.hub.docker.com/v2/%s/manifests/%s", repo+"/"+image, tag), nil)
if err != nil {
fmt.Printf("ERR!! %+v", err)
}
manifestReq.Header.Add("Authorization", "Bearer "+strings.TrimSpace(result.Token))
manifestReq.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v1+json")
mani, err := http.DefaultClient.Do(manifestReq)
if err != nil {
fmt.Printf("ERRRR => %+v", err)
}
var manifest ManiFest
json.NewDecoder(mani.Body).Decode(&manifest)
command := os.Args[3]
args := os.Args[4:len(os.Args)]
cmd := exec.Command(command, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
// Cloneflags: syscall.CLONE_NEWPID,
}
if err := os.MkdirAll("tmp_dir/dev/null", os.ModePerm); err != nil {
log.Fatal(err)
}
if err := os.MkdirAll("tmp_dir/usr/bin/", os.ModePerm); err != nil {
log.Fatal(err)
}
if err := os.MkdirAll("tmp_dir/bin/", os.ModePerm); err != nil {
log.Fatal(err)
}
defer os.RemoveAll("tmp_dir/")
input, err := ioutil.ReadFile(command)
if err != nil {
fmt.Println(err)
return
}
for _, value := range manifest.FSLayers {
req, err := http.NewRequest("GET", "https://registry-1.docker.io/v2/library/"+image+"/blobs/"+value.BlobSum, nil)
if err != nil {
fmt.Println("er1")
}
req.Header.Add("Authorization", "Bearer "+strings.TrimSpace(result.Token))
resp, err = http.DefaultClient.Do(req)
if err != nil {
fmt.Println("er2")
}
defer resp.Body.Close()
f, e := os.Create("tmp_dir/output")
if e != nil {
panic(e)
}
defer f.Close()
f.ReadFrom(resp.Body)
_, err = exec.Command("tar", "xf", "tmp_dir/output", "-C", "tmp_dir/").Output()
if err != nil {
fmt.Printf("OUT ERR untar => %+v", err)
}
// fmt.Printf("output => %+v", out)
os.RemoveAll("tmp_dir/output")
}
if err := syscall.Chroot("tmp_dir/"); err != nil {
log.Fatal(err)