package docker import ( "context" "fmt" "io" "net" "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() // 如果在容器内运行,获取当前容器的网络设置 var currentNetworkName string if inContainer { currentNetworkName = getCurrentContainerNetwork(ctx, cli) if currentNetworkName != "" { // 使用发现的网络替代配置中的网络 cfg.Network = currentNetworkName } } // 端口映射 - 将容器的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 != "" { // 首先检查网络类型,只在用户自定义网络上分配固定IP isUserNetwork := false if cfg.Network != "bridge" && cfg.Network != "host" && cfg.Network != "none" { // 检查网络是否存在 _, err := cli.NetworkInspect(ctx, cfg.Network, types.NetworkInspectOptions{}) if err == nil { isUserNetwork = true } } endpointsConfig := make(map[string]*network.EndpointSettings) endpointSettings := &network.EndpointSettings{} // 只在用户自定义网络上尝试分配固定IP if isUserNetwork { // 自动为容器分配一个递增的IP地址 nextIP, err := getNextAvailableIPInNetwork(ctx, cli, cfg.Network) if err == nil && nextIP != "" { endpointSettings.IPAMConfig = &network.EndpointIPAMConfig{ IPv4Address: nextIP, } } } endpointsConfig[cfg.Network] = endpointSettings networkingConfig.EndpointsConfig = endpointsConfig } // 创建容器 resp, err := cli.ContainerCreate( ctx, containerConfig, hostConfig, networkingConfig, nil, // 平台 name, ) if err != nil { return "", err } return resp.ID, nil } // getNextAvailableIPInNetwork 获取网络中下一个可用的IP地址 func getNextAvailableIPInNetwork(ctx context.Context, cli *client.Client, networkName string) (string, error) { // 获取网络信息 networkResource, err := cli.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{}) if err != nil { return "", fmt.Errorf("无法检查网络: %w", err) } // 确认网络是否有配置子网 if len(networkResource.IPAM.Config) == 0 { return "", fmt.Errorf("网络没有IPAM配置") } // 获取网络子网 subnet := networkResource.IPAM.Config[0].Subnet if subnet == "" { return "", fmt.Errorf("网络子网未定义,不能分配固定IP") } // 解析子网 _, ipNet, err := net.ParseCIDR(subnet) if err != nil { return "", fmt.Errorf("无法解析子网CIDR: %w", err) } // 获取网络中的所有容器 containers, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true}) if err != nil { return "", fmt.Errorf("无法列出容器: %w", err) } // 获取已使用的IP地址 usedIPs := make(map[string]bool) for _, c := range containers { for name, network := range c.NetworkSettings.Networks { if name == networkName && network.IPAddress != "" { usedIPs[network.IPAddress] = true } } } // 获取网络中的所有IP地址,查找最大IP var maxIP net.IP for ip := range usedIPs { ipAddr := net.ParseIP(ip) if ipAddr == nil { continue } // 确保IP在子网内 if !ipNet.Contains(ipAddr) { continue } // 更新最大IP if maxIP == nil || compareIPs(ipAddr, maxIP) > 0 { maxIP = ipAddr } } // 如果网络中没有容器,使用网关地址的下一个IP if maxIP == nil { // 获取网关IP gateway := networkResource.IPAM.Config[0].Gateway if gateway == "" { // 如果没有定义网关,使用子网的第一个可用IP ip := ipNet.IP.To4() if ip == nil { return "", fmt.Errorf("无法获取IPv4地址") } // 通常第一个可用IP是网关后的地址(x.x.x.1 + 1 = x.x.x.2) ip[3]++ maxIP = ip } else { gwIP := net.ParseIP(gateway) if gwIP == nil { return "", fmt.Errorf("无效的网关地址") } // 使用网关IP的下一个地址 maxIP = gwIP.To4() if maxIP == nil { return "", fmt.Errorf("无法获取IPv4网关地址") } maxIP[3]++ } } else { // 如果有容器,则使用最大IP + 1 maxIP = incrementIP(maxIP) } // 确保新IP仍然在子网内 if !ipNet.Contains(maxIP) { return "", fmt.Errorf("无法分配下一个IP,已达到子网范围上限") } return maxIP.String(), nil } // compareIPs 比较两个IP地址的大小 func compareIPs(a, b net.IP) int { // 转为IPv4格式 a4 := a.To4() b4 := b.To4() if a4 == nil || b4 == nil { // 如果任一IP不是IPv4,则无法比较 return 0 } // 逐字节比较 for i := 0; i < 4; i++ { if a4[i] < b4[i] { return -1 } else if a4[i] > b4[i] { return 1 } } return 0 } // incrementIP 将IP地址递增1 func incrementIP(ip net.IP) net.IP { // 创建IP的副本 newIP := make(net.IP, len(ip)) copy(newIP, ip) // 确保使用IPv4格式 ipv4 := newIP.To4() if ipv4 == nil { return newIP } // 递增最后一个字节 for i := 3; i >= 0; i-- { ipv4[i]++ if ipv4[i] > 0 { // 如果没有溢出,退出循环 break } // 如果溢出(变成0),继续递增上一个字节 } return ipv4 } // getNextAvailableIP 获取网络中下一个可用的IP地址 func getNextAvailableIP(ctx context.Context, cli *client.Client, networkName string) (string, error) { // 获取网络信息 networkResource, err := cli.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{}) if err != nil { return "", fmt.Errorf("无法检查网络: %w", err) } // 确认网络类型是否为bridge,并获取子网信息 if networkResource.IPAM.Config == nil || len(networkResource.IPAM.Config) == 0 { return "", fmt.Errorf("网络没有IPAM配置") } // 获取网络子网 subnet := networkResource.IPAM.Config[0].Subnet if subnet == "" { return "", fmt.Errorf("网络子网未定义") } // 解析子网 _, ipNet, err := net.ParseCIDR(subnet) if err != nil { return "", fmt.Errorf("无法解析子网CIDR: %w", err) } // 获取现有容器的IP地址 existingIPs := make(map[string]bool) containers, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true}) if err != nil { return "", fmt.Errorf("无法列出容器: %w", err) } // 收集所有容器在这个网络中使用的IP for _, c := range containers { if c.NetworkSettings != nil { for name, network := range c.NetworkSettings.Networks { if name == networkName && network.IPAddress != "" { existingIPs[network.IPAddress] = true } } } } // 确定最大IP地址 var maxIP net.IP for ipStr := range existingIPs { ip := net.ParseIP(ipStr) if ip == nil { continue } if !ipNet.Contains(ip) { continue // IP不在子网内 } if maxIP == nil || compareIPs(ip, maxIP) > 0 { maxIP = ip } } // 如果没有找到最大IP,使用子网的第一个可用IP(通常是x.x.x.2,因为x.x.x.1通常是网关) if maxIP == nil { firstIP := getFirstUsableIP(ipNet) if firstIP != nil { return firstIP.String(), nil } return "", fmt.Errorf("无法确定子网的第一个可用IP") } // 递增最大IP地址 nextIP := getNextIP(maxIP) // 确保新IP仍然在子网内 if !ipNet.Contains(nextIP) { return "", fmt.Errorf("无法分配下一个IP,已达到子网范围上限") } return nextIP.String(), nil } // getNextIP 获取下一个IP地址 func getNextIP(ip net.IP) net.IP { // 创建IP的副本 nextIP := make(net.IP, len(ip)) copy(nextIP, ip) // 确保使用IPv4格式 nextIP = nextIP.To4() if nextIP == nil { return nil } // 从最后一个字节开始递增 for i := len(nextIP) - 1; i >= 0; i-- { if nextIP[i] < 255 { nextIP[i]++ break } nextIP[i] = 0 // 进位 } return nextIP } // getFirstUsableIP 获取子网中的第一个可用IP func getFirstUsableIP(ipNet *net.IPNet) net.IP { // 子网的第一个地址通常是网络地址,不可用 // 第二个地址通常是网关地址,也可能不可用 // 因此我们从第三个地址开始(通常是x.x.x.2) // 获取子网的起始IP ip := ipNet.IP.To4() if ip == nil { return nil } // 创建副本 firstIP := make(net.IP, len(ip)) copy(firstIP, ip) // 递增到第三个地址 thirdOctet := int(firstIP[2]) fourthOctet := int(firstIP[3]) + 2 // 通常是x.x.x.2 // 处理进位 if fourthOctet > 255 { fourthOctet = fourthOctet % 256 thirdOctet++ if thirdOctet > 255 { // 溢出处理(通常不会发生在正常子网中) return nil } } firstIP[2] = byte(thirdOctet) firstIP[3] = byte(fourthOctet) return firstIP } // getCurrentContainerNetwork 获取当前容器所在的网络 func getCurrentContainerNetwork(ctx context.Context, cli *client.Client) string { // 获取当前容器的ID hostname, err := os.Hostname() if err != nil { return "" } // 查询容器信息 containerInfo, err := cli.ContainerInspect(ctx, hostname) if err != nil { return "" } // 遍历容器的网络配置,获取第一个非空网络名称 for networkName := range containerInfo.NetworkSettings.Networks { if networkName != "bridge" && networkName != "host" && networkName != "none" { return networkName // 返回自定义网络名称 } } // 如果只有默认网络,则返回第一个网络名称 for networkName := range containerInfo.NetworkSettings.Networks { return networkName // 返回第一个找到的网络名称 } return "" } // isRunningInContainer 检测当前程序是否在容器内运行 func isRunningInContainer() bool { // 检查环境变量 IS_DOCKER isDockEnv := os.Getenv("IS_DOCKER") if isDockEnv == "true" || isDockEnv == "1" || isDockEnv == "yes" { return true } // 通过检查cgroup文件判断 if _, err := os.Stat("/.dockerenv"); err == nil { return true } // 检查进程的cgroup信息 if data, err := os.ReadFile("/proc/1/cgroup"); err == nil { return strings.Contains(string(data), "docker") } return false } // 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) } // 使用容器名作为主机名,用于内部网络通信 containerName := inspect.Name if containerName != "" { // 移除前导斜杠 if strings.HasPrefix(containerName, "/") { containerName = containerName[1:] } // 使用容器名:9000作为内部访问地址 return containerName + ":9000", nil } // 备选方案:尝试使用容器IP for _, network := range inspect.NetworkSettings.Networks { if network.IPAddress != "" { // 容器内部访问使用IP:9000格式 return network.IPAddress + ":9000", nil } } 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 }