diff --git a/docs/content/providers/docker.md b/docs/content/providers/docker.md index 2369b88450..2dd5f6427a 100644 --- a/docs/content/providers/docker.md +++ b/docs/content/providers/docker.md @@ -714,3 +714,28 @@ providers: ```bash tab="CLI" --providers.docker.tls.insecureSkipVerify=true ``` + +### `allowEmptyServices` + +_Optional, Default=false_ + +If the parameter is set to `true`, +it allows the creation of an empty [servers load balancer](../routing/services/index.md#servers-load-balancer) +if the targeted docker service is not in a [healthy state](https://docs.docker.com/engine/reference/builder/#healthcheck). +With HTTP services, +this results in `503` HTTP responses instead of `404` ones. + +```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 665e8448a5..cc004a5950 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`: +Allow the creation of services without endpoints. (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 fca1251023..2399bce5ba 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`: +Allow the creation of services without endpoints. (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 92b3357c61..603e8bd41a 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -57,22 +57,23 @@ [providers] providersThrottleDuration = "42s" [providers.docker] + allowEmptyServices = true constraints = "foobar" - watch = true - endpoint = "foobar" defaultRule = "foobar" + endpoint = "foobar" exposedByDefault = true - useBindPortIP = true - swarmMode = true + httpClientTimeout = "42s" network = "foobar" + swarmMode = true swarmModeRefreshSeconds = "42s" - httpClientTimeout = "42s" [providers.docker.tls] ca = "foobar" caOptional = true cert = "foobar" key = "foobar" insecureSkipVerify = true + useBindPortIP = true + watch = true [providers.file] directory = "foobar" watch = true diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index c6f47b78bc..635e7575b5 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -63,22 +63,23 @@ entryPoints: providers: providersThrottleDuration: 42s docker: + allowEmptyServices: true constraints: foobar - watch: true - endpoint: foobar defaultRule: foobar + endpoint: foobar + exposedByDefault: true + httpClientTimeout: 42s + network: foobar + swarmMode: true + swarmModeRefreshSeconds: 42s tls: ca: foobar caOptional: true cert: foobar - key: foobar insecureSkipVerify: true - exposedByDefault: true + key: foobar useBindPortIP: true - swarmMode: true - network: foobar - swarmModeRefreshSeconds: 42s - httpClientTimeout: 42s + watch: true file: directory: foobar watch: true diff --git a/pkg/provider/docker/config.go b/pkg/provider/docker/config.go index a2d77c85b2..6f2b616ca9 100644 --- a/pkg/provider/docker/config.go +++ b/pkg/provider/docker/config.go @@ -89,8 +89,6 @@ func (p *Provider) buildConfiguration(ctx context.Context, containersInspected [ } func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.TCPConfiguration) error { - logger := log.FromContext(ctx) - serviceName := getServiceName(container) if len(configuration.Services) == 0 { @@ -102,11 +100,6 @@ func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, container d } } - if container.Health != "" && container.Health != "healthy" { - logger.Debug("Filtering unhealthy or starting container") - return nil - } - for name, service := range configuration.Services { ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) err := p.addServerTCP(ctxSvc, container, service.LoadBalancer) @@ -119,8 +112,6 @@ func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, container d } func (p *Provider) buildUDPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.UDPConfiguration) error { - logger := log.FromContext(ctx) - serviceName := getServiceName(container) if len(configuration.Services) == 0 { @@ -131,11 +122,6 @@ func (p *Provider) buildUDPServiceConfiguration(ctx context.Context, container d } } - if container.Health != "" && container.Health != "healthy" { - logger.Debug("Filtering unhealthy or starting container") - return nil - } - for name, service := range configuration.Services { ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) err := p.addServerUDP(ctxSvc, container, service.LoadBalancer) @@ -148,8 +134,6 @@ func (p *Provider) buildUDPServiceConfiguration(ctx context.Context, container d } func (p *Provider) buildServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.HTTPConfiguration) error { - logger := log.FromContext(ctx) - serviceName := getServiceName(container) if len(configuration.Services) == 0 { @@ -161,11 +145,6 @@ func (p *Provider) buildServiceConfiguration(ctx context.Context, container dock } } - if container.Health != "" && container.Health != "healthy" { - logger.Debug("Filtering unhealthy or starting container") - return nil - } - for name, service := range configuration.Services { ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) err := p.addServer(ctxSvc, container, service.LoadBalancer) @@ -195,6 +174,11 @@ func (p *Provider) keepContainer(ctx context.Context, container dockerData) bool return false } + if !p.AllowEmptyServices && container.Health != "" && container.Health != "healthy" { + logger.Debug("Filtering unhealthy or starting container") + return false + } + return true } @@ -203,6 +187,10 @@ func (p *Provider) addServerTCP(ctx context.Context, container dockerData, loadB return errors.New("load-balancer is not defined") } + if container.Health != "" && container.Health != "healthy" { + return nil + } + var serverPort string if len(loadBalancer.Servers) > 0 { serverPort = loadBalancer.Servers[0].Port @@ -233,6 +221,10 @@ func (p *Provider) addServerUDP(ctx context.Context, container dockerData, loadB return errors.New("load-balancer is not defined") } + if container.Health != "" && container.Health != "healthy" { + return nil + } + var serverPort string if len(loadBalancer.Servers) > 0 { serverPort = loadBalancer.Servers[0].Port @@ -263,6 +255,10 @@ func (p *Provider) addServer(ctx context.Context, container dockerData, loadBala return errors.New("load-balancer is not defined") } + if container.Health != "" && container.Health != "healthy" { + return nil + } + var serverPort string if len(loadBalancer.Servers) > 0 { serverPort = loadBalancer.Servers[0].Port diff --git a/pkg/provider/docker/config_test.go b/pkg/provider/docker/config_test.go index 49cc01e209..a588f0d617 100644 --- a/pkg/provider/docker/config_test.go +++ b/pkg/provider/docker/config_test.go @@ -375,11 +375,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 +2235,41 @@ func Test_buildConfiguration(t *testing.T) { }, }, { - desc: "one container not healthy", + desc: "one container not healthy without allowEmpty", + allowEmptyServices: false, 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: "not_healthy", + }, + }, + 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 HTTP container not healthy", + allowEmptyServices: true, + containers: []dockerData{ + { + ServiceName: "Test", + Name: "Test", + Health: "not_healthy", }, }, expected: &dynamic.Configuration{ @@ -2283,6 +2301,87 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + { + desc: "one TCP container not healthy", + allowEmptyServices: true, + containers: []dockerData{ + { + ServiceName: "Test", + Name: "Test", + Labels: map[string]string{ + "traefik.tcp.routers.foo.rule": "HostSNI(`foo.bar`)", + }, + Health: "not_healthy", + }, + }, + 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 UDP container not healthy", + allowEmptyServices: true, + containers: []dockerData{ + { + ServiceName: "Test", + Name: "Test", + Labels: map[string]string{ + "traefik.udp.routers.foo": "true", + }, + Health: "not_healthy", + }, + }, + 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{ @@ -3069,9 +3168,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 diff --git a/pkg/provider/docker/docker.go b/pkg/provider/docker/docker.go index eda5318fc3..3cfd23b28e 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:"Allow the creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"` defaultRuleTpl *template.Template }