diff --git a/configuration/configuration.go b/configuration/configuration.go index f3fb3516c..10c3557d9 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -246,6 +246,15 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) { } } + if gc.ConsulCatalog != nil { + if len(gc.ConsulCatalog.Filename) != 0 && gc.ConsulCatalog.TemplateVersion != 2 { + log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.") + gc.ConsulCatalog.TemplateVersion = 1 + } else { + gc.ConsulCatalog.TemplateVersion = 2 + } + } + if gc.Rancher != nil { if len(gc.Rancher.Filename) != 0 && gc.Rancher.TemplateVersion != 2 { log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.") diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 4fd4ef4df..fdf35b41a 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/containous/traefik/integration/try" + "github.com/containous/traefik/provider/label" "github.com/go-check/check" "github.com/hashicorp/consul/api" checker "github.com/vdemeester/shakers" @@ -160,7 +161,6 @@ func (s *ConsulCatalogSuite) TestSingleService(c *check.C) { s.deregisterService("test", whoami.NetworkSettings.IPAddress) err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusNotFound), try.HasBody()) c.Assert(err, checker.IsNil) - } func (s *ConsulCatalogSuite) TestExposedByDefaultFalseSingleService(c *check.C) { @@ -202,13 +202,12 @@ func (s *ConsulCatalogSuite) TestExposedByDefaultFalseSimpleServiceMultipleNode( defer cmd.Process.Kill() whoami := s.composeProject.Container(c, "whoami1") - whoami2 := s.composeProject.Container(c, "whoami2") - err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress) - err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 80, []string{"traefik.enable=true"}) + whoami2 := s.composeProject.Container(c, "whoami2") + err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 80, []string{label.TraefikEnable + "=true"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami2.NetworkSettings.IPAddress) @@ -326,7 +325,7 @@ func (s *ConsulCatalogSuite) TestBasicAuthSimpleService(c *check.C) { whoami := s.composeProject.Container(c, "whoami1") err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{ - "traefik.frontend.auth.basic=test:$2a$06$O5NksJPAcgrC9MuANkSoE.Xe9DSg7KcLLFYNr1Lj6hPcMmvgwxhme,test2:$2y$10$xP1SZ70QbZ4K2bTGKJOhpujkpcLxQcB3kEPF6XAV19IdcqsZTyDEe", + label.TraefikFrontendAuthBasic + "=test:$2a$06$O5NksJPAcgrC9MuANkSoE.Xe9DSg7KcLLFYNr1Lj6hPcMmvgwxhme,test2:$2y$10$xP1SZ70QbZ4K2bTGKJOhpujkpcLxQcB3kEPF6XAV19IdcqsZTyDEe", }) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress) @@ -362,7 +361,8 @@ func (s *ConsulCatalogSuite) TestRefreshConfigTagChange(c *check.C) { whoami := s.composeProject.Container(c, "whoami1") - err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=false", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, + []string{"name=whoami1", label.TraefikEnable + "=false", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress) @@ -370,7 +370,8 @@ func (s *ConsulCatalogSuite) TestRefreshConfigTagChange(c *check.C) { try.BodyContains(whoami.NetworkSettings.IPAddress)) c.Assert(err, checker.NotNil) - err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true", "traefik.backend.circuitbreaker=ResponseCodeRatio(500, 600, 0, 600) > 0.5"}) + err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, + []string{"name=whoami1", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=ResponseCodeRatio(500, 600, 0, 600) > 0.5"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) @@ -403,16 +404,20 @@ func (s *ConsulCatalogSuite) TestCircuitBreaker(c *check.C) { defer cmd.Process.Kill() whoami := s.composeProject.Container(c, "whoami1") - whoami2 := s.composeProject.Container(c, "whoami2") - whoami3 := s.composeProject.Container(c, "whoami3") - - err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, + []string{"name=whoami1", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress) - err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 42, []string{"name=whoami2", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + + whoami2 := s.composeProject.Container(c, "whoami2") + err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 42, + []string{"name=whoami2", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami2.NetworkSettings.IPAddress) - err = s.registerService("test", whoami3.NetworkSettings.IPAddress, 42, []string{"name=whoami3", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + + whoami3 := s.composeProject.Container(c, "whoami3") + err = s.registerService("test", whoami3.NetworkSettings.IPAddress, 42, + []string{"name=whoami3", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami3.NetworkSettings.IPAddress) @@ -452,7 +457,7 @@ func (s *ConsulCatalogSuite) TestRefreshConfigPortChange(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, try.BodyContains(whoami.NetworkSettings.IPAddress)) c.Assert(err, checker.IsNil) - err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true"}) + err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", label.TraefikEnable + "=true"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress) diff --git a/provider/consulcatalog/config.go b/provider/consulcatalog/config.go new file mode 100644 index 000000000..ea5bea174 --- /dev/null +++ b/provider/consulcatalog/config.go @@ -0,0 +1,273 @@ +package consulcatalog + +import ( + "bytes" + "crypto/sha1" + "encoding/base64" + "fmt" + "sort" + "strconv" + "strings" + "text/template" + + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/hashicorp/consul/api" +) + +func (p *Provider) buildConfigurationV2(catalog []catalogUpdate) *types.Configuration { + var funcMap = template.FuncMap{ + "getAttribute": p.getAttribute, + "getTag": getTag, + "hasTag": hasTag, + + // Backend functions + "getNodeBackendName": getNodeBackendName, + "getServiceBackendName": getServiceBackendName, + "getBackendAddress": getBackendAddress, + "getServerName": getServerName, + "getCircuitBreaker": getCircuitBreaker, + "getLoadBalancer": getLoadBalancer, + "getMaxConn": label.GetMaxConn, + "getHealthCheck": label.GetHealthCheck, + "getBuffering": label.GetBuffering, + "getServer": p.getServer, + + // Frontend functions + "getFrontendRule": p.getFrontendRule, + "getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), + "getFrontEndEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints), + "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getWhiteList": label.GetWhiteList, + "getRedirect": label.GetRedirect, + "getErrorPages": label.GetErrorPages, + "getRateLimit": label.GetRateLimit, + "getHeaders": label.GetHeaders, + } + + var allNodes []*api.ServiceEntry + var services []*serviceUpdate + for _, info := range catalog { + if len(info.Nodes) > 0 { + services = append(services, info.Service) + allNodes = append(allNodes, info.Nodes...) + } + } + // Ensure a stable ordering of nodes so that identical configurations may be detected + sort.Sort(nodeSorter(allNodes)) + + templateObjects := struct { + Services []*serviceUpdate + Nodes []*api.ServiceEntry + }{ + Services: services, + Nodes: allNodes, + } + + configuration, err := p.GetConfiguration("templates/consul_catalog.tmpl", funcMap, templateObjects) + if err != nil { + log.WithError(err).Error("Failed to create config") + } + + return configuration +} + +// Specific functions + +func (p *Provider) getFrontendRule(service serviceUpdate) string { + customFrontendRule := label.GetStringValue(service.TraefikLabels, label.TraefikFrontendRule, "") + if customFrontendRule == "" { + customFrontendRule = p.FrontEndRule + } + + tmpl := p.frontEndRuleTemplate + tmpl, err := tmpl.Parse(customFrontendRule) + if err != nil { + log.Errorf("Failed to parse Consul Catalog custom frontend rule: %v", err) + return "" + } + + templateObjects := struct { + ServiceName string + Domain string + Attributes []string + }{ + ServiceName: service.ServiceName, + Domain: p.Domain, + Attributes: service.Attributes, + } + + var buffer bytes.Buffer + err = tmpl.Execute(&buffer, templateObjects) + if err != nil { + log.Errorf("Failed to execute Consul Catalog custom frontend rule template: %v", err) + return "" + } + + return buffer.String() +} + +func (p *Provider) getServer(node *api.ServiceEntry) types.Server { + scheme := p.getAttribute(label.SuffixProtocol, node.Service.Tags, label.DefaultProtocol) + address := getBackendAddress(node) + + return types.Server{ + URL: fmt.Sprintf("%s://%s:%d", scheme, address, node.Service.Port), + Weight: p.getWeight(node.Service.Tags), + } +} + +func (p *Provider) setupFrontEndRuleTemplate() { + var FuncMap = template.FuncMap{ + "getAttribute": p.getAttribute, + "getTag": getTag, + "hasTag": hasTag, + } + p.frontEndRuleTemplate = template.New("consul catalog frontend rule").Funcs(FuncMap) +} + +// Specific functions + +// Only for compatibility +// Deprecated +func getLoadBalancer(labels map[string]string) *types.LoadBalancer { + if v, ok := labels[label.TraefikBackendLoadBalancer]; ok { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancer, label.TraefikBackendLoadBalancerMethod) + if !label.Has(labels, label.TraefikBackendLoadBalancerMethod) { + labels[label.TraefikBackendLoadBalancerMethod] = v + } + } + + return label.GetLoadBalancer(labels) +} + +// Only for compatibility +// Deprecated +func getCircuitBreaker(labels map[string]string) *types.CircuitBreaker { + if v, ok := labels[label.TraefikBackendCircuitBreaker]; ok { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendCircuitBreaker, label.TraefikBackendCircuitBreakerExpression) + if !label.Has(labels, label.TraefikBackendCircuitBreakerExpression) { + labels[label.TraefikBackendCircuitBreakerExpression] = v + } + } + + return label.GetCircuitBreaker(labels) +} + +func getServiceBackendName(service *serviceUpdate) string { + return strings.ToLower(service.ServiceName) +} + +func getNodeBackendName(node *api.ServiceEntry) string { + return strings.ToLower(node.Service.Service) +} + +func getBackendAddress(node *api.ServiceEntry) string { + if node.Service.Address != "" { + return node.Service.Address + } + return node.Node.Address +} + +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, "") + + hash := sha1.New() + _, err := hash.Write([]byte(serviceName)) + if err != nil { + // Impossible case + log.Error(err) + } else { + serviceName = base64.URLEncoding.EncodeToString(hash.Sum(nil)) + } + + // unique int at the end + return provider.Normalize(node.Service.Service + "-" + strconv.Itoa(index) + "-" + serviceName) +} + +func (p *Provider) getWeight(tags []string) int { + weight := p.getIntAttribute(label.SuffixWeight, tags, label.DefaultWeightInt) + + // 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, label.DefaultWeightInt) + } + + return weight +} + +// Base functions + +func (p *Provider) hasAttribute(name string, tags []string) bool { + return hasTag(p.getPrefixedName(name), tags) +} + +func (p *Provider) getAttribute(name string, tags []string, defaultValue string) string { + return getTag(p.getPrefixedName(name), tags, defaultValue) +} + +func (p *Provider) getPrefixedName(name string) string { + if len(p.Prefix) > 0 && len(name) > 0 { + return p.Prefix + "." + name + } + return name +} + +func hasTag(name string, tags []string) bool { + lowerName := strings.ToLower(name) + + for _, tag := range tags { + lowerTag := strings.ToLower(tag) + + // Given the nature of Consul tags, which could be either singular markers, or key=value pairs + if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName { + return true + } + } + return false +} + +func hasTagPrefix(name string, tags []string) bool { + lowerName := strings.ToLower(name) + + for _, tag := range tags { + lowerTag := strings.ToLower(tag) + + if strings.HasPrefix(lowerTag, lowerName) { + return true + } + } + return false +} + +func getTag(name string, tags []string, defaultValue string) string { + lowerName := strings.ToLower(name) + + for _, tag := range tags { + lowerTag := strings.ToLower(tag) + + // Given the nature of Consul tags, which could be either singular markers, or key=value pairs + if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName { + // In case, where a tag might be a key=value, try to split it by the first '=' + kv := strings.SplitN(tag, "=", 2) + + // If the returned result is a key=value pair, return the 'value' component + if len(kv) == 2 { + return kv[1] + } + // If the returned result is a singular marker, return the 'key' component + return kv[0] + } + } + return defaultValue +} diff --git a/provider/consulcatalog/config_root.go b/provider/consulcatalog/config_root.go new file mode 100644 index 000000000..fc2e14889 --- /dev/null +++ b/provider/consulcatalog/config_root.go @@ -0,0 +1,10 @@ +package consulcatalog + +import "github.com/containous/traefik/types" + +func (p *Provider) buildConfiguration(catalog []catalogUpdate) *types.Configuration { + if p.TemplateVersion == 1 { + return p.buildConfigurationV1(catalog) + } + return p.buildConfigurationV2(catalog) +} diff --git a/provider/consulcatalog/config_test.go b/provider/consulcatalog/config_test.go new file mode 100644 index 000000000..960f8a139 --- /dev/null +++ b/provider/consulcatalog/config_test.go @@ -0,0 +1,518 @@ +package consulcatalog + +import ( + "testing" + "text/template" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/assert" +) + +func TestProviderBuildConfiguration(t *testing.T) { + p := &Provider{ + Domain: "localhost", + Prefix: "traefik", + ExposedByDefault: false, + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), + } + + testCases := []struct { + desc string + nodes []catalogUpdate + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "Should build config of nothing", + nodes: []catalogUpdate{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "Should build config with no frontend and backend", + nodes: []catalogUpdate{ + { + Service: &serviceUpdate{ + ServiceName: "test", + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "Should build config who contains one frontend and one backend", + nodes: []catalogUpdate{ + { + Service: &serviceUpdate{ + ServiceName: "test", + Attributes: []string{ + "random.foo=bar", + label.TraefikBackendLoadBalancerMethod + "=drr", + label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5", + label.TraefikBackendMaxConnAmount + "=1000", + label.TraefikBackendMaxConnExtractorFunc + "=client.ip", + label.TraefikFrontendAuthBasic + "=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + }, + Nodes: []*api.ServiceEntry{ + { + Service: &api.AgentService{ + Service: "test", + Address: "127.0.0.1", + Port: 80, + Tags: []string{ + "random.foo=bar", + label.Prefix + "backend.weight=42", // Deprecated label + label.TraefikFrontendPassHostHeader + "=true", + label.TraefikProtocol + "=https", + }, + }, + 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:test.localhost", + }, + }, + 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-0-us4-27hAOu2ARV7nNrmv6GoKlcA": { + URL: "https://127.0.0.1:80", + Weight: 42, + }, + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + MaxConn: &types.MaxConn{ + Amount: 1000, + ExtractorFunc: "client.ip", + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + nodes := fakeLoadTraefikLabelsSlice(test.nodes, p.Prefix) + + actualConfig := p.buildConfigurationV2(nodes) + assert.NotNil(t, actualConfig) + assert.Equal(t, test.expectedBackends, actualConfig.Backends) + assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + +func TestGetTag(t *testing.T) { + testCases := []struct { + desc string + tags []string + key string + defaultValue string + expected string + }{ + { + desc: "Should return value of foo.bar key", + tags: []string{ + "foo.bar=random", + "traefik.backend.weight=42", + "management", + }, + key: "foo.bar", + defaultValue: "0", + expected: "random", + }, + { + desc: "Should return default value when nonexistent key", + tags: []string{ + "foo.bar.foo.bar=random", + "traefik.backend.weight=42", + "management", + }, + key: "foo.bar", + defaultValue: "0", + expected: "0", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getTag(test.key, test.tags, test.defaultValue) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestHasTag(t *testing.T) { + testCases := []struct { + desc string + name string + tags []string + expected bool + }{ + { + desc: "tag without value", + name: "foo", + tags: []string{"foo"}, + expected: true, + }, + { + desc: "tag with value", + name: "foo", + tags: []string{"foo=true"}, + expected: true, + }, + { + desc: "missing tag", + name: "foo", + tags: []string{"foobar=true"}, + expected: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := hasTag(test.name, test.tags) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestProviderGetPrefixedName(t *testing.T) { + testCases := []struct { + desc string + name string + prefix string + expected string + }{ + { + desc: "empty name with prefix", + name: "", + prefix: "foo", + expected: "", + }, + { + desc: "empty name without prefix", + name: "", + prefix: "", + expected: "", + }, + { + desc: "with prefix", + name: "bar", + prefix: "foo", + expected: "foo.bar", + }, + { + desc: "without prefix", + name: "bar", + prefix: "", + expected: "bar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{Prefix: test.prefix} + + actual := p.getPrefixedName(test.name) + assert.Equal(t, test.expected, actual) + }) + } + +} + +func TestProviderGetAttribute(t *testing.T) { + testCases := []struct { + desc string + tags []string + key string + defaultValue string + prefix string + expected string + }{ + { + desc: "Should return tag value 42", + prefix: "traefik", + tags: []string{ + "foo.bar=ramdom", + "traefik.backend.weight=42", + }, + key: "backend.weight", + defaultValue: "0", + expected: "42", + }, + { + desc: "Should return tag default value 0", + prefix: "traefik", + tags: []string{ + "foo.bar=ramdom", + "traefik.backend.wei=42", + }, + key: "backend.weight", + defaultValue: "0", + expected: "0", + }, + { + desc: "Should return tag value 42 when empty prefix", + tags: []string{ + "foo.bar=ramdom", + "backend.weight=42", + }, + key: "backend.weight", + defaultValue: "0", + expected: "42", + }, + { + desc: "Should return default value 0 when empty prefix", + tags: []string{ + "foo.bar=ramdom", + "backend.wei=42", + }, + key: "backend.weight", + defaultValue: "0", + expected: "0", + }, + { + desc: "Should return for.bar key value random when empty prefix", + tags: []string{ + "foo.bar=ramdom", + "backend.wei=42", + }, + key: "foo.bar", + defaultValue: "random", + expected: "ramdom", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + Domain: "localhost", + Prefix: test.prefix, + } + + actual := p.getAttribute(test.key, test.tags, test.defaultValue) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestProviderGetFrontendRule(t *testing.T) { + testCases := []struct { + desc string + service serviceUpdate + expected string + }{ + { + desc: "Should return default host foo.localhost", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{}, + }, + expected: "Host:foo.localhost", + }, + { + desc: "Should return host *.example.com", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=Host:*.example.com", + }, + }, + expected: "Host:*.example.com", + }, + { + desc: "Should return host foo.example.com", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=Host:{{.ServiceName}}.example.com", + }, + }, + expected: "Host:foo.example.com", + }, + { + desc: "Should return path prefix /bar", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=PathPrefix:{{getTag \"contextPath\" .Attributes \"/\"}}", + "contextPath=/bar", + }, + }, + expected: "PathPrefix:/bar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + Domain: "localhost", + Prefix: "traefik", + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), + } + p.setupFrontEndRuleTemplate() + + labels := tagsToNeutralLabels(test.service.Attributes, p.Prefix) + test.service.TraefikLabels = labels + + actual := p.getFrontendRule(test.service) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetBackendAddress(t *testing.T) { + testCases := []struct { + desc string + node *api.ServiceEntry + expected string + }{ + { + desc: "Should return the address of the service", + node: &api.ServiceEntry{ + Node: &api.Node{ + Address: "10.1.0.1", + }, + Service: &api.AgentService{ + Address: "10.2.0.1", + }, + }, + expected: "10.2.0.1", + }, + { + desc: "Should return the address of the node", + node: &api.ServiceEntry{ + Node: &api.Node{ + Address: "10.1.0.1", + }, + Service: &api.AgentService{ + Address: "", + }, + }, + expected: "10.1.0.1", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getBackendAddress(test.node) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetServerName(t *testing.T) { + testCases := []struct { + desc string + node *api.ServiceEntry + expected string + }{ + { + desc: "Should create backend name without tags", + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{}, + }, + }, + expected: "api-0-eUSiqD6uNvvh6zxsY-OeRi8ZbaE", + }, + { + desc: "Should create backend name with multiple tags", + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{"traefik.weight=42", "traefik.enable=true"}, + }, + }, + expected: "api-1-eJ8MR2JxjXyZgs1bhurVa0-9OI8", + }, + { + desc: "Should create backend name with one tag", + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{"a funny looking tag"}, + }, + }, + expected: "api-2-lMCDCsG7sh0SCXOHo4oBOQB-9D4", + }, + } + + for i, test := range testCases { + test := test + i := i + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getServerName(test.node, i) + assert.Equal(t, test.expected, actual) + }) + } +} + +func fakeLoadTraefikLabelsSlice(nodes []catalogUpdate, prefix string) []catalogUpdate { + var result []catalogUpdate + + for _, node := range nodes { + labels := tagsToNeutralLabels(node.Service.Attributes, prefix) + node.Service.TraefikLabels = labels + result = append(result, node) + } + + return result +} diff --git a/provider/consulcatalog/consul_catalog.go b/provider/consulcatalog/consul_catalog.go index e79fe09d4..65c44ac49 100644 --- a/provider/consulcatalog/consul_catalog.go +++ b/provider/consulcatalog/consul_catalog.go @@ -2,6 +2,7 @@ package consulcatalog import ( "errors" + "strconv" "strings" "sync" "text/template" @@ -48,8 +49,9 @@ type Service struct { } type serviceUpdate struct { - ServiceName string - Attributes []string + ServiceName string + Attributes []string + TraefikLabels map[string]string } type catalogUpdate struct { @@ -446,10 +448,13 @@ func (p *Provider) healthyNodes(service string) (catalogUpdate, error) { ).(map[string]bool)).([]string) }, []string{}, nodes).([]string) + labels := tagsToNeutralLabels(tags, p.Prefix) + return catalogUpdate{ Service: &serviceUpdate{ - ServiceName: service, - Attributes: tags, + ServiceName: service, + Attributes: tags, + TraefikLabels: labels, }, Nodes: nodes, }, nil @@ -473,7 +478,18 @@ func (p *Provider) nodeFilter(service string, node *api.ServiceEntry) bool { } func (p *Provider) isServiceEnabled(node *api.ServiceEntry) bool { - return p.getBoolAttribute(label.SuffixEnable, node.Service.Tags, p.ExposedByDefault) + rawValue := getTag(p.getPrefixedName(label.SuffixEnable), node.Service.Tags, "") + + if len(rawValue) == 0 { + return p.ExposedByDefault + } + + value, err := strconv.ParseBool(rawValue) + if err != nil { + log.Errorf("Invalid value for %s: %s", label.SuffixEnable, rawValue) + return p.ExposedByDefault + } + return value } func (p *Provider) getConstraintTags(tags []string) []string { diff --git a/provider/consulcatalog/consul_catalog_config.go b/provider/consulcatalog/consul_catalog_config.go deleted file mode 100644 index 14ab512b6..000000000 --- a/provider/consulcatalog/consul_catalog_config.go +++ /dev/null @@ -1,589 +0,0 @@ -package consulcatalog - -import ( - "bytes" - "crypto/sha1" - "encoding/base64" - "math" - "sort" - "strconv" - "strings" - "text/template" - - "github.com/containous/traefik/log" - "github.com/containous/traefik/provider" - "github.com/containous/traefik/provider/label" - "github.com/containous/traefik/types" - "github.com/hashicorp/consul/api" -) - -func (p *Provider) buildConfiguration(catalog []catalogUpdate) *types.Configuration { - var FuncMap = template.FuncMap{ - "getAttribute": p.getAttribute, - "getTag": getTag, - "hasTag": hasTag, - - // Backend functions - "getBackend": getNodeBackendName, // TODO Deprecated [breaking] getBackend -> getNodeBackendName - "getNodeBackendName": getNodeBackendName, - "getServiceBackendName": getServiceBackendName, - "getBackendAddress": getBackendAddress, - "getBackendName": getServerName, // TODO Deprecated [breaking] getBackendName -> getServerName - "getServerName": getServerName, - "hasMaxconnAttributes": p.hasMaxConnAttributes, // TODO Deprecated [breaking] - "getSticky": p.getSticky, // TODO Deprecated [breaking] - "hasStickinessLabel": p.hasStickinessLabel, // TODO Deprecated [breaking] - "getStickinessCookieName": p.getStickinessCookieName, // TODO Deprecated [breaking] - "getWeight": p.getWeight, // TODO 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, - "getHealthCheck": p.getHealthCheck, - "getBuffering": p.getBuffering, - - // Frontend functions - "getFrontendRule": p.getFrontendRule, - "getBasicAuth": p.getFuncSliceAttribute(label.SuffixFrontendAuthBasic), - "getEntryPoints": getEntryPoints, // TODO Deprecated [breaking] - "getFrontEndEntryPoints": p.getFuncSliceAttribute(label.SuffixFrontendEntryPoints), // TODO [breaking] rename to getEntryPoints when getEntryPoints will be removed - "getPriority": p.getFuncIntAttribute(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt), - "getPassHostHeader": p.getFuncBoolAttribute(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), - "getPassTLSCert": p.getFuncBoolAttribute(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), - "getWhiteList": p.getWhiteList, - "getRedirect": p.getRedirect, - "hasErrorPages": p.getFuncHasAttributePrefix(label.BaseFrontendErrorPage), - "getErrorPages": p.getErrorPages, - "hasRateLimit": p.getFuncHasAttributePrefix(label.BaseFrontendRateLimit), - "getRateLimit": p.getRateLimit, - "getHeaders": p.getHeaders, - } - - var allNodes []*api.ServiceEntry - var services []*serviceUpdate - for _, info := range catalog { - if len(info.Nodes) > 0 { - services = append(services, info.Service) - allNodes = append(allNodes, info.Nodes...) - } - } - // Ensure a stable ordering of nodes so that identical configurations may be detected - sort.Sort(nodeSorter(allNodes)) - - templateObjects := struct { - Services []*serviceUpdate - Nodes []*api.ServiceEntry - }{ - Services: services, - Nodes: allNodes, - } - - configuration, err := p.GetConfiguration("templates/consul_catalog.tmpl", FuncMap, templateObjects) - if err != nil { - log.WithError(err).Error("Failed to create config") - } - - return configuration -} - -func (p *Provider) setupFrontEndRuleTemplate() { - var FuncMap = template.FuncMap{ - "getAttribute": p.getAttribute, - "getTag": getTag, - "hasTag": hasTag, - } - tmpl := template.New("consul catalog frontend rule").Funcs(FuncMap) - p.frontEndRuleTemplate = tmpl -} - -// Specific functions - -func (p *Provider) getFrontendRule(service serviceUpdate) string { - customFrontendRule := p.getAttribute(label.SuffixFrontendRule, service.Attributes, "") - if customFrontendRule == "" { - customFrontendRule = p.FrontEndRule - } - - tmpl := p.frontEndRuleTemplate - tmpl, err := tmpl.Parse(customFrontendRule) - if err != nil { - log.Errorf("Failed to parse Consul Catalog custom frontend rule: %v", err) - return "" - } - - templateObjects := struct { - ServiceName string - Domain string - Attributes []string - }{ - ServiceName: service.ServiceName, - Domain: p.Domain, - Attributes: service.Attributes, - } - - var buffer bytes.Buffer - err = tmpl.Execute(&buffer, templateObjects) - if err != nil { - log.Errorf("Failed to execute Consul Catalog custom frontend rule template: %v", err) - return "" - } - - return buffer.String() -} - -// Deprecated -func (p *Provider) hasMaxConnAttributes(attributes []string) bool { - amount := p.getAttribute(label.SuffixBackendMaxConnAmount, attributes, "") - extractorFunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, attributes, "") - return amount != "" && extractorFunc != "" -} - -// Deprecated -func getEntryPoints(list string) []string { - return strings.Split(list, ",") -} - -func getNodeBackendName(node *api.ServiceEntry) string { - return strings.ToLower(node.Service.Service) -} - -func getServiceBackendName(service *serviceUpdate) string { - return strings.ToLower(service.ServiceName) -} - -func getBackendAddress(node *api.ServiceEntry) string { - if node.Service.Address != "" { - return node.Service.Address - } - return node.Node.Address -} - -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, "") - - hash := sha1.New() - _, err := hash.Write([]byte(serviceName)) - if err != nil { - // Impossible case - log.Error(err) - } else { - serviceName = base64.URLEncoding.EncodeToString(hash.Sum(nil)) - } - - // unique int at the end - return provider.Normalize(node.Service.Service + "-" + strconv.Itoa(index) + "-" + serviceName) -} - -// TODO: Deprecated -// replaced by Stickiness -// Deprecated -func (p *Provider) getSticky(tags []string) string { - stickyTag := p.getAttribute(label.SuffixBackendLoadBalancerSticky, tags, "") - if len(stickyTag) > 0 { - log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) - } else { - stickyTag = "false" - } - return stickyTag -} - -// Deprecated -func (p *Provider) hasStickinessLabel(tags []string) bool { - stickinessTag := p.getAttribute(label.SuffixBackendLoadBalancerStickiness, tags, "") - return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true") -} - -// Deprecated -func (p *Provider) getStickinessCookieName(tags []string) string { - return p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, "") -} - -// Deprecated -func (p *Provider) getWeight(tags []string) int { - weight := p.getIntAttribute(label.SuffixWeight, tags, label.DefaultWeightInt) - - // 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, label.DefaultWeightInt) - } - - return weight -} - -func (p *Provider) 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 *Provider) 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 *Provider) 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, - } -} - -func (p *Provider) getHealthCheck(tags []string) *types.HealthCheck { - path := p.getAttribute(label.SuffixBackendHealthCheckPath, tags, "") - - if len(path) == 0 { - return nil - } - - port := p.getIntAttribute(label.SuffixBackendHealthCheckPort, tags, label.DefaultBackendHealthCheckPort) - interval := p.getAttribute(label.SuffixBackendHealthCheckInterval, tags, "") - - return &types.HealthCheck{ - Path: path, - Port: port, - Interval: interval, - } -} - -func (p *Provider) getBuffering(tags []string) *types.Buffering { - if !p.hasAttributePrefix(label.SuffixBackendBuffering, tags) { - return nil - } - - return &types.Buffering{ - MaxRequestBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMaxRequestBodyBytes, tags, 0), - MaxResponseBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMaxResponseBodyBytes, tags, 0), - MemRequestBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMemRequestBodyBytes, tags, 0), - MemResponseBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMemResponseBodyBytes, tags, 0), - RetryExpression: p.getAttribute(label.SuffixBackendBufferingRetryExpression, tags, ""), - } -} - -func (p *Provider) getWhiteList(tags []string) *types.WhiteList { - ranges := p.getSliceAttribute(label.SuffixFrontendWhiteListSourceRange, tags) - - if len(ranges) > 0 { - return &types.WhiteList{ - SourceRange: ranges, - UseXForwardedFor: p.getBoolAttribute(label.SuffixFrontendWhiteListUseXForwardedFor, tags, false), - } - } - - return nil -} - -func (p *Provider) getRedirect(tags []string) *types.Redirect { - permanent := p.getBoolAttribute(label.SuffixFrontendRedirectPermanent, tags, false) - - if p.hasAttribute(label.SuffixFrontendRedirectEntryPoint, tags) { - return &types.Redirect{ - EntryPoint: p.getAttribute(label.SuffixFrontendRedirectEntryPoint, tags, ""), - Permanent: permanent, - } - } - - if p.hasAttribute(label.SuffixFrontendRedirectRegex, tags) && p.hasAttribute(label.SuffixFrontendRedirectReplacement, tags) { - return &types.Redirect{ - Regex: p.getAttribute(label.SuffixFrontendRedirectRegex, tags, ""), - Replacement: p.getAttribute(label.SuffixFrontendRedirectReplacement, tags, ""), - Permanent: permanent, - } - } - - return nil -} - -func (p *Provider) getErrorPages(tags []string) map[string]*types.ErrorPage { - labels := p.parseTagsToNeutralLabels(tags) - - prefix := label.Prefix + label.BaseFrontendErrorPage - return label.ParseErrorPages(labels, prefix, label.RegexpFrontendErrorPage) -} - -func (p *Provider) getRateLimit(tags []string) *types.RateLimit { - extractorFunc := p.getAttribute(label.SuffixFrontendRateLimitExtractorFunc, tags, "") - if len(extractorFunc) == 0 { - return nil - } - - labels := p.parseTagsToNeutralLabels(tags) - - prefix := label.Prefix + label.BaseFrontendRateLimit - limits := label.ParseRateSets(labels, prefix, label.RegexpFrontendRateLimit) - - return &types.RateLimit{ - ExtractorFunc: extractorFunc, - RateSet: limits, - } -} - -func (p *Provider) getHeaders(tags []string) *types.Headers { - headers := &types.Headers{ - CustomRequestHeaders: p.getMapAttribute(label.SuffixFrontendRequestHeaders, tags), - CustomResponseHeaders: p.getMapAttribute(label.SuffixFrontendResponseHeaders, tags), - SSLProxyHeaders: p.getMapAttribute(label.SuffixFrontendHeadersSSLProxyHeaders, tags), - AllowedHosts: p.getSliceAttribute(label.SuffixFrontendHeadersAllowedHosts, tags), - HostsProxyHeaders: p.getSliceAttribute(label.SuffixFrontendHeadersHostsProxyHeaders, tags), - SSLHost: p.getAttribute(label.SuffixFrontendHeadersSSLHost, tags, ""), - CustomFrameOptionsValue: p.getAttribute(label.SuffixFrontendHeadersCustomFrameOptionsValue, tags, ""), - ContentSecurityPolicy: p.getAttribute(label.SuffixFrontendHeadersContentSecurityPolicy, tags, ""), - PublicKey: p.getAttribute(label.SuffixFrontendHeadersPublicKey, tags, ""), - ReferrerPolicy: p.getAttribute(label.SuffixFrontendHeadersReferrerPolicy, tags, ""), - CustomBrowserXSSValue: p.getAttribute(label.SuffixFrontendHeadersCustomBrowserXSSValue, tags, ""), - STSSeconds: p.getInt64Attribute(label.SuffixFrontendHeadersSTSSeconds, tags, 0), - SSLRedirect: p.getBoolAttribute(label.SuffixFrontendHeadersSSLRedirect, tags, false), - SSLTemporaryRedirect: p.getBoolAttribute(label.SuffixFrontendHeadersSSLTemporaryRedirect, tags, false), - STSIncludeSubdomains: p.getBoolAttribute(label.SuffixFrontendHeadersSTSIncludeSubdomains, tags, false), - STSPreload: p.getBoolAttribute(label.SuffixFrontendHeadersSTSPreload, tags, false), - ForceSTSHeader: p.getBoolAttribute(label.SuffixFrontendHeadersForceSTSHeader, tags, false), - FrameDeny: p.getBoolAttribute(label.SuffixFrontendHeadersFrameDeny, tags, false), - ContentTypeNosniff: p.getBoolAttribute(label.SuffixFrontendHeadersContentTypeNosniff, tags, false), - BrowserXSSFilter: p.getBoolAttribute(label.SuffixFrontendHeadersBrowserXSSFilter, tags, false), - IsDevelopment: p.getBoolAttribute(label.SuffixFrontendHeadersIsDevelopment, tags, false), - } - - if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { - return nil - } - - return headers -} - -// Base functions - -func (p *Provider) parseTagsToNeutralLabels(tags []string) map[string]string { - var labels map[string]string - - for _, tag := range tags { - if strings.HasPrefix(tag, p.Prefix) { - - parts := strings.SplitN(tag, "=", 2) - if len(parts) == 2 { - if labels == nil { - labels = make(map[string]string) - } - - // replace custom prefix by the generic prefix - key := label.Prefix + strings.TrimPrefix(parts[0], p.Prefix+".") - labels[key] = parts[1] - } - } - } - - return labels -} - -func (p *Provider) getFuncStringAttribute(name string, defaultValue string) func(tags []string) string { - return func(tags []string) string { - return p.getAttribute(name, tags, defaultValue) - } -} - -func (p *Provider) getFuncSliceAttribute(name string) func(tags []string) []string { - return func(tags []string) []string { - return p.getSliceAttribute(name, tags) - } -} - -func (p *Provider) getMapAttribute(name string, tags []string) map[string]string { - rawValue := getTag(p.getPrefixedName(name), tags, "") - - if len(rawValue) == 0 { - return nil - } - - return label.ParseMapValue(p.getPrefixedName(name), rawValue) -} - -func (p *Provider) getFuncIntAttribute(name string, defaultValue int) func(tags []string) int { - return func(tags []string) int { - return p.getIntAttribute(name, tags, defaultValue) - } -} - -func (p *Provider) getFuncBoolAttribute(name string, defaultValue bool) func(tags []string) bool { - return func(tags []string) bool { - return p.getBoolAttribute(name, tags, defaultValue) - } -} - -func (p *Provider) getFuncHasAttributePrefix(name string) func(tags []string) bool { - return func(tags []string) bool { - return p.hasAttributePrefix(name, tags) - } -} - -func (p *Provider) 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 *Provider) 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 *Provider) getSliceAttribute(name string, tags []string) []string { - rawValue := getTag(p.getPrefixedName(name), tags, "") - - if len(rawValue) == 0 { - return nil - } - return label.SplitAndTrimString(rawValue, ",") -} - -func (p *Provider) getBoolAttribute(name string, tags []string, defaultValue bool) bool { - rawValue := getTag(p.getPrefixedName(name), tags, "") - - if len(rawValue) == 0 { - return defaultValue - } - - value, err := strconv.ParseBool(rawValue) - if err != nil { - log.Errorf("Invalid value for %s: %s", name, rawValue) - return defaultValue - } - return value -} - -func (p *Provider) hasAttribute(name string, tags []string) bool { - return hasTag(p.getPrefixedName(name), tags) -} - -func (p *Provider) hasAttributePrefix(name string, tags []string) bool { - return hasTagPrefix(p.getPrefixedName(name), tags) -} - -func (p *Provider) getAttribute(name string, tags []string, defaultValue string) string { - return getTag(p.getPrefixedName(name), tags, defaultValue) -} - -func (p *Provider) getPrefixedName(name string) string { - if len(p.Prefix) > 0 && len(name) > 0 { - return p.Prefix + "." + name - } - return name -} - -func hasTag(name string, tags []string) bool { - lowerName := strings.ToLower(name) - - for _, tag := range tags { - lowerTag := strings.ToLower(tag) - - // Given the nature of Consul tags, which could be either singular markers, or key=value pairs - if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName { - return true - } - } - return false -} - -func hasTagPrefix(name string, tags []string) bool { - lowerName := strings.ToLower(name) - - for _, tag := range tags { - lowerTag := strings.ToLower(tag) - - if strings.HasPrefix(lowerTag, lowerName) { - return true - } - } - return false -} - -func getTag(name string, tags []string, defaultValue string) string { - lowerName := strings.ToLower(name) - - for _, tag := range tags { - lowerTag := strings.ToLower(tag) - - // Given the nature of Consul tags, which could be either singular markers, or key=value pairs - if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName { - // In case, where a tag might be a key=value, try to split it by the first '=' - kv := strings.SplitN(tag, "=", 2) - - // If the returned result is a key=value pair, return the 'value' component - if len(kv) == 2 { - return kv[1] - } - // If the returned result is a singular marker, return the 'key' component - return kv[0] - } - - } - return defaultValue -} diff --git a/provider/consulcatalog/consul_catalog_config_test.go b/provider/consulcatalog/consul_catalog_config_test.go deleted file mode 100644 index 8f8b475d0..000000000 --- a/provider/consulcatalog/consul_catalog_config_test.go +++ /dev/null @@ -1,1387 +0,0 @@ -package consulcatalog - -import ( - "testing" - "text/template" - "time" - - "github.com/containous/flaeg" - "github.com/containous/traefik/provider/label" - "github.com/containous/traefik/types" - "github.com/hashicorp/consul/api" - "github.com/stretchr/testify/assert" -) - -func TestProviderBuildConfiguration(t *testing.T) { - provider := &Provider{ - Domain: "localhost", - Prefix: "traefik", - ExposedByDefault: false, - FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", - frontEndRuleTemplate: template.New("consul catalog frontend rule"), - } - - testCases := []struct { - desc string - nodes []catalogUpdate - expectedFrontends map[string]*types.Frontend - expectedBackends map[string]*types.Backend - }{ - { - desc: "Should build config of nothing", - nodes: []catalogUpdate{}, - expectedFrontends: map[string]*types.Frontend{}, - expectedBackends: map[string]*types.Backend{}, - }, - { - desc: "Should build config with no frontend and backend", - nodes: []catalogUpdate{ - { - Service: &serviceUpdate{ - ServiceName: "test", - }, - }, - }, - expectedFrontends: map[string]*types.Frontend{}, - expectedBackends: map[string]*types.Backend{}, - }, - { - desc: "Should build config who contains one frontend and one backend", - nodes: []catalogUpdate{ - { - Service: &serviceUpdate{ - ServiceName: "test", - Attributes: []string{ - "random.foo=bar", - 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{ - { - Service: &api.AgentService{ - Service: "test", - Address: "127.0.0.1", - Port: 80, - Tags: []string{ - "random.foo=bar", - label.Prefix + "backend.weight=42", - label.TraefikFrontendPassHostHeader + "=true", - label.TraefikProtocol + "=https", - }, - }, - 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:test.localhost", - }, - }, - 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-0-us4-27hAOu2ARV7nNrmv6GoKlcA": { - URL: "https://127.0.0.1:80", - Weight: 42, - }, - }, - CircuitBreaker: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - LoadBalancer: &types.LoadBalancer{ - Method: "drr", - }, - MaxConn: &types.MaxConn{ - Amount: 1000, - ExtractorFunc: "client.ip", - }, - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actualConfig := provider.buildConfiguration(test.nodes) - assert.NotNil(t, actualConfig) - assert.Equal(t, test.expectedBackends, actualConfig.Backends) - assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) - }) - } -} - -func TestGetTag(t *testing.T) { - testCases := []struct { - desc string - tags []string - key string - defaultValue string - expected string - }{ - { - desc: "Should return value of foo.bar key", - tags: []string{ - "foo.bar=random", - "traefik.backend.weight=42", - "management", - }, - key: "foo.bar", - defaultValue: "0", - expected: "random", - }, - { - desc: "Should return default value when nonexistent key", - tags: []string{ - "foo.bar.foo.bar=random", - "traefik.backend.weight=42", - "management", - }, - key: "foo.bar", - defaultValue: "0", - expected: "0", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getTag(test.key, test.tags, test.defaultValue) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestHasTag(t *testing.T) { - testCases := []struct { - desc string - name string - tags []string - expected bool - }{ - { - desc: "tag without value", - name: "foo", - tags: []string{"foo"}, - expected: true, - }, - { - desc: "tag with value", - name: "foo", - tags: []string{"foo=true"}, - expected: true, - }, - { - desc: "missing tag", - name: "foo", - tags: []string{"foobar=true"}, - expected: false, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := hasTag(test.name, test.tags) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestProviderGetPrefixedName(t *testing.T) { - testCases := []struct { - desc string - name string - prefix string - expected string - }{ - { - desc: "empty name with prefix", - name: "", - prefix: "foo", - expected: "", - }, - { - desc: "empty name without prefix", - name: "", - prefix: "", - expected: "", - }, - { - desc: "with prefix", - name: "bar", - prefix: "foo", - expected: "foo.bar", - }, - { - desc: "without prefix", - name: "bar", - prefix: "", - expected: "bar", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - pro := &Provider{Prefix: test.prefix} - - actual := pro.getPrefixedName(test.name) - assert.Equal(t, test.expected, actual) - }) - } - -} - -func TestProviderGetAttribute(t *testing.T) { - testCases := []struct { - desc string - tags []string - key string - defaultValue string - prefix string - expected string - }{ - { - desc: "Should return tag value 42", - prefix: "traefik", - tags: []string{ - "foo.bar=ramdom", - "traefik.backend.weight=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "42", - }, - { - desc: "Should return tag default value 0", - prefix: "traefik", - tags: []string{ - "foo.bar=ramdom", - "traefik.backend.wei=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "0", - }, - { - desc: "Should return tag value 42 when empty prefix", - tags: []string{ - "foo.bar=ramdom", - "backend.weight=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "42", - }, - { - desc: "Should return default value 0 when empty prefix", - tags: []string{ - "foo.bar=ramdom", - "backend.wei=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "0", - }, - { - desc: "Should return for.bar key value random when empty prefix", - tags: []string{ - "foo.bar=ramdom", - "backend.wei=42", - }, - key: "foo.bar", - defaultValue: "random", - expected: "ramdom", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - p := &Provider{ - Domain: "localhost", - Prefix: test.prefix, - } - - actual := p.getAttribute(test.key, test.tags, test.defaultValue) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestProviderGetIntAttribute(t *testing.T) { - p := &Provider{ - 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"}, - defaultValue: 666, - expected: 666, - }, - { - 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 TestProviderGetInt64Attribute(t *testing.T) { - p := &Provider{ - 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"}, - defaultValue: 666, - expected: 666, - }, - { - 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 TestProviderGetBoolAttribute(t *testing.T) { - p := &Provider{ - 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=true"}, - defaultValue: true, - expected: true, - }, - { - 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 TestProviderGetSliceAttribute(t *testing.T) { - p := &Provider{ - 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 TestProviderGetFrontendRule(t *testing.T) { - testCases := []struct { - desc string - service serviceUpdate - expected string - }{ - { - desc: "Should return default host foo.localhost", - service: serviceUpdate{ - ServiceName: "foo", - Attributes: []string{}, - }, - expected: "Host:foo.localhost", - }, - { - desc: "Should return host *.example.com", - service: serviceUpdate{ - ServiceName: "foo", - Attributes: []string{ - "traefik.frontend.rule=Host:*.example.com", - }, - }, - expected: "Host:*.example.com", - }, - { - desc: "Should return host foo.example.com", - service: serviceUpdate{ - ServiceName: "foo", - Attributes: []string{ - "traefik.frontend.rule=Host:{{.ServiceName}}.example.com", - }, - }, - expected: "Host:foo.example.com", - }, - { - desc: "Should return path prefix /bar", - service: serviceUpdate{ - ServiceName: "foo", - Attributes: []string{ - "traefik.frontend.rule=PathPrefix:{{getTag \"contextPath\" .Attributes \"/\"}}", - "contextPath=/bar", - }, - }, - expected: "PathPrefix:/bar", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - p := &Provider{ - Domain: "localhost", - Prefix: "traefik", - FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", - frontEndRuleTemplate: template.New("consul catalog frontend rule"), - } - p.setupFrontEndRuleTemplate() - - actual := p.getFrontendRule(test.service) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetBackendAddress(t *testing.T) { - testCases := []struct { - desc string - node *api.ServiceEntry - expected string - }{ - { - desc: "Should return the address of the service", - node: &api.ServiceEntry{ - Node: &api.Node{ - Address: "10.1.0.1", - }, - Service: &api.AgentService{ - Address: "10.2.0.1", - }, - }, - expected: "10.2.0.1", - }, - { - desc: "Should return the address of the node", - node: &api.ServiceEntry{ - Node: &api.Node{ - Address: "10.1.0.1", - }, - Service: &api.AgentService{ - Address: "", - }, - }, - expected: "10.1.0.1", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getBackendAddress(test.node) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestProviderGetServerName(t *testing.T) { - testCases := []struct { - desc string - node *api.ServiceEntry - expected string - }{ - { - desc: "Should create backend name without tags", - node: &api.ServiceEntry{ - Service: &api.AgentService{ - Service: "api", - Address: "10.0.0.1", - Port: 80, - Tags: []string{}, - }, - }, - expected: "api-0-eUSiqD6uNvvh6zxsY-OeRi8ZbaE", - }, - { - desc: "Should create backend name with multiple tags", - node: &api.ServiceEntry{ - Service: &api.AgentService{ - Service: "api", - Address: "10.0.0.1", - Port: 80, - Tags: []string{"traefik.weight=42", "traefik.enable=true"}, - }, - }, - expected: "api-1-eJ8MR2JxjXyZgs1bhurVa0-9OI8", - }, - { - desc: "Should create backend name with one tag", - node: &api.ServiceEntry{ - Service: &api.AgentService{ - Service: "api", - Address: "10.0.0.1", - Port: 80, - Tags: []string{"a funny looking tag"}, - }, - }, - expected: "api-2-lMCDCsG7sh0SCXOHo4oBOQB-9D4", - }, - } - - for i, test := range testCases { - test := test - i := i - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getServerName(test.node, i) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestHasStickinessLabel(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected bool - }{ - { - desc: "label missing", - tags: []string{}, - expected: false, - }, - { - desc: "stickiness=true", - tags: []string{ - label.TraefikBackendLoadBalancerStickiness + "=true", - }, - expected: true, - }, - { - desc: "stickiness=false", - tags: []string{ - label.TraefikBackendLoadBalancerStickiness + "=false", - }, - expected: false, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := p.hasStickinessLabel(test.tags) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestProviderGetCircuitBreaker(t *testing.T) { - p := &Provider{ - 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 TestProviderGetLoadBalancer(t *testing.T) { - p := &Provider{ - 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 TestProviderGetMaxConn(t *testing.T) { - p := &Provider{ - 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) - }) - } -} - -func TestProviderGetHealthCheck(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.HealthCheck - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return nil when Path tag is missing", - tags: []string{ - label.TraefikBackendHealthCheckPort + "=80", - label.TraefikBackendHealthCheckInterval + "=7", - }, - expected: nil, - }, - { - desc: "should return a struct when has tags", - tags: []string{ - label.TraefikBackendHealthCheckPath + "=/health", - label.TraefikBackendHealthCheckPort + "=80", - label.TraefikBackendHealthCheckInterval + "=7", - }, - expected: &types.HealthCheck{ - Path: "/health", - Port: 80, - Interval: "7", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getHealthCheck(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetBuffering(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.Buffering - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return a struct when has proper tags", - tags: []string{ - label.TraefikBackendBufferingMaxResponseBodyBytes + "=10485760", - label.TraefikBackendBufferingMemResponseBodyBytes + "=2097152", - label.TraefikBackendBufferingMaxRequestBodyBytes + "=10485760", - label.TraefikBackendBufferingMemRequestBodyBytes + "=2097152", - label.TraefikBackendBufferingRetryExpression + "=IsNetworkError() && Attempts() <= 2", - }, - expected: &types.Buffering{ - MaxResponseBodyBytes: 10485760, - MemResponseBodyBytes: 2097152, - MaxRequestBodyBytes: 10485760, - MemRequestBodyBytes: 2097152, - RetryExpression: "IsNetworkError() && Attempts() <= 2", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getBuffering(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderWhiteList(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.WhiteList - }{ - { - desc: "should return nil when no white list labels", - expected: nil, - }, - { - desc: "should return a struct when only range", - tags: []string{ - label.TraefikFrontendWhiteListSourceRange + "=10.10.10.10", - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: false, - }, - }, - { - desc: "should return a struct when range and UseXForwardedFor", - tags: []string{ - label.TraefikFrontendWhiteListSourceRange + "=10.10.10.10", - label.TraefikFrontendWhiteListUseXForwardedFor + "=true", - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: true, - }, - }, - { - desc: "should return nil when only UseXForwardedFor", - tags: []string{ - label.TraefikFrontendWhiteListUseXForwardedFor + "=true", - }, - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := p.getWhiteList(test.tags) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestProviderGetRedirect(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.Redirect - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should use only entry point tag when mix regex redirect and entry point redirect", - tags: []string{ - label.TraefikFrontendRedirectEntryPoint + "=https", - label.TraefikFrontendRedirectRegex + "=(.*)", - label.TraefikFrontendRedirectReplacement + "=$1", - }, - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect tag", - tags: []string{ - label.TraefikFrontendRedirectEntryPoint + "=https", - }, - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect tags (permanent)", - tags: []string{ - label.TraefikFrontendRedirectEntryPoint + "=https", - label.TraefikFrontendRedirectPermanent + "=true", - }, - expected: &types.Redirect{ - EntryPoint: "https", - Permanent: true, - }, - }, - { - desc: "should return a struct when regex redirect tags", - tags: []string{ - label.TraefikFrontendRedirectRegex + "=(.*)", - label.TraefikFrontendRedirectReplacement + "=$1", - }, - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - }, - }, - { - desc: "should return a struct when regex redirect tags (permanent)", - tags: []string{ - label.TraefikFrontendRedirectRegex + "=(.*)", - label.TraefikFrontendRedirectReplacement + "=$1", - label.TraefikFrontendRedirectPermanent + "=true", - }, - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - Permanent: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getRedirect(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetErrorPages(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected map[string]*types.ErrorPage - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return a map when tags are present", - tags: []string{ - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus + "=404", - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend + "=foo_backend", - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery + "=foo_query", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus + "=500,600", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend + "=bar_backend", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery + "=bar_query", - }, - expected: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - Query: "foo_query", - Backend: "foo_backend", - }, - "bar": { - Status: []string{"500", "600"}, - Query: "bar_query", - Backend: "bar_backend", - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getErrorPages(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetRateLimit(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.RateLimit - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return a map when tags are present", - tags: []string{ - label.TraefikFrontendRateLimitExtractorFunc + "=client.ip", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod + "=6", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage + "=12", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst + "=18", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod + "=3", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage + "=6", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst + "=9", - }, - expected: &types.RateLimit{ - ExtractorFunc: "client.ip", - RateSet: map[string]*types.Rate{ - "foo": { - Period: flaeg.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - "bar": { - Period: flaeg.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getRateLimit(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetHeaders(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.Headers - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return a struct when has tags", - tags: []string{ - label.TraefikFrontendRequestHeaders + "=Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendResponseHeaders + "=Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendSSLProxyHeaders + "=Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendAllowedHosts + "=foo,bar,bor", - label.TraefikFrontendHostsProxyHeaders + "=foo,bar,bor", - label.TraefikFrontendSSLHost + "=foo", - label.TraefikFrontendCustomFrameOptionsValue + "=foo", - label.TraefikFrontendContentSecurityPolicy + "=foo", - label.TraefikFrontendPublicKey + "=foo", - label.TraefikFrontendReferrerPolicy + "=foo", - label.TraefikFrontendCustomBrowserXSSValue + "=foo", - label.TraefikFrontendSTSSeconds + "=666", - label.TraefikFrontendSSLRedirect + "=true", - label.TraefikFrontendSSLTemporaryRedirect + "=true", - label.TraefikFrontendSTSIncludeSubdomains + "=true", - label.TraefikFrontendSTSPreload + "=true", - label.TraefikFrontendForceSTSHeader + "=true", - label.TraefikFrontendFrameDeny + "=true", - label.TraefikFrontendContentTypeNosniff + "=true", - label.TraefikFrontendBrowserXSSFilter + "=true", - label.TraefikFrontendIsDevelopment + "=true", - }, - expected: &types.Headers{ - CustomRequestHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - CustomResponseHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - SSLProxyHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - AllowedHosts: []string{"foo", "bar", "bor"}, - HostsProxyHeaders: []string{"foo", "bar", "bor"}, - SSLHost: "foo", - CustomFrameOptionsValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - CustomBrowserXSSValue: "foo", - STSSeconds: 666, - SSLRedirect: true, - SSLTemporaryRedirect: true, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - ContentTypeNosniff: true, - BrowserXSSFilter: true, - IsDevelopment: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getHeaders(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} diff --git a/provider/consulcatalog/convert_types.go b/provider/consulcatalog/convert_types.go new file mode 100644 index 000000000..edb320087 --- /dev/null +++ b/provider/consulcatalog/convert_types.go @@ -0,0 +1,29 @@ +package consulcatalog + +import ( + "strings" + + "github.com/containous/traefik/provider/label" +) + +func tagsToNeutralLabels(tags []string, prefix string) map[string]string { + var labels map[string]string + + for _, tag := range tags { + if strings.HasPrefix(tag, prefix) { + + parts := strings.SplitN(tag, "=", 2) + if len(parts) == 2 { + if labels == nil { + labels = make(map[string]string) + } + + // replace custom prefix by the generic prefix + key := label.Prefix + strings.TrimPrefix(parts[0], prefix+".") + labels[key] = parts[1] + } + } + } + + return labels +} diff --git a/provider/consulcatalog/convert_types_test.go b/provider/consulcatalog/convert_types_test.go new file mode 100644 index 000000000..b0c24b02d --- /dev/null +++ b/provider/consulcatalog/convert_types_test.go @@ -0,0 +1,64 @@ +package consulcatalog + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTagsToNeutralLabels(t *testing.T) { + testCases := []struct { + desc string + tags []string + prefix string + expected map[string]string + }{ + { + desc: "without tags", + expected: nil, + }, + { + desc: "with a prefix", + prefix: "test", + tags: []string{ + "test.aaa=01", + "test.bbb=02", + "ccc=03", + "test.ddd=04=to", + }, + expected: map[string]string{ + "traefik.aaa": "01", + "traefik.bbb": "02", + "traefik.ddd": "04=to", + }, + }, + + { + desc: "with an empty prefix", + prefix: "", + tags: []string{ + "test.aaa=01", + "test.bbb=02", + "ccc=03", + "test.ddd=04=to", + }, + expected: map[string]string{ + "traefik.test.aaa": "01", + "traefik.test.bbb": "02", + "traefik.ccc": "03", + "traefik.test.ddd": "04=to", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + labels := tagsToNeutralLabels(test.tags, test.prefix) + + assert.Equal(t, test.expected, labels) + }) + } +} diff --git a/provider/consulcatalog/deprecated_config.go b/provider/consulcatalog/deprecated_config.go new file mode 100644 index 000000000..181674dc1 --- /dev/null +++ b/provider/consulcatalog/deprecated_config.go @@ -0,0 +1,250 @@ +package consulcatalog + +import ( + "bytes" + "sort" + "strconv" + "strings" + "text/template" + + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/hashicorp/consul/api" +) + +// Deprecated +func (p *Provider) buildConfigurationV1(catalog []catalogUpdate) *types.Configuration { + var FuncMap = template.FuncMap{ + "getAttribute": p.getAttribute, + "getTag": getTag, + "hasTag": hasTag, + + // Backend functions + "getBackend": getNodeBackendName, + "getServiceBackendName": getServiceBackendName, + "getBackendAddress": getBackendAddress, + "getBackendName": getServerName, + "hasMaxconnAttributes": p.hasMaxConnAttributesV1, + "getSticky": p.getStickyV1, + "hasStickinessLabel": p.hasStickinessLabelV1, + "getStickinessCookieName": p.getStickinessCookieNameV1, + "getWeight": p.getWeight, + "getProtocol": p.getFuncStringAttribute(label.SuffixProtocol, label.DefaultProtocol), + + // Frontend functions + "getFrontendRule": p.getFrontendRuleV1, + "getBasicAuth": p.getFuncSliceAttribute(label.SuffixFrontendAuthBasic), + "getEntryPoints": getEntryPointsV1, + "getPriority": p.getFuncIntAttribute(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": p.getFuncBoolAttribute(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": p.getFuncBoolAttribute(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), + } + + var allNodes []*api.ServiceEntry + var services []*serviceUpdate + for _, info := range catalog { + if len(info.Nodes) > 0 { + services = append(services, info.Service) + allNodes = append(allNodes, info.Nodes...) + } + } + // Ensure a stable ordering of nodes so that identical configurations may be detected + sort.Sort(nodeSorter(allNodes)) + + templateObjects := struct { + Services []*serviceUpdate + Nodes []*api.ServiceEntry + }{ + Services: services, + Nodes: allNodes, + } + + configuration, err := p.GetConfiguration("templates/consul_catalog-v1.tmpl", FuncMap, templateObjects) + if err != nil { + log.WithError(err).Error("Failed to create config") + } + + return configuration +} + +// Specific functions + +// Deprecated +func (p *Provider) getFrontendRuleV1(service serviceUpdate) string { + customFrontendRule := p.getAttribute(label.SuffixFrontendRule, service.Attributes, "") + if customFrontendRule == "" { + customFrontendRule = p.FrontEndRule + } + + tmpl := p.frontEndRuleTemplate + tmpl, err := tmpl.Parse(customFrontendRule) + if err != nil { + log.Errorf("Failed to parse Consul Catalog custom frontend rule: %v", err) + return "" + } + + templateObjects := struct { + ServiceName string + Domain string + Attributes []string + }{ + ServiceName: service.ServiceName, + Domain: p.Domain, + Attributes: service.Attributes, + } + + var buffer bytes.Buffer + err = tmpl.Execute(&buffer, templateObjects) + if err != nil { + log.Errorf("Failed to execute Consul Catalog custom frontend rule template: %v", err) + return "" + } + + return buffer.String() +} + +// Deprecated +func (p *Provider) hasMaxConnAttributesV1(attributes []string) bool { + amount := p.getAttribute(label.SuffixBackendMaxConnAmount, attributes, "") + extractorFunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, attributes, "") + return amount != "" && extractorFunc != "" +} + +// Deprecated +func getEntryPointsV1(list string) []string { + return strings.Split(list, ",") +} + +// TODO: Deprecated +// replaced by Stickiness +// Deprecated +func (p *Provider) getStickyV1(tags []string) string { + stickyTag := p.getAttribute(label.SuffixBackendLoadBalancerSticky, tags, "") + if len(stickyTag) > 0 { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + } else { + stickyTag = "false" + } + return stickyTag +} + +// Deprecated +func (p *Provider) hasStickinessLabelV1(tags []string) bool { + stickinessTag := p.getAttribute(label.SuffixBackendLoadBalancerStickiness, tags, "") + return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true") +} + +// Deprecated +func (p *Provider) getStickinessCookieNameV1(tags []string) string { + return p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, "") +} + +// Base functions + +// Deprecated +func (p *Provider) getFuncStringAttribute(name string, defaultValue string) func(tags []string) string { + return func(tags []string) string { + return p.getAttribute(name, tags, defaultValue) + } +} + +// Deprecated +func (p *Provider) getFuncSliceAttribute(name string) func(tags []string) []string { + return func(tags []string) []string { + return p.getSliceAttribute(name, tags) + } +} + +// Deprecated +func (p *Provider) getMapAttribute(name string, tags []string) map[string]string { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return nil + } + + return label.ParseMapValue(p.getPrefixedName(name), rawValue) +} + +// Deprecated +func (p *Provider) getFuncIntAttribute(name string, defaultValue int) func(tags []string) int { + return func(tags []string) int { + return p.getIntAttribute(name, tags, defaultValue) + } +} + +func (p *Provider) getFuncBoolAttribute(name string, defaultValue bool) func(tags []string) bool { + return func(tags []string) bool { + return p.getBoolAttribute(name, tags, defaultValue) + } +} + +// Deprecated +func (p *Provider) getFuncHasAttributePrefix(name string) func(tags []string) bool { + return func(tags []string) bool { + return p.hasAttributePrefix(name, tags) + } +} + +// Deprecated +func (p *Provider) 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 +} + +// Deprecated +func (p *Provider) 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 +} + +// Deprecated +func (p *Provider) getSliceAttribute(name string, tags []string) []string { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return nil + } + return label.SplitAndTrimString(rawValue, ",") +} + +// Deprecated +func (p *Provider) getBoolAttribute(name string, tags []string, defaultValue bool) bool { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return defaultValue + } + + value, err := strconv.ParseBool(rawValue) + if err != nil { + log.Errorf("Invalid value for %s: %s", name, rawValue) + return defaultValue + } + return value +} + +func (p *Provider) hasAttributePrefix(name string, tags []string) bool { + return hasTagPrefix(p.getPrefixedName(name), tags) +} diff --git a/provider/consulcatalog/deprecated_config_test.go b/provider/consulcatalog/deprecated_config_test.go new file mode 100644 index 000000000..a5415a425 --- /dev/null +++ b/provider/consulcatalog/deprecated_config_test.go @@ -0,0 +1,444 @@ +package consulcatalog + +import ( + "testing" + "text/template" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/assert" +) + +func TestProviderBuildConfigurationV1(t *testing.T) { + p := &Provider{ + Domain: "localhost", + Prefix: "traefik", + ExposedByDefault: false, + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), + } + + testCases := []struct { + desc string + nodes []catalogUpdate + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "Should build config of nothing", + nodes: []catalogUpdate{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "Should build config with no frontend and backend", + nodes: []catalogUpdate{ + { + Service: &serviceUpdate{ + ServiceName: "test", + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "Should build config who contains one frontend and one backend", + nodes: []catalogUpdate{ + { + Service: &serviceUpdate{ + ServiceName: "test", + Attributes: []string{ + "random.foo=bar", + label.TraefikBackendLoadBalancer + "=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{ + { + Service: &api.AgentService{ + Service: "test", + Address: "127.0.0.1", + Port: 80, + Tags: []string{ + "random.foo=bar", + label.Prefix + "backend.weight=42", + label.TraefikFrontendPassHostHeader + "=true", + label.TraefikProtocol + "=https", + }, + }, + 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:test.localhost", + }, + }, + BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test": { + Servers: map[string]types.Server{ + "test-0-us4-27hAOu2ARV7nNrmv6GoKlcA": { + URL: "https://127.0.0.1:80", + Weight: 42, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + }, + MaxConn: &types.MaxConn{ + Amount: 1000, + ExtractorFunc: "client.ip", + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actualConfig := p.buildConfigurationV1(test.nodes) + assert.NotNil(t, actualConfig) + assert.Equal(t, test.expectedBackends, actualConfig.Backends) + assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + +func TestProviderGetIntAttributeV1(t *testing.T) { + p := &Provider{ + 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"}, + defaultValue: 666, + expected: 666, + }, + { + 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 TestProviderGetInt64AttributeV1(t *testing.T) { + p := &Provider{ + 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"}, + defaultValue: 666, + expected: 666, + }, + { + 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 TestProviderGetBoolAttributeV1(t *testing.T) { + p := &Provider{ + 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=true"}, + defaultValue: true, + expected: true, + }, + { + 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 TestProviderGetSliceAttributeV1(t *testing.T) { + p := &Provider{ + 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 TestProviderGetFrontendRuleV1(t *testing.T) { + testCases := []struct { + desc string + service serviceUpdate + expected string + }{ + { + desc: "Should return default host foo.localhost", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{}, + }, + expected: "Host:foo.localhost", + }, + { + desc: "Should return host *.example.com", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=Host:*.example.com", + }, + }, + expected: "Host:*.example.com", + }, + { + desc: "Should return host foo.example.com", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=Host:{{.ServiceName}}.example.com", + }, + }, + expected: "Host:foo.example.com", + }, + { + desc: "Should return path prefix /bar", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=PathPrefix:{{getTag \"contextPath\" .Attributes \"/\"}}", + "contextPath=/bar", + }, + }, + expected: "PathPrefix:/bar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + Domain: "localhost", + Prefix: "traefik", + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), + } + p.setupFrontEndRuleTemplate() + + actual := p.getFrontendRuleV1(test.service) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestHasStickinessLabelV1(t *testing.T) { + p := &Provider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + tags []string + expected bool + }{ + { + desc: "label missing", + tags: []string{}, + expected: false, + }, + { + desc: "stickiness=true", + tags: []string{ + label.TraefikBackendLoadBalancerStickiness + "=true", + }, + expected: true, + }, + { + desc: "stickiness=false", + tags: []string{ + label.TraefikBackendLoadBalancerStickiness + "=false", + }, + expected: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := p.hasStickinessLabelV1(test.tags) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/templates/consul_catalog-v1.tmpl b/templates/consul_catalog-v1.tmpl new file mode 100644 index 000000000..cb99198b4 --- /dev/null +++ b/templates/consul_catalog-v1.tmpl @@ -0,0 +1,56 @@ +[backends] +{{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" }} + {{with $weight }} + weight = {{ $weight }} + {{end}} +{{end}} + +{{range .Services }} + {{ $service := .ServiceName }} + + {{ $circuitBreaker := getAttribute "backend.circuitbreaker" .Attributes "" }} + {{with $circuitBreaker }} + [backends."backend-{{ $service }}".circuitbreaker] + expression = "{{ $circuitBreaker }}" + {{end}} + + [backends."backend-{{ $service }}".loadbalancer] + method = "{{ getAttribute "backend.loadbalancer" .Attributes "wrr" }}" + sticky = {{ getSticky .Attributes }} + {{if hasStickinessLabel .Attributes }} + [backends."backend-{{ $service }}".loadbalancer.stickiness] + cookieName = "{{ getStickinessCookieName .Attributes }}" + {{end}} + + {{if hasMaxconnAttributes .Attributes }} + [backends."backend-{{ $service }}".maxconn] + amount = {{ getAttribute "backend.maxconn.amount" .Attributes "" }} + extractorfunc = "{{ getAttribute "backend.maxconn.extractorfunc" .Attributes "" }}" + {{end}} + +{{end}} + +[frontends] +{{range .Services }} + [frontends."frontend-{{ .ServiceName }}"] + backend = "backend-{{ .ServiceName }}" + passHostHeader = {{ getAttribute "frontend.passHostHeader" .Attributes "true" }} + priority = {{ getAttribute "frontend.priority" .Attributes "0" }} + + {{ $entryPoints := getAttribute "frontend.entrypoints" .Attributes "" }} + {{with $entryPoints }} + entrypoints = [{{range getEntryPoints $entryPoints }} + "{{ . }}", + {{end}}] + {{end}} + + basicAuth = [{{range getBasicAuth .Attributes }} + "{{ . }}", + {{end}}] + + [frontends."frontend-{{ .ServiceName }}".routes."route-host-{{ .ServiceName }}"] + rule = "{{ getFrontendRule . }}" +{{end}} diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index 7638965f7..cea3bd5c6 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -2,13 +2,13 @@ {{range $service := .Services}} {{ $backendName := getServiceBackendName $service }} - {{ $circuitBreaker := getCircuitBreaker $service.Attributes }} + {{ $circuitBreaker := getCircuitBreaker $service.TraefikLabels }} {{if $circuitBreaker }} [backends."backend-{{ $backendName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $service.Attributes }} + {{ $loadBalancer := getLoadBalancer $service.TraefikLabels }} {{if $loadBalancer }} [backends."backend-{{ $backendName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -19,14 +19,14 @@ {{end}} {{end}} - {{ $maxConn := getMaxConn $service.Attributes }} + {{ $maxConn := getMaxConn $service.TraefikLabels }} {{if $maxConn }} [backends."backend-{{ $backendName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $service.Attributes }} + {{ $healthCheck := getHealthCheck $service.TraefikLabels }} {{if $healthCheck }} [backends."backend-{{ $backendName }}".healthCheck] path = "{{ $healthCheck.Path }}" @@ -34,7 +34,7 @@ interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $service.Attributes }} + {{ $buffering := getBuffering $service.TraefikLabels }} {{if $buffering }} [backends."backend-{{ $backendName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -46,10 +46,10 @@ {{end}} {{range $index, $node := .Nodes}} - + {{ $server := getServer $node }} [backends."backend-{{ getNodeBackendName $node }}".servers."{{ getServerName $node $index }}"] - url = "{{ getProtocol $node.Service.Tags }}://{{ getBackendAddress $node }}:{{ $node.Service.Port }}" - weight = {{ getWeight $node.Service.Tags }} + url = "{{ $server.URL }}" + weight = {{ $server.Weight }} {{end}} @@ -58,19 +58,19 @@ [frontends."frontend-{{ $service.ServiceName }}"] backend = "backend-{{ getServiceBackendName $service }}" - priority = {{ getPriority $service.Attributes }} - passHostHeader = {{ getPassHostHeader $service.Attributes }} - passTLSCert = {{ getPassTLSCert $service.Attributes }} + priority = {{ getPriority $service.TraefikLabels }} + passHostHeader = {{ getPassHostHeader $service.TraefikLabels }} + passTLSCert = {{ getPassTLSCert $service.TraefikLabels }} - entryPoints = [{{range getFrontEndEntryPoints $service.Attributes }} + entryPoints = [{{range getFrontEndEntryPoints $service.TraefikLabels }} "{{.}}", {{end}}] - basicAuth = [{{range getBasicAuth $service.Attributes }} + basicAuth = [{{range getBasicAuth $service.TraefikLabels }} "{{.}}", {{end}}] - {{ $whitelist := getWhiteList $service.Attributes }} + {{ $whitelist := getWhiteList $service.TraefikLabels }} {{if $whitelist }} [frontends."frontend-{{ $service.ServiceName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} @@ -79,7 +79,7 @@ useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $service.Attributes }} + {{ $redirect := getRedirect $service.TraefikLabels }} {{if $redirect }} [frontends."frontend-{{ $service.ServiceName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -88,9 +88,10 @@ permanent = {{ $redirect.Permanent }} {{end}} - {{if hasErrorPages $service.Attributes }} + {{ $errorPages := getErrorPages $service.TraefikLabels }} + {{if $errorPages }} [frontends."frontend-{{ $service.ServiceName }}".errors] - {{range $pageName, $page := getErrorPages $service.Attributes }} + {{range $pageName, $page := $errorPages }} [frontends."frontend-{{ $service.ServiceName }}".errors."{{ $pageName }}"] status = [{{range $page.Status }} "{{.}}", @@ -100,22 +101,20 @@ {{end}} {{end}} - {{if hasRateLimit $service.Attributes }} - {{ $rateLimit := getRateLimit $service.Attributes }} + {{ $rateLimit := getRateLimit $service.TraefikLabels }} + {{if $rateLimit }} [frontends."frontend-{{ $service.ServiceName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" - [frontends."frontend-{{ $service.ServiceName }}".rateLimit.rateSet] - {{range $limitName, $limit := $rateLimit.RateSet }} + {{ range $limitName, $limit := $rateLimit.RateSet }} [frontends."frontend-{{ $service.ServiceName }}".rateLimit.rateSet."{{ $limitName }}"] period = "{{ $limit.Period }}" average = {{ $limit.Average }} burst = {{ $limit.Burst }} {{end}} - {{end}} - {{ $headers := getHeaders $service.Attributes }} + {{ $headers := getHeaders $service.TraefikLabels }} {{if $headers }} [frontends."frontend-{{ $service.ServiceName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }}