Add Consul Catalog provider
This commit is contained in:
parent
d0ed814669
commit
7be2db6e86
52 changed files with 4194 additions and 67 deletions
|
@ -49,6 +49,10 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator {
|
|||
p.quietAddProvider(conf.Rancher)
|
||||
}
|
||||
|
||||
if conf.ConsulCatalog != nil {
|
||||
p.quietAddProvider(conf.ConsulCatalog)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
|
|
200
pkg/provider/consulcatalog/config.go
Normal file
200
pkg/provider/consulcatalog/config.go
Normal file
|
@ -0,0 +1,200 @@
|
|||
package consulcatalog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||
"github.com/containous/traefik/v2/pkg/config/label"
|
||||
"github.com/containous/traefik/v2/pkg/log"
|
||||
"github.com/containous/traefik/v2/pkg/provider"
|
||||
"github.com/containous/traefik/v2/pkg/provider/constraints"
|
||||
"github.com/hashicorp/consul/api"
|
||||
)
|
||||
|
||||
func (p *Provider) buildConfiguration(ctx context.Context, items []itemData) *dynamic.Configuration {
|
||||
configurations := make(map[string]*dynamic.Configuration)
|
||||
|
||||
for _, item := range items {
|
||||
svcName := item.Name + "-" + item.ID
|
||||
ctxSvc := log.With(ctx, log.Str("serviceName", svcName))
|
||||
|
||||
if !p.keepContainer(ctxSvc, item) {
|
||||
continue
|
||||
}
|
||||
|
||||
logger := log.FromContext(ctxSvc)
|
||||
|
||||
confFromLabel, err := label.DecodeConfiguration(item.Labels)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(confFromLabel.TCP.Routers) > 0 || len(confFromLabel.TCP.Services) > 0 {
|
||||
err := p.buildTCPServiceConfiguration(ctxSvc, item, confFromLabel.TCP)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
provider.BuildTCPRouterConfiguration(ctxSvc, confFromLabel.TCP)
|
||||
|
||||
if len(confFromLabel.HTTP.Routers) == 0 &&
|
||||
len(confFromLabel.HTTP.Middlewares) == 0 &&
|
||||
len(confFromLabel.HTTP.Services) == 0 {
|
||||
configurations[svcName] = confFromLabel
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = p.buildServiceConfiguration(ctxSvc, item, confFromLabel.HTTP)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
model := struct {
|
||||
Name string
|
||||
Labels map[string]string
|
||||
}{
|
||||
Name: item.Name,
|
||||
Labels: item.Labels,
|
||||
}
|
||||
|
||||
provider.BuildRouterConfiguration(ctx, confFromLabel.HTTP, item.Name, p.defaultRuleTpl, model)
|
||||
|
||||
configurations[svcName] = confFromLabel
|
||||
}
|
||||
|
||||
return provider.Merge(ctx, configurations)
|
||||
}
|
||||
|
||||
func (p *Provider) keepContainer(ctx context.Context, item itemData) bool {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
if !item.ExtraConf.Enable {
|
||||
logger.Debug("Filtering disabled item")
|
||||
return false
|
||||
}
|
||||
|
||||
matches, err := constraints.Match(item.Labels, p.Constraints)
|
||||
if err != nil {
|
||||
logger.Errorf("Error matching constraints expression: %v", err)
|
||||
return false
|
||||
}
|
||||
if !matches {
|
||||
logger.Debugf("Container pruned by constraint expression: %q", p.Constraints)
|
||||
return false
|
||||
}
|
||||
|
||||
if item.Status != api.HealthPassing && item.Status != api.HealthWarning {
|
||||
logger.Debug("Filtering unhealthy or starting item")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, item itemData, configuration *dynamic.TCPConfiguration) error {
|
||||
if len(configuration.Services) == 0 {
|
||||
configuration.Services = make(map[string]*dynamic.TCPService)
|
||||
|
||||
lb := &dynamic.TCPServersLoadBalancer{}
|
||||
lb.SetDefaults()
|
||||
|
||||
configuration.Services[item.Name] = &dynamic.TCPService{
|
||||
LoadBalancer: lb,
|
||||
}
|
||||
}
|
||||
|
||||
for name, service := range configuration.Services {
|
||||
ctxSvc := log.With(ctx, log.Str(log.ServiceName, name))
|
||||
err := p.addServerTCP(ctxSvc, item, service.LoadBalancer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) buildServiceConfiguration(ctx context.Context, item itemData, configuration *dynamic.HTTPConfiguration) error {
|
||||
if len(configuration.Services) == 0 {
|
||||
configuration.Services = make(map[string]*dynamic.Service)
|
||||
|
||||
lb := &dynamic.ServersLoadBalancer{}
|
||||
lb.SetDefaults()
|
||||
|
||||
configuration.Services[item.Name] = &dynamic.Service{
|
||||
LoadBalancer: lb,
|
||||
}
|
||||
}
|
||||
|
||||
for name, service := range configuration.Services {
|
||||
ctxSvc := log.With(ctx, log.Str(log.ServiceName, name))
|
||||
err := p.addServer(ctxSvc, item, service.LoadBalancer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) addServerTCP(ctx context.Context, item itemData, loadBalancer *dynamic.TCPServersLoadBalancer) error {
|
||||
if loadBalancer == nil {
|
||||
return errors.New("load-balancer is not defined")
|
||||
}
|
||||
|
||||
if len(loadBalancer.Servers) == 0 {
|
||||
loadBalancer.Servers = []dynamic.TCPServer{{}}
|
||||
}
|
||||
|
||||
var port string
|
||||
if item.Port != "" {
|
||||
port = item.Port
|
||||
loadBalancer.Servers[0].Port = ""
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
return errors.New("port is missing")
|
||||
}
|
||||
|
||||
loadBalancer.Servers[0].Address = net.JoinHostPort(item.Address, port)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) addServer(ctx context.Context, item itemData, loadBalancer *dynamic.ServersLoadBalancer) error {
|
||||
if loadBalancer == nil {
|
||||
return errors.New("load-balancer is not defined")
|
||||
}
|
||||
|
||||
var port string
|
||||
if len(loadBalancer.Servers) > 0 {
|
||||
port = loadBalancer.Servers[0].Port
|
||||
}
|
||||
|
||||
if len(loadBalancer.Servers) == 0 {
|
||||
server := dynamic.Server{}
|
||||
server.SetDefaults()
|
||||
|
||||
loadBalancer.Servers = []dynamic.Server{server}
|
||||
}
|
||||
|
||||
if item.Port != "" {
|
||||
port = item.Port
|
||||
loadBalancer.Servers[0].Port = ""
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
return errors.New("port is missing")
|
||||
}
|
||||
|
||||
loadBalancer.Servers[0].URL = fmt.Sprintf("%s://%s", loadBalancer.Servers[0].Scheme, net.JoinHostPort(item.Address, port))
|
||||
loadBalancer.Servers[0].Scheme = ""
|
||||
|
||||
return nil
|
||||
}
|
1850
pkg/provider/consulcatalog/config_test.go
Normal file
1850
pkg/provider/consulcatalog/config_test.go
Normal file
File diff suppressed because it is too large
Load diff
221
pkg/provider/consulcatalog/consul_catalog.go
Normal file
221
pkg/provider/consulcatalog/consul_catalog.go
Normal file
|
@ -0,0 +1,221 @@
|
|||
package consulcatalog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v3"
|
||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||
"github.com/containous/traefik/v2/pkg/job"
|
||||
"github.com/containous/traefik/v2/pkg/log"
|
||||
"github.com/containous/traefik/v2/pkg/provider"
|
||||
"github.com/containous/traefik/v2/pkg/safe"
|
||||
"github.com/containous/traefik/v2/pkg/types"
|
||||
"github.com/hashicorp/consul/api"
|
||||
)
|
||||
|
||||
// DefaultTemplateRule The default template for the default rule.
|
||||
const DefaultTemplateRule = "Host(`{{ normalize .Name }}`)"
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
type itemData struct {
|
||||
ID string
|
||||
Name string
|
||||
Address string
|
||||
Port string
|
||||
Status string
|
||||
Labels map[string]string
|
||||
ExtraConf configuration
|
||||
}
|
||||
|
||||
// Provider holds configurations of the provider.
|
||||
type Provider struct {
|
||||
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"`
|
||||
RefreshInterval types.Duration `description:"Interval for check Consul API. Default 100ms" json:"refreshInterval,omitempty" toml:"refreshInterval,omitempty" yaml:"refreshInterval,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"`
|
||||
|
||||
client *api.Client
|
||||
defaultRuleTpl *template.Template
|
||||
}
|
||||
|
||||
// EndpointConfig holds configurations of the endpoint.
|
||||
type EndpointConfig struct {
|
||||
Address string `description:"The address of the Consul server" json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty" export:"true"`
|
||||
Scheme string `description:"The URI scheme for the Consul server" json:"scheme,omitempty" toml:"scheme,omitempty" yaml:"scheme,omitempty" export:"true"`
|
||||
DataCenter string `description:"Data center to use. If not provided, the default agent data center is used" json:"data center,omitempty" toml:"data center,omitempty" yaml:"datacenter,omitempty" export:"true"`
|
||||
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" export:"true"`
|
||||
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"`
|
||||
EndpointWaitTime types.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"`
|
||||
}
|
||||
|
||||
// SetDefaults sets the default values.
|
||||
func (c *EndpointConfig) SetDefaults() {
|
||||
c.Address = "http://127.0.0.1:8500"
|
||||
}
|
||||
|
||||
// EndpointHTTPAuthConfig holds configurations of the authentication.
|
||||
type EndpointHTTPAuthConfig struct {
|
||||
Username string `description:"Basic Auth username" json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty" export:"true"`
|
||||
Password string `description:"Basic Auth password" json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty" export:"true"`
|
||||
}
|
||||
|
||||
// SetDefaults sets the default values.
|
||||
func (p *Provider) SetDefaults() {
|
||||
endpoint := &EndpointConfig{}
|
||||
endpoint.SetDefaults()
|
||||
p.Endpoint = endpoint
|
||||
p.RefreshInterval = types.Duration(15 * time.Second)
|
||||
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 {
|
||||
return fmt.Errorf("error while parsing default rule: %v", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("error create consul client, %v", err)
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Duration(p.RefreshInterval))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
data, err := p.getConsulServicesData(routineCtx)
|
||||
if err != nil {
|
||||
logger.Errorf("error get consulCatalog data, %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
configuration := p.buildConfiguration(routineCtx, data)
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: "consulcatalog",
|
||||
Configuration: configuration,
|
||||
}
|
||||
case <-routineCtx.Done():
|
||||
ticker.Stop()
|
||||
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 {
|
||||
logger.Errorf("Cannot connect to consulcatalog server %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error) {
|
||||
consulServiceNames, err := p.fetchServices(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data []itemData
|
||||
for name := range consulServiceNames {
|
||||
consulServices, err := p.fetchService(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, consulService := range consulServices {
|
||||
labels := tagsToNeutralLabels(consulService.ServiceTags, p.Prefix)
|
||||
item := itemData{
|
||||
ID: consulService.ServiceID,
|
||||
Name: consulService.ServiceName,
|
||||
Address: consulService.ServiceAddress,
|
||||
Port: strconv.Itoa(consulService.ServicePort),
|
||||
Labels: labels,
|
||||
Status: consulService.Checks.AggregatedStatus(),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (p *Provider) fetchService(ctx context.Context, name string) ([]*api.CatalogService, error) {
|
||||
var tagFilter string
|
||||
if !p.ExposedByDefault {
|
||||
tagFilter = p.Prefix + ".enable=true"
|
||||
}
|
||||
|
||||
consulServices, _, err := p.client.Catalog().Service(name, tagFilter, &api.QueryOptions{})
|
||||
return consulServices, err
|
||||
}
|
||||
|
||||
func (p *Provider) fetchServices(ctx context.Context) (map[string][]string, error) {
|
||||
serviceNames, _, err := p.client.Catalog().Services(&api.QueryOptions{})
|
||||
return serviceNames, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
26
pkg/provider/consulcatalog/convert_types.go
Normal file
26
pkg/provider/consulcatalog/convert_types.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package consulcatalog
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func tagsToNeutralLabels(tags []string, prefix string) map[string]string {
|
||||
var labels map[string]string
|
||||
|
||||
for _, tag := range tags {
|
||||
if strings.HasPrefix(tag, 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 := "traefik." + strings.TrimPrefix(parts[0], prefix+".")
|
||||
labels[key] = parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
64
pkg/provider/consulcatalog/convert_types_test.go
Normal file
64
pkg/provider/consulcatalog/convert_types_test.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package consulcatalog
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTagsToNeutralLabels(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
tags []string
|
||||
prefix string
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
desc: "without tags",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
desc: "with a prefix",
|
||||
prefix: "test",
|
||||
tags: []string{
|
||||
"test.aaa=01",
|
||||
"test.bbb=02",
|
||||
"ccc=03",
|
||||
"test.ddd=04=to",
|
||||
},
|
||||
expected: map[string]string{
|
||||
"traefik.aaa": "01",
|
||||
"traefik.bbb": "02",
|
||||
"traefik.ddd": "04=to",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
desc: "with an empty prefix",
|
||||
prefix: "",
|
||||
tags: []string{
|
||||
"test.aaa=01",
|
||||
"test.bbb=02",
|
||||
"ccc=03",
|
||||
"test.ddd=04=to",
|
||||
},
|
||||
expected: map[string]string{
|
||||
"traefik.test.aaa": "01",
|
||||
"traefik.test.bbb": "02",
|
||||
"traefik.ccc": "03",
|
||||
"traefik.test.ddd": "04=to",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
labels := tagsToNeutralLabels(test.tags, test.prefix)
|
||||
|
||||
assert.Equal(t, test.expected, labels)
|
||||
})
|
||||
}
|
||||
}
|
23
pkg/provider/consulcatalog/label.go
Normal file
23
pkg/provider/consulcatalog/label.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package consulcatalog
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/v2/pkg/config/label"
|
||||
)
|
||||
|
||||
// configuration Contains information from the labels that are globals (not related to the dynamic configuration) or specific to the provider.
|
||||
type configuration struct {
|
||||
Enable bool
|
||||
}
|
||||
|
||||
func (p *Provider) getConfiguration(item itemData) (configuration, error) {
|
||||
conf := configuration{
|
||||
Enable: p.ExposedByDefault,
|
||||
}
|
||||
|
||||
err := label.Decode(item.Labels, &conf, "traefik.consulcatalog.", "traefik.enable")
|
||||
if err != nil {
|
||||
return configuration{}, err
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue