cea9b941cf
Build and Push / build (push) Failing after 13m20s
Go 项目,包含: - 服务端 updater:两阶段协议,ECDSA 签名验证,AES-GCM 加密 - 发送端 dcu-send:Gitea Action CLI - internal/auth:加解密/签名/会话管理 - internal/docker:Docker CLI 容器查找/拉取/重建 - action/:Gitea Action 定义 - deploy/Dockerfile:多阶段构建 - .gitea/workflows/build.yaml:CI/CD
284 lines
8.0 KiB
Go
284 lines
8.0 KiB
Go
// 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
|
|
}
|