376 lines
9.5 KiB
Go
376 lines
9.5 KiB
Go
package docker
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"time"
|
||
|
||
"github.com/docker/docker/api/types"
|
||
"github.com/docker/docker/api/types/container"
|
||
"github.com/docker/docker/api/types/filters"
|
||
"github.com/docker/docker/client"
|
||
)
|
||
|
||
type Client struct {
|
||
cli *client.Client
|
||
}
|
||
|
||
func NewClient() (*Client, error) {
|
||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &Client{cli: cli}, nil
|
||
}
|
||
|
||
func (c *Client) Close() error {
|
||
return c.cli.Close()
|
||
}
|
||
|
||
// PullImage 拉取镜像
|
||
func (c *Client) PullImage(ctx context.Context, image string, authStr string) error {
|
||
return c.PullImageWithProgress(ctx, image, authStr, nil)
|
||
}
|
||
|
||
// GetImageRepoDigests 读取本地镜像的 RepoDigest,用于校验拉取结果是否匹配许可证服务器声明的 digest。
|
||
func (c *Client) GetImageRepoDigests(ctx context.Context, image string) ([]string, error) {
|
||
inspect, _, err := c.cli.ImageInspectWithRaw(ctx, image)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return inspect.RepoDigests, nil
|
||
}
|
||
|
||
// PullImageWithProgress 拉取镜像并报告进度
|
||
func (c *Client) PullImageWithProgress(ctx context.Context, image string, authStr string, onProgress func(current, total int64, status string)) error {
|
||
opts := types.ImagePullOptions{}
|
||
if authStr != "" {
|
||
opts.RegistryAuth = authStr
|
||
}
|
||
|
||
reader, err := c.cli.ImagePull(ctx, image, opts)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer reader.Close()
|
||
|
||
// 解析 Docker 拉取进度
|
||
decoder := json.NewDecoder(reader)
|
||
layerProgress := make(map[string]int64)
|
||
layerTotal := make(map[string]int64)
|
||
|
||
for {
|
||
var event struct {
|
||
Status string `json:"status"`
|
||
ID string `json:"id"`
|
||
Progress string `json:"progress"`
|
||
ProgressDetail struct {
|
||
Current int64 `json:"current"`
|
||
Total int64 `json:"total"`
|
||
} `json:"progressDetail"`
|
||
}
|
||
|
||
if err := decoder.Decode(&event); err != nil {
|
||
if err == io.EOF {
|
||
break
|
||
}
|
||
continue
|
||
}
|
||
|
||
if event.ID != "" && event.ProgressDetail.Total > 0 {
|
||
layerProgress[event.ID] = event.ProgressDetail.Current
|
||
layerTotal[event.ID] = event.ProgressDetail.Total
|
||
}
|
||
|
||
if onProgress != nil {
|
||
var current, total int64
|
||
for id, t := range layerTotal {
|
||
total += t
|
||
current += layerProgress[id]
|
||
}
|
||
status := event.Status
|
||
if event.ID != "" {
|
||
status = event.Status + " " + event.ID
|
||
}
|
||
onProgress(current, total, status)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// ListContainers 列出容器
|
||
func (c *Client) ListContainers(ctx context.Context, nameFilter string) ([]types.Container, error) {
|
||
opts := types.ContainerListOptions{All: true}
|
||
if nameFilter != "" {
|
||
opts.Filters = filters.NewArgs(filters.Arg("name", nameFilter))
|
||
}
|
||
return c.cli.ContainerList(ctx, opts)
|
||
}
|
||
|
||
// ContainerInfo 容器详细信息
|
||
type ContainerInfo struct {
|
||
ID string
|
||
ImageID string
|
||
State string
|
||
Config *container.Config
|
||
HostConfig *container.HostConfig
|
||
}
|
||
|
||
// GetContainer 获取容器信息
|
||
func (c *Client) GetContainer(ctx context.Context, name string) (*ContainerInfo, error) {
|
||
containers, err := c.ListContainers(ctx, name)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, cont := range containers {
|
||
for _, n := range cont.Names {
|
||
if n == "/"+name || n == name {
|
||
// 获取详细信息
|
||
inspect, err := c.cli.ContainerInspect(ctx, cont.ID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &ContainerInfo{
|
||
ID: cont.ID,
|
||
ImageID: cont.ImageID,
|
||
State: cont.State,
|
||
Config: inspect.Config,
|
||
HostConfig: inspect.HostConfig,
|
||
}, nil
|
||
}
|
||
}
|
||
}
|
||
return nil, nil
|
||
}
|
||
|
||
// StopContainer 停止容器
|
||
func (c *Client) StopContainer(ctx context.Context, containerID string) error {
|
||
timeout := 30
|
||
return c.cli.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout})
|
||
}
|
||
|
||
// RemoveContainer 删除容器
|
||
func (c *Client) RemoveContainer(ctx context.Context, containerID string) error {
|
||
return c.cli.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{Force: true})
|
||
}
|
||
|
||
// StartContainer 启动容器
|
||
func (c *Client) StartContainer(ctx context.Context, containerID string) error {
|
||
return c.cli.ContainerStart(ctx, containerID, types.ContainerStartOptions{})
|
||
}
|
||
|
||
// RestartContainer 重启容器
|
||
func (c *Client) RestartContainer(ctx context.Context, containerID string) error {
|
||
timeout := 30
|
||
return c.cli.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeout})
|
||
}
|
||
|
||
// CreateAndStartContainer 创建并启动容器(如果已存在则跳过创建,直接启动)
|
||
func (c *Client) CreateAndStartContainer(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, name string) (string, error) {
|
||
// 检查容器是否已存在
|
||
existing, err := c.GetContainer(ctx, name)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
if existing != nil {
|
||
// 容器已存在,确保它在运行
|
||
if existing.State != "running" {
|
||
if err := c.cli.ContainerStart(ctx, existing.ID, types.ContainerStartOptions{}); err != nil {
|
||
return "", err
|
||
}
|
||
}
|
||
return existing.ID, nil
|
||
}
|
||
|
||
// 容器不存在,创建新容器
|
||
resp, err := c.cli.ContainerCreate(ctx, config, hostConfig, nil, nil, name)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
if err := c.cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return resp.ID, nil
|
||
}
|
||
|
||
// WaitForHealthy 等待容器健康
|
||
func (c *Client) WaitForHealthy(ctx context.Context, containerID string, timeout time.Duration) error {
|
||
if timeout <= 0 {
|
||
return fmt.Errorf("等待容器 %s 健康超时: %s", containerID, timeout)
|
||
}
|
||
|
||
deadline := time.Now().Add(timeout)
|
||
for {
|
||
inspect, err := c.cli.ContainerInspect(ctx, containerID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if inspect.State.Health != nil && inspect.State.Health.Status == "healthy" {
|
||
return nil
|
||
}
|
||
|
||
if inspect.State.Running {
|
||
// 如果没有健康检查,只要运行就算成功
|
||
if inspect.State.Health == nil {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
remaining := time.Until(deadline)
|
||
if remaining <= 0 {
|
||
return fmt.Errorf("等待容器 %s 健康超时: %s", containerID, timeout)
|
||
}
|
||
|
||
sleepFor := 2 * time.Second
|
||
if remaining < sleepFor {
|
||
sleepFor = remaining
|
||
}
|
||
|
||
timer := time.NewTimer(sleepFor)
|
||
select {
|
||
case <-ctx.Done():
|
||
timer.Stop()
|
||
return ctx.Err()
|
||
case <-timer.C:
|
||
}
|
||
}
|
||
}
|
||
|
||
// GetDockerInfo 获取 Docker 信息
|
||
func (c *Client) GetDockerInfo(ctx context.Context) (types.Info, error) {
|
||
return c.cli.Info(ctx)
|
||
}
|
||
|
||
// CreateNetworkIfNotExists 创建网络(如果不存在)
|
||
func (c *Client) CreateNetworkIfNotExists(ctx context.Context, name string) error {
|
||
networks, err := c.cli.NetworkList(ctx, types.NetworkListOptions{
|
||
Filters: filters.NewArgs(filters.Arg("name", name)),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
for _, n := range networks {
|
||
if n.Name == name {
|
||
return nil // 网络已存在
|
||
}
|
||
}
|
||
|
||
_, err = c.cli.NetworkCreate(ctx, name, types.NetworkCreate{
|
||
Driver: "bridge",
|
||
})
|
||
return err
|
||
}
|
||
|
||
// RemoveImage 删除镜像
|
||
func (c *Client) RemoveImage(ctx context.Context, imageID string) error {
|
||
_, err := c.cli.ImageRemove(ctx, imageID, types.ImageRemoveOptions{Force: false, PruneChildren: true})
|
||
return err
|
||
}
|
||
|
||
// PruneImages 清理悬空镜像
|
||
func (c *Client) PruneImages(ctx context.Context) error {
|
||
_, err := c.cli.ImagesPrune(ctx, filters.NewArgs(filters.Arg("dangling", "true")))
|
||
return err
|
||
}
|
||
|
||
// ExecInContainer 在容器内执行命令
|
||
func (c *Client) ExecInContainer(ctx context.Context, containerName string, cmd []string) error {
|
||
// 获取容器
|
||
cont, err := c.GetContainer(ctx, containerName)
|
||
if err != nil || cont == nil {
|
||
return fmt.Errorf("容器 %s 不存在", containerName)
|
||
}
|
||
|
||
// 创建 exec 实例
|
||
execConfig := types.ExecConfig{
|
||
Cmd: cmd,
|
||
AttachStdout: true,
|
||
AttachStderr: true,
|
||
}
|
||
execResp, err := c.cli.ContainerExecCreate(ctx, cont.ID, execConfig)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 执行命令
|
||
if err := c.cli.ContainerExecStart(ctx, execResp.ID, types.ExecStartCheck{}); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 等待执行完成
|
||
for {
|
||
inspect, err := c.cli.ContainerExecInspect(ctx, execResp.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !inspect.Running {
|
||
if inspect.ExitCode != 0 {
|
||
return fmt.Errorf("命令执行失败,退出码: %d", inspect.ExitCode)
|
||
}
|
||
break
|
||
}
|
||
time.Sleep(100 * time.Millisecond)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// ExecOnHost 在主机上执行命令(通过临时容器)
|
||
func (c *Client) ExecOnHost(ctx context.Context, cmd string) error {
|
||
// 使用可配置的 alpine 镜像,默认走官方镜像,不再绑定旧 Harbor
|
||
alpineImage := os.Getenv("ALPINE_IMAGE")
|
||
if alpineImage == "" {
|
||
alpineImage = "alpine:3.21"
|
||
}
|
||
|
||
// 先拉取镜像
|
||
if err := c.PullImage(ctx, alpineImage, ""); err != nil {
|
||
return fmt.Errorf("拉取 alpine 镜像失败: %w", err)
|
||
}
|
||
|
||
cfg := &container.Config{
|
||
Image: alpineImage,
|
||
Cmd: []string{"sh", "-c", cmd},
|
||
}
|
||
hcfg := &container.HostConfig{
|
||
Binds: []string{"/:/host"},
|
||
AutoRemove: true,
|
||
}
|
||
|
||
// 修改命令,使其在 /host 目录下执行
|
||
cfg.Cmd = []string{"sh", "-c", "cd /host && " + cmd}
|
||
|
||
resp, err := c.cli.ContainerCreate(ctx, cfg, hcfg, nil, nil, "")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := c.cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 等待容器完成
|
||
statusCh, errCh := c.cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
|
||
select {
|
||
case err := <-errCh:
|
||
if err != nil {
|
||
return err
|
||
}
|
||
case status := <-statusCh:
|
||
if status.StatusCode != 0 {
|
||
return fmt.Errorf("命令执行失败,退出码: %d", status.StatusCode)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|