2016-02-02 20:03:40 +03:00
package provider
import (
"errors"
2016-06-20 20:13:22 +03:00
"sort"
2016-04-15 10:56:06 +03:00
"strconv"
2016-02-02 20:03:40 +03:00
"strings"
"text/template"
"time"
2016-05-20 18:17:38 +03:00
"github.com/BurntSushi/ty/fun"
2016-02-02 20:03:40 +03:00
log "github.com/Sirupsen/logrus"
"github.com/cenkalti/backoff"
2016-03-31 19:57:08 +03:00
"github.com/containous/traefik/safe"
2016-02-24 18:43:39 +03:00
"github.com/containous/traefik/types"
2016-08-19 12:09:54 +03:00
"github.com/containous/traefik/utils"
2016-02-02 20:03:40 +03:00
"github.com/hashicorp/consul/api"
)
const (
// DefaultWatchWaitTime is the duration to wait when polling consul
DefaultWatchWaitTime = 15 * time . Second
2016-04-12 10:49:37 +03:00
// DefaultConsulCatalogTagPrefix is a prefix for additional service/node configurations
DefaultConsulCatalogTagPrefix = "traefik"
2016-02-02 20:03:40 +03:00
)
// ConsulCatalog holds configurations of the Consul catalog provider.
type ConsulCatalog struct {
2016-06-24 10:58:42 +03:00
BaseProvider ` mapstructure:",squash" `
Endpoint string ` description:"Consul server endpoint" `
Domain string ` description:"Default domain used" `
client * api . Client
Prefix string
2016-04-12 10:49:37 +03:00
}
type serviceUpdate struct {
ServiceName string
Attributes [ ] string
2016-02-02 20:03:40 +03:00
}
type catalogUpdate struct {
2016-04-12 10:49:37 +03:00
Service * serviceUpdate
2016-02-02 20:03:40 +03:00
Nodes [ ] * api . ServiceEntry
}
2016-06-20 20:13:22 +03:00
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
}
2016-02-02 20:03:40 +03:00
func ( provider * ConsulCatalog ) watchServices ( stopCh <- chan struct { } ) <- chan map [ string ] [ ] string {
watchCh := make ( chan map [ string ] [ ] string )
catalog := provider . client . Catalog ( )
2016-03-31 19:57:08 +03:00
safe . Go ( func ( ) {
2016-02-02 20:03:40 +03:00
defer close ( watchCh )
opts := & api . QueryOptions { WaitTime : DefaultWatchWaitTime }
for {
select {
case <- stopCh :
return
default :
}
data , meta , err := catalog . Services ( opts )
if err != nil {
log . WithError ( err ) . Errorf ( "Failed to list services" )
return
}
// If LastIndex didn't change then it means `Get` returned
// because of the WaitTime and the key didn't changed.
if opts . WaitIndex == meta . LastIndex {
continue
}
opts . WaitIndex = meta . LastIndex
if data != nil {
watchCh <- data
}
}
2016-03-31 19:57:08 +03:00
} )
2016-02-02 20:03:40 +03:00
return watchCh
}
func ( provider * ConsulCatalog ) healthyNodes ( service string ) ( catalogUpdate , error ) {
health := provider . client . Health ( )
opts := & api . QueryOptions { }
data , _ , err := health . Service ( service , "" , true , opts )
if err != nil {
log . WithError ( err ) . Errorf ( "Failed to fetch details of " + service )
return catalogUpdate { } , err
}
2016-05-20 18:17:38 +03:00
nodes := fun . Filter ( func ( node * api . ServiceEntry ) bool {
2016-05-30 16:05:58 +03:00
constraintTags := provider . getContraintTags ( node . Service . Tags )
2016-05-20 18:17:38 +03:00
ok , failingConstraint := provider . MatchConstraints ( constraintTags )
if ok == false && failingConstraint != nil {
2016-05-30 16:05:58 +03:00
log . Debugf ( "Service %v pruned by '%v' constraint" , service , failingConstraint . String ( ) )
2016-04-12 10:49:37 +03:00
}
2016-05-20 18:17:38 +03:00
return ok
} , 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-04-12 10:49:37 +03:00
2016-02-02 20:03:40 +03:00
return catalogUpdate {
2016-04-12 10:49:37 +03:00
Service : & serviceUpdate {
ServiceName : service ,
Attributes : tags ,
} ,
2016-05-30 16:05:58 +03:00
Nodes : nodes ,
2016-02-02 20:03:40 +03:00
} , nil
}
2016-04-12 10:49:37 +03:00
func ( provider * ConsulCatalog ) getEntryPoints ( list string ) [ ] string {
return strings . Split ( list , "," )
}
2016-02-02 20:03:40 +03:00
func ( provider * ConsulCatalog ) getBackend ( node * api . ServiceEntry ) string {
return strings . ToLower ( node . Service . Service )
}
2016-04-12 10:49:37 +03:00
func ( provider * ConsulCatalog ) getFrontendRule ( service serviceUpdate ) string {
customFrontendRule := provider . getAttribute ( "frontend.rule" , service . Attributes , "" )
if customFrontendRule != "" {
return customFrontendRule
}
return "Host:" + service . ServiceName + "." + provider . Domain
}
2016-04-15 10:56:06 +03:00
func ( provider * ConsulCatalog ) getBackendAddress ( node * api . ServiceEntry ) string {
if node . Service . Address != "" {
return node . Service . Address
}
return node . Node . Address
}
func ( provider * ConsulCatalog ) getBackendName ( node * api . ServiceEntry , index int ) string {
2016-06-20 20:13:22 +03:00
serviceName := strings . ToLower ( node . Service . Service ) + "--" + node . Service . Address + "--" + strconv . Itoa ( node . Service . Port )
2016-05-02 17:14:21 +03:00
for _ , tag := range node . Service . Tags {
serviceName += "--" + normalize ( tag )
2016-04-15 10:56:06 +03:00
}
2016-05-02 17:14:21 +03:00
2016-04-15 10:56:06 +03:00
serviceName = strings . Replace ( serviceName , "." , "-" , - 1 )
serviceName = strings . Replace ( serviceName , "=" , "-" , - 1 )
// unique int at the end
serviceName += "--" + strconv . Itoa ( index )
return serviceName
}
2016-04-12 10:49:37 +03:00
func ( provider * ConsulCatalog ) getAttribute ( name string , tags [ ] string , defaultValue string ) string {
for _ , tag := range tags {
2016-04-19 10:58:31 +03:00
if strings . Index ( strings . ToLower ( tag ) , DefaultConsulCatalogTagPrefix + "." ) == 0 {
if kv := strings . SplitN ( tag [ len ( DefaultConsulCatalogTagPrefix + "." ) : ] , "=" , 2 ) ; len ( kv ) == 2 && strings . ToLower ( kv [ 0 ] ) == strings . ToLower ( name ) {
2016-04-12 10:49:37 +03:00
return kv [ 1 ]
}
}
}
return defaultValue
2016-02-02 20:03:40 +03:00
}
2016-05-30 16:05:58 +03:00
func ( provider * ConsulCatalog ) getContraintTags ( tags [ ] string ) [ ] string {
var list [ ] string
for _ , tag := range tags {
if strings . Index ( strings . ToLower ( tag ) , DefaultConsulCatalogTagPrefix + ".tags=" ) == 0 {
splitedTags := strings . Split ( tag [ len ( DefaultConsulCatalogTagPrefix + ".tags=" ) : ] , "," )
list = append ( list , splitedTags ... )
}
}
return list
}
2016-02-02 20:03:40 +03:00
func ( provider * ConsulCatalog ) buildConfig ( catalog [ ] catalogUpdate ) * types . Configuration {
var FuncMap = template . FuncMap {
2016-04-15 10:56:06 +03:00
"getBackend" : provider . getBackend ,
"getFrontendRule" : provider . getFrontendRule ,
"getBackendName" : provider . getBackendName ,
"getBackendAddress" : provider . getBackendAddress ,
"getAttribute" : provider . getAttribute ,
"getEntryPoints" : provider . getEntryPoints ,
2016-02-02 20:03:40 +03:00
}
allNodes := [ ] * api . ServiceEntry { }
2016-04-12 10:49:37 +03:00
services := [ ] * serviceUpdate { }
2016-02-02 20:03:40 +03:00
for _ , info := range catalog {
2016-04-22 12:11:33 +03:00
for _ , node := range info . Nodes {
isEnabled := provider . getAttribute ( "enable" , node . Service . Tags , "true" )
if isEnabled != "false" && len ( info . Nodes ) > 0 {
services = append ( services , info . Service )
allNodes = append ( allNodes , info . Nodes ... )
break
}
2016-02-02 20:03:40 +03:00
}
}
2016-06-20 20:13:22 +03:00
// Ensure a stable ordering of nodes so that identical configurations may be detected
sort . Sort ( nodeSorter ( allNodes ) )
2016-02-02 20:03:40 +03:00
templateObjects := struct {
2016-04-12 10:49:37 +03:00
Services [ ] * serviceUpdate
2016-02-02 20:03:40 +03:00
Nodes [ ] * api . ServiceEntry
} {
2016-04-12 10:49:37 +03:00
Services : services ,
2016-02-02 20:03:40 +03:00
Nodes : allNodes ,
}
configuration , err := provider . getConfiguration ( "templates/consul_catalog.tmpl" , FuncMap , templateObjects )
if err != nil {
log . WithError ( err ) . Error ( "Failed to create config" )
}
return configuration
}
func ( provider * ConsulCatalog ) 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 . WithFields ( log . Fields {
"service" : name ,
} ) . Debug ( "Fetching service" )
healthy , err := provider . healthyNodes ( name )
if err != nil {
return nil , err
}
2016-05-30 16:05:58 +03:00
// 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 20:03:40 +03:00
}
}
return nodes , nil
}
2016-04-13 21:36:23 +03:00
func ( provider * ConsulCatalog ) watch ( configurationChan chan <- types . ConfigMessage , stop chan bool ) error {
2016-02-02 20:03:40 +03:00
stopCh := make ( chan struct { } )
serviceCatalog := provider . watchServices ( stopCh )
defer close ( stopCh )
for {
select {
2016-04-13 21:36:23 +03:00
case <- stop :
return nil
2016-02-02 20:03:40 +03:00
case index , ok := <- serviceCatalog :
if ! ok {
return errors . New ( "Consul service list nil" )
}
log . Debug ( "List of services changed" )
nodes , err := provider . getNodes ( index )
if err != nil {
return err
}
configuration := provider . buildConfig ( nodes )
configurationChan <- types . ConfigMessage {
ProviderName : "consul_catalog" ,
Configuration : configuration ,
}
}
}
}
// Provide allows the provider to provide configurations to traefik
// using the given configuration channel.
2016-05-31 10:54:42 +03:00
func ( provider * ConsulCatalog ) Provide ( configurationChan chan <- types . ConfigMessage , pool * safe . Pool , constraints [ ] types . Constraint ) error {
2016-02-02 20:03:40 +03:00
config := api . DefaultConfig ( )
config . Address = provider . Endpoint
client , err := api . NewClient ( config )
if err != nil {
return err
}
provider . client = client
2016-05-30 16:05:58 +03:00
provider . Constraints = append ( provider . Constraints , constraints ... )
2016-02-02 20:03:40 +03:00
2016-04-13 21:36:23 +03:00
pool . Go ( func ( stop chan bool ) {
2016-02-02 20:03:40 +03:00
notify := func ( err error , time time . Duration ) {
log . Errorf ( "Consul connection error %+v, retrying in %s" , err , time )
}
worker := func ( ) error {
2016-04-13 21:36:23 +03:00
return provider . watch ( configurationChan , stop )
2016-02-02 20:03:40 +03:00
}
2016-08-19 12:09:54 +03:00
err := utils . RetryNotifyJob ( worker , backoff . NewExponentialBackOff ( ) , notify )
2016-02-02 20:03:40 +03:00
if err != nil {
2016-08-19 11:36:54 +03:00
log . Errorf ( "Cannot connect to consul server %+v" , err )
2016-02-02 20:03:40 +03:00
}
2016-03-31 19:57:08 +03:00
} )
2016-02-02 20:03:40 +03:00
return err
}