From 7d6c778211a1007d2fe9c98b2b93b3f32e33785d Mon Sep 17 00:00:00 2001 From: Alex Antonov Date: Mon, 8 May 2017 12:46:53 -0500 Subject: [PATCH] Enhanced flexibility in Consul Catalog configuration --- docs/toml.md | 11 +- .../fixtures/consul_catalog/simple.toml | 1 + provider/consul/consul_catalog.go | 90 +++++++++-- provider/consul/consul_catalog_test.go | 144 +++++++++++++++++- server/configuration.go | 1 + traefik.sample.toml | 15 ++ 6 files changed, 242 insertions(+), 20 deletions(-) diff --git a/docs/toml.md b/docs/toml.md index 59456257c..aa58f7713 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -1320,6 +1320,15 @@ domain = "consul.localhost" # Optional # prefix = "traefik" + +# Default frontEnd Rule for Consul services +# The format is a Go Template with ".ServiceName", ".Domain" and ".Attributes" available +# "getTag(name, tags, defaultValue)", "hasTag(name, tags)" and "getAttribute(name, tags, defaultValue)" functions are available +# "getAttribute(...)" function uses prefixed tag names based on "prefix" value +# +# Optional +# +frontEndRule = "Host:{{.ServiceName}}.{{Domain}}" ``` This backend will create routes matching on hostname based on the service name @@ -1334,7 +1343,7 @@ Additional settings can be defined using Consul Catalog tags: - `traefik.backend.loadbalancer=drr`: override the default load balancing mode - `traefik.backend.maxconn.amount=10`: set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. - `traefik.backend.maxconn.extractorfunc=client.ip`: set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. -- `traefik.frontend.rule=Host:test.traefik.io`: override the default frontend rule (Default: `Host:{containerName}.{domain}`). +- `traefik.frontend.rule=Host:test.traefik.io`: override the default frontend rule (Default: `Host:{{.ServiceName}}.{{.Domain}}`). - `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend. - `traefik.frontend.priority=10`: override default frontend priority - `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. diff --git a/integration/fixtures/consul_catalog/simple.toml b/integration/fixtures/consul_catalog/simple.toml index 45988382a..55fb8e989 100644 --- a/integration/fixtures/consul_catalog/simple.toml +++ b/integration/fixtures/consul_catalog/simple.toml @@ -7,3 +7,4 @@ logLevel = "DEBUG" [consulCatalog] domain = "consul.localhost" +frontEndRule = "Host:{{.ServiceName}}.{{.Domain}}" diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index 4bf1f9c92..603374bea 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -1,6 +1,7 @@ package consul import ( + "bytes" "errors" "sort" "strconv" @@ -31,7 +32,9 @@ type CatalogProvider struct { Endpoint string `description:"Consul server endpoint"` Domain string `description:"Default domain used"` Prefix string `description:"Prefix used for Consul catalog tags"` + FrontEndRule string `description:"Frontend rule used for Consul services"` client *api.Client + frontEndRuleTemplate *template.Template } type serviceUpdate struct { @@ -137,9 +140,9 @@ func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) { } nodes := fun.Filter(func(node *api.ServiceEntry) bool { - constraintTags := p.getContraintTags(node.Service.Tags) + constraintTags := p.getConstraintTags(node.Service.Tags) ok, failingConstraint := p.MatchConstraints(constraintTags) - if ok == false && failingConstraint != nil { + if !ok && failingConstraint != nil { log.Debugf("Service %v pruned by '%v' constraint", service, failingConstraint.String()) } return ok @@ -162,6 +165,13 @@ func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) { }, nil } +func (p *CatalogProvider) getPrefixedName(name string) string { + if len(p.Prefix) > 0 { + return p.Prefix + "." + name + } + return name +} + func (p *CatalogProvider) getEntryPoints(list string) []string { return strings.Split(list, ",") } @@ -172,10 +182,35 @@ func (p *CatalogProvider) getBackend(node *api.ServiceEntry) string { func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string { customFrontendRule := p.getAttribute("frontend.rule", service.Attributes, "") - if customFrontendRule != "" { - return customFrontendRule + if customFrontendRule == "" { + customFrontendRule = p.FrontEndRule } - return "Host:" + service.ServiceName + "." + p.Domain + + t := p.frontEndRuleTemplate + t, err := t.Parse(customFrontendRule) + if err != nil { + log.Errorf("failed to parse Consul Catalog custom frontend rule: %s", err) + return "" + } + + templateObjects := struct { + ServiceName string + Domain string + Attributes []string + }{ + ServiceName: service.ServiceName, + Domain: p.Domain, + Attributes: service.Attributes, + } + + var buffer bytes.Buffer + err = t.Execute(&buffer, templateObjects) + if err != nil { + log.Errorf("failed to execute Consul Catalog custom frontend rule template: %s", err) + return "" + } + + return buffer.String() } func (p *CatalogProvider) getBackendAddress(node *api.ServiceEntry) string { @@ -201,22 +236,42 @@ func (p *CatalogProvider) getBackendName(node *api.ServiceEntry, index int) stri } func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string { + return p.getTag(p.getPrefixedName(name), tags, defaultValue) +} + +func (p *CatalogProvider) hasTag(name string, tags []string) bool { + // Very-very unlikely that a Consul tag would ever start with '=!=' + tag := p.getTag(name, tags, "=!=") + return tag != "=!=" +} + +func (p *CatalogProvider) getTag(name string, tags []string, defaultValue string) string { for _, tag := range tags { - if strings.Index(strings.ToLower(tag), p.Prefix+".") == 0 { - if kv := strings.SplitN(tag[len(p.Prefix+"."):], "=", 2); len(kv) == 2 && strings.ToLower(kv[0]) == strings.ToLower(name) { - return kv[1] + // 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.Index(strings.ToLower(tag), strings.ToLower(name)) == 0 { + // 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 } -func (p *CatalogProvider) getContraintTags(tags []string) []string { +func (p *CatalogProvider) getConstraintTags(tags []string) []string { var list []string for _, tag := range tags { - if strings.Index(strings.ToLower(tag), p.Prefix+".tags=") == 0 { - splitedTags := strings.Split(tag[len(p.Prefix+".tags="):], ",") + // If 'AllTagsConstraintFiltering' is disabled, we look for a Consul tag named 'traefik.tags' (unless different 'prefix' is configured) + if strings.Index(strings.ToLower(tag), p.getPrefixedName("tags=")) == 0 { + // If 'traefik.tags=' tag is found, take the tag value and split by ',' adding the result to the list to be returned + splitedTags := strings.Split(tag[len(p.getPrefixedName("tags=")):], ",") list = append(list, splitedTags...) } } @@ -231,6 +286,8 @@ func (p *CatalogProvider) buildConfig(catalog []catalogUpdate) *types.Configurat "getBackendName": p.getBackendName, "getBackendAddress": p.getBackendAddress, "getAttribute": p.getAttribute, + "getTag": p.getTag, + "hasTag": p.hasTag, "getEntryPoints": p.getEntryPoints, "hasMaxconnAttributes": p.hasMaxconnAttributes, } @@ -326,6 +383,16 @@ func (p *CatalogProvider) watch(configurationChan chan<- types.ConfigMessage, st } } +func (p *CatalogProvider) setupFrontEndTemplate() { + var FuncMap = template.FuncMap{ + "getAttribute": p.getAttribute, + "getTag": p.getTag, + "hasTag": p.hasTag, + } + t := template.New("consul catalog frontend rule").Funcs(FuncMap) + p.frontEndRuleTemplate = t +} + // Provide allows the consul catalog provider to provide configurations to traefik // using the given configuration channel. func (p *CatalogProvider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { @@ -337,6 +404,7 @@ func (p *CatalogProvider) Provide(configurationChan chan<- types.ConfigMessage, } p.client = client p.Constraints = append(p.Constraints, constraints...) + p.setupFrontEndTemplate() pool.Go(func(stop chan bool) { notify := func(err error, time time.Duration) { diff --git a/provider/consul/consul_catalog_test.go b/provider/consul/consul_catalog_test.go index a76cef2da..325c6338e 100644 --- a/provider/consul/consul_catalog_test.go +++ b/provider/consul/consul_catalog_test.go @@ -4,6 +4,7 @@ import ( "reflect" "sort" "testing" + "text/template" "github.com/containous/traefik/types" "github.com/hashicorp/consul/api" @@ -11,9 +12,12 @@ import ( func TestConsulCatalogGetFrontendRule(t *testing.T) { provider := &CatalogProvider{ - Domain: "localhost", - Prefix: "traefik", + Domain: "localhost", + Prefix: "traefik", + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), } + provider.setupFrontEndTemplate() services := []struct { service serviceUpdate @@ -35,12 +39,73 @@ func TestConsulCatalogGetFrontendRule(t *testing.T) { }, expected: "Host:*.example.com", }, + { + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=Host:{{.ServiceName}}.example.com", + }, + }, + expected: "Host:foo.example.com", + }, + { + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=PathPrefix:{{getTag \"contextPath\" .Attributes \"/\"}}", + "contextPath=/bar", + }, + }, + expected: "PathPrefix:/bar", + }, } for _, e := range services { actual := provider.getFrontendRule(e.service) if actual != e.expected { - t.Fatalf("expected %q, got %q", e.expected, actual) + t.Fatalf("expected %s, got %s", e.expected, actual) + } + } +} + +func TestConsulCatalogGetTag(t *testing.T) { + provider := &CatalogProvider{ + Domain: "localhost", + Prefix: "traefik", + } + + services := []struct { + tags []string + key string + defaultValue string + expected string + }{ + { + tags: []string{ + "foo.bar=random", + "traefik.backend.weight=42", + "management", + }, + key: "foo.bar", + defaultValue: "0", + expected: "random", + }, + } + + actual := provider.hasTag("management", []string{"management"}) + if !actual { + t.Fatalf("expected %v, got %v", true, actual) + } + + actual = provider.hasTag("management", []string{"management=yes"}) + if !actual { + t.Fatalf("expected %v, got %v", true, actual) + } + + for _, e := range services { + actual := provider.getTag(e.key, e.tags, e.defaultValue) + if actual != e.expected { + t.Fatalf("expected %s, got %s", e.expected, actual) } } } @@ -77,10 +142,71 @@ func TestConsulCatalogGetAttribute(t *testing.T) { }, } + expected := provider.Prefix + ".foo" + actual := provider.getPrefixedName("foo") + if actual != expected { + t.Fatalf("expected %s, got %s", expected, actual) + } + for _, e := range services { actual := provider.getAttribute(e.key, e.tags, e.defaultValue) if actual != e.expected { - t.Fatalf("expected %q, got %q", e.expected, actual) + t.Fatalf("expected %s, got %s", e.expected, actual) + } + } +} + +func TestConsulCatalogGetAttributeWithEmptyPrefix(t *testing.T) { + provider := &CatalogProvider{ + Domain: "localhost", + Prefix: "", + } + + services := []struct { + tags []string + key string + defaultValue string + expected string + }{ + { + tags: []string{ + "foo.bar=ramdom", + "backend.weight=42", + }, + key: "backend.weight", + defaultValue: "0", + expected: "42", + }, + { + tags: []string{ + "foo.bar=ramdom", + "backend.wei=42", + }, + key: "backend.weight", + defaultValue: "0", + expected: "0", + }, + { + tags: []string{ + "foo.bar=ramdom", + "backend.wei=42", + }, + key: "foo.bar", + defaultValue: "random", + expected: "ramdom", + }, + } + + expected := "foo" + actual := provider.getPrefixedName("foo") + if actual != expected { + t.Fatalf("expected %s, got %s", expected, actual) + } + + for _, e := range services { + actual := provider.getAttribute(e.key, e.tags, e.defaultValue) + if actual != e.expected { + t.Fatalf("expected %s, got %s", e.expected, actual) } } } @@ -122,7 +248,7 @@ func TestConsulCatalogGetBackendAddress(t *testing.T) { for _, e := range services { actual := provider.getBackendAddress(e.node) if actual != e.expected { - t.Fatalf("expected %q, got %q", e.expected, actual) + t.Fatalf("expected %s, got %s", e.expected, actual) } } } @@ -175,15 +301,17 @@ func TestConsulCatalogGetBackendName(t *testing.T) { for i, e := range services { actual := provider.getBackendName(e.node, i) if actual != e.expected { - t.Fatalf("expected %q, got %q", e.expected, actual) + t.Fatalf("expected %s, got %s", e.expected, actual) } } } func TestConsulCatalogBuildConfig(t *testing.T) { provider := &CatalogProvider{ - Domain: "localhost", - Prefix: "traefik", + Domain: "localhost", + Prefix: "traefik", + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), } cases := []struct { diff --git a/server/configuration.go b/server/configuration.go index 471252123..e4926e3e4 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -397,6 +397,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { defaultConsulCatalog.Endpoint = "127.0.0.1:8500" defaultConsulCatalog.Constraints = types.Constraints{} defaultConsulCatalog.Prefix = "traefik" + defaultConsulCatalog.FrontEndRule = "Host:{{.ServiceName}}.{{.Domain}}" // default Etcd var defaultEtcd etcd.Provider diff --git a/traefik.sample.toml b/traefik.sample.toml index f29d68e0b..029bcf006 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -857,6 +857,21 @@ # # prefix = "traefik" +# Default frontEnd Rule for Consul services +# - The format is a Go Template with ".ServiceName", ".Domain" and ".Attributes" available +# -- "getTag(name, tags, defaultValue)", "hasTag(name, tags)" and "getAttribute(name, tags, defaultValue)" functions are available +# --- "getAttribute(...)" function uses prefixed tag names based on "prefix" value +# +# Optional +# +#frontEndRule = "Host:{{.ServiceName}}.{{Domain}}" + +# Should use all Consul catalog tags for constraint filtering +# +# Optional +# +#allTagsConstraintFiltering = false + # Constraints # # Optional