Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhanced flexibility in Consul Catalog configuration #1565

Merged
merged 1 commit into from
Jun 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`.
Expand Down
1 change: 1 addition & 0 deletions integration/fixtures/consul_catalog/simple.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ logLevel = "DEBUG"

[consulCatalog]
domain = "consul.localhost"
frontEndRule = "Host:{{.ServiceName}}.{{.Domain}}"
90 changes: 79 additions & 11 deletions provider/consul/consul_catalog.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package consul

import (
"bytes"
"errors"
"sort"
"strconv"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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, ",")
}
Expand All @@ -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
}

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 "Host:" + service.ServiceName + "." + p.Domain

return buffer.String()
}

func (p *CatalogProvider) getBackendAddress(node *api.ServiceEntry) string {
Expand All @@ -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...)
}
}
Expand All @@ -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,
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down
144 changes: 136 additions & 8 deletions provider/consul/consul_catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import (
"reflect"
"sort"
"testing"
"text/template"

"github.com/containous/traefik/types"
"github.com/hashicorp/consul/api"
)

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
Expand All @@ -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)
}
}
}
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading