1
0
Fork 0

Allow discovering non-running Docker containers

This commit is contained in:
Alexis Couvreur 2025-10-24 08:08:04 -04:00 committed by GitHub
parent 5c489c05fc
commit 10be359327
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 600 additions and 11 deletions

View file

@ -12,6 +12,7 @@ func containerJSON(ops ...func(*containertypes.InspectResponse)) containertypes.
ContainerJSONBase: &containertypes.ContainerJSONBase{
Name: "fake",
HostConfig: &containertypes.HostConfig{},
State: &containertypes.State{},
},
Config: &containertypes.Config{},
NetworkSettings: &containertypes.NetworkSettings{

View file

@ -114,6 +114,11 @@ func (p *DynConfBuilder) buildTCPServiceConfiguration(ctx context.Context, conta
}
}
// Keep an empty server load-balancer for non-running containers.
if container.Status != "" && container.Status != containertypes.StateRunning {
return nil
}
// Keep an empty server load-balancer for unhealthy containers.
if container.Health != "" && container.Health != containertypes.Healthy {
return nil
}
@ -138,6 +143,11 @@ func (p *DynConfBuilder) buildUDPServiceConfiguration(ctx context.Context, conta
}
}
// Keep an empty server load-balancer for non-running containers.
if container.Status != "" && container.Status != containertypes.StateRunning {
return nil
}
// Keep an empty server load-balancer for unhealthy containers.
if container.Health != "" && container.Health != containertypes.Healthy {
return nil
}
@ -164,6 +174,11 @@ func (p *DynConfBuilder) buildServiceConfiguration(ctx context.Context, containe
}
}
// Keep an empty server load-balancer for non-running containers.
if container.Status != "" && container.Status != containertypes.StateRunning {
return nil
}
// Keep an empty server load-balancer for unhealthy containers.
if container.Health != "" && container.Health != containertypes.Healthy {
return nil
}
@ -196,6 +211,19 @@ func (p *DynConfBuilder) keepContainer(ctx context.Context, container dockerData
return false
}
// AllowNonRunning has precedence over AllowEmptyServices.
// If AllowNonRunning is true, we don't care about the container health/status,
// and we need to quit before checking it.
// Only configurable with the Docker provider.
if container.ExtraConf.AllowNonRunning {
return true
}
if container.Status != "" && container.Status != containertypes.StateRunning {
logger.Debug().Msg("Filtering non running container")
return false
}
if !p.AllowEmptyServices && container.Health != "" && container.Health != containertypes.Healthy {
logger.Debug().Msg("Filtering unhealthy or starting container")
return false

View file

@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
)
@ -3935,6 +3936,464 @@ func TestDynConfBuilder_build(t *testing.T) {
}
}
func TestDynConfBuilder_build_allowNonRunning(t *testing.T) {
testCases := []struct {
desc string
containers []dockerData
expected *dynamic.Configuration
}{
{
desc: "exited container with allowNonRunning=true should create router and service without servers",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Status: "exited",
Health: "",
ExtraConf: configuration{
Enable: true,
AllowNonRunning: true,
},
NetworkSettings: networkSettings{
NetworkMode: "bridge",
Ports: nat.PortMap{
"80/tcp": []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
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`)",
DefaultRule: true,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"Test": {
LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: pointer(true),
Strategy: "wrr",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Stores: map[string]tls.Store{},
},
},
},
{
desc: "exited container with allowNonRunning=false should not create anything",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Status: "exited",
Health: "",
ExtraConf: configuration{
Enable: true,
AllowNonRunning: false,
},
NetworkSettings: networkSettings{
NetworkMode: "bridge",
Ports: nat.PortMap{
"80/tcp": []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
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{},
},
TLS: &dynamic.TLSConfiguration{
Stores: map[string]tls.Store{},
},
},
},
{
desc: "running container with allowNonRunning=true should work normally with servers",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Status: "running",
Health: "",
ExtraConf: configuration{
Enable: true,
AllowNonRunning: true,
},
NetworkSettings: networkSettings{
NetworkMode: "bridge",
Ports: nat.PortMap{
"80/tcp": []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
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`)",
DefaultRule: true,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"Test": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:80",
},
},
PassHostHeader: pointer(true),
Strategy: "wrr",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Stores: map[string]tls.Store{},
},
},
},
{
desc: "created container with allowNonRunning=true should create router and service without servers)",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Status: "created",
Health: "",
ExtraConf: configuration{
Enable: true,
AllowNonRunning: true,
},
NetworkSettings: networkSettings{
NetworkMode: "bridge",
Ports: nat.PortMap{
"80/tcp": []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
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`)",
DefaultRule: true,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"Test": {
LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: pointer(true),
Strategy: "wrr",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Stores: map[string]tls.Store{},
},
},
},
{
desc: "dead container with allowNonRunning=true should create router and service without servers)",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Status: "dead",
Health: "",
ExtraConf: configuration{
Enable: true,
AllowNonRunning: true,
},
NetworkSettings: networkSettings{
NetworkMode: "bridge",
Ports: nat.PortMap{
"80/tcp": []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
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`)",
DefaultRule: true,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"Test": {
LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: pointer(true),
Strategy: "wrr",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Stores: map[string]tls.Store{},
},
},
},
{
desc: "exited container with TCP configuration and allowNonRunning=true should create TCP service without servers",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Status: "exited",
Health: "",
Labels: map[string]string{
"traefik.tcp.routers.Test.rule": "HostSNI(`test.localhost`)",
},
ExtraConf: configuration{
Enable: true,
AllowNonRunning: true,
},
NetworkSettings: networkSettings{
NetworkMode: "bridge",
Ports: nat.PortMap{
"80/tcp": []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"Test": {
Service: "Test",
Rule: "HostSNI(`test.localhost`)",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"Test": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
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{},
},
TLS: &dynamic.TLSConfiguration{
Stores: map[string]tls.Store{},
},
},
},
{
desc: "exited container with UDP configuration and allowNonRunning=true should create UDP service without servers",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Status: "exited",
Health: "",
Labels: map[string]string{
"traefik.udp.routers.Test.entrypoints": "udp",
},
ExtraConf: configuration{
Enable: true,
AllowNonRunning: true,
},
NetworkSettings: networkSettings{
NetworkMode: "bridge",
Ports: nat.PortMap{
"80/udp": []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{
"Test": {
Service: "Test",
EntryPoints: []string{"udp"},
},
},
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{},
},
TLS: &dynamic.TLSConfiguration{
Stores: map[string]tls.Store{},
},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(DefaultTemplateRule, nil)
require.NoError(t, err)
p := Shared{
ExposedByDefault: true,
DefaultRule: DefaultTemplateRule,
defaultRuleTpl: defaultRuleTpl,
}
builder := NewDynConfBuilder(p, nil, false)
configuration := builder.build(t.Context(), test.containers)
assert.Equal(t, test.expected, configuration)
})
}
}
func TestDynConfBuilder_getIPPort_docker(t *testing.T) {
type expected struct {
ip string

View file

@ -10,6 +10,7 @@ type dockerData struct {
ID string
ServiceName string
Name string
Status string
Labels map[string]string // List of labels set to container or service
NetworkSettings networkSettings
Health string

View file

@ -165,7 +165,9 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
}
func (p *Provider) listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) ([]dockerData, error) {
containerList, err := dockerClient.ContainerList(ctx, container.ListOptions{})
containerList, err := dockerClient.ContainerList(ctx, container.ListOptions{
All: true,
})
if err != nil {
return nil, err
}

View file

@ -43,9 +43,9 @@ func inspectContainers(ctx context.Context, dockerClient client.ContainerAPIClie
return dockerData{}
}
// This condition is here to avoid to have empty IP https://github.com/traefik/traefik/issues/2459
// We register only container which are running
if containerInspected.ContainerJSONBase != nil && containerInspected.ContainerJSONBase.State != nil && containerInspected.ContainerJSONBase.State.Running {
// Always parse all containers (running and stopped)
// The allowNonRunning filtering will be applied later in service configuration
if containerInspected.ContainerJSONBase != nil && containerInspected.ContainerJSONBase.State != nil {
return parseContainer(containerInspected)
}
@ -61,6 +61,7 @@ func parseContainer(container containertypes.InspectResponse) dockerData {
dData.ID = container.ContainerJSONBase.ID
dData.Name = container.ContainerJSONBase.Name
dData.ServiceName = dData.Name // Default ServiceName to be the container's Name.
dData.Status = container.ContainerJSONBase.State.Status
if container.ContainerJSONBase.HostConfig != nil {
dData.NetworkSettings.NetworkMode = container.ContainerJSONBase.HostConfig.NetworkMode

View file

@ -16,17 +16,24 @@ const (
// configuration contains information from the labels that are globals (not related to the dynamic configuration)
// or specific to the provider.
type configuration struct {
Enable bool
Network string
LBSwarm bool
Enable bool
Network string
LBSwarm bool
AllowNonRunning bool
}
type labelConfiguration struct {
Enable bool
Docker *specificConfiguration
Docker *dockerSpecificConfiguration
Swarm *specificConfiguration
}
type dockerSpecificConfiguration struct {
Network *string
LBSwarm bool
AllowNonRunning bool
}
type specificConfiguration struct {
Network *string
LBSwarm bool
@ -43,9 +50,15 @@ func (p *Shared) extractDockerLabels(container dockerData) (configuration, error
network = *conf.Docker.Network
}
var allowNonRunning bool
if conf.Docker != nil {
allowNonRunning = conf.Docker.AllowNonRunning
}
return configuration{
Enable: conf.Enable,
Network: network,
Enable: conf.Enable,
Network: network,
AllowNonRunning: allowNonRunning,
}, nil
}