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:
Vincent Demeester 2016-04-13 17:17:10 +02:00
commit 5c8d9f4eb9
6 changed files with 189 additions and 29 deletions

1
cmd.go
View file

@ -142,6 +142,7 @@ func init() {
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.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.Watch, "zookeeper.watch", true, "Watch provider")

View file

@ -675,11 +675,26 @@ endpoint = "127.0.0.1:8500"
# Optional
#
domain = "consul.localhost"
# Prefix for Consul catalog tags
#
# Optional
#
prefix = "traefik"
```
This backend will create routes matching on hostname based on the service name
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

View file

@ -39,7 +39,7 @@ func (s *ConsulCatalogSuite) SetUpSuite(c *check.C) {
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()
_, err := catalog.Register(
&api.CatalogRegistration{
@ -50,6 +50,7 @@ func (s *ConsulCatalogSuite) registerService(name string, address string, port i
Service: name,
Address: address,
Port: port,
Tags: tags,
},
},
&api.WriteOptions{},
@ -93,7 +94,7 @@ func (s *ConsulCatalogSuite) TestSingleService(c *check.C) {
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"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress)

View file

@ -16,6 +16,8 @@ import (
const (
// DefaultWatchWaitTime is the duration to wait when polling consul
DefaultWatchWaitTime = 15 * time.Second
// DefaultConsulCatalogTagPrefix is a prefix for additional service/node configurations
DefaultConsulCatalogTagPrefix = "traefik"
)
// ConsulCatalog holds configurations of the Consul catalog provider.
@ -24,10 +26,16 @@ type ConsulCatalog struct {
Endpoint string
Domain string
client *api.Client
Prefix string
}
type serviceUpdate struct {
ServiceName string
Attributes []string
}
type catalogUpdate struct {
Service string
Service *serviceUpdate
Nodes []*api.ServiceEntry
}
@ -79,41 +87,76 @@ func (provider *ConsulCatalog) healthyNodes(service string) (catalogUpdate, erro
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{
Service: service,
Nodes: data,
Service: &serviceUpdate{
ServiceName: service,
Attributes: tags,
},
Nodes: data,
}, nil
}
func (provider *ConsulCatalog) getEntryPoints(list string) []string {
return strings.Split(list, ",")
}
func (provider *ConsulCatalog) getBackend(node *api.ServiceEntry) string {
return strings.ToLower(node.Service.Service)
}
func (provider *ConsulCatalog) getFrontendValue(service string) string {
return "Host:" + service + "." + provider.Domain
func (provider *ConsulCatalog) getFrontendRule(service serviceUpdate) string {
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 {
var FuncMap = template.FuncMap{
"getBackend": provider.getBackend,
"getFrontendValue": provider.getFrontendValue,
"replace": replace,
"getBackend": provider.getBackend,
"getFrontendRule": provider.getFrontendRule,
"getAttribute": provider.getAttribute,
"getEntryPoints": provider.getEntryPoints,
"replace": replace,
}
allNodes := []*api.ServiceEntry{}
serviceNames := []string{}
services := []*serviceUpdate{}
for _, info := range catalog {
if len(info.Nodes) > 0 {
serviceNames = append(serviceNames, info.Service)
services = append(services, info.Service)
allNodes = append(allNodes, info.Nodes...)
}
}
templateObjects := struct {
Services []string
Services []*serviceUpdate
Nodes []*api.ServiceEntry
}{
Services: serviceNames,
Services: services,
Nodes: allNodes,
}

View file

@ -14,17 +14,68 @@ func TestConsulCatalogGetFrontendRule(t *testing.T) {
}
services := []struct {
service string
service serviceUpdate
expected string
}{
{
service: "foo",
service: serviceUpdate{
ServiceName: "foo",
Attributes: []string{},
},
expected: "Host:foo.localhost",
},
{
service: serviceUpdate{
ServiceName: "foo",
Attributes: []string{
"traefik.frontend.rule=Host:*.example.com",
},
},
expected: "Host:*.example.com",
},
}
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 {
t.Fatalf("expected %q, got %q", e.expected, actual)
}
@ -49,7 +100,10 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
{
nodes: []catalogUpdate{
{
Service: "test",
Service: &serviceUpdate{
ServiceName: "test",
Attributes: []string{},
},
},
},
expectedFrontends: map[string]*types.Frontend{},
@ -58,13 +112,26 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
{
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{
{
Service: &api.AgentService{
Service: "test",
Address: "127.0.0.1",
Port: 80,
Tags: []string{
"traefik.backend.weight=42",
"random.foo=bar",
"traefik.backend.passHostHeader=true",
"traefik.protocol=https",
},
},
Node: &api.Node{
Node: "localhost",
@ -88,11 +155,16 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
"backend-test": {
Servers: map[string]types.Server{
"test--127-0-0-1--80": {
URL: "http://127.0.0.1:80",
URL: "https://127.0.0.1:80",
Weight: 42,
},
},
CircuitBreaker: nil,
LoadBalancer: nil,
CircuitBreaker: &types.CircuitBreaker{
Expression: "NetworkErrorRatio() > 0.5",
},
LoadBalancer: &types.LoadBalancer{
Method: "drr",
},
},
},
},

View file

@ -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}}]
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}}
[frontends]{{range .Services}}
[frontends.frontend-{{.}}]
backend = "backend-{{.}}"
passHostHeader = false
[frontends.frontend-{{.}}.routes.route-host-{{.}}]
rule = "{{getFrontendValue .}}"
[frontends.frontend-{{.ServiceName}}]
backend = "backend-{{.ServiceName}}"
passHostHeader = {{getAttribute "frontend.passHostHeader" .Attributes "false"}}
{{$entryPoints := getAttribute "frontend.entrypoints" .Attributes ""}}
{{with $entryPoints}}
entrypoints = [{{range getEntryPoints $entryPoints}}
"{{.}}",
{{end}}]
{{end}}
[frontends.frontend-{{.ServiceName}}.routes.route-host-{{.ServiceName}}]
rule = "{{getFrontendRule .}}"
{{end}}