锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

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

时间:2023-04-21 03:07:00 mmt330系列油中微水变送器

简介

我们在上一篇文章中实现了rm本文将完善以往的文件系统隔离,实现容器与容器之间的文件系统隔离

源码说明

同时放到了Gitee和Github上,都可以获得

  • Gitee: https://gitee.com/free-love/docker-demo
  • GitHub: https://github.com/lw1243925457/dockerDemo

本章对应的版本标签为: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
}
锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章