From f16219f90a6b950c82cdfa3d4cfe49b65c2be0a6 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 25 Aug 2017 17:32:03 +0200 Subject: [PATCH] Exposed by default feature in Consul Catalog --- cmd/traefik/configuration.go | 1 + docs/toml.md | 7 ++ integration/consul_catalog_test.go | 85 ++++++++++++++ .../resources/compose/consul_catalog.yml | 4 +- provider/consul/consul_catalog.go | 45 +++++--- provider/consul/consul_catalog_test.go | 105 +++++++++++++++++- templates/consul_catalog.tmpl | 12 +- traefik.sample.toml | 6 + 8 files changed, 241 insertions(+), 24 deletions(-) diff --git a/cmd/traefik/configuration.go b/cmd/traefik/configuration.go index bf668be63..5093a2e88 100644 --- a/cmd/traefik/configuration.go +++ b/cmd/traefik/configuration.go @@ -83,6 +83,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { // default CatalogProvider var defaultConsulCatalog consul.CatalogProvider defaultConsulCatalog.Endpoint = "127.0.0.1:8500" + defaultConsulCatalog.ExposedByDefault = true defaultConsulCatalog.Constraints = types.Constraints{} defaultConsulCatalog.Prefix = "traefik" defaultConsulCatalog.FrontEndRule = "Host:{{.ServiceName}}.{{.Domain}}" diff --git a/docs/toml.md b/docs/toml.md index 61266e9ad..859b45fc1 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -1481,6 +1481,13 @@ endpoint = "127.0.0.1:8500" # domain = "consul.localhost" +# Expose Consul catalog services by default in traefik +# +# Optional +# Default: true +# +exposedByDefault = false + # Prefix for Consul catalog tags # # Optional diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 04efbc513..16de26030 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -117,3 +117,88 @@ func (s *ConsulCatalogSuite) TestSingleService(c *check.C) { err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) c.Assert(err, checker.IsNil) } + +func (s *ConsulCatalogSuite) TestExposedByDefaultFalseSingleService(c *check.C) { + cmd, _ := s.cmdTraefik( + withConfigFile("fixtures/consul_catalog/simple.toml"), + "--consulCatalog", + "--consulCatalog.exposedByDefault=false", + "--consulCatalog.endpoint="+s.consulIP+":8500", + "--consulCatalog.domain=consul.localhost") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + nginx := s.composeProject.Container(c, "nginx") + + 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) + + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + + err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusNotFound), try.HasBody()) + c.Assert(err, checker.IsNil) +} + +func (s *ConsulCatalogSuite) TestExposedByDefaultFalseSimpleServiceMultipleNode(c *check.C) { + cmd, _ := s.cmdTraefik( + withConfigFile("fixtures/consul_catalog/simple.toml"), + "--consulCatalog", + "--consulCatalog.exposedByDefault=false", + "--consulCatalog.endpoint="+s.consulIP+":8500", + "--consulCatalog.domain=consul.localhost") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + nginx := s.composeProject.Container(c, "nginx") + nginx2 := s.composeProject.Container(c, "nginx2") + + 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) + + err = s.registerService("test", nginx2.NetworkSettings.IPAddress, 80, []string{"traefik.enable=true"}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx2.NetworkSettings.IPAddress) + + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + + err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) +} + +func (s *ConsulCatalogSuite) TestExposedByDefaultTrueSimpleServiceMultipleNode(c *check.C) { + cmd, _ := s.cmdTraefik( + withConfigFile("fixtures/consul_catalog/simple.toml"), + "--consulCatalog", + "--consulCatalog.exposedByDefault=true", + "--consulCatalog.endpoint="+s.consulIP+":8500", + "--consulCatalog.domain=consul.localhost") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + nginx := s.composeProject.Container(c, "nginx") + nginx2 := s.composeProject.Container(c, "nginx2") + + 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) + + err = s.registerService("test", nginx2.NetworkSettings.IPAddress, 80, []string{}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx2.NetworkSettings.IPAddress) + + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + + err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) +} diff --git a/integration/resources/compose/consul_catalog.yml b/integration/resources/compose/consul_catalog.yml index 1d5395281..54f874f2d 100644 --- a/integration/resources/compose/consul_catalog.yml +++ b/integration/resources/compose/consul_catalog.yml @@ -13,5 +13,5 @@ consul: - "8302/udp" nginx: image: nginx:alpine - ports: - - "8881:80" +nginx2: + image: nginx:alpine diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index 74c08b60e..e221c8e1a 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -31,6 +31,7 @@ type CatalogProvider struct { provider.BaseProvider `mapstructure:",squash"` Endpoint string `description:"Consul server endpoint"` Domain string `description:"Default domain used"` + ExposedByDefault bool `description:"Expose Consul services by default"` Prefix string `description:"Prefix used for Consul catalog tags"` FrontEndRule string `description:"Frontend rule used for Consul services"` client *api.Client @@ -209,12 +210,7 @@ func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) { } nodes := fun.Filter(func(node *api.ServiceEntry) bool { - constraintTags := p.getConstraintTags(node.Service.Tags) - ok, failingConstraint := p.MatchConstraints(constraintTags) - if !ok && failingConstraint != nil { - log.Debugf("Service %v pruned by '%v' constraint", service, failingConstraint.String()) - } - return ok + return p.nodeFilter(service, node) }, data).([]*api.ServiceEntry) //Merge tags of nodes matching constraints, in a single slice. @@ -234,6 +230,32 @@ func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) { }, nil } +func (p *CatalogProvider) nodeFilter(service string, node *api.ServiceEntry) bool { + // Filter disabled application. + if !p.isServiceEnabled(node) { + log.Debugf("Filtering disabled Consul service %s", service) + return false + } + + // Filter by constraints. + constraintTags := p.getConstraintTags(node.Service.Tags) + ok, failingConstraint := p.MatchConstraints(constraintTags) + if !ok && failingConstraint != nil { + log.Debugf("Service %v pruned by '%v' constraint", service, failingConstraint.String()) + return false + } + return true +} + +func (p *CatalogProvider) isServiceEnabled(node *api.ServiceEntry) bool { + enable, err := strconv.ParseBool(p.getAttribute("enable", node.Service.Tags, strconv.FormatBool(p.ExposedByDefault))) + if err != nil { + log.Debugf("Invalid value for enable, set to %b", p.ExposedByDefault) + return p.ExposedByDefault + } + return enable +} + func (p *CatalogProvider) getPrefixedName(name string) string { if len(p.Prefix) > 0 { return p.Prefix + "." + name @@ -364,14 +386,9 @@ func (p *CatalogProvider) buildConfig(catalog []catalogUpdate) *types.Configurat allNodes := []*api.ServiceEntry{} services := []*serviceUpdate{} for _, info := range catalog { - for _, node := range info.Nodes { - isEnabled := p.getAttribute("enable", node.Service.Tags, "true") - if isEnabled != "false" && len(info.Nodes) > 0 { - services = append(services, info.Service) - allNodes = append(allNodes, info.Nodes...) - break - } - + if len(info.Nodes) > 0 { + services = append(services, info.Service) + allNodes = append(allNodes, info.Nodes...) } } // Ensure a stable ordering of nodes so that identical configurations may be detected diff --git a/provider/consul/consul_catalog_test.go b/provider/consul/consul_catalog_test.go index e92269b6e..21eff493e 100644 --- a/provider/consul/consul_catalog_test.go +++ b/provider/consul/consul_catalog_test.go @@ -311,6 +311,7 @@ func TestConsulCatalogBuildConfig(t *testing.T) { provider := &CatalogProvider{ Domain: "localhost", Prefix: "traefik", + ExposedByDefault: false, FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", frontEndRuleTemplate: template.New("consul catalog frontend rule"), } @@ -330,7 +331,6 @@ func TestConsulCatalogBuildConfig(t *testing.T) { { Service: &serviceUpdate{ ServiceName: "test", - Attributes: []string{}, }, }, }, @@ -750,3 +750,106 @@ func TestConsulCatalogGetChangedKeys(t *testing.T) { } } } + +func TestConsulCatalogFilterEnabled(t *testing.T) { + cases := []struct { + desc string + exposedByDefault bool + node *api.ServiceEntry + expected bool + }{ + { + desc: "exposed", + exposedByDefault: true, + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{""}, + }, + }, + expected: true, + }, + { + desc: "exposed and tolerated by valid label value", + exposedByDefault: true, + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{"", "traefik.enable=true"}, + }, + }, + expected: true, + }, + { + desc: "exposed and tolerated by invalid label value", + exposedByDefault: true, + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{"", "traefik.enable=bad"}, + }, + }, + expected: true, + }, + { + desc: "exposed but overridden by label", + exposedByDefault: true, + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{"", "traefik.enable=false"}, + }, + }, + expected: false, + }, + { + desc: "non-exposed", + exposedByDefault: false, + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{""}, + }, + }, + expected: false, + }, + { + desc: "non-exposed but overridden by label", + exposedByDefault: false, + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{"", "traefik.enable=true"}, + }, + }, + expected: true, + }, + } + + for _, c := range cases { + c := c + t.Run(c.desc, func(t *testing.T) { + t.Parallel() + provider := &CatalogProvider{ + Domain: "localhost", + Prefix: "traefik", + ExposedByDefault: c.exposedByDefault, + } + if provider.nodeFilter("test", c.node) != c.expected { + t.Errorf("got unexpected filtering = %t", !c.expected) + } + }) + } +} diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index 1a2bd26f0..1787232f2 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -1,12 +1,10 @@ [backends] {{range $index, $node := .Nodes}} - {{if ne (getAttribute "enable" $node.Service.Tags "true") "false"}} - [backends."backend-{{getBackend $node}}".servers."{{getBackendName $node $index}}"] - url = "{{getAttribute "protocol" $node.Service.Tags "http"}}://{{getBackendAddress $node}}:{{$node.Service.Port}}" - {{$weight := getAttribute "backend.weight" $node.Service.Tags "0"}} - {{with $weight}} - weight = {{$weight}} - {{end}} + [backends."backend-{{getBackend $node}}".servers."{{getBackendName $node $index}}"] + url = "{{getAttribute "protocol" $node.Service.Tags "http"}}://{{getBackendAddress $node}}:{{$node.Service.Port}}" + {{$weight := getAttribute "backend.weight" $node.Service.Tags "0"}} + {{with $weight}} + weight = {{$weight}} {{end}} {{end}} diff --git a/traefik.sample.toml b/traefik.sample.toml index edabad95b..4a01f4f98 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -935,6 +935,12 @@ # # domain = "consul.localhost" +# Expose Consul catalog services by default in traefik +# +# Optional +# +# exposedByDefault = true + # Prefix for Consul catalog tags # # Optional