ConsulCatalog StrictChecks

This commit is contained in:
DJ Enriquez 2024-02-27 12:30:04 -08:00 committed by GitHub
parent c5808af4d9
commit 0e89a6bec7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 521 additions and 12 deletions

View file

@ -714,6 +714,32 @@ providers:
# ...
```
### `strictChecks`
_Optional, Default="passing,warning"_
Define which [Consul Service health checks](https://developer.hashicorp.com/consul/docs/services/usage/checks#define-initial-health-check-status) are allowed to take on traffic.
```yaml tab="File (YAML)"
providers:
consulCatalog:
strictChecks:
- "passing"
- "warning"
# ...
```
```toml tab="File (TOML)"
[providers.consulCatalog]
strictChecks = ["passing", "warning"]
# ...
```
```bash tab="CLI"
--providers.consulcatalog.strictChecks=passing,warning
# ...
```
### `watch`
_Optional, Default=false_

View file

@ -537,6 +537,9 @@ Name of the Traefik service in Consul Catalog (needs to be registered via the or
`--providers.consulcatalog.stale`:
Use stale consistency for catalog reads. (Default: ```false```)
`--providers.consulcatalog.strictchecks`:
A list of service health statuses to allow taking traffic. (Default: ```passing, warning```)
`--providers.consulcatalog.watch`:
Watch Consul API events. (Default: ```false```)

View file

@ -513,6 +513,9 @@ Name of the Traefik service in Consul Catalog (needs to be registered via the or
`TRAEFIK_PROVIDERS_CONSULCATALOG_STALE`:
Use stale consistency for catalog reads. (Default: ```false```)
`TRAEFIK_PROVIDERS_CONSULCATALOG_STRICTCHECKS`:
A list of service health statuses to allow taking traffic. (Default: ```passing, warning```)
`TRAEFIK_PROVIDERS_CONSULCATALOG_WATCH`:
Watch Consul API events. (Default: ```false```)

View file

@ -161,6 +161,7 @@
connectByDefault = true
serviceName = "foobar"
watch = true
strictChecks = ["foobar", "foobar"]
namespaces = ["foobar", "foobar"]
[providers.consulCatalog.endpoint]
address = "foobar"

View file

@ -192,6 +192,9 @@ providers:
connectByDefault: true
serviceName: foobar
watch: true
strictChecks:
- foobar
- foobar
namespaces:
- foobar
- foobar

View file

@ -132,8 +132,8 @@ func (p *Provider) keepContainer(ctx context.Context, item itemData) bool {
return false
}
if item.Status != api.HealthPassing && item.Status != api.HealthWarning {
logger.Debug().Msg("Filtering unhealthy or starting item")
if !p.includesHealthStatus(item.Status) {
logger.Debug().Msgf("Status %q is not included in the configured strictChecks of %q", item.Status, strings.Join(p.StrictChecks, ","))
return false
}
@ -324,3 +324,8 @@ func getName(i itemData) string {
hasher.Write([]byte(strings.Join(tags, "")))
return provider.Normalize(fmt.Sprintf("%s-%d", i.Name, hasher.Sum64()))
}
// defaultStrictChecks returns the default healthchecks to allow an upstream to be registered a route for loadbalancers.
func defaultStrictChecks() []string {
return []string{api.HealthPassing, api.HealthWarning}
}

View file

@ -287,11 +287,13 @@ func TestDefaultRule(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var config Configuration
config.SetDefaults()
config.DefaultRule = test.defaultRule
p := Provider{
Configuration: Configuration{
ExposedByDefault: true,
DefaultRule: test.defaultRule,
},
Configuration: config,
}
err := p.Init()
@ -3125,13 +3127,15 @@ func Test_buildConfiguration(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var config Configuration
config.SetDefaults()
config.DefaultRule = "Host(`{{ normalize .Name }}.traefik.wtf`)"
config.ConnectAware = test.ConnectAware
config.Constraints = test.constraints
p := Provider{
Configuration: Configuration{
ExposedByDefault: true,
DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)",
ConnectAware: test.ConnectAware,
Constraints: test.constraints,
},
Configuration: config,
}
err := p.Init()
@ -3206,3 +3210,449 @@ func extractNSFromProvider(providers []*Provider) []string {
}
return res
}
func TestFilterHealthStatuses(t *testing.T) {
testCases := []struct {
desc string
items []itemData
strictChecks []string
expected *dynamic.Configuration
}{
{
// No value passed in here, we assume the default of ["passing", "warning"]
desc: "test default strict checks",
strictChecks: defaultStrictChecks(),
items: []itemData{
{
ID: "id",
Node: "Node1",
Name: "Test1",
Address: "127.0.0.1",
Port: "80",
Labels: nil,
Status: api.HealthPassing,
},
{
ID: "id",
Node: "Node2",
Name: "Test2",
Address: "127.0.0.1",
Port: "81",
Labels: nil,
Status: api.HealthWarning,
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"Test1": {
Service: "Test1",
Rule: "Host(`foo.bar`)",
DefaultRule: true,
},
"Test2": {
Service: "Test2",
Rule: "Host(`foo.bar`)",
DefaultRule: true,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"Test1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:80",
},
},
PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
"Test2": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:81",
},
},
PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
{
// The item's health status is not included in the default checks, do not expect any containers
desc: "test status not included",
strictChecks: defaultStrictChecks(),
items: []itemData{
{
ID: "id",
Node: "Node1",
Name: "Test",
Address: "127.0.0.1",
Port: "80",
Labels: nil,
Status: api.HealthCritical,
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
{
// Allow only "warning" status containers to be included
desc: "test only include warning",
strictChecks: []string{api.HealthWarning},
items: []itemData{
{
ID: "id",
Node: "Node1",
Name: "Test1",
Address: "127.0.0.1",
Port: "80",
Labels: nil,
Status: api.HealthPassing,
},
{
ID: "id2",
Node: "Node2",
Name: "Test2",
Address: "127.0.0.1",
Port: "81",
Labels: nil,
Status: api.HealthWarning,
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"Test2": {
Service: "Test2",
Rule: "Host(`foo.bar`)",
DefaultRule: true,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"Test2": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:81",
},
},
PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
{
// Reject "critical" health status
desc: "test critical status not included",
strictChecks: defaultStrictChecks(),
items: []itemData{
{
ID: "id",
Node: "Node1",
Name: "Test1",
Address: "127.0.0.1",
Port: "80",
Labels: nil,
Status: api.HealthPassing,
},
{
ID: "id2",
Node: "Node2",
Name: "Test2",
Address: "127.0.0.1",
Port: "81",
Labels: nil,
Status: api.HealthWarning,
},
{
ID: "id3",
Node: "Node3",
Name: "Test3",
Address: "127.0.0.1",
Port: "82",
Labels: nil,
Status: api.HealthCritical,
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"Test1": {
Service: "Test1",
Rule: "Host(`foo.bar`)",
DefaultRule: true,
},
"Test2": {
Service: "Test2",
Rule: "Host(`foo.bar`)",
DefaultRule: true,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"Test1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:80",
},
},
PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
"Test2": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:81",
},
},
PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
{
// The "any" health status allows for all status types, including ones not yet directly included in Consul
desc: "test include 'any' health status",
strictChecks: []string{api.HealthAny},
items: []itemData{
{
ID: "id",
Node: "Node1",
Name: "Test1",
Address: "127.0.0.1",
Port: "80",
Labels: nil,
Status: api.HealthPassing,
},
{
ID: "id2",
Node: "Node2",
Name: "Test2",
Address: "127.0.0.1",
Port: "81",
Labels: nil,
Status: api.HealthWarning,
},
{
ID: "id3",
Node: "Node3",
Name: "Test3",
Address: "127.0.0.1",
Port: "82",
Labels: nil,
Status: api.HealthCritical,
},
{
ID: "id4",
Node: "Node4",
Name: "Test4",
Address: "127.0.0.1",
Port: "83",
Labels: nil,
Status: "some unsupported status",
},
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"Test1": {
Service: "Test1",
Rule: "Host(`foo.bar`)",
DefaultRule: true,
},
"Test2": {
Service: "Test2",
Rule: "Host(`foo.bar`)",
DefaultRule: true,
},
"Test3": {
Service: "Test3",
Rule: "Host(`foo.bar`)",
DefaultRule: true,
},
"Test4": {
Service: "Test4",
Rule: "Host(`foo.bar`)",
DefaultRule: true,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"Test1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:80",
},
},
PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
"Test2": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:81",
},
},
PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
"Test3": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:82",
},
},
PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
"Test4": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:83",
},
},
PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var config Configuration
config.SetDefaults()
config.DefaultRule = "Host(`foo.bar`)"
if test.strictChecks != nil {
config.StrictChecks = test.strictChecks
}
p := Provider{
Configuration: config,
}
err := p.Init()
require.NoError(t, err)
for i := 0; i < len(test.items); i++ {
var err error
test.items[i].ExtraConf, err = p.getExtraConf(test.items[i].Labels)
require.NoError(t, err)
}
configuration := p.buildConfiguration(context.Background(), test.items, nil)
assert.Equal(t, test.expected, configuration)
})
}
}

View file

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"strings"
"text/template"
"time"
@ -88,6 +89,7 @@ type Configuration struct {
ConnectByDefault bool `description:"Consider every service as Connect capable by default." json:"connectByDefault,omitempty" toml:"connectByDefault,omitempty" yaml:"connectByDefault,omitempty" export:"true"`
ServiceName string `description:"Name of the Traefik service in Consul Catalog (needs to be registered via the orchestrator or manually)." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"`
Watch bool `description:"Watch Consul API events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"`
StrictChecks []string `description:"A list of service health statuses to allow taking traffic." json:"strictChecks,omitempty" toml:"strictChecks,omitempty" yaml:"strictChecks,omitempty" export:"true"`
}
// SetDefaults sets the default values.
@ -98,6 +100,7 @@ func (c *Configuration) SetDefaults() {
c.ExposedByDefault = true
c.DefaultRule = defaultTemplateRule
c.ServiceName = "traefik"
c.StrictChecks = defaultStrictChecks()
}
// Provider is the Consul Catalog provider implementation.
@ -578,6 +581,21 @@ func (p *Provider) watchConnectTLS(ctx context.Context) error {
}
}
// includesHealthStatus returns true if the status passed in exists in the configured StrictChecks configuration. Statuses are case insensitive.
func (p *Provider) includesHealthStatus(status string) bool {
for _, s := range p.StrictChecks {
// If the "any" status is included, assume all health checks are included
if strings.EqualFold(s, api.HealthAny) {
return true
}
if strings.EqualFold(s, status) {
return true
}
}
return false
}
func createClient(namespace string, endpoint *EndpointConfig) (*api.Client, error) {
config := api.Config{
Address: endpoint.Address,