diff --git a/cmd.go b/cmd.go index 3cbab053a..e4ea1a4b0 100644 --- a/cmd.go +++ b/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") diff --git a/docs/toml.md b/docs/toml.md index 2dd786bfc..792624f64 100644 --- a/docs/toml.md +++ b/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 diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 34700ffa6..333596da2 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -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) diff --git a/provider/consul_catalog.go b/provider/consul_catalog.go index 66dd9926e..23c5fe6dd 100644 --- a/provider/consul_catalog.go +++ b/provider/consul_catalog.go @@ -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, } diff --git a/provider/consul_catalog_test.go b/provider/consul_catalog_test.go index 413a35cec..4f967ae60 100644 --- a/provider/consul_catalog_test.go +++ b/provider/consul_catalog_test.go @@ -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", + }, }, }, }, diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index d7c5adbc7..a6fe4dad3 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -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}} diff --git a/templates/kv.tmpl b/templates/kv.tmpl index 97793784f..168ba0e5c 100644 --- a/templates/kv.tmpl +++ b/templates/kv.tmpl @@ -27,7 +27,7 @@ [frontends]{{range $frontends}} {{$frontend := Last .}} {{$entryPoints := SplitGet . "/entrypoints"}} - [frontends.{{$frontend}}] + [frontends."{{$frontend}}"] backend = "{{Get "" . "/backend"}}" passHostHeader = {{Get "false" . "/passHostHeader"}} entryPoints = [{{range $entryPoints}} @@ -35,7 +35,7 @@ {{end}}] {{$routes := List . "/routes/"}} {{range $routes}} - [frontends.{{$frontend}}.routes.{{Last .}}] + [frontends."{{$frontend}}".routes."{{Last .}}"] rule = "{{Get "" . "/rule"}}" {{end}} {{end}}