diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 40765059c..b84cf4cc5 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -250,9 +250,9 @@ func (s *ConsulCatalogSuite) TestExposedByDefaultTrueSimpleServiceMultipleNode(c err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains("nginx1", "nginx2")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, + try.BodyContains(nginx.NetworkSettings.IPAddress, nginx2.NetworkSettings.IPAddress)) c.Assert(err, checker.IsNil) - } func (s *ConsulCatalogSuite) TestRefreshConfigWithMultipleNodeWithoutHealthCheck(c *check.C) { @@ -285,27 +285,30 @@ func (s *ConsulCatalogSuite) TestRefreshConfigWithMultipleNodeWithoutHealthCheck err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains("nginx1")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, + try.BodyContains(nginx.NetworkSettings.IPAddress)) c.Assert(err, checker.IsNil) err = s.registerService("test", nginx2.NetworkSettings.IPAddress, 80, []string{"name=nginx2"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains("nginx1", "nginx2")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, + try.BodyContains(nginx.NetworkSettings.IPAddress, nginx2.NetworkSettings.IPAddress)) c.Assert(err, checker.IsNil) s.deregisterService("test", nginx2.NetworkSettings.IPAddress) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains("nginx1")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, + try.BodyContains(nginx.NetworkSettings.IPAddress)) c.Assert(err, checker.IsNil) err = s.registerService("test", nginx2.NetworkSettings.IPAddress, 80, []string{"name=nginx2"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", nginx2.NetworkSettings.IPAddress) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains("nginx1", "nginx2")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, + try.BodyContains(nginx.NetworkSettings.IPAddress, nginx2.NetworkSettings.IPAddress)) c.Assert(err, checker.IsNil) - } func (s *ConsulCatalogSuite) TestBasicAuthSimpleService(c *check.C) { @@ -363,7 +366,8 @@ func (s *ConsulCatalogSuite) TestRefreshConfigTagChange(c *check.C) { c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, try.BodyContains("nginx1")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, + try.BodyContains(nginx.NetworkSettings.IPAddress)) c.Assert(err, checker.NotNil) err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1", "traefik.enable=true", "traefik.backend.circuitbreaker=ResponseCodeRatio(500, 600, 0, 600) > 0.5"}) @@ -376,7 +380,8 @@ func (s *ConsulCatalogSuite) TestRefreshConfigTagChange(c *check.C) { err = try.Request(req, 20*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains("nginx1")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, + try.BodyContains(nginx.NetworkSettings.IPAddress)) c.Assert(err, checker.IsNil) } @@ -444,7 +449,7 @@ func (s *ConsulCatalogSuite) TestRefreshConfigPortChange(c *check.C) { err = try.Request(req, 20*time.Second, try.StatusCodeIs(http.StatusBadGateway)) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, try.BodyContains("nginx1")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, try.BodyContains(nginx.NetworkSettings.IPAddress)) c.Assert(err, checker.IsNil) err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1", "traefik.enable=true"}) @@ -452,7 +457,7 @@ func (s *ConsulCatalogSuite) TestRefreshConfigPortChange(c *check.C) { defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains("nginx1")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains(nginx.NetworkSettings.IPAddress)) c.Assert(err, checker.IsNil) err = try.Request(req, 20*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index b6c1db4f4..59fe59235 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -385,12 +385,12 @@ func getServicePorts(services []*api.CatalogService) []int { func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) { health := p.client.Health() - opts := &api.QueryOptions{} - data, _, err := health.Service(service, "", true, opts) + data, _, err := health.Service(service, "", true, &api.QueryOptions{}) if err != nil { log.WithError(err).Errorf("Failed to fetch details of %s", service) return catalogUpdate{}, err } + nodes := fun.Filter(func(node *api.ServiceEntry) bool { return p.nodeFilter(service, node) }, data).([]*api.ServiceEntry) diff --git a/provider/consul/consul_catalog_config.go b/provider/consul/consul_catalog_config.go index b66a86538..9bca70dbb 100644 --- a/provider/consul/consul_catalog_config.go +++ b/provider/consul/consul_catalog_config.go @@ -2,6 +2,9 @@ package consul import ( "bytes" + "crypto/sha1" + "encoding/base64" + "math" "sort" "strconv" "strings" @@ -23,16 +26,25 @@ func (p *CatalogProvider) buildConfiguration(catalog []catalogUpdate) *types.Con // Backend functions "getBackend": getBackend, "getBackendAddress": getBackendAddress, - "hasMaxconnAttributes": p.hasMaxConnAttributes, - "getSticky": p.getSticky, - "hasStickinessLabel": p.hasStickinessLabel, - "getStickinessCookieName": p.getStickinessCookieName, + "getBackendName": getServerName, // Deprecated [breaking] getBackendName -> getServerName + "getServerName": getServerName, + "hasMaxconnAttributes": p.hasMaxConnAttributes, // Deprecated [breaking] + "getSticky": p.getSticky, // Deprecated [breaking] + "hasStickinessLabel": p.hasStickinessLabel, // Deprecated [breaking] + "getStickinessCookieName": p.getStickinessCookieName, // Deprecated [breaking] + "getWeight": p.getWeight, // Deprecated [breaking] Must replaced by a simple: "getWeight": p.getFuncIntAttribute(label.SuffixWeight, 0) + "getProtocol": p.getFuncStringAttribute(label.SuffixProtocol, label.DefaultProtocol), + "getCircuitBreaker": p.getCircuitBreaker, + "getLoadBalancer": p.getLoadBalancer, + "getMaxConn": p.getMaxConn, // Frontend functions - "getBackendName": getBackendName, - "getFrontendRule": p.getFrontendRule, - "getBasicAuth": p.getBasicAuth, - "getEntryPoints": getEntryPoints, + "getFrontendRule": p.getFrontendRule, + "getBasicAuth": p.getFuncSliceAttribute(label.SuffixFrontendAuthBasic), + "getEntryPoints": getEntryPoints, // Deprecated [breaking] + "getFrontEndEntryPoints": p.getFuncSliceAttribute(label.SuffixFrontendEntryPoints), // TODO [breaking] rename to getEntryPoints when getEntryPoints will be removed + "getPriority": p.getFuncIntAttribute(label.SuffixFrontendPriority, 0), + "getPassHostHeader": p.getFuncBoolAttribute(label.SuffixFrontendPassHostHeader, true), } var allNodes []*api.ServiceEntry @@ -107,10 +119,7 @@ func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string { return buffer.String() } -func (p *CatalogProvider) getBasicAuth(tags []string) []string { - return p.getSliceAttribute(label.SuffixFrontendAuthBasic, tags) -} - +// Deprecated func (p *CatalogProvider) hasMaxConnAttributes(attributes []string) bool { amount := p.getAttribute(label.SuffixBackendMaxConnAmount, attributes, "") extractorFunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, attributes, "") @@ -133,19 +142,22 @@ func getBackendAddress(node *api.ServiceEntry) string { return node.Node.Address } -func getBackendName(node *api.ServiceEntry, index int) string { - serviceName := strings.ToLower(node.Service.Service) + "--" + node.Service.Address + "--" + strconv.Itoa(node.Service.Port) +func getServerName(node *api.ServiceEntry, index int) string { + serviceName := node.Service.Service + node.Service.Address + strconv.Itoa(node.Service.Port) + // TODO sort tags ? + serviceName += strings.Join(node.Service.Tags, "") - for _, tag := range node.Service.Tags { - serviceName += "--" + provider.Normalize(tag) + hash := sha1.New() + _, err := hash.Write([]byte(serviceName)) + if err != nil { + // Impossible case + log.Error(err) + } else { + serviceName = base64.URLEncoding.EncodeToString(hash.Sum(nil)) } - serviceName = strings.Replace(serviceName, ".", "-", -1) - serviceName = strings.Replace(serviceName, "=", "-", -1) - // unique int at the end - serviceName += "--" + strconv.Itoa(index) - return serviceName + return provider.Normalize(node.Service.Service + "-" + strconv.Itoa(index) + "-" + serviceName) } // TODO: Deprecated @@ -161,17 +173,153 @@ func (p *CatalogProvider) getSticky(tags []string) string { return stickyTag } +// Deprecated func (p *CatalogProvider) hasStickinessLabel(tags []string) bool { stickinessTag := p.getAttribute(label.SuffixBackendLoadBalancerStickiness, tags, "") return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true") } +// Deprecated func (p *CatalogProvider) getStickinessCookieName(tags []string) string { return p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, "") } +// Deprecated +func (p *CatalogProvider) getWeight(tags []string) int { + weight := p.getIntAttribute(label.SuffixWeight, tags, 0) + + // Deprecated + deprecatedWeightTag := "backend." + label.SuffixWeight + if p.hasAttribute(deprecatedWeightTag, tags) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", + p.getPrefixedName(deprecatedWeightTag), p.getPrefixedName(label.SuffixWeight)) + + weight = p.getIntAttribute(deprecatedWeightTag, tags, 0) + } + + return weight +} + +func (p *CatalogProvider) getCircuitBreaker(tags []string) *types.CircuitBreaker { + circuitBreaker := p.getAttribute(label.SuffixBackendCircuitBreakerExpression, tags, "") + + if p.hasAttribute(label.SuffixBackendCircuitBreaker, tags) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", + p.getPrefixedName(label.SuffixBackendCircuitBreaker), p.getPrefixedName(label.SuffixBackendCircuitBreakerExpression)) + + circuitBreaker = p.getAttribute(label.SuffixBackendCircuitBreaker, tags, "") + } + + if len(circuitBreaker) == 0 { + return nil + } + + return &types.CircuitBreaker{Expression: circuitBreaker} +} + +func (p *CatalogProvider) getLoadBalancer(tags []string) *types.LoadBalancer { + rawSticky := p.getSticky(tags) + sticky, err := strconv.ParseBool(rawSticky) + if err != nil { + log.Debugf("Invalid sticky value: %s", rawSticky) + sticky = false + } + + method := p.getAttribute(label.SuffixBackendLoadBalancerMethod, tags, label.DefaultBackendLoadBalancerMethod) + + // Deprecated + deprecatedMethodTag := "backend.loadbalancer" + if p.hasAttribute(deprecatedMethodTag, tags) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", + p.getPrefixedName(deprecatedMethodTag), p.getPrefixedName(label.SuffixWeight)) + + method = p.getAttribute(deprecatedMethodTag, tags, label.SuffixBackendLoadBalancerMethod) + } + + lb := &types.LoadBalancer{ + Method: method, + Sticky: sticky, + } + + if p.getBoolAttribute(label.SuffixBackendLoadBalancerStickiness, tags, false) { + lb.Stickiness = &types.Stickiness{ + CookieName: p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, ""), + } + } + + return lb +} + +func (p *CatalogProvider) getMaxConn(tags []string) *types.MaxConn { + amount := p.getInt64Attribute(label.SuffixBackendMaxConnAmount, tags, math.MinInt64) + extractorFunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, tags, label.DefaultBackendMaxconnExtractorFunc) + + if amount == math.MinInt64 || len(extractorFunc) == 0 { + return nil + } + + return &types.MaxConn{ + Amount: amount, + ExtractorFunc: extractorFunc, + } +} + // Base functions +func (p *CatalogProvider) getFuncStringAttribute(name string, defaultValue string) func(tags []string) string { + return func(tags []string) string { + return p.getAttribute(name, tags, defaultValue) + } +} + +func (p *CatalogProvider) getFuncSliceAttribute(name string) func(tags []string) []string { + return func(tags []string) []string { + return p.getSliceAttribute(name, tags) + } +} + +func (p *CatalogProvider) getFuncIntAttribute(name string, defaultValue int) func(tags []string) int { + return func(tags []string) int { + return p.getIntAttribute(name, tags, defaultValue) + } +} + +func (p *CatalogProvider) getFuncBoolAttribute(name string, defaultValue bool) func(tags []string) bool { + return func(tags []string) bool { + return p.getBoolAttribute(name, tags, defaultValue) + } +} + +func (p *CatalogProvider) getInt64Attribute(name string, tags []string, defaultValue int64) int64 { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return defaultValue + } + + value, err := strconv.ParseInt(rawValue, 10, 64) + if err != nil { + log.Errorf("Invalid value for %s: %s", name, rawValue) + return defaultValue + } + return value +} + +func (p *CatalogProvider) getIntAttribute(name string, tags []string, defaultValue int) int { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return defaultValue + } + + value, err := strconv.Atoi(rawValue) + if err != nil { + log.Errorf("Invalid value for %s: %s", name, rawValue) + return defaultValue + } + return value +} + func (p *CatalogProvider) getSliceAttribute(name string, tags []string) []string { rawValue := getTag(p.getPrefixedName(name), tags, "") diff --git a/provider/consul/consul_catalog_config_test.go b/provider/consul/consul_catalog_config_test.go index edf0f60fb..4919145dc 100644 --- a/provider/consul/consul_catalog_config_test.go +++ b/provider/consul/consul_catalog_config_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestBuildConfiguration(t *testing.T) { +func TestCatalogProviderBuildConfiguration(t *testing.T) { provider := &CatalogProvider{ Domain: "localhost", Prefix: "traefik", @@ -50,12 +50,12 @@ func TestBuildConfiguration(t *testing.T) { Service: &serviceUpdate{ ServiceName: "test", Attributes: []string{ - "traefik.backend.loadbalancer=drr", - "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5", "random.foo=bar", - "traefik.backend.maxconn.amount=1000", - "traefik.backend.maxconn.extractorfunc=client.ip", - "traefik.frontend.auth.basic=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "backend.loadbalancer=drr", + label.TraefikBackendCircuitBreaker + "=NetworkErrorRatio() > 0.5", + label.TraefikBackendMaxConnAmount + "=1000", + label.TraefikBackendMaxConnExtractorFunc + "=client.ip", + label.TraefikFrontendAuthBasic + "=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", }, }, Nodes: []*api.ServiceEntry{ @@ -65,10 +65,10 @@ func TestBuildConfiguration(t *testing.T) { Address: "127.0.0.1", Port: 80, Tags: []string{ - "traefik.backend.weight=42", "random.foo=bar", - "traefik.backend.passHostHeader=true", - "traefik.protocol=https", + label.Prefix + "backend.weight=42", + label.TraefikFrontendPassHostHeader + "=true", + label.TraefikProtocol + "=https", }, }, Node: &api.Node{ @@ -88,13 +88,14 @@ func TestBuildConfiguration(t *testing.T) { Rule: "Host:test.localhost", }, }, - BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + EntryPoints: []string{}, + BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, }, }, expectedBackends: map[string]*types.Backend{ "backend-test": { Servers: map[string]types.Server{ - "test--127-0-0-1--80--traefik-backend-weight-42--random-foo-bar--traefik-backend-passHostHeader-true--traefik-protocol-https--0": { + "test-0-us4-27hAOu2ARV7nNrmv6GoKlcA": { URL: "https://127.0.0.1:80", Weight: 42, }, @@ -208,7 +209,7 @@ func TestHasTag(t *testing.T) { } } -func TestGetPrefixedName(t *testing.T) { +func TestCatalogProviderGetPrefixedName(t *testing.T) { testCases := []struct { desc string name string @@ -255,7 +256,7 @@ func TestGetPrefixedName(t *testing.T) { } -func TestGetAttribute(t *testing.T) { +func TestCatalogProviderGetAttribute(t *testing.T) { testCases := []struct { desc string tags []string @@ -334,7 +335,212 @@ func TestGetAttribute(t *testing.T) { } } -func TestGetFrontendRule(t *testing.T) { +func TestCatalogProviderGetIntAttribute(t *testing.T) { + p := &CatalogProvider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + name string + tags []string + defaultValue int + expected int + }{ + { + desc: "should return default value when empty name", + name: "", + tags: []string{"traefik.foo=10"}, + expected: 0, + }, + { + desc: "should return default value when empty tags", + name: "traefik.foo", + tags: nil, + expected: 0, + }, + { + desc: "should return default value when value is not a int", + name: "foo", + tags: []string{"traefik.foo=bar"}, + expected: 0, + }, + { + desc: "should return a value when tag exist", + name: "foo", + tags: []string{"traefik.foo=10"}, + expected: 10, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getIntAttribute(test.name, test.tags, test.defaultValue) + + assert.Equal(t, test.expected, result) + }) + } +} + +func TestCatalogProviderGetInt64Attribute(t *testing.T) { + p := &CatalogProvider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + name string + tags []string + defaultValue int64 + expected int64 + }{ + { + desc: "should return default value when empty name", + name: "", + tags: []string{"traefik.foo=10"}, + expected: 0, + }, + { + desc: "should return default value when empty tags", + name: "traefik.foo", + tags: nil, + expected: 0, + }, + { + desc: "should return default value when value is not a int", + name: "foo", + tags: []string{"traefik.foo=bar"}, + expected: 0, + }, + { + desc: "should return a value when tag exist", + name: "foo", + tags: []string{"traefik.foo=10"}, + expected: 10, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getInt64Attribute(test.name, test.tags, test.defaultValue) + + assert.Equal(t, test.expected, result) + }) + } +} + +func TestCatalogProviderGetBoolAttribute(t *testing.T) { + p := &CatalogProvider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + name string + tags []string + defaultValue bool + expected bool + }{ + { + desc: "should return default value when empty name", + name: "", + tags: []string{"traefik.foo=10"}, + expected: false, + }, + { + desc: "should return default value when empty tags", + name: "traefik.foo", + tags: nil, + expected: false, + }, + { + desc: "should return default value when value is not a bool", + name: "foo", + tags: []string{"traefik.foo=bar"}, + expected: false, + }, + { + desc: "should return a value when tag exist", + name: "foo", + tags: []string{"traefik.foo=true"}, + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getBoolAttribute(test.name, test.tags, test.defaultValue) + + assert.Equal(t, test.expected, result) + }) + } +} + +func TestCatalogProviderGetSliceAttribute(t *testing.T) { + p := &CatalogProvider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + name string + tags []string + expected []string + }{ + { + desc: "should return nil when empty name", + name: "", + tags: []string{"traefik.foo=bar,bor,bir"}, + expected: nil, + }, + { + desc: "should return nil when empty tags", + name: "foo", + tags: nil, + expected: nil, + }, + { + desc: "should return nil when tag doesn't have value", + name: "", + tags: []string{"traefik.foo="}, + expected: nil, + }, + { + desc: "should return a slice when tag contains comma separated values", + name: "foo", + tags: []string{"traefik.foo=bar,bor,bir"}, + expected: []string{"bar", "bor", "bir"}, + }, + { + desc: "should return a slice when tag contains one value", + name: "foo", + tags: []string{"traefik.foo=bar"}, + expected: []string{"bar"}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getSliceAttribute(test.name, test.tags) + + assert.Equal(t, test.expected, result) + }) + } +} + +func TestCatalogProviderGetFrontendRule(t *testing.T) { testCases := []struct { desc string service serviceUpdate @@ -386,15 +592,15 @@ func TestGetFrontendRule(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - provider := &CatalogProvider{ + p := &CatalogProvider{ Domain: "localhost", Prefix: "traefik", FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", frontEndRuleTemplate: template.New("consul catalog frontend rule"), } - provider.setupFrontEndRuleTemplate() + p.setupFrontEndRuleTemplate() - actual := provider.getFrontendRule(test.service) + actual := p.getFrontendRule(test.service) assert.Equal(t, test.expected, actual) }) } @@ -443,7 +649,7 @@ func TestGetBackendAddress(t *testing.T) { } } -func TestGetBackendName(t *testing.T) { +func TestCatalogProviderGetServerName(t *testing.T) { testCases := []struct { desc string node *api.ServiceEntry @@ -459,7 +665,7 @@ func TestGetBackendName(t *testing.T) { Tags: []string{}, }, }, - expected: "api--10-0-0-1--80--0", + expected: "api-0-eUSiqD6uNvvh6zxsY-OeRi8ZbaE", }, { desc: "Should create backend name with multiple tags", @@ -471,7 +677,7 @@ func TestGetBackendName(t *testing.T) { Tags: []string{"traefik.weight=42", "traefik.enable=true"}, }, }, - expected: "api--10-0-0-1--80--traefik-weight-42--traefik-enable-true--1", + expected: "api-1-eJ8MR2JxjXyZgs1bhurVa0-9OI8", }, { desc: "Should create backend name with one tag", @@ -483,7 +689,7 @@ func TestGetBackendName(t *testing.T) { Tags: []string{"a funny looking tag"}, }, }, - expected: "api--10-0-0-1--80--a-funny-looking-tag--2", + expected: "api-2-lMCDCsG7sh0SCXOHo4oBOQB-9D4", }, } @@ -493,46 +699,17 @@ func TestGetBackendName(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := getBackendName(test.node, i) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetBasicAuth(t *testing.T) { - testCases := []struct { - desc string - tags []string - expected []string - }{ - { - desc: "label missing", - tags: []string{}, - expected: []string{}, - }, - { - desc: "label existing", - tags: []string{ - "traefik.frontend.auth.basic=user:password", - }, - expected: []string{"user:password"}, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - provider := &CatalogProvider{ - Prefix: "traefik", - } - actual := provider.getBasicAuth(test.tags) + actual := getServerName(test.node, i) assert.Equal(t, test.expected, actual) }) } } func TestHasStickinessLabel(t *testing.T) { + p := &CatalogProvider{ + Prefix: "traefik", + } + testCases := []struct { desc string tags []string @@ -564,8 +741,208 @@ func TestHasStickinessLabel(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := hasStickinessLabel(test.tags) + actual := p.hasStickinessLabel(test.tags) assert.Equal(t, test.expected, actual) }) } } + +func TestCatalogProviderGetCircuitBreaker(t *testing.T) { + p := &CatalogProvider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + tags []string + expected *types.CircuitBreaker + }{ + { + desc: "should return nil when no tags", + tags: []string{}, + expected: nil, + }, + { + desc: "should return a struct when has tag", + tags: []string{label.Prefix + label.SuffixBackendCircuitBreaker + "=foo"}, + expected: &types.CircuitBreaker{ + Expression: "foo", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getCircuitBreaker(test.tags) + + assert.Equal(t, test.expected, result) + }) + } +} + +func TestCatalogProviderGetLoadBalancer(t *testing.T) { + p := &CatalogProvider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + tags []string + expected *types.LoadBalancer + }{ + { + desc: "should return a default struct when no tags", + tags: []string{}, + expected: &types.LoadBalancer{ + Method: "wrr", + }, + }, + { + desc: "should return a struct when has Method tag", + tags: []string{label.Prefix + "backend.loadbalancer" + "=drr"}, + expected: &types.LoadBalancer{ + Method: "drr", + }, + }, + { + desc: "should return a struct when has Sticky tag", + tags: []string{ + label.Prefix + label.SuffixBackendLoadBalancerSticky + "=true", + }, + expected: &types.LoadBalancer{ + Method: "wrr", + Sticky: true, + }, + }, + { + desc: "should skip Sticky when Sticky tag has invalid value", + tags: []string{ + label.Prefix + label.SuffixBackendLoadBalancerSticky + "=goo", + }, + expected: &types.LoadBalancer{ + Method: "wrr", + }, + }, + { + desc: "should return a struct when has Stickiness tag", + tags: []string{ + label.Prefix + label.SuffixBackendLoadBalancerStickiness + "=true", + }, + expected: &types.LoadBalancer{ + Method: "wrr", + Stickiness: &types.Stickiness{}, + }, + }, + { + desc: "should skip Stickiness when Stickiness tag has invalid value", + tags: []string{ + label.Prefix + label.SuffixBackendLoadBalancerStickiness + "=goo", + }, + expected: &types.LoadBalancer{ + Method: "wrr", + }, + }, + { + desc: "should return a struct when has Stickiness tag", + tags: []string{ + label.Prefix + label.SuffixBackendLoadBalancerStickiness + "=true", + label.Prefix + label.SuffixBackendLoadBalancerStickinessCookieName + "=bar", + }, + expected: &types.LoadBalancer{ + Method: "wrr", + Stickiness: &types.Stickiness{ + CookieName: "bar", + }, + }, + }, + { + desc: "should skip Stickiness when Stickiness tag has false as value", + tags: []string{ + label.Prefix + label.SuffixBackendLoadBalancerStickiness + "=false", + label.Prefix + label.SuffixBackendLoadBalancerStickinessCookieName + "=bar", + }, + expected: &types.LoadBalancer{ + Method: "wrr", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getLoadBalancer(test.tags) + + assert.Equal(t, test.expected, result) + }) + } +} + +func TestCatalogProviderGetMaxConn(t *testing.T) { + p := &CatalogProvider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + tags []string + expected *types.MaxConn + }{ + { + desc: "should return nil when no tags", + tags: []string{}, + expected: nil, + }, + { + desc: "should return a struct when Amount & ExtractorFunc tags", + tags: []string{ + label.Prefix + label.SuffixBackendMaxConnAmount + "=10", + label.Prefix + label.SuffixBackendMaxConnExtractorFunc + "=bar", + }, + expected: &types.MaxConn{ + ExtractorFunc: "bar", + Amount: 10, + }, + }, + { + desc: "should return nil when Amount tags is missing", + tags: []string{ + label.Prefix + label.SuffixBackendMaxConnExtractorFunc + "=bar", + }, + expected: nil, + }, + { + desc: "should return nil when ExtractorFunc tags is empty", + tags: []string{ + label.Prefix + label.SuffixBackendMaxConnAmount + "=10", + label.Prefix + label.SuffixBackendMaxConnExtractorFunc + "=", + }, + expected: nil, + }, + { + desc: "should return a struct when ExtractorFunc tags is missing", + tags: []string{ + label.Prefix + label.SuffixBackendMaxConnAmount + "=10", + }, + expected: &types.MaxConn{ + ExtractorFunc: label.DefaultBackendMaxconnExtractorFunc, + Amount: 10, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getMaxConn(test.tags) + + assert.Equal(t, test.expected, result) + }) + } +} diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index eedb79359..4eca85c20 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -2,55 +2,56 @@ {{range $service := .Services}} {{$sname := $service.ServiceName}} - {{$circuitBreaker := getAttribute "backend.circuitbreaker" $service.Attributes ""}} - {{with $circuitBreaker}} - [backends."backend-{{$sname}}".circuitBreaker] - expression = "{{$circuitBreaker}}" + {{ $circuitBreaker := getCircuitBreaker $service.Attributes }} + {{if $circuitBreaker }} + [backends."backend-{{ $sname }}".circuitBreaker] + expression = "{{ $circuitBreaker.Expression }}" {{end}} - [backends."backend-{{$sname}}".loadBalancer] - method = "{{getAttribute "backend.loadbalancer" $service.Attributes "wrr"}}" - sticky = {{getSticky $service.Attributes}} - {{if hasStickinessLabel $service.Attributes}} - [backends."backend-{{$sname}}".loadBalancer.stickiness] - cookieName = "{{getStickinessCookieName $service.Attributes}}" + {{ $loadBalancer := getLoadBalancer $service.Attributes }} + {{if $loadBalancer }} + [backends."backend-{{ $sname }}".loadBalancer] + method = "{{ $loadBalancer.Method }}" + sticky = {{ $loadBalancer.Sticky }} + {{if $loadBalancer.Stickiness }} + [backends."backend-{{ $sname }}".loadBalancer.stickiness] + cookieName = "{{ $loadBalancer.Stickiness.CookieName }}" {{end}} + {{end}} - {{if hasMaxconnAttributes $service.Attributes}} - [backends."backend-{{$sname}}".maxConn] - amount = {{getAttribute "backend.maxconn.amount" $service.Attributes "" }} - extractorFunc = "{{getAttribute "backend.maxconn.extractorfunc" $service.Attributes "" }}" + {{ $maxConn := getMaxConn $service.Attributes }} + {{if $maxConn }} + [backends."backend-{{ $sname }}".maxConn] + extractorFunc = "{{ $maxConn.ExtractorFunc }}" + amount = {{ $maxConn.Amount }} {{end}} {{end}} {{range $index, $node := .Nodes}} - [backends."backend-{{getBackend $node}}".servers."{{getBackendName $node $index}}"] - url = "{{getAttribute "protocol" $node.Service.Tags "http"}}://{{getBackendAddress $node}}:{{$node.Service.Port}}" - weight = {{ getAttribute "backend.weight" $node.Service.Tags "0" }} + [backends."backend-{{ getBackend $node }}".servers."{{ getServerName $node $index }}"] + url = "{{ getProtocol $node.Service.Tags }}://{{ getBackendAddress $node }}:{{ $node.Service.Port }}" + weight = {{ getWeight $node.Service.Tags }} {{end}} [frontends] {{range $service := .Services}} - [frontends."frontend-{{$service.ServiceName}}"] - backend = "backend-{{$service.ServiceName}}" - priority = {{getAttribute "frontend.priority" $service.Attributes "0"}} - passHostHeader = {{getAttribute "frontend.passHostHeader" $service.Attributes "true"}} + [frontends."frontend-{{ $service.ServiceName }}"] + backend = "backend-{{ $service.ServiceName }}" + priority = {{ getPriority $service.Attributes }} + passHostHeader = {{ getPassHostHeader $service.Attributes }} - {{$entryPoints := getAttribute "frontend.entrypoints" $service.Attributes ""}} - {{with $entryPoints}} - entryPoints = [{{range getEntryPoints $entryPoints}} + entryPoints = [{{range getFrontEndEntryPoints $service.Attributes }} "{{.}}", {{end}}] - {{end}} - basicAuth = [{{range getBasicAuth $service.Attributes}} + basicAuth = [{{range getBasicAuth $service.Attributes }} "{{.}}", {{end}}] [frontends."frontend-{{$service.ServiceName}}".routes."route-host-{{$service.ServiceName}}"] - rule = "{{getFrontendRule $service}}" + rule = "{{ getFrontendRule $service }}" {{end}}