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
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user