🎨 优化容器网络配置,支持自动分配可用IP地址并修改删除按钮确认逻辑
All checks were successful
BuildImage / build-image (push) Successful in 4m0s
All checks were successful
BuildImage / build-image (push) Successful in 4m0s
This commit is contained in:
parent
37a628b5c3
commit
c409dc5a02
80
docker-compose.yaml
Normal file
80
docker-compose.yaml
Normal file
@ -0,0 +1,80 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# 微信机器人管理系统
|
||||
wechat-robot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: wechat-robot
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080" # 应用端口
|
||||
volumes:
|
||||
- ./configs:/app/configs # 配置文件
|
||||
- ./data:/app/data # 数据文件
|
||||
- ./logs:/app/logs # 日志文件
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Docker socket 用于容器管理
|
||||
# 添加 docker 组的 GID 到容器,999 是常见的 docker 组 GID,但在不同系统可能不同
|
||||
group_add:
|
||||
- "999" # 请确认您系统上的 docker 组 GID
|
||||
environment:
|
||||
- APP_ENV=production
|
||||
- TZ=Asia/Shanghai
|
||||
- DOCKER_HOST=unix:///var/run/docker.sock
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
networks:
|
||||
- wechat-network
|
||||
|
||||
# PostgreSQL 数据库
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
container_name: wechat-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: password # 生产环境请修改此密码
|
||||
POSTGRES_DB: wechat_demo
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- wechat-network
|
||||
|
||||
# Redis 服务
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: wechat-redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --requirepass pGhQKwj7DE7FbFL1 # 与配置中的密码一致
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- wechat-network
|
||||
|
||||
# PGAdmin(可选)数据库管理工具
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4
|
||||
container_name: wechat-pgadmin
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: admin@example.com
|
||||
PGADMIN_DEFAULT_PASSWORD: admin # 生产环境请修改此密码
|
||||
ports:
|
||||
- "5050:80"
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- wechat-network
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
|
||||
networks:
|
||||
wechat-network:
|
||||
driver: bridge
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -36,6 +37,16 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string,
|
||||
// 检测是否在容器内运行
|
||||
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{
|
||||
@ -94,8 +105,31 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string,
|
||||
// 设置网络配置
|
||||
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)
|
||||
endpointsConfig[cfg.Network] = &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
|
||||
}
|
||||
|
||||
@ -116,11 +150,341 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string,
|
||||
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 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)
|
||||
}
|
||||
|
||||
// 获取网络中的所有容器
|
||||
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")
|
||||
return isDockEnv == "true" || isDockEnv == "1" || isDockEnv == "yes"
|
||||
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 查找指定镜像的容器已使用的最大端口号
|
||||
@ -286,7 +650,7 @@ func GetContainerIP(ctx context.Context, containerID string, cfg *config.DockerC
|
||||
|
||||
// GetContainerHost 获取容器的访问地址(格式:ip:port)
|
||||
func GetContainerHost(ctx context.Context, containerID string, cfg *config.DockerConfig) (string, error) {
|
||||
// 如果在Docker环境中运行,需要获取容器真实IP
|
||||
// 如果在Docker环境中运行,需要获取容器真实IP或容器名
|
||||
if isRunningInContainer() {
|
||||
cli := GetClient()
|
||||
|
||||
@ -295,16 +659,26 @@ func GetContainerHost(ctx context.Context, containerID string, cfg *config.Docke
|
||||
return "", fmt.Errorf("无法检查容器: %w", err)
|
||||
}
|
||||
|
||||
// 遍历网络配置找到IP地址
|
||||
for networkName, network := range inspect.NetworkSettings.Networks {
|
||||
// 使用容器名作为主机名,用于内部网络通信
|
||||
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
|
||||
}
|
||||
// 防止在没有获取到IP的情况下继续循环
|
||||
fmt.Printf("Network %s has no IP address\n", networkName)
|
||||
}
|
||||
return "localhost:9000", fmt.Errorf("未找到容器IP地址")
|
||||
|
||||
return "localhost:9000", fmt.Errorf("未找到容器名或IP地址")
|
||||
} else {
|
||||
// 如果不是在Docker环境中运行,需要获取端口映射
|
||||
cli := GetClient()
|
||||
|
@ -19,7 +19,8 @@
|
||||
<a href="/admin/robots/{{.Robot.ID}}/login" class="inline-flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors mr-2">
|
||||
<i class="fas fa-qrcode mr-2"></i> 登录
|
||||
</a>
|
||||
<button data-confirm="确定要删除此机器人吗?" data-confirm-type="danger" onclick="deleteRobot({{.Robot.ID}})" class="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 transition-colors">
|
||||
<!-- 修改删除按钮,使用confirmDialog而非直接调用deleteRobot函数 -->
|
||||
<button onclick="confirmDelete({{.Robot.ID}}, '{{.Robot.Nickname}}')" class="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 transition-colors">
|
||||
<i class="fas fa-trash mr-2"></i> 删除
|
||||
</button>
|
||||
</div>
|
||||
@ -255,6 +256,25 @@
|
||||
setInterval(loadStats, 5000); // 每5秒更新一次
|
||||
});
|
||||
|
||||
// 确认删除函数
|
||||
async function confirmDelete(id, name) {
|
||||
if (typeof confirmDialog === 'function') {
|
||||
const confirmed = await confirmDialog(
|
||||
`确定要删除机器人"${name}"吗?此操作将永久删除容器及相关数据,无法恢复!`,
|
||||
{ type: 'danger', title: '删除机器人' }
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
deleteRobot(id);
|
||||
}
|
||||
} else {
|
||||
// 降级方案:使用原生confirm
|
||||
if (confirm(`确定要删除机器人"${name}"吗?此操作不可恢复!`)) {
|
||||
deleteRobot(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除机器人
|
||||
function deleteRobot(id) {
|
||||
fetch(`/admin/robots/${id}`, {
|
||||
|
Loading…
x
Reference in New Issue
Block a user