diff --git a/internal/api/v1/docker/container.go b/internal/api/v1/docker/container.go index 108a9815..f320500d 100644 --- a/internal/api/v1/docker/container.go +++ b/internal/api/v1/docker/container.go @@ -28,21 +28,21 @@ func GetContainer(c *gin.Context) { return } - dockerHost, ok := docker.GetDockerHostByContainerID(id) + dockerCfg, ok := docker.GetDockerCfgByContainerID(id) if !ok { c.JSON(http.StatusNotFound, apitypes.Error("container not found")) return } - client, err := docker.NewClient(dockerHost) + dockerClient, err := docker.NewClient(dockerCfg) if err != nil { c.Error(apitypes.InternalServerError(err, "failed to create docker client")) 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 { c.Error(apitypes.InternalServerError(err, "failed to inspect container")) return @@ -54,7 +54,7 @@ func GetContainer(c *gin.Context) { } c.JSON(http.StatusOK, &Container{ - Server: dockerHost, + Server: dockerCfg.URL, Name: cont.Name, ID: cont.ID, Image: cont.Image, diff --git a/internal/api/v1/docker/logs.go b/internal/api/v1/docker/logs.go index 917ee1f2..bfaf6ddc 100644 --- a/internal/api/v1/docker/logs.go +++ b/internal/api/v1/docker/logs.go @@ -57,13 +57,13 @@ func Logs(c *gin.Context) { } // TODO: implement levels - dockerHost, ok := docker.GetDockerHostByContainerID(id) + dockerCfg, ok := docker.GetDockerCfgByContainerID(id) if !ok { c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id))) return } - dockerClient, err := docker.NewClient(dockerHost) + dockerClient, err := docker.NewClient(dockerCfg) if err != nil { c.Error(apitypes.InternalServerError(err, "failed to get docker client")) return @@ -105,7 +105,7 @@ func Logs(c *gin.Context) { return } log.Err(err). - Str("server", dockerHost). + Str("server", dockerCfg.URL). Str("container", id). Msg("failed to de-multiplex logs") } diff --git a/internal/api/v1/docker/restart.go b/internal/api/v1/docker/restart.go index 5a72eb30..6df9ad47 100644 --- a/internal/api/v1/docker/restart.go +++ b/internal/api/v1/docker/restart.go @@ -28,13 +28,13 @@ func Restart(c *gin.Context) { return } - dockerHost, ok := docker.GetDockerHostByContainerID(req.ID) + dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID) if !ok { c.JSON(http.StatusNotFound, apitypes.Error("container not found")) return } - client, err := docker.NewClient(dockerHost) + client, err := docker.NewClient(dockerCfg) if err != nil { c.Error(apitypes.InternalServerError(err, "failed to create docker client")) return diff --git a/internal/api/v1/docker/start.go b/internal/api/v1/docker/start.go index ad3eb176..ff7501cc 100644 --- a/internal/api/v1/docker/start.go +++ b/internal/api/v1/docker/start.go @@ -34,13 +34,13 @@ func Start(c *gin.Context) { return } - dockerHost, ok := docker.GetDockerHostByContainerID(req.ID) + dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID) if !ok { c.JSON(http.StatusNotFound, apitypes.Error("container not found")) return } - client, err := docker.NewClient(dockerHost) + client, err := docker.NewClient(dockerCfg) if err != nil { c.Error(apitypes.InternalServerError(err, "failed to create docker client")) return diff --git a/internal/api/v1/docker/stop.go b/internal/api/v1/docker/stop.go index 5db90737..0263f3ea 100644 --- a/internal/api/v1/docker/stop.go +++ b/internal/api/v1/docker/stop.go @@ -34,13 +34,13 @@ func Stop(c *gin.Context) { return } - dockerHost, ok := docker.GetDockerHostByContainerID(req.ID) + dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID) if !ok { c.JSON(http.StatusNotFound, apitypes.Error("container not found")) return } - client, err := docker.NewClient(dockerHost) + client, err := docker.NewClient(dockerCfg) if err != nil { c.Error(apitypes.InternalServerError(err, "failed to create docker client")) return diff --git a/internal/config/state.go b/internal/config/state.go index 8af6e1dc..52fa2c71 100644 --- a/internal/config/state.go +++ b/internal/config/state.go @@ -318,9 +318,9 @@ func (state *state) loadRouteProviders() error { }) } - for name, dockerHost := range providers.Docker { + for name, dockerCfg := range providers.Docker { providersProducer.Go(func() { - providersCh <- route.NewDockerProvider(name, dockerHost) + providersCh <- route.NewDockerProvider(name, dockerCfg) }) } diff --git a/internal/config/types/config.go b/internal/config/types/config.go index c8a578f1..25bc262c 100644 --- a/internal/config/types/config.go +++ b/internal/config/types/config.go @@ -32,12 +32,12 @@ type ( HealthCheck types.HealthCheckConfig `json:"healthcheck"` } Providers struct { - 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"` - Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"` - Notification []*notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"` - Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"` - MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"` + Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"` + Docker map[string]types.DockerProviderConfig `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys"` + Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"` + Notification []*notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"` + Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"` + MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"` } ) @@ -68,7 +68,7 @@ func init() { return true }) 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 { if k == "" { return false diff --git a/internal/docker/client.go b/internal/docker/client.go index c4f4de8f..9f0da07a 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -18,6 +18,7 @@ import ( "github.com/rs/zerolog/log" "github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/godoxy/internal/common" + "github.com/yusing/godoxy/internal/types" httputils "github.com/yusing/goutils/http" "github.com/yusing/goutils/task" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" @@ -28,6 +29,8 @@ type ( SharedClient struct { *client.Client + cfg types.DockerProviderConfig + refCount atomic.Int32 closedOn atomic.Int64 @@ -120,7 +123,7 @@ func Clients() map[string]*SharedClient { // Returns: // - Client: the Docker client connection. // - 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) u := false @@ -128,6 +131,8 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) { u = unique[0] } + host := cfg.URL + if !u { clientMapMu.Lock() 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...) if err != nil { return nil, err @@ -192,6 +201,7 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) { c := &SharedClient{ Client: client, + cfg: cfg, addr: addr, key: host, dial: dial, @@ -228,7 +238,7 @@ func (c *SharedClient) InterceptHTTPClient(intercept httputils.InterceptFunc) { func (c *SharedClient) CloneUnique() *SharedClient { // there will be no error here // since we are using the same host from a valid client. - c, _ = NewClient(c.key, true) + c, _ = NewClient(c.cfg, true) return c } diff --git a/internal/docker/container.go b/internal/docker/container.go index e838d4f4..0edea0d1 100644 --- a/internal/docker/container.go +++ b/internal/docker/container.go @@ -28,7 +28,7 @@ var ( 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) _, isExplicit := c.Labels[LabelAliases] @@ -46,7 +46,7 @@ func FromDocker(c *container.Summary, dockerHost string) (res *types.Container) isExcluded, _ := strconv.ParseBool(helper.getDeleteLabel(LabelExclude)) res = &types.Container{ - DockerHost: dockerHost, + DockerCfg: dockerCfg, Image: helper.parseImage(), ContainerName: helper.getName(), ContainerID: c.ID, @@ -68,11 +68,11 @@ func FromDocker(c *container.Summary, dockerHost string) (res *types.Container) State: c.State, } - if agent.IsDockerHostAgent(dockerHost) { + if agent.IsDockerHostAgent(dockerCfg.URL) { var ok bool - res.Agent, ok = agent.GetAgent(dockerHost) + res.Agent, ok = agent.GetAgent(dockerCfg.URL) 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 { - client, err := NewClient(c.DockerHost) + dockerClient, err := NewClient(c.DockerCfg) if err != nil { 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 { return err } @@ -162,14 +162,14 @@ func isDatabase(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 } // 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 } - url, err := url.Parse(c.DockerHost) + url, err := url.Parse(c.DockerCfg.URL) if err != nil { return false } @@ -189,7 +189,7 @@ func setPublicHostname(c *types.Container) { c.PublicHostname = "127.0.0.1" return } - url, err := url.Parse(c.DockerHost) + url, err := url.Parse(c.DockerCfg.URL) if err != nil { c.PublicHostname = "127.0.0.1" return @@ -257,7 +257,7 @@ func loadDeleteIdlewatcherLabels(c *types.Container, helper containerHelper) { if hasIdleTimeout { idwCfg := new(types.IdlewatcherConfig) idwCfg.Docker = &types.DockerConfig{ - DockerHost: c.DockerHost, + DockerCfg: c.DockerCfg, ContainerID: c.ContainerID, ContainerName: c.ContainerName, } diff --git a/internal/docker/container_test.go b/internal/docker/container_test.go index 3dddd829..def1357b 100644 --- a/internal/docker/container_test.go +++ b/internal/docker/container_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/docker/docker/api/types/container" + "github.com/yusing/godoxy/internal/types" expect "github.com/yusing/goutils/testing" ) @@ -36,7 +37,7 @@ func TestContainerExplicit(t *testing.T) { for _, tt := range tests { 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) }) } @@ -73,7 +74,7 @@ func TestContainerHostNetworkMode(t *testing.T) { } for _, tt := range tests { 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) }) } diff --git a/internal/docker/id_lookup.go b/internal/docker/id_lookup.go index f0f4c78a..3f442303 100644 --- a/internal/docker/id_lookup.go +++ b/internal/docker/id_lookup.go @@ -2,18 +2,19 @@ package docker import ( "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) { - return idDockerHostMap.Load(id) +func GetDockerCfgByContainerID(id string) (types.DockerProviderConfig, bool) { + return idDockerCfgMap.Load(id) } -func SetDockerHostByContainerID(id, host string) { - idDockerHostMap.Store(id, host) +func SetDockerCfgByContainerID(id string, cfg types.DockerProviderConfig) { + idDockerCfgMap.Store(id, cfg) } -func DeleteDockerHostByContainerID(id string) { - idDockerHostMap.Delete(id) +func DeleteDockerCfgByContainerID(id string) { + idDockerCfgMap.Delete(id) } diff --git a/internal/docker/list_containers.go b/internal/docker/list_containers.go index e1a98e52..25bb721d 100644 --- a/internal/docker/list_containers.go +++ b/internal/docker/list_containers.go @@ -5,6 +5,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" + "github.com/yusing/godoxy/internal/types" ) var listOptions = container.ListOptions{ @@ -19,8 +20,8 @@ var listOptions = container.ListOptions{ All: true, } -func ListContainers(ctx context.Context, clientHost string) ([]container.Summary, error) { - dockerClient, err := NewClient(clientHost) +func ListContainers(ctx context.Context, dockerCfg types.DockerProviderConfig) ([]container.Summary, error) { + dockerClient, err := NewClient(dockerCfg) if err != nil { return nil, err } diff --git a/internal/idlewatcher/provider/docker.go b/internal/idlewatcher/provider/docker.go index 9f9bb2ce..0a0510af 100644 --- a/internal/idlewatcher/provider/docker.go +++ b/internal/idlewatcher/provider/docker.go @@ -19,14 +19,14 @@ type DockerProvider struct { var startOptions = container.StartOptions{} -func NewDockerProvider(dockerHost, containerID string) (idlewatcher.Provider, error) { - client, err := docker.NewClient(dockerHost) +func NewDockerProvider(dockerCfg types.DockerProviderConfig, containerID string) (idlewatcher.Provider, error) { + client, err := docker.NewClient(dockerCfg) if err != nil { return nil, err } return &DockerProvider{ client: client, - watcher: watcher.NewDockerWatcher(dockerHost), + watcher: watcher.NewDockerWatcher(dockerCfg), containerID: containerID, }, nil } diff --git a/internal/idlewatcher/watcher.go b/internal/idlewatcher/watcher.go index 0be101b1..e9d7ed41 100644 --- a/internal/idlewatcher/watcher.go +++ b/internal/idlewatcher/watcher.go @@ -209,7 +209,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig) depCont := depRoute.ContainerInfo() if depCont != nil { depCfg.Docker = &types.DockerConfig{ - DockerHost: depCont.DockerHost, + DockerCfg: depCont.DockerCfg, ContainerID: depCont.ContainerID, ContainerName: depCont.ContainerName, } @@ -256,7 +256,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig) var kind string switch { 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" default: p, err = provider.NewProxmoxProvider(cfg.Proxmox.Node, cfg.Proxmox.VMID) diff --git a/internal/route/provider/docker.go b/internal/route/provider/docker.go index 54f55b1f..f9451ef8 100755 --- a/internal/route/provider/docker.go +++ b/internal/route/provider/docker.go @@ -18,8 +18,9 @@ import ( ) type DockerProvider struct { - name, dockerHost string - l zerolog.Logger + name string + dockerCfg types.DockerProviderConfig + l zerolog.Logger } const ( @@ -29,10 +30,10 @@ const ( var ErrAliasRefIndexOutOfRange = gperr.New("index out of range") -func DockerProviderImpl(name, dockerHost string) ProviderImpl { +func DockerProviderImpl(name string, dockerCfg types.DockerProviderConfig) ProviderImpl { return &DockerProvider{ name, - dockerHost, + dockerCfg, 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 { - return watcher.NewDockerWatcher(p.dockerHost) + return watcher.NewDockerWatcher(p.dockerCfg) } func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - containers, err := docker.ListContainers(ctx, p.dockerHost) + containers, err := docker.ListContainers(ctx, p.dockerCfg) if err != nil { return nil, gperr.Wrap(err) } @@ -70,7 +71,7 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) { routes := make(route.Routes) for _, c := range containers { - container := docker.FromDocker(&c, p.dockerHost) + container := docker.FromDocker(&c, p.dockerCfg) if container.Errors != nil { errs.Add(container.Errors) diff --git a/internal/route/provider/docker_labels_test.go b/internal/route/provider/docker_labels_test.go index bb7e4b54..57a2d1d9 100644 --- a/internal/route/provider/docker_labels_test.go +++ b/internal/route/provider/docker_labels_test.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/goccy/go-yaml" "github.com/yusing/godoxy/internal/docker" + "github.com/yusing/godoxy/internal/types" expect "github.com/yusing/goutils/testing" _ "embed" @@ -28,7 +29,7 @@ func TestParseDockerLabels(t *testing.T) { Ports: []container.Port{ {Type: "tcp", PrivatePort: 1234, PublicPort: 1234}, }, - }, "/var/run/docker.sock"), + }, types.DockerProviderConfig{URL: "unix:///var/run/docker.sock"}), ) expect.NoError(t, err) expect.True(t, routes.Contains("app")) diff --git a/internal/route/provider/docker_test.go b/internal/route/provider/docker_test.go index 65c6df0d..91c16bda 100644 --- a/internal/route/provider/docker_test.go +++ b/internal/route/provider/docker_test.go @@ -10,6 +10,7 @@ import ( D "github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/route" routeTypes "github.com/yusing/godoxy/internal/route/types" + "github.com/yusing/godoxy/internal/types" expect "github.com/yusing/goutils/testing" ) @@ -30,7 +31,7 @@ func makeRoutes(cont *container.Summary, dockerHostIP ...string) route.Routes { } cont.ID = "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 { r.Finalize() } @@ -38,7 +39,7 @@ func makeRoutes(cont *container.Summary, dockerHostIP ...string) route.Routes { } func TestExplicitOnly(t *testing.T) { - p := NewDockerProvider("a!", "") + p := NewDockerProvider("a!", types.DockerProviderConfig{}) expect.True(t, p.IsExplicitOnly()) } @@ -198,7 +199,7 @@ func TestApplyLabelWithRefIndexError(t *testing.T) { "proxy.*.port": "4444", "proxy.#4.scheme": "https", }, - }, "") + }, types.DockerProviderConfig{}) var p DockerProvider _, err := p.routesFromContainerLabels(c) expect.ErrorIs(t, ErrAliasRefIndexOutOfRange, err) @@ -210,7 +211,7 @@ func TestApplyLabelWithRefIndexError(t *testing.T) { D.LabelAliases: "a,b", "proxy.#0.host": "localhost", }, - }, "") + }, types.DockerProviderConfig{}) _, err = p.routesFromContainerLabels(c) expect.ErrorIs(t, ErrAliasRefIndexOutOfRange, err) } diff --git a/internal/route/provider/provider.go b/internal/route/provider/provider.go index aca701bb..ce6e2cb5 100644 --- a/internal/route/provider/provider.go +++ b/internal/route/provider/provider.go @@ -69,13 +69,13 @@ func NewFileProvider(filename string) (p *Provider, err error) { return p, err } -func NewDockerProvider(name string, dockerHost string) *Provider { - if dockerHost == common.DockerHostFromEnv { - dockerHost = env.GetEnvString("DOCKER_HOST", client.DefaultDockerHost) +func NewDockerProvider(name string, dockerCfg types.DockerProviderConfig) *Provider { + if dockerCfg.URL == common.DockerHostFromEnv { + dockerCfg.URL = env.GetEnvString("DOCKER_HOST", client.DefaultDockerHost) } p := newProvider(provider.ProviderTypeDocker) - p.ProviderImpl = DockerProviderImpl(name, dockerHost) + p.ProviderImpl = DockerProviderImpl(name, dockerCfg) p.watcher = p.NewWatcher() return p } @@ -84,7 +84,9 @@ func NewAgentProvider(cfg *agent.AgentConfig) *Provider { p := newProvider(provider.ProviderTypeAgent) agent := &AgentProvider{ AgentConfig: cfg, - docker: DockerProviderImpl(cfg.Name, cfg.FakeDockerHost()), + docker: DockerProviderImpl(cfg.Name, types.DockerProviderConfig{ + URL: cfg.FakeDockerHost(), + }), } p.ProviderImpl = agent p.watcher = p.NewWatcher() diff --git a/internal/route/route.go b/internal/route/route.go index 394c4899..ff625695 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -391,7 +391,7 @@ func (r *Route) start(parent task.Parent) gperr.Error { } if cont := r.ContainerInfo(); cont != nil { - docker.SetDockerHostByContainerID(cont.ContainerID, cont.DockerHost) + docker.SetDockerCfgByContainerID(cont.ContainerID, cont.DockerCfg) } if !excluded { @@ -406,7 +406,7 @@ func (r *Route) start(parent task.Parent) gperr.Error { func (r *Route) Finish(reason any) { if cont := r.ContainerInfo(); cont != nil { - docker.DeleteDockerHostByContainerID(cont.ContainerID) + docker.DeleteDockerCfgByContainerID(cont.ContainerID) } r.FinishAndWait(reason) } diff --git a/internal/types/docker.go b/internal/types/docker.go index 54cfe543..b1d3b037 100644 --- a/internal/types/docker.go +++ b/internal/types/docker.go @@ -13,10 +13,10 @@ type ( PortMapping = map[int]container.Port Container struct { - DockerHost string `json:"docker_host"` - Image *ContainerImage `json:"image"` - ContainerName string `json:"container_name"` - ContainerID string `json:"container_id"` + DockerCfg DockerProviderConfig `json:"docker_cfg"` + Image *ContainerImage `json:"image"` + ContainerName string `json:"container_name"` + ContainerID string `json:"container_id"` State container.ContainerState `json:"state"` diff --git a/internal/types/docker_provider_config.go b/internal/types/docker_provider_config.go new file mode 100644 index 00000000..e005b2f1 --- /dev/null +++ b/internal/types/docker_provider_config.go @@ -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() +} diff --git a/internal/types/docker_provider_config_test.go b/internal/types/docker_provider_config_test.go new file mode 100644 index 00000000..c68b5c37 --- /dev/null +++ b/internal/types/docker_provider_config_test.go @@ -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) + } + }) + } +} diff --git a/internal/types/idlewatcher.go b/internal/types/idlewatcher.go index 3188b98b..38d22554 100644 --- a/internal/types/idlewatcher.go +++ b/internal/types/idlewatcher.go @@ -38,9 +38,9 @@ type ( ContainerSignal string // @name ContainerSignal DockerConfig struct { - DockerHost string `json:"docker_host" validate:"required"` - ContainerID string `json:"container_id" validate:"required"` - ContainerName string `json:"container_name" validate:"required"` + DockerCfg DockerProviderConfig `json:"docker_cfg" validate:"required"` + ContainerID string `json:"container_id" validate:"required"` + ContainerName string `json:"container_name" validate:"required"` } // @name DockerConfig ProxmoxConfig struct { Node string `json:"node" validate:"required"` diff --git a/internal/watcher/docker_watcher.go b/internal/watcher/docker_watcher.go index 7a8a8fc2..af34d30f 100644 --- a/internal/watcher/docker_watcher.go +++ b/internal/watcher/docker_watcher.go @@ -10,12 +10,15 @@ import ( "github.com/docker/docker/client" "github.com/rs/zerolog/log" "github.com/yusing/godoxy/internal/docker" + "github.com/yusing/godoxy/internal/types" "github.com/yusing/godoxy/internal/watcher/events" gperr "github.com/yusing/goutils/errs" ) type ( - DockerWatcher string + DockerWatcher struct { + cfg types.DockerProviderConfig + } DockerListOptions = dockerEvents.ListOptions ) @@ -55,8 +58,10 @@ func DockerFilterContainerNameID(nameOrID string) filters.KeyValuePair { return filters.Arg("container", nameOrID) } -func NewDockerWatcher(host string) DockerWatcher { - return DockerWatcher(host) +func NewDockerWatcher(dockerCfg types.DockerProviderConfig) DockerWatcher { + return DockerWatcher{ + cfg: dockerCfg, + } } 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) go func() { - client, err := docker.NewClient(string(w)) + client, err := docker.NewClient(w.cfg) if err != nil { errCh <- gperr.Wrap(err, "docker watcher: failed to initialize client") return diff --git a/internal/watcher/health/monitor/monitor.go b/internal/watcher/health/monitor/monitor.go index f9268bc4..bb9ce2b7 100644 --- a/internal/watcher/health/monitor/monitor.go +++ b/internal/watcher/health/monitor/monitor.go @@ -61,7 +61,7 @@ func NewMonitor(r types.Route) types.HealthMonCheck { } if r.IsDocker() { cont := r.ContainerInfo() - client, err := docker.NewClient(cont.DockerHost) + client, err := docker.NewClient(cont.DockerCfg) if err != nil { return mon }