2025-04-02 14:29:44 +08:00

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