package docker import ( "context" "fmt" "io" "os" "strconv" "strings" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "gitee.ltd/lxh/wechat-robot/internal/config" ) // ContainerInfo 容器信息 type ContainerInfo struct { ID string `json:"id"` Name string `json:"name"` Image string `json:"image"` Status string `json:"status"` Created int64 `json:"created"` Labels map[string]string `json:"labels"` } // CreateContainer 创建容器 func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string, env []string, labels map[string]string, port int) (string, error) { cli := GetClient() // 检测是否在容器内运行 inContainer := isRunningInContainer() // 端口映射 - 将容器的9000端口映射到主机的端口 hostPort := nat.Port("9000/tcp") exposedPorts := nat.PortSet{ hostPort: struct{}{}, } // 设置主机端口映射 portBindings := nat.PortMap{} // 只有在非容器环境下才进行端口映射 if !inContainer { // 如果没有指定端口,则自动分配 if port <= 0 { // 查找同镜像容器的最大端口号并加1 maxPort, err := findMaxPortForImage(ctx, cli, cfg.ImageName) if err != nil { // 如果出错,使用默认端口9001 port = 9001 } else { port = maxPort + 1 } } // 添加端口映射 portBindings[hostPort] = []nat.PortBinding{ { HostIP: "0.0.0.0", HostPort: strconv.Itoa(port), }, } } // 添加Redis环境变量 if cfg.Redis.Host != "" { env = append(env, fmt.Sprintf("REDIS_HOST=%s", cfg.Redis.Host)) env = append(env, fmt.Sprintf("REDIS_PASSWORD=%s", cfg.Redis.Password)) env = append(env, fmt.Sprintf("REDIS_DB=%d", cfg.Redis.DB)) } // 设置容器配置 containerConfig := &container.Config{ Image: cfg.ImageName, Env: env, ExposedPorts: exposedPorts, Labels: labels, } // 设置主机配置 hostConfig := &container.HostConfig{ PortBindings: portBindings, RestartPolicy: container.RestartPolicy{ Name: "unless-stopped", }, } // 设置网络配置 networkingConfig := &network.NetworkingConfig{} if cfg.Network != "" { endpointsConfig := make(map[string]*network.EndpointSettings) endpointsConfig[cfg.Network] = &network.EndpointSettings{} networkingConfig.EndpointsConfig = endpointsConfig } // 创建容器 resp, err := cli.ContainerCreate( ctx, containerConfig, hostConfig, networkingConfig, nil, // 平台 name, ) if err != nil { return "", err } return resp.ID, nil } // isRunningInContainer 检测当前程序是否在容器内运行 func isRunningInContainer() bool { // 检查环境变量 IS_DOCKER isDockEnv := os.Getenv("IS_DOCKER") return isDockEnv == "true" || isDockEnv == "1" || isDockEnv == "yes" } // findMaxPortForImage 查找指定镜像的容器已使用的最大端口号 func findMaxPortForImage(ctx context.Context, cli *client.Client, imageName string) (int, error) { // 默认起始端口 maxPort := 9000 // 获取所有容器 containers, err := cli.ContainerList(ctx, types.ContainerListOptions{ All: true, }) if err != nil { return maxPort, err } // 遍历容器,查找使用相同镜像且端口映射最大的容器 for _, c := range containers { if c.Image == imageName || strings.HasPrefix(c.Image, imageName+":") { for _, port := range c.Ports { // 找到与内部9000端口映射的主机端口 if port.PrivatePort == 9000 && int(port.PublicPort) > maxPort { maxPort = int(port.PublicPort) } } } } return maxPort, nil } // StartContainer 启动容器 func StartContainer(ctx context.Context, containerID string) error { cli := GetClient() return cli.ContainerStart(ctx, containerID, types.ContainerStartOptions{}) } // StopContainer 停止容器 func StopContainer(ctx context.Context, containerID string, timeout *time.Duration) error { cli := GetClient() t := int(timeout.Seconds()) return cli.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &t}) } // RemoveContainer 删除容器 func RemoveContainer(ctx context.Context, containerID string, force bool) error { cli := GetClient() return cli.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{ Force: force, }) } // ListContainers 列出容器 func ListContainers(ctx context.Context, filterArgs map[string][]string) ([]ContainerInfo, error) { cli := GetClient() // 构建过滤器 filterSet := filters.NewArgs() for k, vals := range filterArgs { for _, v := range vals { filterSet.Add(k, v) } } containers, err := cli.ContainerList(ctx, types.ContainerListOptions{ All: true, // 包括未运行的容器 Filters: filterSet, }) if err != nil { return nil, err } var containerInfos []ContainerInfo for _, c := range containers { containerInfos = append(containerInfos, ContainerInfo{ ID: c.ID, Name: c.Names[0][1:], // 去掉前面的/ Image: c.Image, Status: c.Status, Created: c.Created, Labels: c.Labels, }) } return containerInfos, nil } // GetContainerLogs 获取容器日志 func GetContainerLogs(ctx context.Context, containerID string, tail string) (string, error) { cli := GetClient() options := types.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, Tail: tail, } logs, err := cli.ContainerLogs(ctx, containerID, options) if err != nil { return "", err } defer logs.Close() // 读取日志内容 logContent, err := io.ReadAll(logs) if err != nil { return "", err } return string(logContent), nil } // GetContainerStatus 获取容器状态 func GetContainerStatus(ctx context.Context, containerID string) (string, error) { cli := GetClient() inspect, err := cli.ContainerInspect(ctx, containerID) if err != nil { return "", err } if inspect.State.Running { return "running", nil } else if inspect.State.Paused { return "paused", nil } else if inspect.State.Restarting { return "restarting", nil } else { return "stopped", nil } } // WaitForContainer 等待容器达到指定状态 func WaitForContainer(ctx context.Context, containerID string) (<-chan container.WaitResponse, <-chan error) { cli := GetClient() return cli.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) } // GetContainerIP 获取容器的IP地址 func GetContainerIP(ctx context.Context, containerID string, cfg *config.DockerConfig) (string, error) { // 如果在Docker环境中运行,需要获取容器真实IP if isRunningInContainer() { cli := GetClient() inspect, err := cli.ContainerInspect(ctx, containerID) if err != nil { return "", fmt.Errorf("无法检查容器: %w", err) } // 遍历网络配置找到IP地址 for networkName, network := range inspect.NetworkSettings.Networks { if network.IPAddress != "" { return network.IPAddress, nil } // 防止在没有获取到IP的情况下继续循环 fmt.Printf("Network %s has no IP address\n", networkName) } return "localhost", fmt.Errorf("未找到容器IP地址") } else { // 如果不是在Docker环境中运行,使用配置中的主机地址 return extractHostIP(cfg.Host), nil } } // GetContainerHost 获取容器的访问地址(格式:ip:port) func GetContainerHost(ctx context.Context, containerID string, cfg *config.DockerConfig) (string, error) { // 如果在Docker环境中运行,需要获取容器真实IP if isRunningInContainer() { cli := GetClient() inspect, err := cli.ContainerInspect(ctx, containerID) if err != nil { return "", fmt.Errorf("无法检查容器: %w", err) } // 遍历网络配置找到IP地址 for networkName, network := range inspect.NetworkSettings.Networks { if network.IPAddress != "" { // 容器内部访问使用IP:9000格式 return network.IPAddress + ":9000", nil } // 防止在没有获取到IP的情况下继续循环 fmt.Printf("Network %s has no IP address\n", networkName) } return "localhost:9000", fmt.Errorf("未找到容器IP地址") } else { // 如果不是在Docker环境中运行,需要获取端口映射 cli := GetClient() inspect, err := cli.ContainerInspect(ctx, containerID) if err != nil { return "", fmt.Errorf("无法检查容器: %w", err) } hostIP := extractHostIP(cfg.Host) // 查找9000端口的映射 for _, port := range inspect.NetworkSettings.Ports["9000/tcp"] { if port.HostPort != "" { // 主机访问使用hostIP:映射端口格式 return hostIP + ":" + port.HostPort, nil } } // 如果找不到端口映射,返回默认的localhost:9000 return hostIP + ":9000", nil } } // extractHostIP 从Docker主机地址中提取IP func extractHostIP(host string) string { // 处理http格式URL if strings.HasPrefix(host, "http://") { host = strings.TrimPrefix(host, "http://") } else if strings.HasPrefix(host, "https://") { host = strings.TrimPrefix(host, "https://") } else if strings.HasPrefix(host, "tcp://") { host = strings.TrimPrefix(host, "tcp://") } // 如果是unix socket或空值,返回localhost if host == "" || strings.HasPrefix(host, "unix://") { return "localhost" } // 分离端口号 if index := strings.LastIndex(host, ":"); index != -1 { host = host[:index] } return host }