自己动手写Docker系列 -- 5.7实现通过容器制作镜像
时间:2023-04-21 03:07:00
简介
我们在上一篇文章中实现了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
}