自己动手写Docker系列 -- 5.7实现通过容器制作镜像

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情

简介

在上篇中我们实现了rm命令,删除存在的容器,本篇中,将完善之前的文件系统隔离,实现容器与容器之间的文件系统隔离

源码说明

同时放到了Gitee和Github上,都可进行获取

本章节对应的版本标签是:5.7,防止后面代码过多,不好查看,可切换到标签版本进行查看

代码实现

实现该功能的主要思路如下:

在以前的文章:自己动手写Docker系列 -- 4.2使用AUFS包装busybox

实现了容器文件系统与宿主机文件的隔离,但目前为止,所有的容器都是使用的同一个目录,容器与容器之间存在相互影响

本篇文章目的就是为了消除这边影响,实现容器与容器之间的文件系统也进行隔离

实现思路:

以前的文件系统如下:

  • 只读层:busybox系统,这个只能读,是系统的基础
  • 可写层:writerLayer,这个是容器内部的可写层,能进行对应的修改
  • 挂载层:mnt,挂载外部的文件系统,类似于虚拟机的文件共享

要实现容器间的文件系统隔离,就是在可写层和挂载层再加一层,以容器名称进行隔离,也就是:

扫描二维码关注公众号,回复: 13788230 查看本文章
  • 只读层:不变
  • 可写层:再加容器名为名的目录,进行隔离,也就是 writeLayer/{容器名称}
  • 挂载层:再加容器名为名的目录,进行隔离,也就是 mnt/{容器名称}

文件系统进行隔离后,我们commit的时候,对应的对容器可写层进程打包即可

根据思路,代码实现也比较简单,自己理清思路后,很快便能进行改造实现

修改容器启动时可写层和挂载层,以容器名进行隔离

在容器启动的时候,获取当前的容器名,用于构建相关的隔离目录

func Run(tty, detach bool, cmdArray []string, config *subsystem.ResourceConfig, volume, containerName string) {
	// 容器容器名
	id, containerName := getContainerName(containerName)

	pwd, err := os.Getwd()
	if err != nil {
		log.Errorf("Run get pwd err: %v", err)
		return
	}
	mntUrl := pwd + "/mnt/"
	rootUrl := pwd + "/"
	// 传入初始化进程,初始化工作空间
	parent, writePipe := container.NewParentProcess(tty, containerName, rootUrl, mntUrl, volume)
	if err := parent.Start(); err != nil {
		log.Error(err)
		// 如果fork进程出现异常,但有相关的文件已经进行了挂载,需要进行清理,避免后面运行报错时,需要手工清理
		// 删除容器工作空间进行改造
		deleteWorkSpace(rootUrl, mntUrl, volume, containerName)
		return
	}

    // 记录容器信息进行改造
    containerName, err = recordContainerInfo(parent.Process.Pid, cmdArray, id, containerName)
    if err != nil {
       log.Errorf("record contariner info err: %v", err)
       return
	}
	
	......

	log.Infof("parent process run")
	if !detach {
		_ = parent.Wait()
		// 删除容器工作空间进行改造
		deleteWorkSpace(rootUrl, mntUrl, volume, containerName)
		// 删除容器信息进行改造
		deleteContainerInfo(containerName)
	}
	os.Exit(-1)
}
复制代码

获取容器名

func getContainerName(containerName string) (string, string) {
	id := randStringBytes(10)
	if containerName == "" {
		containerName = id
	}
	return id, containerName
}
复制代码

生成容器信息

func recordContainerInfo(pid int, cmdArray []string, id, containerName string) (string, error) {
	createTime := time.Now().Format("2000-01-01 00:00:00")
	command := strings.Join(cmdArray, " ")
	containerInfo := &container.ContainerInfo{
		ID:         id,
		Pid:        strconv.Itoa(pid),
		Command:    command,
		CreateTime: createTime,
		Status:     container.RUNNING,
		Name:       containerName,
	}

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

	dirUrl := fmt.Sprintf(container.DefaultInfoLocation, containerName)
	if err := os.MkdirAll(dirUrl, 0622); err != nil {
		return "", fmt.Errorf("mkdir %s err: %v", dirUrl, err)
	}
	fileName := dirUrl + "/" + container.ConfigName
	file, err := os.Create(fileName)
	defer file.Close()
	if err != nil {
		return "", fmt.Errorf("create file %s, err: %v", fileName, err)
	}

	if _, err := file.WriteString(jsonStr); err != nil {
		return "", fmt.Errorf("file write string err: %v", err)
	}
	return containerName, nil
}
复制代码

删除容器信息

func deleteContainerInfo(containerName string) {
	dirUrl := fmt.Sprintf(container.DefaultInfoLocation, containerName)
	if err := os.RemoveAll(dirUrl); err != nil {
		log.Errorf("remove dir %s err: %v", dirUrl, err)
	}
}
复制代码

删除容器工作空间

func deleteWorkSpace(rootUrl, mntUrl, volume, containerName string) {
	unmountVolume(mntUrl, volume, containerName)
	deleteMountPoint(mntUrl + containerName + "/")
	deleteWriteLayer(rootUrl, containerName)
}

func unmountVolume(mntUrl, volume, containerName string) {
	if volume == "" {
		return
	}
	volumeUrls := strings.Split(volume, ":")
	if len(volumeUrls) != 2 || volumeUrls[0] == "" || volumeUrls[1] == "" {
		return
	}

	// 卸载容器内的 volume 挂载点的文件系统
	containerUrl := mntUrl + containerName + "/" + volumeUrls[1]
	cmd := exec.Command("umount", containerUrl)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Errorf("ummount volume failed: %v", err)
	}
}

func deleteMountPoint(mntUrl string) {
	cmd := exec.Command("umount", mntUrl)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Errorf("deleteMountPoint umount %s err : %v", mntUrl, err)
	}
	if err := os.RemoveAll(mntUrl); err != nil {
		log.Errorf("deleteMountPoint remove %s err : %v", mntUrl, err)
	}
}

func deleteWriteLayer(rootUrl, containerName string) {
	writeUrl := rootUrl + "writeLayer/" + containerName
	if err := os.RemoveAll(writeUrl); err != nil {
		log.Errorf("deleteMountPoint remove %s err : %v", writeUrl, err)
	}
}
复制代码

下面是容器初始化时,构造容器工作空间的改造

func NewParentProcess(tty bool, containerName, rootUrl, mntUrl, volume string) (*exec.Cmd, *os.File) {
	......
	
	// 将管道的一端传入fork的进程中
	cmd.ExtraFiles = []*os.File{readPipe}
	if err := newWorkSpace(rootUrl, mntUrl, volume, containerName); err != nil {
		log.Errorf("new work space err: %v", err)
		return nil, nil
	}
	cmd.Dir = mntUrl
	return cmd, writePipe
}

func newWorkSpace(rootUrl, mntUrl, volume, containerName string) error {
	if err := createReadOnlyLayer(rootUrl); err != nil {
		return err
	}
	if err := createWriteLayer(rootUrl, containerName); err != nil {
		return err
	}
	if err := createMountPoint(rootUrl, mntUrl, containerName); err != nil {
		return err
	}
	if err := mountExtractVolume(mntUrl, volume, containerName); err != nil {
		return err
	}
	return nil
}

// 我们直接把busybox放到了工程目录下,直接作为容器的只读层
func createReadOnlyLayer(rootUrl string) error {
	busyboxUrl := rootUrl + "busybox/"
	exist, err := pathExist(busyboxUrl)
	if err != nil {
		return err
	}
	if !exist {
		return fmt.Errorf("busybox dir don't exist: %s", busyboxUrl)
	}
	return nil
}

// 创建一个名为writeLayer的文件夹作为容器的唯一可写层
func createWriteLayer(rootUrl, containerName string) error {
	writeUrl := rootUrl + "writeLayer/" + containerName + "/"
	exist, err := pathExist(writeUrl)
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	if !exist {
		if err := os.MkdirAll(writeUrl, 0777); err != nil {
			return fmt.Errorf("create write layer failed: %v", err)
		}
	}
	return nil
}

func createMountPoint(rootUrl, mntUrl, containerName string) error {
	// 创建mnt文件夹作为挂载点
	mountPath := mntUrl + containerName + "/"
	exist, err := pathExist(mountPath)
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	if !exist {
		if err := os.MkdirAll(mountPath, 0777); err != nil {
			return fmt.Errorf("mkdir faild: %v", err)
		}
	}
	// 把writeLayer和busybox目录mount到mnt目录下
	dirs := "dirs=" + rootUrl + "writeLayer:" + rootUrl + "busybox"
	cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mountPath)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("mmt dir err: %v", err)
	}
	return nil
}

func mountExtractVolume(mntUrl, volume, containerName string) error {
	if volume == "" {
		return nil
	}
	volumeUrls := strings.Split(volume, ":")
	length := len(volumeUrls)
	if length != 2 || volumeUrls[0] == "" || volumeUrls[1] == "" {
		return fmt.Errorf("volume parameter input is not corrent")
	}
	return mountVolume(mntUrl+containerName+"/", volumeUrls)
}

func mountVolume(mntUrl string, volumeUrls []string) error {
	// 如果宿主机文件目录不存在则创建
	parentUrl := volumeUrls[0]
	exist, err := pathExist(parentUrl)
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	if !exist {
		// 使用mkdir all 递归创建文件夹
		if err := os.MkdirAll(parentUrl, 0777); err != nil {
			return fmt.Errorf("mkdir parent dir err: %v", err)
		}
	}

	// 在容器文件系统内创建挂载点
	containerUrl := mntUrl + volumeUrls[1]
	if err := os.MkdirAll(containerUrl, 0777); err != nil {
		return fmt.Errorf("mkdir container volume err: %v", err)
	}

	// 把宿主机文件目录挂载到容器挂载点
	dirs := "dirs=" + parentUrl
	cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", containerUrl)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("mount volume err: %v", err)
	}
	return nil
}
复制代码

这样,就把容器启动时,相关之间根据容器名,进行了目录隔离

修改commit命令,打包容器对应目录

修改下commit命令,根据传入的容器名,打包对应的目录

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")
		}
		containerName := context.Args().Get(0)
		return run.CommitContainer(containerName)
	},
}
复制代码

具体的打包实现

func CommitContainer(containerName string) error {
	pwd, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("Run get pwd err: %v", err)
	}
	mntUrl := pwd + "/mnt/" + containerName
	imageTar := pwd + "/" + containerName + ".tar"
	log.Infof("commit file path: %s", imageTar)
	if _, err := exec.Command("tar", "-czf", imageTar, "-C", mntUrl, ".").CombinedOutput(); err != nil {
		return fmt.Errorf("tar folder err: %s, %v", mntUrl, err)
	}
	log.Infof("end commit file: %s", imageTar)
	return nil
}
复制代码

猜你喜欢

转载自juejin.im/post/7086069688664326157