Files
devstar/modules/docker/docker_api.go
2025-08-11 11:29:50 +08:00

334 lines
9.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 表示目录存在
}