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().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")

View file

@ -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

View file

@ -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)

View file

@ -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,
} }

View file

@ -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",
},
}, },
}, },
}, },

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}}] [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}}