1
0
Fork 0

Support multiple namespaces for Consul and ConsulCatalog providers

Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
Romain 2022-06-03 12:00:09 +02:00 committed by GitHub
parent f352c34136
commit f90e3817e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 531 additions and 152 deletions

View file

@ -109,11 +109,15 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator {
}
if conf.ConsulCatalog != nil {
p.quietAddProvider(conf.ConsulCatalog)
for _, pvd := range conf.ConsulCatalog.BuildProviders() {
p.quietAddProvider(pvd)
}
}
if conf.Consul != nil {
p.quietAddProvider(conf.Consul)
for _, pvd := range conf.Consul.BuildProviders() {
p.quietAddProvider(pvd)
}
}
if conf.Etcd != nil {

View file

@ -219,7 +219,7 @@ func TestDefaultRule(t *testing.T) {
Status: api.HealthPassing,
},
},
defaultRule: DefaultTemplateRule,
defaultRule: defaultTemplateRule,
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
@ -262,8 +262,10 @@ func TestDefaultRule(t *testing.T) {
t.Parallel()
p := Provider{
ExposedByDefault: true,
DefaultRule: test.defaultRule,
Configuration: Configuration{
ExposedByDefault: true,
DefaultRule: test.defaultRule,
},
}
err := p.Init()
@ -2618,10 +2620,12 @@ func Test_buildConfiguration(t *testing.T) {
t.Parallel()
p := Provider{
ExposedByDefault: true,
DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)",
ConnectAware: test.ConnectAware,
Constraints: test.constraints,
Configuration: Configuration{
ExposedByDefault: true,
DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)",
ConnectAware: test.ConnectAware,
Constraints: test.constraints,
},
}
err := p.Init()
@ -2651,3 +2655,55 @@ func Test_buildConfiguration(t *testing.T) {
})
}
}
func TestNamespaces(t *testing.T) {
testCases := []struct {
desc string
namespace string
namespaces []string
expectedNamespaces []string
}{
{
desc: "no defined namespaces",
expectedNamespaces: []string{""},
},
{
desc: "deprecated: use of defined namespace",
namespace: "test-ns",
expectedNamespaces: []string{"test-ns"},
},
{
desc: "use of 1 defined namespaces",
namespaces: []string{"test-ns"},
expectedNamespaces: []string{"test-ns"},
},
{
desc: "use of multiple defined namespaces",
namespaces: []string{"test-ns1", "test-ns2", "test-ns3", "test-ns4"},
expectedNamespaces: []string{"test-ns1", "test-ns2", "test-ns3", "test-ns4"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
pb := &ProviderBuilder{
Namespace: test.namespace,
Namespaces: test.namespaces,
}
assert.Equal(t, test.expectedNamespaces, extractNSFromProvider(pb.BuildProviders()))
})
}
}
func extractNSFromProvider(providers []*Provider) []string {
res := make([]string, len(providers))
for i, p := range providers {
res[i] = p.namespace
}
return res
}

View file

@ -22,8 +22,11 @@ import (
"github.com/traefik/traefik/v2/pkg/types"
)
// DefaultTemplateRule The default template for the default rule.
const DefaultTemplateRule = "Host(`{{ normalize .Name }}`)"
// defaultTemplateRule is the default template for the default rule.
const defaultTemplateRule = "Host(`{{ normalize .Name }}`)"
// providerName is the Consul Catalog provider name.
const providerName = "consulcatalog"
var _ provider.Provider = (*Provider)(nil)
@ -41,12 +44,50 @@ type itemData struct {
ExtraConf configuration
}
// Provider holds configurations of the provider.
type Provider struct {
// ProviderBuilder is responsible for constructing namespaced instances of the Consul Catalog provider.
type ProviderBuilder struct {
Configuration `export:"true"`
// Deprecated: use Namespaces option instead.
Namespace string `description:"Sets the namespace used to discover services (Consul Enterprise only)." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"`
Namespaces []string `description:"Sets the namespaces used to discover services (Consul Enterprise only)." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty"`
}
// BuildProviders builds Consul Catalog provider instances for the given namespaces configuration.
func (p *ProviderBuilder) BuildProviders() []*Provider {
// We can warn about that, because we've already made sure before that
// Namespace and Namespaces are mutually exclusive.
if p.Namespace != "" {
log.WithoutContext().Warnf("Namespace option is deprecated, please use the Namespaces option instead.")
}
if len(p.Namespaces) == 0 {
return []*Provider{{
Configuration: p.Configuration,
name: providerName,
// p.Namespace could very well be empty.
namespace: p.Namespace,
}}
}
var providers []*Provider
for _, namespace := range p.Namespaces {
providers = append(providers, &Provider{
Configuration: p.Configuration,
name: providerName + "-" + namespace,
namespace: namespace,
})
}
return providers
}
// Configuration represents the Consul Catalog provider configuration.
type Configuration 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 ptypes.Duration `description:"Interval for check Consul API. Default 15s" json:"refreshInterval,omitempty" toml:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty" export:"true"`
Prefix string `description:"Prefix for consul service tags." json:"prefix,omitempty" toml:"prefix,omitempty" yaml:"prefix,omitempty" export:"true"`
RefreshInterval ptypes.Duration `description:"Interval for check Consul API." json:"refreshInterval,omitempty" toml:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty" export:"true"`
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"`
@ -55,9 +96,25 @@ type Provider struct {
ConnectAware bool `description:"Enable Consul Connect support." json:"connectAware,omitempty" toml:"connectAware,omitempty" yaml:"connectAware,omitempty" export:"true"`
ConnectByDefault bool `description:"Consider every service as Connect capable by default." json:"connectByDefault,omitempty" toml:"connectByDefault,omitempty" yaml:"connectByDefault,omitempty" export:"true"`
ServiceName string `description:"Name of the Traefik service in Consul Catalog (needs to be registered via the orchestrator or manually)." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"`
Namespace string `description:"Sets the namespace used to discover services (Consul Enterprise only)." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty" export:"true"`
Watch bool `description:"Watch Consul API events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"`
}
// SetDefaults sets the default values.
func (c *Configuration) SetDefaults() {
c.Endpoint = &EndpointConfig{}
c.RefreshInterval = ptypes.Duration(15 * time.Second)
c.Prefix = "traefik"
c.ExposedByDefault = true
c.DefaultRule = defaultTemplateRule
c.ServiceName = "traefik"
}
// Provider is the Consul Catalog provider implementation.
type Provider struct {
Configuration
name string
namespace string
client *api.Client
defaultRuleTpl *template.Template
certChan chan *connectCert
@ -81,17 +138,6 @@ type EndpointHTTPAuthConfig struct {
Password string `description:"Basic Auth password" json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty" loggable:"false"`
}
// SetDefaults sets the default values.
func (p *Provider) SetDefaults() {
endpoint := &EndpointConfig{}
p.Endpoint = endpoint
p.RefreshInterval = ptypes.Duration(15 * time.Second)
p.Prefix = "traefik"
p.ExposedByDefault = true
p.DefaultRule = DefaultTemplateRule
p.ServiceName = "traefik"
}
// Init the provider.
func (p *Provider) Init() error {
defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil)
@ -103,19 +149,24 @@ func (p *Provider) Init() error {
p.certChan = make(chan *connectCert, 1)
p.watchServicesChan = make(chan struct{}, 1)
// In case they didn't initialize Provider with BuildProviders.
if p.name == "" {
p.name = providerName
}
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 {
var err error
p.client, err = createClient(p.Namespace, p.Endpoint)
p.client, err = createClient(p.namespace, p.Endpoint)
if err != nil {
return fmt.Errorf("failed to create consul client: %w", err)
}
pool.GoCtx(func(routineCtx context.Context) {
ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "consulcatalog"))
ctxLog := log.With(routineCtx, log.Str(log.ProviderName, p.name))
logger := log.FromContext(ctxLog)
operation := func() error {
@ -210,7 +261,7 @@ func (p *Provider) loadConfiguration(ctx context.Context, certInfo *connectCert,
}
configurationChan <- dynamic.Message{
ProviderName: "consulcatalog",
ProviderName: p.name,
Configuration: p.buildConfiguration(ctx, data, certInfo),
}

View file

@ -4,30 +4,80 @@ import (
"errors"
"github.com/kvtools/valkeyrie/store"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/provider/kv"
)
// providerName is the Consul provider name.
const providerName = "consul"
var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider.
type Provider struct {
// ProviderBuilder is responsible for constructing namespaced instances of the Consul provider.
type ProviderBuilder struct {
kv.Provider `export:"true"`
// Deprecated: use Namespaces instead.
Namespace string `description:"Sets the namespace used to discover the configuration (Consul Enterprise only)." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"`
Namespaces []string `description:"Sets the namespaces used to discover the configuration (Consul Enterprise only)." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty"`
}
// SetDefaults sets the default values.
func (p *Provider) SetDefaults() {
func (p *ProviderBuilder) SetDefaults() {
p.Provider.SetDefaults()
p.Endpoints = []string{"127.0.0.1:8500"}
}
// BuildProviders builds Consul provider instances for the given namespaces configuration.
func (p *ProviderBuilder) BuildProviders() []*Provider {
// We can warn about that, because we've already made sure before that
// Namespace and Namespaces are mutually exclusive.
if p.Namespace != "" {
log.WithoutContext().Warnf("Namespace option is deprecated, please use the Namespaces option instead.")
}
if len(p.Namespaces) == 0 {
return []*Provider{{
Provider: p.Provider,
name: providerName,
// p.Namespace could very well be empty.
namespace: p.Namespace,
}}
}
var providers []*Provider
for _, namespace := range p.Namespaces {
providers = append(providers, &Provider{
Provider: p.Provider,
name: providerName + "-" + namespace,
namespace: namespace,
})
}
return providers
}
// Provider holds configurations of the provider.
type Provider struct {
kv.Provider
name string
namespace string
}
// Init the provider.
func (p *Provider) Init() error {
// Wildcard namespace allows fetching KV values from any namespace for recursive requests (see https://www.consul.io/api/kv#ns).
// As we are not supporting multiple namespaces at the same time, wildcard namespace is not allowed.
if p.Namespace == "*" {
if p.namespace == "*" {
return errors.New("wildcard namespace is not supported")
}
return p.Provider.Init(store.CONSUL, "consul")
// In case they didn't initialize with BuildProviders.
if p.name == "" {
p.name = providerName
}
return p.Provider.Init(store.CONSUL, p.name, p.namespace)
}

View file

@ -0,0 +1,59 @@
package consul
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNamespaces(t *testing.T) {
testCases := []struct {
desc string
namespace string
namespaces []string
expectedNamespaces []string
}{
{
desc: "no defined namespaces",
expectedNamespaces: []string{""},
},
{
desc: "deprecated: use of defined namespace",
namespace: "test-ns",
expectedNamespaces: []string{"test-ns"},
},
{
desc: "use of 1 defined namespaces",
namespaces: []string{"test-ns"},
expectedNamespaces: []string{"test-ns"},
},
{
desc: "use of multiple defined namespaces",
namespaces: []string{"test-ns1", "test-ns2", "test-ns3", "test-ns4"},
expectedNamespaces: []string{"test-ns1", "test-ns2", "test-ns3", "test-ns4"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
pb := &ProviderBuilder{
Namespace: test.namespace,
Namespaces: test.namespaces,
}
assert.Equal(t, test.expectedNamespaces, extractNSFromProvider(pb.BuildProviders()))
})
}
}
func extractNSFromProvider(providers []*Provider) []string {
res := make([]string, len(providers))
for i, p := range providers {
res[i] = p.namespace
}
return res
}

View file

@ -21,5 +21,5 @@ func (p *Provider) SetDefaults() {
// Init the provider.
func (p *Provider) Init() error {
return p.Provider.Init(store.ETCDV3, "etcd")
return p.Provider.Init(store.ETCDV3, "etcd", "")
}

View file

@ -30,12 +30,12 @@ type Provider struct {
Username string `description:"KV Username" json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty" loggable:"false"`
Password string `description:"KV Password" json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty" loggable:"false"`
Token string `description:"KV Token" json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
Namespace string `description:"KV Namespace" json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"`
TLS *types.ClientTLS `description:"Enable TLS support" json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true" `
name string
namespace string
storeType store.Backend
kvClient store.Store
name string
}
// SetDefaults sets the default values.
@ -44,11 +44,12 @@ func (p *Provider) SetDefaults() {
}
// Init the provider.
func (p *Provider) Init(storeType store.Backend, name string) error {
func (p *Provider) Init(storeType store.Backend, name, namespace string) error {
ctx := log.With(context.Background(), log.Str(log.ProviderName, name))
p.storeType = storeType
p.name = name
p.namespace = namespace
p.storeType = storeType
kvClient, err := p.createKVClient(ctx)
if err != nil {
@ -167,7 +168,7 @@ func (p *Provider) createKVClient(ctx context.Context) (store.Store, error) {
Username: p.Username,
Password: p.Password,
Token: p.Token,
Namespace: p.Namespace,
Namespace: p.namespace,
}
if p.TLS != nil {

View file

@ -21,5 +21,5 @@ func (p *Provider) SetDefaults() {
// Init the provider.
func (p *Provider) Init() error {
return p.Provider.Init(store.REDIS, "redis")
return p.Provider.Init(store.REDIS, "redis", "")
}

View file

@ -21,5 +21,5 @@ func (p *Provider) SetDefaults() {
// Init the provider.
func (p *Provider) Init() error {
return p.Provider.Init(store.ZK, "zookeeper")
return p.Provider.Init(store.ZK, "zookeeper", "")
}