package docker import ( "archive/tar" "bytes" "context" "fmt" "net" "os" "os/exec" "path/filepath" "strings" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" ) func CreateDockerClient(ctx *context.Context) (*client.Client, error) { log.Info("检查 Docker 环境") // 1. 检查 Docker 环境 dockerSocketPath, err := GetDockerSocketPath() if err != nil { return nil, err } log.Info("dockerSocketPath: %s", dockerSocketPath) // 2. 创建docker client 并且检查是否运行 cli, err := checkIfDockerRunning(*ctx, dockerSocketPath) if err != nil { return nil, err } return cli, nil } var commonSocketPaths = []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", } // 优先级配置文件的DockerHost字段 > DOCKER_HOST环境变量 > docker普通默认路径 func GetDockerSocketPath() (string, error) { // 校验DockerHost配置 if setting.Devcontainer.DockerHost != "" && net.ParseIP(setting.Devcontainer.DockerHost) != nil { return setting.Devcontainer.DockerHost, nil } // 检查环境变量 socket, found := os.LookupEnv("DOCKER_HOST") if found { return socket, nil } // 测试Docker默认路径 for _, p := range commonSocketPaths { 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 } } return "", fmt.Errorf("Docker未安装") } func checkIfDockerRunning(ctx context.Context, configDockerHost string) (*client.Client, error) { opts := []client.Opt{ client.FromEnv, } if configDockerHost != "" { opts = append(opts, client.WithHost(configDockerHost)) } 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) } return cli, nil } // 获取容器端口映射到了主机哪个端口,参数: DockerClient、containerID、容器端口号 func GetMappedPort(cli *client.Client, containerID string, port string) (string, error) { // 获取容器详细信息 containerJSON, err := cli.ContainerInspect(context.Background(), containerID) if err != nil { return "", 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 { return binding.HostPort, nil } } } return "", fmt.Errorf("容器未开放端口" + port) } func GetAllMappedPort(cli *client.Client, containerID string) (string, error) { var result string = "\n" // 获取容器详细信息 containerJSON, err := cli.ContainerInspect(context.Background(), containerID) if err != nil { return "", err } // 获取端口映射信息 portBindings := containerJSON.NetworkSettings.Ports for containerPort, bindings := range portBindings { for _, binding := range bindings { result += fmt.Sprintf("容器端口 %s 映射到主机 %s 端口 %s \n", containerPort, binding.HostIP, binding.HostPort) } } return result, 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 } 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 } 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 ExecCommandInContainer(ctx *context.Context, cli *client.Client, containerID string, command string) (string, error) { 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 } func DeleteContainer(cli *client.Client, containerID string) error { // 删除容器 options := types.ContainerRemoveOptions{ Force: true, // 强制删除正在运行的容器 RemoveVolumes: true, // 删除数据卷 RemoveLinks: false, // 删除链接(已弃用) } if err := cli.ContainerRemove(context.Background(), containerID, options); err != nil { log.Info("删除 %s 容器失败: %v", containerID, err) return err } log.Info("容器 %s 已成功删除\n", containerID) return nil } // pullImage 用于拉取指定的 Docker 镜像 func PullImageSync(cli *client.Client, dockerHost string, image string) error { script := "docker " + "-H " + dockerHost + " pull " + image cmd := exec.Command("sh", "-c", script) output, err := cmd.CombinedOutput() log.Info(string(output)) if err != nil { return err } return nil } func CreateAndStartContainer(cli *client.Client, opts *CreateDevcontainerOptions) (string, error) { ctx := context.Background() // 创建容器配置 config := &container.Config{ Image: opts.Image, Cmd: opts.CommandList, Env: opts.ContainerEnv, AttachStdout: true, AttachStderr: true, Tty: true, OpenStdin: true, ExposedPorts: opts.ForwardPorts, } // 设置容器配置 hostConfig := &container.HostConfig{ //Hosts //ExtraHosts: []string{"host.docker.internal:" + host}, PublishAllPorts: true, Binds: nil, RestartPolicy: container.RestartPolicy{ Name: "always", }, PortBindings: opts.PortBindings, } if len(opts.Binds) > 0 { hostConfig.Binds = opts.Binds } // 创建容器 resp, err := cli.ContainerCreate(ctx, config, hostConfig, nil, nil, opts.Name) if err != nil { log.Info("fail to create container %v", err) return "", err } // 启动容器 err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) if err != nil { log.Info("fail to start container %v", err) return "", err } // 创建 tar 归档文件 var buf bytes.Buffer tw := tar.NewWriter(&buf) defer tw.Close() // 添加文件到 tar 归档 addFileToTar(tw, "devcontainer_init.sh", opts.InitializeCommand, 0777) addFileToTar(tw, "devcontainer_restart.sh", opts.RestartCommand, 0777) //ExecCommandInContainer(&ctx, cli, resp.ID, "touch /home/devcontainer_init.sh && chomd +x /home/devcontainer_init.sh") err = cli.CopyToContainer(ctx, resp.ID, "/home", bytes.NewReader(buf.Bytes()), types.CopyToContainerOptions{}) if err != nil { log.Info("%v", err) return "", err } // 获取日志流 out, _ := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, Follow: false, }) defer out.Close() // 5. 解析日志 var stdoutBuf, stderrBuf bytes.Buffer _, _ = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, out) return stdoutBuf.String() + "\n" + stderrBuf.String(), nil } // addFileToTar 将文件添加到 tar 归档 func addFileToTar(tw *tar.Writer, filename string, content string, mode int64) error { hdr := &tar.Header{ Name: filename, Mode: mode, Size: int64(len(content)), } if err := tw.WriteHeader(hdr); err != nil { return err } if _, err := tw.Write([]byte(content)); err != nil { return err } return nil }