From f0589b310f74673719a3ba4740ffd27a696d0cea Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Doumenjou Date: Mon, 16 Apr 2018 18:14:04 +0200 Subject: [PATCH] Fix multiple frontends with docker-compose --scale --- integration/docker_compose_test.go | 78 ++++++++++++++++ integration/docker_test.go | 2 +- integration/integration_test.go | 1 + integration/resources/compose/minimal.yml | 4 + provider/docker/config.go | 19 +++- .../docker/config_container_docker_test.go | 89 +++++++++++++++++++ provider/docker/deprecated_config.go | 9 +- 7 files changed, 194 insertions(+), 8 deletions(-) create mode 100644 integration/docker_compose_test.go create mode 100644 integration/resources/compose/minimal.yml diff --git a/integration/docker_compose_test.go b/integration/docker_compose_test.go new file mode 100644 index 000000000..cc5ab2c08 --- /dev/null +++ b/integration/docker_compose_test.go @@ -0,0 +1,78 @@ +package integration + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "os" + "time" + + "github.com/containous/traefik/integration/try" + "github.com/containous/traefik/testhelpers" + "github.com/containous/traefik/types" + "github.com/go-check/check" + checker "github.com/vdemeester/shakers" +) + +const ( + composeProject = "minimal" +) + +// Docker test suites +type DockerComposeSuite struct { + BaseSuite +} + +func (s *DockerComposeSuite) SetUpSuite(c *check.C) { + s.createComposeProject(c, composeProject) + s.composeProject.Start(c) +} + +func (s *DockerComposeSuite) TearDownSuite(c *check.C) { + // shutdown and delete compose project + if s.composeProject != nil { + s.composeProject.Stop(c) + } +} + +func (s *DockerComposeSuite) TestComposeScale(c *check.C) { + var serviceCount = 2 + var composeService = "whoami1" + + s.composeProject.Scale(c, composeService, serviceCount) + + file := s.adaptFileForHost(c, "fixtures/docker/simple.toml") + defer os.Remove(file) + + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + req := testhelpers.MustNewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil) + req.Host = "my.super.host" + + _, err = try.ResponseUntilStatusCode(req, 1500*time.Millisecond, http.StatusOK) + c.Assert(err, checker.IsNil) + + resp, err := http.Get("http://127.0.0.1:8080/api/providers/docker") + c.Assert(err, checker.IsNil) + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + c.Assert(err, checker.IsNil) + + var provider types.Configuration + c.Assert(json.Unmarshal(body, &provider), checker.IsNil) + + // check that we have only one backend with n servers + c.Assert(provider.Backends, checker.HasLen, 1) + + myBackend := provider.Backends["backend-"+composeService+"-integrationtest"+composeProject] + c.Assert(myBackend, checker.NotNil) + c.Assert(myBackend.Servers, checker.HasLen, serviceCount) + + // check that we have only one frontend + c.Assert(provider.Frontends, checker.HasLen, 1) +} diff --git a/integration/docker_test.go b/integration/docker_test.go index ad845063a..9e22f7b92 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -99,7 +99,7 @@ func (s *DockerSuite) TestSimpleConfiguration(c *check.C) { defer cmd.Process.Kill() // TODO validate : run on 80 - // Expected a 404 as we did not comfigure anything + // Expected a 404 as we did not configure anything err = try.GetRequest("http://127.0.0.1:8000/", 500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound)) c.Assert(err, checker.IsNil) } diff --git a/integration/integration_test.go b/integration/integration_test.go index 2e147c934..6439029e3 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -41,6 +41,7 @@ func init() { check.Suite(&ConstraintSuite{}) check.Suite(&ConsulCatalogSuite{}) check.Suite(&ConsulSuite{}) + check.Suite(&DockerComposeSuite{}) check.Suite(&DockerSuite{}) check.Suite(&DynamoDBSuite{}) check.Suite(&EtcdSuite{}) diff --git a/integration/resources/compose/minimal.yml b/integration/resources/compose/minimal.yml new file mode 100644 index 000000000..8490e59a7 --- /dev/null +++ b/integration/resources/compose/minimal.yml @@ -0,0 +1,4 @@ +whoami1: + image: emilevauge/whoami + labels: + - traefik.frontend.rule=PathPrefix:/whoami diff --git a/provider/docker/config.go b/provider/docker/config.go index bc6155a84..030ac2cef 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -67,12 +67,13 @@ func (p *Provider) buildConfigurationV2(containersInspected []dockerData) *types container.SegmentLabels = labels container.SegmentName = segmentName - // Frontends - if _, exists := serviceNames[container.ServiceName+segmentName]; !exists { + serviceNamesKey := getServiceNameKey(container, p.SwarmMode, segmentName) + + if _, exists := serviceNames[serviceNamesKey]; !exists { frontendName := p.getFrontendName(container, idx) frontends[frontendName] = append(frontends[frontendName], container) - if len(container.ServiceName+segmentName) > 0 { - serviceNames[container.ServiceName+segmentName] = struct{}{} + if len(serviceNamesKey) > 0 { + serviceNames[serviceNamesKey] = struct{}{} } } @@ -104,6 +105,16 @@ func (p *Provider) buildConfigurationV2(containersInspected []dockerData) *types return configuration } +func getServiceNameKey(container dockerData, swarmMode bool, segmentName string) string { + serviceNameKey := container.ServiceName + + if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); !swarmMode && err == nil { + serviceNameKey = values[labelDockerComposeService] + values[labelDockerComposeProject] + } + + return serviceNameKey + segmentName +} + func (p *Provider) containerFilter(container dockerData) bool { if !label.IsEnabled(container.Labels, p.ExposedByDefault) { log.Debugf("Filtering disabled container %s", container.Name) diff --git a/provider/docker/config_container_docker_test.go b/provider/docker/config_container_docker_test.go index d05497235..f1ac3718e 100644 --- a/provider/docker/config_container_docker_test.go +++ b/provider/docker/config_container_docker_test.go @@ -304,6 +304,95 @@ func TestDockerBuildConfiguration(t *testing.T) { }, }, }, + { + desc: "when docker compose scale with different compose service names", + containers: []docker.ContainerJSON{ + containerJSON( + name("test_0"), + labels(map[string]string{ + labelDockerComposeProject: "myProject", + labelDockerComposeService: "myService", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + containerJSON( + name("test_1"), + labels(map[string]string{ + labelDockerComposeProject: "myProject", + labelDockerComposeService: "myService", + }), + + ports(nat.PortMap{ + "80/tcp": {}, + }), + + withNetwork("bridge", ipv4("127.0.0.2")), + ), + containerJSON( + name("test_2"), + labels(map[string]string{ + labelDockerComposeProject: "myProject", + labelDockerComposeService: "myService2", + }), + + ports(nat.PortMap{ + "80/tcp": {}, + }), + + withNetwork("bridge", ipv4("127.0.0.3")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-myService-myProject-docker-localhost-0": { + Backend: "backend-myService-myProject", + PassHostHeader: true, + EntryPoints: []string{}, + BasicAuth: []string{}, + Routes: map[string]types.Route{ + "route-frontend-Host-myService-myProject-docker-localhost-0": { + Rule: "Host:myService.myProject.docker.localhost", + }, + }, + }, + "frontend-Host-myService2-myProject-docker-localhost-2": { + Backend: "backend-myService2-myProject", + PassHostHeader: true, + EntryPoints: []string{}, + BasicAuth: []string{}, + Routes: map[string]types.Route{ + "route-frontend-Host-myService2-myProject-docker-localhost-2": { + Rule: "Host:myService2.myProject.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-myService-myProject": { + Servers: map[string]types.Server{ + "server-test-0": { + URL: "http://127.0.0.1:80", + Weight: label.DefaultWeight, + }, "server-test-1": { + URL: "http://127.0.0.2:80", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + "backend-myService2-myProject": { + Servers: map[string]types.Server{ + "server-test-2": { + URL: "http://127.0.0.3:80", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + }, + }, } for _, test := range testCases { diff --git a/provider/docker/deprecated_config.go b/provider/docker/deprecated_config.go index 2e4308a78..0954466a1 100644 --- a/provider/docker/deprecated_config.go +++ b/provider/docker/deprecated_config.go @@ -125,11 +125,14 @@ func (p *Provider) buildConfigurationV1(containersInspected []dockerData) *types servers := map[string][]dockerData{} serviceNames := make(map[string]struct{}) for idx, container := range filteredContainers { - if _, exists := serviceNames[container.ServiceName]; !exists { + + serviceNamesKey := getServiceNameKey(container, p.SwarmMode, "") + + if _, exists := serviceNames[serviceNamesKey]; !exists { frontendName := p.getFrontendNameV1(container, idx) frontends[frontendName] = append(frontends[frontendName], container) - if len(container.ServiceName) > 0 { - serviceNames[container.ServiceName] = struct{}{} + if len(serviceNamesKey) > 0 { + serviceNames[serviceNamesKey] = struct{}{} } } backendName := getBackendNameV1(container)