Add allowEmptyServices for Docker provider
This commit is contained in:
parent
c51e590591
commit
aff334ffb4
8 changed files with 253 additions and 37 deletions
|
@ -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
|
||||||
|
```
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue