From aff334ffb4b99f013d70b416d64482f8a6397c23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me?= Date: Wed, 6 Jul 2022 10:24:08 +0200 Subject: [PATCH] Add allowEmptyServices for Docker provider --- docs/content/providers/docker.md | 27 +++ .../reference/static-configuration/cli-ref.md | 3 + .../reference/static-configuration/env-ref.md | 3 + .../reference/static-configuration/file.toml | 1 + .../reference/static-configuration/file.yaml | 1 + pkg/provider/docker/config.go | 33 ++- pkg/provider/docker/config_test.go | 221 ++++++++++++++++-- pkg/provider/docker/docker.go | 1 + 8 files changed, 253 insertions(+), 37 deletions(-) diff --git a/docs/content/providers/docker.md b/docs/content/providers/docker.md index 2369b8845..cdb323527 100644 --- a/docs/content/providers/docker.md +++ b/docs/content/providers/docker.md @@ -714,3 +714,30 @@ providers: ```bash tab="CLI" --providers.docker.tls.insecureSkipVerify=true ``` + +### `allowEmptyServices` + +_Optional, Default=false_ + +If the parameter is set to `true`, +any [servers load balancer](../routing/services/index.md#servers-load-balancer) defined for Docker containers is created +regardless of the [healthiness](https://docs.docker.com/engine/reference/builder/#healthcheck) of the corresponding containers. +It also then stays alive and responsive even at times when it becomes empty, +i.e. when all its children containers become unhealthy. +This results in `503` HTTP responses instead of `404` ones, +in the above cases. + +```yaml tab="File (YAML)" +providers: + docker: + allowEmptyServices: true +``` + +```toml tab="File (TOML)" +[providers.docker] + allowEmptyServices = true +``` + +```bash tab="CLI" +--providers.docker.allowEmptyServices=true +``` diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 665e8448a..87a07d232 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -519,6 +519,9 @@ Watch Consul API events. (Default: ```false```) `--providers.docker`: Enable Docker backend with default settings. (Default: ```false```) +`--providers.docker.allowemptyservices`: +Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services. (Default: ```false```) + `--providers.docker.constraints`: Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container. diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index fca125102..3c0a89f4d 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -519,6 +519,9 @@ KV Username `TRAEFIK_PROVIDERS_DOCKER`: Enable Docker backend with default settings. (Default: ```false```) +`TRAEFIK_PROVIDERS_DOCKER_ALLOWEMPTYSERVICES`: +Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services. (Default: ```false```) + `TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS`: Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container. diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 92b3357c6..04a0174bf 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -67,6 +67,7 @@ network = "foobar" swarmModeRefreshSeconds = "42s" httpClientTimeout = "42s" + allowEmptyServices = true [providers.docker.tls] ca = "foobar" caOptional = true diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index c6f47b78b..f472bb455 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -79,6 +79,7 @@ providers: network: foobar swarmModeRefreshSeconds: 42s httpClientTimeout: 42s + allowEmptyServices: true file: directory: foobar watch: true diff --git a/pkg/provider/docker/config.go b/pkg/provider/docker/config.go index fe1e6c655..9de876c63 100644 --- a/pkg/provider/docker/config.go +++ b/pkg/provider/docker/config.go @@ -7,6 +7,7 @@ import ( "net" "strings" + dockertypes "github.com/docker/docker/api/types" "github.com/docker/go-connections/nat" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/label" @@ -100,10 +101,13 @@ func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, container d } } + if container.Health != "" && container.Health != dockertypes.Healthy { + return nil + } + for name, service := range configuration.Services { - ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) - err := p.addServerTCP(ctxSvc, container, service.LoadBalancer) - if err != nil { + ctx := log.With(ctx, log.Str(log.ServiceName, name)) + if err := p.addServerTCP(ctx, container, service.LoadBalancer); err != nil { return fmt.Errorf("service %q error: %w", name, err) } } @@ -116,16 +120,18 @@ func (p *Provider) buildUDPServiceConfiguration(ctx context.Context, container d if len(configuration.Services) == 0 { configuration.Services = make(map[string]*dynamic.UDPService) - lb := &dynamic.UDPServersLoadBalancer{} configuration.Services[serviceName] = &dynamic.UDPService{ - LoadBalancer: lb, + LoadBalancer: &dynamic.UDPServersLoadBalancer{}, } } + if container.Health != "" && container.Health != dockertypes.Healthy { + return nil + } + for name, service := range configuration.Services { - ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) - err := p.addServerUDP(ctxSvc, container, service.LoadBalancer) - if err != nil { + ctx := log.With(ctx, log.Str(log.ServiceName, name)) + if err := p.addServerUDP(ctx, container, service.LoadBalancer); err != nil { return fmt.Errorf("service %q error: %w", name, err) } } @@ -145,10 +151,13 @@ func (p *Provider) buildServiceConfiguration(ctx context.Context, container dock } } + if container.Health != "" && container.Health != dockertypes.Healthy { + return nil + } + for name, service := range configuration.Services { - ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) - err := p.addServer(ctxSvc, container, service.LoadBalancer) - if err != nil { + ctx := log.With(ctx, log.Str(log.ServiceName, name)) + if err := p.addServer(ctx, container, service.LoadBalancer); err != nil { return fmt.Errorf("service %q error: %w", name, err) } } @@ -174,7 +183,7 @@ func (p *Provider) keepContainer(ctx context.Context, container dockerData) bool return false } - if container.Health != "" && container.Health != "healthy" { + if !p.AllowEmptyServices && container.Health != "" && container.Health != dockertypes.Healthy { logger.Debug("Filtering unhealthy or starting container") return false } diff --git a/pkg/provider/docker/config_test.go b/pkg/provider/docker/config_test.go index c02e31683..f58999459 100644 --- a/pkg/provider/docker/config_test.go +++ b/pkg/provider/docker/config_test.go @@ -13,9 +13,6 @@ import ( "github.com/traefik/traefik/v2/pkg/config/dynamic" ) -func Int(v int) *int { return &v } -func Bool(v bool) *bool { return &v } - func TestDefaultRule(t *testing.T) { testCases := []struct { desc string @@ -375,11 +372,12 @@ func TestDefaultRule(t *testing.T) { func Test_buildConfiguration(t *testing.T) { testCases := []struct { - desc string - containers []dockerData - useBindPortIP bool - constraints string - expected *dynamic.Configuration + desc string + containers []dockerData + useBindPortIP bool + constraints string + expected *dynamic.Configuration + allowEmptyServices bool }{ { desc: "invalid HTTP service definition", @@ -2234,24 +2232,12 @@ func Test_buildConfiguration(t *testing.T) { }, }, { - desc: "one container not healthy", + desc: "one unhealthy HTTP container", containers: []dockerData{ { ServiceName: "Test", Name: "Test", - Labels: map[string]string{}, - NetworkSettings: networkSettings{ - Ports: nat.PortMap{ - nat.Port("80/tcp"): []nat.PortBinding{}, - }, - Networks: map[string]*networkData{ - "bridge": { - Name: "bridge", - Addr: "127.0.0.1", - }, - }, - }, - Health: "not_healthy", + Health: docker.Unhealthy, }, }, expected: &dynamic.Configuration{ @@ -2272,6 +2258,186 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + { + desc: "one unhealthy HTTP container with allowEmptyServices", + allowEmptyServices: true, + containers: []dockerData{ + { + ServiceName: "Test", + Name: "Test", + Health: docker.Unhealthy, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test.traefik.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one unhealthy TCP container", + containers: []dockerData{ + { + ServiceName: "Test", + Name: "Test", + Health: docker.Unhealthy, + Labels: map[string]string{ + "traefik.tcp.routers.foo.rule": "HostSNI(`foo.bar`)", + }, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one unhealthy TCP container with allowEmptyServices", + allowEmptyServices: true, + containers: []dockerData{ + { + ServiceName: "Test", + Name: "Test", + Health: docker.Unhealthy, + Labels: map[string]string{ + "traefik.tcp.routers.foo.rule": "HostSNI(`foo.bar`)", + }, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "foo": { + Service: "Test", + Rule: "HostSNI(`foo.bar`)", + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "Test": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + TerminationDelay: Int(100), + }, + }, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one unhealthy UDP container", + containers: []dockerData{ + { + ServiceName: "Test", + Name: "Test", + Health: docker.Unhealthy, + Labels: map[string]string{ + "traefik.udp.routers.foo": "true", + }, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one unhealthy UDP container with allowEmptyServices", + allowEmptyServices: true, + containers: []dockerData{ + { + ServiceName: "Test", + Name: "Test", + Labels: map[string]string{ + "traefik.udp.routers.foo": "true", + }, + Health: docker.Unhealthy, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "foo": { + Service: "Test", + }, + }, + Services: map[string]*dynamic.UDPService{ + "Test": { + LoadBalancer: &dynamic.UDPServersLoadBalancer{}, + }, + }, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, { desc: "one container with non matching constraints", containers: []dockerData{ @@ -3058,9 +3224,10 @@ func Test_buildConfiguration(t *testing.T) { t.Parallel() p := Provider{ - ExposedByDefault: true, - DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)", - UseBindPortIP: test.useBindPortIP, + AllowEmptyServices: test.allowEmptyServices, + DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)", + ExposedByDefault: true, + UseBindPortIP: test.useBindPortIP, } p.Constraints = test.constraints @@ -3515,3 +3682,7 @@ func TestSwarmGetPort(t *testing.T) { }) } } + +func Int(v int) *int { return &v } + +func Bool(v bool) *bool { return &v } diff --git a/pkg/provider/docker/docker.go b/pkg/provider/docker/docker.go index eda5318fc..f1281e593 100644 --- a/pkg/provider/docker/docker.go +++ b/pkg/provider/docker/docker.go @@ -59,6 +59,7 @@ type Provider struct { Network string `description:"Default Docker network used." json:"network,omitempty" toml:"network,omitempty" yaml:"network,omitempty" export:"true"` SwarmModeRefreshSeconds ptypes.Duration `description:"Polling interval for swarm mode." json:"swarmModeRefreshSeconds,omitempty" toml:"swarmModeRefreshSeconds,omitempty" yaml:"swarmModeRefreshSeconds,omitempty" export:"true"` HTTPClientTimeout ptypes.Duration `description:"Client timeout for HTTP connections." json:"httpClientTimeout,omitempty" toml:"httpClientTimeout,omitempty" yaml:"httpClientTimeout,omitempty" export:"true"` + AllowEmptyServices bool `description:"Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"` defaultRuleTpl *template.Template }