Use GoLang to write a Docker from scratch (mirroring articles) - "Handwriting Docker by yourself" reading notes

Use GoLang to write a Docker from scratch (mirroring articles) - "Handwriting Docker by yourself" reading notes

series:

  1. Use GoLang to write a Docker from scratch (concepts) - "Handwriting Docker by yourself" reading notes
  2. Use GoLang to write a Docker from scratch (container)-"Do it yourself Docker" reading notes
  3. Use GoLang to write a Docker from scratch (mirroring articles) - "Handwriting Docker by yourself" reading notes
  4. Use GoLang to write a Docker from scratch (container advanced/final?) - "Handwriting Docker by yourself" reading notes

This article is the third in the series-the mirror article.

1. The shortcomings of the previous small demo

After finishing the previous work, there will be a feeling that the container has been realized, the only difference is the feeling of various details. But there are actually many problems.

  • For example, if you use ls, you will find that it is still in the parent thread's directory.
  • The mount points are also inherited from the parent process.

This is different from our usual use of docker. This is the lack of mirroring.

2. This demo --busybox mirror

What this demo wants to achieve is the image of busybox. Busybox provides many unix tools. It is a very useful mirror. This time we will implement this mirroring.

3. Implement rootfs

3.1 init.go

This is very long, let's take it step by step:

  1. 1. we put all the mount steps in a function setupMount.
  2. Among them, in addition to the/and proc that have been mounted before, a pivotRoot is also called.
  3. In pivot_root, we did the following things:
    1. 1. we remount root and let it bind itself. This root actually refers to the root directory of the image file.
      • ? What's the point of doing this:
        • Because we want to ensure that root is a mount point, which will be useful later.
    2. Then we create a new folder called .put_old, any name will do, just for temporary storage.
    3. Then we call syscall.pivotRoot(root, putOld), this function will move the current rootfs mount to the second parameter, where our second parameter is the folder created in the previous step. In other words, we moved the current rootfs mount to .put_old.
      • If you enter the .put_old folder and call ls at this time, you will find that this is the root directory on your operating system that you are most familiar with.
    4. In addition, pivotRoot will take the first parameter as the new rootfs mount. And our first parameter is root (the root directory of the image file)
    5. unmount the old rootfs mount, which is .put_old
    6. Delete .put_old.
    7. With this set, our new rootfs mount will no longer see the old rootfs mount.
//already in container //initiate the container func InitProcess () error { //read command from pipe, will plug if write side is not ready containerCmd := readCommand() if containerCmd == nil || len (containerCmd) == 0 { return fmt.Errorf( "Init process fails, containerCmd is nil" ) } //setup all mount commands if err:= setupMount(); err != nil { logrus.Errorf( "setup mount fails: %v" , err) return err } //look for the path of container command //so we don't need to type "/bin/ls", but "ls" commandPath, err := exec.LookPath(containerCmd[ 0 ]) if err != nil { logrus.Errorf( "initProcess look path fails: %v" , err) return err } //log commandPath info //if you type "ls", it will be "/bin/ls" logrus.Infof( "Find commandPath: %v" , commandPath) if err := syscall.Exec(commandPath, containerCmd, os .Environ()); err != nil { logrus.Errorf(err.Error()) } return nil } func readCommand () [] string { //3 is the index of readPipe pipe := os.NewFile( uintptr ( 3 ), "pipe" ) msg, err := ioutil.ReadAll(pipe) if err != nil { logrus.Errorf( "read pipe fails: %v" , err) return nil } return strings.Split( string (msg), "" ) } //integration of all mount commands func setupMount () error { //ensure that container mount and parent mount has no shared propagation if err := syscall.Mount( "" , "/" , "" , syscall.MS_PRIVATE|syscall.MS_REC, "" ); err != nil { logrus.Errorf( "mount/fails: %v" , err) return err } //get current directory pwd, err := os.Getwd() if err != nil { return err } logrus.Infof( "current location is: %v" , pwd) //use current directory as the root if err:= pivotRoot(pwd); err != nil { logrus.Errorf( "pivot root fails: %v" , err) return err } //mount proc filesystem defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV if err := syscall.Mount( "proc" , "/proc" , "proc" , uintptr ( defaultMountFlags ), "" ); err != nil { logrus.Errorf( "mount/proc fails: %v" , err) return err } //mount tmpfs if err := syscall.Mount( "tmpfs" , "/dev" , "tmpfs" , syscall.MS_NOSUID | syscall.MS_STRICTATIME, "mode=755" ); err != nil { logrus.Errorf( "mount/dev fails: %v" , err) return err } return nil } //change the container rootfs to image rootfs func pivotRoot (root string ) error { //what it does? //remember root is just a parameter now, it's not the rootfs, it's what we want to create //this command ensure that root is a mount point which bind itself if err:= syscall.Mount(root , root, "bind" , syscall.MS_BIND | syscall.MS_REC, "" ); err != nil { return fmt.Errorf( "remount root fails: %v" , err) } //create the putOld directory to store old putOld := path.Join(root, ".put_old" ) if err:= os.Mkdir(putOld, 0777 ); err != nil { return fmt.Errorf( "create putOld directory fails: %v" , err) } //pivot old root mount to putOld //and mount the first parameter as the new root mount //which means,'/.put_old/' is the old rootfs //the first parameter must be a mount point, that's why we remount root itself at the beginning if err := syscall.PivotRoot(root, putOld); err != nil { return fmt.Errorf( "pivot_root fails: %v" , err) } //chdir do exactly the same as cd. chdir is a syscall, cd is a program //change to root directory if err:= syscall.Chdir( "/" ); err != nil { return fmt.Errorf( "chdir fails: %v" , err) } //after the previous process, the current filesystem is the new root //the old filesystem is .put_old //finally, we need to unmount the old root mount before remove it //change the putOld dir, because we are in the new rootfs now //the root became "/" putOld = path.Join( "/" , ".put_old" ) if err:= syscall.Unmount(putOld, syscall.MNT_DETACH); err != nil { return fmt.Errorf( "unmount fails: %v" , err) } //remove the old mount point return os.Remove(putOld) } Copy code

4. Implement AUFS

4.1 First implement aufs mount

Although there is a lot of code here, the main thing is to implement the last section of my first article .
The file is containerProcess.go

//containerProcess.go func NewProcess (tty bool ) (*exec.Cmd, *os.File) { readPipe, writePipe, err := os.Pipe() if err != nil { logrus.Errorf( "New Pipe Error: %v" , err) return nil , nil } //create a new command which run itself //the first arguments is `init` which is in the "container/init.go" file //so, the <cmd> will be interpret as "docker init <containerCmd>" cmd := exec.Command( "/proc/self/exe" , "init" ) //new namespaces, thanks to Linux cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET, } //this is what presudo terminal means //link the container's stdio to os if tty { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr } cmd.ExtraFiles = []*os.File{readPipe} imagesRootURL := "./images/" mntURL := "./mnt/" newWorkspace(imagesRootURL, mntURL) cmd.Dir = mntURL return cmd, writePipe } func newWorkspace (imagesRootURL string , mntURL string ) { createReadOnlyLayer(imagesRootURL) createWriteLayer(imagesRootURL) createMountPoint(imagesRootURL, mntURL) } func createReadOnlyLayer (imagesRootURL string ) { readOnlyLayerURL := imagesRootURL + "busybox/" imageTarURL := imagesRootURL + "busybox.tar" isExist, err := pathExist(readOnlyLayerURL) if err != nil { logrus.Infof( "fail to judge whether path exist: %v" , err) } if isExist == false { if err := os.Mkdir(readOnlyLayerURL, 0777 ); err != nil { logrus.Errorf( "fail to create dir %s: %v" , readOnlyLayerURL, err) } if _, err := exec.Command( "tar" , "-xvf" , imageTarURL, "-C" , readOnlyLayerURL).CombinedOutput(); err != nil { logrus.Errorf( "fail to untar %s: %v" , imageTarURL, err) } } } func createWriteLayer (imagesRootURL string ) { writeLayerURL := imagesRootURL + "writeLayer/" if err := os.Mkdir(writeLayerURL, 0777 ); err != nil { logrus.Errorf( "fail to create dir %s: %v" , writeLayerURL, err) } } func createMountPoint (imagesRootURL string , mntURL string ) { if err := os.Mkdir(mntURL, 0777 ); err != nil { logrus.Errorf( "fail to create dir %s: %v" , mntURL, err) } //mount the readOnly layer and writeLayer on the mntURL dirs := "dirs=" + imagesRootURL + "writeLayer:" + imagesRootURL + "busybox" cmd := exec.Command( "mount" , "-t" , "aufs" , "-o" , dirs, "none" , mntURL) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err:=cmd.Run(); err != nil { logrus.Error(err) } } func pathExist (path string ) ( bool , error) { _, err := os.Stat(path) if err == nil { return true , nil } if os.IsNotExist(err) { return false , nil } return false , err } Copy code

4.2 Delete AUFS

In docker, exiting the container is also accompanied by deleting the write layer. There are three steps here:

  1. unmount mnt directory (note that unmount in the linux command line is called umount without n)
  2. Delete the mnt directory
  3. Delete write layer

code show as below:

4.2.1 containerProcess.go

//delete AUFS (delete write layer) func DeleteWorkspace (imagesRootURL string , mntURL string ) { deleteMount(mntURL) deleteWriteLayer(imagesRootURL) } func deleteMount (mntURL string ) { cmd := exec.Command( "umount" , mntURL) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { logrus.Errorf( "umount fails: %v" , err) } if err := os.RemoveAll(mntURL); err != nil { logrus.Errorf( "remove dir %s error: %v" , mntURL, err) } } func deleteWriteLayer (imagesRootURL string ) { writeLayerURL := imagesRootURL + "writeLayer/" if err:= os.RemoveAll(writeLayerURL); err != nil { logrus.Errorf( "remove dir %s error: %v" , writeLayerURL, err) } } Copy code

4.2.2 run.go

  1. When the container process ends, the function defined above is called.
//This is the function what `docker run` will call func Run (tty bool , containerCmd [] string , res *subsystems.ResourceConfig) { //this is "docker init <containerCmd>" initProcess, writePipe := container.NewProcess(tty) //start the init process if err := initProcess.Start(); err != nil { logrus.Error(err) } //create container manager to control resource config on all hierarchies //this is the cgroupPath cm := cgroups.NewCgroupManager( "oyishyi-docker-first-cgroup" ) defer cm.Remove() if err := cm.Set(res); err != nil { logrus.Error(err) } if err := cm.AddProcess(initProcess.Process.Pid); err != nil { logrus.Error(err) } //send command to write side //will close the plug sendInitCommand(containerCmd, writePipe) if err := initProcess.Wait(); err != nil { logrus.Error(err) } imagesRootURL := "./images/" mntURL := "./mnt/" container.DeleteWorkspace(imagesRootURL, mntURL) os.Exit( -1 ) } Copy code

5. Realize volume data persistence

5.1 Add volume

Volume only needs to add a createVolume function to the previously defined newWorkspace.

func createVolume (imagesRootURL string , mntURL string , volume string ) { if volume == "" { return } //extract url from volume input volumeURls := strings.Split(volume, ":" ) if len (volumeURls) == 2 && volumeURls[ 0 ] != "" && volumeURls[ 1 ] != "" { mountVolume(imagesRootURL, mntURL, volumeURls) logrus.Infof( "volume created: %q" , volumeURls) } else { logrus.Warn( "volume path not valid" ) } } func mountVolume (imagesRootURL string , mntURL string , volumeURls [] string ) { hostVolumeURL := volumeURls[ 0 ] containerVolumeURL := mntURL + volumeURls[ 1 ] //create host dir, which store the real data if err:= os.Mkdir(hostVolumeURL, 0777 ); err != nil { logrus.Errorf( "create host volume directory fails: %v" , err) } //create container dir, which is a mount point //currently not in container(container root is not "/"), so mntURL prefix is needed if err:= os.Mkdir(containerVolumeURL, 0777 ); err != nil { logrus.Errorf( "create container %s volume fails: %v" , containerVolumeURL, err) } dirs := "dirs=" + hostVolumeURL cmd := exec.Command( "mount" , "-t" , "aufs" , dirs, "none" , containerVolumeURL) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err:= cmd.Run(); err != nil { logrus.Errorf( "mount container volume fails: %v" , err) } } Copy code

5.2 Delete volume

Delete volume can be combined with the previous deleteMount function, which is named deleteMountWithVolume:

  1. umount volume mount
  2. umount whole container mount
  3. delete whole container mount(consequently deleting volume mount)
func deleteMountWithVolume (mntURL string , volume string ) { //umount volume mount volumeURls := strings.Split(volume, ":" ) if len (volumeURls) == 2 && volumeURls[ 0 ] != "" && volumeURls[ 1 ] != "" { volumeMountURL := mntURL + volumeURls[ 1 ] cmd := exec.Command( "umount" , volumeMountURL) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { logrus.Errorf( "umount volume mount %s fails: %v" , volumeMountURL, err) } } //umount container mount cmd := exec.Command( "umount" , mntURL) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { logrus.Errorf( "umount container mount %s fails: %v" , mntURL, err) } //delete whole container mount if err := os.RemoveAll(mntURL); err != nil { logrus.Errorf( "remove dir %s error: %v" , mntURL, err) } } Copy code

5.3 Implementation of docker commit

As the name suggests, docker commit is implemented because it is very simple.
In fact, just pack the mnt folder and it's done.

5.3.1 commands.go

var commitCommand = cli.Command{ Name: "commit" , Usage: "commit the container into image" , Action: func (context *cli.Context) error { args := context.Args() if args.Len() == 0 { return errors.New( "Commit what?" ) } imageName := args.Get( 0 ) dockerCommands.CommitContainer(imageName) return nil }, } Copy code

5.3.2 dockerCommands/commit.go

package dockerCommands import ( "github.com/sirupsen/logrus" "os/exec" ) func CommitContainer (imageName string ) { mntURL := "./mnt" storeURL := "./images/" + imageName + ".tar" logrus.Infof( "stored path: %v" , storeURL) cmd := exec.Command( "tar" , "-czf" , storeURL, "-C" , mntURL, "." ) if err := cmd.Run(); err != nil { logrus.Errorf( "Tar folder %s fails %v" , mntURL, err) } } Copy code