diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index 5ea91f283..b6c1db4f4 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -2,7 +2,6 @@ package consul import ( "errors" - "strconv" "strings" "text/template" "time" @@ -95,7 +94,7 @@ func (p *CatalogProvider) Provide(configurationChan chan<- types.ConfigMessage, } p.client = client p.Constraints = append(p.Constraints, constraints...) - p.setupFrontEndTemplate() + p.setupFrontEndRuleTemplate() pool.Go(func(stop chan bool) { notify := func(err error, time time.Duration) { @@ -431,48 +430,7 @@ func (p *CatalogProvider) nodeFilter(service string, node *api.ServiceEntry) boo } func (p *CatalogProvider) isServiceEnabled(node *api.ServiceEntry) bool { - enable, err := strconv.ParseBool(p.getAttribute(label.SuffixEnable, node.Service.Tags, strconv.FormatBool(p.ExposedByDefault))) - if err != nil { - log.Debugf("Invalid value for enable, set to %b", p.ExposedByDefault) - return p.ExposedByDefault - } - return enable -} - -func (p *CatalogProvider) getPrefixedName(name string) string { - if len(p.Prefix) > 0 && len(name) > 0 { - return p.Prefix + "." + name - } - return name -} - -func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string { - return getTag(p.getPrefixedName(name), tags, defaultValue) -} - -func hasTag(name string, tags []string) bool { - // Very-very unlikely that a Consul tag would ever start with '=!=' - tag := getTag(name, tags, "=!=") - return tag != "=!=" -} - -func getTag(name string, tags []string, defaultValue string) string { - for _, tag := range tags { - // Given the nature of Consul tags, which could be either singular markers, or key=value pairs, we check if the consul tag starts with 'name' - if strings.HasPrefix(strings.ToLower(tag), strings.ToLower(name)) { - // In case, where a tag might be a key=value, try to split it by the first '=' - // - If the first element (which would always be there, even if the tag is a singular marker without '=' in it - if kv := strings.SplitN(tag, "=", 2); strings.ToLower(kv[0]) == strings.ToLower(name) { - // 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 + return p.getBoolAttribute(label.SuffixEnable, node.Service.Tags, p.ExposedByDefault) } func (p *CatalogProvider) getConstraintTags(tags []string) []string { diff --git a/provider/consul/consul_catalog_config.go b/provider/consul/consul_catalog_config.go index 13c87f572..b66a86538 100644 --- a/provider/consul/consul_catalog_config.go +++ b/provider/consul/consul_catalog_config.go @@ -16,19 +16,23 @@ import ( func (p *CatalogProvider) buildConfiguration(catalog []catalogUpdate) *types.Configuration { var FuncMap = template.FuncMap{ + "getAttribute": p.getAttribute, + "getTag": getTag, + "hasTag": hasTag, + + // Backend functions "getBackend": getBackend, - "getFrontendRule": p.getFrontendRule, - "getBackendName": getBackendName, "getBackendAddress": getBackendAddress, - "getBasicAuth": p.getBasicAuth, + "hasMaxconnAttributes": p.hasMaxConnAttributes, "getSticky": p.getSticky, "hasStickinessLabel": p.hasStickinessLabel, "getStickinessCookieName": p.getStickinessCookieName, - "getAttribute": p.getAttribute, - "getTag": getTag, - "hasTag": hasTag, - "getEntryPoints": getEntryPoints, - "hasMaxconnAttributes": p.hasMaxConnAttributes, + + // Frontend functions + "getBackendName": getBackendName, + "getFrontendRule": p.getFrontendRule, + "getBasicAuth": p.getBasicAuth, + "getEntryPoints": getEntryPoints, } var allNodes []*api.ServiceEntry @@ -58,7 +62,7 @@ func (p *CatalogProvider) buildConfiguration(catalog []catalogUpdate) *types.Con return configuration } -func (p *CatalogProvider) setupFrontEndTemplate() { +func (p *CatalogProvider) setupFrontEndRuleTemplate() { var FuncMap = template.FuncMap{ "getAttribute": p.getAttribute, "getTag": getTag, @@ -68,6 +72,8 @@ func (p *CatalogProvider) setupFrontEndTemplate() { p.frontEndRuleTemplate = tmpl } +// Specific functions + func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string { customFrontendRule := p.getAttribute(label.SuffixFrontendRule, service.Attributes, "") if customFrontendRule == "" { @@ -102,19 +108,16 @@ func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string { } func (p *CatalogProvider) getBasicAuth(tags []string) []string { - list := p.getAttribute(label.SuffixFrontendAuthBasic, tags, "") - if list != "" { - return strings.Split(list, ",") - } - return []string{} + return p.getSliceAttribute(label.SuffixFrontendAuthBasic, tags) } func (p *CatalogProvider) hasMaxConnAttributes(attributes []string) bool { amount := p.getAttribute(label.SuffixBackendMaxConnAmount, attributes, "") - extractorfunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, attributes, "") - return amount != "" && extractorfunc != "" + extractorFunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, attributes, "") + return amount != "" && extractorFunc != "" } +// Deprecated func getEntryPoints(list string) []string { return strings.Split(list, ",") } @@ -146,7 +149,8 @@ func getBackendName(node *api.ServiceEntry, index int) string { } // TODO: Deprecated -// Deprecated replaced by Stickiness +// replaced by Stickiness +// Deprecated func (p *CatalogProvider) getSticky(tags []string) string { stickyTag := p.getAttribute(label.SuffixBackendLoadBalancerSticky, tags, "") if len(stickyTag) > 0 { @@ -165,3 +169,77 @@ func (p *CatalogProvider) hasStickinessLabel(tags []string) bool { func (p *CatalogProvider) getStickinessCookieName(tags []string) string { return p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, "") } + +// Base functions + +func (p *CatalogProvider) getSliceAttribute(name string, tags []string) []string { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return nil + } + return label.SplitAndTrimString(rawValue, ",") +} + +func (p *CatalogProvider) 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 *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string { + return getTag(p.getPrefixedName(name), tags, defaultValue) +} + +func (p *CatalogProvider) 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 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/consul/consul_catalog_config_test.go b/provider/consul/consul_catalog_config_test.go new file mode 100644 index 000000000..edf0f60fb --- /dev/null +++ b/provider/consul/consul_catalog_config_test.go @@ -0,0 +1,571 @@ +package consul + +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 TestBuildConfiguration(t *testing.T) { + provider := &CatalogProvider{ + 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{ + "traefik.backend.loadbalancer=drr", + "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5", + "random.foo=bar", + "traefik.backend.maxconn.amount=1000", + "traefik.backend.maxconn.extractorfunc=client.ip", + "traefik.frontend.auth.basic=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + }, + Nodes: []*api.ServiceEntry{ + { + Service: &api.AgentService{ + Service: "test", + Address: "127.0.0.1", + Port: 80, + Tags: []string{ + "traefik.backend.weight=42", + "random.foo=bar", + "traefik.backend.passHostHeader=true", + "traefik.protocol=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--127-0-0-1--80--traefik-backend-weight-42--random-foo-bar--traefik-backend-passHostHeader-true--traefik-protocol-https--0": { + 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 TestGetPrefixedName(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 := &CatalogProvider{Prefix: test.prefix} + + actual := pro.getPrefixedName(test.name) + assert.Equal(t, test.expected, actual) + }) + } + +} + +func TestGetAttribute(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 := &CatalogProvider{ + Domain: "localhost", + Prefix: test.prefix, + } + + actual := p.getAttribute(test.key, test.tags, test.defaultValue) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetFrontendRule(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() + + provider := &CatalogProvider{ + Domain: "localhost", + Prefix: "traefik", + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), + } + provider.setupFrontEndRuleTemplate() + + actual := provider.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 TestGetBackendName(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--10-0-0-1--80--0", + }, + { + 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--10-0-0-1--80--traefik-weight-42--traefik-enable-true--1", + }, + { + 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--10-0-0-1--80--a-funny-looking-tag--2", + }, + } + + for i, test := range testCases { + test := test + i := i + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getBackendName(test.node, i) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetBasicAuth(t *testing.T) { + testCases := []struct { + desc string + tags []string + expected []string + }{ + { + desc: "label missing", + tags: []string{}, + expected: []string{}, + }, + { + desc: "label existing", + tags: []string{ + "traefik.frontend.auth.basic=user:password", + }, + expected: []string{"user:password"}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + provider := &CatalogProvider{ + Prefix: "traefik", + } + actual := provider.getBasicAuth(test.tags) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestHasStickinessLabel(t *testing.T) { + 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 := hasStickinessLabel(test.tags) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/provider/consul/consul_catalog_test.go b/provider/consul/consul_catalog_test.go index 26517a348..208850f6c 100644 --- a/provider/consul/consul_catalog_test.go +++ b/provider/consul/consul_catalog_test.go @@ -3,527 +3,12 @@ package consul import ( "sort" "testing" - "text/template" "github.com/BurntSushi/ty/fun" - "github.com/containous/traefik/provider/label" - "github.com/containous/traefik/types" "github.com/hashicorp/consul/api" "github.com/stretchr/testify/assert" ) -func TestGetPrefixedName(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 := &CatalogProvider{Prefix: test.prefix} - - actual := pro.getPrefixedName(test.name) - assert.Equal(t, test.expected, actual) - }) - } - -} - -func TestGetFrontendRule(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() - - provider := &CatalogProvider{ - Domain: "localhost", - Prefix: "traefik", - FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", - frontEndRuleTemplate: template.New("consul catalog frontend rule"), - } - provider.setupFrontEndTemplate() - - actual := provider.getFrontendRule(test.service) - assert.Equal(t, test.expected, actual) - }) - } -} - -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 TestGetAttribute(t *testing.T) { - provider := &CatalogProvider{ - Domain: "localhost", - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - key string - defaultValue string - expected string - }{ - { - desc: "Should return tag value 42", - tags: []string{ - "foo.bar=ramdom", - "traefik.backend.weight=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "42", - }, - { - desc: "Should return tag default value 0", - tags: []string{ - "foo.bar=ramdom", - "traefik.backend.wei=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "0", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := provider.getAttribute(test.key, test.tags, test.defaultValue) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetAttributeWithEmptyPrefix(t *testing.T) { - provider := &CatalogProvider{ - Domain: "localhost", - Prefix: "", - } - - testCases := []struct { - desc string - tags []string - key string - defaultValue string - expected string - }{ - { - desc: "Should return tag value 42", - tags: []string{ - "foo.bar=ramdom", - "backend.weight=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "42", - }, - { - desc: "Should return default value 0", - tags: []string{ - "foo.bar=ramdom", - "backend.wei=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "0", - }, - { - desc: "Should return for.bar key value random", - 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() - - actual := provider.getAttribute(test.key, test.tags, test.defaultValue) - 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 TestGetBackendName(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--10-0-0-1--80--0", - }, - { - 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--10-0-0-1--80--traefik-weight-42--traefik-enable-true--1", - }, - { - 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--10-0-0-1--80--a-funny-looking-tag--2", - }, - } - - for i, test := range testCases { - test := test - i := i - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getBackendName(test.node, i) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestBuildConfiguration(t *testing.T) { - provider := &CatalogProvider{ - 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{ - "traefik.backend.loadbalancer=drr", - "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5", - "random.foo=bar", - "traefik.backend.maxconn.amount=1000", - "traefik.backend.maxconn.extractorfunc=client.ip", - "traefik.frontend.auth.basic=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - }, - }, - Nodes: []*api.ServiceEntry{ - { - Service: &api.AgentService{ - Service: "test", - Address: "127.0.0.1", - Port: 80, - Tags: []string{ - "traefik.backend.weight=42", - "random.foo=bar", - "traefik.backend.passHostHeader=true", - "traefik.protocol=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--127-0-0-1--80--traefik-backend-weight-42--random-foo-bar--traefik-backend-passHostHeader-true--traefik-protocol-https--0": { - 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.Equal(t, test.expectedBackends, actualConfig.Backends) - assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) - }) - } -} - func TestNodeSorter(t *testing.T) { testCases := []struct { desc string @@ -972,81 +457,6 @@ func TestFilterEnabled(t *testing.T) { } } -func TestGetBasicAuth(t *testing.T) { - testCases := []struct { - desc string - tags []string - expected []string - }{ - { - desc: "label missing", - tags: []string{}, - expected: []string{}, - }, - { - desc: "label existing", - tags: []string{ - "traefik.frontend.auth.basic=user:password", - }, - expected: []string{"user:password"}, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - provider := &CatalogProvider{ - Prefix: "traefik", - } - actual := provider.getBasicAuth(test.tags) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestHasStickinessLabel(t *testing.T) { - p := &CatalogProvider{ - 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 TestGetChangedStringKeys(t *testing.T) { testCases := []struct { desc string @@ -1096,7 +506,7 @@ func TestGetChangedStringKeys(t *testing.T) { } } -func TestHasNodeOrTagschanged(t *testing.T) { +func TestHasServiceChanged(t *testing.T) { testCases := []struct { desc string current map[string]Service