refactor(consulCatalog): review and split.

This commit is contained in:
Fernandez Ludovic 2017-12-04 20:02:15 +01:00 committed by Traefiker
parent c705d6f9b3
commit 1710800cc0
3 changed files with 544 additions and 402 deletions

View file

@ -1,9 +1,7 @@
package consul package consul
import ( import (
"bytes"
"errors" "errors"
"sort"
"strconv" "strconv"
"strings" "strings"
"text/template" "text/template"
@ -14,6 +12,7 @@ import (
"github.com/containous/traefik/job" "github.com/containous/traefik/job"
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
"github.com/containous/traefik/provider" "github.com/containous/traefik/provider"
"github.com/containous/traefik/provider/label"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
@ -38,6 +37,13 @@ type CatalogProvider struct {
frontEndRuleTemplate *template.Template frontEndRuleTemplate *template.Template
} }
// Service represent a Consul service.
type Service struct {
Name string
Tags []string
Nodes []string
}
type serviceUpdate struct { type serviceUpdate struct {
ServiceName string ServiceName string
Attributes []string Attributes []string
@ -59,63 +65,149 @@ func (a nodeSorter) Swap(i int, j int) {
} }
func (a nodeSorter) Less(i int, j int) bool { func (a nodeSorter) Less(i int, j int) bool {
lentr := a[i] lEntry := a[i]
rentr := a[j] rEntry := a[j]
ls := strings.ToLower(lentr.Service.Service) ls := strings.ToLower(lEntry.Service.Service)
lr := strings.ToLower(rentr.Service.Service) lr := strings.ToLower(rEntry.Service.Service)
if ls != lr { if ls != lr {
return ls < lr return ls < lr
} }
if lentr.Service.Address != rentr.Service.Address { if lEntry.Service.Address != rEntry.Service.Address {
return lentr.Service.Address < rentr.Service.Address return lEntry.Service.Address < rEntry.Service.Address
} }
if lentr.Node.Address != rentr.Node.Address { if lEntry.Node.Address != rEntry.Node.Address {
return lentr.Node.Address < rentr.Node.Address return lEntry.Node.Address < rEntry.Node.Address
} }
return lentr.Service.Port < rentr.Service.Port return lEntry.Service.Port < rEntry.Service.Port
} }
func hasChanged(current map[string]Service, previous map[string]Service) bool { // Provide allows the consul catalog provider to provide configurations to traefik
addedServiceKeys, removedServiceKeys := getChangedServiceKeys(current, previous) // using the given configuration channel.
return len(removedServiceKeys) > 0 || len(addedServiceKeys) > 0 || hasNodeOrTagsChanged(current, previous) func (p *CatalogProvider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
config := api.DefaultConfig()
config.Address = p.Endpoint
client, err := api.NewClient(config)
if err != nil {
return err
}
p.client = client
p.Constraints = append(p.Constraints, constraints...)
p.setupFrontEndTemplate()
pool.Go(func(stop chan bool) {
notify := func(err error, time time.Duration) {
log.Errorf("Consul connection error %+v, retrying in %s", err, time)
}
operation := func() error {
return p.watch(configurationChan, stop)
}
errRetry := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
if errRetry != nil {
log.Errorf("Cannot connect to consul server %+v", errRetry)
}
})
return err
} }
func getChangedServiceKeys(current map[string]Service, previous map[string]Service) ([]string, []string) { func (p *CatalogProvider) watch(configurationChan chan<- types.ConfigMessage, stop chan bool) error {
currKeySet := fun.Set(fun.Keys(current).([]string)).(map[string]bool) stopCh := make(chan struct{})
prevKeySet := fun.Set(fun.Keys(previous).([]string)).(map[string]bool) watchCh := make(chan map[string][]string)
errorCh := make(chan error)
addedKeys := fun.Difference(currKeySet, prevKeySet).(map[string]bool) p.watchHealthState(stopCh, watchCh, errorCh)
removedKeys := fun.Difference(prevKeySet, currKeySet).(map[string]bool) p.watchCatalogServices(stopCh, watchCh, errorCh)
return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string) defer close(stopCh)
} defer close(watchCh)
func hasNodeOrTagsChanged(current map[string]Service, previous map[string]Service) bool { for {
var added []string select {
var removed []string case <-stop:
for key, value := range current { return nil
if prevValue, ok := previous[key]; ok { case index, ok := <-watchCh:
addedNodesKeys, removedNodesKeys := getChangedStringKeys(value.Nodes, prevValue.Nodes) if !ok {
added = append(added, addedNodesKeys...) return errors.New("Consul service list nil")
removed = append(removed, removedNodesKeys...) }
addedTagsKeys, removedTagsKeys := getChangedStringKeys(value.Tags, prevValue.Tags) log.Debug("List of services changed")
added = append(added, addedTagsKeys...) nodes, err := p.getNodes(index)
removed = append(removed, removedTagsKeys...) if err != nil {
return err
}
configuration := p.buildConfiguration(nodes)
configurationChan <- types.ConfigMessage{
ProviderName: "consul_catalog",
Configuration: configuration,
}
case err := <-errorCh:
return err
} }
} }
return len(added) > 0 || len(removed) > 0
} }
func getChangedStringKeys(currState []string, prevState []string) ([]string, []string) { func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh chan<- map[string][]string, errorCh chan<- error) {
currKeySet := fun.Set(currState).(map[string]bool) catalog := p.client.Catalog()
prevKeySet := fun.Set(prevState).(map[string]bool)
addedKeys := fun.Difference(currKeySet, prevKeySet).(map[string]bool) safe.Go(func() {
removedKeys := fun.Difference(prevKeySet, currKeySet).(map[string]bool) // variable to hold previous state
var flashback map[string]Service
return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string) options := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
for {
select {
case <-stopCh:
return
default:
}
data, meta, err := catalog.Services(options)
if err != nil {
log.Errorf("Failed to list services: %v", err)
errorCh <- err
return
}
if options.WaitIndex == meta.LastIndex {
continue
}
options.WaitIndex = meta.LastIndex
if data != nil {
current := make(map[string]Service)
for key, value := range data {
nodes, _, err := catalog.Service(key, "", &api.QueryOptions{})
if err != nil {
log.Errorf("Failed to get detail of service %s: %v", key, err)
errorCh <- err
return
}
nodesID := getServiceIds(nodes)
if service, ok := current[key]; ok {
service.Tags = value
service.Nodes = nodesID
} else {
service := Service{
Name: key,
Tags: value,
Nodes: nodesID,
}
current[key] = service
}
}
// A critical note is that the return of a blocking request is no guarantee of a change.
// It is possible that there was an idempotent write that does not affect the result of the query.
// Thus it is required to do extra check for changes...
if hasChanged(current, flashback) {
watchCh <- data
flashback = current
}
}
}
})
} }
func (p *CatalogProvider) watchHealthState(stopCh <-chan struct{}, watchCh chan<- map[string][]string, errorCh chan<- error) { func (p *CatalogProvider) watchHealthState(stopCh <-chan struct{}, watchCh chan<- map[string][]string, errorCh chan<- error) {
@ -162,7 +254,7 @@ func (p *CatalogProvider) watchHealthState(stopCh <-chan struct{}, watchCh chan<
// The response should be unified with watchCatalogServices // The response should be unified with watchCatalogServices
data, _, err := catalog.Services(&api.QueryOptions{}) data, _, err := catalog.Services(&api.QueryOptions{})
if err != nil { if err != nil {
log.Errorf("Failed to list services: %s", err) log.Errorf("Failed to list services: %v", err)
errorCh <- err errorCh <- err
return return
} }
@ -189,74 +281,67 @@ func (p *CatalogProvider) watchHealthState(stopCh <-chan struct{}, watchCh chan<
}) })
} }
// Service represent a Consul service. func (p *CatalogProvider) getNodes(index map[string][]string) ([]catalogUpdate, error) {
type Service struct { visited := make(map[string]bool)
Name string
Tags []string
Nodes []string
}
func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh chan<- map[string][]string, errorCh chan<- error) { var nodes []catalogUpdate
catalog := p.client.Catalog() for service := range index {
name := strings.ToLower(service)
safe.Go(func() { if !strings.Contains(name, " ") && !visited[name] {
// variable to hold previous state visited[name] = true
var flashback map[string]Service log.WithField("service", name).Debug("Fetching service")
healthy, err := p.healthyNodes(name)
options := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
for {
select {
case <-stopCh:
return
default:
}
data, meta, err := catalog.Services(options)
if err != nil { if err != nil {
log.Errorf("Failed to list services: %s", err) return nil, err
errorCh <- err
return
} }
// healthy.Nodes can be empty if constraints do not match, without throwing error
if options.WaitIndex == meta.LastIndex { if healthy.Service != nil && len(healthy.Nodes) > 0 {
continue nodes = append(nodes, healthy)
}
options.WaitIndex = meta.LastIndex
if data != nil {
current := make(map[string]Service)
for key, value := range data {
nodes, _, err := catalog.Service(key, "", &api.QueryOptions{})
if err != nil {
log.Errorf("Failed to get detail of service %s: %s", key, err)
errorCh <- err
return
}
nodesID := getServiceIds(nodes)
if service, ok := current[key]; ok {
service.Tags = value
service.Nodes = nodesID
} else {
service := Service{
Name: key,
Tags: value,
Nodes: nodesID,
}
current[key] = service
}
}
// A critical note is that the return of a blocking request is no guarantee of a change.
// It is possible that there was an idempotent write that does not affect the result of the query.
// Thus it is required to do extra check for changes...
if hasChanged(current, flashback) {
watchCh <- data
flashback = current
}
} }
} }
}) }
return nodes, nil
}
func hasChanged(current map[string]Service, previous map[string]Service) bool {
addedServiceKeys, removedServiceKeys := getChangedServiceKeys(current, previous)
return len(removedServiceKeys) > 0 || len(addedServiceKeys) > 0 || hasNodeOrTagsChanged(current, previous)
}
func getChangedServiceKeys(current map[string]Service, previous map[string]Service) ([]string, []string) {
currKeySet := fun.Set(fun.Keys(current).([]string)).(map[string]bool)
prevKeySet := fun.Set(fun.Keys(previous).([]string)).(map[string]bool)
addedKeys := fun.Difference(currKeySet, prevKeySet).(map[string]bool)
removedKeys := fun.Difference(prevKeySet, currKeySet).(map[string]bool)
return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string)
}
func hasNodeOrTagsChanged(current map[string]Service, previous map[string]Service) bool {
var added []string
var removed []string
for key, value := range current {
if prevValue, ok := previous[key]; ok {
addedNodesKeys, removedNodesKeys := getChangedStringKeys(value.Nodes, prevValue.Nodes)
added = append(added, addedNodesKeys...)
removed = append(removed, removedNodesKeys...)
addedTagsKeys, removedTagsKeys := getChangedStringKeys(value.Tags, prevValue.Tags)
added = append(added, addedTagsKeys...)
removed = append(removed, removedTagsKeys...)
}
}
return len(added) > 0 || len(removed) > 0
}
func getChangedStringKeys(currState []string, prevState []string) ([]string, []string) {
currKeySet := fun.Set(currState).(map[string]bool)
prevKeySet := fun.Set(prevState).(map[string]bool)
addedKeys := fun.Difference(currKeySet, prevKeySet).(map[string]bool)
removedKeys := fun.Difference(prevKeySet, currKeySet).(map[string]bool)
return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string)
} }
func getServiceIds(services []*api.CatalogService) []string { func getServiceIds(services []*api.CatalogService) []string {
@ -314,7 +399,7 @@ func (p *CatalogProvider) nodeFilter(service string, node *api.ServiceEntry) boo
} }
func (p *CatalogProvider) isServiceEnabled(node *api.ServiceEntry) bool { func (p *CatalogProvider) isServiceEnabled(node *api.ServiceEntry) bool {
enable, err := strconv.ParseBool(p.getAttribute("enable", node.Service.Tags, strconv.FormatBool(p.ExposedByDefault))) enable, err := strconv.ParseBool(p.getAttribute(label.SuffixEnable, node.Service.Tags, strconv.FormatBool(p.ExposedByDefault)))
if err != nil { if err != nil {
log.Debugf("Invalid value for enable, set to %b", p.ExposedByDefault) log.Debugf("Invalid value for enable, set to %b", p.ExposedByDefault)
return p.ExposedByDefault return p.ExposedByDefault
@ -323,116 +408,26 @@ func (p *CatalogProvider) isServiceEnabled(node *api.ServiceEntry) bool {
} }
func (p *CatalogProvider) getPrefixedName(name string) string { func (p *CatalogProvider) getPrefixedName(name string) string {
if len(p.Prefix) > 0 { if len(p.Prefix) > 0 && len(name) > 0 {
return p.Prefix + "." + name return p.Prefix + "." + name
} }
return name return name
} }
func (p *CatalogProvider) getEntryPoints(list string) []string {
return strings.Split(list, ",")
}
func (p *CatalogProvider) getBackend(node *api.ServiceEntry) string {
return strings.ToLower(node.Service.Service)
}
func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string {
customFrontendRule := p.getAttribute("frontend.rule", service.Attributes, "")
if customFrontendRule == "" {
customFrontendRule = p.FrontEndRule
}
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 {
if node.Service.Address != "" {
return node.Service.Address
}
return node.Node.Address
}
func (p *CatalogProvider) getBackendName(node *api.ServiceEntry, index int) string {
serviceName := strings.ToLower(node.Service.Service) + "--" + node.Service.Address + "--" + strconv.Itoa(node.Service.Port)
for _, tag := range node.Service.Tags {
serviceName += "--" + provider.Normalize(tag)
}
serviceName = strings.Replace(serviceName, ".", "-", -1)
serviceName = strings.Replace(serviceName, "=", "-", -1)
// unique int at the end
serviceName += "--" + strconv.Itoa(index)
return serviceName
}
func (p *CatalogProvider) getBasicAuth(tags []string) []string {
list := p.getAttribute("frontend.auth.basic", tags, "")
if list != "" {
return strings.Split(list, ",")
}
return []string{}
}
func (p *CatalogProvider) getSticky(tags []string) string {
stickyTag := p.getTag(types.LabelBackendLoadbalancerSticky, tags, "")
if len(stickyTag) > 0 {
log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness)
} else {
stickyTag = "false"
}
return stickyTag
}
func (p *CatalogProvider) hasStickinessLabel(tags []string) bool {
stickinessTag := p.getTag(types.LabelBackendLoadbalancerStickiness, tags, "")
return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true")
}
func (p *CatalogProvider) getStickinessCookieName(tags []string) string {
return p.getTag(types.LabelBackendLoadbalancerStickinessCookieName, tags, "")
}
func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string { func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string {
return p.getTag(p.getPrefixedName(name), tags, defaultValue) return getTag(p.getPrefixedName(name), tags, defaultValue)
} }
func (p *CatalogProvider) hasTag(name string, tags []string) bool { func hasTag(name string, tags []string) bool {
// Very-very unlikely that a Consul tag would ever start with '=!=' // Very-very unlikely that a Consul tag would ever start with '=!='
tag := p.getTag(name, tags, "=!=") tag := getTag(name, tags, "=!=")
return tag != "=!=" return tag != "=!="
} }
func (p *CatalogProvider) getTag(name string, tags []string, defaultValue string) string { func getTag(name string, tags []string, defaultValue string) string {
for _, tag := range tags { for _, tag := range tags {
// 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' // 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 { if strings.HasPrefix(strings.ToLower(tag), strings.ToLower(name)) {
// In case, where a tag might be a key=value, try to split it by the first '=' // 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 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 kv := strings.SplitN(tag, "=", 2); strings.ToLower(kv[0]) == strings.ToLower(name) {
@ -449,165 +444,17 @@ func (p *CatalogProvider) getTag(name string, tags []string, defaultValue string
} }
func (p *CatalogProvider) getConstraintTags(tags []string) []string { func (p *CatalogProvider) getConstraintTags(tags []string) []string {
var list []string var values []string
prefix := p.getPrefixedName("tags=")
for _, tag := range tags { for _, tag := range tags {
// We look for a Consul tag named 'traefik.tags' (unless different 'prefix' is configured) // 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 strings.HasPrefix(strings.ToLower(tag), prefix) {
// If 'traefik.tags=' tag is found, take the tag value and split by ',' adding the result to the list to be returned // 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=")):], ",") splitedTags := label.SplitAndTrimString(tag[len(prefix):], ",")
list = append(list, splitedTags...) values = append(values, splitedTags...)
} }
} }
return list return values
}
func (p *CatalogProvider) buildConfig(catalog []catalogUpdate) *types.Configuration {
var FuncMap = template.FuncMap{
"getBackend": p.getBackend,
"getFrontendRule": p.getFrontendRule,
"getBackendName": p.getBackendName,
"getBackendAddress": p.getBackendAddress,
"getBasicAuth": p.getBasicAuth,
"getSticky": p.getSticky,
"hasStickinessLabel": p.hasStickinessLabel,
"getStickinessCookieName": p.getStickinessCookieName,
"getAttribute": p.getAttribute,
"getTag": p.getTag,
"hasTag": p.hasTag,
"getEntryPoints": p.getEntryPoints,
"hasMaxconnAttributes": p.hasMaxconnAttributes,
}
allNodes := []*api.ServiceEntry{}
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 *CatalogProvider) hasMaxconnAttributes(attributes []string) bool {
amount := p.getAttribute("backend.maxconn.amount", attributes, "")
extractorfunc := p.getAttribute("backend.maxconn.extractorfunc", attributes, "")
if amount != "" && extractorfunc != "" {
return true
}
return false
}
func (p *CatalogProvider) getNodes(index map[string][]string) ([]catalogUpdate, error) {
visited := make(map[string]bool)
nodes := []catalogUpdate{}
for service := range index {
name := strings.ToLower(service)
if !strings.Contains(name, " ") && !visited[name] {
visited[name] = true
log.WithField("service", name).Debug("Fetching service")
healthy, err := p.healthyNodes(name)
if err != nil {
return nil, err
}
// healthy.Nodes can be empty if constraints do not match, without throwing error
if healthy.Service != nil && len(healthy.Nodes) > 0 {
nodes = append(nodes, healthy)
}
}
}
return nodes, nil
}
func (p *CatalogProvider) watch(configurationChan chan<- types.ConfigMessage, stop chan bool) error {
stopCh := make(chan struct{})
watchCh := make(chan map[string][]string)
errorCh := make(chan error)
p.watchHealthState(stopCh, watchCh, errorCh)
p.watchCatalogServices(stopCh, watchCh, errorCh)
defer close(stopCh)
defer close(watchCh)
for {
select {
case <-stop:
return nil
case index, ok := <-watchCh:
if !ok {
return errors.New("Consul service list nil")
}
log.Debug("List of services changed")
nodes, err := p.getNodes(index)
if err != nil {
return err
}
configuration := p.buildConfig(nodes)
configurationChan <- types.ConfigMessage{
ProviderName: "consul_catalog",
Configuration: configuration,
}
case err := <-errorCh:
return err
}
}
}
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 {
config := api.DefaultConfig()
config.Address = p.Endpoint
client, err := api.NewClient(config)
if err != nil {
return err
}
p.client = client
p.Constraints = append(p.Constraints, constraints...)
p.setupFrontEndTemplate()
pool.Go(func(stop chan bool) {
notify := func(err error, time time.Duration) {
log.Errorf("Consul connection error %+v, retrying in %s", err, time)
}
operation := func() error {
return p.watch(configurationChan, stop)
}
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
if err != nil {
log.Errorf("Cannot connect to consul server %+v", err)
}
})
return err
} }

View file

@ -0,0 +1,167 @@
package consul
import (
"bytes"
"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 *CatalogProvider) buildConfiguration(catalog []catalogUpdate) *types.Configuration {
var FuncMap = template.FuncMap{
"getBackend": getBackend,
"getFrontendRule": p.getFrontendRule,
"getBackendName": getBackendName,
"getBackendAddress": getBackendAddress,
"getBasicAuth": p.getBasicAuth,
"getSticky": getSticky,
"hasStickinessLabel": hasStickinessLabel,
"getStickinessCookieName": getStickinessCookieName,
"getAttribute": p.getAttribute,
"getTag": getTag,
"hasTag": hasTag,
"getEntryPoints": getEntryPoints,
"hasMaxconnAttributes": p.hasMaxconnAttributes,
}
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 *CatalogProvider) setupFrontEndTemplate() {
var FuncMap = template.FuncMap{
"getAttribute": p.getAttribute,
"getTag": getTag,
"hasTag": hasTag,
}
tmpl := template.New("consul catalog frontend rule").Funcs(FuncMap)
p.frontEndRuleTemplate = tmpl
}
func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string {
customFrontendRule := p.getAttribute("frontend.rule", 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()
}
func (p *CatalogProvider) getBasicAuth(tags []string) []string {
list := p.getAttribute("frontend.auth.basic", tags, "")
if list != "" {
return strings.Split(list, ",")
}
return []string{}
}
func (p *CatalogProvider) hasMaxconnAttributes(attributes []string) bool {
amount := p.getAttribute("backend.maxconn.amount", attributes, "")
extractorfunc := p.getAttribute("backend.maxconn.extractorfunc", attributes, "")
return amount != "" && extractorfunc != ""
}
func getEntryPoints(list string) []string {
return strings.Split(list, ",")
}
func getBackend(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 getBackendName(node *api.ServiceEntry, index int) string {
serviceName := strings.ToLower(node.Service.Service) + "--" + node.Service.Address + "--" + strconv.Itoa(node.Service.Port)
for _, tag := range node.Service.Tags {
serviceName += "--" + provider.Normalize(tag)
}
serviceName = strings.Replace(serviceName, ".", "-", -1)
serviceName = strings.Replace(serviceName, "=", "-", -1)
// unique int at the end
serviceName += "--" + strconv.Itoa(index)
return serviceName
}
// TODO: Deprecated
// Deprecated replaced by Stickiness
func getSticky(tags []string) string {
stickyTag := getTag(label.TraefikBackendLoadBalancerSticky, tags, "")
if len(stickyTag) > 0 {
log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness)
} else {
stickyTag = "false"
}
return stickyTag
}
func hasStickinessLabel(tags []string) bool {
stickinessTag := getTag(label.TraefikBackendLoadBalancerStickiness, tags, "")
return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true")
}
func getStickinessCookieName(tags []string) string {
return getTag(label.TraefikBackendLoadBalancerStickinessCookieName, tags, "")
}

View file

@ -6,12 +6,60 @@ import (
"text/template" "text/template"
"github.com/BurntSushi/ty/fun" "github.com/BurntSushi/ty/fun"
"github.com/containous/traefik/provider/label"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestConsulCatalogGetFrontendRule(t *testing.T) { func TestGetPrefixedName(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()
pro := &CatalogProvider{Prefix: test.prefix}
actual := pro.getPrefixedName(test.name)
assert.Equal(t, test.expected, actual)
})
}
}
func TestGetFrontendRule(t *testing.T) {
provider := &CatalogProvider{ provider := &CatalogProvider{
Domain: "localhost", Domain: "localhost",
Prefix: "traefik", Prefix: "traefik",
@ -77,12 +125,7 @@ func TestConsulCatalogGetFrontendRule(t *testing.T) {
} }
} }
func TestConsulCatalogGetTag(t *testing.T) { func TestGetTag(t *testing.T) {
provider := &CatalogProvider{
Domain: "localhost",
Prefix: "traefik",
}
testCases := []struct { testCases := []struct {
desc string desc string
tags []string tags []string
@ -101,23 +144,69 @@ func TestConsulCatalogGetTag(t *testing.T) {
defaultValue: "0", defaultValue: "0",
expected: "random", 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",
},
} }
assert.Equal(t, true, provider.hasTag("management", []string{"management"}))
assert.Equal(t, true, provider.hasTag("management", []string{"management=yes"}))
for _, test := range testCases { for _, test := range testCases {
test := test test := test
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := provider.getTag(test.key, test.tags, test.defaultValue) actual := getTag(test.key, test.tags, test.defaultValue)
assert.Equal(t, test.expected, actual) assert.Equal(t, test.expected, actual)
}) })
} }
} }
func TestConsulCatalogGetAttribute(t *testing.T) { 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 TestGetAttribute(t *testing.T) {
provider := &CatalogProvider{ provider := &CatalogProvider{
Domain: "localhost", Domain: "localhost",
Prefix: "traefik", Prefix: "traefik",
@ -152,8 +241,6 @@ func TestConsulCatalogGetAttribute(t *testing.T) {
}, },
} }
assert.Equal(t, provider.Prefix+".foo", provider.getPrefixedName("foo"))
for _, test := range testCases { for _, test := range testCases {
test := test test := test
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
@ -165,7 +252,7 @@ func TestConsulCatalogGetAttribute(t *testing.T) {
} }
} }
func TestConsulCatalogGetAttributeWithEmptyPrefix(t *testing.T) { func TestGetAttributeWithEmptyPrefix(t *testing.T) {
provider := &CatalogProvider{ provider := &CatalogProvider{
Domain: "localhost", Domain: "localhost",
Prefix: "", Prefix: "",
@ -210,8 +297,6 @@ func TestConsulCatalogGetAttributeWithEmptyPrefix(t *testing.T) {
}, },
} }
assert.Equal(t, "foo", provider.getPrefixedName("foo"))
for _, test := range testCases { for _, test := range testCases {
test := test test := test
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
@ -223,12 +308,7 @@ func TestConsulCatalogGetAttributeWithEmptyPrefix(t *testing.T) {
} }
} }
func TestConsulCatalogGetBackendAddress(t *testing.T) { func TestGetBackendAddress(t *testing.T) {
provider := &CatalogProvider{
Domain: "localhost",
Prefix: "traefik",
}
testCases := []struct { testCases := []struct {
desc string desc string
node *api.ServiceEntry node *api.ServiceEntry
@ -265,18 +345,13 @@ func TestConsulCatalogGetBackendAddress(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := provider.getBackendAddress(test.node) actual := getBackendAddress(test.node)
assert.Equal(t, test.expected, actual) assert.Equal(t, test.expected, actual)
}) })
} }
} }
func TestConsulCatalogGetBackendName(t *testing.T) { func TestGetBackendName(t *testing.T) {
provider := &CatalogProvider{
Domain: "localhost",
Prefix: "traefik",
}
testCases := []struct { testCases := []struct {
desc string desc string
node *api.ServiceEntry node *api.ServiceEntry
@ -326,13 +401,13 @@ func TestConsulCatalogGetBackendName(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := provider.getBackendName(test.node, i) actual := getBackendName(test.node, i)
assert.Equal(t, test.expected, actual) assert.Equal(t, test.expected, actual)
}) })
} }
} }
func TestConsulCatalogBuildConfig(t *testing.T) { func TestBuildConfiguration(t *testing.T) {
provider := &CatalogProvider{ provider := &CatalogProvider{
Domain: "localhost", Domain: "localhost",
Prefix: "traefik", Prefix: "traefik",
@ -441,14 +516,14 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
actualConfig := provider.buildConfig(test.nodes) actualConfig := provider.buildConfiguration(test.nodes)
assert.Equal(t, test.expectedBackends, actualConfig.Backends) assert.Equal(t, test.expectedBackends, actualConfig.Backends)
assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) assert.Equal(t, test.expectedFrontends, actualConfig.Frontends)
}) })
} }
} }
func TestConsulCatalogNodeSorter(t *testing.T) { func TestNodeSorter(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
nodes []*api.ServiceEntry nodes []*api.ServiceEntry
@ -648,7 +723,7 @@ func TestConsulCatalogNodeSorter(t *testing.T) {
} }
} }
func TestConsulCatalogGetChangedKeys(t *testing.T) { func TestGetChangedKeys(t *testing.T) {
type Input struct { type Input struct {
currState map[string]Service currState map[string]Service
prevState map[string]Service prevState map[string]Service
@ -794,7 +869,7 @@ func TestConsulCatalogGetChangedKeys(t *testing.T) {
} }
} }
func TestConsulCatalogFilterEnabled(t *testing.T) { func TestFilterEnabled(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
exposedByDefault bool exposedByDefault bool
@ -896,7 +971,7 @@ func TestConsulCatalogFilterEnabled(t *testing.T) {
} }
} }
func TestConsulCatalogGetBasicAuth(t *testing.T) { func TestGetBasicAuth(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
tags []string tags []string
@ -929,7 +1004,7 @@ func TestConsulCatalogGetBasicAuth(t *testing.T) {
} }
} }
func TestConsulCatalogHasStickinessLabel(t *testing.T) { func TestHasStickinessLabel(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
tags []string tags []string
@ -943,35 +1018,31 @@ func TestConsulCatalogHasStickinessLabel(t *testing.T) {
{ {
desc: "stickiness=true", desc: "stickiness=true",
tags: []string{ tags: []string{
types.LabelBackendLoadbalancerStickiness + "=true", label.TraefikBackendLoadBalancerStickiness + "=true",
}, },
expected: true, expected: true,
}, },
{ {
desc: "stickiness=false", desc: "stickiness=false",
tags: []string{ tags: []string{
types.LabelBackendLoadbalancerStickiness + "=false", label.TraefikBackendLoadBalancerStickiness + "=false",
}, },
expected: false, expected: false,
}, },
} }
provider := &CatalogProvider{
Prefix: "traefik",
}
for _, test := range testCases { for _, test := range testCases {
test := test test := test
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := provider.hasStickinessLabel(test.tags) actual := hasStickinessLabel(test.tags)
assert.Equal(t, test.expected, actual) assert.Equal(t, test.expected, actual)
}) })
} }
} }
func TestConsulCatalogGetChangedStringKeys(t *testing.T) { func TestGetChangedStringKeys(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
current []string current []string
@ -1020,7 +1091,7 @@ func TestConsulCatalogGetChangedStringKeys(t *testing.T) {
} }
} }
func TestConsulCatalogHasNodeOrTagschanged(t *testing.T) { func TestHasNodeOrTagschanged(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
current map[string]Service current map[string]Service
@ -1124,7 +1195,7 @@ func TestConsulCatalogHasNodeOrTagschanged(t *testing.T) {
} }
} }
func TestConsulCatalogHasChanged(t *testing.T) { func TestHasChanged(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
current map[string]Service current map[string]Service
@ -1239,3 +1310,60 @@ func TestConsulCatalogHasChanged(t *testing.T) {
}) })
} }
} }
func TestGetConstraintTags(t *testing.T) {
provider := &CatalogProvider{
Domain: "localhost",
Prefix: "traefik",
}
testCases := []struct {
desc string
tags []string
expected []string
}{
{
desc: "nil tags",
},
{
desc: "invalid tag",
tags: []string{"tags=foobar"},
expected: nil,
},
{
desc: "wrong tag",
tags: []string{"traefik_tags=foobar"},
expected: nil,
},
{
desc: "empty value",
tags: []string{"traefik.tags="},
expected: nil,
},
{
desc: "simple tag",
tags: []string{"traefik.tags=foobar "},
expected: []string{"foobar"},
},
{
desc: "multiple values tag",
tags: []string{"traefik.tags=foobar, fiibir"},
expected: []string{"foobar", "fiibir"},
},
{
desc: "multiple tags",
tags: []string{"traefik.tags=foobar", "traefik.tags=foobor"},
expected: []string{"foobar", "foobor"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
constraints := provider.getConstraintTags(test.tags)
assert.EqualValues(t, test.expected, constraints)
})
}
}