2019-10-15 15:34:08 +00:00
package consulcatalog
import (
"context"
"fmt"
"strconv"
2020-02-13 09:26:04 +00:00
"strings"
2019-10-15 15:34:08 +00:00
"text/template"
"time"
2020-02-26 09:36:05 +00:00
"github.com/cenkalti/backoff/v4"
2019-10-15 15:34:08 +00:00
"github.com/hashicorp/consul/api"
2020-08-17 16:04:03 +00:00
ptypes "github.com/traefik/paerser/types"
2020-09-16 13:46:04 +00:00
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/job"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/provider/constraints"
"github.com/traefik/traefik/v2/pkg/safe"
"github.com/traefik/traefik/v2/pkg/types"
2019-10-15 15:34:08 +00:00
)
// DefaultTemplateRule The default template for the default rule.
const DefaultTemplateRule = "Host(`{{ normalize .Name }}`)"
var _ provider . Provider = ( * Provider ) ( nil )
type itemData struct {
ID string
2019-11-27 15:24:06 +00:00
Node string
2019-10-15 15:34:08 +00:00
Name string
Address string
Port string
Status string
Labels map [ string ] string
2019-11-29 16:16:05 +00:00
Tags [ ] string
2019-10-15 15:34:08 +00:00
ExtraConf configuration
}
// Provider holds configurations of the provider.
type Provider struct {
2019-10-31 10:56:05 +00:00
Constraints string ` description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true" `
Endpoint * EndpointConfig ` description:"Consul endpoint settings" json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" export:"true" `
Prefix string ` description:"Prefix for consul service tags. Default 'traefik'" json:"prefix,omitempty" toml:"prefix,omitempty" yaml:"prefix,omitempty" export:"true" `
2020-08-17 16:04:03 +00:00
RefreshInterval ptypes . Duration ` description:"Interval for check Consul API. Default 100ms" json:"refreshInterval,omitempty" toml:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty" export:"true" `
2019-10-31 10:56:05 +00:00
RequireConsistent bool ` description:"Forces the read to be fully consistent." json:"requireConsistent,omitempty" toml:"requireConsistent,omitempty" yaml:"requireConsistent,omitempty" export:"true" `
Stale bool ` description:"Use stale consistency for catalog reads." json:"stale,omitempty" toml:"stale,omitempty" yaml:"stale,omitempty" export:"true" `
Cache bool ` description:"Use local agent caching for catalog reads." json:"cache,omitempty" toml:"cache,omitempty" yaml:"cache,omitempty" export:"true" `
ExposedByDefault bool ` description:"Expose containers by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true" `
DefaultRule string ` description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty" `
2019-10-15 15:34:08 +00:00
client * api . Client
defaultRuleTpl * template . Template
}
// EndpointConfig holds configurations of the endpoint.
type EndpointConfig struct {
2020-10-30 11:44:05 +00:00
Address string ` description:"The address of the Consul server" json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty" `
Scheme string ` description:"The URI scheme for the Consul server" json:"scheme,omitempty" toml:"scheme,omitempty" yaml:"scheme,omitempty" `
DataCenter string ` description:"Data center to use. If not provided, the default agent data center is used" json:"datacenter,omitempty" toml:"datacenter,omitempty" yaml:"datacenter,omitempty" `
Token string ` description:"Token is used to provide a per-request ACL token which overrides the agent's default token" json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" `
2019-10-15 15:34:08 +00:00
TLS * types . ClientTLS ` description:"Enable TLS support." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true" `
HTTPAuth * EndpointHTTPAuthConfig ` description:"Auth info to use for http access" json:"httpAuth,omitempty" toml:"httpAuth,omitempty" yaml:"httpAuth,omitempty" export:"true" `
2020-08-17 16:04:03 +00:00
EndpointWaitTime ptypes . Duration ` description:"WaitTime limits how long a Watch will block. If not provided, the agent default values will be used" json:"endpointWaitTime,omitempty" toml:"endpointWaitTime,omitempty" yaml:"endpointWaitTime,omitempty" export:"true" `
2019-10-15 15:34:08 +00:00
}
// SetDefaults sets the default values.
func ( c * EndpointConfig ) SetDefaults ( ) {
2020-10-19 08:28:03 +00:00
c . Address = "127.0.0.1:8500"
2019-10-15 15:34:08 +00:00
}
// EndpointHTTPAuthConfig holds configurations of the authentication.
type EndpointHTTPAuthConfig struct {
2020-10-30 11:44:05 +00:00
Username string ` description:"Basic Auth username" json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty" `
Password string ` description:"Basic Auth password" json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty" `
2019-10-15 15:34:08 +00:00
}
// SetDefaults sets the default values.
func ( p * Provider ) SetDefaults ( ) {
endpoint := & EndpointConfig { }
endpoint . SetDefaults ( )
p . Endpoint = endpoint
2020-08-17 16:04:03 +00:00
p . RefreshInterval = ptypes . Duration ( 15 * time . Second )
2019-10-15 15:34:08 +00:00
p . Prefix = "traefik"
p . ExposedByDefault = true
p . DefaultRule = DefaultTemplateRule
}
// Init the provider.
func ( p * Provider ) Init ( ) error {
defaultRuleTpl , err := provider . MakeDefaultRuleTemplate ( p . DefaultRule , nil )
if err != nil {
2020-05-11 10:06:07 +00:00
return fmt . Errorf ( "error while parsing default rule: %w" , err )
2019-10-15 15:34:08 +00:00
}
p . defaultRuleTpl = defaultRuleTpl
return nil
}
// Provide allows the consul catalog provider to provide configurations to traefik using the given configuration channel.
func ( p * Provider ) Provide ( configurationChan chan <- dynamic . Message , pool * safe . Pool ) error {
pool . GoCtx ( func ( routineCtx context . Context ) {
ctxLog := log . With ( routineCtx , log . Str ( log . ProviderName , "consulcatalog" ) )
logger := log . FromContext ( ctxLog )
operation := func ( ) error {
var err error
p . client , err = createClient ( p . Endpoint )
if err != nil {
2020-11-16 19:44:04 +00:00
return fmt . Errorf ( "unable to create consul client: %w" , err )
2019-10-15 15:34:08 +00:00
}
2020-11-16 19:44:04 +00:00
// get configuration at the provider's startup.
err = p . loadConfiguration ( routineCtx , configurationChan )
if err != nil {
return fmt . Errorf ( "failed to get consul catalog data: %w" , err )
}
// Periodic refreshes.
2019-10-15 15:34:08 +00:00
ticker := time . NewTicker ( time . Duration ( p . RefreshInterval ) )
2020-11-16 19:44:04 +00:00
defer ticker . Stop ( )
2019-10-15 15:34:08 +00:00
for {
select {
case <- ticker . C :
2020-11-16 19:44:04 +00:00
err = p . loadConfiguration ( routineCtx , configurationChan )
2019-10-15 15:34:08 +00:00
if err != nil {
2020-11-16 19:44:04 +00:00
return fmt . Errorf ( "failed to refresh consul catalog data: %w" , err )
2019-10-15 15:34:08 +00:00
}
case <- routineCtx . Done ( ) :
return nil
}
}
}
notify := func ( err error , time time . Duration ) {
logger . Errorf ( "Provider connection error %+v, retrying in %s" , err , time )
}
err := backoff . RetryNotify ( safe . OperationWithRecover ( operation ) , backoff . WithContext ( job . NewBackOff ( backoff . NewExponentialBackOff ( ) ) , ctxLog ) , notify )
if err != nil {
2019-10-31 10:56:05 +00:00
logger . Errorf ( "Cannot connect to consul catalog server %+v" , err )
2019-10-15 15:34:08 +00:00
}
} )
return nil
}
2020-11-16 19:44:04 +00:00
func ( p * Provider ) loadConfiguration ( ctx context . Context , configurationChan chan <- dynamic . Message ) error {
data , err := p . getConsulServicesData ( ctx )
if err != nil {
return err
}
configurationChan <- dynamic . Message {
ProviderName : "consulcatalog" ,
Configuration : p . buildConfiguration ( ctx , data ) ,
}
return nil
}
2019-10-15 15:34:08 +00:00
func ( p * Provider ) getConsulServicesData ( ctx context . Context ) ( [ ] itemData , error ) {
consulServiceNames , err := p . fetchServices ( ctx )
if err != nil {
return nil , err
}
var data [ ] itemData
2020-02-13 09:26:04 +00:00
for _ , name := range consulServiceNames {
2020-11-17 16:30:03 +00:00
consulServices , statuses , err := p . fetchService ( ctx , name )
2019-10-15 15:34:08 +00:00
if err != nil {
return nil , err
}
2020-11-17 16:30:03 +00:00
for _ , consulService := range consulServices {
2019-11-14 10:10:06 +00:00
address := consulService . ServiceAddress
if address == "" {
address = consulService . Address
}
2020-11-17 16:30:03 +00:00
status , exists := statuses [ consulService . ID + consulService . ServiceID ]
if ! exists {
status = api . HealthAny
}
2019-10-15 15:34:08 +00:00
item := itemData {
ID : consulService . ServiceID ,
2019-11-27 15:24:06 +00:00
Node : consulService . Node ,
2019-10-15 15:34:08 +00:00
Name : consulService . ServiceName ,
2019-11-14 10:10:06 +00:00
Address : address ,
2019-10-15 15:34:08 +00:00
Port : strconv . Itoa ( consulService . ServicePort ) ,
2019-11-29 16:16:05 +00:00
Labels : tagsToNeutralLabels ( consulService . ServiceTags , p . Prefix ) ,
Tags : consulService . ServiceTags ,
2020-11-17 16:30:03 +00:00
Status : status ,
2019-10-15 15:34:08 +00:00
}
extraConf , err := p . getConfiguration ( item )
if err != nil {
log . FromContext ( ctx ) . Errorf ( "Skip item %s: %v" , item . Name , err )
continue
}
item . ExtraConf = extraConf
data = append ( data , item )
}
}
return data , nil
}
2020-11-17 16:30:03 +00:00
func ( p * Provider ) fetchService ( ctx context . Context , name string ) ( [ ] * api . CatalogService , map [ string ] string , error ) {
2019-10-15 15:34:08 +00:00
var tagFilter string
if ! p . ExposedByDefault {
tagFilter = p . Prefix + ".enable=true"
}
2019-10-31 10:56:05 +00:00
opts := & api . QueryOptions { AllowStale : p . Stale , RequireConsistent : p . RequireConsistent , UseCache : p . Cache }
2020-11-17 16:30:03 +00:00
opts = opts . WithContext ( ctx )
2019-12-19 10:00:07 +00:00
2019-10-31 10:56:05 +00:00
consulServices , _ , err := p . client . Catalog ( ) . Service ( name , tagFilter , opts )
2019-12-19 10:00:07 +00:00
if err != nil {
return nil , nil , err
}
healthServices , _ , err := p . client . Health ( ) . Service ( name , tagFilter , false , opts )
2020-11-17 16:30:03 +00:00
if err != nil {
return nil , nil , err
}
// Index status by service and node so it can be retrieved from a CatalogService even if the health and services
// are not in sync.
statuses := make ( map [ string ] string )
for _ , health := range healthServices {
if health . Service == nil || health . Node == nil {
continue
}
statuses [ health . Node . ID + health . Service . ID ] = health . Checks . AggregatedStatus ( )
}
return consulServices , statuses , err
2019-10-15 15:34:08 +00:00
}
2020-02-13 09:26:04 +00:00
func ( p * Provider ) fetchServices ( ctx context . Context ) ( [ ] string , error ) {
// The query option "Filter" is not supported by /catalog/services.
// https://www.consul.io/api/catalog.html#list-services
2019-10-31 10:56:05 +00:00
opts := & api . QueryOptions { AllowStale : p . Stale , RequireConsistent : p . RequireConsistent , UseCache : p . Cache }
serviceNames , _ , err := p . client . Catalog ( ) . Services ( opts )
2020-02-13 09:26:04 +00:00
if err != nil {
return nil , err
}
// The keys are the service names, and the array values provide all known tags for a given service.
// https://www.consul.io/api/catalog.html#list-services
var filtered [ ] string
for svcName , tags := range serviceNames {
logger := log . FromContext ( log . With ( ctx , log . Str ( "serviceName" , svcName ) ) )
if ! p . ExposedByDefault && ! contains ( tags , p . Prefix + ".enable=true" ) {
logger . Debug ( "Filtering disabled item" )
continue
}
if contains ( tags , p . Prefix + ".enable=false" ) {
logger . Debug ( "Filtering disabled item" )
continue
}
matches , err := constraints . MatchTags ( tags , p . Constraints )
if err != nil {
logger . Errorf ( "Error matching constraints expression: %v" , err )
continue
}
if ! matches {
logger . Debugf ( "Container pruned by constraint expression: %q" , p . Constraints )
continue
}
filtered = append ( filtered , svcName )
}
return filtered , err
}
func contains ( values [ ] string , val string ) bool {
for _ , value := range values {
if strings . EqualFold ( value , val ) {
return true
}
}
return false
2019-10-15 15:34:08 +00:00
}
func createClient ( cfg * EndpointConfig ) ( * api . Client , error ) {
config := api . Config {
Address : cfg . Address ,
Scheme : cfg . Scheme ,
Datacenter : cfg . DataCenter ,
WaitTime : time . Duration ( cfg . EndpointWaitTime ) ,
Token : cfg . Token ,
}
if cfg . HTTPAuth != nil {
config . HttpAuth = & api . HttpBasicAuth {
Username : cfg . HTTPAuth . Username ,
Password : cfg . HTTPAuth . Password ,
}
}
if cfg . TLS != nil {
config . TLSConfig = api . TLSConfig {
Address : cfg . Address ,
CAFile : cfg . TLS . CA ,
CertFile : cfg . TLS . Cert ,
KeyFile : cfg . TLS . Key ,
InsecureSkipVerify : cfg . TLS . InsecureSkipVerify ,
}
}
return api . NewClient ( & config )
}