Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Nomad canary deployment #9216

Merged
merged 5 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/content/routing/providers/consul-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

#### `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.
mpl marked this conversation as resolved.
Show resolved Hide resolved

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

Traefik is capable of detecting the port to use, by following the default consul Catalog flow.
Expand Down
13 changes: 13 additions & 0 deletions docs/content/routing/providers/nomad.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

#### `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.
mpl marked this conversation as resolved.
Show resolved Hide resolved

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

Traefik is capable of detecting the port to use, by following the default Nomad Service Discovery flow.
Expand Down
65 changes: 37 additions & 28 deletions pkg/provider/consulcatalog/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"context"
"errors"
"fmt"
"hash/fnv"
"net"
"sort"
"strings"

"github.com/hashicorp/consul/api"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
Expand Down Expand Up @@ -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 {
tcpOrUDP = true

err := p.buildTCPServiceConfiguration(ctxSvc, item, confFromLabel.TCP)
if err != nil {
if err := p.buildTCPServiceConfiguration(item, confFromLabel.TCP); err != nil {
logger.Error(err)
continue
}
Expand All @@ -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 {
tcpOrUDP = true

err := p.buildUDPServiceConfiguration(ctxSvc, item, confFromLabel.UDP)
if err != nil {
if err := p.buildUDPServiceConfiguration(item, confFromLabel.UDP); err != nil {
logger.Error(err)
continue
}
Expand All @@ -75,8 +76,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, items []itemData, cer
}
}

err = p.buildServiceConfiguration(ctxSvc, item, confFromLabel.HTTP)
if err != nil {
if err = p.buildServiceConfiguration(item, confFromLabel.HTTP); err != nil {
logger.Error(err)
continue
}
Expand All @@ -89,7 +89,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, items []itemData, cer
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
}
Expand Down Expand Up @@ -128,75 +128,69 @@ func (p *Provider) keepContainer(ctx context.Context, item itemData) bool {
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 {
configuration.Services = make(map[string]*dynamic.TCPService)

lb := &dynamic.TCPServersLoadBalancer{}
lb.SetDefaults()

configuration.Services[provider.Normalize(item.Name)] = &dynamic.TCPService{
configuration.Services[getName(item)] = &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 {
for _, service := range configuration.Services {
if err := p.addServerTCP(item, service.LoadBalancer); err != nil {
return err
}
}

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 {
configuration.Services = make(map[string]*dynamic.UDPService)

lb := &dynamic.UDPServersLoadBalancer{}

configuration.Services[provider.Normalize(item.Name)] = &dynamic.UDPService{
configuration.Services[getName(item)] = &dynamic.UDPService{
LoadBalancer: lb,
}
}

for name, service := range configuration.Services {
ctxSvc := log.With(ctx, log.Str(log.ServiceName, name))
err := p.addServerUDP(ctxSvc, item, service.LoadBalancer)
if err != nil {
for _, service := range configuration.Services {
if err := p.addServerUDP(item, service.LoadBalancer); err != nil {
return err
}
}

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 {
configuration.Services = make(map[string]*dynamic.Service)

lb := &dynamic.ServersLoadBalancer{}
lb.SetDefaults()

configuration.Services[provider.Normalize(item.Name)] = &dynamic.Service{
configuration.Services[getName(item)] = &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 {
for _, service := range configuration.Services {
if err := p.addServer(item, service.LoadBalancer); err != nil {
return err
}
}

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 {
return errors.New("load-balancer is not defined")
}
Expand Down Expand Up @@ -227,7 +221,7 @@ func (p *Provider) addServerTCP(ctx context.Context, item itemData, loadBalancer
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 {
return errors.New("load-balancer is not defined")
}
Expand All @@ -254,7 +248,7 @@ func (p *Provider) addServerUDP(ctx context.Context, item itemData, loadBalancer
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 {
return errors.New("load-balancer is not defined")
}
Expand Down Expand Up @@ -300,3 +294,18 @@ func (p *Provider) addServer(ctx context.Context, item itemData, loadBalancer *d
func itemServersTransportKey(item itemData) string {
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()))
}