Exposed by default feature in Consul Catalog
This commit is contained in:
parent
e0af17a17a
commit
f16219f90a
8 changed files with 241 additions and 24 deletions
|
@ -83,6 +83,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
|
||||||
// default CatalogProvider
|
// default CatalogProvider
|
||||||
var defaultConsulCatalog consul.CatalogProvider
|
var defaultConsulCatalog consul.CatalogProvider
|
||||||
defaultConsulCatalog.Endpoint = "127.0.0.1:8500"
|
defaultConsulCatalog.Endpoint = "127.0.0.1:8500"
|
||||||
|
defaultConsulCatalog.ExposedByDefault = true
|
||||||
defaultConsulCatalog.Constraints = types.Constraints{}
|
defaultConsulCatalog.Constraints = types.Constraints{}
|
||||||
defaultConsulCatalog.Prefix = "traefik"
|
defaultConsulCatalog.Prefix = "traefik"
|
||||||
defaultConsulCatalog.FrontEndRule = "Host:{{.ServiceName}}.{{.Domain}}"
|
defaultConsulCatalog.FrontEndRule = "Host:{{.ServiceName}}.{{.Domain}}"
|
||||||
|
|
|
@ -1481,6 +1481,13 @@ endpoint = "127.0.0.1:8500"
|
||||||
#
|
#
|
||||||
domain = "consul.localhost"
|
domain = "consul.localhost"
|
||||||
|
|
||||||
|
# Expose Consul catalog services by default in traefik
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: true
|
||||||
|
#
|
||||||
|
exposedByDefault = false
|
||||||
|
|
||||||
# Prefix for Consul catalog tags
|
# Prefix for Consul catalog tags
|
||||||
#
|
#
|
||||||
# Optional
|
# Optional
|
||||||
|
|
|
@ -117,3 +117,88 @@ func (s *ConsulCatalogSuite) TestSingleService(c *check.C) {
|
||||||
err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
|
err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
|
||||||
c.Assert(err, checker.IsNil)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -13,5 +13,5 @@ consul:
|
||||||
- "8302/udp"
|
- "8302/udp"
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
ports:
|
nginx2:
|
||||||
- "8881:80"
|
image: nginx:alpine
|
||||||
|
|
|
@ -31,6 +31,7 @@ type CatalogProvider struct {
|
||||||
provider.BaseProvider `mapstructure:",squash"`
|
provider.BaseProvider `mapstructure:",squash"`
|
||||||
Endpoint string `description:"Consul server endpoint"`
|
Endpoint string `description:"Consul server endpoint"`
|
||||||
Domain string `description:"Default domain used"`
|
Domain string `description:"Default domain used"`
|
||||||
|
ExposedByDefault bool `description:"Expose Consul services by default"`
|
||||||
Prefix string `description:"Prefix used for Consul catalog tags"`
|
Prefix string `description:"Prefix used for Consul catalog tags"`
|
||||||
FrontEndRule string `description:"Frontend rule used for Consul services"`
|
FrontEndRule string `description:"Frontend rule used for Consul services"`
|
||||||
client *api.Client
|
client *api.Client
|
||||||
|
@ -209,12 +210,7 @@ func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes := fun.Filter(func(node *api.ServiceEntry) bool {
|
nodes := fun.Filter(func(node *api.ServiceEntry) bool {
|
||||||
constraintTags := p.getConstraintTags(node.Service.Tags)
|
return p.nodeFilter(service, node)
|
||||||
ok, failingConstraint := p.MatchConstraints(constraintTags)
|
|
||||||
if !ok && failingConstraint != nil {
|
|
||||||
log.Debugf("Service %v pruned by '%v' constraint", service, failingConstraint.String())
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}, data).([]*api.ServiceEntry)
|
}, data).([]*api.ServiceEntry)
|
||||||
|
|
||||||
//Merge tags of nodes matching constraints, in a single slice.
|
//Merge tags of nodes matching constraints, in a single slice.
|
||||||
|
@ -234,6 +230,32 @@ func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) {
|
||||||
}, nil
|
}, 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 {
|
func (p *CatalogProvider) getPrefixedName(name string) string {
|
||||||
if len(p.Prefix) > 0 {
|
if len(p.Prefix) > 0 {
|
||||||
return p.Prefix + "." + name
|
return p.Prefix + "." + name
|
||||||
|
@ -364,14 +386,9 @@ func (p *CatalogProvider) buildConfig(catalog []catalogUpdate) *types.Configurat
|
||||||
allNodes := []*api.ServiceEntry{}
|
allNodes := []*api.ServiceEntry{}
|
||||||
services := []*serviceUpdate{}
|
services := []*serviceUpdate{}
|
||||||
for _, info := range catalog {
|
for _, info := range catalog {
|
||||||
for _, node := range info.Nodes {
|
if len(info.Nodes) > 0 {
|
||||||
isEnabled := p.getAttribute("enable", node.Service.Tags, "true")
|
|
||||||
if isEnabled != "false" && len(info.Nodes) > 0 {
|
|
||||||
services = append(services, info.Service)
|
services = append(services, info.Service)
|
||||||
allNodes = append(allNodes, info.Nodes...)
|
allNodes = append(allNodes, info.Nodes...)
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ensure a stable ordering of nodes so that identical configurations may be detected
|
// Ensure a stable ordering of nodes so that identical configurations may be detected
|
||||||
|
|
|
@ -311,6 +311,7 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
|
||||||
provider := &CatalogProvider{
|
provider := &CatalogProvider{
|
||||||
Domain: "localhost",
|
Domain: "localhost",
|
||||||
Prefix: "traefik",
|
Prefix: "traefik",
|
||||||
|
ExposedByDefault: false,
|
||||||
FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}",
|
FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}",
|
||||||
frontEndRuleTemplate: template.New("consul catalog frontend rule"),
|
frontEndRuleTemplate: template.New("consul catalog frontend rule"),
|
||||||
}
|
}
|
||||||
|
@ -330,7 +331,6 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
|
||||||
{
|
{
|
||||||
Service: &serviceUpdate{
|
Service: &serviceUpdate{
|
||||||
ServiceName: "test",
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
[backends]
|
[backends]
|
||||||
{{range $index, $node := .Nodes}}
|
{{range $index, $node := .Nodes}}
|
||||||
{{if ne (getAttribute "enable" $node.Service.Tags "true") "false"}}
|
|
||||||
[backends."backend-{{getBackend $node}}".servers."{{getBackendName $node $index}}"]
|
[backends."backend-{{getBackend $node}}".servers."{{getBackendName $node $index}}"]
|
||||||
url = "{{getAttribute "protocol" $node.Service.Tags "http"}}://{{getBackendAddress $node}}:{{$node.Service.Port}}"
|
url = "{{getAttribute "protocol" $node.Service.Tags "http"}}://{{getBackendAddress $node}}:{{$node.Service.Port}}"
|
||||||
{{$weight := getAttribute "backend.weight" $node.Service.Tags "0"}}
|
{{$weight := getAttribute "backend.weight" $node.Service.Tags "0"}}
|
||||||
{{with $weight}}
|
{{with $weight}}
|
||||||
weight = {{$weight}}
|
weight = {{$weight}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{range .Services}}
|
{{range .Services}}
|
||||||
|
|
|
@ -935,6 +935,12 @@
|
||||||
#
|
#
|
||||||
# domain = "consul.localhost"
|
# domain = "consul.localhost"
|
||||||
|
|
||||||
|
# Expose Consul catalog services by default in traefik
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
# exposedByDefault = true
|
||||||
|
|
||||||
# Prefix for Consul catalog tags
|
# Prefix for Consul catalog tags
|
||||||
#
|
#
|
||||||
# Optional
|
# Optional
|
||||||
|
|
Loading…
Reference in a new issue