mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 22:30:47 +01:00
Moved non-agent-specific logic from agent/pkg/agent/ to internal/agentpool/: - pool.go: Agent pool management (Get, Add, Remove, List, Iter, etc.) - http_requests.go: HTTP utilities (health checks, forwarding, websockets, reverse proxy) - agent.go: Agent struct with HTTP client management This separates general-purpose pool management from agent-specific configuration, improving code organization and making the agent package focused on agent config only.
285 lines
6.8 KiB
Go
285 lines
6.8 KiB
Go
package docker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/docker/go-connections/nat"
|
|
"github.com/moby/moby/api/types/container"
|
|
"github.com/moby/moby/client"
|
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
|
"github.com/yusing/godoxy/internal/agentpool"
|
|
"github.com/yusing/godoxy/internal/serialization"
|
|
"github.com/yusing/godoxy/internal/types"
|
|
gperr "github.com/yusing/goutils/errs"
|
|
)
|
|
|
|
var DummyContainer = new(types.Container)
|
|
|
|
var EnvDockerHost = os.Getenv("DOCKER_HOST")
|
|
|
|
var (
|
|
ErrNetworkNotFound = errors.New("network not found")
|
|
ErrNoNetwork = errors.New("no network found")
|
|
)
|
|
|
|
func FromDocker(c *container.Summary, dockerCfg types.DockerProviderConfig) (res *types.Container) {
|
|
actualLabels := maps.Clone(c.Labels)
|
|
|
|
_, isExplicit := c.Labels[LabelAliases]
|
|
helper := containerHelper{c}
|
|
if !isExplicit {
|
|
// walk through all labels to check if any label starts with NSProxy.
|
|
for lbl := range c.Labels {
|
|
if strings.HasPrefix(lbl, NSProxy+".") {
|
|
isExplicit = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
network := helper.getDeleteLabel(LabelNetwork)
|
|
|
|
isExcluded, _ := strconv.ParseBool(helper.getDeleteLabel(LabelExclude))
|
|
res = &types.Container{
|
|
DockerCfg: dockerCfg,
|
|
Image: helper.parseImage(),
|
|
ContainerName: helper.getName(),
|
|
ContainerID: c.ID,
|
|
|
|
Labels: c.Labels,
|
|
ActualLabels: actualLabels,
|
|
|
|
Mounts: helper.getMounts(),
|
|
|
|
Network: network,
|
|
PublicPortMapping: helper.getPublicPortMapping(),
|
|
PrivatePortMapping: helper.getPrivatePortMapping(),
|
|
|
|
Aliases: helper.getAliases(),
|
|
IsExcluded: isExcluded,
|
|
IsExplicit: isExplicit,
|
|
IsHostNetworkMode: c.HostConfig.NetworkMode == "host",
|
|
Running: c.Status == "running" || c.State == "running",
|
|
State: c.State,
|
|
}
|
|
|
|
if agent.IsDockerHostAgent(dockerCfg.URL) {
|
|
var ok bool
|
|
res.Agent, ok = agentpool.Get(dockerCfg.URL)
|
|
if !ok {
|
|
addError(res, fmt.Errorf("agent %q not found", dockerCfg.URL))
|
|
}
|
|
}
|
|
|
|
setPrivateHostname(res, helper)
|
|
setPublicHostname(res)
|
|
loadDeleteIdlewatcherLabels(res, helper)
|
|
|
|
if res.PrivateHostname == "" && res.PublicHostname == "" && res.Running {
|
|
addError(res, ErrNoNetwork)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func IsBlacklisted(c *types.Container) bool {
|
|
return IsBlacklistedImage(c.Image) || isDatabase(c)
|
|
}
|
|
|
|
func UpdatePorts(ctx context.Context, c *types.Container) error {
|
|
dockerClient, err := NewClient(c.DockerCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dockerClient.Close()
|
|
|
|
inspect, err := dockerClient.ContainerInspect(ctx, c.ContainerID, client.ContainerInspectOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for port := range inspect.Container.Config.ExposedPorts {
|
|
proto, portStr := nat.SplitProtoPort(port.String())
|
|
portInt, _ := nat.ParsePort(portStr)
|
|
if portInt == 0 {
|
|
continue
|
|
}
|
|
c.PublicPortMapping[portInt] = container.PortSummary{
|
|
PublicPort: uint16(portInt), //nolint:gosec
|
|
PrivatePort: uint16(portInt), //nolint:gosec
|
|
Type: proto,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DockerComposeProject(c *types.Container) string {
|
|
return c.Labels["com.docker.compose.project"]
|
|
}
|
|
|
|
func DockerComposeService(c *types.Container) string {
|
|
return c.Labels["com.docker.compose.service"]
|
|
}
|
|
|
|
func Dependencies(c *types.Container) []string {
|
|
deps := c.Labels[LabelDependsOn]
|
|
if deps == "" {
|
|
deps = c.Labels["com.docker.compose.depends_on"]
|
|
}
|
|
return strings.Split(deps, ",")
|
|
}
|
|
|
|
var databaseMPs = map[string]struct{}{
|
|
"/var/lib/postgresql/data": {},
|
|
"/var/lib/mysql": {},
|
|
"/var/lib/mongodb": {},
|
|
"/var/lib/mariadb": {},
|
|
"/var/lib/memcached": {},
|
|
"/var/lib/rabbitmq": {},
|
|
}
|
|
|
|
func isDatabase(c *types.Container) bool {
|
|
if c.Mounts != nil { // only happens in test
|
|
for _, m := range c.Mounts.Iter {
|
|
if _, ok := databaseMPs[m]; ok {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, v := range c.PrivatePortMapping {
|
|
switch v.PrivatePort {
|
|
// postgres, mysql or mariadb, redis, memcached, mongodb
|
|
case 5432, 3306, 6379, 11211, 27017:
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isLocal(c *types.Container) bool {
|
|
if strings.HasPrefix(c.DockerCfg.URL, "unix://") {
|
|
return true
|
|
}
|
|
// treat it as local if the docker host is the same as the environment variable
|
|
if c.DockerCfg.URL == EnvDockerHost {
|
|
return true
|
|
}
|
|
url, err := url.Parse(c.DockerCfg.URL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
hostname := url.Hostname()
|
|
if hostname == "localhost" {
|
|
return true
|
|
}
|
|
ip := net.ParseIP(hostname)
|
|
if ip != nil {
|
|
return ip.IsLoopback() || ip.IsUnspecified()
|
|
}
|
|
return false
|
|
}
|
|
|
|
func setPublicHostname(c *types.Container) {
|
|
if !c.Running {
|
|
return
|
|
}
|
|
if isLocal(c) {
|
|
c.PublicHostname = "127.0.0.1"
|
|
return
|
|
}
|
|
url, err := url.Parse(c.DockerCfg.URL)
|
|
if err != nil {
|
|
c.PublicHostname = "127.0.0.1"
|
|
return
|
|
}
|
|
c.PublicHostname = url.Hostname()
|
|
}
|
|
|
|
func setPrivateHostname(c *types.Container, helper containerHelper) {
|
|
if !isLocal(c) && c.Agent == nil {
|
|
return
|
|
}
|
|
if helper.NetworkSettings == nil {
|
|
return
|
|
}
|
|
if c.Network != "" {
|
|
v, ok := helper.NetworkSettings.Networks[c.Network]
|
|
if ok && v.IPAddress.IsValid() {
|
|
c.PrivateHostname = v.IPAddress.String()
|
|
return
|
|
}
|
|
// try {project_name}_{network_name}
|
|
if proj := DockerComposeProject(c); proj != "" {
|
|
oldNetwork, newNetwork := c.Network, fmt.Sprintf("%s_%s", proj, c.Network)
|
|
if newNetwork != oldNetwork {
|
|
v, ok = helper.NetworkSettings.Networks[newNetwork]
|
|
if ok && v.IPAddress.IsValid() {
|
|
c.Network = newNetwork // update network to the new one
|
|
c.PrivateHostname = v.IPAddress.String()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
nearest := gperr.DoYouMeanField(c.Network, helper.NetworkSettings.Networks)
|
|
addError(c, fmt.Errorf("network %q not found, %w", c.Network, nearest))
|
|
return
|
|
}
|
|
// fallback to first network if no network is specified
|
|
for k, v := range helper.NetworkSettings.Networks {
|
|
if v.IPAddress.IsValid() {
|
|
c.Network = k // update network to the first network
|
|
c.PrivateHostname = v.IPAddress.String()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func loadDeleteIdlewatcherLabels(c *types.Container, helper containerHelper) {
|
|
hasIdleTimeout := false
|
|
cfg := make(map[string]any, len(idlewatcherLabels))
|
|
for lbl, key := range idlewatcherLabels {
|
|
value := helper.getDeleteLabel(lbl)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
cfg[key] = value
|
|
switch lbl {
|
|
case LabelIdleTimeout:
|
|
hasIdleTimeout = true
|
|
case LabelDependsOn:
|
|
cfg[key] = Dependencies(c)
|
|
}
|
|
}
|
|
|
|
// set only if idlewatcher is enabled
|
|
if hasIdleTimeout {
|
|
idwCfg := new(types.IdlewatcherConfig)
|
|
idwCfg.Docker = &types.DockerConfig{
|
|
DockerCfg: c.DockerCfg,
|
|
ContainerID: c.ContainerID,
|
|
ContainerName: c.ContainerName,
|
|
}
|
|
|
|
err := serialization.MapUnmarshalValidate(cfg, idwCfg)
|
|
if err != nil {
|
|
addError(c, err)
|
|
} else {
|
|
c.IdlewatcherConfig = idwCfg
|
|
}
|
|
}
|
|
}
|
|
|
|
func addError(c *types.Container, err error) {
|
|
if c.Errors == nil {
|
|
c.Errors = new(types.ContainerError)
|
|
}
|
|
c.Errors.Add(err)
|
|
}
|