Enhanced flexibility in Consul Catalog configuration
This commit is contained in:
parent
9c27a98821
commit
7d6c778211
6 changed files with 242 additions and 20 deletions
11
docs/toml.md
11
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`.
|
||||
|
|
|
@ -7,3 +7,4 @@ logLevel = "DEBUG"
|
|||
|
||||
[consulCatalog]
|
||||
domain = "consul.localhost"
|
||||
frontEndRule = "Host:{{.ServiceName}}.{{.Domain}}"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue