🎨 优化容器网络配置,支持自动分配可用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"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -36,6 +37,16 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string,
|
|||||||
// 检测是否在容器内运行
|
// 检测是否在容器内运行
|
||||||
inContainer := isRunningInContainer()
|
inContainer := isRunningInContainer()
|
||||||
|
|
||||||
|
// 如果在容器内运行,获取当前容器的网络设置
|
||||||
|
var currentNetworkName string
|
||||||
|
if inContainer {
|
||||||
|
currentNetworkName = getCurrentContainerNetwork(ctx, cli)
|
||||||
|
if currentNetworkName != "" {
|
||||||
|
// 使用发现的网络替代配置中的网络
|
||||||
|
cfg.Network = currentNetworkName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 端口映射 - 将容器的9000端口映射到主机的端口
|
// 端口映射 - 将容器的9000端口映射到主机的端口
|
||||||
hostPort := nat.Port("9000/tcp")
|
hostPort := nat.Port("9000/tcp")
|
||||||
exposedPorts := nat.PortSet{
|
exposedPorts := nat.PortSet{
|
||||||
@ -94,8 +105,31 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string,
|
|||||||
// 设置网络配置
|
// 设置网络配置
|
||||||
networkingConfig := &network.NetworkingConfig{}
|
networkingConfig := &network.NetworkingConfig{}
|
||||||
if cfg.Network != "" {
|
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 := 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
|
networkingConfig.EndpointsConfig = endpointsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,11 +150,341 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string,
|
|||||||
return resp.ID, nil
|
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 检测当前程序是否在容器内运行
|
// isRunningInContainer 检测当前程序是否在容器内运行
|
||||||
func isRunningInContainer() bool {
|
func isRunningInContainer() bool {
|
||||||
// 检查环境变量 IS_DOCKER
|
// 检查环境变量 IS_DOCKER
|
||||||
isDockEnv := os.Getenv("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 查找指定镜像的容器已使用的最大端口号
|
// findMaxPortForImage 查找指定镜像的容器已使用的最大端口号
|
||||||
@ -286,7 +650,7 @@ func GetContainerIP(ctx context.Context, containerID string, cfg *config.DockerC
|
|||||||
|
|
||||||
// GetContainerHost 获取容器的访问地址(格式:ip:port)
|
// GetContainerHost 获取容器的访问地址(格式:ip:port)
|
||||||
func GetContainerHost(ctx context.Context, containerID string, cfg *config.DockerConfig) (string, error) {
|
func GetContainerHost(ctx context.Context, containerID string, cfg *config.DockerConfig) (string, error) {
|
||||||
// 如果在Docker环境中运行,需要获取容器真实IP
|
// 如果在Docker环境中运行,需要获取容器真实IP或容器名
|
||||||
if isRunningInContainer() {
|
if isRunningInContainer() {
|
||||||
cli := GetClient()
|
cli := GetClient()
|
||||||
|
|
||||||
@ -295,16 +659,26 @@ func GetContainerHost(ctx context.Context, containerID string, cfg *config.Docke
|
|||||||
return "", fmt.Errorf("无法检查容器: %w", err)
|
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 != "" {
|
if network.IPAddress != "" {
|
||||||
// 容器内部访问使用IP:9000格式
|
// 容器内部访问使用IP:9000格式
|
||||||
return network.IPAddress + ":9000", nil
|
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 {
|
} else {
|
||||||
// 如果不是在Docker环境中运行,需要获取端口映射
|
// 如果不是在Docker环境中运行,需要获取端口映射
|
||||||
cli := GetClient()
|
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">
|
<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> 登录
|
<i class="fas fa-qrcode mr-2"></i> 登录
|
||||||
</a>
|
</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> 删除
|
<i class="fas fa-trash mr-2"></i> 删除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -255,6 +256,25 @@
|
|||||||
setInterval(loadStats, 5000); // 每5秒更新一次
|
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) {
|
function deleteRobot(id) {
|
||||||
fetch(`/admin/robots/${id}`, {
|
fetch(`/admin/robots/${id}`, {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user