Enhanced flexibility in Consul Catalog configuration

This commit is contained in:
Alex Antonov 2017-05-08 12:46:53 -05:00 committed by Ludovic Fernandez
parent 9c27a98821
commit 7d6c778211
6 changed files with 242 additions and 20 deletions

View file

@ -1320,6 +1320,15 @@ domain = "consul.localhost"
# Optional # Optional
# #
prefix = "traefik" 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 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.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.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.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.passHostHeader=true`: forward client `Host` header to the backend.
- `traefik.frontend.priority=10`: override default frontend priority - `traefik.frontend.priority=10`: override default frontend priority
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. - `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.

View file

@ -7,3 +7,4 @@ logLevel = "DEBUG"
[consulCatalog] [consulCatalog]
domain = "consul.localhost" domain = "consul.localhost"
frontEndRule = "Host:{{.ServiceName}}.{{.Domain}}"

View file

@ -1,6 +1,7 @@
package consul package consul
import ( import (
"bytes"
"errors" "errors"
"sort" "sort"
"strconv" "strconv"
@ -31,7 +32,9 @@ type CatalogProvider struct {
Endpoint string `description:"Consul server endpoint"` Endpoint string `description:"Consul server endpoint"`
Domain string `description:"Default domain used"` Domain string `description:"Default domain used"`
Prefix string `description:"Prefix used for Consul catalog tags"` Prefix string `description:"Prefix used for Consul catalog tags"`
FrontEndRule string `description:"Frontend rule used for Consul services"`
client *api.Client client *api.Client
frontEndRuleTemplate *template.Template
} }
type serviceUpdate struct { type serviceUpdate struct {
@ -137,9 +140,9 @@ func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) {
} }
nodes := fun.Filter(func(node *api.ServiceEntry) bool { 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) 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()) log.Debugf("Service %v pruned by '%v' constraint", service, failingConstraint.String())
} }
return ok return ok
@ -162,6 +165,13 @@ func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) {
}, nil }, 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 { func (p *CatalogProvider) getEntryPoints(list string) []string {
return strings.Split(list, ",") return strings.Split(list, ",")
} }
@ -172,10 +182,35 @@ func (p *CatalogProvider) getBackend(node *api.ServiceEntry) string {
func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string { func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string {
customFrontendRule := p.getAttribute("frontend.rule", service.Attributes, "") customFrontendRule := p.getAttribute("frontend.rule", service.Attributes, "")
if customFrontendRule != "" { if customFrontendRule == "" {
return 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 { 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 { 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 { for _, tag := range tags {
if strings.Index(strings.ToLower(tag), p.Prefix+".") == 0 { // 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 kv := strings.SplitN(tag[len(p.Prefix+"."):], "=", 2); len(kv) == 2 && strings.ToLower(kv[0]) == strings.ToLower(name) { if strings.Index(strings.ToLower(tag), strings.ToLower(name)) == 0 {
return kv[1] // 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 defaultValue
} }
func (p *CatalogProvider) getContraintTags(tags []string) []string { func (p *CatalogProvider) getConstraintTags(tags []string) []string {
var list []string var list []string
for _, tag := range tags { for _, tag := range tags {
if strings.Index(strings.ToLower(tag), p.Prefix+".tags=") == 0 { // If 'AllTagsConstraintFiltering' is disabled, we look for a Consul tag named 'traefik.tags' (unless different 'prefix' is configured)
splitedTags := strings.Split(tag[len(p.Prefix+".tags="):], ",") 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...) list = append(list, splitedTags...)
} }
} }
@ -231,6 +286,8 @@ func (p *CatalogProvider) buildConfig(catalog []catalogUpdate) *types.Configurat
"getBackendName": p.getBackendName, "getBackendName": p.getBackendName,
"getBackendAddress": p.getBackendAddress, "getBackendAddress": p.getBackendAddress,
"getAttribute": p.getAttribute, "getAttribute": p.getAttribute,
"getTag": p.getTag,
"hasTag": p.hasTag,
"getEntryPoints": p.getEntryPoints, "getEntryPoints": p.getEntryPoints,
"hasMaxconnAttributes": p.hasMaxconnAttributes, "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 // Provide allows the consul catalog provider to provide configurations to traefik
// using the given configuration channel. // using the given configuration channel.
func (p *CatalogProvider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { 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.client = client
p.Constraints = append(p.Constraints, constraints...) p.Constraints = append(p.Constraints, constraints...)
p.setupFrontEndTemplate()
pool.Go(func(stop chan bool) { pool.Go(func(stop chan bool) {
notify := func(err error, time time.Duration) { notify := func(err error, time time.Duration) {

View file

@ -4,6 +4,7 @@ import (
"reflect" "reflect"
"sort" "sort"
"testing" "testing"
"text/template"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
@ -11,9 +12,12 @@ import (
func TestConsulCatalogGetFrontendRule(t *testing.T) { func TestConsulCatalogGetFrontendRule(t *testing.T) {
provider := &CatalogProvider{ provider := &CatalogProvider{
Domain: "localhost", Domain: "localhost",
Prefix: "traefik", Prefix: "traefik",
FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}",
frontEndRuleTemplate: template.New("consul catalog frontend rule"),
} }
provider.setupFrontEndTemplate()
services := []struct { services := []struct {
service serviceUpdate service serviceUpdate
@ -35,12 +39,73 @@ func TestConsulCatalogGetFrontendRule(t *testing.T) {
}, },
expected: "Host:*.example.com", 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 { for _, e := range services {
actual := provider.getFrontendRule(e.service) actual := provider.getFrontendRule(e.service)
if actual != e.expected { 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 { for _, e := range services {
actual := provider.getAttribute(e.key, e.tags, e.defaultValue) actual := provider.getAttribute(e.key, e.tags, e.defaultValue)
if actual != e.expected { 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 { for _, e := range services {
actual := provider.getBackendAddress(e.node) actual := provider.getBackendAddress(e.node)
if actual != e.expected { 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 { for i, e := range services {
actual := provider.getBackendName(e.node, i) actual := provider.getBackendName(e.node, i)
if actual != e.expected { 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) { func TestConsulCatalogBuildConfig(t *testing.T) {
provider := &CatalogProvider{ provider := &CatalogProvider{
Domain: "localhost", Domain: "localhost",
Prefix: "traefik", Prefix: "traefik",
FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}",
frontEndRuleTemplate: template.New("consul catalog frontend rule"),
} }
cases := []struct { cases := []struct {

View file

@ -397,6 +397,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
defaultConsulCatalog.Endpoint = "127.0.0.1:8500" defaultConsulCatalog.Endpoint = "127.0.0.1:8500"
defaultConsulCatalog.Constraints = types.Constraints{} defaultConsulCatalog.Constraints = types.Constraints{}
defaultConsulCatalog.Prefix = "traefik" defaultConsulCatalog.Prefix = "traefik"
defaultConsulCatalog.FrontEndRule = "Host:{{.ServiceName}}.{{.Domain}}"
// default Etcd // default Etcd
var defaultEtcd etcd.Provider var defaultEtcd etcd.Provider

View file

@ -857,6 +857,21 @@
# #
# prefix = "traefik" # 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 # Constraints
# #
# Optional # Optional