feat: docker over tls (#178)

This commit is contained in:
Yuzerion
2025-12-23 12:01:11 +08:00
committed by yusing
parent 9acb9fa50f
commit 8340d93ab7
25 changed files with 312 additions and 85 deletions

View File

@@ -28,21 +28,21 @@ func GetContainer(c *gin.Context) {
return return
} }
dockerHost, ok := docker.GetDockerHostByContainerID(id) dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
if !ok { if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found")) c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return return
} }
client, err := docker.NewClient(dockerHost) dockerClient, err := docker.NewClient(dockerCfg)
if err != nil { if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client")) c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return return
} }
defer client.Close() defer dockerClient.Close()
cont, err := client.ContainerInspect(c.Request.Context(), id) cont, err := dockerClient.ContainerInspect(c.Request.Context(), id)
if err != nil { if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to inspect container")) c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
return return
@@ -54,7 +54,7 @@ func GetContainer(c *gin.Context) {
} }
c.JSON(http.StatusOK, &Container{ c.JSON(http.StatusOK, &Container{
Server: dockerHost, Server: dockerCfg.URL,
Name: cont.Name, Name: cont.Name,
ID: cont.ID, ID: cont.ID,
Image: cont.Image, Image: cont.Image,

View File

@@ -57,13 +57,13 @@ func Logs(c *gin.Context) {
} }
// TODO: implement levels // TODO: implement levels
dockerHost, ok := docker.GetDockerHostByContainerID(id) dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
if !ok { if !ok {
c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id))) c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id)))
return return
} }
dockerClient, err := docker.NewClient(dockerHost) dockerClient, err := docker.NewClient(dockerCfg)
if err != nil { if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to get docker client")) c.Error(apitypes.InternalServerError(err, "failed to get docker client"))
return return
@@ -105,7 +105,7 @@ func Logs(c *gin.Context) {
return return
} }
log.Err(err). log.Err(err).
Str("server", dockerHost). Str("server", dockerCfg.URL).
Str("container", id). Str("container", id).
Msg("failed to de-multiplex logs") Msg("failed to de-multiplex logs")
} }

View File

@@ -28,13 +28,13 @@ func Restart(c *gin.Context) {
return return
} }
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID) dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
if !ok { if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found")) c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return return
} }
client, err := docker.NewClient(dockerHost) client, err := docker.NewClient(dockerCfg)
if err != nil { if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client")) c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return return

View File

@@ -34,13 +34,13 @@ func Start(c *gin.Context) {
return return
} }
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID) dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
if !ok { if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found")) c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return return
} }
client, err := docker.NewClient(dockerHost) client, err := docker.NewClient(dockerCfg)
if err != nil { if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client")) c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return return

View File

@@ -34,13 +34,13 @@ func Stop(c *gin.Context) {
return return
} }
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID) dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
if !ok { if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found")) c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return return
} }
client, err := docker.NewClient(dockerHost) client, err := docker.NewClient(dockerCfg)
if err != nil { if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client")) c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return return

View File

@@ -318,9 +318,9 @@ func (state *state) loadRouteProviders() error {
}) })
} }
for name, dockerHost := range providers.Docker { for name, dockerCfg := range providers.Docker {
providersProducer.Go(func() { providersProducer.Go(func() {
providersCh <- route.NewDockerProvider(name, dockerHost) providersCh <- route.NewDockerProvider(name, dockerCfg)
}) })
} }

View File

@@ -32,12 +32,12 @@ type (
HealthCheck types.HealthCheckConfig `json:"healthcheck"` HealthCheck types.HealthCheckConfig `json:"healthcheck"`
} }
Providers struct { Providers struct {
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"` Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"` Docker map[string]types.DockerProviderConfig `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys"`
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"` Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
Notification []*notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"` Notification []*notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"` Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"` MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
} }
) )
@@ -68,7 +68,7 @@ func init() {
return true return true
}) })
serialization.MustRegisterValidation("non_empty_docker_keys", func(fl validator.FieldLevel) bool { serialization.MustRegisterValidation("non_empty_docker_keys", func(fl validator.FieldLevel) bool {
m := fl.Field().Interface().(map[string]string) m := fl.Field().Interface().(map[string]types.DockerProviderConfig)
for k := range m { for k := range m {
if k == "" { if k == "" {
return false return false

View File

@@ -18,6 +18,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/types"
httputils "github.com/yusing/goutils/http" httputils "github.com/yusing/goutils/http"
"github.com/yusing/goutils/task" "github.com/yusing/goutils/task"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
@@ -28,6 +29,8 @@ type (
SharedClient struct { SharedClient struct {
*client.Client *client.Client
cfg types.DockerProviderConfig
refCount atomic.Int32 refCount atomic.Int32
closedOn atomic.Int64 closedOn atomic.Int64
@@ -120,7 +123,7 @@ func Clients() map[string]*SharedClient {
// Returns: // Returns:
// - Client: the Docker client connection. // - Client: the Docker client connection.
// - error: an error if the connection failed. // - error: an error if the connection failed.
func NewClient(host string, unique ...bool) (*SharedClient, error) { func NewClient(cfg types.DockerProviderConfig, unique ...bool) (*SharedClient, error) {
initClientCleanerOnce.Do(initClientCleaner) initClientCleanerOnce.Do(initClientCleaner)
u := false u := false
@@ -128,6 +131,8 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
u = unique[0] u = unique[0]
} }
host := cfg.URL
if !u { if !u {
clientMapMu.Lock() clientMapMu.Lock()
defer clientMapMu.Unlock() defer clientMapMu.Unlock()
@@ -185,6 +190,10 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
} }
} }
if cfg.TLS != nil {
opt = append(opt, client.WithTLSClientConfig(cfg.TLS.CAFile, cfg.TLS.CertFile, cfg.TLS.KeyFile))
}
client, err := client.NewClientWithOpts(opt...) client, err := client.NewClientWithOpts(opt...)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -192,6 +201,7 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
c := &SharedClient{ c := &SharedClient{
Client: client, Client: client,
cfg: cfg,
addr: addr, addr: addr,
key: host, key: host,
dial: dial, dial: dial,
@@ -228,7 +238,7 @@ func (c *SharedClient) InterceptHTTPClient(intercept httputils.InterceptFunc) {
func (c *SharedClient) CloneUnique() *SharedClient { func (c *SharedClient) CloneUnique() *SharedClient {
// there will be no error here // there will be no error here
// since we are using the same host from a valid client. // since we are using the same host from a valid client.
c, _ = NewClient(c.key, true) c, _ = NewClient(c.cfg, true)
return c return c
} }

View File

@@ -28,7 +28,7 @@ var (
ErrNoNetwork = errors.New("no network found") ErrNoNetwork = errors.New("no network found")
) )
func FromDocker(c *container.Summary, dockerHost string) (res *types.Container) { func FromDocker(c *container.Summary, dockerCfg types.DockerProviderConfig) (res *types.Container) {
actualLabels := maps.Clone(c.Labels) actualLabels := maps.Clone(c.Labels)
_, isExplicit := c.Labels[LabelAliases] _, isExplicit := c.Labels[LabelAliases]
@@ -46,7 +46,7 @@ func FromDocker(c *container.Summary, dockerHost string) (res *types.Container)
isExcluded, _ := strconv.ParseBool(helper.getDeleteLabel(LabelExclude)) isExcluded, _ := strconv.ParseBool(helper.getDeleteLabel(LabelExclude))
res = &types.Container{ res = &types.Container{
DockerHost: dockerHost, DockerCfg: dockerCfg,
Image: helper.parseImage(), Image: helper.parseImage(),
ContainerName: helper.getName(), ContainerName: helper.getName(),
ContainerID: c.ID, ContainerID: c.ID,
@@ -68,11 +68,11 @@ func FromDocker(c *container.Summary, dockerHost string) (res *types.Container)
State: c.State, State: c.State,
} }
if agent.IsDockerHostAgent(dockerHost) { if agent.IsDockerHostAgent(dockerCfg.URL) {
var ok bool var ok bool
res.Agent, ok = agent.GetAgent(dockerHost) res.Agent, ok = agent.GetAgent(dockerCfg.URL)
if !ok { if !ok {
addError(res, fmt.Errorf("agent %q not found", dockerHost)) addError(res, fmt.Errorf("agent %q not found", dockerCfg.URL))
} }
} }
@@ -91,13 +91,13 @@ func IsBlacklisted(c *types.Container) bool {
} }
func UpdatePorts(c *types.Container) error { func UpdatePorts(c *types.Container) error {
client, err := NewClient(c.DockerHost) dockerClient, err := NewClient(c.DockerCfg)
if err != nil { if err != nil {
return err return err
} }
defer client.Close() defer dockerClient.Close()
inspect, err := client.ContainerInspect(context.Background(), c.ContainerID) inspect, err := dockerClient.ContainerInspect(context.Background(), c.ContainerID)
if err != nil { if err != nil {
return err return err
} }
@@ -162,14 +162,14 @@ func isDatabase(c *types.Container) bool {
} }
func isLocal(c *types.Container) bool { func isLocal(c *types.Container) bool {
if strings.HasPrefix(c.DockerHost, "unix://") { if strings.HasPrefix(c.DockerCfg.URL, "unix://") {
return true return true
} }
// treat it as local if the docker host is the same as the environment variable // treat it as local if the docker host is the same as the environment variable
if c.DockerHost == EnvDockerHost { if c.DockerCfg.URL == EnvDockerHost {
return true return true
} }
url, err := url.Parse(c.DockerHost) url, err := url.Parse(c.DockerCfg.URL)
if err != nil { if err != nil {
return false return false
} }
@@ -189,7 +189,7 @@ func setPublicHostname(c *types.Container) {
c.PublicHostname = "127.0.0.1" c.PublicHostname = "127.0.0.1"
return return
} }
url, err := url.Parse(c.DockerHost) url, err := url.Parse(c.DockerCfg.URL)
if err != nil { if err != nil {
c.PublicHostname = "127.0.0.1" c.PublicHostname = "127.0.0.1"
return return
@@ -257,7 +257,7 @@ func loadDeleteIdlewatcherLabels(c *types.Container, helper containerHelper) {
if hasIdleTimeout { if hasIdleTimeout {
idwCfg := new(types.IdlewatcherConfig) idwCfg := new(types.IdlewatcherConfig)
idwCfg.Docker = &types.DockerConfig{ idwCfg.Docker = &types.DockerConfig{
DockerHost: c.DockerHost, DockerCfg: c.DockerCfg,
ContainerID: c.ContainerID, ContainerID: c.ContainerID,
ContainerName: c.ContainerName, ContainerName: c.ContainerName,
} }

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/yusing/godoxy/internal/types"
expect "github.com/yusing/goutils/testing" expect "github.com/yusing/goutils/testing"
) )
@@ -36,7 +37,7 @@ func TestContainerExplicit(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := FromDocker(&container.Summary{Names: []string{"test"}, State: "test", Labels: tt.labels}, "") c := FromDocker(&container.Summary{Names: []string{"test"}, State: "test", Labels: tt.labels}, types.DockerProviderConfig{})
expect.Equal(t, c.IsExplicit, tt.isExplicit) expect.Equal(t, c.IsExplicit, tt.isExplicit)
}) })
} }
@@ -73,7 +74,7 @@ func TestContainerHostNetworkMode(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := FromDocker(tt.container, "") c := FromDocker(tt.container, types.DockerProviderConfig{})
expect.Equal(t, c.IsHostNetworkMode, tt.isHostNetworkMode) expect.Equal(t, c.IsHostNetworkMode, tt.isHostNetworkMode)
}) })
} }

View File

@@ -2,18 +2,19 @@ package docker
import ( import (
"github.com/puzpuzpuz/xsync/v4" "github.com/puzpuzpuz/xsync/v4"
"github.com/yusing/godoxy/internal/types"
) )
var idDockerHostMap = xsync.NewMap[string, string](xsync.WithPresize(100)) var idDockerCfgMap = xsync.NewMap[string, types.DockerProviderConfig](xsync.WithPresize(100))
func GetDockerHostByContainerID(id string) (string, bool) { func GetDockerCfgByContainerID(id string) (types.DockerProviderConfig, bool) {
return idDockerHostMap.Load(id) return idDockerCfgMap.Load(id)
} }
func SetDockerHostByContainerID(id, host string) { func SetDockerCfgByContainerID(id string, cfg types.DockerProviderConfig) {
idDockerHostMap.Store(id, host) idDockerCfgMap.Store(id, cfg)
} }
func DeleteDockerHostByContainerID(id string) { func DeleteDockerCfgByContainerID(id string) {
idDockerHostMap.Delete(id) idDockerCfgMap.Delete(id)
} }

View File

@@ -5,6 +5,7 @@ import (
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/yusing/godoxy/internal/types"
) )
var listOptions = container.ListOptions{ var listOptions = container.ListOptions{
@@ -19,8 +20,8 @@ var listOptions = container.ListOptions{
All: true, All: true,
} }
func ListContainers(ctx context.Context, clientHost string) ([]container.Summary, error) { func ListContainers(ctx context.Context, dockerCfg types.DockerProviderConfig) ([]container.Summary, error) {
dockerClient, err := NewClient(clientHost) dockerClient, err := NewClient(dockerCfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -19,14 +19,14 @@ type DockerProvider struct {
var startOptions = container.StartOptions{} var startOptions = container.StartOptions{}
func NewDockerProvider(dockerHost, containerID string) (idlewatcher.Provider, error) { func NewDockerProvider(dockerCfg types.DockerProviderConfig, containerID string) (idlewatcher.Provider, error) {
client, err := docker.NewClient(dockerHost) client, err := docker.NewClient(dockerCfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &DockerProvider{ return &DockerProvider{
client: client, client: client,
watcher: watcher.NewDockerWatcher(dockerHost), watcher: watcher.NewDockerWatcher(dockerCfg),
containerID: containerID, containerID: containerID,
}, nil }, nil
} }

View File

@@ -209,7 +209,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
depCont := depRoute.ContainerInfo() depCont := depRoute.ContainerInfo()
if depCont != nil { if depCont != nil {
depCfg.Docker = &types.DockerConfig{ depCfg.Docker = &types.DockerConfig{
DockerHost: depCont.DockerHost, DockerCfg: depCont.DockerCfg,
ContainerID: depCont.ContainerID, ContainerID: depCont.ContainerID,
ContainerName: depCont.ContainerName, ContainerName: depCont.ContainerName,
} }
@@ -256,7 +256,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
var kind string var kind string
switch { switch {
case cfg.Docker != nil: case cfg.Docker != nil:
p, err = provider.NewDockerProvider(cfg.Docker.DockerHost, cfg.Docker.ContainerID) p, err = provider.NewDockerProvider(cfg.Docker.DockerCfg, cfg.Docker.ContainerID)
kind = "docker" kind = "docker"
default: default:
p, err = provider.NewProxmoxProvider(cfg.Proxmox.Node, cfg.Proxmox.VMID) p, err = provider.NewProxmoxProvider(cfg.Proxmox.Node, cfg.Proxmox.VMID)

View File

@@ -18,8 +18,9 @@ import (
) )
type DockerProvider struct { type DockerProvider struct {
name, dockerHost string name string
l zerolog.Logger dockerCfg types.DockerProviderConfig
l zerolog.Logger
} }
const ( const (
@@ -29,10 +30,10 @@ const (
var ErrAliasRefIndexOutOfRange = gperr.New("index out of range") var ErrAliasRefIndexOutOfRange = gperr.New("index out of range")
func DockerProviderImpl(name, dockerHost string) ProviderImpl { func DockerProviderImpl(name string, dockerCfg types.DockerProviderConfig) ProviderImpl {
return &DockerProvider{ return &DockerProvider{
name, name,
dockerHost, dockerCfg,
log.With().Str("type", "docker").Str("name", name).Logger(), log.With().Str("type", "docker").Str("name", name).Logger(),
} }
} }
@@ -54,14 +55,14 @@ func (p *DockerProvider) Logger() *zerolog.Logger {
} }
func (p *DockerProvider) NewWatcher() watcher.Watcher { func (p *DockerProvider) NewWatcher() watcher.Watcher {
return watcher.NewDockerWatcher(p.dockerHost) return watcher.NewDockerWatcher(p.dockerCfg)
} }
func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) { func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
containers, err := docker.ListContainers(ctx, p.dockerHost) containers, err := docker.ListContainers(ctx, p.dockerCfg)
if err != nil { if err != nil {
return nil, gperr.Wrap(err) return nil, gperr.Wrap(err)
} }
@@ -70,7 +71,7 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
routes := make(route.Routes) routes := make(route.Routes)
for _, c := range containers { for _, c := range containers {
container := docker.FromDocker(&c, p.dockerHost) container := docker.FromDocker(&c, p.dockerCfg)
if container.Errors != nil { if container.Errors != nil {
errs.Add(container.Errors) errs.Add(container.Errors)

View File

@@ -6,6 +6,7 @@ import (
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
"github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/types"
expect "github.com/yusing/goutils/testing" expect "github.com/yusing/goutils/testing"
_ "embed" _ "embed"
@@ -28,7 +29,7 @@ func TestParseDockerLabels(t *testing.T) {
Ports: []container.Port{ Ports: []container.Port{
{Type: "tcp", PrivatePort: 1234, PublicPort: 1234}, {Type: "tcp", PrivatePort: 1234, PublicPort: 1234},
}, },
}, "/var/run/docker.sock"), }, types.DockerProviderConfig{URL: "unix:///var/run/docker.sock"}),
) )
expect.NoError(t, err) expect.NoError(t, err)
expect.True(t, routes.Contains("app")) expect.True(t, routes.Contains("app"))

View File

@@ -10,6 +10,7 @@ import (
D "github.com/yusing/godoxy/internal/docker" D "github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/route" "github.com/yusing/godoxy/internal/route"
routeTypes "github.com/yusing/godoxy/internal/route/types" routeTypes "github.com/yusing/godoxy/internal/route/types"
"github.com/yusing/godoxy/internal/types"
expect "github.com/yusing/goutils/testing" expect "github.com/yusing/goutils/testing"
) )
@@ -30,7 +31,7 @@ func makeRoutes(cont *container.Summary, dockerHostIP ...string) route.Routes {
} }
cont.ID = "test" cont.ID = "test"
p.name = "test" p.name = "test"
routes := expect.Must(p.routesFromContainerLabels(D.FromDocker(cont, host))) routes := expect.Must(p.routesFromContainerLabels(D.FromDocker(cont, types.DockerProviderConfig{URL: host})))
for _, r := range routes { for _, r := range routes {
r.Finalize() r.Finalize()
} }
@@ -38,7 +39,7 @@ func makeRoutes(cont *container.Summary, dockerHostIP ...string) route.Routes {
} }
func TestExplicitOnly(t *testing.T) { func TestExplicitOnly(t *testing.T) {
p := NewDockerProvider("a!", "") p := NewDockerProvider("a!", types.DockerProviderConfig{})
expect.True(t, p.IsExplicitOnly()) expect.True(t, p.IsExplicitOnly())
} }
@@ -198,7 +199,7 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
"proxy.*.port": "4444", "proxy.*.port": "4444",
"proxy.#4.scheme": "https", "proxy.#4.scheme": "https",
}, },
}, "") }, types.DockerProviderConfig{})
var p DockerProvider var p DockerProvider
_, err := p.routesFromContainerLabels(c) _, err := p.routesFromContainerLabels(c)
expect.ErrorIs(t, ErrAliasRefIndexOutOfRange, err) expect.ErrorIs(t, ErrAliasRefIndexOutOfRange, err)
@@ -210,7 +211,7 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
D.LabelAliases: "a,b", D.LabelAliases: "a,b",
"proxy.#0.host": "localhost", "proxy.#0.host": "localhost",
}, },
}, "") }, types.DockerProviderConfig{})
_, err = p.routesFromContainerLabels(c) _, err = p.routesFromContainerLabels(c)
expect.ErrorIs(t, ErrAliasRefIndexOutOfRange, err) expect.ErrorIs(t, ErrAliasRefIndexOutOfRange, err)
} }

View File

@@ -69,13 +69,13 @@ func NewFileProvider(filename string) (p *Provider, err error) {
return p, err return p, err
} }
func NewDockerProvider(name string, dockerHost string) *Provider { func NewDockerProvider(name string, dockerCfg types.DockerProviderConfig) *Provider {
if dockerHost == common.DockerHostFromEnv { if dockerCfg.URL == common.DockerHostFromEnv {
dockerHost = env.GetEnvString("DOCKER_HOST", client.DefaultDockerHost) dockerCfg.URL = env.GetEnvString("DOCKER_HOST", client.DefaultDockerHost)
} }
p := newProvider(provider.ProviderTypeDocker) p := newProvider(provider.ProviderTypeDocker)
p.ProviderImpl = DockerProviderImpl(name, dockerHost) p.ProviderImpl = DockerProviderImpl(name, dockerCfg)
p.watcher = p.NewWatcher() p.watcher = p.NewWatcher()
return p return p
} }
@@ -84,7 +84,9 @@ func NewAgentProvider(cfg *agent.AgentConfig) *Provider {
p := newProvider(provider.ProviderTypeAgent) p := newProvider(provider.ProviderTypeAgent)
agent := &AgentProvider{ agent := &AgentProvider{
AgentConfig: cfg, AgentConfig: cfg,
docker: DockerProviderImpl(cfg.Name, cfg.FakeDockerHost()), docker: DockerProviderImpl(cfg.Name, types.DockerProviderConfig{
URL: cfg.FakeDockerHost(),
}),
} }
p.ProviderImpl = agent p.ProviderImpl = agent
p.watcher = p.NewWatcher() p.watcher = p.NewWatcher()

View File

@@ -391,7 +391,7 @@ func (r *Route) start(parent task.Parent) gperr.Error {
} }
if cont := r.ContainerInfo(); cont != nil { if cont := r.ContainerInfo(); cont != nil {
docker.SetDockerHostByContainerID(cont.ContainerID, cont.DockerHost) docker.SetDockerCfgByContainerID(cont.ContainerID, cont.DockerCfg)
} }
if !excluded { if !excluded {
@@ -406,7 +406,7 @@ func (r *Route) start(parent task.Parent) gperr.Error {
func (r *Route) Finish(reason any) { func (r *Route) Finish(reason any) {
if cont := r.ContainerInfo(); cont != nil { if cont := r.ContainerInfo(); cont != nil {
docker.DeleteDockerHostByContainerID(cont.ContainerID) docker.DeleteDockerCfgByContainerID(cont.ContainerID)
} }
r.FinishAndWait(reason) r.FinishAndWait(reason)
} }

View File

@@ -13,10 +13,10 @@ type (
PortMapping = map[int]container.Port PortMapping = map[int]container.Port
Container struct { Container struct {
DockerHost string `json:"docker_host"` DockerCfg DockerProviderConfig `json:"docker_cfg"`
Image *ContainerImage `json:"image"` Image *ContainerImage `json:"image"`
ContainerName string `json:"container_name"` ContainerName string `json:"container_name"`
ContainerID string `json:"container_id"` ContainerID string `json:"container_id"`
State container.ContainerState `json:"state"` State container.ContainerState `json:"state"`

View File

@@ -0,0 +1,82 @@
package types
import (
"encoding/json"
"fmt"
"net"
"net/url"
"os"
"strconv"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/serialization"
gperr "github.com/yusing/goutils/errs"
)
type DockerProviderConfig struct {
URL string `json:"url,omitempty"`
TLS *DockerTLSConfig `json:"tls,omitempty"`
} // @name DockerProviderConfig
type DockerProviderConfigDetailed struct {
Scheme string `json:"scheme,omitempty" validate:"required,oneof=http https tls"`
Host string `json:"host,omitempty" validate:"required,hostname|ip"`
Port int `json:"port,omitempty" validate:"required,min=1,max=65535"`
TLS *DockerTLSConfig `json:"tls" validate:"omitempty"`
}
type DockerTLSConfig struct {
CAFile string `json:"ca_file,omitempty" validate:"required"`
CertFile string `json:"cert_file,omitempty" validate:"required"`
KeyFile string `json:"key_file,omitempty" validate:"required"`
} // @name DockerTLSConfig
func (cfg *DockerProviderConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(cfg.URL)
}
func (cfg *DockerProviderConfig) Parse(value string) error {
u, err := url.Parse(value)
if err != nil {
return err
}
switch u.Scheme {
case "http", "https", "tls":
default:
return fmt.Errorf("invalid scheme: %s", u.Scheme)
}
cfg.URL = u.String()
return nil
}
func (cfg *DockerProviderConfig) UnmarshalMap(m map[string]any) gperr.Error {
var tmp DockerProviderConfigDetailed
var err = serialization.MapUnmarshalValidate(m, &tmp)
if err != nil {
return err
}
cfg.URL = fmt.Sprintf("%s://%s", tmp.Scheme, net.JoinHostPort(tmp.Host, strconv.Itoa(tmp.Port)))
cfg.TLS = tmp.TLS
if cfg.TLS != nil {
if err := checkFilesOk(cfg.TLS.CAFile, cfg.TLS.CertFile, cfg.TLS.KeyFile); err != nil {
return gperr.Wrap(err)
}
}
return nil
}
func checkFilesOk(files ...string) error {
if common.IsTest {
return nil
}
var errs gperr.Builder
for _, file := range files {
if _, err := os.Stat(file); err != nil {
errs.Add(err)
}
}
return errs.Error()
}

View File

@@ -0,0 +1,122 @@
package types
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yusing/godoxy/internal/serialization"
)
func TestDockerProviderConfigUnmarshalMap(t *testing.T) {
t.Run("string", func(t *testing.T) {
var cfg map[string]*DockerProviderConfig
err := serialization.UnmarshalValidateYAML([]byte("test: http://localhost:2375"), &cfg)
assert.NoError(t, err)
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375"}, cfg["test"])
})
t.Run("detailed", func(t *testing.T) {
var cfg map[string]*DockerProviderConfig
err := serialization.UnmarshalValidateYAML([]byte(`
test:
scheme: http
host: localhost
port: 2375
tls:
ca_file: /etc/ssl/ca.crt
cert_file: /etc/ssl/cert.crt
key_file: /etc/ssl/key.crt`), &cfg)
assert.Error(t, err, os.ErrNotExist)
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375", TLS: &DockerTLSConfig{CAFile: "/etc/ssl/ca.crt", CertFile: "/etc/ssl/cert.crt", KeyFile: "/etc/ssl/key.crt"}}, cfg["test"])
})
}
func TestDockerProviderConfigValidation(t *testing.T) {
tests := []struct {
name string
yamlStr string
wantErr bool
}{
{name: "valid url", yamlStr: "test: http://localhost:2375", wantErr: false},
{name: "invalid url", yamlStr: "test: ftp://localhost/2375", wantErr: true},
{name: "valid scheme", yamlStr: `
test:
scheme: http
host: localhost
port: 2375
`, wantErr: false},
{name: "invalid scheme", yamlStr: `
test:
scheme: invalid
host: localhost
port: 2375
`, wantErr: true},
{name: "valid host (ipv4)", yamlStr: `
test:
scheme: http
host: 127.0.0.1
port: 2375
`, wantErr: false},
{name: "valid host (ipv6)", yamlStr: `
test:
scheme: http
host: ::1
port: 2375
`, wantErr: false},
{name: "valid host (hostname)", yamlStr: `
test:
scheme: http
host: example.com
port: 2375
`, wantErr: false},
{name: "invalid host", yamlStr: `
test:
scheme: http
host: invalid:1234
port: 2375
`, wantErr: true},
{name: "valid port", yamlStr: `
test:
scheme: http
host: localhost
port: 2375
`, wantErr: false},
{name: "invalid port", yamlStr: `
test:
scheme: http
host: localhost
port: 65536
`, wantErr: true},
{name: "valid tls", yamlStr: `
test:
scheme: tls
host: localhost
port: 2375
tls:
ca_file: /etc/ssl/ca.crt
cert_file: /etc/ssl/cert.crt
key_file: /etc/ssl/key.crt
`, wantErr: false},
{name: "invalid tls (missing cert file and key file)", yamlStr: `
test:
scheme: tls
host: localhost
port: 2375
tls:
ca_file: /etc/ssl/ca.crt
`, wantErr: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var cfg map[string]*DockerProviderConfig
err := serialization.UnmarshalValidateYAML([]byte(test.yamlStr), &cfg)
if test.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -38,9 +38,9 @@ type (
ContainerSignal string // @name ContainerSignal ContainerSignal string // @name ContainerSignal
DockerConfig struct { DockerConfig struct {
DockerHost string `json:"docker_host" validate:"required"` DockerCfg DockerProviderConfig `json:"docker_cfg" validate:"required"`
ContainerID string `json:"container_id" validate:"required"` ContainerID string `json:"container_id" validate:"required"`
ContainerName string `json:"container_name" validate:"required"` ContainerName string `json:"container_name" validate:"required"`
} // @name DockerConfig } // @name DockerConfig
ProxmoxConfig struct { ProxmoxConfig struct {
Node string `json:"node" validate:"required"` Node string `json:"node" validate:"required"`

View File

@@ -10,12 +10,15 @@ import (
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/watcher/events" "github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
) )
type ( type (
DockerWatcher string DockerWatcher struct {
cfg types.DockerProviderConfig
}
DockerListOptions = dockerEvents.ListOptions DockerListOptions = dockerEvents.ListOptions
) )
@@ -55,8 +58,10 @@ func DockerFilterContainerNameID(nameOrID string) filters.KeyValuePair {
return filters.Arg("container", nameOrID) return filters.Arg("container", nameOrID)
} }
func NewDockerWatcher(host string) DockerWatcher { func NewDockerWatcher(dockerCfg types.DockerProviderConfig) DockerWatcher {
return DockerWatcher(host) return DockerWatcher{
cfg: dockerCfg,
}
} }
func (w DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan gperr.Error) { func (w DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan gperr.Error) {
@@ -68,7 +73,7 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
errCh := make(chan gperr.Error) errCh := make(chan gperr.Error)
go func() { go func() {
client, err := docker.NewClient(string(w)) client, err := docker.NewClient(w.cfg)
if err != nil { if err != nil {
errCh <- gperr.Wrap(err, "docker watcher: failed to initialize client") errCh <- gperr.Wrap(err, "docker watcher: failed to initialize client")
return return

View File

@@ -61,7 +61,7 @@ func NewMonitor(r types.Route) types.HealthMonCheck {
} }
if r.IsDocker() { if r.IsDocker() {
cont := r.ContainerInfo() cont := r.ContainerInfo()
client, err := docker.NewClient(cont.DockerHost) client, err := docker.NewClient(cont.DockerCfg)
if err != nil { if err != nil {
return mon return mon
} }