自己动手写docker2

时间:Jan. 11, 2021 分类:

目录:

构建镜像

pivot_root

pivot_root是一个系统调用,用于改变root文件系统。pivot_root可以将当前进程的root文件系统移动到put_old目录,然后使new_root成为新的root文件系统。new_root和put_old不能同时存在当前root的同一个文件系统

pivot_root和chroot不同的是,pivot_root将整个系统切换到一个新的目录,而移除了对之前root文件系统的依赖,可以umount掉原来的root文件系统。而chroot是针对进程,而系统的其他部分还运行在旧的root文件系统

pivotRoot方法逻辑为

func pivotRoot(root string) error {
    /**
      为了使当前root的老 root 和新 root 不在同一个文件系统下,我们把root重新mount了一次
      bind mount是把相同的内容换了一个挂载点的挂载方法
    */
    if err := syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
        return fmt.Errorf("Mount rootfs to itself error: %v", err)
    }
    // 创建 rootfs/.pivot_root 存储 old_root
    pivotDir := filepath.Join(root, ".pivot_root")
    if err := os.Mkdir(pivotDir, 0777); err != nil {
        return err
    }
    // pivot_root 到新的rootfs, 现在老的 old_root 是挂载在rootfs/.pivot_root
    // 挂载点现在依然可以在mount命令中看到
    if err := syscall.PivotRoot(root, pivotDir); err != nil {
        return fmt.Errorf("pivot_root %v", err)
    }
    // 修改当前的工作目录到根目录
    if err := syscall.Chdir("/"); err != nil {
        return fmt.Errorf("chdir / %v", err)
    }

    pivotDir = filepath.Join("/", ".pivot_root")
    // umount rootfs/.pivot_root
    if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil {
        return fmt.Errorf("unmount pivot_root dir %v", err)
    }
    // 删除临时文件夹
    return os.Remove(pivotDir)
}

进行pivotRoot和挂载操作

func setUpMount() {
    pwd, err := os.Getwd()
    if err != nil {
        log.Errorf("Get current location error %v", err)
        return
    }
    log.Infof("Current location is %s", pwd)
    pivotRoot(pwd)

    //mount proc
    defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
    syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")

    syscall.Mount("tmpfs", "/dev", "tmpfs", syscall.MS_NOSUID|syscall.MS_STRICTATIME, "mode=755")
}

根目录可以使用busybox的

mkdir busybox
docker pull busybox
docker run -d busybox top -b
docker export -o busybox.tar <container-id>
tar xf busybox.tar -C busybox/
cd busybox

在busybox目录启动容器

使用aufs包装busybox

docker在启动容器的时候,除了镜像的只读层larer,会启动两个layer,container-init layer和write layer

创建只读层,将tar包解压到指定目录

func CreateReadOnlyLayer(rootURL string) {
    busyboxURL := rootURL + "busybox/"
    busyboxTarURL := rootURL + "busybox.tar"
    exist, err := PathExists(busyboxURL)
    if err != nil {
        log.Infof("Fail to judge whether dir %s exists. %v", busyboxURL, err)
    }
    if exist == false {
        if err := os.Mkdir(busyboxURL, 0777); err != nil {
            log.Errorf("Mkdir dir %s error. %v", busyboxURL, err)
        }
        if _, err := exec.Command("tar", "-xvf", busyboxTarURL, "-C", busyboxURL).CombinedOutput(); err != nil {
            log.Errorf("Untar dir %s error %v", busyboxURL, err)
        }
    }
}

创建可写层

func CreateWriteLayer(rootURL string) {
    writeURL := rootURL + "writeLayer/"
    if err := os.Mkdir(writeURL, 0777); err != nil {
        log.Errorf("Mkdir dir %s error. %v", writeURL, err)
    }
}

通过aufs合并挂载

func CreateMountPoint(rootURL string, mntURL string) {
    if err := os.Mkdir(mntURL, 0777); err != nil {
        log.Errorf("Mkdir dir %s error. %v", mntURL, err)
    }
    dirs := "dirs=" + rootURL + "writeLayer:" + rootURL + "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 {
        log.Errorf("%v", err)
    }
}

整体就是

//Create a AUFS filesystem as container root workspace
func NewWorkSpace(rootURL string, mntURL string) {
    CreateReadOnlyLayer(rootURL)
    CreateWriteLayer(rootURL)
    CreateMountPoint(rootURL, mntURL)
}

父进程在fork子进程之前,会将这些层挂载,并配置好子进程再进行启动

func NewParentProcess(tty bool) (*exec.Cmd, *os.File) {
    readPipe, writePipe, err := NewPipe()
    if err != nil {
        log.Errorf("New pipe error %v", err)
        return nil, nil
    }
    cmd := exec.Command("/proc/self/exe", "init")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
            syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
    }
    if tty {
        cmd.Stdin = os.Stdin
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
    }
    cmd.ExtraFiles = []*os.File{readPipe}
    mntURL := "/root/mnt/"
    rootURL := "/root/"
    NewWorkSpace(rootURL, mntURL)
    // 执行目录替换为mntURL
    cmd.Dir = mntURL
    return cmd, writePipe
}

在删除容器的时候父进程也会将容器对应的层删除

  • umount通过aufs挂载的目录,并删除
  • 删除可写层
//Delete the AUFS filesystem while container exit
func DeleteWorkSpace(rootURL string, mntURL string){
    DeleteMountPoint(rootURL, mntURL)
    DeleteWriteLayer(rootURL)
}

func DeleteMountPoint(rootURL string, mntURL string){
    cmd := exec.Command("umount", mntURL)
    cmd.Stdout=os.Stdout
    cmd.Stderr=os.Stderr
    if err := cmd.Run(); err != nil {
        log.Errorf("%v",err)
    }
    if err := os.RemoveAll(mntURL); err != nil {
        log.Errorf("Remove dir %s error %v", mntURL, err)
    }
}

func DeleteWriteLayer(rootURL string) {
    writeURL := rootURL + "writeLayer/"
    if err := os.RemoveAll(writeURL); err != nil {
        log.Errorf("Remove dir %s error %v", writeURL, err)
    }

volume数据卷

就是在之前的基础上,容器启动时

  1. 创建只读层
  2. 创建读写层
  3. 创建挂载点,将只读层和读写层通过aufs的方式合并挂载到挂载点
  4. 将挂载点作为容器根目录

容器退出时

  1. 卸载挂载点,并删除
  2. 删除读写层

添加-v标签识别挂载目录

    Flags: []cli.Flag{
        cli.BoolFlag{
            Name:  "ti",
            Usage: "enable tty",
        },
        cli.StringFlag{
            Name:  "v",
            Usage: "volume",
        },
    },

在完成之前的挂载后,挂载volume

func NewWorkSpace(rootURL string, mntURL string, volume string) {
    CreateReadOnlyLayer(rootURL)
    CreateWriteLayer(rootURL)
    CreateMountPoint(rootURL, mntURL)
    if(volume != ""){
        volumeURLs := volumeUrlExtract(volume)
        length := len(volumeURLs)
        if(length == 2 && volumeURLs[0] != "" && volumeURLs[1] !=""){
            MountVolume(rootURL, mntURL, volumeURLs)
            log.Infof("%q",volumeURLs)
        }else{
            log.Infof("Volume parameter input is not correct.")
        }
    }
}

MountVolume在mnt挂载点中创建挂载目录,将volume也通过aufs的方式挂载到mnt挂载点的目录中

func MountVolume(rootURL string, mntURL string, volumeURLs []string)  {
    parentUrl := volumeURLs[0]
    if err := os.Mkdir(parentUrl, 0777); err != nil {
        log.Infof("Mkdir parent dir %s error. %v", parentUrl, err)
    }
    containerUrl := volumeURLs[1]
    containerVolumeURL := mntURL + containerUrl
    if err := os.Mkdir(containerVolumeURL, 0777); err != nil {
        log.Infof("Mkdir container dir %s error. %v", containerVolumeURL, err)
    }
    dirs := "dirs=" + parentUrl
    cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", containerVolumeURL)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
        log.Errorf("Mount volume failed. %v", err)
    }

}

退出的时候先卸载volume的挂载点,然后再执行之前的退出流程

func DeleteMountPoint(rootURL string, mntURL string){
    cmd := exec.Command("umount", mntURL)
    cmd.Stdout=os.Stdout
    cmd.Stderr=os.Stderr
    if err := cmd.Run(); err != nil {
        log.Errorf("%v",err)
    }
    if err := os.RemoveAll(mntURL); err != nil {
        log.Infof("Remove mountpoint dir %s error %v", mntURL, err)
    }
}

func DeleteWorkSpace(rootURL string, mntURL string, volume string){
    if(volume != ""){
        volumeURLs := volumeUrlExtract(volume)
        length := len(volumeURLs)
        if(length == 2 && volumeURLs[0] != "" && volumeURLs[1] !=""){
            DeleteMountPointWithVolume(rootURL, mntURL, volumeURLs)
        }else{
            DeleteMountPoint(rootURL, mntURL)
        }
    }else {
        DeleteMountPoint(rootURL, mntURL)
    }
    DeleteWriteLayer(rootURL)
}

镜像打包

容器退出的时候是会删除可写层的内容,commit的功能是将运行状态保留下来

添加commit命令

    app.Commands = []cli.Command{
        initCommand,
        runCommand,
        commitCommand,
    }

对应的操作

var commitCommand = cli.Command{
    Name:  "commit",
    Usage: "commit a container into image",
    Action: func(context *cli.Context) error {
        if len(context.Args()) < 1 {
            return fmt.Errorf("Missing container name")
        }
        imageName := context.Args().Get(0)
        //commitContainer(containerName)
        commitContainer(imageName)
        return nil
    },
}

将文件系统的目录进行打包

func commitContainer(imageName string){
    mntURL := "/root/mnt"
    imageTar := "/root/" + imageName + ".tar"
    fmt.Printf("%s",imageTar)
    if _, err := exec.Command("tar", "-czf", imageTar, "-C", mntURL, ".").CombinedOutput(); err != nil {
        log.Errorf("Tar folder %s error %v", mntURL, err)
    }
}

构建容器进阶

容器后台运行

docker是通过runc的detach的功能,使得runc启动容器后,交由父进程containerd-shim接管

cli加入-d参数

var runCommand = cli.Command{
    Name: "run",
    Usage: `Create a container with namespace and cgroups limit
            mydocker run -ti [command]`,
    Flags: []cli.Flag{
        cli.BoolFlag{
            Name:  "ti",
            Usage: "enable tty",
        },
        cli.BoolFlag{
            Name:  "d",
            Usage: "detach container",
        },
        ...

对于-it和-d服务有不同的操作,但是不能同时生效

        createTty := context.Bool("ti")
        detach := context.Bool("d")

        if createTty && detach {
            return fmt.Errorf("ti and d paramter can not both provided")
        }

然后对这两种情况进行判断处理

func Run(tty bool, comArray []string, res *subsystems.ResourceConfig) {
    parent, writePipe := container.NewParentProcess(tty)
    if parent == nil {
        log.Errorf("New parent process error")
        return
    }
    if err := parent.Start(); err != nil {
        log.Error(err)
    }
    // use mydocker-cgroup as cgroup name
    cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")
    defer cgroupManager.Destroy()
    cgroupManager.Set(res)
    cgroupManager.Apply(parent.Process.Pid)

    sendInitCommand(comArray, writePipe)
    if tty {
        parent.Wait()
    }
}

如果为-ti, 有tty终端就需要parent.Wait()等待子进程的退出,而detach创建的容器不需要等待,子进程容器由init进程管理

查看运行的容器

首先要有容器名,增加启动的flag

        cli.StringFlag{
            Name:  "name",
            Usage: "container name",
        },

将容器名称和Pid等信息记录,容器记录

type ContainerInfo struct {
    Pid         string `json:"pid"` //容器的init进程在宿主机上的 PID
    Id          string `json:"id"`  //容器Id
    Name        string `json:"name"`  //容器名
    Command     string `json:"command"`    //容器内init运行命令
    CreatedTime string `json:"createTime"` //创建时间
    Status      string `json:"status"`     //容器的状态
}

一个生成Id的方法

func randStringBytes(n int) string {
    letterBytes := "1234567890"
    rand.Seed(time.Now().UnixNano())
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Intn(len(letterBytes))]
    }
    return string(b)
}

然后将容器相关信息写入文件

func recordContainerInfo(containerPID int, commandArray []string, containerName string) (string, error) {
    // 生成随机Id
    id := randStringBytes(10)
    createTime := time.Now().Format("2006-01-02 15:04:05")
    command := strings.Join(commandArray, "")
    if containerName == "" {
        containerName = id
    }
    containerInfo := &container.ContainerInfo{
        Id:          id,
        Pid:         strconv.Itoa(containerPID),
        Command:     command,
        CreatedTime: createTime,
        Status:      container.RUNNING,
        Name:        containerName,
    }

    jsonBytes, err := json.Marshal(containerInfo)
    if err != nil {
        log.Errorf("Record container info error %v", err)
        return "", err
    }
    jsonStr := string(jsonBytes)

    // 配置信息路径
    dirUrl := fmt.Sprintf(container.DefaultInfoLocation, containerName)
    if err := os.MkdirAll(dirUrl, 0622); err != nil {
        log.Errorf("Mkdir error %s error %v", dirUrl, err)
        return "", err
    }
    fileName := dirUrl + "/" + container.ConfigName
    file, err := os.Create(fileName)
    defer file.Close()
    if err != nil {
        log.Errorf("Create file %s error %v", fileName, err)
        return "", err
    }
    if _, err := file.WriteString(jsonStr); err != nil {
        log.Errorf("File write string error %v", err)
        return "", err
    }

    return containerName, nil
}

在Run方法中启动容器的之后执行这个方法

func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, containerName string) {
    ...
    if err := parent.Start(); err != nil {
        log.Error(err)
    }

    //record container info
    containerName, err := recordContainerInfo(parent.Process.Pid, comArray, containerName)
    ...

如果是使用了tty,在退出的时候需要将这些写入的数据文件删除

然后才是提供查看运行的容器的ps命令

var listCommand = cli.Command{
    Name:  "ps",
    Usage: "list all the containers",
    Action: func(context *cli.Context) error {
        ListContainers()
        return nil
    },
}

ListContainers方法将存储信息的文件路径下的文件进行解析,并进行格式化输出

func ListContainers() {
    dirURL := fmt.Sprintf(container.DefaultInfoLocation, "")
    dirURL = dirURL[:len(dirURL)-1]
    // 获取存储路径下的文件
    files, err := ioutil.ReadDir(dirURL)
    if err != nil {
        log.Errorf("Read dir %s error %v", dirURL, err)
        return
    }

    var containers []*container.ContainerInfo
    for _, file := range files {
        tmpContainer, err := getContainerInfo(file)
        if err != nil {
            log.Errorf("Get container info error %v", err)
            continue
        }
        containers = append(containers, tmpContainer)
    }

    w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)
    fmt.Fprint(w, "ID\tNAME\tPID\tSTATUS\tCOMMAND\tCREATED\n")
    for _, item := range containers {
        fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
            item.Id,
            item.Name,
            item.Pid,
            item.Status,
            item.Command,
            item.CreatedTime)
    }
    if err := w.Flush(); err != nil {
        log.Errorf("Flush error %v", err)
        return
    }
}

查看容器日志

实现的是docker logs命令,原理是将进程的标准输出挂载到日志文件中进行读取

将标准输出指向到log日志

func NewParentProcess(tty bool, containerName string) (*exec.Cmd, *os.File) {
    readPipe, writePipe, err := NewPipe()
    if err != nil {
        log.Errorf("New pipe error %v", err)
        return nil, nil
    }
    cmd := exec.Command("/proc/self/exe", "init")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
            syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
    }

    if tty {
        cmd.Stdin = os.Stdin
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
    } else {
        // 创建日志目录
        dirURL := fmt.Sprintf(DefaultInfoLocation, containerName)
        if err := os.MkdirAll(dirURL, 0622); err != nil {
            log.Errorf("NewParentProcess mkdir %s error %v", dirURL, err)
            return nil, nil
        }
        stdLogFilePath := dirURL + ContainerLogFile
        stdLogFile, err := os.Create(stdLogFilePath)
        if err != nil {
            log.Errorf("NewParentProcess create file %s error %v", stdLogFilePath, err)
            return nil, nil
        }
        cmd.Stdout = stdLogFile
    }

    cmd.ExtraFiles = []*os.File{readPipe}
    cmd.Dir = "/root/busybox"
    return cmd, writePipe
}

然后是读取日志的log命令

var logCommand = cli.Command{
    Name: "logs",
    Usage: "print logs of a container",
    Action: func(context *cli.Context) error {
        if len(context.Args()) < 1 {
            return fmt.Errorf("Please input your container name")
        }
        containerName := context.Args().Get(0)
        logContainer(containerName)
        return nil
    },
}

对应的方法对日志文件读取并进行输出

func logContainer(containerName string) {
    dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
    logFileLocation := dirURL + container.ContainerLogFile
    file, err := os.Open(logFileLocation)
    defer file.Close()
    if err != nil {
        log.Errorf("Log container open file %s error %v", logFileLocation, err)
        return
    }
    content, err := ioutil.ReadAll(file)
    if err != nil {
        log.Errorf("Log container read file %s error %v", logFileLocation, err)
        return
    }
    fmt.Fprint(os.Stdout, string(content))
}

进入容器的namespaces

进入容器namespace,就是exec对应的功能了。

setns系统调用,可以根据pid进入指定的namespace,原理是打开/proc/[pid]/ns/目录下的文件。但是对于mount namespace,一个多线程的进程是不能使用setns调用进入命名空间的。对于go来说每启动一个程序都会进入多线程状态,所以无法直接使用系统调用进入mount namespace,需要借助c来实现

Cgo允许go程序调用c的函数和标准库,示例

package rand

/*
#include <stdlib.h>
*/
import "C"

func Random() int {
    return int(C.random())
}

func Seed(i int) {
    C.srandom(C.uint(i))
}

这里Go的标准库是并没有这个包的,因为本来就不是一个包,而是Cgo创建的一个特殊命名空间,用来与C的命名空间交互,分别调用了C中的random和uint函数

这里调用setns

package nsenter

/*
#include <errno.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>


// __attribute__类似构造函数((constructor)),一旦被引用就会自动执行
__attribute__((constructor)) void enter_namespace(void) {
    char *mydocker_pid;
    mydocker_pid = getenv("mydocker_pid");
    // 从环境变量中获取需要进入的PID
    if (mydocker_pid) {
        //fprintf(stdout, "got mydocker_pid=%s\n", mydocker_pid);
    } else {
        //fprintf(stdout, "missing mydocker_pid env skip nsenter");
        return;
    }
    char *mydocker_cmd;
    // 从环境变量中获取需要执行的命令
    mydocker_cmd = getenv("mydocker_cmd");
    if (mydocker_cmd) {
        //fprintf(stdout, "got mydocker_cmd=%s\n", mydocker_cmd);
    } else {
        //fprintf(stdout, "missing mydocker_cmd env skip nsenter");
        return;
    }
    int i;
    char nspath[1024];
    char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" };

    for (i=0; i<5; i++) {
        sprintf(nspath, "/proc/%s/ns/%s", mydocker_pid, namespaces[i]);
        int fd = open(nspath, O_RDONLY);

        if (setns(fd, 0) == -1) {
            //fprintf(stderr, "setns on %s namespace failed: %s\n", namespaces[i], strerror(errno));
        } else {
            //fprintf(stdout, "setns on %s namespace succeeded\n", namespaces[i]);
        }
        close(fd);
    }
    // 在进入的ns中执行凝练
    int res = system(mydocker_cmd);
    exit(0);
    return;
}
*/
import "C"

对于exec命令,此时调用ExecContainer会执行一个cgo的程序,但是因为没有环境变量就退出了

var execCommand = cli.Command{
    Name: "exec",
    Usage: "exec a command into container",
    Action: func(context *cli.Context) error {
        //This is for callback
        if os.Getenv(ENV_EXEC_PID) != "" {
            log.Infof("pid callback pid %s", os.Getgid())
            return nil
        }

        if len(context.Args()) < 2 {
            return fmt.Errorf("Missing container name or command")
        }
        containerName := context.Args().Get(0)
        var commandArray []string
        for _, arg := range context.Args().Tail() {
            commandArray = append(commandArray, arg)
        }
        ExecContainer(containerName, commandArray)
        return nil
    },
}

调用ExecContainer写入环境变量,再通过/proc/self/exec再执行cgo程序就成正常执行

func ExecContainer(containerName string, comArray []string) {
    pid, err := getContainerPidByName(containerName)
    if err != nil {
        log.Errorf("Exec container getContainerPidByName %s error %v", containerName, err)
        return
    }
    cmdStr := strings.Join(comArray, " ")
    log.Infof("container pid %s", pid)
    log.Infof("command %s", cmdStr)

    cmd := exec.Command("/proc/self/exe", "exec")
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    os.Setenv(ENV_EXEC_PID, pid)
    os.Setenv(ENV_EXEC_CMD, cmdStr)

    if err := cmd.Run(); err != nil {
        log.Errorf("Exec container %s error %v", containerName, err)
    }
}

func getContainerPidByName(containerName string) (string, error) {
    dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
    configFilePath := dirURL + container.ConfigName
    contentBytes, err := ioutil.ReadFile(configFilePath)
    if err != nil {
        return "", err
    }
    var containerInfo container.ContainerInfo
    if err := json.Unmarshal(contentBytes, &containerInfo); err != nil {
        return "", err
    }
    return containerInfo.Pid, nil
}

容器停止

容器停止只需要找到容器对应的主进程pid,发送SIGTERM信号就行了

var stopCommand = cli.Command{
    Name: "stop",
    Usage: "stop a container",
    Action: func(context *cli.Context) error {
        if len(context.Args()) < 1 {
            return fmt.Errorf("Missing container name")
        }
        containerName := context.Args().Get(0)
        stopContainer(containerName)
        return nil
    },
}

对应的方法

func stopContainer(containerName string) {
    pid, err := GetContainerPidByName(containerName)
    if err != nil {
        log.Errorf("Get contaienr pid by name %s error %v", containerName, err)
        return
    }
    pidInt, err := strconv.Atoi(pid)
    if err != nil {
        log.Errorf("Conver pid from string to int error %v", err)
        return
    }
    if err := syscall.Kill(pidInt, syscall.SIGTERM); err != nil {
        log.Errorf("Stop container %s error %v", containerName, err)
        return
    }
    containerInfo, err := getContainerInfoByName(containerName)
    if err != nil {
        log.Errorf("Get container %s info error %v", containerName, err)
        return
    }
    containerInfo.Status = container.STOP
    containerInfo.Pid = " "
    newContentBytes, err := json.Marshal(containerInfo)
    if err != nil {
        log.Errorf("Json marshal %s error %v", containerName, err)
        return
    }
    dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
    configFilePath := dirURL + container.ConfigName
    if err := ioutil.WriteFile(configFilePath, newContentBytes, 0622); err != nil {
        log.Errorf("Write file %s error", configFilePath, err)
    }
}

容器删除

删除的信息包括存储的信息和容器层数据

var removeCommand = cli.Command{
    Name: "rm",
    Usage: "remove unused containers",
    Action: func(context *cli.Context) error {
        if len(context.Args()) < 1 {
            return fmt.Errorf("Missing container name")
        }
        containerName := context.Args().Get(0)
        removeContainer(containerName)
        return nil
    },
}

对应的操作

func removeContainer(containerName string) {
    containerInfo, err := getContainerInfoByName(containerName)
    if err != nil {
        log.Errorf("Get container %s info error %v", containerName, err)
        return
    }
    if containerInfo.Status != container.STOP {
        log.Errorf("Couldn't remove running container")
        return
    }
    dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
    if err := os.RemoveAll(dirURL); err != nil {
        log.Errorf("Remove file %s error %v", dirURL, err)
        return
    }
}

通过容器制作镜像

我们需要将当前容器的数据进行打包

var commitCommand = cli.Command{
    Name:  "commit",
    Usage: "commit a container into image",
    Action: func(context *cli.Context) error {
        if len(context.Args()) < 2 {
            return fmt.Errorf("Missing container name and image name")
        }
        containerName := context.Args().Get(0)
        imageName := context.Args().Get(1)
        commitContainer(containerName, imageName)
        return nil
    },
}

对应的操作

func commitContainer(containerName, imageName string){
    mntURL := fmt.Sprintf(container.MntUrl, containerName)
    mntURL += "/"

    imageTar := container.RootUrl + "/" + imageName + ".tar"

    if _, err := exec.Command("tar", "-czf", imageTar, "-C", mntURL, ".").CombinedOutput(); err != nil {
        log.Errorf("Tar folder %s error %v", mntURL, err)
    }
}

但是启动的时候需要为每一个容器配置不同的image和mnt namespace

获取镜像名

        //get image name
        imageName := cmdArray[0]
        cmdArray = cmdArray[1:]

启动的时候也指定镜像

    parent, writePipe := container.NewParentProcess(tty, containerName, volume, imageName, envSlice)

每个容器的aufs挂载的目录也会不同了,读写层目录也是和原来一样区分开

    MntUrl              string = "/root/mnt/%s"
    WriteLayerUrl       string = "/root/writeLayer/%s"

获取镜像层的方式就是到指定的镜像目录获取对应的镜像tar包存储路径,然后解压到指定目录

在启动的时候进行aufs挂载的方式

如果希望镜像是分层的,就需要单独在目录存储每层的读写层数据,然后一个这些读写层的数据

容器指定环境变量运行

环境变量参考docker的-e参数,然后在运行子进程的时候传入

func NewParentProcess(tty bool, containerName, volume, imageName string, envSlice []string) (*exec.Cmd, *os.File) {
    ...
    cmd.Env = append(os.Environ(), envSlice...)
    ...
}

但是通过exec进入容器是获取不到环境变量的,因为exec是一个单独的进程,通过setns进入到容器命名空间的,所以需要根据容器名获取容器PID,获取到PID下的环境变量

func getEnvsByPid(pid string) []string {
    path := fmt.Sprintf("/proc/%s/environ", pid)
    contentBytes, err := ioutil.ReadFile(path)
    if err != nil {
        log.Errorf("Read file %s error %v", path, err)
        return nil
    }
    //env split by \u0000
    //多个环境变量的分隔符为"\u0000"
    envs := strings.Split(string(contentBytes), "\u0000")
    return envs
}

将宿主机的环境变量和容器的环境变量组合作为exec进入容器的环境变量

    containerEnvs := getEnvsByPid(pid)
    cmd.Env = append(os.Environ(), containerEnvs...)