Files
devstar/modules/docker/docker_api.go
2025-04-28 21:28:16 +08:00

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
}