2025-08-10 03:29:09 +00:00
|
|
|
package docker
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2025-09-20 01:56:37 +00:00
|
|
|
"fmt"
|
2025-08-10 03:29:09 +00:00
|
|
|
"io"
|
|
|
|
|
"os"
|
2025-09-20 01:56:37 +00:00
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
2025-08-10 03:29:09 +00:00
|
|
|
|
2025-09-20 01:56:37 +00:00
|
|
|
"code.gitea.io/gitea/modules/log"
|
2025-08-10 03:29:09 +00:00
|
|
|
"github.com/docker/docker/api/types"
|
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
|
|
|
"github.com/docker/docker/client"
|
2025-09-20 01:56:37 +00:00
|
|
|
"github.com/docker/docker/pkg/stdcopy"
|
|
|
|
|
"github.com/docker/go-connections/nat"
|
2025-08-10 03:29:09 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// CreateDockerClient 创建Docker客户端
|
|
|
|
|
func CreateDockerClient(ctx context.Context) (*client.Client, error) {
|
2025-09-20 01:56:37 +00:00
|
|
|
log.Info("检查 Docker 环境")
|
|
|
|
|
// 1. 检查 Docker 环境
|
|
|
|
|
dockerSocketPath, err := GetDockerSocketPath()
|
2025-08-10 03:29:09 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2025-09-20 01:56:37 +00:00
|
|
|
log.Info("dockerSocketPath: %s", dockerSocketPath)
|
|
|
|
|
// 2. 创建docker client 并且检查是否运行
|
|
|
|
|
|
|
|
|
|
opts := []client.Opt{
|
|
|
|
|
client.FromEnv,
|
|
|
|
|
}
|
|
|
|
|
if dockerSocketPath != "" {
|
|
|
|
|
opts = append(opts, client.WithHost(dockerSocketPath))
|
|
|
|
|
}
|
|
|
|
|
cli, err := client.NewClientWithOpts(opts...)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
_, err = cli.Ping(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("docker未运行, %w", err)
|
|
|
|
|
}
|
2025-08-10 03:29:09 +00:00
|
|
|
return cli, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetDockerSocketPath 获取Docker Socket路径
|
|
|
|
|
func GetDockerSocketPath() (string, error) {
|
2025-09-20 01:56:37 +00:00
|
|
|
// 检查环境变量
|
|
|
|
|
socket, found := os.LookupEnv("DOCKER_HOST")
|
|
|
|
|
if found {
|
|
|
|
|
return socket, nil
|
|
|
|
|
}
|
2025-08-10 03:29:09 +00:00
|
|
|
// 检查常见的Docker socket路径
|
|
|
|
|
socketPaths := []string{
|
|
|
|
|
"/var/run/docker.sock",
|
|
|
|
|
"/run/podman/podman.sock",
|
|
|
|
|
"$HOME/.colima/docker.sock",
|
|
|
|
|
"$XDG_RUNTIME_DIR/docker.sock",
|
|
|
|
|
"$XDG_RUNTIME_DIR/podman/podman.sock",
|
|
|
|
|
`\\.\pipe\docker_engine`,
|
|
|
|
|
"$HOME/.docker/run/docker.sock",
|
|
|
|
|
}
|
2025-09-20 01:56:37 +00:00
|
|
|
// 测试Docker默认路径
|
|
|
|
|
for _, p := range socketPaths {
|
|
|
|
|
if _, err := os.Lstat(os.ExpandEnv(p)); err == nil {
|
|
|
|
|
if strings.HasPrefix(p, `\\.\`) {
|
|
|
|
|
return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), nil
|
|
|
|
|
}
|
|
|
|
|
return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), nil
|
2025-08-10 03:29:09 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-20 01:56:37 +00:00
|
|
|
// 找不到
|
|
|
|
|
return "", fmt.Errorf("Docker未安装")
|
|
|
|
|
}
|
|
|
|
|
func GetContainerID(cli *client.Client, containerName string) (string, error) {
|
|
|
|
|
containerJSON, err := cli.ContainerInspect(context.Background(), containerName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return containerJSON.ID, nil
|
|
|
|
|
}
|
2025-08-10 03:29:09 +00:00
|
|
|
|
2025-09-20 01:56:37 +00:00
|
|
|
func GetContainerStatus(cli *client.Client, containerID string) (string, error) {
|
|
|
|
|
containerInfo, err := cli.ContainerInspect(context.Background(), containerID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state := containerInfo.State
|
|
|
|
|
return state.Status, nil
|
|
|
|
|
}
|
|
|
|
|
func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error {
|
|
|
|
|
script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " "
|
|
|
|
|
cmd := exec.Command("sh", "-c", script)
|
|
|
|
|
_, err := cmd.CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
// 推送到仓库
|
|
|
|
|
script = "docker " + "-H " + dockerHost + " push " + imageRef
|
|
|
|
|
cmd = exec.Command("sh", "-c", script)
|
|
|
|
|
_, err = cmd.CombinedOutput()
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2025-08-10 03:29:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PullImage 拉取Docker镜像
|
2025-09-20 01:56:37 +00:00
|
|
|
func PullImage(ctx context.Context, cli *client.Client, dockerHost, imageName string) error {
|
2025-08-10 03:29:09 +00:00
|
|
|
|
|
|
|
|
reader, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer reader.Close()
|
|
|
|
|
|
|
|
|
|
// 读取并丢弃输出,确保拉取完成
|
|
|
|
|
_, err = io.Copy(io.Discard, reader)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateAndStartContainer 创建并启动容器
|
2025-09-20 01:56:37 +00:00
|
|
|
func CreateAndStartContainer(ctx context.Context, cli *client.Client, imageName string, cmd []string, env []string, binds []string, ports nat.PortSet, containerName string) error {
|
2025-08-10 03:29:09 +00:00
|
|
|
|
|
|
|
|
// 配置容器
|
|
|
|
|
config := &container.Config{
|
|
|
|
|
Image: imageName,
|
|
|
|
|
Env: env,
|
|
|
|
|
}
|
2025-09-20 01:56:37 +00:00
|
|
|
if ports != nil {
|
|
|
|
|
config.ExposedPorts = ports
|
|
|
|
|
}
|
2025-08-10 03:29:09 +00:00
|
|
|
if cmd != nil {
|
|
|
|
|
config.Cmd = cmd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hostConfig := &container.HostConfig{
|
|
|
|
|
Binds: binds,
|
2025-09-20 01:56:37 +00:00
|
|
|
RestartPolicy: container.RestartPolicy{
|
|
|
|
|
Name: "always", // 设置为 always
|
|
|
|
|
},
|
|
|
|
|
PublishAllPorts: true,
|
2025-08-10 03:29:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建容器
|
|
|
|
|
resp, err := cli.ContainerCreate(ctx, config, hostConfig, nil, nil, containerName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 启动容器
|
|
|
|
|
return cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DeleteContainer 停止并删除指定名称的容器
|
2025-09-20 01:56:37 +00:00
|
|
|
func DeleteContainer(ctx context.Context, cli *client.Client, containerName string) error {
|
2025-08-10 03:29:09 +00:00
|
|
|
|
|
|
|
|
// 首先尝试停止容器
|
|
|
|
|
timeout := 10
|
|
|
|
|
err := cli.ContainerStop(ctx, containerName, container.StopOptions{
|
|
|
|
|
Timeout: &timeout,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
// 如果容器已经停止或不存在,继续执行删除操作
|
|
|
|
|
// 这里不返回错误,因为我们的目标是删除容器
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除容器
|
|
|
|
|
err = cli.ContainerRemove(ctx, containerName, types.ContainerRemoveOptions{
|
|
|
|
|
Force: true, // 强制删除,即使容器正在运行
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-09-20 01:56:37 +00:00
|
|
|
func IsContainerNotFound(err error) bool {
|
|
|
|
|
if client.IsErrNotFound(err) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取容器端口映射到了主机哪个端口,参数: DockerClient、containerName、容器端口号
|
|
|
|
|
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
|
|
|
|
|
// 创建 Docker 客户端
|
|
|
|
|
cli, err := CreateDockerClient(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
// 获取容器 ID
|
|
|
|
|
containerID, err := GetContainerID(cli, containerName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
// 获取容器详细信息
|
|
|
|
|
containerJSON, err := cli.ContainerInspect(context.Background(), containerID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取端口映射信息
|
|
|
|
|
portBindings := containerJSON.NetworkSettings.Ports
|
|
|
|
|
for containerPort, bindings := range portBindings {
|
|
|
|
|
for _, binding := range bindings {
|
|
|
|
|
log.Info("容器端口 %s 映射到主机 %s 端口 %s \n", containerPort, binding.HostIP, binding.HostPort)
|
|
|
|
|
if containerPort.Port() == port {
|
|
|
|
|
port_64, err := strconv.ParseUint(binding.HostPort, 10, 16)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
return uint16(port_64), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0, fmt.Errorf("容器未开放端口 %s", port)
|
|
|
|
|
}
|
|
|
|
|
func ExecCommandInContainer(ctx context.Context, cli *client.Client, containerName string, command string) (string, error) {
|
|
|
|
|
containerID, err := GetContainerID(cli, containerName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Info("创建执行实例失败", err)
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
cmdList := []string{"sh", "-c", command}
|
|
|
|
|
execConfig := types.ExecConfig{
|
|
|
|
|
Cmd: cmdList,
|
|
|
|
|
AttachStdout: true,
|
|
|
|
|
AttachStderr: true,
|
|
|
|
|
}
|
|
|
|
|
// 创建执行实例
|
|
|
|
|
exec, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Info("创建执行实例失败", err)
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
// 附加到执行实例
|
|
|
|
|
resp, err := cli.ContainerExecAttach(context.Background(), exec.ID, types.ExecStartCheck{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Info("命令附加到执行实例失败", err)
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Close()
|
|
|
|
|
// 启动执行实例
|
|
|
|
|
err = cli.ContainerExecStart(context.Background(), exec.ID, types.ExecStartCheck{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Info("启动执行实例失败", err)
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
// 自定义缓冲区
|
|
|
|
|
var outBuf, errBuf strings.Builder
|
|
|
|
|
|
|
|
|
|
// 读取输出
|
|
|
|
|
_, err = stdcopy.StdCopy(&outBuf, &errBuf, resp.Reader)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Info("Error reading output for command %v: %v\n", command, err)
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return outBuf.String() + errBuf.String(), nil
|
|
|
|
|
}
|