// Package docker 封装 Docker CLI 调用,负责查找、拉取和重建容器。 package docker import ( "bytes" "encoding/json" "fmt" "os/exec" "strings" "time" ) const ( composeProjectLabel = "com.docker.compose.project" composeServiceLabel = "com.docker.compose.service" ) // Updater 封装 Docker 容器更新操作。 type Updater struct { pullTimeout time.Duration restartTimeout time.Duration } // NewUpdater 创建 Updater 实例。 func NewUpdater(pullTimeout, restartTimeout time.Duration) *Updater { return &Updater{ pullTimeout: pullTimeout, restartTimeout: restartTimeout, } } // ContainerInfo 容器信息摘要。 type ContainerInfo struct { ID string `json:"ID"` Name string `json:"Name"` Image string `json:"Image"` Project string `json:"Project"` Service string `json:"Service"` } // containerInspect 对应 docker inspect 的部分字段。 type containerInspect struct { ID string `json:"Id"` Name string `json:"Name"` Config struct { Image string `json:"Image"` Cmd []string `json:"Cmd"` Entrypoint []string `json:"Entrypoint"` Env []string `json:"Env"` ExposedPorts map[string]struct{} `json:"ExposedPorts"` Labels map[string]string `json:"Labels"` WorkingDir string `json:"WorkingDir"` User string `json:"User"` Hostname string `json:"Hostname"` } `json:"Config"` HostConfig struct { NetworkMode string `json:"NetworkMode"` Privileged bool `json:"Privileged"` RestartPolicy struct { Name string `json:"Name"` } `json:"RestartPolicy"` PortBindings map[string][]struct { HostPort string `json:"HostPort"` } `json:"PortBindings"` Binds []string `json:"Binds"` Links []string `json:"Links"` ExtraHosts []string `json:"ExtraHosts"` } `json:"HostConfig"` Mounts []struct { Type string `json:"Type"` Source string `json:"Source"` Target string `json:"Target"` } `json:"Mounts"` NetworkSettings struct { Networks map[string]struct { Aliases []string `json:"Aliases"` } `json:"Networks"` } `json:"NetworkSettings"` } // FindContainerByLabels 通过 compose project + service 标签查找容器。 func (u *Updater) FindContainerByLabels(project, service string) (*ContainerInfo, error) { args := []string{ "ps", "--filter", fmt.Sprintf("label=%s=%s", composeProjectLabel, project), "--filter", fmt.Sprintf("label=%s=%s", composeServiceLabel, service), "--format", "{{.ID}}\t{{.Image}}\t{{.Names}}", "--latest", } out, err := u.docker(args...) if err != nil { return nil, fmt.Errorf("find container: %w", err) } lines := strings.Split(strings.TrimSpace(out), "\n") if len(lines) == 0 || lines[0] == "" { return nil, fmt.Errorf("container not found: project=%s service=%s", project, service) } parts := strings.SplitN(lines[0], "\t", 3) if len(parts) < 3 { return nil, fmt.Errorf("unexpected docker ps output: %s", out) } return &ContainerInfo{ ID: parts[0], Image: parts[1], Name: parts[2], Project: project, Service: service, }, nil } // PullImage 拉取指定镜像。 func (u *Updater) PullImage(imageName string) error { _, err := u.docker("pull", imageName) return err } // RecreateContainer 重建容器:拉取 → 重命名 → 创建 → 启动 → 清理。 // 返回新容器 ID。镜像无变化时跳过重建。 func (u *Updater) RecreateContainer(project, service string) (string, error) { info, err := u.FindContainerByLabels(project, service) if err != nil { return "", err } // 拉取新镜像 if err := u.PullImage(info.Image); err != nil { return "", fmt.Errorf("pull: %w", err) } // 检查镜像是否有变化 oldImageID, err := u.getContainerImageID(info.ID) if err != nil { return "", fmt.Errorf("old image ID: %w", err) } newImageID, err := u.getImageID(info.Image) if err != nil { return "", fmt.Errorf("new image ID: %w", err) } if oldImageID == newImageID { return info.ID, nil } // 检查旧容器配置 inspectJSON, err := u.inspectContainer(info.ID) if err != nil { return "", fmt.Errorf("inspect: %w", err) } oldName := strings.TrimPrefix(inspectJSON.Name, "/") oldRenamed := oldName + "-old-" + time.Now().Format("150405") // 重命名旧容器 if _, err := u.docker("rename", info.ID, oldRenamed); err != nil { return "", fmt.Errorf("rename old: %w", err) } // 创建新容器 runArgs := u.buildRunArgs(inspectJSON, info.Image, oldName) createOut, err := u.docker(runArgs...) if err != nil { _, _ = u.docker("rename", info.ID, oldName) // 恢复 return "", fmt.Errorf("create: %w", err) } newID := strings.TrimSpace(createOut) // 删除旧容器 if _, err := u.docker("rm", "-f", info.ID); err != nil { fmt.Printf("warning: remove old %s: %v\n", info.ID[:12], err) } return newID, nil } // RestartContainer 仅重启(会拉取最新镜像后再重建)。 func (u *Updater) RestartContainer(project, service string) (string, error) { _, err := u.FindContainerByLabels(project, service) if err != nil { return "", err } return u.RecreateContainer(project, service) } func (u *Updater) getContainerImageID(containerID string) (string, error) { return u.inspectField(containerID, "{{.Image}}") } func (u *Updater) getImageID(imageName string) (string, error) { out, err := u.docker("image", "inspect", "--format", "{{.Id}}", imageName) if err != nil { return "", err } return strings.TrimSpace(out), nil } func (u *Updater) inspectField(containerID, format string) (string, error) { out, err := u.docker("inspect", "--format", format, containerID) if err != nil { return "", err } return strings.TrimSpace(out), nil } func (u *Updater) inspectContainer(containerID string) (*containerInspect, error) { out, err := u.docker("inspect", containerID) if err != nil { return nil, err } var containers []containerInspect if err := json.Unmarshal([]byte(out), &containers); err != nil { return nil, fmt.Errorf("parse inspect: %w", err) } if len(containers) == 0 { return nil, fmt.Errorf("container not found: %s", containerID) } return &containers[0], nil } // buildRunArgs 从旧容器配置构建 docker run 参数。 func (u *Updater) buildRunArgs(inspect *containerInspect, imageName, containerName string) []string { args := []string{"run", "-d"} args = append(args, "--name", containerName) rp := inspect.HostConfig.RestartPolicy.Name if rp == "" { rp = "unless-stopped" } args = append(args, "--restart", rp) for _, env := range inspect.Config.Env { args = append(args, "-e", env) } for _, m := range inspect.Mounts { if m.Source == "" { continue } args = append(args, "-v", fmt.Sprintf("%s:%s", m.Source, m.Target)) } for port, bindings := range inspect.HostConfig.PortBindings { for _, b := range bindings { if b.HostPort == "" { args = append(args, "-p", port) } else { args = append(args, "-p", fmt.Sprintf("%s:%s", b.HostPort, port)) } } } if nm := inspect.HostConfig.NetworkMode; nm != "" && nm != "default" { args = append(args, "--network", string(nm)) } for _, h := range inspect.HostConfig.ExtraHosts { args = append(args, "--add-host", h) } for k, v := range inspect.Config.Labels { args = append(args, "-l", fmt.Sprintf("%s=%s", k, v)) } if wd := inspect.Config.WorkingDir; wd != "" { args = append(args, "-w", wd) } if u := inspect.Config.User; u != "" { args = append(args, "-u", u) } if hn := inspect.Config.Hostname; hn != "" { args = append(args, "--hostname", hn) } if inspect.HostConfig.Privileged { args = append(args, "--privileged") } for _, link := range inspect.HostConfig.Links { args = append(args, "--link", link) } args = append(args, imageName) return args } // docker 执行 docker CLI 命令。 func (u *Updater) docker(args ...string) (string, error) { cmd := exec.Command("docker", args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return "", fmt.Errorf("docker %s: %v\nstderr: %s", strings.Join(args, " "), err, strings.TrimSpace(stderr.String())) } return stdout.String(), nil }