334 lines
9.4 KiB
Go
334 lines
9.4 KiB
Go
package docker
|
||
|
||
import (
|
||
"archive/tar"
|
||
"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) 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
|
||
}
|
||
|
||
return 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
|
||
}
|
||
|
||
// ImageExists 检查指定镜像是否存在
|
||
// 返回值:
|
||
// - bool: 镜像是否存在(true=存在,false=不存在)
|
||
// - error: 非空表示检查过程中发生错误
|
||
func ImageExists(imageName string) (bool, error) {
|
||
// 上下文
|
||
ctx := context.Background()
|
||
// 创建 Docker 客户端
|
||
cli, err := CreateDockerClient(&ctx)
|
||
if err != nil {
|
||
return false, err // 其他错误
|
||
}
|
||
// 获取镜像信息
|
||
_, _, err = cli.ImageInspectWithRaw(ctx, imageName)
|
||
if err != nil {
|
||
return false, err // 其他错误
|
||
}
|
||
return true, nil // 镜像存在
|
||
}
|
||
func CheckDirExists(cli *client.Client, containerID, dirPath string) (bool, error) {
|
||
// 创建 exec 配置
|
||
execConfig := types.ExecConfig{
|
||
Cmd: []string{"test", "-d", dirPath}, // 检查目录是否存在
|
||
AttachStdout: true,
|
||
AttachStderr: true,
|
||
}
|
||
|
||
// 创建 exec 实例
|
||
execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
|
||
if err != nil {
|
||
return false, fmt.Errorf("创建 exec 实例失败: %v", err)
|
||
}
|
||
|
||
// 执行命令
|
||
var exitCode int
|
||
err = cli.ContainerExecStart(context.Background(), execResp.ID, types.ExecStartCheck{})
|
||
if err != nil {
|
||
return false, fmt.Errorf("启动 exec 命令失败: %v", err)
|
||
}
|
||
|
||
// 获取命令执行结果
|
||
resp, err := cli.ContainerExecInspect(context.Background(), execResp.ID)
|
||
if err != nil {
|
||
return false, fmt.Errorf("获取 exec 结果失败: %v", err)
|
||
}
|
||
|
||
exitCode = resp.ExitCode
|
||
return exitCode == 0, nil // 退出码为 0 表示目录存在
|
||
}
|