五、构建镜像
5.1 使用busybox创建容器
5.1.1 busybox
在之前,容器的挂载点继承自父进程的所有挂载点,因为缺少了镜像。
busybox
是一个集合了非常多UNIX工具的箱子,它可以提供非常多在UNIX环境下经常使用的命令,可以说
busybox 提供了一个非常完整而且小巧的系统。
获得 busybox 文件系统的 rootfs 很简单,可以使用
docker export
将一个镜像打成一个 tar包。
docker pull busybox docker run -d busybox top -b docker export -o busybox.tar [容器id]
5.1.2 pivot_root
pivot_root
是一个系统调用,主要功能是去改变当前的
root 文件系统 。 pivot_root
可以将当前进程的 root
文件系统移动到 put_old 文件夹中,然后使 new_root
成为新的
root 文件系统。new_root
和put_old
必须不能同时存在 当前 root 的同 一个文件系统中 。
pivot_root
和 chroot
的主要区别是,
pivot_root
是把整个系统切换到一个新的 root
目录 ,而移除对之前 root 文件系统的依赖,这样你就能够
umount
原先的 root 文件系统。而 chroot
是针对某个进程 ,系统的其他部分依旧运行于老的 root
目录中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func pivotRoot (root string ) error { 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) } syscall.Mount("" , "/" , "" , syscall.MS_REC|syscall.MS_PRIVATE, "" ) pivotDir := filepath.Join(root, ".pivot_root" ) if err := os.Mkdir(pivotDir, 0777 ); err != nil { return err } 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" ) if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil { return fmt.Errorf("unmount pivot_root dir %v" , err) } return os.Remove(pivotDir) }
这里oldroot就是之前容器启动后挂载的linux文件目录,也就是当前目录,现在把当前目录作为一个新挂载点,以前的挂载点是现在挂载点的子目录 ,chdir("/")
切换到当前挂载后,就可以把原来的挂载卸载掉删除,就只剩新挂载点了。
虽然旧的挂载点和新的挂载点是同一个目录,但是其实是在两个不同的Mount Namespace
中,所以容器内修改文件就不会影响宿主机的目录文件。这才是上面工作的最终目的。
pivot_root官方手册
pivot_root改变当前进程所在mount namespace内的所有进程的root
mount移到put_old,然后将new_root作为新的root mount;
pivot_root并没有修改当前调用进程的工作目录,通常需要使用chdir("/")来实现切换到新的root
mount的根目录。
rount mount可以理解为rootfs,也就是“/”,pivot_root将所在mount
namespace的“/”改为了new_root
注意,pivot_root没有改变当前调用进程的工作目录
注意,pivot_root的调用前提需要明确在fork进程时指定mount
namespace参数
主要约束条件:
new_root和put_old都必须是目录
new_root和put_old不在同一个mount namespace中
put_old必须是new_root,或者是new_root的子目录
new_root必须是mount point,且不能是当前mount namespace的“/”
注意,pivot_root(new_root,
put_old),且chdir("/")后,put_old是“/”的子目录,可以unmount
在docker中,使用pivot_root实现rootfs切换和隔离,也遵循pivot_root的使用约束
首先创建一个new_root的临时子目录作为put_old,然后调用pivot_root实现切换
chdir("/")
umount put_old and clear
运行过程中会出现pivotRoot error pivot_root invalid argument
的报错,可以先执行 unshare -m
命令,然后将
busybox/.pivot_root
文件夹删除,注意这个文件必须手动删除,不然启动会报错,再次重试即可。
原因是systemd会将 fs 修改为 shared
,pivot root
不允许 parent mount point
和 new mount point
是
shared
。
docker runC中也给出了解决方案:// Make parent mount PRIVATE
if it was shared .
if err := syscall.Mount(root, root, "bind" , syscall.MS_BIND|syscall.MS_REC|syscall.MS_PRIVATE, "" ); err != nil { ...
或者修改为MS_SLAVE
也可。
unshare
有一个选项--propagation private|shared|slave|unchanged
可控制创建mnt
namespace时挂载点的共享方式。
private:表示新创建的mnt namespace中的挂载点的shared
subtrees属性都设置为private,即ns1和ns2的挂载点互不影响
shared:表示新创建的mnt namespace中的挂载点的shared
subtrees属性都设置为shared,即ns1或ns2中新增或移除子挂载点都会同步到另一方
slave:表示新创建的mnt namespace中的挂载点的shared
subtrees属性都设置为slave,即ns1中新增或移除子挂载点会影响ns2,但ns2不会影响ns1
unchanged:表示拷贝挂载点信息时也拷贝挂载点的shared
subtrees属性,也就是说挂载点A原来是shared,在mnt
namespace中也将是shared
不指定--progapation
选项时,创建的mount
namespace中的挂载点的shared subtrees默认值是private
可以看到创建容器后pwd
结果为/
,且新增一个aaa.txt
在宿主机镜像的目录下没有影响,说明容器的文件系统不会影响宿主机了。
image-20221022232012119
5.2 使用OverlayFS挂载容器镜像
5.2.1 前置工作
书中使用的是AUFS进行挂载,但是AUFS已经稍有过时了,所以使用OverlayFS进行挂载,大体结构相同。
我们预期想要达到的效果是:通过./GoDocker run -it -image busybox.tar sh
命令,能自动去镜像目录寻找镜像,并且根据Overlayfs
的结构自动创建相应的容器工作目录,并自动执行overlay挂载,使得容器内Mount Namespace
隔离,并且只能操作容器镜像目录的文件内容,无法看到容器外的文件。
步骤如下:
首先定义固定的工作目录:
const ImageLowerName = "lower-layer" const UpperLayerName = "container-layer" const WorkLayerName = "work" const MergeLayerName = "merge" const ImageRootPath = "/root/godocker/images/" const ContainerRootPath = "/root/godocker/containers/"
ImageRootPath
下约定存放所有的镜像文件,当然用户也可以通过命令行指定镜像,ContainerRootPath
约定存放容器的工作目录,我们会给每个容器生成一个id,容器的所有文件都存放在这个文件夹下。
上面的四行约定为OverlayFS需要的四个文件夹名称。
5.2.2 初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func NewWorkSpace (imageUrl string ) string { CreateRootDir() id := GenerateContainerId(24 ) log.Infof("container id:%s" , id) containerDir := ContainerRootPath + id + "/" if err := os.Mkdir(containerDir, 0777 ); err != nil { log.Errorf("Mkdir container dir %s error. %v" , containerDir, err) } CreateWorkAndMergeDir(containerDir) err := CreateLowerLayer(containerDir, imageUrl) if err != nil { return "" } CreateUpperLayer(containerDir) ExecuteMountOverlayFS(containerDir) return containerDir }
通过NewWorkSpace()
进行初始化工作,包括创建根目录、生成容器id、创建OverlayFS目录、挂载OverlayFS。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func CreateLowerLayer (containerDir string , imageUrl string ) error { imageExists, err := PathExists(imageUrl) if err != nil { log.Errorf("founding image error!" ) return err } if imageExists == false { panic ("image not found!" ) } lowerLayerDir := containerDir + ImageLowerName if err := os.Mkdir(lowerLayerDir, 0777 ); err != nil { log.Errorf("Mkdir dir %s error. %v" , lowerLayerDir, err) return err } if _, err := exec.Command("tar" , "-xvf" , imageUrl, "-C" , lowerLayerDir).CombinedOutput(); err != nil { log.Errorf("Untar dir %s error %v" , lowerLayerDir, err) return err } log.Infof("untar image %s success!" , imageUrl) return nil }
CreateLowerLayer()
创建只读的lowerlayer
,并将镜像解压至该目录(非容器的工作目录)。
func ExecuteMountOverlayFS (containerDir string ) { log.Infof("pwd: %s" , containerDir) mountCmd := exec.Command("mount" , "-t" , "overlay" , "overlay" , "-o" , "lowerdir=lower-layer,upperdir=container-layer,workdir=work" , "merge" ) mountCmd.Dir = containerDir mountCmd.Stdout = os.Stdout mountCmd.Stderr = os.Stderr if err := mountCmd.Run(); err != nil { panic (err) } log.Infof("mount overlayfs success!" ) }
创建完需要的文件夹,就需要进行挂载。
首先将mount
的命令配置好,然后切换到生成的包含了四个文件夹的容器目录,在这个目录执行挂载。
挂载完成后,merge
目录就包含了所有layer的文件,merge
目录也就是容器的工作目录。
然后最后需要进行容器的清理工作,包括卸载挂载、删除工作目录等。
注意到有这行代码:
unshare := exec.Command("unshare", "-m")
因为本人在使用过程中,只有先执行这句代码,pivot_root
系统调用才会成功,否则会报错invalid Argument
。
意思是取消共享父进程的Mount NameSpace
,按理说创建了Mount Namepsace
就应该是隔离的了,不知道为什么要手动取消共享。
但是这样会导致只在容器内挂载overlayfs
,这样是不符合要求的。
所以正确做法应该是:
先执行一次syscall.Mount("", "/", "", syscall.MS_REC|syscall.MS_PRIVATE, "")
来真正隔离。
func pivotRoot (root string ) error { if err := syscall.Mount(root, root, "bind" , syscall.MS_BIND|syscall.MS_REC, "" ); err != nil { log.Errorf("Moun t rootfs to itself error: %v" , err) return fmt.Errorf("Mount rootfs to itself error: %v" , err) } syscall.Mount("" , "/" , "" , syscall.MS_REC|syscall.MS_PRIVATE, "" )
5.2.3 完整核心代码
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 package containerimport ( log "github.com/sirupsen/logrus" "math/rand" "os" "os/exec" "strings" "syscall" "time" )const ImageLowerName = "lower-layer" const UpperLayerName = "container-layer" const WorkLayerName = "work" const MergeLayerName = "merge" const ImageRootPath = "/root/godocker/images/" const ContainerRootPath = "/root/godocker/containers/" type OverlayParameter struct { lowerDir string upperDir string workDir string mergeDir string }type UserParameter struct { ImageName string Root string }func NewParentProcess (tty bool , parameter *UserParameter) (*exec.Cmd, *os.File, string ) { 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} imageUrl := ImageRootPath if strings.Index(parameter.ImageName, "/" ) > 0 { imageUrl = parameter.ImageName } else { imageUrl += parameter.ImageName } workDir := NewWorkSpace(imageUrl) if len (workDir) <= 1 { panic ("create workspace error!" ) } cmd.Dir = workDir + MergeLayerName return cmd, writePipe, workDir }func NewPipe () (*os.File, *os.File, error ) { read, write, err := os.Pipe() if err != nil { return nil , nil , err } return read, write, nil }func NewWorkSpace (imageUrl string ) string { CreateRootDir() id := GenerateContainerId(24 ) log.Infof("container id:%s" , id) containerDir := ContainerRootPath + id + "/" if err := os.Mkdir(containerDir, 0777 ); err != nil { log.Errorf("Mkdir container dir %s error. %v" , containerDir, err) } CreateWorkAndMergeDir(containerDir) err := CreateLowerLayer(containerDir, imageUrl) if err != nil { return "" } CreateUpperLayer(containerDir) ExecuteMountOverlayFS(containerDir) return containerDir }func CreateRootDir () { imageExists, err := PathExists(ImageRootPath) if err != nil { panic ("founding image root dir error!" ) } if imageExists == false { if err := os.Mkdir(ImageRootPath, 0777 ); err != nil { log.Errorf("Mkdir dir %s error. %v" , ImageRootPath, err) panic (err) } } containerExists, err := PathExists(ContainerRootPath) if err != nil { panic ("founding container root dir error!" ) } if containerExists == false { if err := os.Mkdir(ContainerRootPath, 0777 ); err != nil { log.Errorf("Mkdir dir %s error. %v" , ContainerRootPath, err) panic (err) } } }func CreateLowerLayer (containerDir string , imageUrl string ) error { imageExists, err := PathExists(imageUrl) if err != nil { log.Errorf("founding image error!" ) return err } if imageExists == false { panic ("image not found!" ) } lowerLayerDir := containerDir + ImageLowerName if err := os.Mkdir(lowerLayerDir, 0777 ); err != nil { log.Errorf("Mkdir dir %s error. %v" , lowerLayerDir, err) return err } if _, err := exec.Command("tar" , "-xvf" , imageUrl, "-C" , lowerLayerDir).CombinedOutput(); err != nil { log.Errorf("Untar dir %s error %v" , lowerLayerDir, err) return err } log.Infof("untar image %s success!" , imageUrl) return nil }func CreateUpperLayer (containerDir string ) { upperDir := containerDir + UpperLayerName if err := os.Mkdir(upperDir, 0777 ); err != nil { log.Errorf("Mkdir dir %s error. %v" , upperDir, err) } log.Infof("mkdir upperlayer %s success!" , upperDir) }func CreateWorkAndMergeDir (containerDir string ) { workUrl := containerDir + WorkLayerName exist, err := PathExists(workUrl) if err != nil { log.Errorf("fail to judge whether dir if exist! %v" , err) } if exist == false { if err := os.Mkdir(workUrl, 0777 ); err != nil { log.Errorf("Mkdir dir %s error. %v" , workUrl, err) } log.Infof("mkdir workdir %s success!" , workUrl) } mergeUrl := containerDir + MergeLayerName mexist, err := PathExists(mergeUrl) if err != nil { log.Errorf("fail to judge whether dir if exist! %v" , err) panic (err) } if mexist == false { if err := os.Mkdir(mergeUrl, 0777 ); err != nil { log.Errorf("Mkdir dir %s error. %v" , mergeUrl, err) panic (err) } log.Infof("mkdir mergedir %s success!" , mergeUrl) } }func ExecuteMountOverlayFS (containerDir string ) { log.Infof("pwd: %s" , containerDir) mountCmd := exec.Command("mount" , "-t" , "overlay" , "overlay" , "-o" , "lowerdir=lower-layer,upperdir=container-layer,workdir=work" , "merge" ) mountCmd.Dir = containerDir mountCmd.Stdout = os.Stdout mountCmd.Stderr = os.Stderr if err := mountCmd.Run(); err != nil { panic (err) } log.Infof("mount overlayfs success!" ) }func DeleteWorkSpace (containerDir string ) { DeleteMountPoint(containerDir) DeleteUpperLayer(containerDir) }func DeleteMountPoint (containerDir string ) { mergeUrl := containerDir + MergeLayerName cmd := exec.Command("umount" , mergeUrl) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Errorf("%v" , err) } if err := os.RemoveAll(mergeUrl); err != nil { log.Errorf("Remove dir %s error %v" , mergeUrl, err) } log.Infof("MountPoint Clear success!" ) }func DeleteUpperLayer (containerDir string ) { upperLayerUrl := containerDir + UpperLayerName if err := os.RemoveAll(upperLayerUrl); err != nil { log.Errorf("Remove dir %s error %v" , upperLayerUrl, err) } log.Infof("Upperlayer Clear success!" ) }func PathExists (path string ) (bool , error ) { _, err := os.Stat(path) if err == nil { return true , nil } if os.IsNotExist(err) { return false , nil } return false , err }func verifyRootUrl (rootUrl string ) string { if strings.Compare("/" , string (rootUrl[len (rootUrl)-1 ])) != 0 { rootUrl += "/" } return rootUrl }func GenerateContainerId (n int ) string { rand.Seed(time.Now().Unix()) var letters = []rune ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ) b := make ([]rune , n) for i := range b { b[i] = letters[rand.Intn(len (letters))] } return string (b) }
5.2.4 运行效果
测试效果如下:
image-20221024122818616
image-20221024122850707
可以看到容器初始化成功,创建了工作目录,并且环境进行了隔离、只能看到工作目录的内容。
并且在容器外可以看到成功挂载了Overlayfs
。
image-20221024122931403
我们尝试在容器内修改文件。
image-20221024123130754
能够按照OverlayFS
的原理进行修改。
退出后也能够自动卸载挂载、完成清理工作。
image-20221024123236297
5.2.5 总结
由于Mount Namespace
的shared subtrees
问题,书中也没有说明,真是踩了大坑,不过了解原理后就熟悉了。
原因是:首先进行了Mount bind
,把root重新mount了一次,然后调用pivot_root。原因就在于这里,重新mount后由于shared subtrees
,调用肯定会不成功。要么手动执行unshare -m
,但是这样是在运行程序前执行的,还是和原来的root隔离了,所以后面overlayfs挂载只能挂载在容器上,而宿主机就看不到挂载,自然merge目录也就没内容了。
由于shared subtrees
,之前创建mount
namepsace后挂载proc其实是将宿主机的proc挂载过来了,所以会发现宿主机很多命令都没法用了。
然后得知mount --make-private /
可以将挂载树变为private
,就不会再共享操作了。
然后第一次在Mount bind
前进行了调用,结果当时看似一切都解决了,能够完美运行,结果发现重启就崩了,因为把宿主机原始的rootfs给隔离了,导致系统启动找不到文件了。还好虚拟机有快照。
才明白应该在Mount bind
之后,pivot_root
之前进行调用,并且在创建子进程之前进行挂载,这样就能完美解决了。
5.3 实现Volume数据卷
我们上一节实现了挂载了镜像文件系统,但是如果退出容器会卸载挂载点、删除upperlayer,那么在容器中的操作就无法进行持久化了,Volume数据卷就可以实现持久化,退出容器后,容器的内容仍然保存在宿主机上。
5.3.1 建立挂载
因为用户可能挂载也可能不挂载,所以在创建容器的时候需要进行判断是否进行挂载,并且校验挂载的路径是否输入正确。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 func NewWorkSpace (imageUrl string , parameter *UserParameter) string { CreateRootDir() id := GenerateContainerId(24 ) log.Infof("container id:%s" , id) containerDir := ContainerRootPath + id + "/" if err := os.Mkdir(containerDir, 0777 ); err != nil { log.Errorf("Mkdir container dir %s error. %v" , containerDir, err) } CreateWorkAndMergeDir(containerDir) err := CreateLowerLayer(containerDir, imageUrl) if err != nil { return "" } CreateUpperLayer(containerDir) ExecuteMountOverlayFS(containerDir) volume := parameter.Volume if volume != "" { volumeURLs := volumeUrlExtract(volume) length := len (volumeURLs) if length == 2 && volumeURLs[0 ] != "" && volumeURLs[1 ] != "" { MountVolume(containerDir, volumeURLs) } else { log.Infof("Volume parameter input is not correct." ) } } return containerDir }func volumeUrlExtract (volume string ) []string { var volumeURLs []string volumeURLs = strings.Split(volume, ":" ) return volumeURLs }
如果需要挂载,我们就通过MountVolume(containerDir, volumeURLs)
来进行挂载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 func MountVolume (containerDir string , volumeURLs []string ) { parentUrl := volumeURLs[0 ] exists, err := PathExists(parentUrl) if err != nil { return } if exists == false { if err := os.Mkdir(parentUrl, 0777 ); err != nil { log.Errorf("Mkdir parent dir %s error. %v" , parentUrl, err) } } containerVolumeUrl := volumeURLs[1 ] containerVolumeURL := containerDir + "merge" + containerVolumeUrl if err := os.Mkdir(containerVolumeURL, 0777 ); err != nil { log.Errorf("Mkdir container dir %s error. %v" , containerVolumeURL, err) } cmd := exec.Command("mount" , "--bind" , parentUrl, containerVolumeURL) log.Infof("rootUrl: %s, containerVulumeUrl: %s" , parentUrl, containerVolumeURL) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Errorf("Mount volume failed. %v" , err) panic (err) } log.Infof("mount volume success!" ) }
可以看到最终是通过Mount --bind
来进行挂载的。书中是通过aufs
进行挂载,但是我觉得没有必要,普通的挂载就可以了,Mount --bind
相当于将两个文件夹指向同一个inode
.
https://blog.csdn.net/allway2/article/details/122136813
5.3.2 清理工作
下面就是退出容器时的清理工作。
注意,这里我们有两个挂载点:
容器工作目录containerDir/merge
容器内挂载点containerDir/merge/{mountdir}
所以卸载挂载的时候需要先卸载容器内挂载点 ,再卸载容器工作目录挂载点。否则会报错mount device busy
。
func UmountVolume (containerDir string , volumeUrl string ) { containerVolumeUrl := containerDir + "merge" + volumeUrl cmd := exec.Command("umount" , containerVolumeUrl) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Errorf("Umount volume failed. %v" , err) } mergeUrl := containerDir + MergeLayerName DeleteMergeMountPoint(mergeUrl) }
注意的是,在测试中发现,如果挂载了数据卷,mount
会出现两次容器目录的挂载 ,如果卸载数据卷挂载点后不卸载一次容器目录,那么后面卸载工作目录的时候只卸载一次,就会导致文件夹删除不了。所以这里需要卸载一次工作目录。
同时需要修改一下,如果用户没有进行挂载,那么就不卸载数据卷挂载。
func DeleteWorkSpace (containerDir string , volumeMount string ) { if volumeMount != "" { volumeURLs := volumeUrlExtract(volumeMount) length := len (volumeURLs) if length == 2 && volumeURLs[0 ] != "" && volumeURLs[1 ] != "" { UmountVolume(containerDir, volumeURLs[1 ]) } DeleteMountPoint(containerDir) DeleteUpperLayer(containerDir) } }
5.3.3 测试效果
通过./GoDocker run -it -image busybox.tar -v /root/busybox:/busybox sh
启动容器,表示通过镜像busybox创建容器,并且将宿主机/root/busybox
目录挂载到容器内/busybox
目录。
image-20221025114916172
可以看到宿主机/root/busybox
有3个文件,容器启动成功后能够在根目录看到这三个文件。
如果宿主机目录不存在的话,也会自动创建空文件夹然后进行挂载。
我们增加一个文件ddd.txt
。
image-20221025115106229
可以看到宿主机挂载的目录也能够同步更新。
然后我们退出容器。
image-20221025115146253
可以看到退出容器后,宿主机挂载的目录仍然存在,如果我们重新启动一个容器,将宿主机这个目录再次挂载,那么在容器内又可以看到这4个文件。这样就实现了数据卷的挂载。
5.3.4 容器启动和清理工作流程图
## 5.4 实现简单镜像打包
镜像打包原理很简单,因为容器退出后会删除掉merge
目录,我们将merge
目录的内容打包就可以了。
注意的是,我们不能在容器里面进行打包,因为容器里面的挂载和宿主机是隔离的,所以只能在容器正在运行的时候宿主机进行打包 ,因为容器退出就会进行清理。
类似docker,我们也是在宿主机进行打包的,因为docker的容器支持后台运行,我们现在还暂不支持。
首先我们实现命令:
var commitCommand = cli.Command{ Name: "commit" , Usage: "commit a container to image" , Action: func (ctx *cli.Context) error { if len (ctx.Args()) < 1 { return fmt.Errorf("missing container command" ) } containerId := ctx.Args().Get(0 ) imageName := ctx.Args().Get(1 ) commitContainer(containerId, imageName) return nil }, }
这里我们会使用./GoDocker commit [容器id] [镜像名称]
进行打包,所以直接根据ctx
获取参数即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func commitContainer (containerId, imageName string ) { containerDir := container.ContainerRootPath + containerId + "/merge" containerExist, err := container.PathExists(containerDir) if containerExist == false { fmt.Printf("container %s not found!\n" , containerId) return } imageDir := container.ImageRootPath + imageName + ".tar" exists, err := container.PathExists(imageDir) if err != nil { return } if exists == true { fmt.Printf("image name %s exists!\n" , imageName) return } _, err = exec.Command("tar" , "-czf" , imageDir, containerDir).CombinedOutput() if err != nil { fmt.Println("tar image fail!" ) return } fmt.Printf("commit image to %s success\n" , imageName) }
5.4.1 测试效果
首先启动docker,然后进行打包。
image-20221026115835199
image-20221026115933282
可以看到参数不正确进行了提示,打包成功后出现在了镜像目录下。