Add allowEmptyServices for Docker provider

This commit is contained in:
Jérôme 2022-07-06 10:24:08 +02:00 committed by GitHub
parent c51e590591
commit aff334ffb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 253 additions and 37 deletions

View file

@ -714,3 +714,30 @@ providers:
```bash tab="CLI" ```bash tab="CLI"
--providers.docker.tls.insecureSkipVerify=true --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
```

View file

@ -519,6 +519,9 @@ Watch Consul API events. (Default: ```false```)
`--providers.docker`: `--providers.docker`:
Enable Docker backend with default settings. (Default: ```false```) 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`: `--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. Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container.

View file

@ -519,6 +519,9 @@ KV Username
`TRAEFIK_PROVIDERS_DOCKER`: `TRAEFIK_PROVIDERS_DOCKER`:
Enable Docker backend with default settings. (Default: ```false```) 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`: `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. Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container.

View file

@ -67,6 +67,7 @@
network = "foobar" network = "foobar"
swarmModeRefreshSeconds = "42s" swarmModeRefreshSeconds = "42s"
httpClientTimeout = "42s" httpClientTimeout = "42s"
allowEmptyServices = true
[providers.docker.tls] [providers.docker.tls]
ca = "foobar" ca = "foobar"
caOptional = true caOptional = true

View file

@ -79,6 +79,7 @@ providers:
network: foobar network: foobar
swarmModeRefreshSeconds: 42s swarmModeRefreshSeconds: 42s
httpClientTimeout: 42s httpClientTimeout: 42s
allowEmptyServices: true
file: file:
directory: foobar directory: foobar
watch: true watch: true

View file

@ -7,6 +7,7 @@ import (
"net" "net"
"strings" "strings"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/config/label" "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 { for name, service := range configuration.Services {
ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) ctx := log.With(ctx, log.Str(log.ServiceName, name))
err := p.addServerTCP(ctxSvc, container, service.LoadBalancer) if err := p.addServerTCP(ctx, container, service.LoadBalancer); err != nil {
if err != nil {
return fmt.Errorf("service %q error: %w", name, err) 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 { if len(configuration.Services) == 0 {
configuration.Services = make(map[string]*dynamic.UDPService) configuration.Services = make(map[string]*dynamic.UDPService)
lb := &dynamic.UDPServersLoadBalancer{}
configuration.Services[serviceName] = &dynamic.UDPService{ 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 { for name, service := range configuration.Services {
ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) ctx := log.With(ctx, log.Str(log.ServiceName, name))
err := p.addServerUDP(ctxSvc, container, service.LoadBalancer) if err := p.addServerUDP(ctx, container, service.LoadBalancer); err != nil {
if err != nil {
return fmt.Errorf("service %q error: %w", name, err) 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 { for name, service := range configuration.Services {
ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) ctx := log.With(ctx, log.Str(log.ServiceName, name))
err := p.addServer(ctxSvc, container, service.LoadBalancer) if err := p.addServer(ctx, container, service.LoadBalancer); err != nil {
if err != nil {
return fmt.Errorf("service %q error: %w", name, err) 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 return false
} }
if container.Health != "" && container.Health != "healthy" { if !p.AllowEmptyServices && container.Health != "" && container.Health != dockertypes.Healthy {
logger.Debug("Filtering unhealthy or starting container") logger.Debug("Filtering unhealthy or starting container")
return false return false
} }

View file

@ -13,9 +13,6 @@ import (
"github.com/traefik/traefik/v2/pkg/config/dynamic" "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) { func TestDefaultRule(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
@ -380,6 +377,7 @@ func Test_buildConfiguration(t *testing.T) {
useBindPortIP bool useBindPortIP bool
constraints string constraints string
expected *dynamic.Configuration expected *dynamic.Configuration
allowEmptyServices bool
}{ }{
{ {
desc: "invalid HTTP service definition", 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{ containers: []dockerData{
{ {
ServiceName: "Test", ServiceName: "Test",
Name: "Test", Name: "Test",
Labels: map[string]string{}, Health: docker.Unhealthy,
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",
}, },
}, },
expected: &dynamic.Configuration{ 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", desc: "one container with non matching constraints",
containers: []dockerData{ containers: []dockerData{
@ -3058,8 +3224,9 @@ func Test_buildConfiguration(t *testing.T) {
t.Parallel() t.Parallel()
p := Provider{ p := Provider{
ExposedByDefault: true, AllowEmptyServices: test.allowEmptyServices,
DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)", DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)",
ExposedByDefault: true,
UseBindPortIP: test.useBindPortIP, UseBindPortIP: test.useBindPortIP,
} }
p.Constraints = test.constraints 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 }

View file

@ -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"` 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"` 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"` 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 defaultRuleTpl *template.Template
} }