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().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")
|
||||
|
|
15
docs/toml.md
15
docs/toml.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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}}
|
||||
|
|
Loading…
Reference in a new issue