diff --git a/docs/configuration/backends/consulcatalog.md b/docs/configuration/backends/consulcatalog.md index be3a6b9ea..4eb124a54 100644 --- a/docs/configuration/backends/consulcatalog.md +++ b/docs/configuration/backends/consulcatalog.md @@ -151,6 +151,17 @@ Additional settings can be defined using Consul Catalog tags. | `.frontend.whiteList.sourceRange=RANGE` | Sets a list of IP-Ranges which are allowed to access.
An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | | `.frontend.whiteList.useXForwardedFor=true` | Uses `X-Forwarded-For` header as valid source of IP for the white list. | +### Multiple frontends for a single service + +If you need to support multiple frontends for a service, for example when having multiple `rules` that can't be combined, specify them as follows: + +``` +.frontends.A.rule=Host:A:PathPrefix:/A +.frontends.B.rule=Host:B:PathPrefix:/ +``` + +`A` and `B` here are just arbitrary names, they can be anything. You can use any setting that applies to `.frontend` from the table above. + ### Custom Headers !!! note diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 7607b41d8..548677e04 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -674,3 +674,49 @@ func (s *ConsulCatalogSuite) TestMaintenanceMode(c *check.C) { err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) c.Assert(err, checker.IsNil) } + +func (s *ConsulCatalogSuite) TestMultipleFrontendRule(c *check.C) { + cmd, display := s.traefikCmd( + withConfigFile("fixtures/consul_catalog/simple.toml"), + "--consulCatalog", + "--consulCatalog.endpoint="+s.consulIP+":8500", + "--consulCatalog.domain=consul.localhost") + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // Wait for Traefik to turn ready. + err = try.GetRequest("http://127.0.0.1:8000/", 2*time.Second, try.StatusCodeIs(http.StatusNotFound)) + c.Assert(err, checker.IsNil) + + whoami := s.composeProject.Container(c, "whoami1") + + err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, + []string{ + "traefik.frontends.service1.rule=Host:whoami1.consul.localhost", + "traefik.frontends.service2.rule=Host:whoami2.consul.localhost", + }) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + + err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "whoami1.consul.localhost" + + err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "whoami2.consul.localhost" + + err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) +} diff --git a/provider/consulcatalog/config.go b/provider/consulcatalog/config.go index 73d8c37e1..94020e40f 100644 --- a/provider/consulcatalog/config.go +++ b/provider/consulcatalog/config.go @@ -55,7 +55,7 @@ func (p *Provider) buildConfigurationV2(catalog []catalogUpdate) *types.Configur var services []*serviceUpdate for _, info := range catalog { if len(info.Nodes) > 0 { - services = append(services, info.Service) + services = append(services, p.generateFrontends(info.Service)...) allNodes = append(allNodes, info.Nodes...) } } @@ -161,6 +161,9 @@ func getCircuitBreaker(labels map[string]string) *types.CircuitBreaker { } func getServiceBackendName(service *serviceUpdate) string { + if service.ParentServiceName != "" { + return strings.ToLower(service.ParentServiceName) + } return strings.ToLower(service.ServiceName) } diff --git a/provider/consulcatalog/config_test.go b/provider/consulcatalog/config_test.go index 332a14dee..11d7f440c 100644 --- a/provider/consulcatalog/config_test.go +++ b/provider/consulcatalog/config_test.go @@ -120,6 +120,80 @@ func TestProviderBuildConfiguration(t *testing.T) { }, }, }, + { + desc: "Should build config which contains three frontends and one backend", + nodes: []catalogUpdate{ + { + Service: &serviceUpdate{ + ServiceName: "test", + Attributes: []string{ + "random.foo=bar", + label.Prefix + "frontend.rule=Host:A", + label.Prefix + "frontends.test1.rule=Host:B", + label.Prefix + "frontends.test2.rule=Host:C", + }, + }, + Nodes: []*api.ServiceEntry{ + { + Service: &api.AgentService{ + Service: "test", + Address: "127.0.0.1", + Port: 80, + Tags: []string{ + "random.foo=bar", + }, + }, + Node: &api.Node{ + Node: "localhost", + Address: "127.0.0.1", + }, + }, + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-test": { + Backend: "backend-test", + PassHostHeader: true, + Routes: map[string]types.Route{ + "route-host-test": { + Rule: "Host:A", + }, + }, + EntryPoints: []string{}, + }, + "frontend-test-test1": { + Backend: "backend-test", + PassHostHeader: true, + Routes: map[string]types.Route{ + "route-host-test-test1": { + Rule: "Host:B", + }, + }, + EntryPoints: []string{}, + }, + "frontend-test-test2": { + Backend: "backend-test", + PassHostHeader: true, + Routes: map[string]types.Route{ + "route-host-test-test2": { + Rule: "Host:C", + }, + }, + EntryPoints: []string{}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test": { + Servers: map[string]types.Server{ + "test-0-O0Tnh-SwzY69M6SurTKP3wNKkzI": { + URL: "http://127.0.0.1:80", + Weight: 1, + }, + }, + }, + }, + }, { desc: "Should build config with a basic auth with a backward compatibility", nodes: []catalogUpdate{ diff --git a/provider/consulcatalog/consul_catalog.go b/provider/consulcatalog/consul_catalog.go index 312fd6da5..297096206 100644 --- a/provider/consulcatalog/consul_catalog.go +++ b/provider/consulcatalog/consul_catalog.go @@ -50,9 +50,15 @@ type Service struct { } type serviceUpdate struct { - ServiceName string - Attributes []string - TraefikLabels map[string]string + ServiceName string + ParentServiceName string + Attributes []string + TraefikLabels map[string]string +} + +type frontendSegment struct { + Name string + Labels map[string]string } type catalogUpdate struct { @@ -560,3 +566,52 @@ func (p *Provider) getConstraintTags(tags []string) []string { return values } + +func (p *Provider) generateFrontends(service *serviceUpdate) []*serviceUpdate { + frontends := make([]*serviceUpdate, 0) + // to support .frontend.xxx + frontends = append(frontends, &serviceUpdate{ + ServiceName: service.ServiceName, + ParentServiceName: service.ServiceName, + Attributes: service.Attributes, + TraefikLabels: service.TraefikLabels, + }) + + // loop over children of .frontends.* + for _, frontend := range getSegments(p.Prefix+".frontends", p.Prefix, service.TraefikLabels) { + frontends = append(frontends, &serviceUpdate{ + ServiceName: service.ServiceName + "-" + frontend.Name, + ParentServiceName: service.ServiceName, + Attributes: service.Attributes, + TraefikLabels: frontend.Labels, + }) + } + + return frontends +} +func getSegments(path string, prefix string, tree map[string]string) []*frontendSegment { + segments := make([]*frontendSegment, 0) + // find segment names + segmentNames := make(map[string]bool) + for key := range tree { + if strings.HasPrefix(key, path+".") { + segmentNames[strings.SplitN(strings.TrimPrefix(key, path+"."), ".", 2)[0]] = true + } + } + + // get labels for each segment found + for segment := range segmentNames { + labels := make(map[string]string) + for key, value := range tree { + if strings.HasPrefix(key, path+"."+segment) { + labels[prefix+".frontend"+strings.TrimPrefix(key, path+"."+segment)] = value + } + } + segments = append(segments, &frontendSegment{ + Name: segment, + Labels: labels, + }) + } + + return segments +}