diff --git a/docs/content/migration/v3.md b/docs/content/migration/v3.md index 9d16bc47a..62d7fc75c 100644 --- a/docs/content/migration/v3.md +++ b/docs/content/migration/v3.md @@ -160,3 +160,10 @@ the `backendtlspolicies` and `backendtlspolicies/status` rights have to be added In `v3.2.1`, the `X-Forwarded-Prefix` header is now handled like the other `X-Forwarded-*` headers: Traefik removes it when it's sent from an untrusted source. Please refer to the Forwarded headers [documentation](../routing/entrypoints.md#forwarded-headers) for more details. + +## v3.2.2 + +### Swarm Provider + +In `v3.2.2`, the `traefik.docker.network` and `traefik.docker.lbswarm` labels have been deprecated, +please use the `traefik.swarm.network` and `traefik.swarm.lbswarm` labels instead. diff --git a/docs/content/reference/dynamic-configuration/swarm.yml b/docs/content/reference/dynamic-configuration/swarm.yml index 6f9e1c62f..1b40b4483 100644 --- a/docs/content/reference/dynamic-configuration/swarm.yml +++ b/docs/content/reference/dynamic-configuration/swarm.yml @@ -1,3 +1,3 @@ - "traefik.enable=true" -- "traefik.docker.network=foobar" -- "traefik.docker.lbswarm=true" +- "traefik.swarm.network=foobar" +- "traefik.swarm.lbswarm=true" diff --git a/docs/content/routing/providers/swarm.md b/docs/content/routing/providers/swarm.md index 84b984633..1f1680bdd 100644 --- a/docs/content/routing/providers/swarm.md +++ b/docs/content/routing/providers/swarm.md @@ -648,10 +648,10 @@ You can tell Traefik to consider (or not) the container by setting `traefik.enab This option overrides the value of `exposedByDefault`. -#### `traefik.docker.network` +#### `traefik.swarm.network` ```yaml -- "traefik.docker.network=mynetwork" +- "traefik.swarm.network=mynetwork" ``` Overrides the default docker network to use for connections to the container. @@ -659,13 +659,10 @@ Overrides the default docker network to use for connections to the container. If a container is linked to several networks, be sure to set the proper network name (you can check this with `docker inspect `), otherwise it will randomly pick one (depending on how docker is returning them). -!!! warning - The Docker Swarm provider still uses the same per-container mechanism as the Docker provider, so therefore the label still uses the `docker` keyword intentionally. - !!! warning When deploying a stack from a compose file `stack`, the networks defined are prefixed with `stack`. -#### `traefik.docker.lbswarm` +#### `traefik.swarm.lbswarm` ```yaml - "traefik.docker.lbswarm=true" @@ -675,6 +672,3 @@ Enables Swarm's inbuilt load balancer (only relevant in Swarm Mode). If you enable this option, Traefik will use the virtual IP provided by docker swarm instead of the containers IPs. Which means that Traefik will not perform any kind of load balancing and will delegate this task to swarm. - -!!! warning - The Docker Swarm provider still uses the same per-container mechanism as the Docker provider, so therefore the label still uses the `docker` keyword intentionally. diff --git a/pkg/provider/docker/config.go b/pkg/provider/docker/config.go index d69fbbe81..4d819862a 100644 --- a/pkg/provider/docker/config.go +++ b/pkg/provider/docker/config.go @@ -21,10 +21,11 @@ import ( type DynConfBuilder struct { Shared apiClient client.APIClient + swarm bool } -func NewDynConfBuilder(configuration Shared, apiClient client.APIClient) *DynConfBuilder { - return &DynConfBuilder{Shared: configuration, apiClient: apiClient} +func NewDynConfBuilder(configuration Shared, apiClient client.APIClient, swarm bool) *DynConfBuilder { + return &DynConfBuilder{Shared: configuration, apiClient: apiClient, swarm: swarm} } func (p *DynConfBuilder) build(ctx context.Context, containersInspected []dockerData) *dynamic.Configuration { @@ -321,16 +322,16 @@ func (p *DynConfBuilder) getIPAddress(ctx context.Context, container dockerData) logger := log.Ctx(ctx) netNotFound := false - if container.ExtraConf.Docker.Network != "" { + if container.ExtraConf.Network != "" { settings := container.NetworkSettings if settings.Networks != nil { - network := settings.Networks[container.ExtraConf.Docker.Network] + network := settings.Networks[container.ExtraConf.Network] if network != nil { return network.Addr } netNotFound = true - logger.Warn().Msgf("Could not find network named %q for container %q. Maybe you're missing the project's prefix in the label?", container.ExtraConf.Docker.Network, container.Name) + logger.Warn().Msgf("Could not find network named %q for container %q. Maybe you're missing the project's prefix in the label?", container.ExtraConf.Network, container.Name) } } @@ -360,12 +361,12 @@ func (p *DynConfBuilder) getIPAddress(ctx context.Context, container dockerData) containerParsed := parseContainer(containerInspected) extraConf, err := p.extractLabels(containerParsed) if err != nil { - logger.Warn().Err(err).Msgf("Unable to get IP address for container %s : failed to get extra configuration for container %s", container.Name, containerInspected.Name) + logger.Warn().Err(err).Msgf("Unable to get IP address for container %s: failed to get extra configuration for container %s", container.Name, containerInspected.Name) return "" } - if extraConf.Docker.Network == "" { - extraConf.Docker.Network = container.ExtraConf.Docker.Network + if extraConf.Network == "" { + extraConf.Network = container.ExtraConf.Network } containerParsed.ExtraConf = extraConf @@ -396,3 +397,10 @@ func (p *DynConfBuilder) getPortBinding(container dockerData, serverPort string) return nil, fmt.Errorf("unable to find the external IP:Port for the container %q", container.Name) } + +func (p *DynConfBuilder) extractLabels(container dockerData) (configuration, error) { + if p.swarm { + return p.Shared.extractSwarmLabels(container) + } + return p.Shared.extractDockerLabels(container) +} diff --git a/pkg/provider/docker/config_test.go b/pkg/provider/docker/config_test.go index adddd6468..2a6f8ed32 100644 --- a/pkg/provider/docker/config_test.go +++ b/pkg/provider/docker/config_test.go @@ -405,18 +405,16 @@ func TestDynConfBuilder_DefaultRule(t *testing.T) { DefaultRule: test.defaultRule, }, } + require.NoError(t, p.Init()) - err := p.Init() - require.NoError(t, err) + builder := NewDynConfBuilder(p.Shared, nil, false) for i := range len(test.containers) { var err error - test.containers[i].ExtraConf, err = p.extractLabels(test.containers[i]) + test.containers[i].ExtraConf, err = builder.extractLabels(test.containers[i]) require.NoError(t, err) } - builder := NewDynConfBuilder(p.Shared, nil) - configuration := builder.build(context.Background(), test.containers) assert.Equal(t, test.expected, configuration) @@ -3662,17 +3660,16 @@ func TestDynConfBuilder_build(t *testing.T) { } p.Constraints = test.constraints - err := p.Init() - require.NoError(t, err) + require.NoError(t, p.Init()) + + builder := NewDynConfBuilder(p.Shared, nil, false) for i := range len(test.containers) { var err error - test.containers[i].ExtraConf, err = p.extractLabels(test.containers[i]) + test.containers[i].ExtraConf, err = builder.extractLabels(test.containers[i]) require.NoError(t, err) } - builder := NewDynConfBuilder(p.Shared, nil) - configuration := builder.build(context.Background(), test.containers) assert.Equal(t, test.expected, configuration) @@ -3843,7 +3840,7 @@ func TestDynConfBuilder_getIPPort_docker(t *testing.T) { builder := NewDynConfBuilder(Shared{ Network: "testnet", UseBindPortIP: true, - }, nil) + }, nil, false) actualIP, actualPort, actualError := builder.getIPPort(context.Background(), dData, test.serverPort) if test.expected.error { @@ -3956,12 +3953,12 @@ func TestDynConfBuilder_getIPAddress_docker(t *testing.T) { dData := parseContainer(test.container) - dData.ExtraConf.Docker.Network = conf.Network + dData.ExtraConf.Network = conf.Network if len(test.network) > 0 { - dData.ExtraConf.Docker.Network = test.network + dData.ExtraConf.Network = test.network } - builder := NewDynConfBuilder(conf, nil) + builder := NewDynConfBuilder(conf, nil, false) actual := builder.getIPAddress(context.Background(), dData) assert.Equal(t, test.expected, actual) @@ -3995,7 +3992,7 @@ func TestDynConfBuilder_getIPAddress_swarm(t *testing.T) { { service: swarmService( serviceLabels(map[string]string{ - "traefik.docker.network": "barnet", + "traefik.swarm.network": "barnet", }), withEndpointSpec(modeVIP), withEndpoint( @@ -4019,12 +4016,13 @@ func TestDynConfBuilder_getIPAddress_swarm(t *testing.T) { t.Run(strconv.Itoa(serviceID), func(t *testing.T) { t.Parallel() - p := &SwarmProvider{} + var p SwarmProvider + require.NoError(t, p.Init()) dData, err := p.parseService(context.Background(), test.service, test.networks) require.NoError(t, err) - builder := NewDynConfBuilder(p.Shared, nil) + builder := NewDynConfBuilder(p.Shared, nil, false) actual := builder.getIPAddress(context.Background(), dData) assert.Equal(t, test.expected, actual) }) diff --git a/pkg/provider/docker/pdocker.go b/pkg/provider/docker/pdocker.go index 9b7506711..44ff3470d 100644 --- a/pkg/provider/docker/pdocker.go +++ b/pkg/provider/docker/pdocker.go @@ -79,7 +79,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. } defer func() { _ = dockerClient.Close() }() - builder := NewDynConfBuilder(p.Shared, dockerClient) + builder := NewDynConfBuilder(p.Shared, dockerClient, false) serverVersion, err := dockerClient.ServerVersion(ctx) if err != nil { @@ -179,7 +179,7 @@ func (p *Provider) listContainers(ctx context.Context, dockerClient client.Conta continue } - extraConf, err := p.extractLabels(dData) + extraConf, err := p.extractDockerLabels(dData) if err != nil { log.Ctx(ctx).Error().Err(err).Msgf("Skip container %s", getServiceName(dData)) continue diff --git a/pkg/provider/docker/pswarm.go b/pkg/provider/docker/pswarm.go index 07f5dbd0b..07ad3cabc 100644 --- a/pkg/provider/docker/pswarm.go +++ b/pkg/provider/docker/pswarm.go @@ -82,7 +82,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool * } defer func() { _ = dockerClient.Close() }() - builder := NewDynConfBuilder(p.Shared, dockerClient) + builder := NewDynConfBuilder(p.Shared, dockerClient, true) serverVersion, err := dockerClient.ServerVersion(ctx) if err != nil { @@ -200,7 +200,7 @@ func (p *SwarmProvider) listServices(ctx context.Context, dockerClient client.AP continue } - if dData.ExtraConf.Docker.LBSwarm { + if dData.ExtraConf.LBSwarm { if len(dData.NetworkSettings.Networks) > 0 { dockerDataList = append(dockerDataList, dData) } @@ -229,37 +229,38 @@ func (p *SwarmProvider) parseService(ctx context.Context, service swarmtypes.Ser NetworkSettings: networkSettings{}, } - extraConf, err := p.extractLabels(dData) + extraConf, err := p.extractSwarmLabels(dData) if err != nil { return dockerData{}, err } dData.ExtraConf = extraConf - if service.Spec.EndpointSpec != nil { - if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeDNSRR { - if dData.ExtraConf.Docker.LBSwarm { - logger.Warn().Msgf("Ignored %s endpoint-mode not supported, service name: %s. Fallback to Traefik load balancing", swarmtypes.ResolutionModeDNSRR, service.Spec.Annotations.Name) + if service.Spec.EndpointSpec == nil { + return dData, nil + } + if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeDNSRR { + if dData.ExtraConf.LBSwarm { + logger.Warn().Msgf("Ignored %s endpoint-mode not supported, service name: %s. Fallback to Traefik load balancing", swarmtypes.ResolutionModeDNSRR, service.Spec.Annotations.Name) + } + } else if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeVIP { + dData.NetworkSettings.Networks = make(map[string]*networkData) + for _, virtualIP := range service.Endpoint.VirtualIPs { + networkService := networkMap[virtualIP.NetworkID] + if networkService == nil { + logger.Debug().Msgf("Network not found, id: %s", virtualIP.NetworkID) + continue } - } else if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeVIP { - dData.NetworkSettings.Networks = make(map[string]*networkData) - for _, virtualIP := range service.Endpoint.VirtualIPs { - networkService := networkMap[virtualIP.NetworkID] - if networkService != nil { - if len(virtualIP.Addr) > 0 { - ip, _, _ := net.ParseCIDR(virtualIP.Addr) - network := &networkData{ - Name: networkService.Name, - ID: virtualIP.NetworkID, - Addr: ip.String(), - } - dData.NetworkSettings.Networks[network.Name] = network - } else { - logger.Debug().Msgf("No virtual IPs found in network %s", virtualIP.NetworkID) - } - } else { - logger.Debug().Msgf("Network not found, id: %s", virtualIP.NetworkID) - } + if len(virtualIP.Addr) == 0 { + logger.Debug().Msgf("No virtual IPs found in network %s", virtualIP.NetworkID) + continue } + ip, _, _ := net.ParseCIDR(virtualIP.Addr) + network := &networkData{ + Name: networkService.Name, + ID: virtualIP.NetworkID, + Addr: ip.String(), + } + dData.NetworkSettings.Networks[network.Name] = network } } return dData, nil diff --git a/pkg/provider/docker/pswarm_test.go b/pkg/provider/docker/pswarm_test.go index c9fae5f12..5406c99be 100644 --- a/pkg/provider/docker/pswarm_test.go +++ b/pkg/provider/docker/pswarm_test.go @@ -65,7 +65,9 @@ func TestListTasks(t *testing.T) { t.Run(strconv.Itoa(caseID), func(t *testing.T) { t.Parallel() - p := SwarmProvider{} + var p SwarmProvider + require.NoError(t, p.Init()) + dockerData, err := p.parseService(context.Background(), test.service, test.networks) require.NoError(t, err) @@ -100,8 +102,8 @@ func TestSwarmProvider_listServices(t *testing.T) { swarmService( serviceName("service1"), serviceLabels(map[string]string{ - "traefik.docker.network": "barnet", - "traefik.docker.LBSwarm": "true", + "traefik.swarm.network": "barnet", + "traefik.swarm.LBSwarm": "true", }), withEndpointSpec(modeVIP), withEndpoint( @@ -111,8 +113,8 @@ func TestSwarmProvider_listServices(t *testing.T) { swarmService( serviceName("service2"), serviceLabels(map[string]string{ - "traefik.docker.network": "barnet", - "traefik.docker.LBSwarm": "true", + "traefik.swarm.network": "barnet", + "traefik.swarm.LBSwarm": "true", }), withEndpointSpec(modeDNSRR)), }, @@ -126,8 +128,8 @@ func TestSwarmProvider_listServices(t *testing.T) { swarmService( serviceName("service1"), serviceLabels(map[string]string{ - "traefik.docker.network": "barnet", - "traefik.docker.LBSwarm": "true", + "traefik.swarm.network": "barnet", + "traefik.swarm.LBSwarm": "true", }), withEndpointSpec(modeVIP), withEndpoint( @@ -137,8 +139,8 @@ func TestSwarmProvider_listServices(t *testing.T) { swarmService( serviceName("service2"), serviceLabels(map[string]string{ - "traefik.docker.network": "barnet", - "traefik.docker.LBSwarm": "true", + "traefik.swarm.network": "barnet", + "traefik.swarm.LBSwarm": "true", }), withEndpointSpec(modeDNSRR)), }, @@ -173,7 +175,7 @@ func TestSwarmProvider_listServices(t *testing.T) { swarmService( serviceName("service1"), serviceLabels(map[string]string{ - "traefik.docker.network": "barnet", + "traefik.swarm.network": "barnet", }), withEndpointSpec(modeVIP), withEndpoint( @@ -183,7 +185,7 @@ func TestSwarmProvider_listServices(t *testing.T) { swarmService( serviceName("service2"), serviceLabels(map[string]string{ - "traefik.docker.network": "barnet", + "traefik.swarm.network": "barnet", }), withEndpointSpec(modeDNSRR)), }, @@ -233,7 +235,8 @@ func TestSwarmProvider_listServices(t *testing.T) { dockerClient := &fakeServicesClient{services: test.services, tasks: test.tasks, dockerVersion: test.dockerVersion, networks: test.networks} - p := SwarmProvider{} + var p SwarmProvider + require.NoError(t, p.Init()) serviceDockerData, err := p.listServices(context.Background(), dockerClient) assert.NoError(t, err) @@ -351,7 +354,8 @@ func TestSwarmProvider_parseService_task(t *testing.T) { t.Run(strconv.Itoa(caseID), func(t *testing.T) { t.Parallel() - p := SwarmProvider{} + var p SwarmProvider + require.NoError(t, p.Init()) dData, err := p.parseService(context.Background(), test.service, test.networks) require.NoError(t, err) diff --git a/pkg/provider/docker/shared_labels.go b/pkg/provider/docker/shared_labels.go index f17e51508..6eab7143d 100644 --- a/pkg/provider/docker/shared_labels.go +++ b/pkg/provider/docker/shared_labels.go @@ -1,8 +1,10 @@ package docker import ( + "errors" "fmt" + "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/config/label" ) @@ -11,29 +13,73 @@ const ( labelDockerComposeService = "com.docker.compose.service" ) -// configuration Contains information from the labels that are globals (not related to the dynamic configuration) +// 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 - Docker specificConfiguration -} - -type specificConfiguration struct { + Enable bool Network string LBSwarm bool } -func (p *Shared) extractLabels(container dockerData) (configuration, error) { - conf := configuration{ - Enable: p.ExposedByDefault, - Docker: specificConfiguration{ - Network: p.Network, - }, +type labelConfiguration struct { + Enable bool + Docker *specificConfiguration + Swarm *specificConfiguration +} + +type specificConfiguration struct { + Network *string + LBSwarm bool +} + +func (p *Shared) extractDockerLabels(container dockerData) (configuration, error) { + conf := labelConfiguration{Enable: p.ExposedByDefault} + if err := label.Decode(container.Labels, &conf, "traefik.docker.", "traefik.enable"); err != nil { + return configuration{}, fmt.Errorf("decoding Docker labels: %w", err) } - err := label.Decode(container.Labels, &conf, "traefik.docker.", "traefik.enable") - if err != nil { - return configuration{}, err + network := p.Network + if conf.Docker != nil && conf.Docker.Network != nil { + network = *conf.Docker.Network + } + + return configuration{ + Enable: conf.Enable, + Network: network, + }, nil +} + +func (p *Shared) extractSwarmLabels(container dockerData) (configuration, error) { + labelConf := labelConfiguration{Enable: p.ExposedByDefault} + if err := label.Decode(container.Labels, &labelConf, "traefik.enable", "traefik.docker.", "traefik.swarm."); err != nil { + return configuration{}, fmt.Errorf("decoding Swarm labels: %w", err) + } + + if labelConf.Docker != nil && labelConf.Swarm != nil { + return configuration{}, errors.New("both Docker and Swarm labels are defined") + } + + conf := configuration{ + Enable: labelConf.Enable, + Network: p.Network, + } + + if labelConf.Docker != nil { + log.Warn().Msg("Labels traefik.docker.* for Swarm provider are deprecated. Please use traefik.swarm.* labels instead") + + conf.LBSwarm = labelConf.Docker.LBSwarm + + if labelConf.Docker.Network != nil { + conf.Network = *labelConf.Docker.Network + } + } + + if labelConf.Swarm != nil { + conf.LBSwarm = labelConf.Swarm.LBSwarm + + if labelConf.Swarm.Network != nil { + conf.Network = *labelConf.Swarm.Network + } } return conf, nil diff --git a/pkg/provider/docker/shared_test.go b/pkg/provider/docker/shared_test.go index 0f542d00a..90022c067 100644 --- a/pkg/provider/docker/shared_test.go +++ b/pkg/provider/docker/shared_test.go @@ -98,7 +98,8 @@ func Test_getPort_swarm(t *testing.T) { t.Run(strconv.Itoa(serviceID), func(t *testing.T) { t.Parallel() - p := SwarmProvider{} + var p SwarmProvider + require.NoError(t, p.Init()) dData, err := p.parseService(context.Background(), test.service, test.networks) require.NoError(t, err)