Merge pull request #274 from samber/consul-catalog-with-tags-settings
feat(traefik,consul-catalog): Set attributes from consul catalog tags (loadbalancer,circuitbreaker,weight)
This commit is contained in:
commit
5c8d9f4eb9
6 changed files with 189 additions and 29 deletions
1
cmd.go
1
cmd.go
|
@ -142,6 +142,7 @@ func init() {
|
||||||
traefikCmd.PersistentFlags().BoolVar(&arguments.consulCatalog, "consulCatalog", false, "Enable Consul catalog backend")
|
traefikCmd.PersistentFlags().BoolVar(&arguments.consulCatalog, "consulCatalog", false, "Enable Consul catalog backend")
|
||||||
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Domain, "consulCatalog.domain", "", "Default domain used")
|
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Domain, "consulCatalog.domain", "", "Default domain used")
|
||||||
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Endpoint, "consulCatalog.endpoint", "127.0.0.1:8500", "Consul server endpoint")
|
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Endpoint, "consulCatalog.endpoint", "127.0.0.1:8500", "Consul server endpoint")
|
||||||
|
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Prefix, "consulCatalog.prefix", "traefik", "Consul catalog tag prefix")
|
||||||
|
|
||||||
traefikCmd.PersistentFlags().BoolVar(&arguments.zookeeper, "zookeeper", false, "Enable Zookeeper backend")
|
traefikCmd.PersistentFlags().BoolVar(&arguments.zookeeper, "zookeeper", false, "Enable Zookeeper backend")
|
||||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Zookeeper.Watch, "zookeeper.watch", true, "Watch provider")
|
traefikCmd.PersistentFlags().BoolVar(&arguments.Zookeeper.Watch, "zookeeper.watch", true, "Watch provider")
|
||||||
|
|
15
docs/toml.md
15
docs/toml.md
|
@ -675,11 +675,26 @@ endpoint = "127.0.0.1:8500"
|
||||||
# Optional
|
# Optional
|
||||||
#
|
#
|
||||||
domain = "consul.localhost"
|
domain = "consul.localhost"
|
||||||
|
|
||||||
|
# Prefix for Consul catalog tags
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
prefix = "traefik"
|
||||||
```
|
```
|
||||||
|
|
||||||
This backend will create routes matching on hostname based on the service name
|
This backend will create routes matching on hostname based on the service name
|
||||||
used in consul.
|
used in consul.
|
||||||
|
|
||||||
|
Additional settings can be defined using Consul Catalog tags:
|
||||||
|
- ```traefik.enable=false```: disable this container in Træfɪk
|
||||||
|
- ```traefik.protocol=https```: override the default `http` protocol
|
||||||
|
- ```traefik.backend.weight=10```: assign this weight to the container
|
||||||
|
- ```traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5```
|
||||||
|
- ```traefik.backend.loadbalancer=drr```: override the default load balancing mode
|
||||||
|
- ```traefik.frontend.rule=Host:test.traefik.io```: override the default frontend rule (Default: `Host:{containerName}.{domain}`). See [frontends](#frontends).
|
||||||
|
- ```traefik.frontend.passHostHeader=true```: forward client `Host` header to the backend.
|
||||||
|
- ```traefik.frontend.entryPoints=http,https```: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
|
||||||
|
|
||||||
## Etcd backend
|
## Etcd backend
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ func (s *ConsulCatalogSuite) SetUpSuite(c *check.C) {
|
||||||
time.Sleep(2000 * time.Millisecond)
|
time.Sleep(2000 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConsulCatalogSuite) registerService(name string, address string, port int) error {
|
func (s *ConsulCatalogSuite) registerService(name string, address string, port int, tags []string) error {
|
||||||
catalog := s.consulClient.Catalog()
|
catalog := s.consulClient.Catalog()
|
||||||
_, err := catalog.Register(
|
_, err := catalog.Register(
|
||||||
&api.CatalogRegistration{
|
&api.CatalogRegistration{
|
||||||
|
@ -50,6 +50,7 @@ func (s *ConsulCatalogSuite) registerService(name string, address string, port i
|
||||||
Service: name,
|
Service: name,
|
||||||
Address: address,
|
Address: address,
|
||||||
Port: port,
|
Port: port,
|
||||||
|
Tags: tags,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&api.WriteOptions{},
|
&api.WriteOptions{},
|
||||||
|
@ -93,7 +94,7 @@ func (s *ConsulCatalogSuite) TestSingleService(c *check.C) {
|
||||||
|
|
||||||
nginx := s.composeProject.Container(c, "nginx")
|
nginx := s.composeProject.Container(c, "nginx")
|
||||||
|
|
||||||
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80)
|
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{})
|
||||||
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
|
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
|
||||||
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress)
|
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ import (
|
||||||
const (
|
const (
|
||||||
// DefaultWatchWaitTime is the duration to wait when polling consul
|
// DefaultWatchWaitTime is the duration to wait when polling consul
|
||||||
DefaultWatchWaitTime = 15 * time.Second
|
DefaultWatchWaitTime = 15 * time.Second
|
||||||
|
// DefaultConsulCatalogTagPrefix is a prefix for additional service/node configurations
|
||||||
|
DefaultConsulCatalogTagPrefix = "traefik"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConsulCatalog holds configurations of the Consul catalog provider.
|
// ConsulCatalog holds configurations of the Consul catalog provider.
|
||||||
|
@ -24,10 +26,16 @@ type ConsulCatalog struct {
|
||||||
Endpoint string
|
Endpoint string
|
||||||
Domain string
|
Domain string
|
||||||
client *api.Client
|
client *api.Client
|
||||||
|
Prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceUpdate struct {
|
||||||
|
ServiceName string
|
||||||
|
Attributes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type catalogUpdate struct {
|
type catalogUpdate struct {
|
||||||
Service string
|
Service *serviceUpdate
|
||||||
Nodes []*api.ServiceEntry
|
Nodes []*api.ServiceEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,41 +87,76 @@ func (provider *ConsulCatalog) healthyNodes(service string) (catalogUpdate, erro
|
||||||
return catalogUpdate{}, err
|
return catalogUpdate{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set := map[string]bool{}
|
||||||
|
tags := []string{}
|
||||||
|
for _, node := range data {
|
||||||
|
for _, tag := range node.Service.Tags {
|
||||||
|
if _, ok := set[tag]; ok == false {
|
||||||
|
set[tag] = true
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return catalogUpdate{
|
return catalogUpdate{
|
||||||
Service: service,
|
Service: &serviceUpdate{
|
||||||
Nodes: data,
|
ServiceName: service,
|
||||||
|
Attributes: tags,
|
||||||
|
},
|
||||||
|
Nodes: data,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (provider *ConsulCatalog) getEntryPoints(list string) []string {
|
||||||
|
return strings.Split(list, ",")
|
||||||
|
}
|
||||||
|
|
||||||
func (provider *ConsulCatalog) getBackend(node *api.ServiceEntry) string {
|
func (provider *ConsulCatalog) getBackend(node *api.ServiceEntry) string {
|
||||||
return strings.ToLower(node.Service.Service)
|
return strings.ToLower(node.Service.Service)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *ConsulCatalog) getFrontendValue(service string) string {
|
func (provider *ConsulCatalog) getFrontendRule(service serviceUpdate) string {
|
||||||
return "Host:" + service + "." + provider.Domain
|
customFrontendRule := provider.getAttribute("frontend.rule", service.Attributes, "")
|
||||||
|
if customFrontendRule != "" {
|
||||||
|
return customFrontendRule
|
||||||
|
}
|
||||||
|
return "Host:" + service.ServiceName + "." + provider.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *ConsulCatalog) getAttribute(name string, tags []string, defaultValue string) string {
|
||||||
|
for _, tag := range tags {
|
||||||
|
if strings.Index(tag, DefaultConsulCatalogTagPrefix+".") == 0 {
|
||||||
|
if kv := strings.SplitN(tag[len(DefaultConsulCatalogTagPrefix+"."):], "=", 2); len(kv) == 2 && kv[0] == name {
|
||||||
|
return kv[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *ConsulCatalog) buildConfig(catalog []catalogUpdate) *types.Configuration {
|
func (provider *ConsulCatalog) buildConfig(catalog []catalogUpdate) *types.Configuration {
|
||||||
var FuncMap = template.FuncMap{
|
var FuncMap = template.FuncMap{
|
||||||
"getBackend": provider.getBackend,
|
"getBackend": provider.getBackend,
|
||||||
"getFrontendValue": provider.getFrontendValue,
|
"getFrontendRule": provider.getFrontendRule,
|
||||||
"replace": replace,
|
"getAttribute": provider.getAttribute,
|
||||||
|
"getEntryPoints": provider.getEntryPoints,
|
||||||
|
"replace": replace,
|
||||||
}
|
}
|
||||||
|
|
||||||
allNodes := []*api.ServiceEntry{}
|
allNodes := []*api.ServiceEntry{}
|
||||||
serviceNames := []string{}
|
services := []*serviceUpdate{}
|
||||||
for _, info := range catalog {
|
for _, info := range catalog {
|
||||||
if len(info.Nodes) > 0 {
|
if len(info.Nodes) > 0 {
|
||||||
serviceNames = append(serviceNames, info.Service)
|
services = append(services, info.Service)
|
||||||
allNodes = append(allNodes, info.Nodes...)
|
allNodes = append(allNodes, info.Nodes...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templateObjects := struct {
|
templateObjects := struct {
|
||||||
Services []string
|
Services []*serviceUpdate
|
||||||
Nodes []*api.ServiceEntry
|
Nodes []*api.ServiceEntry
|
||||||
}{
|
}{
|
||||||
Services: serviceNames,
|
Services: services,
|
||||||
Nodes: allNodes,
|
Nodes: allNodes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,17 +14,68 @@ func TestConsulCatalogGetFrontendRule(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
services := []struct {
|
services := []struct {
|
||||||
service string
|
service serviceUpdate
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
service: "foo",
|
service: serviceUpdate{
|
||||||
|
ServiceName: "foo",
|
||||||
|
Attributes: []string{},
|
||||||
|
},
|
||||||
expected: "Host:foo.localhost",
|
expected: "Host:foo.localhost",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
service: serviceUpdate{
|
||||||
|
ServiceName: "foo",
|
||||||
|
Attributes: []string{
|
||||||
|
"traefik.frontend.rule=Host:*.example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "Host:*.example.com",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, e := range services {
|
for _, e := range services {
|
||||||
actual := provider.getFrontendValue(e.service)
|
actual := provider.getFrontendRule(e.service)
|
||||||
|
if actual != e.expected {
|
||||||
|
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsulCatalogGetAttribute(t *testing.T) {
|
||||||
|
provider := &ConsulCatalog{
|
||||||
|
Domain: "localhost",
|
||||||
|
}
|
||||||
|
|
||||||
|
services := []struct {
|
||||||
|
tags []string
|
||||||
|
key string
|
||||||
|
defaultValue string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
tags: []string{
|
||||||
|
"foo.bar=ramdom",
|
||||||
|
"traefik.backend.weight=42",
|
||||||
|
},
|
||||||
|
key: "backend.weight",
|
||||||
|
defaultValue: "",
|
||||||
|
expected: "42",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tags: []string{
|
||||||
|
"foo.bar=ramdom",
|
||||||
|
"traefik.backend.wei=42",
|
||||||
|
},
|
||||||
|
key: "backend.weight",
|
||||||
|
defaultValue: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range services {
|
||||||
|
actual := provider.getAttribute(e.key, e.tags, e.defaultValue)
|
||||||
if actual != e.expected {
|
if actual != e.expected {
|
||||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||||
}
|
}
|
||||||
|
@ -49,7 +100,10 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
|
||||||
{
|
{
|
||||||
nodes: []catalogUpdate{
|
nodes: []catalogUpdate{
|
||||||
{
|
{
|
||||||
Service: "test",
|
Service: &serviceUpdate{
|
||||||
|
ServiceName: "test",
|
||||||
|
Attributes: []string{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedFrontends: map[string]*types.Frontend{},
|
expectedFrontends: map[string]*types.Frontend{},
|
||||||
|
@ -58,13 +112,26 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
|
||||||
{
|
{
|
||||||
nodes: []catalogUpdate{
|
nodes: []catalogUpdate{
|
||||||
{
|
{
|
||||||
Service: "test",
|
Service: &serviceUpdate{
|
||||||
|
ServiceName: "test",
|
||||||
|
Attributes: []string{
|
||||||
|
"traefik.backend.loadbalancer=drr",
|
||||||
|
"traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5",
|
||||||
|
"random.foo=bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
Nodes: []*api.ServiceEntry{
|
Nodes: []*api.ServiceEntry{
|
||||||
{
|
{
|
||||||
Service: &api.AgentService{
|
Service: &api.AgentService{
|
||||||
Service: "test",
|
Service: "test",
|
||||||
Address: "127.0.0.1",
|
Address: "127.0.0.1",
|
||||||
Port: 80,
|
Port: 80,
|
||||||
|
Tags: []string{
|
||||||
|
"traefik.backend.weight=42",
|
||||||
|
"random.foo=bar",
|
||||||
|
"traefik.backend.passHostHeader=true",
|
||||||
|
"traefik.protocol=https",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Node: &api.Node{
|
Node: &api.Node{
|
||||||
Node: "localhost",
|
Node: "localhost",
|
||||||
|
@ -88,11 +155,16 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
|
||||||
"backend-test": {
|
"backend-test": {
|
||||||
Servers: map[string]types.Server{
|
Servers: map[string]types.Server{
|
||||||
"test--127-0-0-1--80": {
|
"test--127-0-0-1--80": {
|
||||||
URL: "http://127.0.0.1:80",
|
URL: "https://127.0.0.1:80",
|
||||||
|
Weight: 42,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CircuitBreaker: nil,
|
CircuitBreaker: &types.CircuitBreaker{
|
||||||
LoadBalancer: nil,
|
Expression: "NetworkErrorRatio() > 0.5",
|
||||||
|
},
|
||||||
|
LoadBalancer: &types.LoadBalancer{
|
||||||
|
Method: "drr",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,12 +1,40 @@
|
||||||
[backends]{{range .Nodes}}
|
[backends]
|
||||||
|
{{range .Nodes}}
|
||||||
|
{{if ne (getAttribute "enable" .Service.Tags "true") "false"}}
|
||||||
[backends.backend-{{getBackend .}}.servers.{{.Service.Service | replace "." "-"}}--{{.Service.Address | replace "." "-"}}--{{.Service.Port}}]
|
[backends.backend-{{getBackend .}}.servers.{{.Service.Service | replace "." "-"}}--{{.Service.Address | replace "." "-"}}--{{.Service.Port}}]
|
||||||
url = "http://{{.Service.Address}}:{{.Service.Port}}"
|
url = "{{getAttribute "protocol" .Service.Tags "http"}}://{{.Service.Address}}:{{.Service.Port}}"
|
||||||
|
{{$weight := getAttribute "backend.weight" .Service.Tags ""}}
|
||||||
|
{{with $weight}}
|
||||||
|
weight = {{$weight}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{range .Services}}
|
||||||
|
{{$service := .ServiceName}}
|
||||||
|
{{$circuitBreaker := getAttribute "backend.circuitbreaker" .Attributes ""}}
|
||||||
|
{{with $circuitBreaker}}
|
||||||
|
[backends.backend-{{$service}}.circuitbreaker]
|
||||||
|
expression = "{{$circuitBreaker}}"
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{$loadBalancer := getAttribute "backend.loadbalancer" .Attributes ""}}
|
||||||
|
{{with $loadBalancer}}
|
||||||
|
[backends.backend-{{$service}}.loadbalancer]
|
||||||
|
method = "{{$loadBalancer}}"
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
[frontends]{{range .Services}}
|
[frontends]{{range .Services}}
|
||||||
[frontends.frontend-{{.}}]
|
[frontends.frontend-{{.ServiceName}}]
|
||||||
backend = "backend-{{.}}"
|
backend = "backend-{{.ServiceName}}"
|
||||||
passHostHeader = false
|
passHostHeader = {{getAttribute "frontend.passHostHeader" .Attributes "false"}}
|
||||||
[frontends.frontend-{{.}}.routes.route-host-{{.}}]
|
{{$entryPoints := getAttribute "frontend.entrypoints" .Attributes ""}}
|
||||||
rule = "{{getFrontendValue .}}"
|
{{with $entryPoints}}
|
||||||
|
entrypoints = [{{range getEntryPoints $entryPoints}}
|
||||||
|
"{{.}}",
|
||||||
|
{{end}}]
|
||||||
|
{{end}}
|
||||||
|
[frontends.frontend-{{.ServiceName}}.routes.route-host-{{.ServiceName}}]
|
||||||
|
rule = "{{getFrontendRule .}}"
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in a new issue