refactor: Consul Catalog labels.

This commit is contained in:
Fernandez Ludovic 2018-03-28 02:18:07 +02:00 committed by Traefiker Bot
parent 46db91ce73
commit ff61cc971e
14 changed files with 1715 additions and 2018 deletions

View file

@ -246,6 +246,15 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) {
}
}
if gc.ConsulCatalog != nil {
if len(gc.ConsulCatalog.Filename) != 0 && gc.ConsulCatalog.TemplateVersion != 2 {
log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.")
gc.ConsulCatalog.TemplateVersion = 1
} else {
gc.ConsulCatalog.TemplateVersion = 2
}
}
if gc.Rancher != nil {
if len(gc.Rancher.Filename) != 0 && gc.Rancher.TemplateVersion != 2 {
log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.")

View file

@ -6,6 +6,7 @@ import (
"time"
"github.com/containous/traefik/integration/try"
"github.com/containous/traefik/provider/label"
"github.com/go-check/check"
"github.com/hashicorp/consul/api"
checker "github.com/vdemeester/shakers"
@ -160,7 +161,6 @@ func (s *ConsulCatalogSuite) TestSingleService(c *check.C) {
s.deregisterService("test", whoami.NetworkSettings.IPAddress)
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusNotFound), try.HasBody())
c.Assert(err, checker.IsNil)
}
func (s *ConsulCatalogSuite) TestExposedByDefaultFalseSingleService(c *check.C) {
@ -202,13 +202,12 @@ func (s *ConsulCatalogSuite) TestExposedByDefaultFalseSimpleServiceMultipleNode(
defer cmd.Process.Kill()
whoami := s.composeProject.Container(c, "whoami1")
whoami2 := s.composeProject.Container(c, "whoami2")
err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 80, []string{"traefik.enable=true"})
whoami2 := s.composeProject.Container(c, "whoami2")
err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 80, []string{label.TraefikEnable + "=true"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", whoami2.NetworkSettings.IPAddress)
@ -326,7 +325,7 @@ func (s *ConsulCatalogSuite) TestBasicAuthSimpleService(c *check.C) {
whoami := s.composeProject.Container(c, "whoami1")
err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{
"traefik.frontend.auth.basic=test:$2a$06$O5NksJPAcgrC9MuANkSoE.Xe9DSg7KcLLFYNr1Lj6hPcMmvgwxhme,test2:$2y$10$xP1SZ70QbZ4K2bTGKJOhpujkpcLxQcB3kEPF6XAV19IdcqsZTyDEe",
label.TraefikFrontendAuthBasic + "=test:$2a$06$O5NksJPAcgrC9MuANkSoE.Xe9DSg7KcLLFYNr1Lj6hPcMmvgwxhme,test2:$2y$10$xP1SZ70QbZ4K2bTGKJOhpujkpcLxQcB3kEPF6XAV19IdcqsZTyDEe",
})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
@ -362,7 +361,8 @@ func (s *ConsulCatalogSuite) TestRefreshConfigTagChange(c *check.C) {
whoami := s.composeProject.Container(c, "whoami1")
err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=false", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"})
err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80,
[]string{"name=whoami1", label.TraefikEnable + "=false", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
@ -370,7 +370,8 @@ func (s *ConsulCatalogSuite) TestRefreshConfigTagChange(c *check.C) {
try.BodyContains(whoami.NetworkSettings.IPAddress))
c.Assert(err, checker.NotNil)
err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true", "traefik.backend.circuitbreaker=ResponseCodeRatio(500, 600, 0, 600) > 0.5"})
err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80,
[]string{"name=whoami1", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=ResponseCodeRatio(500, 600, 0, 600) > 0.5"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
@ -403,16 +404,20 @@ func (s *ConsulCatalogSuite) TestCircuitBreaker(c *check.C) {
defer cmd.Process.Kill()
whoami := s.composeProject.Container(c, "whoami1")
whoami2 := s.composeProject.Container(c, "whoami2")
whoami3 := s.composeProject.Container(c, "whoami3")
err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"})
err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80,
[]string{"name=whoami1", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 42, []string{"name=whoami2", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"})
whoami2 := s.composeProject.Container(c, "whoami2")
err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 42,
[]string{"name=whoami2", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", whoami2.NetworkSettings.IPAddress)
err = s.registerService("test", whoami3.NetworkSettings.IPAddress, 42, []string{"name=whoami3", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"})
whoami3 := s.composeProject.Container(c, "whoami3")
err = s.registerService("test", whoami3.NetworkSettings.IPAddress, 42,
[]string{"name=whoami3", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", whoami3.NetworkSettings.IPAddress)
@ -452,7 +457,7 @@ func (s *ConsulCatalogSuite) TestRefreshConfigPortChange(c *check.C) {
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, try.BodyContains(whoami.NetworkSettings.IPAddress))
c.Assert(err, checker.IsNil)
err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true"})
err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", label.TraefikEnable + "=true"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)

View file

@ -0,0 +1,273 @@
package consulcatalog
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"fmt"
"sort"
"strconv"
"strings"
"text/template"
"github.com/containous/traefik/log"
"github.com/containous/traefik/provider"
"github.com/containous/traefik/provider/label"
"github.com/containous/traefik/types"
"github.com/hashicorp/consul/api"
)
func (p *Provider) buildConfigurationV2(catalog []catalogUpdate) *types.Configuration {
var funcMap = template.FuncMap{
"getAttribute": p.getAttribute,
"getTag": getTag,
"hasTag": hasTag,
// Backend functions
"getNodeBackendName": getNodeBackendName,
"getServiceBackendName": getServiceBackendName,
"getBackendAddress": getBackendAddress,
"getServerName": getServerName,
"getCircuitBreaker": getCircuitBreaker,
"getLoadBalancer": getLoadBalancer,
"getMaxConn": label.GetMaxConn,
"getHealthCheck": label.GetHealthCheck,
"getBuffering": label.GetBuffering,
"getServer": p.getServer,
// Frontend functions
"getFrontendRule": p.getFrontendRule,
"getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic),
"getFrontEndEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints),
"getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt),
"getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool),
"getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert),
"getWhiteList": label.GetWhiteList,
"getRedirect": label.GetRedirect,
"getErrorPages": label.GetErrorPages,
"getRateLimit": label.GetRateLimit,
"getHeaders": label.GetHeaders,
}
var allNodes []*api.ServiceEntry
var services []*serviceUpdate
for _, info := range catalog {
if len(info.Nodes) > 0 {
services = append(services, info.Service)
allNodes = append(allNodes, info.Nodes...)
}
}
// Ensure a stable ordering of nodes so that identical configurations may be detected
sort.Sort(nodeSorter(allNodes))
templateObjects := struct {
Services []*serviceUpdate
Nodes []*api.ServiceEntry
}{
Services: services,
Nodes: allNodes,
}
configuration, err := p.GetConfiguration("templates/consul_catalog.tmpl", funcMap, templateObjects)
if err != nil {
log.WithError(err).Error("Failed to create config")
}
return configuration
}
// Specific functions
func (p *Provider) getFrontendRule(service serviceUpdate) string {
customFrontendRule := label.GetStringValue(service.TraefikLabels, label.TraefikFrontendRule, "")
if customFrontendRule == "" {
customFrontendRule = p.FrontEndRule
}
tmpl := p.frontEndRuleTemplate
tmpl, err := tmpl.Parse(customFrontendRule)
if err != nil {
log.Errorf("Failed to parse Consul Catalog custom frontend rule: %v", err)
return ""
}
templateObjects := struct {
ServiceName string
Domain string
Attributes []string
}{
ServiceName: service.ServiceName,
Domain: p.Domain,
Attributes: service.Attributes,
}
var buffer bytes.Buffer
err = tmpl.Execute(&buffer, templateObjects)
if err != nil {
log.Errorf("Failed to execute Consul Catalog custom frontend rule template: %v", err)
return ""
}
return buffer.String()
}
func (p *Provider) getServer(node *api.ServiceEntry) types.Server {
scheme := p.getAttribute(label.SuffixProtocol, node.Service.Tags, label.DefaultProtocol)
address := getBackendAddress(node)
return types.Server{
URL: fmt.Sprintf("%s://%s:%d", scheme, address, node.Service.Port),
Weight: p.getWeight(node.Service.Tags),
}
}
func (p *Provider) setupFrontEndRuleTemplate() {
var FuncMap = template.FuncMap{
"getAttribute": p.getAttribute,
"getTag": getTag,
"hasTag": hasTag,
}
p.frontEndRuleTemplate = template.New("consul catalog frontend rule").Funcs(FuncMap)
}
// Specific functions
// Only for compatibility
// Deprecated
func getLoadBalancer(labels map[string]string) *types.LoadBalancer {
if v, ok := labels[label.TraefikBackendLoadBalancer]; ok {
log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancer, label.TraefikBackendLoadBalancerMethod)
if !label.Has(labels, label.TraefikBackendLoadBalancerMethod) {
labels[label.TraefikBackendLoadBalancerMethod] = v
}
}
return label.GetLoadBalancer(labels)
}
// Only for compatibility
// Deprecated
func getCircuitBreaker(labels map[string]string) *types.CircuitBreaker {
if v, ok := labels[label.TraefikBackendCircuitBreaker]; ok {
log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendCircuitBreaker, label.TraefikBackendCircuitBreakerExpression)
if !label.Has(labels, label.TraefikBackendCircuitBreakerExpression) {
labels[label.TraefikBackendCircuitBreakerExpression] = v
}
}
return label.GetCircuitBreaker(labels)
}
func getServiceBackendName(service *serviceUpdate) string {
return strings.ToLower(service.ServiceName)
}
func getNodeBackendName(node *api.ServiceEntry) string {
return strings.ToLower(node.Service.Service)
}
func getBackendAddress(node *api.ServiceEntry) string {
if node.Service.Address != "" {
return node.Service.Address
}
return node.Node.Address
}
func getServerName(node *api.ServiceEntry, index int) string {
serviceName := node.Service.Service + node.Service.Address + strconv.Itoa(node.Service.Port)
// TODO sort tags ?
serviceName += strings.Join(node.Service.Tags, "")
hash := sha1.New()
_, err := hash.Write([]byte(serviceName))
if err != nil {
// Impossible case
log.Error(err)
} else {
serviceName = base64.URLEncoding.EncodeToString(hash.Sum(nil))
}
// unique int at the end
return provider.Normalize(node.Service.Service + "-" + strconv.Itoa(index) + "-" + serviceName)
}
func (p *Provider) getWeight(tags []string) int {
weight := p.getIntAttribute(label.SuffixWeight, tags, label.DefaultWeightInt)
// Deprecated
deprecatedWeightTag := "backend." + label.SuffixWeight
if p.hasAttribute(deprecatedWeightTag, tags) {
log.Warnf("Deprecated configuration found: %s. Please use %s.",
p.getPrefixedName(deprecatedWeightTag), p.getPrefixedName(label.SuffixWeight))
weight = p.getIntAttribute(deprecatedWeightTag, tags, label.DefaultWeightInt)
}
return weight
}
// Base functions
func (p *Provider) hasAttribute(name string, tags []string) bool {
return hasTag(p.getPrefixedName(name), tags)
}
func (p *Provider) getAttribute(name string, tags []string, defaultValue string) string {
return getTag(p.getPrefixedName(name), tags, defaultValue)
}
func (p *Provider) getPrefixedName(name string) string {
if len(p.Prefix) > 0 && len(name) > 0 {
return p.Prefix + "." + name
}
return name
}
func hasTag(name string, tags []string) bool {
lowerName := strings.ToLower(name)
for _, tag := range tags {
lowerTag := strings.ToLower(tag)
// Given the nature of Consul tags, which could be either singular markers, or key=value pairs
if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName {
return true
}
}
return false
}
func hasTagPrefix(name string, tags []string) bool {
lowerName := strings.ToLower(name)
for _, tag := range tags {
lowerTag := strings.ToLower(tag)
if strings.HasPrefix(lowerTag, lowerName) {
return true
}
}
return false
}
func getTag(name string, tags []string, defaultValue string) string {
lowerName := strings.ToLower(name)
for _, tag := range tags {
lowerTag := strings.ToLower(tag)
// Given the nature of Consul tags, which could be either singular markers, or key=value pairs
if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName {
// In case, where a tag might be a key=value, try to split it by the first '='
kv := strings.SplitN(tag, "=", 2)
// 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
}

View file

@ -0,0 +1,10 @@
package consulcatalog
import "github.com/containous/traefik/types"
func (p *Provider) buildConfiguration(catalog []catalogUpdate) *types.Configuration {
if p.TemplateVersion == 1 {
return p.buildConfigurationV1(catalog)
}
return p.buildConfigurationV2(catalog)
}

View file

@ -0,0 +1,518 @@
package consulcatalog
import (
"testing"
"text/template"
"github.com/containous/traefik/provider/label"
"github.com/containous/traefik/types"
"github.com/hashicorp/consul/api"
"github.com/stretchr/testify/assert"
)
func TestProviderBuildConfiguration(t *testing.T) {
p := &Provider{
Domain: "localhost",
Prefix: "traefik",
ExposedByDefault: false,
FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}",
frontEndRuleTemplate: template.New("consul catalog frontend rule"),
}
testCases := []struct {
desc string
nodes []catalogUpdate
expectedFrontends map[string]*types.Frontend
expectedBackends map[string]*types.Backend
}{
{
desc: "Should build config of nothing",
nodes: []catalogUpdate{},
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
desc: "Should build config with no frontend and backend",
nodes: []catalogUpdate{
{
Service: &serviceUpdate{
ServiceName: "test",
},
},
},
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
desc: "Should build config who contains one frontend and one backend",
nodes: []catalogUpdate{
{
Service: &serviceUpdate{
ServiceName: "test",
Attributes: []string{
"random.foo=bar",
label.TraefikBackendLoadBalancerMethod + "=drr",
label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5",
label.TraefikBackendMaxConnAmount + "=1000",
label.TraefikBackendMaxConnExtractorFunc + "=client.ip",
label.TraefikFrontendAuthBasic + "=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
},
},
Nodes: []*api.ServiceEntry{
{
Service: &api.AgentService{
Service: "test",
Address: "127.0.0.1",
Port: 80,
Tags: []string{
"random.foo=bar",
label.Prefix + "backend.weight=42", // Deprecated label
label.TraefikFrontendPassHostHeader + "=true",
label.TraefikProtocol + "=https",
},
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.1",
},
},
},
},
},
expectedFrontends: map[string]*types.Frontend{
"frontend-test": {
Backend: "backend-test",
PassHostHeader: true,
Routes: map[string]types.Route{
"route-host-test": {
Rule: "Host:test.localhost",
},
},
EntryPoints: []string{},
BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test": {
Servers: map[string]types.Server{
"test-0-us4-27hAOu2ARV7nNrmv6GoKlcA": {
URL: "https://127.0.0.1:80",
Weight: 42,
},
},
LoadBalancer: &types.LoadBalancer{
Method: "drr",
},
CircuitBreaker: &types.CircuitBreaker{
Expression: "NetworkErrorRatio() > 0.5",
},
MaxConn: &types.MaxConn{
Amount: 1000,
ExtractorFunc: "client.ip",
},
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
nodes := fakeLoadTraefikLabelsSlice(test.nodes, p.Prefix)
actualConfig := p.buildConfigurationV2(nodes)
assert.NotNil(t, actualConfig)
assert.Equal(t, test.expectedBackends, actualConfig.Backends)
assert.Equal(t, test.expectedFrontends, actualConfig.Frontends)
})
}
}
func TestGetTag(t *testing.T) {
testCases := []struct {
desc string
tags []string
key string
defaultValue string
expected string
}{
{
desc: "Should return value of foo.bar key",
tags: []string{
"foo.bar=random",
"traefik.backend.weight=42",
"management",
},
key: "foo.bar",
defaultValue: "0",
expected: "random",
},
{
desc: "Should return default value when nonexistent key",
tags: []string{
"foo.bar.foo.bar=random",
"traefik.backend.weight=42",
"management",
},
key: "foo.bar",
defaultValue: "0",
expected: "0",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := getTag(test.key, test.tags, test.defaultValue)
assert.Equal(t, test.expected, actual)
})
}
}
func TestHasTag(t *testing.T) {
testCases := []struct {
desc string
name string
tags []string
expected bool
}{
{
desc: "tag without value",
name: "foo",
tags: []string{"foo"},
expected: true,
},
{
desc: "tag with value",
name: "foo",
tags: []string{"foo=true"},
expected: true,
},
{
desc: "missing tag",
name: "foo",
tags: []string{"foobar=true"},
expected: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := hasTag(test.name, test.tags)
assert.Equal(t, test.expected, actual)
})
}
}
func TestProviderGetPrefixedName(t *testing.T) {
testCases := []struct {
desc string
name string
prefix string
expected string
}{
{
desc: "empty name with prefix",
name: "",
prefix: "foo",
expected: "",
},
{
desc: "empty name without prefix",
name: "",
prefix: "",
expected: "",
},
{
desc: "with prefix",
name: "bar",
prefix: "foo",
expected: "foo.bar",
},
{
desc: "without prefix",
name: "bar",
prefix: "",
expected: "bar",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := &Provider{Prefix: test.prefix}
actual := p.getPrefixedName(test.name)
assert.Equal(t, test.expected, actual)
})
}
}
func TestProviderGetAttribute(t *testing.T) {
testCases := []struct {
desc string
tags []string
key string
defaultValue string
prefix string
expected string
}{
{
desc: "Should return tag value 42",
prefix: "traefik",
tags: []string{
"foo.bar=ramdom",
"traefik.backend.weight=42",
},
key: "backend.weight",
defaultValue: "0",
expected: "42",
},
{
desc: "Should return tag default value 0",
prefix: "traefik",
tags: []string{
"foo.bar=ramdom",
"traefik.backend.wei=42",
},
key: "backend.weight",
defaultValue: "0",
expected: "0",
},
{
desc: "Should return tag value 42 when empty prefix",
tags: []string{
"foo.bar=ramdom",
"backend.weight=42",
},
key: "backend.weight",
defaultValue: "0",
expected: "42",
},
{
desc: "Should return default value 0 when empty prefix",
tags: []string{
"foo.bar=ramdom",
"backend.wei=42",
},
key: "backend.weight",
defaultValue: "0",
expected: "0",
},
{
desc: "Should return for.bar key value random when empty prefix",
tags: []string{
"foo.bar=ramdom",
"backend.wei=42",
},
key: "foo.bar",
defaultValue: "random",
expected: "ramdom",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := &Provider{
Domain: "localhost",
Prefix: test.prefix,
}
actual := p.getAttribute(test.key, test.tags, test.defaultValue)
assert.Equal(t, test.expected, actual)
})
}
}
func TestProviderGetFrontendRule(t *testing.T) {
testCases := []struct {
desc string
service serviceUpdate
expected string
}{
{
desc: "Should return default host foo.localhost",
service: serviceUpdate{
ServiceName: "foo",
Attributes: []string{},
},
expected: "Host:foo.localhost",
},
{
desc: "Should return host *.example.com",
service: serviceUpdate{
ServiceName: "foo",
Attributes: []string{
"traefik.frontend.rule=Host:*.example.com",
},
},
expected: "Host:*.example.com",
},
{
desc: "Should return host foo.example.com",
service: serviceUpdate{
ServiceName: "foo",
Attributes: []string{
"traefik.frontend.rule=Host:{{.ServiceName}}.example.com",
},
},
expected: "Host:foo.example.com",
},
{
desc: "Should return path prefix /bar",
service: serviceUpdate{
ServiceName: "foo",
Attributes: []string{
"traefik.frontend.rule=PathPrefix:{{getTag \"contextPath\" .Attributes \"/\"}}",
"contextPath=/bar",
},
},
expected: "PathPrefix:/bar",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := &Provider{
Domain: "localhost",
Prefix: "traefik",
FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}",
frontEndRuleTemplate: template.New("consul catalog frontend rule"),
}
p.setupFrontEndRuleTemplate()
labels := tagsToNeutralLabels(test.service.Attributes, p.Prefix)
test.service.TraefikLabels = labels
actual := p.getFrontendRule(test.service)
assert.Equal(t, test.expected, actual)
})
}
}
func TestGetBackendAddress(t *testing.T) {
testCases := []struct {
desc string
node *api.ServiceEntry
expected string
}{
{
desc: "Should return the address of the service",
node: &api.ServiceEntry{
Node: &api.Node{
Address: "10.1.0.1",
},
Service: &api.AgentService{
Address: "10.2.0.1",
},
},
expected: "10.2.0.1",
},
{
desc: "Should return the address of the node",
node: &api.ServiceEntry{
Node: &api.Node{
Address: "10.1.0.1",
},
Service: &api.AgentService{
Address: "",
},
},
expected: "10.1.0.1",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := getBackendAddress(test.node)
assert.Equal(t, test.expected, actual)
})
}
}
func TestGetServerName(t *testing.T) {
testCases := []struct {
desc string
node *api.ServiceEntry
expected string
}{
{
desc: "Should create backend name without tags",
node: &api.ServiceEntry{
Service: &api.AgentService{
Service: "api",
Address: "10.0.0.1",
Port: 80,
Tags: []string{},
},
},
expected: "api-0-eUSiqD6uNvvh6zxsY-OeRi8ZbaE",
},
{
desc: "Should create backend name with multiple tags",
node: &api.ServiceEntry{
Service: &api.AgentService{
Service: "api",
Address: "10.0.0.1",
Port: 80,
Tags: []string{"traefik.weight=42", "traefik.enable=true"},
},
},
expected: "api-1-eJ8MR2JxjXyZgs1bhurVa0-9OI8",
},
{
desc: "Should create backend name with one tag",
node: &api.ServiceEntry{
Service: &api.AgentService{
Service: "api",
Address: "10.0.0.1",
Port: 80,
Tags: []string{"a funny looking tag"},
},
},
expected: "api-2-lMCDCsG7sh0SCXOHo4oBOQB-9D4",
},
}
for i, test := range testCases {
test := test
i := i
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := getServerName(test.node, i)
assert.Equal(t, test.expected, actual)
})
}
}
func fakeLoadTraefikLabelsSlice(nodes []catalogUpdate, prefix string) []catalogUpdate {
var result []catalogUpdate
for _, node := range nodes {
labels := tagsToNeutralLabels(node.Service.Attributes, prefix)
node.Service.TraefikLabels = labels
result = append(result, node)
}
return result
}

View file

@ -2,6 +2,7 @@ package consulcatalog
import (
"errors"
"strconv"
"strings"
"sync"
"text/template"
@ -48,8 +49,9 @@ type Service struct {
}
type serviceUpdate struct {
ServiceName string
Attributes []string
ServiceName string
Attributes []string
TraefikLabels map[string]string
}
type catalogUpdate struct {
@ -446,10 +448,13 @@ func (p *Provider) healthyNodes(service string) (catalogUpdate, error) {
).(map[string]bool)).([]string)
}, []string{}, nodes).([]string)
labels := tagsToNeutralLabels(tags, p.Prefix)
return catalogUpdate{
Service: &serviceUpdate{
ServiceName: service,
Attributes: tags,
ServiceName: service,
Attributes: tags,
TraefikLabels: labels,
},
Nodes: nodes,
}, nil
@ -473,7 +478,18 @@ func (p *Provider) nodeFilter(service string, node *api.ServiceEntry) bool {
}
func (p *Provider) isServiceEnabled(node *api.ServiceEntry) bool {
return p.getBoolAttribute(label.SuffixEnable, node.Service.Tags, p.ExposedByDefault)
rawValue := getTag(p.getPrefixedName(label.SuffixEnable), node.Service.Tags, "")
if len(rawValue) == 0 {
return p.ExposedByDefault
}
value, err := strconv.ParseBool(rawValue)
if err != nil {
log.Errorf("Invalid value for %s: %s", label.SuffixEnable, rawValue)
return p.ExposedByDefault
}
return value
}
func (p *Provider) getConstraintTags(tags []string) []string {

View file

@ -1,589 +0,0 @@
package consulcatalog
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"math"
"sort"
"strconv"
"strings"
"text/template"
"github.com/containous/traefik/log"
"github.com/containous/traefik/provider"
"github.com/containous/traefik/provider/label"
"github.com/containous/traefik/types"
"github.com/hashicorp/consul/api"
)
func (p *Provider) buildConfiguration(catalog []catalogUpdate) *types.Configuration {
var FuncMap = template.FuncMap{
"getAttribute": p.getAttribute,
"getTag": getTag,
"hasTag": hasTag,
// Backend functions
"getBackend": getNodeBackendName, // TODO Deprecated [breaking] getBackend -> getNodeBackendName
"getNodeBackendName": getNodeBackendName,
"getServiceBackendName": getServiceBackendName,
"getBackendAddress": getBackendAddress,
"getBackendName": getServerName, // TODO Deprecated [breaking] getBackendName -> getServerName
"getServerName": getServerName,
"hasMaxconnAttributes": p.hasMaxConnAttributes, // TODO Deprecated [breaking]
"getSticky": p.getSticky, // TODO Deprecated [breaking]
"hasStickinessLabel": p.hasStickinessLabel, // TODO Deprecated [breaking]
"getStickinessCookieName": p.getStickinessCookieName, // TODO Deprecated [breaking]
"getWeight": p.getWeight, // TODO Deprecated [breaking] Must replaced by a simple: "getWeight": p.getFuncIntAttribute(label.SuffixWeight, 0)
"getProtocol": p.getFuncStringAttribute(label.SuffixProtocol, label.DefaultProtocol),
"getCircuitBreaker": p.getCircuitBreaker,
"getLoadBalancer": p.getLoadBalancer,
"getMaxConn": p.getMaxConn,
"getHealthCheck": p.getHealthCheck,
"getBuffering": p.getBuffering,
// Frontend functions
"getFrontendRule": p.getFrontendRule,
"getBasicAuth": p.getFuncSliceAttribute(label.SuffixFrontendAuthBasic),
"getEntryPoints": getEntryPoints, // TODO Deprecated [breaking]
"getFrontEndEntryPoints": p.getFuncSliceAttribute(label.SuffixFrontendEntryPoints), // TODO [breaking] rename to getEntryPoints when getEntryPoints will be removed
"getPriority": p.getFuncIntAttribute(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt),
"getPassHostHeader": p.getFuncBoolAttribute(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool),
"getPassTLSCert": p.getFuncBoolAttribute(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert),
"getWhiteList": p.getWhiteList,
"getRedirect": p.getRedirect,
"hasErrorPages": p.getFuncHasAttributePrefix(label.BaseFrontendErrorPage),
"getErrorPages": p.getErrorPages,
"hasRateLimit": p.getFuncHasAttributePrefix(label.BaseFrontendRateLimit),
"getRateLimit": p.getRateLimit,
"getHeaders": p.getHeaders,
}
var allNodes []*api.ServiceEntry
var services []*serviceUpdate
for _, info := range catalog {
if len(info.Nodes) > 0 {
services = append(services, info.Service)
allNodes = append(allNodes, info.Nodes...)
}
}
// Ensure a stable ordering of nodes so that identical configurations may be detected
sort.Sort(nodeSorter(allNodes))
templateObjects := struct {
Services []*serviceUpdate
Nodes []*api.ServiceEntry
}{
Services: services,
Nodes: allNodes,
}
configuration, err := p.GetConfiguration("templates/consul_catalog.tmpl", FuncMap, templateObjects)
if err != nil {
log.WithError(err).Error("Failed to create config")
}
return configuration
}
func (p *Provider) setupFrontEndRuleTemplate() {
var FuncMap = template.FuncMap{
"getAttribute": p.getAttribute,
"getTag": getTag,
"hasTag": hasTag,
}
tmpl := template.New("consul catalog frontend rule").Funcs(FuncMap)
p.frontEndRuleTemplate = tmpl
}
// Specific functions
func (p *Provider) getFrontendRule(service serviceUpdate) string {
customFrontendRule := p.getAttribute(label.SuffixFrontendRule, service.Attributes, "")
if customFrontendRule == "" {
customFrontendRule = p.FrontEndRule
}
tmpl := p.frontEndRuleTemplate
tmpl, err := tmpl.Parse(customFrontendRule)
if err != nil {
log.Errorf("Failed to parse Consul Catalog custom frontend rule: %v", err)
return ""
}
templateObjects := struct {
ServiceName string
Domain string
Attributes []string
}{
ServiceName: service.ServiceName,
Domain: p.Domain,
Attributes: service.Attributes,
}
var buffer bytes.Buffer
err = tmpl.Execute(&buffer, templateObjects)
if err != nil {
log.Errorf("Failed to execute Consul Catalog custom frontend rule template: %v", err)
return ""
}
return buffer.String()
}
// Deprecated
func (p *Provider) hasMaxConnAttributes(attributes []string) bool {
amount := p.getAttribute(label.SuffixBackendMaxConnAmount, attributes, "")
extractorFunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, attributes, "")
return amount != "" && extractorFunc != ""
}
// Deprecated
func getEntryPoints(list string) []string {
return strings.Split(list, ",")
}
func getNodeBackendName(node *api.ServiceEntry) string {
return strings.ToLower(node.Service.Service)
}
func getServiceBackendName(service *serviceUpdate) string {
return strings.ToLower(service.ServiceName)
}
func getBackendAddress(node *api.ServiceEntry) string {
if node.Service.Address != "" {
return node.Service.Address
}
return node.Node.Address
}
func getServerName(node *api.ServiceEntry, index int) string {
serviceName := node.Service.Service + node.Service.Address + strconv.Itoa(node.Service.Port)
// TODO sort tags ?
serviceName += strings.Join(node.Service.Tags, "")
hash := sha1.New()
_, err := hash.Write([]byte(serviceName))
if err != nil {
// Impossible case
log.Error(err)
} else {
serviceName = base64.URLEncoding.EncodeToString(hash.Sum(nil))
}
// unique int at the end
return provider.Normalize(node.Service.Service + "-" + strconv.Itoa(index) + "-" + serviceName)
}
// TODO: Deprecated
// replaced by Stickiness
// Deprecated
func (p *Provider) getSticky(tags []string) string {
stickyTag := p.getAttribute(label.SuffixBackendLoadBalancerSticky, tags, "")
if len(stickyTag) > 0 {
log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness)
} else {
stickyTag = "false"
}
return stickyTag
}
// Deprecated
func (p *Provider) hasStickinessLabel(tags []string) bool {
stickinessTag := p.getAttribute(label.SuffixBackendLoadBalancerStickiness, tags, "")
return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true")
}
// Deprecated
func (p *Provider) getStickinessCookieName(tags []string) string {
return p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, "")
}
// Deprecated
func (p *Provider) getWeight(tags []string) int {
weight := p.getIntAttribute(label.SuffixWeight, tags, label.DefaultWeightInt)
// Deprecated
deprecatedWeightTag := "backend." + label.SuffixWeight
if p.hasAttribute(deprecatedWeightTag, tags) {
log.Warnf("Deprecated configuration found: %s. Please use %s.",
p.getPrefixedName(deprecatedWeightTag), p.getPrefixedName(label.SuffixWeight))
weight = p.getIntAttribute(deprecatedWeightTag, tags, label.DefaultWeightInt)
}
return weight
}
func (p *Provider) getCircuitBreaker(tags []string) *types.CircuitBreaker {
circuitBreaker := p.getAttribute(label.SuffixBackendCircuitBreakerExpression, tags, "")
if p.hasAttribute(label.SuffixBackendCircuitBreaker, tags) {
log.Warnf("Deprecated configuration found: %s. Please use %s.",
p.getPrefixedName(label.SuffixBackendCircuitBreaker), p.getPrefixedName(label.SuffixBackendCircuitBreakerExpression))
circuitBreaker = p.getAttribute(label.SuffixBackendCircuitBreaker, tags, "")
}
if len(circuitBreaker) == 0 {
return nil
}
return &types.CircuitBreaker{Expression: circuitBreaker}
}
func (p *Provider) getLoadBalancer(tags []string) *types.LoadBalancer {
rawSticky := p.getSticky(tags)
sticky, err := strconv.ParseBool(rawSticky)
if err != nil {
log.Debugf("Invalid sticky value: %s", rawSticky)
sticky = false
}
method := p.getAttribute(label.SuffixBackendLoadBalancerMethod, tags, label.DefaultBackendLoadBalancerMethod)
// Deprecated
deprecatedMethodTag := "backend.loadbalancer"
if p.hasAttribute(deprecatedMethodTag, tags) {
log.Warnf("Deprecated configuration found: %s. Please use %s.",
p.getPrefixedName(deprecatedMethodTag), p.getPrefixedName(label.SuffixWeight))
method = p.getAttribute(deprecatedMethodTag, tags, label.SuffixBackendLoadBalancerMethod)
}
lb := &types.LoadBalancer{
Method: method,
Sticky: sticky,
}
if p.getBoolAttribute(label.SuffixBackendLoadBalancerStickiness, tags, false) {
lb.Stickiness = &types.Stickiness{
CookieName: p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, ""),
}
}
return lb
}
func (p *Provider) getMaxConn(tags []string) *types.MaxConn {
amount := p.getInt64Attribute(label.SuffixBackendMaxConnAmount, tags, math.MinInt64)
extractorFunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, tags, label.DefaultBackendMaxconnExtractorFunc)
if amount == math.MinInt64 || len(extractorFunc) == 0 {
return nil
}
return &types.MaxConn{
Amount: amount,
ExtractorFunc: extractorFunc,
}
}
func (p *Provider) getHealthCheck(tags []string) *types.HealthCheck {
path := p.getAttribute(label.SuffixBackendHealthCheckPath, tags, "")
if len(path) == 0 {
return nil
}
port := p.getIntAttribute(label.SuffixBackendHealthCheckPort, tags, label.DefaultBackendHealthCheckPort)
interval := p.getAttribute(label.SuffixBackendHealthCheckInterval, tags, "")
return &types.HealthCheck{
Path: path,
Port: port,
Interval: interval,
}
}
func (p *Provider) getBuffering(tags []string) *types.Buffering {
if !p.hasAttributePrefix(label.SuffixBackendBuffering, tags) {
return nil
}
return &types.Buffering{
MaxRequestBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMaxRequestBodyBytes, tags, 0),
MaxResponseBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMaxResponseBodyBytes, tags, 0),
MemRequestBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMemRequestBodyBytes, tags, 0),
MemResponseBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMemResponseBodyBytes, tags, 0),
RetryExpression: p.getAttribute(label.SuffixBackendBufferingRetryExpression, tags, ""),
}
}
func (p *Provider) getWhiteList(tags []string) *types.WhiteList {
ranges := p.getSliceAttribute(label.SuffixFrontendWhiteListSourceRange, tags)
if len(ranges) > 0 {
return &types.WhiteList{
SourceRange: ranges,
UseXForwardedFor: p.getBoolAttribute(label.SuffixFrontendWhiteListUseXForwardedFor, tags, false),
}
}
return nil
}
func (p *Provider) getRedirect(tags []string) *types.Redirect {
permanent := p.getBoolAttribute(label.SuffixFrontendRedirectPermanent, tags, false)
if p.hasAttribute(label.SuffixFrontendRedirectEntryPoint, tags) {
return &types.Redirect{
EntryPoint: p.getAttribute(label.SuffixFrontendRedirectEntryPoint, tags, ""),
Permanent: permanent,
}
}
if p.hasAttribute(label.SuffixFrontendRedirectRegex, tags) && p.hasAttribute(label.SuffixFrontendRedirectReplacement, tags) {
return &types.Redirect{
Regex: p.getAttribute(label.SuffixFrontendRedirectRegex, tags, ""),
Replacement: p.getAttribute(label.SuffixFrontendRedirectReplacement, tags, ""),
Permanent: permanent,
}
}
return nil
}
func (p *Provider) getErrorPages(tags []string) map[string]*types.ErrorPage {
labels := p.parseTagsToNeutralLabels(tags)
prefix := label.Prefix + label.BaseFrontendErrorPage
return label.ParseErrorPages(labels, prefix, label.RegexpFrontendErrorPage)
}
func (p *Provider) getRateLimit(tags []string) *types.RateLimit {
extractorFunc := p.getAttribute(label.SuffixFrontendRateLimitExtractorFunc, tags, "")
if len(extractorFunc) == 0 {
return nil
}
labels := p.parseTagsToNeutralLabels(tags)
prefix := label.Prefix + label.BaseFrontendRateLimit
limits := label.ParseRateSets(labels, prefix, label.RegexpFrontendRateLimit)
return &types.RateLimit{
ExtractorFunc: extractorFunc,
RateSet: limits,
}
}
func (p *Provider) getHeaders(tags []string) *types.Headers {
headers := &types.Headers{
CustomRequestHeaders: p.getMapAttribute(label.SuffixFrontendRequestHeaders, tags),
CustomResponseHeaders: p.getMapAttribute(label.SuffixFrontendResponseHeaders, tags),
SSLProxyHeaders: p.getMapAttribute(label.SuffixFrontendHeadersSSLProxyHeaders, tags),
AllowedHosts: p.getSliceAttribute(label.SuffixFrontendHeadersAllowedHosts, tags),
HostsProxyHeaders: p.getSliceAttribute(label.SuffixFrontendHeadersHostsProxyHeaders, tags),
SSLHost: p.getAttribute(label.SuffixFrontendHeadersSSLHost, tags, ""),
CustomFrameOptionsValue: p.getAttribute(label.SuffixFrontendHeadersCustomFrameOptionsValue, tags, ""),
ContentSecurityPolicy: p.getAttribute(label.SuffixFrontendHeadersContentSecurityPolicy, tags, ""),
PublicKey: p.getAttribute(label.SuffixFrontendHeadersPublicKey, tags, ""),
ReferrerPolicy: p.getAttribute(label.SuffixFrontendHeadersReferrerPolicy, tags, ""),
CustomBrowserXSSValue: p.getAttribute(label.SuffixFrontendHeadersCustomBrowserXSSValue, tags, ""),
STSSeconds: p.getInt64Attribute(label.SuffixFrontendHeadersSTSSeconds, tags, 0),
SSLRedirect: p.getBoolAttribute(label.SuffixFrontendHeadersSSLRedirect, tags, false),
SSLTemporaryRedirect: p.getBoolAttribute(label.SuffixFrontendHeadersSSLTemporaryRedirect, tags, false),
STSIncludeSubdomains: p.getBoolAttribute(label.SuffixFrontendHeadersSTSIncludeSubdomains, tags, false),
STSPreload: p.getBoolAttribute(label.SuffixFrontendHeadersSTSPreload, tags, false),
ForceSTSHeader: p.getBoolAttribute(label.SuffixFrontendHeadersForceSTSHeader, tags, false),
FrameDeny: p.getBoolAttribute(label.SuffixFrontendHeadersFrameDeny, tags, false),
ContentTypeNosniff: p.getBoolAttribute(label.SuffixFrontendHeadersContentTypeNosniff, tags, false),
BrowserXSSFilter: p.getBoolAttribute(label.SuffixFrontendHeadersBrowserXSSFilter, tags, false),
IsDevelopment: p.getBoolAttribute(label.SuffixFrontendHeadersIsDevelopment, tags, false),
}
if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() {
return nil
}
return headers
}
// Base functions
func (p *Provider) parseTagsToNeutralLabels(tags []string) map[string]string {
var labels map[string]string
for _, tag := range tags {
if strings.HasPrefix(tag, p.Prefix) {
parts := strings.SplitN(tag, "=", 2)
if len(parts) == 2 {
if labels == nil {
labels = make(map[string]string)
}
// replace custom prefix by the generic prefix
key := label.Prefix + strings.TrimPrefix(parts[0], p.Prefix+".")
labels[key] = parts[1]
}
}
}
return labels
}
func (p *Provider) getFuncStringAttribute(name string, defaultValue string) func(tags []string) string {
return func(tags []string) string {
return p.getAttribute(name, tags, defaultValue)
}
}
func (p *Provider) getFuncSliceAttribute(name string) func(tags []string) []string {
return func(tags []string) []string {
return p.getSliceAttribute(name, tags)
}
}
func (p *Provider) getMapAttribute(name string, tags []string) map[string]string {
rawValue := getTag(p.getPrefixedName(name), tags, "")
if len(rawValue) == 0 {
return nil
}
return label.ParseMapValue(p.getPrefixedName(name), rawValue)
}
func (p *Provider) getFuncIntAttribute(name string, defaultValue int) func(tags []string) int {
return func(tags []string) int {
return p.getIntAttribute(name, tags, defaultValue)
}
}
func (p *Provider) getFuncBoolAttribute(name string, defaultValue bool) func(tags []string) bool {
return func(tags []string) bool {
return p.getBoolAttribute(name, tags, defaultValue)
}
}
func (p *Provider) getFuncHasAttributePrefix(name string) func(tags []string) bool {
return func(tags []string) bool {
return p.hasAttributePrefix(name, tags)
}
}
func (p *Provider) getInt64Attribute(name string, tags []string, defaultValue int64) int64 {
rawValue := getTag(p.getPrefixedName(name), tags, "")
if len(rawValue) == 0 {
return defaultValue
}
value, err := strconv.ParseInt(rawValue, 10, 64)
if err != nil {
log.Errorf("Invalid value for %s: %s", name, rawValue)
return defaultValue
}
return value
}
func (p *Provider) getIntAttribute(name string, tags []string, defaultValue int) int {
rawValue := getTag(p.getPrefixedName(name), tags, "")
if len(rawValue) == 0 {
return defaultValue
}
value, err := strconv.Atoi(rawValue)
if err != nil {
log.Errorf("Invalid value for %s: %s", name, rawValue)
return defaultValue
}
return value
}
func (p *Provider) getSliceAttribute(name string, tags []string) []string {
rawValue := getTag(p.getPrefixedName(name), tags, "")
if len(rawValue) == 0 {
return nil
}
return label.SplitAndTrimString(rawValue, ",")
}
func (p *Provider) getBoolAttribute(name string, tags []string, defaultValue bool) bool {
rawValue := getTag(p.getPrefixedName(name), tags, "")
if len(rawValue) == 0 {
return defaultValue
}
value, err := strconv.ParseBool(rawValue)
if err != nil {
log.Errorf("Invalid value for %s: %s", name, rawValue)
return defaultValue
}
return value
}
func (p *Provider) hasAttribute(name string, tags []string) bool {
return hasTag(p.getPrefixedName(name), tags)
}
func (p *Provider) hasAttributePrefix(name string, tags []string) bool {
return hasTagPrefix(p.getPrefixedName(name), tags)
}
func (p *Provider) getAttribute(name string, tags []string, defaultValue string) string {
return getTag(p.getPrefixedName(name), tags, defaultValue)
}
func (p *Provider) getPrefixedName(name string) string {
if len(p.Prefix) > 0 && len(name) > 0 {
return p.Prefix + "." + name
}
return name
}
func hasTag(name string, tags []string) bool {
lowerName := strings.ToLower(name)
for _, tag := range tags {
lowerTag := strings.ToLower(tag)
// Given the nature of Consul tags, which could be either singular markers, or key=value pairs
if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName {
return true
}
}
return false
}
func hasTagPrefix(name string, tags []string) bool {
lowerName := strings.ToLower(name)
for _, tag := range tags {
lowerTag := strings.ToLower(tag)
if strings.HasPrefix(lowerTag, lowerName) {
return true
}
}
return false
}
func getTag(name string, tags []string, defaultValue string) string {
lowerName := strings.ToLower(name)
for _, tag := range tags {
lowerTag := strings.ToLower(tag)
// Given the nature of Consul tags, which could be either singular markers, or key=value pairs
if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName {
// In case, where a tag might be a key=value, try to split it by the first '='
kv := strings.SplitN(tag, "=", 2)
// 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
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
package consulcatalog
import (
"strings"
"github.com/containous/traefik/provider/label"
)
func tagsToNeutralLabels(tags []string, prefix string) map[string]string {
var labels map[string]string
for _, tag := range tags {
if strings.HasPrefix(tag, prefix) {
parts := strings.SplitN(tag, "=", 2)
if len(parts) == 2 {
if labels == nil {
labels = make(map[string]string)
}
// replace custom prefix by the generic prefix
key := label.Prefix + strings.TrimPrefix(parts[0], prefix+".")
labels[key] = parts[1]
}
}
}
return labels
}

View file

@ -0,0 +1,64 @@
package consulcatalog
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTagsToNeutralLabels(t *testing.T) {
testCases := []struct {
desc string
tags []string
prefix string
expected map[string]string
}{
{
desc: "without tags",
expected: nil,
},
{
desc: "with a prefix",
prefix: "test",
tags: []string{
"test.aaa=01",
"test.bbb=02",
"ccc=03",
"test.ddd=04=to",
},
expected: map[string]string{
"traefik.aaa": "01",
"traefik.bbb": "02",
"traefik.ddd": "04=to",
},
},
{
desc: "with an empty prefix",
prefix: "",
tags: []string{
"test.aaa=01",
"test.bbb=02",
"ccc=03",
"test.ddd=04=to",
},
expected: map[string]string{
"traefik.test.aaa": "01",
"traefik.test.bbb": "02",
"traefik.ccc": "03",
"traefik.test.ddd": "04=to",
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
labels := tagsToNeutralLabels(test.tags, test.prefix)
assert.Equal(t, test.expected, labels)
})
}
}

View file

@ -0,0 +1,250 @@
package consulcatalog
import (
"bytes"
"sort"
"strconv"
"strings"
"text/template"
"github.com/containous/traefik/log"
"github.com/containous/traefik/provider/label"
"github.com/containous/traefik/types"
"github.com/hashicorp/consul/api"
)
// Deprecated
func (p *Provider) buildConfigurationV1(catalog []catalogUpdate) *types.Configuration {
var FuncMap = template.FuncMap{
"getAttribute": p.getAttribute,
"getTag": getTag,
"hasTag": hasTag,
// Backend functions
"getBackend": getNodeBackendName,
"getServiceBackendName": getServiceBackendName,
"getBackendAddress": getBackendAddress,
"getBackendName": getServerName,
"hasMaxconnAttributes": p.hasMaxConnAttributesV1,
"getSticky": p.getStickyV1,
"hasStickinessLabel": p.hasStickinessLabelV1,
"getStickinessCookieName": p.getStickinessCookieNameV1,
"getWeight": p.getWeight,
"getProtocol": p.getFuncStringAttribute(label.SuffixProtocol, label.DefaultProtocol),
// Frontend functions
"getFrontendRule": p.getFrontendRuleV1,
"getBasicAuth": p.getFuncSliceAttribute(label.SuffixFrontendAuthBasic),
"getEntryPoints": getEntryPointsV1,
"getPriority": p.getFuncIntAttribute(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt),
"getPassHostHeader": p.getFuncBoolAttribute(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool),
"getPassTLSCert": p.getFuncBoolAttribute(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert),
}
var allNodes []*api.ServiceEntry
var services []*serviceUpdate
for _, info := range catalog {
if len(info.Nodes) > 0 {
services = append(services, info.Service)
allNodes = append(allNodes, info.Nodes...)
}
}
// Ensure a stable ordering of nodes so that identical configurations may be detected
sort.Sort(nodeSorter(allNodes))
templateObjects := struct {
Services []*serviceUpdate
Nodes []*api.ServiceEntry
}{
Services: services,
Nodes: allNodes,
}
configuration, err := p.GetConfiguration("templates/consul_catalog-v1.tmpl", FuncMap, templateObjects)
if err != nil {
log.WithError(err).Error("Failed to create config")
}
return configuration
}
// Specific functions
// Deprecated
func (p *Provider) getFrontendRuleV1(service serviceUpdate) string {
customFrontendRule := p.getAttribute(label.SuffixFrontendRule, service.Attributes, "")
if customFrontendRule == "" {
customFrontendRule = p.FrontEndRule
}
tmpl := p.frontEndRuleTemplate
tmpl, err := tmpl.Parse(customFrontendRule)
if err != nil {
log.Errorf("Failed to parse Consul Catalog custom frontend rule: %v", err)
return ""
}
templateObjects := struct {
ServiceName string
Domain string
Attributes []string
}{
ServiceName: service.ServiceName,
Domain: p.Domain,
Attributes: service.Attributes,
}
var buffer bytes.Buffer
err = tmpl.Execute(&buffer, templateObjects)
if err != nil {
log.Errorf("Failed to execute Consul Catalog custom frontend rule template: %v", err)
return ""
}
return buffer.String()
}
// Deprecated
func (p *Provider) hasMaxConnAttributesV1(attributes []string) bool {
amount := p.getAttribute(label.SuffixBackendMaxConnAmount, attributes, "")
extractorFunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, attributes, "")
return amount != "" && extractorFunc != ""
}
// Deprecated
func getEntryPointsV1(list string) []string {
return strings.Split(list, ",")
}
// TODO: Deprecated
// replaced by Stickiness
// Deprecated
func (p *Provider) getStickyV1(tags []string) string {
stickyTag := p.getAttribute(label.SuffixBackendLoadBalancerSticky, tags, "")
if len(stickyTag) > 0 {
log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness)
} else {
stickyTag = "false"
}
return stickyTag
}
// Deprecated
func (p *Provider) hasStickinessLabelV1(tags []string) bool {
stickinessTag := p.getAttribute(label.SuffixBackendLoadBalancerStickiness, tags, "")
return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true")
}
// Deprecated
func (p *Provider) getStickinessCookieNameV1(tags []string) string {
return p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, "")
}
// Base functions
// Deprecated
func (p *Provider) getFuncStringAttribute(name string, defaultValue string) func(tags []string) string {
return func(tags []string) string {
return p.getAttribute(name, tags, defaultValue)
}
}
// Deprecated
func (p *Provider) getFuncSliceAttribute(name string) func(tags []string) []string {
return func(tags []string) []string {
return p.getSliceAttribute(name, tags)
}
}
// Deprecated
func (p *Provider) getMapAttribute(name string, tags []string) map[string]string {
rawValue := getTag(p.getPrefixedName(name), tags, "")
if len(rawValue) == 0 {
return nil
}
return label.ParseMapValue(p.getPrefixedName(name), rawValue)
}
// Deprecated
func (p *Provider) getFuncIntAttribute(name string, defaultValue int) func(tags []string) int {
return func(tags []string) int {
return p.getIntAttribute(name, tags, defaultValue)
}
}
func (p *Provider) getFuncBoolAttribute(name string, defaultValue bool) func(tags []string) bool {
return func(tags []string) bool {
return p.getBoolAttribute(name, tags, defaultValue)
}
}
// Deprecated
func (p *Provider) getFuncHasAttributePrefix(name string) func(tags []string) bool {
return func(tags []string) bool {
return p.hasAttributePrefix(name, tags)
}
}
// Deprecated
func (p *Provider) getInt64Attribute(name string, tags []string, defaultValue int64) int64 {
rawValue := getTag(p.getPrefixedName(name), tags, "")
if len(rawValue) == 0 {
return defaultValue
}
value, err := strconv.ParseInt(rawValue, 10, 64)
if err != nil {
log.Errorf("Invalid value for %s: %s", name, rawValue)
return defaultValue
}
return value
}
// Deprecated
func (p *Provider) getIntAttribute(name string, tags []string, defaultValue int) int {
rawValue := getTag(p.getPrefixedName(name), tags, "")
if len(rawValue) == 0 {
return defaultValue
}
value, err := strconv.Atoi(rawValue)
if err != nil {
log.Errorf("Invalid value for %s: %s", name, rawValue)
return defaultValue
}
return value
}
// Deprecated
func (p *Provider) getSliceAttribute(name string, tags []string) []string {
rawValue := getTag(p.getPrefixedName(name), tags, "")
if len(rawValue) == 0 {
return nil
}
return label.SplitAndTrimString(rawValue, ",")
}
// Deprecated
func (p *Provider) getBoolAttribute(name string, tags []string, defaultValue bool) bool {
rawValue := getTag(p.getPrefixedName(name), tags, "")
if len(rawValue) == 0 {
return defaultValue
}
value, err := strconv.ParseBool(rawValue)
if err != nil {
log.Errorf("Invalid value for %s: %s", name, rawValue)
return defaultValue
}
return value
}
func (p *Provider) hasAttributePrefix(name string, tags []string) bool {
return hasTagPrefix(p.getPrefixedName(name), tags)
}

View file

@ -0,0 +1,444 @@
package consulcatalog
import (
"testing"
"text/template"
"github.com/containous/traefik/provider/label"
"github.com/containous/traefik/types"
"github.com/hashicorp/consul/api"
"github.com/stretchr/testify/assert"
)
func TestProviderBuildConfigurationV1(t *testing.T) {
p := &Provider{
Domain: "localhost",
Prefix: "traefik",
ExposedByDefault: false,
FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}",
frontEndRuleTemplate: template.New("consul catalog frontend rule"),
}
testCases := []struct {
desc string
nodes []catalogUpdate
expectedFrontends map[string]*types.Frontend
expectedBackends map[string]*types.Backend
}{
{
desc: "Should build config of nothing",
nodes: []catalogUpdate{},
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
desc: "Should build config with no frontend and backend",
nodes: []catalogUpdate{
{
Service: &serviceUpdate{
ServiceName: "test",
},
},
},
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
desc: "Should build config who contains one frontend and one backend",
nodes: []catalogUpdate{
{
Service: &serviceUpdate{
ServiceName: "test",
Attributes: []string{
"random.foo=bar",
label.TraefikBackendLoadBalancer + "=drr",
label.TraefikBackendCircuitBreaker + "=NetworkErrorRatio() > 0.5",
label.TraefikBackendMaxConnAmount + "=1000",
label.TraefikBackendMaxConnExtractorFunc + "=client.ip",
label.TraefikFrontendAuthBasic + "=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
},
},
Nodes: []*api.ServiceEntry{
{
Service: &api.AgentService{
Service: "test",
Address: "127.0.0.1",
Port: 80,
Tags: []string{
"random.foo=bar",
label.Prefix + "backend.weight=42",
label.TraefikFrontendPassHostHeader + "=true",
label.TraefikProtocol + "=https",
},
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.1",
},
},
},
},
},
expectedFrontends: map[string]*types.Frontend{
"frontend-test": {
Backend: "backend-test",
PassHostHeader: true,
Routes: map[string]types.Route{
"route-host-test": {
Rule: "Host:test.localhost",
},
},
BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test": {
Servers: map[string]types.Server{
"test-0-us4-27hAOu2ARV7nNrmv6GoKlcA": {
URL: "https://127.0.0.1:80",
Weight: 42,
},
},
CircuitBreaker: &types.CircuitBreaker{
Expression: "NetworkErrorRatio() > 0.5",
},
LoadBalancer: &types.LoadBalancer{
Method: "drr",
},
MaxConn: &types.MaxConn{
Amount: 1000,
ExtractorFunc: "client.ip",
},
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actualConfig := p.buildConfigurationV1(test.nodes)
assert.NotNil(t, actualConfig)
assert.Equal(t, test.expectedBackends, actualConfig.Backends)
assert.Equal(t, test.expectedFrontends, actualConfig.Frontends)
})
}
}
func TestProviderGetIntAttributeV1(t *testing.T) {
p := &Provider{
Prefix: "traefik",
}
testCases := []struct {
desc string
name string
tags []string
defaultValue int
expected int
}{
{
desc: "should return default value when empty name",
name: "",
tags: []string{"traefik.foo=10"},
defaultValue: 666,
expected: 666,
},
{
desc: "should return default value when empty tags",
name: "traefik.foo",
tags: nil,
expected: 0,
},
{
desc: "should return default value when value is not a int",
name: "foo",
tags: []string{"traefik.foo=bar"},
expected: 0,
},
{
desc: "should return a value when tag exist",
name: "foo",
tags: []string{"traefik.foo=10"},
expected: 10,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
result := p.getIntAttribute(test.name, test.tags, test.defaultValue)
assert.Equal(t, test.expected, result)
})
}
}
func TestProviderGetInt64AttributeV1(t *testing.T) {
p := &Provider{
Prefix: "traefik",
}
testCases := []struct {
desc string
name string
tags []string
defaultValue int64
expected int64
}{
{
desc: "should return default value when empty name",
name: "",
tags: []string{"traefik.foo=10"},
defaultValue: 666,
expected: 666,
},
{
desc: "should return default value when empty tags",
name: "traefik.foo",
tags: nil,
expected: 0,
},
{
desc: "should return default value when value is not a int",
name: "foo",
tags: []string{"traefik.foo=bar"},
expected: 0,
},
{
desc: "should return a value when tag exist",
name: "foo",
tags: []string{"traefik.foo=10"},
expected: 10,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
result := p.getInt64Attribute(test.name, test.tags, test.defaultValue)
assert.Equal(t, test.expected, result)
})
}
}
func TestProviderGetBoolAttributeV1(t *testing.T) {
p := &Provider{
Prefix: "traefik",
}
testCases := []struct {
desc string
name string
tags []string
defaultValue bool
expected bool
}{
{
desc: "should return default value when empty name",
name: "",
tags: []string{"traefik.foo=true"},
defaultValue: true,
expected: true,
},
{
desc: "should return default value when empty tags",
name: "traefik.foo",
tags: nil,
expected: false,
},
{
desc: "should return default value when value is not a bool",
name: "foo",
tags: []string{"traefik.foo=bar"},
expected: false,
},
{
desc: "should return a value when tag exist",
name: "foo",
tags: []string{"traefik.foo=true"},
expected: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
result := p.getBoolAttribute(test.name, test.tags, test.defaultValue)
assert.Equal(t, test.expected, result)
})
}
}
func TestProviderGetSliceAttributeV1(t *testing.T) {
p := &Provider{
Prefix: "traefik",
}
testCases := []struct {
desc string
name string
tags []string
expected []string
}{
{
desc: "should return nil when empty name",
name: "",
tags: []string{"traefik.foo=bar,bor,bir"},
expected: nil,
},
{
desc: "should return nil when empty tags",
name: "foo",
tags: nil,
expected: nil,
},
{
desc: "should return nil when tag doesn't have value",
name: "",
tags: []string{"traefik.foo="},
expected: nil,
},
{
desc: "should return a slice when tag contains comma separated values",
name: "foo",
tags: []string{"traefik.foo=bar,bor,bir"},
expected: []string{"bar", "bor", "bir"},
},
{
desc: "should return a slice when tag contains one value",
name: "foo",
tags: []string{"traefik.foo=bar"},
expected: []string{"bar"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
result := p.getSliceAttribute(test.name, test.tags)
assert.Equal(t, test.expected, result)
})
}
}
func TestProviderGetFrontendRuleV1(t *testing.T) {
testCases := []struct {
desc string
service serviceUpdate
expected string
}{
{
desc: "Should return default host foo.localhost",
service: serviceUpdate{
ServiceName: "foo",
Attributes: []string{},
},
expected: "Host:foo.localhost",
},
{
desc: "Should return host *.example.com",
service: serviceUpdate{
ServiceName: "foo",
Attributes: []string{
"traefik.frontend.rule=Host:*.example.com",
},
},
expected: "Host:*.example.com",
},
{
desc: "Should return host foo.example.com",
service: serviceUpdate{
ServiceName: "foo",
Attributes: []string{
"traefik.frontend.rule=Host:{{.ServiceName}}.example.com",
},
},
expected: "Host:foo.example.com",
},
{
desc: "Should return path prefix /bar",
service: serviceUpdate{
ServiceName: "foo",
Attributes: []string{
"traefik.frontend.rule=PathPrefix:{{getTag \"contextPath\" .Attributes \"/\"}}",
"contextPath=/bar",
},
},
expected: "PathPrefix:/bar",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := &Provider{
Domain: "localhost",
Prefix: "traefik",
FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}",
frontEndRuleTemplate: template.New("consul catalog frontend rule"),
}
p.setupFrontEndRuleTemplate()
actual := p.getFrontendRuleV1(test.service)
assert.Equal(t, test.expected, actual)
})
}
}
func TestHasStickinessLabelV1(t *testing.T) {
p := &Provider{
Prefix: "traefik",
}
testCases := []struct {
desc string
tags []string
expected bool
}{
{
desc: "label missing",
tags: []string{},
expected: false,
},
{
desc: "stickiness=true",
tags: []string{
label.TraefikBackendLoadBalancerStickiness + "=true",
},
expected: true,
},
{
desc: "stickiness=false",
tags: []string{
label.TraefikBackendLoadBalancerStickiness + "=false",
},
expected: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := p.hasStickinessLabelV1(test.tags)
assert.Equal(t, test.expected, actual)
})
}
}

View file

@ -0,0 +1,56 @@
[backends]
{{range $index, $node := .Nodes }}
[backends."backend-{{ getBackend $node }}".servers."{{ getBackendName $node $index }}"]
url = "{{ getAttribute "protocol" $node.Service.Tags "http" }}://{{ getBackendAddress $node }}:{{ $node.Service.Port }}"
{{ $weight := getAttribute "backend.weight" $node.Service.Tags "0" }}
{{with $weight }}
weight = {{ $weight }}
{{end}}
{{end}}
{{range .Services }}
{{ $service := .ServiceName }}
{{ $circuitBreaker := getAttribute "backend.circuitbreaker" .Attributes "" }}
{{with $circuitBreaker }}
[backends."backend-{{ $service }}".circuitbreaker]
expression = "{{ $circuitBreaker }}"
{{end}}
[backends."backend-{{ $service }}".loadbalancer]
method = "{{ getAttribute "backend.loadbalancer" .Attributes "wrr" }}"
sticky = {{ getSticky .Attributes }}
{{if hasStickinessLabel .Attributes }}
[backends."backend-{{ $service }}".loadbalancer.stickiness]
cookieName = "{{ getStickinessCookieName .Attributes }}"
{{end}}
{{if hasMaxconnAttributes .Attributes }}
[backends."backend-{{ $service }}".maxconn]
amount = {{ getAttribute "backend.maxconn.amount" .Attributes "" }}
extractorfunc = "{{ getAttribute "backend.maxconn.extractorfunc" .Attributes "" }}"
{{end}}
{{end}}
[frontends]
{{range .Services }}
[frontends."frontend-{{ .ServiceName }}"]
backend = "backend-{{ .ServiceName }}"
passHostHeader = {{ getAttribute "frontend.passHostHeader" .Attributes "true" }}
priority = {{ getAttribute "frontend.priority" .Attributes "0" }}
{{ $entryPoints := getAttribute "frontend.entrypoints" .Attributes "" }}
{{with $entryPoints }}
entrypoints = [{{range getEntryPoints $entryPoints }}
"{{ . }}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth .Attributes }}
"{{ . }}",
{{end}}]
[frontends."frontend-{{ .ServiceName }}".routes."route-host-{{ .ServiceName }}"]
rule = "{{ getFrontendRule . }}"
{{end}}

View file

@ -2,13 +2,13 @@
{{range $service := .Services}}
{{ $backendName := getServiceBackendName $service }}
{{ $circuitBreaker := getCircuitBreaker $service.Attributes }}
{{ $circuitBreaker := getCircuitBreaker $service.TraefikLabels }}
{{if $circuitBreaker }}
[backends."backend-{{ $backendName }}".circuitBreaker]
expression = "{{ $circuitBreaker.Expression }}"
{{end}}
{{ $loadBalancer := getLoadBalancer $service.Attributes }}
{{ $loadBalancer := getLoadBalancer $service.TraefikLabels }}
{{if $loadBalancer }}
[backends."backend-{{ $backendName }}".loadBalancer]
method = "{{ $loadBalancer.Method }}"
@ -19,14 +19,14 @@
{{end}}
{{end}}
{{ $maxConn := getMaxConn $service.Attributes }}
{{ $maxConn := getMaxConn $service.TraefikLabels }}
{{if $maxConn }}
[backends."backend-{{ $backendName }}".maxConn]
extractorFunc = "{{ $maxConn.ExtractorFunc }}"
amount = {{ $maxConn.Amount }}
{{end}}
{{ $healthCheck := getHealthCheck $service.Attributes }}
{{ $healthCheck := getHealthCheck $service.TraefikLabels }}
{{if $healthCheck }}
[backends."backend-{{ $backendName }}".healthCheck]
path = "{{ $healthCheck.Path }}"
@ -34,7 +34,7 @@
interval = "{{ $healthCheck.Interval }}"
{{end}}
{{ $buffering := getBuffering $service.Attributes }}
{{ $buffering := getBuffering $service.TraefikLabels }}
{{if $buffering }}
[backends."backend-{{ $backendName }}".buffering]
maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }}
@ -46,10 +46,10 @@
{{end}}
{{range $index, $node := .Nodes}}
{{ $server := getServer $node }}
[backends."backend-{{ getNodeBackendName $node }}".servers."{{ getServerName $node $index }}"]
url = "{{ getProtocol $node.Service.Tags }}://{{ getBackendAddress $node }}:{{ $node.Service.Port }}"
weight = {{ getWeight $node.Service.Tags }}
url = "{{ $server.URL }}"
weight = {{ $server.Weight }}
{{end}}
@ -58,19 +58,19 @@
[frontends."frontend-{{ $service.ServiceName }}"]
backend = "backend-{{ getServiceBackendName $service }}"
priority = {{ getPriority $service.Attributes }}
passHostHeader = {{ getPassHostHeader $service.Attributes }}
passTLSCert = {{ getPassTLSCert $service.Attributes }}
priority = {{ getPriority $service.TraefikLabels }}
passHostHeader = {{ getPassHostHeader $service.TraefikLabels }}
passTLSCert = {{ getPassTLSCert $service.TraefikLabels }}
entryPoints = [{{range getFrontEndEntryPoints $service.Attributes }}
entryPoints = [{{range getFrontEndEntryPoints $service.TraefikLabels }}
"{{.}}",
{{end}}]
basicAuth = [{{range getBasicAuth $service.Attributes }}
basicAuth = [{{range getBasicAuth $service.TraefikLabels }}
"{{.}}",
{{end}}]
{{ $whitelist := getWhiteList $service.Attributes }}
{{ $whitelist := getWhiteList $service.TraefikLabels }}
{{if $whitelist }}
[frontends."frontend-{{ $service.ServiceName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
@ -79,7 +79,7 @@
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $service.Attributes }}
{{ $redirect := getRedirect $service.TraefikLabels }}
{{if $redirect }}
[frontends."frontend-{{ $service.ServiceName }}".redirect]
entryPoint = "{{ $redirect.EntryPoint }}"
@ -88,9 +88,10 @@
permanent = {{ $redirect.Permanent }}
{{end}}
{{if hasErrorPages $service.Attributes }}
{{ $errorPages := getErrorPages $service.TraefikLabels }}
{{if $errorPages }}
[frontends."frontend-{{ $service.ServiceName }}".errors]
{{range $pageName, $page := getErrorPages $service.Attributes }}
{{range $pageName, $page := $errorPages }}
[frontends."frontend-{{ $service.ServiceName }}".errors."{{ $pageName }}"]
status = [{{range $page.Status }}
"{{.}}",
@ -100,22 +101,20 @@
{{end}}
{{end}}
{{if hasRateLimit $service.Attributes }}
{{ $rateLimit := getRateLimit $service.Attributes }}
{{ $rateLimit := getRateLimit $service.TraefikLabels }}
{{if $rateLimit }}
[frontends."frontend-{{ $service.ServiceName }}".rateLimit]
extractorFunc = "{{ $rateLimit.ExtractorFunc }}"
[frontends."frontend-{{ $service.ServiceName }}".rateLimit.rateSet]
{{range $limitName, $limit := $rateLimit.RateSet }}
{{ range $limitName, $limit := $rateLimit.RateSet }}
[frontends."frontend-{{ $service.ServiceName }}".rateLimit.rateSet."{{ $limitName }}"]
period = "{{ $limit.Period }}"
average = {{ $limit.Average }}
burst = {{ $limit.Burst }}
{{end}}
{{end}}
{{ $headers := getHeaders $service.Attributes }}
{{ $headers := getHeaders $service.TraefikLabels }}
{{if $headers }}
[frontends."frontend-{{ $service.ServiceName }}".headers]
SSLRedirect = {{ $headers.SSLRedirect }}