Enhanced flexibility in Consul Catalog configuration
This commit is contained in:
parent
9c27a98821
commit
7d6c778211
6 changed files with 242 additions and 20 deletions
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue