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"
--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`:
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.

View file

@ -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.

View file

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

View file

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

View file

@ -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
}

View file

@ -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
@ -380,6 +377,7 @@ func Test_buildConfiguration(t *testing.T) {
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,8 +3224,9 @@ func Test_buildConfiguration(t *testing.T) {
t.Parallel()
p := Provider{
ExposedByDefault: true,
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 }

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"`
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
}