355 lines
9.3 KiB
Go
355 lines
9.3 KiB
Go
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
|
||
}
|