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:

  1. fork, which duplicates the process
  2. exec, 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 chrooted 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:

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:

  1. You need to get an auth token, but you don’t need a username/password
    Say your image is busybox/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>"
  2. 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
  3. 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)