写点什么

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

作者:
  • 2022 年 4 月 13 日
  • 本文字数:4981 字

    阅读完需:约 16 分钟

简介

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

源码说明

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



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

代码实现

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


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


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


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


实现思路:


以前的文件系统如下:


  • 只读层:busybox 系统,这个只能读,是系统的基础

  • 可写层:writerLayer,这个是容器内部的可写层,能进行对应的修改

  • 挂载层:mnt,挂载外部的文件系统,类似于虚拟机的文件共享


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


  • 只读层:不变

  • 可写层:再加容器名为名的目录,进行隔离,也就是 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}
复制代码


发布于: 18 小时前阅读数: 30
用户头像

关注

还未添加个人签名 2018.09.09 加入

代码是门手艺活,也是门艺术活

评论

发布
暂无评论
自己动手写Docker系列 -- 5.7实现通过容器制作镜像_Go_萧_InfoQ写作平台