自己动手写 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
}
版权声明: 本文为 InfoQ 作者【萧】的原创文章。
原文链接:【http://xie.infoq.cn/article/8587517810432f9a9a52e9807】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
萧
还未添加个人签名 2018.09.09 加入
代码是门手艺活,也是门艺术活
评论