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
#
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`.

View file

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

View file

@ -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) {

View file

@ -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 {

View file

@ -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

View file

@ -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