refactor(consulCatalog): review and split.
This commit is contained in:
parent
c705d6f9b3
commit
1710800cc0
3 changed files with 544 additions and 402 deletions
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
167
provider/consul/consul_catalog_config.go
Normal file
167
provider/consul/consul_catalog_config.go
Normal 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, "")
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue