Support Nomad canary deployment

Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
Co-authored-by: Mathieu Lonjaret <mathieu.lonjaret@gmail.com>
This commit is contained in:
Romain 2022-08-01 17:52:08 +02:00 committed by GitHub
parent ab94bbaece
commit 2a2ea759d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 593 additions and 51 deletions

View file

@ -470,6 +470,20 @@ You can tell Traefik to consider (or not) the service as a Connect capable one b
This option overrides the value of `connectByDefault`. This option overrides the value of `connectByDefault`.
#### `traefik.consulcatalog.canary`
```yaml
traefik.consulcatalog.canary=true
```
When ConsulCatalog, in the context of a Nomad orchestrator,
is a provider (of service registration) for Traefik,
one might have the need to distinguish within Traefik between a [Canary](https://learn.hashicorp.com/tutorials/nomad/job-blue-green-and-canary-deployments#deploy-with-canaries) instance of a service, or a production one.
For example if one does not want them to be part of the same load-balancer.
Therefore, this option, which is meant to be provided as one of the values of the `canary_tags` field in the Nomad [service stanza](https://www.nomadproject.io/docs/job-specification/service#canary_tags),
allows Traefik to identify that the associated instance is a canary one.
#### Port Lookup #### Port Lookup
Traefik is capable of detecting the port to use, by following the default consul Catalog flow. Traefik is capable of detecting the port to use, by following the default consul Catalog flow.

View file

@ -460,6 +460,19 @@ You can tell Traefik to consider (or not) the service by setting `traefik.enable
This option overrides the value of `exposedByDefault`. This option overrides the value of `exposedByDefault`.
#### `traefik.nomad.canary`
```yaml
traefik.nomad.canary=true
```
When Nomad orchestrator is a provider (of service registration) for Traefik,
one might have the need to distinguish within Traefik between a [Canary](https://learn.hashicorp.com/tutorials/nomad/job-blue-green-and-canary-deployments#deploy-with-canaries) instance of a service, or a production one.
For example if one does not want them to be part of the same load-balancer.
Therefore, this option, which is meant to be provided as one of the values of the `canary_tags` field in the Nomad [service stanza](https://www.nomadproject.io/docs/job-specification/service#canary_tags),
allows Traefik to identify that the associated instance is a canary one.
#### Port Lookup #### Port Lookup
Traefik is capable of detecting the port to use, by following the default Nomad Service Discovery flow. Traefik is capable of detecting the port to use, by following the default Nomad Service Discovery flow.

View file

@ -4,7 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"hash/fnv"
"net" "net"
"sort"
"strings"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/dynamic"
@ -37,8 +40,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, items []itemData, cer
if len(confFromLabel.TCP.Routers) > 0 || len(confFromLabel.TCP.Services) > 0 { if len(confFromLabel.TCP.Routers) > 0 || len(confFromLabel.TCP.Services) > 0 {
tcpOrUDP = true tcpOrUDP = true
err := p.buildTCPServiceConfiguration(ctxSvc, item, confFromLabel.TCP) if err := p.buildTCPServiceConfiguration(item, confFromLabel.TCP); err != nil {
if err != nil {
logger.Error(err) logger.Error(err)
continue continue
} }
@ -49,8 +51,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, items []itemData, cer
if len(confFromLabel.UDP.Routers) > 0 || len(confFromLabel.UDP.Services) > 0 { if len(confFromLabel.UDP.Routers) > 0 || len(confFromLabel.UDP.Services) > 0 {
tcpOrUDP = true tcpOrUDP = true
err := p.buildUDPServiceConfiguration(ctxSvc, item, confFromLabel.UDP) if err := p.buildUDPServiceConfiguration(item, confFromLabel.UDP); err != nil {
if err != nil {
logger.Error(err) logger.Error(err)
continue continue
} }
@ -75,8 +76,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, items []itemData, cer
} }
} }
err = p.buildServiceConfiguration(ctxSvc, item, confFromLabel.HTTP) if err = p.buildServiceConfiguration(item, confFromLabel.HTTP); err != nil {
if err != nil {
logger.Error(err) logger.Error(err)
continue continue
} }
@ -89,7 +89,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, items []itemData, cer
Labels: item.Labels, Labels: item.Labels,
} }
provider.BuildRouterConfiguration(ctx, confFromLabel.HTTP, provider.Normalize(item.Name), p.defaultRuleTpl, model) provider.BuildRouterConfiguration(ctx, confFromLabel.HTTP, getName(item), p.defaultRuleTpl, model)
configurations[svcName] = confFromLabel configurations[svcName] = confFromLabel
} }
@ -128,22 +128,20 @@ func (p *Provider) keepContainer(ctx context.Context, item itemData) bool {
return true return true
} }
func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, item itemData, configuration *dynamic.TCPConfiguration) error { func (p *Provider) buildTCPServiceConfiguration(item itemData, configuration *dynamic.TCPConfiguration) error {
if len(configuration.Services) == 0 { if len(configuration.Services) == 0 {
configuration.Services = make(map[string]*dynamic.TCPService) configuration.Services = make(map[string]*dynamic.TCPService)
lb := &dynamic.TCPServersLoadBalancer{} lb := &dynamic.TCPServersLoadBalancer{}
lb.SetDefaults() lb.SetDefaults()
configuration.Services[provider.Normalize(item.Name)] = &dynamic.TCPService{ configuration.Services[getName(item)] = &dynamic.TCPService{
LoadBalancer: lb, LoadBalancer: lb,
} }
} }
for name, service := range configuration.Services { for _, service := range configuration.Services {
ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) if err := p.addServerTCP(item, service.LoadBalancer); err != nil {
err := p.addServerTCP(ctxSvc, item, service.LoadBalancer)
if err != nil {
return err return err
} }
} }
@ -151,21 +149,19 @@ func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, item itemDa
return nil return nil
} }
func (p *Provider) buildUDPServiceConfiguration(ctx context.Context, item itemData, configuration *dynamic.UDPConfiguration) error { func (p *Provider) buildUDPServiceConfiguration(item itemData, configuration *dynamic.UDPConfiguration) error {
if len(configuration.Services) == 0 { if len(configuration.Services) == 0 {
configuration.Services = make(map[string]*dynamic.UDPService) configuration.Services = make(map[string]*dynamic.UDPService)
lb := &dynamic.UDPServersLoadBalancer{} lb := &dynamic.UDPServersLoadBalancer{}
configuration.Services[provider.Normalize(item.Name)] = &dynamic.UDPService{ configuration.Services[getName(item)] = &dynamic.UDPService{
LoadBalancer: lb, LoadBalancer: lb,
} }
} }
for name, service := range configuration.Services { for _, service := range configuration.Services {
ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) if err := p.addServerUDP(item, service.LoadBalancer); err != nil {
err := p.addServerUDP(ctxSvc, item, service.LoadBalancer)
if err != nil {
return err return err
} }
} }
@ -173,22 +169,20 @@ func (p *Provider) buildUDPServiceConfiguration(ctx context.Context, item itemDa
return nil return nil
} }
func (p *Provider) buildServiceConfiguration(ctx context.Context, item itemData, configuration *dynamic.HTTPConfiguration) error { func (p *Provider) buildServiceConfiguration(item itemData, configuration *dynamic.HTTPConfiguration) error {
if len(configuration.Services) == 0 { if len(configuration.Services) == 0 {
configuration.Services = make(map[string]*dynamic.Service) configuration.Services = make(map[string]*dynamic.Service)
lb := &dynamic.ServersLoadBalancer{} lb := &dynamic.ServersLoadBalancer{}
lb.SetDefaults() lb.SetDefaults()
configuration.Services[provider.Normalize(item.Name)] = &dynamic.Service{ configuration.Services[getName(item)] = &dynamic.Service{
LoadBalancer: lb, LoadBalancer: lb,
} }
} }
for name, service := range configuration.Services { for _, service := range configuration.Services {
ctxSvc := log.With(ctx, log.Str(log.ServiceName, name)) if err := p.addServer(item, service.LoadBalancer); err != nil {
err := p.addServer(ctxSvc, item, service.LoadBalancer)
if err != nil {
return err return err
} }
} }
@ -196,7 +190,7 @@ func (p *Provider) buildServiceConfiguration(ctx context.Context, item itemData,
return nil return nil
} }
func (p *Provider) addServerTCP(ctx context.Context, item itemData, loadBalancer *dynamic.TCPServersLoadBalancer) error { func (p *Provider) addServerTCP(item itemData, loadBalancer *dynamic.TCPServersLoadBalancer) error {
if loadBalancer == nil { if loadBalancer == nil {
return errors.New("load-balancer is not defined") return errors.New("load-balancer is not defined")
} }
@ -227,7 +221,7 @@ func (p *Provider) addServerTCP(ctx context.Context, item itemData, loadBalancer
return nil return nil
} }
func (p *Provider) addServerUDP(ctx context.Context, item itemData, loadBalancer *dynamic.UDPServersLoadBalancer) error { func (p *Provider) addServerUDP(item itemData, loadBalancer *dynamic.UDPServersLoadBalancer) error {
if loadBalancer == nil { if loadBalancer == nil {
return errors.New("load-balancer is not defined") return errors.New("load-balancer is not defined")
} }
@ -254,7 +248,7 @@ func (p *Provider) addServerUDP(ctx context.Context, item itemData, loadBalancer
return nil return nil
} }
func (p *Provider) addServer(ctx context.Context, item itemData, loadBalancer *dynamic.ServersLoadBalancer) error { func (p *Provider) addServer(item itemData, loadBalancer *dynamic.ServersLoadBalancer) error {
if loadBalancer == nil { if loadBalancer == nil {
return errors.New("load-balancer is not defined") return errors.New("load-balancer is not defined")
} }
@ -300,3 +294,18 @@ func (p *Provider) addServer(ctx context.Context, item itemData, loadBalancer *d
func itemServersTransportKey(item itemData) string { func itemServersTransportKey(item itemData) string {
return provider.Normalize("tls-" + item.Namespace + "-" + item.Datacenter + "-" + item.Name) return provider.Normalize("tls-" + item.Namespace + "-" + item.Datacenter + "-" + item.Name)
} }
func getName(i itemData) string {
if !i.ExtraConf.ConsulCatalog.Canary {
return provider.Normalize(i.Name)
}
tags := make([]string, len(i.Tags))
copy(tags, i.Tags)
sort.Strings(tags)
hasher := fnv.New64()
hasher.Write([]byte(strings.Join(tags, "")))
return provider.Normalize(fmt.Sprintf("%s-%d", i.Name, hasher.Sum64()))
}

View file

@ -273,7 +273,7 @@ func TestDefaultRule(t *testing.T) {
for i := 0; i < len(test.items); i++ { for i := 0; i < len(test.items); i++ {
var err error var err error
test.items[i].ExtraConf, err = p.getConfiguration(test.items[i].Labels) test.items[i].ExtraConf, err = p.getExtraConf(test.items[i].Labels)
require.NoError(t, err) require.NoError(t, err)
} }
@ -2611,6 +2611,253 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
}, },
{
desc: "two HTTP service instances with one canary",
ConnectAware: true,
items: []itemData{
{
ID: "1",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Labels: map[string]string{
"traefik.consulcatalog.connect": "true",
},
Address: "127.0.0.1",
Port: "80",
Status: api.HealthPassing,
},
{
ID: "2",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Labels: map[string]string{
"traefik.consulcatalog.connect": "true",
"traefik.consulcatalog.canary": "true",
},
Address: "127.0.0.2",
Port: "80",
Status: api.HealthPassing,
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"Test": {
Service: "Test",
Rule: "Host(`Test.traefik.wtf`)",
},
"Test-97077516270503695": {
Service: "Test-97077516270503695",
Rule: "Host(`Test.traefik.wtf`)",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"Test": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "https://127.0.0.1:80",
},
},
PassHostHeader: Bool(true),
ServersTransport: "tls-ns-dc1-Test",
},
},
"Test-97077516270503695": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "https://127.0.0.2:80",
},
},
PassHostHeader: Bool(true),
ServersTransport: "tls-ns-dc1-Test",
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"tls-ns-dc1-Test": {
ServerName: "ns-dc1-Test",
InsecureSkipVerify: true,
RootCAs: []tls.FileOrContent{
"root",
},
Certificates: []tls.Certificate{
{
CertFile: "cert",
KeyFile: "key",
},
},
PeerCertURI: "spiffe:///ns/ns/dc/dc1/svc/Test",
},
},
},
},
},
{
desc: "two TCP service instances with one canary",
ConnectAware: true,
items: []itemData{
{
ID: "1",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Labels: map[string]string{
"traefik.tcp.routers.test.rule": "HostSNI(`foobar`)",
},
Address: "127.0.0.1",
Port: "80",
Status: api.HealthPassing,
},
{
ID: "2",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Labels: map[string]string{
"traefik.consulcatalog.canary": "true",
"traefik.tcp.routers.test-canary.rule": "HostSNI(`canary.foobar`)",
},
Address: "127.0.0.2",
Port: "80",
Status: api.HealthPassing,
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"test": {
Service: "Test",
Rule: "HostSNI(`foobar`)",
},
"test-canary": {
Service: "Test-17573747155436217342",
Rule: "HostSNI(`canary.foobar`)",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"Test": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{Address: "127.0.0.1:80"},
},
TerminationDelay: Int(100),
},
},
"Test-17573747155436217342": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{Address: "127.0.0.2:80"},
},
TerminationDelay: Int(100),
},
},
},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
{
desc: "two UDP service instances with one canary",
ConnectAware: true,
items: []itemData{
{
ID: "1",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Labels: map[string]string{
"traefik.udp.routers.test.entrypoints": "udp",
},
Address: "127.0.0.1",
Port: "80",
Status: api.HealthPassing,
},
{
ID: "2",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Labels: map[string]string{
"traefik.consulcatalog.canary": "true",
"traefik.udp.routers.test-canary.entrypoints": "udp",
},
Address: "127.0.0.2",
Port: "80",
Status: api.HealthPassing,
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{
"test": {
EntryPoints: []string{"udp"},
Service: "Test",
},
"test-canary": {
EntryPoints: []string{"udp"},
Service: "Test-12825244908842506376",
},
},
Services: map[string]*dynamic.UDPService{
"Test": {
LoadBalancer: &dynamic.UDPServersLoadBalancer{
Servers: []dynamic.UDPServer{
{Address: "127.0.0.1:80"},
},
},
},
"Test-12825244908842506376": {
LoadBalancer: &dynamic.UDPServersLoadBalancer{
Servers: []dynamic.UDPServer{
{Address: "127.0.0.2:80"},
},
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
} }
for _, test := range testCases { for _, test := range testCases {
@ -2633,7 +2880,7 @@ func Test_buildConfiguration(t *testing.T) {
for i := 0; i < len(test.items); i++ { for i := 0; i < len(test.items); i++ {
var err error var err error
test.items[i].ExtraConf, err = p.getConfiguration(test.items[i].Labels) test.items[i].ExtraConf, err = p.getExtraConf(test.items[i].Labels)
require.NoError(t, err) require.NoError(t, err)
var tags []string var tags []string

View file

@ -283,13 +283,13 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error
for name, tags := range serviceNames { for name, tags := range serviceNames {
logger := log.FromContext(log.With(ctx, log.Str("serviceName", name))) logger := log.FromContext(log.With(ctx, log.Str("serviceName", name)))
svcCfg, err := p.getConfiguration(tagsToNeutralLabels(tags, p.Prefix)) extraConf, err := p.getExtraConf(tagsToNeutralLabels(tags, p.Prefix))
if err != nil { if err != nil {
logger.Errorf("Skip service: %v", err) logger.Errorf("Skip service: %v", err)
continue continue
} }
if !svcCfg.Enable { if !extraConf.Enable {
logger.Debug("Filtering disabled item") logger.Debug("Filtering disabled item")
continue continue
} }
@ -305,12 +305,12 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error
continue continue
} }
if !p.ConnectAware && svcCfg.ConsulCatalog.Connect { if !p.ConnectAware && extraConf.ConsulCatalog.Connect {
logger.Debugf("Filtering out Connect aware item, Connect support is not enabled") logger.Debugf("Filtering out Connect aware item, Connect support is not enabled")
continue continue
} }
consulServices, statuses, err := p.fetchService(ctx, name, svcCfg.ConsulCatalog.Connect) consulServices, statuses, err := p.fetchService(ctx, name, extraConf.ConsulCatalog.Connect)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -344,7 +344,7 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error
Status: status, Status: status,
} }
extraConf, err := p.getConfiguration(item.Labels) extraConf, err := p.getExtraConf(item.Labels)
if err != nil { if err != nil {
log.FromContext(ctx).Errorf("Skip item %s: %v", item.Name, err) log.FromContext(ctx).Errorf("Skip item %s: %v", item.Name, err)
continue continue

View file

@ -4,17 +4,19 @@ import (
"github.com/traefik/traefik/v2/pkg/config/label" "github.com/traefik/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. // configuration contains information from the labels that are globals (not related to the dynamic configuration) or specific to the provider.
type configuration struct { type configuration struct {
Enable bool Enable bool
ConsulCatalog specificConfiguration ConsulCatalog specificConfiguration
} }
type specificConfiguration struct { type specificConfiguration struct {
Connect bool Connect bool // <prefix>.consulcatalog.connect is the corresponding label.
Canary bool // <prefix>.consulcatalog.canary is the corresponding label.
} }
func (p *Provider) getConfiguration(labels map[string]string) (configuration, error) { // getExtraConf returns a configuration with settings which are not part of the dynamic configuration (e.g. "<prefix>.enable").
func (p *Provider) getExtraConf(labels map[string]string) (configuration, error) {
conf := configuration{ conf := configuration{
Enable: p.ExposedByDefault, Enable: p.ExposedByDefault,
ConsulCatalog: specificConfiguration{Connect: p.ConnectByDefault}, ConsulCatalog: specificConfiguration{Connect: p.ConnectByDefault},

View file

@ -4,8 +4,11 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"hash/fnv"
"net" "net"
"sort"
"strconv" "strconv"
"strings"
"github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/config/label" "github.com/traefik/traefik/v2/pkg/config/label"
@ -76,7 +79,7 @@ func (p *Provider) buildConfig(ctx context.Context, items []item) *dynamic.Confi
Labels: labels, Labels: labels,
} }
provider.BuildRouterConfiguration(ctx, config.HTTP, provider.Normalize(i.Name), p.defaultRuleTpl, model) provider.BuildRouterConfiguration(ctx, config.HTTP, getName(i), p.defaultRuleTpl, model)
configurations[svcName] = config configurations[svcName] = config
} }
@ -90,7 +93,7 @@ func (p *Provider) buildTCPConfig(i item, configuration *dynamic.TCPConfiguratio
lb := new(dynamic.TCPServersLoadBalancer) lb := new(dynamic.TCPServersLoadBalancer)
lb.SetDefaults() lb.SetDefaults()
configuration.Services[provider.Normalize(i.Name)] = &dynamic.TCPService{ configuration.Services[getName(i)] = &dynamic.TCPService{
LoadBalancer: lb, LoadBalancer: lb,
} }
} }
@ -108,7 +111,7 @@ func (p *Provider) buildUDPConfig(i item, configuration *dynamic.UDPConfiguratio
if len(configuration.Services) == 0 { if len(configuration.Services) == 0 {
configuration.Services = make(map[string]*dynamic.UDPService) configuration.Services = make(map[string]*dynamic.UDPService)
configuration.Services[provider.Normalize(i.Name)] = &dynamic.UDPService{ configuration.Services[getName(i)] = &dynamic.UDPService{
LoadBalancer: new(dynamic.UDPServersLoadBalancer), LoadBalancer: new(dynamic.UDPServersLoadBalancer),
} }
} }
@ -129,7 +132,7 @@ func (p *Provider) buildServiceConfig(i item, configuration *dynamic.HTTPConfigu
lb := new(dynamic.ServersLoadBalancer) lb := new(dynamic.ServersLoadBalancer)
lb.SetDefaults() lb.SetDefaults()
configuration.Services[provider.Normalize(i.Name)] = &dynamic.Service{ configuration.Services[getName(i)] = &dynamic.Service{
LoadBalancer: lb, LoadBalancer: lb,
} }
} }
@ -265,3 +268,18 @@ func (p *Provider) addServer(i item, lb *dynamic.ServersLoadBalancer) error {
return nil return nil
} }
func getName(i item) string {
if !i.ExtraConf.Canary {
return provider.Normalize(i.Name)
}
tags := make([]string, len(i.Tags))
copy(tags, i.Tags)
sort.Strings(tags)
hasher := fnv.New64()
hasher.Write([]byte(strings.Join(tags, "")))
return provider.Normalize(fmt.Sprintf("%s-%d", i.Name, hasher.Sum64()))
}

View file

@ -2209,6 +2209,239 @@ func Test_buildConfig(t *testing.T) {
}, },
}, },
}, },
{
desc: "two HTTP service instances with one canary",
items: []item{
{
ID: "1",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Tags: []string{},
Address: "127.0.0.1",
Port: 80,
ExtraConf: configuration{Enable: true},
},
{
ID: "2",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Tags: []string{
"traefik.nomad.canary = true",
},
Address: "127.0.0.2",
Port: 80,
ExtraConf: configuration{
Enable: true,
Canary: true,
},
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"Test": {
Service: "Test",
Rule: "Host(`Test.traefik.test`)",
},
"Test-1234154071633021619": {
Service: "Test-1234154071633021619",
Rule: "Host(`Test.traefik.test`)",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"Test": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:80",
},
},
PassHostHeader: Bool(true),
},
},
"Test-1234154071633021619": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
{
desc: "two TCP service instances with one canary",
items: []item{
{
ID: "1",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Tags: []string{
"traefik.tcp.routers.test.rule = HostSNI(`foobar`)",
},
Address: "127.0.0.1",
Port: 80,
ExtraConf: configuration{Enable: true},
},
{
ID: "2",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Tags: []string{
"traefik.nomad.canary = true",
"traefik.tcp.routers.test-canary.rule = HostSNI(`canary.foobar`)",
},
Address: "127.0.0.2",
Port: 80,
ExtraConf: configuration{
Enable: true,
Canary: true,
},
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"test": {
Service: "Test",
Rule: "HostSNI(`foobar`)",
},
"test-canary": {
Service: "Test-8769860286750522282",
Rule: "HostSNI(`canary.foobar`)",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"Test": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{Address: "127.0.0.1:80"},
},
TerminationDelay: Int(100),
},
},
"Test-8769860286750522282": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{Address: "127.0.0.2:80"},
},
TerminationDelay: Int(100),
},
},
},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
{
desc: "two UDP service instances with one canary",
items: []item{
{
ID: "1",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Tags: []string{
"traefik.udp.routers.test.entrypoints = udp",
},
Address: "127.0.0.1",
Port: 80,
ExtraConf: configuration{Enable: true},
},
{
ID: "2",
Node: "Node1",
Datacenter: "dc1",
Name: "Test",
Namespace: "ns",
Tags: []string{
"traefik.nomad.canary = true",
"traefik.udp.routers.test-canary.entrypoints = udp",
},
Address: "127.0.0.2",
Port: 80,
ExtraConf: configuration{
Enable: true,
Canary: true,
},
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{
"test": {
EntryPoints: []string{"udp"},
Service: "Test",
},
"test-canary": {
EntryPoints: []string{"udp"},
Service: "Test-1611429260986126224",
},
},
Services: map[string]*dynamic.UDPService{
"Test": {
LoadBalancer: &dynamic.UDPServersLoadBalancer{
Servers: []dynamic.UDPServer{
{Address: "127.0.0.1:80"},
},
},
},
"Test-1611429260986126224": {
LoadBalancer: &dynamic.UDPServersLoadBalancer{
Servers: []dynamic.UDPServer{
{Address: "127.0.0.2:80"},
},
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
} }
for _, test := range testCases { for _, test := range testCases {

View file

@ -185,19 +185,25 @@ func createClient(namespace string, endpoint *EndpointConfig) (*api.Client, erro
// configuration contains information from the service's tags that are globals // configuration contains information from the service's tags that are globals
// (not specific to the dynamic configuration). // (not specific to the dynamic configuration).
type configuration struct { type configuration struct {
Enable bool // <prefix>.enable Enable bool // <prefix>.enable is the corresponding label.
Canary bool // <prefix>.nomad.canary is the corresponding label.
} }
// globalConfig returns a configuration with settings not specific to the dynamic configuration (i.e. "<prefix>.enable"). // getExtraConf returns a configuration with settings which are not part of the dynamic configuration (e.g. "<prefix>.enable").
func (p *Provider) globalConfig(tags []string) configuration { func (p *Provider) getExtraConf(tags []string) configuration {
enabled := p.ExposedByDefault
labels := tagsToLabels(tags, p.Prefix) labels := tagsToLabels(tags, p.Prefix)
enabled := p.ExposedByDefault
if v, exists := labels["traefik.enable"]; exists { if v, exists := labels["traefik.enable"]; exists {
enabled = strings.EqualFold(v, "true") enabled = strings.EqualFold(v, "true")
} }
return configuration{Enable: enabled} var canary bool
if v, exists := labels["traefik.nomad.canary"]; exists {
canary = strings.EqualFold(v, "true")
}
return configuration{Enable: enabled, Canary: canary}
} }
func (p *Provider) getNomadServiceData(ctx context.Context) ([]item, error) { func (p *Provider) getNomadServiceData(ctx context.Context) ([]item, error) {
@ -216,8 +222,8 @@ func (p *Provider) getNomadServiceData(ctx context.Context) ([]item, error) {
for _, service := range stub.Services { for _, service := range stub.Services {
logger := log.FromContext(log.With(ctx, log.Str("serviceName", service.ServiceName))) logger := log.FromContext(log.With(ctx, log.Str("serviceName", service.ServiceName)))
globalCfg := p.globalConfig(service.Tags) extraConf := p.getExtraConf(service.Tags)
if !globalCfg.Enable { if !extraConf.Enable {
logger.Debug("Filter Nomad service that is not enabled") logger.Debug("Filter Nomad service that is not enabled")
continue continue
} }
@ -248,7 +254,7 @@ func (p *Provider) getNomadServiceData(ctx context.Context) ([]item, error) {
Address: i.Address, Address: i.Address,
Port: i.Port, Port: i.Port,
Tags: i.Tags, Tags: i.Tags,
ExtraConf: p.globalConfig(i.Tags), ExtraConf: p.getExtraConf(i.Tags),
}) })
} }
} }

View file

@ -65,7 +65,7 @@ func Test_globalConfig(t *testing.T) {
for _, test := range cases { for _, test := range cases {
t.Run(test.Name, func(t *testing.T) { t.Run(test.Name, func(t *testing.T) {
p := Provider{ExposedByDefault: test.ExposedByDefault, Prefix: test.Prefix} p := Provider{ExposedByDefault: test.ExposedByDefault, Prefix: test.Prefix}
result := p.globalConfig(test.Tags) result := p.getExtraConf(test.Tags)
require.Equal(t, test.exp, result) require.Equal(t, test.exp, result)
}) })
} }