李寻欢 37b766368f
All checks were successful
BuildImage / build-image (push) Successful in 4m40s
🎨 优化网络配置,添加子网定义以支持固定IP分配,并更新微信状态显示
2025-04-07 11:56:04 +08:00

729 lines
18 KiB
Go
Raw 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 (
"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
}