traefik/provider/consul/consul_catalog.go

643 lines
19 KiB
Go
Raw Normal View History

package consul
2016-02-02 17:03:40 +00:00
import (
"bytes"
2016-02-02 17:03:40 +00:00
"errors"
"sort"
"strconv"
2016-02-02 17:03:40 +00:00
"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/safe"
"github.com/containous/traefik/types"
2016-02-02 17:03:40 +00:00
"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 {
2017-10-02 08:32:02 +00:00
provider.BaseProvider `mapstructure:",squash" export:"true"`
Endpoint string `description:"Consul server endpoint"`
Domain string `description:"Default domain used"`
2017-10-02 08:32:02 +00:00
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
}
type serviceUpdate struct {
ServiceName string
Attributes []string
2016-02-02 17:03:40 +00:00
}
type catalogUpdate struct {
Service *serviceUpdate
2016-02-02 17:03:40 +00:00
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 {
lentr := a[i]
rentr := a[j]
ls := strings.ToLower(lentr.Service.Service)
lr := strings.ToLower(rentr.Service.Service)
if ls != lr {
return ls < lr
}
if lentr.Service.Address != rentr.Service.Address {
return lentr.Service.Address < rentr.Service.Address
}
if lentr.Node.Address != rentr.Node.Address {
return lentr.Node.Address < rentr.Node.Address
}
return lentr.Service.Port < rentr.Service.Port
}
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)
2016-02-02 17:03:40 +00:00
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
}
2017-09-08 18:50:04 +00:00
}
}
return false
2017-09-08 18:50:04 +00:00
}
func getChangedStringKeys(currState []string, prevState []string) ([]string, []string) {
2017-09-08 18:50:04 +00:00
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)
}
2017-10-16 14:58:03 +00:00
func (p *CatalogProvider) watchHealthState(stopCh <-chan struct{}, watchCh chan<- map[string][]string, errorCh chan<- error) {
health := p.client.Health()
catalog := p.client.Catalog()
2016-02-02 17:03:40 +00:00
safe.Go(func() {
// variable to hold previous state
2017-09-08 18:50:04 +00:00
var flashback []string
2016-02-02 17:03:40 +00:00
options := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
2016-02-02 17:03:40 +00:00
for {
select {
case <-stopCh:
return
default:
}
// Listening to changes that leads to `passing` state or degrades from it.
2017-09-08 18:50:04 +00:00
healthyState, meta, err := health.State("passing", options)
if err != nil {
log.WithError(err).Error("Failed to retrieve health checks")
2017-10-16 14:58:03 +00:00
errorCh <- err
return
}
2017-09-08 18:50:04 +00:00
var current []string
if healthyState != nil {
for _, healthy := range healthyState {
current = append(current, healthy.ServiceID)
}
}
2016-02-02 17:03:40 +00:00
// 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 {
2016-02-02 17:03:40 +00:00
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: %s", err)
2017-10-16 14:58:03 +00:00
errorCh <- err
return
}
2016-02-02 17:03:40 +00:00
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
2017-09-08 18:50:04 +00:00
flashback = current
}
if len(removedKeys) > 0 {
log.WithField("MissingServices", removedKeys).Debug("Health State change detected.")
watchCh <- data
2017-09-08 18:50:04 +00:00
flashback = current
}
2016-02-02 17:03:40 +00:00
}
}
})
}
2017-09-08 18:50:04 +00:00
// Service represent a Consul service.
type Service struct {
Name string
Tags []string
Nodes []string
Ports []int
2017-09-08 18:50:04 +00:00
}
2017-10-16 14:58:03 +00:00
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
2017-09-08 18:50:04 +00:00
var flashback map[string]Service
options := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
2016-02-02 17:03:40 +00:00
for {
select {
case <-stopCh:
return
default:
}
data, meta, err := catalog.Services(options)
if err != nil {
log.Errorf("Failed to list services: %s", err)
2017-10-16 14:58:03 +00:00
errorCh <- err
return
}
if options.WaitIndex == meta.LastIndex {
continue
}
options.WaitIndex = meta.LastIndex
if data != nil {
2017-09-29 14:30:03 +00:00
current := make(map[string]Service)
2017-09-08 18:50:04 +00:00
for key, value := range data {
nodes, _, err := catalog.Service(key, "", &api.QueryOptions{})
2017-09-08 18:50:04 +00:00
if err != nil {
log.Errorf("Failed to get detail of service %s: %s", key, err)
2017-10-16 14:58:03 +00:00
errorCh <- err
2017-09-08 18:50:04 +00:00
return
}
nodesID := getServiceIds(nodes)
ports := getServicePorts(nodes)
2017-09-08 18:50:04 +00:00
if service, ok := current[key]; ok {
service.Tags = value
service.Nodes = nodesID
service.Ports = ports
2017-09-08 18:50:04 +00:00
} else {
service := Service{
Name: key,
Tags: value,
Nodes: nodesID,
Ports: ports,
2017-09-08 18:50:04 +00:00
}
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
2017-09-29 14:30:03 +00:00
flashback = current
}
}
}
})
2016-02-02 17:03:40 +00:00
}
2017-09-08 18:50:04 +00:00
func getServiceIds(services []*api.CatalogService) []string {
var serviceIds []string
for _, service := range services {
serviceIds = append(serviceIds, service.ID)
2017-09-08 18:50:04 +00:00
}
return serviceIds
}
2016-02-02 17:03:40 +00:00
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()
2016-02-02 17:03:40 +00:00
opts := &api.QueryOptions{}
data, _, err := health.Service(service, "", true, opts)
if err != nil {
log.WithError(err).Errorf("Failed to fetch details of %s", service)
2016-02-02 17:03:40 +00:00
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)
2016-02-02 17:03:40 +00:00
return catalogUpdate{
Service: &serviceUpdate{
ServiceName: service,
Attributes: tags,
},
Nodes: nodes,
2016-02-02 17:03:40 +00:00
}, 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 {
enable, err := strconv.ParseBool(p.getAttribute("enable", node.Service.Tags, strconv.FormatBool(p.ExposedByDefault)))
if err != nil {
log.Debugf("Invalid value for enable, set to %b", p.ExposedByDefault)
return p.ExposedByDefault
}
return enable
}
func (p *CatalogProvider) getPrefixedName(name string) string {
if len(p.Prefix) > 0 {
return p.Prefix + "." + name
}
return name
}
func (p *CatalogProvider) getEntryPoints(list string) []string {
return strings.Split(list, ",")
}
func (p *CatalogProvider) getBackend(node *api.ServiceEntry) string {
2016-02-02 17:03:40 +00:00
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
}
2017-09-07 13:28:02 +00:00
func (p *CatalogProvider) getBasicAuth(tags []string) []string {
list := p.getAttribute("frontend.auth.basic", tags, "")
if list != "" {
return strings.Split(list, ",")
}
return []string{}
}
2017-10-16 15:38:03 +00:00
func (p *CatalogProvider) getSticky(tags []string) string {
stickyTag := p.getAttribute(types.SuffixBackendLoadBalancerSticky, tags, "")
2017-10-10 09:10:02 +00:00
if len(stickyTag) > 0 {
2017-10-12 15:50:03 +00:00
log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness)
2017-10-16 15:38:03 +00:00
} else {
stickyTag = "false"
2017-10-10 09:10:02 +00:00
}
2017-10-16 15:38:03 +00:00
return stickyTag
}
2017-10-10 09:10:02 +00:00
2017-10-16 15:38:03 +00:00
func (p *CatalogProvider) hasStickinessLabel(tags []string) bool {
stickinessTag := p.getAttribute(types.SuffixBackendLoadBalancerStickiness, tags, "")
2017-10-16 15:38:03 +00:00
return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true")
2017-10-10 09:10:02 +00:00
}
func (p *CatalogProvider) getStickinessCookieName(tags []string) string {
return p.getAttribute(types.SuffixBackendLoadBalancerStickinessCookieName, tags, "")
2017-10-10 09:10:02 +00:00
}
func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string {
return p.getTag(p.getPrefixedName(name), tags, defaultValue)
}
func (p *CatalogProvider) hasTag(name string, tags []string) bool {
// Very-very unlikely that a Consul tag would ever start with '=!='
tag := p.getTag(name, tags, "=!=")
return tag != "=!="
}
func (p *CatalogProvider) getTag(name string, tags []string, defaultValue string) string {
for _, tag := range tags {
// Given the nature of Consul tags, which could be either singular markers, or key=value pairs, we check if the consul tag starts with 'name'
if strings.Index(strings.ToLower(tag), strings.ToLower(name)) == 0 {
// In case, where a tag might be a key=value, try to split it by the first '='
// - If the first element (which would always be there, even if the tag is a singular marker without '=' in it
if kv := strings.SplitN(tag, "=", 2); strings.ToLower(kv[0]) == strings.ToLower(name) {
// If the returned result is a key=value pair, return the 'value' component
if len(kv) == 2 {
return kv[1]
}
// If the returned result is a singular marker, return the 'key' component
return kv[0]
}
}
}
return defaultValue
2016-02-02 17:03:40 +00:00
}
func (p *CatalogProvider) getConstraintTags(tags []string) []string {
var list []string
for _, tag := range tags {
// We look for a Consul tag named 'traefik.tags' (unless different 'prefix' is configured)
if strings.Index(strings.ToLower(tag), p.getPrefixedName("tags=")) == 0 {
// If 'traefik.tags=' tag is found, take the tag value and split by ',' adding the result to the list to be returned
splitedTags := strings.Split(tag[len(p.getPrefixedName("tags=")):], ",")
list = append(list, splitedTags...)
}
}
return list
}
func (p *CatalogProvider) buildConfig(catalog []catalogUpdate) *types.Configuration {
2016-02-02 17:03:40 +00:00
var FuncMap = template.FuncMap{
2017-10-10 09:10:02 +00:00
"getBackend": p.getBackend,
"getFrontendRule": p.getFrontendRule,
"getBackendName": p.getBackendName,
"getBackendAddress": p.getBackendAddress,
"getBasicAuth": p.getBasicAuth,
2017-10-16 15:38:03 +00:00
"getSticky": p.getSticky,
2017-10-10 09:10:02 +00:00
"hasStickinessLabel": p.hasStickinessLabel,
"getStickinessCookieName": p.getStickinessCookieName,
"getAttribute": p.getAttribute,
"getTag": p.getTag,
"hasTag": p.hasTag,
"getEntryPoints": p.getEntryPoints,
"hasMaxconnAttributes": p.hasMaxconnAttributes,
2016-02-02 17:03:40 +00:00
}
var allNodes []*api.ServiceEntry
var services []*serviceUpdate
2016-02-02 17:03:40 +00:00
for _, info := range catalog {
if len(info.Nodes) > 0 {
services = append(services, info.Service)
allNodes = append(allNodes, info.Nodes...)
2016-02-02 17:03:40 +00:00
}
}
// Ensure a stable ordering of nodes so that identical configurations may be detected
sort.Sort(nodeSorter(allNodes))
2016-02-02 17:03:40 +00:00
templateObjects := struct {
Services []*serviceUpdate
2016-02-02 17:03:40 +00:00
Nodes []*api.ServiceEntry
}{
Services: services,
2016-02-02 17:03:40 +00:00
Nodes: allNodes,
}
configuration, err := p.GetConfiguration("templates/consul_catalog.tmpl", FuncMap, templateObjects)
2016-02-02 17:03:40 +00:00
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, "")
2016-08-25 03:46:47 +00:00
if amount != "" && extractorfunc != "" {
return true
}
return false
}
func (p *CatalogProvider) getNodes(index map[string][]string) ([]catalogUpdate, error) {
2016-02-02 17:03:40 +00:00
visited := make(map[string]bool)
var nodes []catalogUpdate
2016-02-02 17:03:40 +00:00
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)
2016-02-02 17:03:40 +00:00
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)
}
2016-02-02 17:03:40 +00:00
}
}
return nodes, nil
}
func (p *CatalogProvider) watch(configurationChan chan<- types.ConfigMessage, stop chan bool) error {
2016-02-02 17:03:40 +00:00
stopCh := make(chan struct{})
watchCh := make(chan map[string][]string)
2017-10-16 14:58:03 +00:00
errorCh := make(chan error)
2017-10-16 14:58:03 +00:00
p.watchHealthState(stopCh, watchCh, errorCh)
p.watchCatalogServices(stopCh, watchCh, errorCh)
2016-02-02 17:03:40 +00:00
defer close(stopCh)
defer close(watchCh)
2016-02-02 17:03:40 +00:00
for {
select {
case <-stop:
return nil
case index, ok := <-watchCh:
2016-02-02 17:03:40 +00:00
if !ok {
return errors.New("consul service list nil")
2016-02-02 17:03:40 +00:00
}
log.Debug("List of services changed")
nodes, err := p.getNodes(index)
2016-02-02 17:03:40 +00:00
if err != nil {
return err
}
configuration := p.buildConfig(nodes)
2016-02-02 17:03:40 +00:00
configurationChan <- types.ConfigMessage{
ProviderName: "consul_catalog",
Configuration: configuration,
}
2017-10-16 14:58:03 +00:00
case err := <-errorCh:
return err
2016-02-02 17:03:40 +00:00
}
}
}
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
2016-02-02 17:03:40 +00:00
// using the given configuration channel.
func (p *CatalogProvider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
2016-02-02 17:03:40 +00:00
config := api.DefaultConfig()
config.Address = p.Endpoint
2016-02-02 17:03:40 +00:00
client, err := api.NewClient(config)
if err != nil {
return err
}
p.client = client
p.Constraints = append(p.Constraints, constraints...)
p.setupFrontEndTemplate()
2016-02-02 17:03:40 +00:00
pool.Go(func(stop chan bool) {
2016-02-02 17:03:40 +00:00
notify := func(err error, time time.Duration) {
log.Errorf("Consul connection error %+v, retrying in %s", err, time)
}
2016-08-19 12:24:09 +00:00
operation := func() error {
return p.watch(configurationChan, stop)
2016-02-02 17:03:40 +00:00
}
2016-12-08 12:32:12 +00:00
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
2016-02-02 17:03:40 +00:00
if err != nil {
2016-08-19 08:36:54 +00:00
log.Errorf("Cannot connect to consul server %+v", err)
2016-02-02 17:03:40 +00:00
}
})
2016-02-02 17:03:40 +00:00
return err
}