310 lines
8.8 KiB
Go
310 lines
8.8 KiB
Go
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
|
|
}
|