1
0
Fork 0

Split consul and consul catalog.

This commit is contained in:
Ludovic Fernandez 2018-01-04 15:56:03 +01:00 committed by Traefiker
parent 06d528a2bd
commit 61ecb4cd18
7 changed files with 89 additions and 86 deletions

View file

@ -1,450 +0,0 @@
package consul
import (
"errors"
"strings"
"text/template"
"time"
"github.com/BurntSushi/ty/fun"
"github.com/cenk/backoff"
"github.com/containous/traefik/job"
"github.com/containous/traefik/log"
"github.com/containous/traefik/provider"
"github.com/containous/traefik/provider/label"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
"github.com/hashicorp/consul/api"
)
const (
// DefaultWatchWaitTime is the duration to wait when polling consul
DefaultWatchWaitTime = 15 * time.Second
)
var _ provider.Provider = (*CatalogProvider)(nil)
// CatalogProvider holds configurations of the Consul catalog provider.
type CatalogProvider struct {
provider.BaseProvider `mapstructure:",squash" export:"true"`
Endpoint string `description:"Consul server endpoint"`
Domain string `description:"Default domain used"`
ExposedByDefault bool `description:"Expose Consul services by default" export:"true"`
Prefix string `description:"Prefix used for Consul catalog tags" export:"true"`
FrontEndRule string `description:"Frontend rule used for Consul services" export:"true"`
client *api.Client
frontEndRuleTemplate *template.Template
}
// Service represent a Consul service.
type Service struct {
Name string
Tags []string
Nodes []string
Ports []int
}
type serviceUpdate struct {
ServiceName string
Attributes []string
}
type catalogUpdate struct {
Service *serviceUpdate
Nodes []*api.ServiceEntry
}
type nodeSorter []*api.ServiceEntry
func (a nodeSorter) Len() int {
return len(a)
}
func (a nodeSorter) Swap(i int, j int) {
a[i], a[j] = a[j], a[i]
}
func (a nodeSorter) Less(i int, j int) bool {
lEntry := a[i]
rEntry := a[j]
ls := strings.ToLower(lEntry.Service.Service)
lr := strings.ToLower(rEntry.Service.Service)
if ls != lr {
return ls < lr
}
if lEntry.Service.Address != rEntry.Service.Address {
return lEntry.Service.Address < rEntry.Service.Address
}
if lEntry.Node.Address != rEntry.Node.Address {
return lEntry.Node.Address < rEntry.Node.Address
}
return lEntry.Service.Port < rEntry.Service.Port
}
// 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.setupFrontEndRuleTemplate()
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 (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.buildConfiguration(nodes)
configurationChan <- types.ConfigMessage{
ProviderName: "consul_catalog",
Configuration: configuration,
}
case err := <-errorCh:
return err
}
}
}
func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh chan<- map[string][]string, errorCh chan<- error) {
catalog := p.client.Catalog()
safe.Go(func() {
// variable to hold previous state
var flashback map[string]Service
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)
ports := getServicePorts(nodes)
if service, ok := current[key]; ok {
service.Tags = value
service.Nodes = nodesID
service.Ports = ports
} else {
service := Service{
Name: key,
Tags: value,
Nodes: nodesID,
Ports: ports,
}
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) {
health := p.client.Health()
catalog := p.client.Catalog()
safe.Go(func() {
// variable to hold previous state
var flashback []string
options := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
for {
select {
case <-stopCh:
return
default:
}
// Listening to changes that leads to `passing` state or degrades from it.
healthyState, meta, err := health.State("passing", options)
if err != nil {
log.WithError(err).Error("Failed to retrieve health checks")
errorCh <- err
return
}
var current []string
if healthyState != nil {
for _, healthy := range healthyState {
current = append(current, healthy.ServiceID)
}
}
// If LastIndex didn't change then it means `Get` returned
// because of the WaitTime and the key didn't changed.
if options.WaitIndex == meta.LastIndex {
continue
}
options.WaitIndex = meta.LastIndex
// The response should be unified with watchCatalogServices
data, _, err := catalog.Services(&api.QueryOptions{})
if err != nil {
log.Errorf("Failed to list services: %v", err)
errorCh <- err
return
}
if data != nil {
// 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...
addedKeys, removedKeys := getChangedStringKeys(current, flashback)
if len(addedKeys) > 0 {
log.WithField("DiscoveredServices", addedKeys).Debug("Health State change detected.")
watchCh <- data
flashback = current
}
if len(removedKeys) > 0 {
log.WithField("MissingServices", removedKeys).Debug("Health State change detected.")
watchCh <- data
flashback = current
}
}
}
})
}
func (p *CatalogProvider) getNodes(index map[string][]string) ([]catalogUpdate, error) {
visited := make(map[string]bool)
var 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 hasChanged(current map[string]Service, previous map[string]Service) bool {
if len(current) != len(previous) {
return true
}
addedServiceKeys, removedServiceKeys := getChangedServiceKeys(current, previous)
return len(removedServiceKeys) > 0 || len(addedServiceKeys) > 0 || hasServiceChanged(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 hasServiceChanged(current map[string]Service, previous map[string]Service) bool {
for key, value := range current {
if prevValue, ok := previous[key]; ok {
addedNodesKeys, removedNodesKeys := getChangedStringKeys(value.Nodes, prevValue.Nodes)
if len(addedNodesKeys) > 0 || len(removedNodesKeys) > 0 {
return true
}
addedTagsKeys, removedTagsKeys := getChangedStringKeys(value.Tags, prevValue.Tags)
if len(addedTagsKeys) > 0 || len(removedTagsKeys) > 0 {
return true
}
addedPortsKeys, removedPortsKeys := getChangedIntKeys(value.Ports, prevValue.Ports)
if len(addedPortsKeys) > 0 || len(removedPortsKeys) > 0 {
return true
}
}
}
return false
}
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 getChangedIntKeys(currState []int, prevState []int) ([]int, []int) {
currKeySet := fun.Set(currState).(map[int]bool)
prevKeySet := fun.Set(prevState).(map[int]bool)
addedKeys := fun.Difference(currKeySet, prevKeySet).(map[int]bool)
removedKeys := fun.Difference(prevKeySet, currKeySet).(map[int]bool)
return fun.Keys(addedKeys).([]int), fun.Keys(removedKeys).([]int)
}
func getServiceIds(services []*api.CatalogService) []string {
var serviceIds []string
for _, service := range services {
serviceIds = append(serviceIds, service.ID)
}
return serviceIds
}
func getServicePorts(services []*api.CatalogService) []int {
var servicePorts []int
for _, service := range services {
servicePorts = append(servicePorts, service.ServicePort)
}
return servicePorts
}
func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) {
health := p.client.Health()
data, _, err := health.Service(service, "", true, &api.QueryOptions{})
if err != nil {
log.WithError(err).Errorf("Failed to fetch details of %s", service)
return catalogUpdate{}, err
}
nodes := fun.Filter(func(node *api.ServiceEntry) bool {
return p.nodeFilter(service, node)
}, data).([]*api.ServiceEntry)
// Merge tags of nodes matching constraints, in a single slice.
tags := fun.Foldl(func(node *api.ServiceEntry, set []string) []string {
return fun.Keys(fun.Union(
fun.Set(set),
fun.Set(node.Service.Tags),
).(map[string]bool)).([]string)
}, []string{}, nodes).([]string)
return catalogUpdate{
Service: &serviceUpdate{
ServiceName: service,
Attributes: tags,
},
Nodes: nodes,
}, nil
}
func (p *CatalogProvider) nodeFilter(service string, node *api.ServiceEntry) bool {
// Filter disabled application.
if !p.isServiceEnabled(node) {
log.Debugf("Filtering disabled Consul service %s", service)
return false
}
// Filter by constraints.
constraintTags := p.getConstraintTags(node.Service.Tags)
ok, failingConstraint := p.MatchConstraints(constraintTags)
if !ok && failingConstraint != nil {
log.Debugf("Service %v pruned by '%v' constraint", service, failingConstraint.String())
return false
}
return true
}
func (p *CatalogProvider) isServiceEnabled(node *api.ServiceEntry) bool {
return p.getBoolAttribute(label.SuffixEnable, node.Service.Tags, p.ExposedByDefault)
}
func (p *CatalogProvider) getConstraintTags(tags []string) []string {
var values []string
prefix := p.getPrefixedName("tags=")
for _, tag := range tags {
// We look for a Consul tag named 'traefik.tags' (unless different 'prefix' is configured)
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
splitedTags := label.SplitAndTrimString(tag[len(prefix):], ",")
values = append(values, splitedTags...)
}
}
return values
}

View file

@ -1,556 +0,0 @@
package consul
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 *CatalogProvider) buildConfiguration(catalog []catalogUpdate) *types.Configuration {
var FuncMap = template.FuncMap{
"getAttribute": p.getAttribute,
"getTag": getTag,
"hasTag": hasTag,
// Backend functions
"getBackend": getNodeBackendName, // Deprecated [breaking] getBackend -> getNodeBackendName
"getNodeBackendName": getNodeBackendName,
"getServiceBackendName": getServiceBackendName,
"getBackendAddress": getBackendAddress,
"getBackendName": getServerName, // Deprecated [breaking] getBackendName -> getServerName
"getServerName": getServerName,
"hasMaxconnAttributes": p.hasMaxConnAttributes, // Deprecated [breaking]
"getSticky": p.getSticky, // Deprecated [breaking]
"hasStickinessLabel": p.hasStickinessLabel, // Deprecated [breaking]
"getStickinessCookieName": p.getStickinessCookieName, // Deprecated [breaking]
"getWeight": p.getWeight, // 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,
// Frontend functions
"getFrontendRule": p.getFrontendRule,
"getBasicAuth": p.getFuncSliceAttribute(label.SuffixFrontendAuthBasic),
"getEntryPoints": getEntryPoints, // Deprecated [breaking]
"getFrontEndEntryPoints": p.getFuncSliceAttribute(label.SuffixFrontendEntryPoints), // TODO [breaking] rename to getEntryPoints when getEntryPoints will be removed
"getPriority": p.getFuncIntAttribute(label.SuffixFrontendPriority, 0),
"getPassHostHeader": p.getFuncBoolAttribute(label.SuffixFrontendPassHostHeader, true),
"getPassTLSCert": p.getFuncBoolAttribute(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert),
"getWhitelistSourceRange": p.getFuncSliceAttribute(label.SuffixFrontendWhitelistSourceRange),
"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 *CatalogProvider) 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 *CatalogProvider) 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 *CatalogProvider) 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 *CatalogProvider) 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 *CatalogProvider) hasStickinessLabel(tags []string) bool {
stickinessTag := p.getAttribute(label.SuffixBackendLoadBalancerStickiness, tags, "")
return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true")
}
// Deprecated
func (p *CatalogProvider) getStickinessCookieName(tags []string) string {
return p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, "")
}
// Deprecated
func (p *CatalogProvider) getWeight(tags []string) int {
weight := p.getIntAttribute(label.SuffixWeight, tags, 0)
// 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, 0)
}
return weight
}
func (p *CatalogProvider) 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 *CatalogProvider) 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 *CatalogProvider) 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 *CatalogProvider) 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 *CatalogProvider) getRedirect(tags []string) *types.Redirect {
if p.hasAttribute(label.SuffixFrontendRedirectEntryPoint, tags) {
return &types.Redirect{
EntryPoint: p.getAttribute(label.SuffixFrontendRedirectEntryPoint, tags, ""),
}
}
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, ""),
}
}
return nil
}
func (p *CatalogProvider) 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 *CatalogProvider) 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 *CatalogProvider) 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, ""),
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 *CatalogProvider) 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 *CatalogProvider) getFuncStringAttribute(name string, defaultValue string) func(tags []string) string {
return func(tags []string) string {
return p.getAttribute(name, tags, defaultValue)
}
}
func (p *CatalogProvider) getFuncSliceAttribute(name string) func(tags []string) []string {
return func(tags []string) []string {
return p.getSliceAttribute(name, tags)
}
}
func (p *CatalogProvider) 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 *CatalogProvider) getFuncIntAttribute(name string, defaultValue int) func(tags []string) int {
return func(tags []string) int {
return p.getIntAttribute(name, tags, defaultValue)
}
}
func (p *CatalogProvider) getFuncBoolAttribute(name string, defaultValue bool) func(tags []string) bool {
return func(tags []string) bool {
return p.getBoolAttribute(name, tags, defaultValue)
}
}
func (p *CatalogProvider) getFuncHasAttributePrefix(name string) func(tags []string) bool {
return func(tags []string) bool {
return p.hasAttributePrefix(name, tags)
}
}
func (p *CatalogProvider) 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 *CatalogProvider) 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 *CatalogProvider) getSliceAttribute(name string, tags []string) []string {
rawValue := getTag(p.getPrefixedName(name), tags, "")
if len(rawValue) == 0 {
return nil
}
return label.SplitAndTrimString(rawValue, ",")
}
func (p *CatalogProvider) 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 *CatalogProvider) hasAttribute(name string, tags []string) bool {
return hasTag(p.getPrefixedName(name), tags)
}
func (p *CatalogProvider) hasAttributePrefix(name string, tags []string) bool {
return hasTagPrefix(p.getPrefixedName(name), tags)
}
func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string {
return getTag(p.getPrefixedName(name), tags, defaultValue)
}
func (p *CatalogProvider) 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

@ -1,844 +0,0 @@
package consul
import (
"sort"
"testing"
"github.com/BurntSushi/ty/fun"
"github.com/hashicorp/consul/api"
"github.com/stretchr/testify/assert"
)
func TestNodeSorter(t *testing.T) {
testCases := []struct {
desc string
nodes []*api.ServiceEntry
expected []*api.ServiceEntry
}{
{
desc: "Should sort nothing",
nodes: []*api.ServiceEntry{},
expected: []*api.ServiceEntry{},
},
{
desc: "Should sort by node address",
nodes: []*api.ServiceEntry{
{
Service: &api.AgentService{
Service: "foo",
Address: "127.0.0.1",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.1",
},
},
},
expected: []*api.ServiceEntry{
{
Service: &api.AgentService{
Service: "foo",
Address: "127.0.0.1",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.1",
},
},
},
},
{
desc: "Should sort by service name",
nodes: []*api.ServiceEntry{
{
Service: &api.AgentService{
Service: "foo",
Address: "127.0.0.2",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.2",
},
},
{
Service: &api.AgentService{
Service: "bar",
Address: "127.0.0.2",
Port: 81,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.2",
},
},
{
Service: &api.AgentService{
Service: "foo",
Address: "127.0.0.1",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.1",
},
},
{
Service: &api.AgentService{
Service: "bar",
Address: "127.0.0.2",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.2",
},
},
},
expected: []*api.ServiceEntry{
{
Service: &api.AgentService{
Service: "bar",
Address: "127.0.0.2",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.2",
},
},
{
Service: &api.AgentService{
Service: "bar",
Address: "127.0.0.2",
Port: 81,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.2",
},
},
{
Service: &api.AgentService{
Service: "foo",
Address: "127.0.0.1",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.1",
},
},
{
Service: &api.AgentService{
Service: "foo",
Address: "127.0.0.2",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.2",
},
},
},
},
{
desc: "Should sort by node address",
nodes: []*api.ServiceEntry{
{
Service: &api.AgentService{
Service: "foo",
Address: "",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.2",
},
},
{
Service: &api.AgentService{
Service: "foo",
Address: "",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.1",
},
},
},
expected: []*api.ServiceEntry{
{
Service: &api.AgentService{
Service: "foo",
Address: "",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.1",
},
},
{
Service: &api.AgentService{
Service: "foo",
Address: "",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.2",
},
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
sort.Sort(nodeSorter(test.nodes))
actual := test.nodes
assert.Equal(t, test.expected, actual)
})
}
}
func TestGetChangedKeys(t *testing.T) {
type Input struct {
currState map[string]Service
prevState map[string]Service
}
type Output struct {
addedKeys []string
removedKeys []string
}
testCases := []struct {
desc string
input Input
output Output
}{
{
desc: "Should add 0 services and removed 0",
input: Input{
currState: map[string]Service{
"foo-service": {Name: "v1"},
"bar-service": {Name: "v1"},
"baz-service": {Name: "v1"},
"qux-service": {Name: "v1"},
"quux-service": {Name: "v1"},
"quuz-service": {Name: "v1"},
"corge-service": {Name: "v1"},
"grault-service": {Name: "v1"},
"garply-service": {Name: "v1"},
"waldo-service": {Name: "v1"},
"fred-service": {Name: "v1"},
"plugh-service": {Name: "v1"},
"xyzzy-service": {Name: "v1"},
"thud-service": {Name: "v1"},
},
prevState: map[string]Service{
"foo-service": {Name: "v1"},
"bar-service": {Name: "v1"},
"baz-service": {Name: "v1"},
"qux-service": {Name: "v1"},
"quux-service": {Name: "v1"},
"quuz-service": {Name: "v1"},
"corge-service": {Name: "v1"},
"grault-service": {Name: "v1"},
"garply-service": {Name: "v1"},
"waldo-service": {Name: "v1"},
"fred-service": {Name: "v1"},
"plugh-service": {Name: "v1"},
"xyzzy-service": {Name: "v1"},
"thud-service": {Name: "v1"},
},
},
output: Output{
addedKeys: []string{},
removedKeys: []string{},
},
},
{
desc: "Should add 3 services and removed 0",
input: Input{
currState: map[string]Service{
"foo-service": {Name: "v1"},
"bar-service": {Name: "v1"},
"baz-service": {Name: "v1"},
"qux-service": {Name: "v1"},
"quux-service": {Name: "v1"},
"quuz-service": {Name: "v1"},
"corge-service": {Name: "v1"},
"grault-service": {Name: "v1"},
"garply-service": {Name: "v1"},
"waldo-service": {Name: "v1"},
"fred-service": {Name: "v1"},
"plugh-service": {Name: "v1"},
"xyzzy-service": {Name: "v1"},
"thud-service": {Name: "v1"},
},
prevState: map[string]Service{
"foo-service": {Name: "v1"},
"bar-service": {Name: "v1"},
"baz-service": {Name: "v1"},
"corge-service": {Name: "v1"},
"grault-service": {Name: "v1"},
"garply-service": {Name: "v1"},
"waldo-service": {Name: "v1"},
"fred-service": {Name: "v1"},
"plugh-service": {Name: "v1"},
"xyzzy-service": {Name: "v1"},
"thud-service": {Name: "v1"},
},
},
output: Output{
addedKeys: []string{"qux-service", "quux-service", "quuz-service"},
removedKeys: []string{},
},
},
{
desc: "Should add 2 services and removed 2",
input: Input{
currState: map[string]Service{
"foo-service": {Name: "v1"},
"qux-service": {Name: "v1"},
"quux-service": {Name: "v1"},
"quuz-service": {Name: "v1"},
"corge-service": {Name: "v1"},
"grault-service": {Name: "v1"},
"garply-service": {Name: "v1"},
"waldo-service": {Name: "v1"},
"fred-service": {Name: "v1"},
"plugh-service": {Name: "v1"},
"xyzzy-service": {Name: "v1"},
"thud-service": {Name: "v1"},
},
prevState: map[string]Service{
"foo-service": {Name: "v1"},
"bar-service": {Name: "v1"},
"baz-service": {Name: "v1"},
"qux-service": {Name: "v1"},
"quux-service": {Name: "v1"},
"quuz-service": {Name: "v1"},
"corge-service": {Name: "v1"},
"waldo-service": {Name: "v1"},
"fred-service": {Name: "v1"},
"plugh-service": {Name: "v1"},
"xyzzy-service": {Name: "v1"},
"thud-service": {Name: "v1"},
},
},
output: Output{
addedKeys: []string{"grault-service", "garply-service"},
removedKeys: []string{"bar-service", "baz-service"},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
addedKeys, removedKeys := getChangedServiceKeys(test.input.currState, test.input.prevState)
assert.Equal(t, fun.Set(test.output.addedKeys), fun.Set(addedKeys), "Added keys comparison results: got %q, want %q", addedKeys, test.output.addedKeys)
assert.Equal(t, fun.Set(test.output.removedKeys), fun.Set(removedKeys), "Removed keys comparison results: got %q, want %q", removedKeys, test.output.removedKeys)
})
}
}
func TestFilterEnabled(t *testing.T) {
testCases := []struct {
desc string
exposedByDefault bool
node *api.ServiceEntry
expected bool
}{
{
desc: "exposed",
exposedByDefault: true,
node: &api.ServiceEntry{
Service: &api.AgentService{
Service: "api",
Address: "10.0.0.1",
Port: 80,
Tags: []string{""},
},
},
expected: true,
},
{
desc: "exposed and tolerated by valid label value",
exposedByDefault: true,
node: &api.ServiceEntry{
Service: &api.AgentService{
Service: "api",
Address: "10.0.0.1",
Port: 80,
Tags: []string{"", "traefik.enable=true"},
},
},
expected: true,
},
{
desc: "exposed and tolerated by invalid label value",
exposedByDefault: true,
node: &api.ServiceEntry{
Service: &api.AgentService{
Service: "api",
Address: "10.0.0.1",
Port: 80,
Tags: []string{"", "traefik.enable=bad"},
},
},
expected: true,
},
{
desc: "exposed but overridden by label",
exposedByDefault: true,
node: &api.ServiceEntry{
Service: &api.AgentService{
Service: "api",
Address: "10.0.0.1",
Port: 80,
Tags: []string{"", "traefik.enable=false"},
},
},
expected: false,
},
{
desc: "non-exposed",
exposedByDefault: false,
node: &api.ServiceEntry{
Service: &api.AgentService{
Service: "api",
Address: "10.0.0.1",
Port: 80,
Tags: []string{""},
},
},
expected: false,
},
{
desc: "non-exposed but overridden by label",
exposedByDefault: false,
node: &api.ServiceEntry{
Service: &api.AgentService{
Service: "api",
Address: "10.0.0.1",
Port: 80,
Tags: []string{"", "traefik.enable=true"},
},
},
expected: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
provider := &CatalogProvider{
Domain: "localhost",
Prefix: "traefik",
ExposedByDefault: test.exposedByDefault,
}
actual := provider.nodeFilter("test", test.node)
assert.Equal(t, test.expected, actual)
})
}
}
func TestGetChangedStringKeys(t *testing.T) {
testCases := []struct {
desc string
current []string
previous []string
expectedAdded []string
expectedRemoved []string
}{
{
desc: "1 element added, 0 removed",
current: []string{"chou"},
previous: []string{},
expectedAdded: []string{"chou"},
expectedRemoved: []string{},
}, {
desc: "0 element added, 0 removed",
current: []string{"chou"},
previous: []string{"chou"},
expectedAdded: []string{},
expectedRemoved: []string{},
},
{
desc: "0 element added, 1 removed",
current: []string{},
previous: []string{"chou"},
expectedAdded: []string{},
expectedRemoved: []string{"chou"},
},
{
desc: "1 element added, 1 removed",
current: []string{"carotte"},
previous: []string{"chou"},
expectedAdded: []string{"carotte"},
expectedRemoved: []string{"chou"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actualAdded, actualRemoved := getChangedStringKeys(test.current, test.previous)
assert.Equal(t, test.expectedAdded, actualAdded)
assert.Equal(t, test.expectedRemoved, actualRemoved)
})
}
}
func TestHasServiceChanged(t *testing.T) {
testCases := []struct {
desc string
current map[string]Service
previous map[string]Service
expected bool
}{
{
desc: "Change detected due to change of nodes",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node2"},
Tags: []string{},
},
},
expected: true,
},
{
desc: "No change missing current service",
current: make(map[string]Service),
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{},
},
},
expected: false,
},
{
desc: "No change on nodes",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{},
},
},
expected: false,
},
{
desc: "No change on nodes and tags",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo=bar"},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo=bar"},
},
},
expected: false,
},
{
desc: "Change detected on tags",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo=bar"},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo"},
},
},
expected: true,
},
{
desc: "Change detected on ports",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo=bar"},
Ports: []int{80},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo"},
Ports: []int{81},
},
},
expected: true,
},
{
desc: "Change detected on ports",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo=bar"},
Ports: []int{80},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo"},
Ports: []int{81, 82},
},
},
expected: true,
},
{
desc: "No Change detected",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo"},
Ports: []int{80},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo"},
Ports: []int{80},
},
},
expected: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := hasServiceChanged(test.current, test.previous)
assert.Equal(t, test.expected, actual)
})
}
}
func TestHasChanged(t *testing.T) {
testCases := []struct {
desc string
current map[string]Service
previous map[string]Service
expected bool
}{
{
desc: "Change detected due to change new service",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{},
},
},
previous: make(map[string]Service),
expected: true,
},
{
desc: "Change detected due to change service removed",
current: make(map[string]Service),
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{},
},
},
expected: true,
},
{
desc: "Change detected due to change of nodes",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node2"},
Tags: []string{},
},
},
expected: true,
},
{
desc: "No change on nodes",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{},
},
},
expected: false,
},
{
desc: "No change on nodes and tags",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo=bar"},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo=bar"},
},
},
expected: false,
},
{
desc: "Change detected on tags",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo=bar"},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo"},
},
},
expected: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := hasChanged(test.current, test.previous)
assert.Equal(t, test.expected, actual)
})
}
}
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)
})
}
}