一起养成写作习惯!这是我参与「掘金日新计划 · 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命令还是有些异常,会显示已经退出运行的容器
也是可能是我自己写的有问题,后面需要调整修复下