自己动手写Docker系列 -- 5.2实现查看运行中的容器

479 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情

简介

在上篇中我们实现了将容器后台运行,本篇中我们将实现docker的ps命令,查看当前正在运行中的容器列表

源码说明

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

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

代码实现

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

1.在容器启动的时候,将容器的信息写到指定的目录的文件中

2.在查看正在运行中的容器时,读取存放容器信息文件的目录,获取所有的容器信息文件,便可以得到正在运行中的容器列表

3.容器退出的时候,将指定目录下的文件进行删除

核心思路就如上所示,总体来说是比较简单的,下面我们就开始看具体的代码实现

容器启动时,存储容器信息,退出时删除

在启动函数中,我们在启动时,添加存储容器信息的逻辑

首先我们定义好容器信息结构类,如下:

ContainerInfo 是容器信息类,存放容器的信息

定义了容器状态的常量,为后面做准备,DefaultInfoLocation是约定的容器信息存放的指定目录,ConfigName是约定的容器信息存储文件名

type ContainerInfo struct {
	Pid        string `json:"pid"` // 容器的init进程在宿主机上的PID
	ID         string `json:"id"`
	Name       string `json:"name"`
	Command    string `json:"command"`
	CreateTime string `json:"createTime"`
	Status     string `json:"status"`
}

var (
	RUNNING             = "running"
	STOP                = "stop"
	EXIT                = "exited"
	DefaultInfoLocation = "/var/run/mydocker/%s/"
	ConfigName          = "config.json"
)

在run命令中,我们添加可选的-name参数,来设置容器的名称

var RunCommand = cli.Command{
	Name:  "run",
	Usage: `Create a container with namespace and cgroups limit mydocker run -ti [command]`,
	Flags: []cli.Flag{
		......
		// 提供run后面的-name指定容器名字参数
		cli.StringFlag{
			Name:  "name",
			Usage: "container name",
		},
	},
	Action: func(context *cli.Context) error {
		......
		// 将取到的容器名称传递下去,如果没有则为空
		containerName := context.String("name")
		run.Run(tty, detach, cmdArray, resConfig, volume, containerName)
		return nil
	},
}

具体的Run函数逻辑如下:

func Run(tty, detach bool, cmdArray []string, config *subsystem.ResourceConfig, volume, containerName string) {
	......

	// 记录容器信息
	containerName, err = recordContainerInfo(parent.Process.Pid, cmdArray, 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)
		// 容器退出时,删除容器信息
		deleteContainerInfo(containerName)
	}
	os.Exit(-1)
}

存储容器信息的函数逻辑如下:

1.首先是生成运行时容器的基本信息

2.将其序列化成JSON存储在约定的文件中

如果没有传入容器名,则随机取名


func recordContainerInfo(pid int, cmdArray []string, containerName string) (string, error) {
	id := randStringBytes(10)
	createTime := time.Now().Format("2000-01-01 00:00:00")
	command := strings.Join(cmdArray, " ")
	if containerName == "" {
		containerName = id
	}
	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 randStringBytes(n int) string {
	letterBytes := "1234567890"
	rand.Seed(time.Now().UnixNano())
	b := make([]byte, n)
	for i := range b {
		b[i] = letterBytes[rand.Intn(len(letterBytes))]
	}
	return string(b)
}

退出时删除容器的逻辑比较简单,直接删除文件即可:

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)
	}
}

读取文件列表,显示正在运行中的容器

在上面的代码代码中,我们可以得到容器的信息存放在: /var/run/mydocker/{containerName}/config.json

我们遍历 /var/run/mydocker 便可以得到所有的容器目录,读取其下的config.json便可以得到容器信息

我们首先添加 main.go 中添加 ps 命令

func main() {
	app := cli.NewApp()
	app.Name = "mydocker"
	app.Usage = usage

	app.Commands = []cli.Command{
		command.InitCommand,
		command.RunCommand,
		command.CommitCommand,
		command.ListCommand,
	}

	app.Before = func(context *cli.Context) error {
		log.SetFormatter(&log.JSONFormatter{})
		log.SetOutput(os.Stdout)
		return nil
	}

	if err := app.Run(os.Args); err != nil {
		log.Fatal(err)
	}
}

在main_command添加ps command

var ListCommand = cli.Command{
	Name:  "ps",
	Usage: "list all the container",
	Action: func(context *cli.Context) error {
		return run.ListContainers()
	},
}

ps 命令的具体实现如下:

遍历 /var/run/mydocker 便可以得到所有的容器目录,读取其下的config.json便可以得到容器信息

然后在控制台上进行打印

func ListContainers() error {
	dirUrl := fmt.Sprintf(container.DefaultInfoLocation, "")
	dirUrl = dirUrl[:len(dirUrl)-1]
	files, err := ioutil.ReadDir(dirUrl)
	if err != nil {
		return fmt.Errorf("read dir %s err: %v", dirUrl, err)
	}

	var containers []*container.ContainerInfo
	for _, file := range files {
		tmpContainer, err := getContainerInfo(file)
		if err != nil {
			return err
		}
		containers = append(containers, tmpContainer)
	}

	w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)
	_, _ = fmt.Fprint(w, "ID\tNAME\tPID\tSTATUS\tCOMMAND\tCREATED\n")
	for _, item := range containers {
		_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", item.ID, item.Name, item.Pid, item.Status, item.Command, item.CreateTime)
	}
	if err := w.Flush(); err != nil {
		return fmt.Errorf("flush ps write err: %v", err)
	}
	return nil
}

func getContainerInfo(file fs.FileInfo) (*container.ContainerInfo, error) {
	containerName := file.Name()
	configFileDir := fmt.Sprintf(container.DefaultInfoLocation, containerName)
	configFilePath := configFileDir + container.ConfigName
	content, err := ioutil.ReadFile(configFilePath)
	if err != nil {
		return nil, fmt.Errorf("read file %s err: %v", configFilePath, err)
	}
	var containerInfo container.ContainerInfo
	if err := json.Unmarshal(content, &containerInfo); err != nil {
		return nil, fmt.Errorf("json unmarshal err: %v", err)
	}
	return &containerInfo, nil
}

运行测试

我们启动两个后台容器,一个有名字,一个没有,结果如下,很nice

➜  dockerDemo git:(main) ./main run -d sh
{"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-03-22T20:36:06+08:00"}
{"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-03-22T20:36:06+08:00"}
{"level":"info","msg":"all command is : sh","time":"2022-03-22T20:36:06+08:00"}
{"level":"info","msg":"parent process run","time":"2022-03-22T20:36:06+08:00"}
➜  dockerDemo git:(main) ./main run -d -name test1 sh
{"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-03-22T20:36:14+08:00"}
{"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-03-22T20:36:14+08:00"}
{"level":"info","msg":"all command is : sh","time":"2022-03-22T20:36:14+08:00"}
{"level":"info","msg":"parent process run","time":"2022-03-22T20:36:14+08:00"}
➜  dockerDemo git:(main) ./main ps
ID           NAME         PID         STATUS      COMMAND     CREATED
7475097580   7475097580   21886       running     sh          22000-03-03 00:00:00
3160412281   test1        21912       running     sh          22000-03-03 00:00:00
➜  dockerDemo git:(main)

但目前还存在问题,如果是前台运行的容器,退出后,容器信息文件会随着删除,但后台运行的就不行,导致ps命令还是有些异常,会显示已经退出运行的容器

也是可能是我自己写的有问题,后面需要调整修复下