From 0d7d5a0318c29abc88e63cf4333f4234c0de7ad8 Mon Sep 17 00:00:00 2001 From: Sylvain Rabot Date: Mon, 28 Mar 2022 17:08:09 +0200 Subject: [PATCH 1/9] Upgrade quic-go to v0.26.0 --- go.mod | 2 +- go.sum | 17 ++++++++--------- pkg/config/static/entrypoints.go | 2 +- pkg/server/server_entrypoint_tcp_http3.go | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index b55373535..e8a4c440e 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/instana/go-sensor v1.38.3 github.com/klauspost/compress v1.13.0 github.com/kvtools/valkeyrie v0.4.0 - github.com/lucas-clemente/quic-go v0.25.0 + github.com/lucas-clemente/quic-go v0.26.0 github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f github.com/miekg/dns v1.1.45 github.com/mitchellh/copystructure v1.0.0 diff --git a/go.sum b/go.sum index 46160fe25..e1b0a8327 100644 --- a/go.sum +++ b/go.sum @@ -1124,8 +1124,8 @@ github.com/liquidweb/liquidweb-go v1.6.3/go.mod h1:SuXXp+thr28LnjEw18AYtWwIbWMHS github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/looplab/fsm v0.1.0 h1:Qte7Zdn/5hBNbXzP7yxVU4OIFHWXBovyTT2LaBTyC20= github.com/looplab/fsm v0.1.0/go.mod h1:m2VaOfDHxqXBBMgc26m6yUOwkFn8H2AlJDE+jd/uafI= -github.com/lucas-clemente/quic-go v0.25.0 h1:K+X9Gvd7JXsOHtU0N2icZ2Nw3rx82uBej3mP4CLgibc= -github.com/lucas-clemente/quic-go v0.25.0/go.mod h1:YtzP8bxRVCBlO77yRanE264+fY/T2U9ZlW1AaHOsMOg= +github.com/lucas-clemente/quic-go v0.26.0 h1:ALBQXr9UJ8A1LyzvceX4jd9QFsHvlI0RR6BkV16o00A= +github.com/lucas-clemente/quic-go v0.26.0/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -1146,13 +1146,12 @@ github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= -github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= -github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco= -github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= -github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk= -github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8= -github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1 h1:EnzzN9fPUkUck/1CuY1FlzBaIYMoiBsdwTNmNGkwUUM= -github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1/go.mod h1:PUhIQk19LoFt2174H4+an8TYvWOGjb/hHwphBeaDHwI= +github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ= +github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= +github.com/marten-seemann/qtls-go1-17 v0.1.1 h1:DQjHPq+aOzUeh9/lixAGunn6rIOQyWChPSI4+hgW7jc= +github.com/marten-seemann/qtls-go1-17 v0.1.1/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s= +github.com/marten-seemann/qtls-go1-18 v0.1.1 h1:qp7p7XXUFL7fpBvSS1sWD+uSqPvzNQK43DH+/qEkj0Y= +github.com/marten-seemann/qtls-go1-18 v0.1.1/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index 224776e29..3e027ffb0 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -61,7 +61,7 @@ type HTTPConfig struct { // HTTP3Config is the HTTP3 configuration of an entry point. type HTTP3Config struct { - AdvertisedPort int32 `description:"UDP port to advertise, on which HTTP/3 is available." json:"advertisedPort,omitempty" toml:"advertisedPort,omitempty" yaml:"advertisedPort,omitempty" export:"true"` + AdvertisedPort int `description:"UDP port to advertise, on which HTTP/3 is available." json:"advertisedPort,omitempty" toml:"advertisedPort,omitempty" yaml:"advertisedPort,omitempty" export:"true"` } // Redirections is a set of redirection for an entry point. diff --git a/pkg/server/server_entrypoint_tcp_http3.go b/pkg/server/server_entrypoint_tcp_http3.go index 2c40a0f2f..1a1c694b7 100644 --- a/pkg/server/server_entrypoint_tcp_http3.go +++ b/pkg/server/server_entrypoint_tcp_http3.go @@ -47,7 +47,7 @@ func newHTTP3Server(ctx context.Context, configuration *static.EntryPoint, https } h3.Server = &http3.Server{ - Port: uint32(configuration.HTTP3.AdvertisedPort), + Port: configuration.HTTP3.AdvertisedPort, Server: &http.Server{ Addr: configuration.GetAddress(), Handler: httpsServer.Server.(*http.Server).Handler, From 8c56d1a3388bb1ce52e3fd6a76ecedde8e78de8b Mon Sep 17 00:00:00 2001 From: Tom Moulard Date: Mon, 4 Apr 2022 11:46:07 +0200 Subject: [PATCH 2/9] Allow HTTP/2 max concurrent stream configuration --- .../reference/static-configuration/cli-ref.md | 5 ++- .../reference/static-configuration/env-ref.md | 5 ++- .../reference/static-configuration/file.toml | 2 ++ .../reference/static-configuration/file.yaml | 2 ++ docs/content/routing/entrypoints.md | 31 +++++++++++++++++++ pkg/config/static/entrypoints.go | 15 ++++++++- pkg/server/server_entrypoint_tcp.go | 24 +++++++++++++- .../server_entrypoint_tcp_http3_test.go | 1 + pkg/server/server_entrypoint_tcp_test.go | 3 ++ 9 files changed, 84 insertions(+), 4 deletions(-) diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 9345cded4..84296d2cc 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -147,8 +147,11 @@ Subject alternative names. `--entrypoints..http.tls.options`: Default TLS options for the routers linked to the entry point. +`--entrypoints..http2.maxconcurrentstreams`: +Specifies the number of concurrent streams per connection that each client is allowed to initiate. (Default: ```250```) + `--entrypoints..http3`: -HTTP3 configuration. (Default: ```false```) +HTTP/3 configuration. (Default: ```false```) `--entrypoints..http3.advertisedport`: UDP port to advertise, on which HTTP/3 is available. (Default: ```0```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 340917400..bd3d96f98 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -114,8 +114,11 @@ Trust only forwarded headers from selected IPs. `TRAEFIK_ENTRYPOINTS__HTTP`: HTTP configuration. +`TRAEFIK_ENTRYPOINTS__HTTP2_MAXCONCURRENTSTREAMS`: +Specifies the number of concurrent streams per connection that each client is allowed to initiate. (Default: ```250```) + `TRAEFIK_ENTRYPOINTS__HTTP3`: -HTTP3 configuration. (Default: ```false```) +HTTP/3 configuration. (Default: ```false```) `TRAEFIK_ENTRYPOINTS__HTTP3_ADVERTISEDPORT`: UDP port to advertise, on which HTTP/3 is available. (Default: ```0```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index f84a975d6..96a70ee68 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -30,6 +30,8 @@ trustedIPs = ["foobar", "foobar"] [entryPoints.EntryPoint0.udp] timeout = 42 + [entryPoints.EntryPoint0.http2] + maxConcurrentStreams = 42 [entryPoints.EntryPoint0.http3] advertisedPort = 42 [entryPoints.EntryPoint0.http] diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 260734fd0..a8d84e93e 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -32,6 +32,8 @@ entryPoints: trustedIPs: - foobar - foobar + http2: + maxConcurrentStreams: 42 http3: advertisedPort: 42 udp: diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 2aa6b6faf..3913c4fb3 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -100,6 +100,8 @@ They can be defined by using a file (YAML or TOML) or CLI arguments. entryPoints: name: address: ":8888" # same as ":8888/tcp" + http2: + maxConcurrentStreams: 42 http3: advertisedPort: 8888 transport: @@ -127,6 +129,8 @@ They can be defined by using a file (YAML or TOML) or CLI arguments. [entryPoints] [entryPoints.name] address = ":8888" # same as ":8888/tcp" + [entryPoints.name.http2] + maxConcurrentStreams = 42 [entryPoints.name.http3] advertisedPort = 8888 [entryPoints.name.transport] @@ -148,6 +152,7 @@ They can be defined by using a file (YAML or TOML) or CLI arguments. ```bash tab="CLI" ## Static configuration --entryPoints.name.address=:8888 # same as :8888/tcp + --entryPoints.name.http2.maxConcurrentStreams=42 --entryPoints.name.http3.advertisedport=8888 --entryPoints.name.transport.lifeCycle.requestAcceptGraceTimeout=42 --entryPoints.name.transport.lifeCycle.graceTimeOut=42 @@ -223,6 +228,32 @@ If both TCP and UDP are wanted for the same port, two entryPoints definitions ar Full details for how to specify `address` can be found in [net.Listen](https://golang.org/pkg/net/#Listen) (and [net.Dial](https://golang.org/pkg/net/#Dial)) of the doc for go. +### HTTP/2 + +#### `maxConcurrentStreams` + +_Optional, Default=250_ + +`maxConcurrentStreams` specifies the number of concurrent streams per connection that each client is allowed to initiate. +The `maxConcurrentStreams` value must be greater than zero. + +```yaml tab="File (YAML)" +entryPoints: + foo: + http2: + maxConcurrentStreams: 250 +``` + +```toml tab="File (TOML)" +[entryPoints.foo] + [entryPoints.foo.http2] + maxConcurrentStreams = 250 +``` + +```bash tab="CLI" +--entryPoints.name.http2.maxConcurrentStreams=250 +``` + ### HTTP/3 #### `http3` diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index 3e027ffb0..f97d78e22 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -16,7 +16,8 @@ type EntryPoint struct { ProxyProtocol *ProxyProtocol `description:"Proxy-Protocol configuration." json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` ForwardedHeaders *ForwardedHeaders `description:"Trust client forwarding headers." json:"forwardedHeaders,omitempty" toml:"forwardedHeaders,omitempty" yaml:"forwardedHeaders,omitempty" export:"true"` HTTP HTTPConfig `description:"HTTP configuration." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" export:"true"` - HTTP3 *HTTP3Config `description:"HTTP3 configuration." json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + HTTP2 *HTTP2Config `description:"HTTP/2 configuration." json:"http2,omitempty" toml:"http2,omitempty" yaml:"http2,omitempty" export:"true"` + HTTP3 *HTTP3Config `description:"HTTP/3 configuration." json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` UDP *UDPConfig `description:"UDP configuration." json:"udp,omitempty" toml:"udp,omitempty" yaml:"udp,omitempty"` } @@ -50,6 +51,8 @@ func (ep *EntryPoint) SetDefaults() { ep.ForwardedHeaders = &ForwardedHeaders{} ep.UDP = &UDPConfig{} ep.UDP.SetDefaults() + ep.HTTP2 = &HTTP2Config{} + ep.HTTP2.SetDefaults() } // HTTPConfig is the HTTP configuration of an entry point. @@ -59,6 +62,16 @@ type HTTPConfig struct { TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` } +// HTTP2Config is the HTTP2 configuration of an entry point. +type HTTP2Config struct { + MaxConcurrentStreams int32 `description:"Specifies the number of concurrent streams per connection that each client is allowed to initiate." json:"maxConcurrentStreams,omitempty" toml:"maxConcurrentStreams,omitempty" yaml:"maxConcurrentStreams,omitempty" export:"true"` +} + +// SetDefaults sets the default values. +func (c *HTTP2Config) SetDefaults() { + c.MaxConcurrentStreams = 250 // https://cs.opensource.google/go/x/net/+/cd36cc07:http2/server.go;l=58 +} + // HTTP3Config is the HTTP3 configuration of an entry point. type HTTP3Config struct { AdvertisedPort int `description:"UDP port to advertise, on which HTTP/3 is available." json:"advertisedPort,omitempty" toml:"advertisedPort,omitempty" yaml:"advertisedPort,omitempty" export:"true"` diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 41bd85fc1..0d45dc564 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -7,6 +7,8 @@ import ( stdlog "log" "net" "net/http" + "os" + "strings" "sync" "syscall" "time" @@ -507,6 +509,10 @@ type httpServer struct { } func createHTTPServer(ctx context.Context, ln net.Listener, configuration *static.EntryPoint, withH2c bool, reqDecorator *requestdecorator.RequestDecorator) (*httpServer, error) { + if configuration.HTTP2.MaxConcurrentStreams < 0 { + return nil, errors.New("max concurrent streams value must be greater than or equal to zero") + } + httpSwitcher := middlewares.NewHandlerSwitcher(router.BuildDefaultHTTPRouter()) next, err := alice.New(requestdecorator.WrapHandler(reqDecorator)).Then(httpSwitcher) @@ -524,7 +530,9 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati } if withH2c { - handler = h2c.NewHandler(handler, &http2.Server{}) + handler = h2c.NewHandler(handler, &http2.Server{ + MaxConcurrentStreams: uint32(configuration.HTTP2.MaxConcurrentStreams), + }) } serverHTTP := &http.Server{ @@ -535,6 +543,20 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati IdleTimeout: time.Duration(configuration.Transport.RespondingTimeouts.IdleTimeout), } + // ConfigureServer configures HTTP/2 with the MaxConcurrentStreams option for the given server. + // Also keeping behavior the same as + // https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/net/http/server.go;l=3262 + if !strings.Contains(os.Getenv("GODEBUG"), "http2server=0") { + err = http2.ConfigureServer(serverHTTP, &http2.Server{ + MaxConcurrentStreams: uint32(configuration.HTTP2.MaxConcurrentStreams), + NewWriteScheduler: func() http2.WriteScheduler { return http2.NewPriorityWriteScheduler(nil) }, + }) + + if err != nil { + return nil, fmt.Errorf("configure HTTP/2 server: %w", err) + } + } + listener := newHTTPForwarder(ln) go func() { err := serverHTTP.Serve(listener) diff --git a/pkg/server/server_entrypoint_tcp_http3_test.go b/pkg/server/server_entrypoint_tcp_http3_test.go index bf863653a..6544dd2e0 100644 --- a/pkg/server/server_entrypoint_tcp_http3_test.go +++ b/pkg/server/server_entrypoint_tcp_http3_test.go @@ -88,6 +88,7 @@ func TestHTTP3AdvertisedPort(t *testing.T) { Address: "127.0.0.1:8090", Transport: epConfig, ForwardedHeaders: &static.ForwardedHeaders{}, + HTTP2: &static.HTTP2Config{}, HTTP3: &static.HTTP3Config{ AdvertisedPort: 8080, }, diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index 1477629a7..73cc9c28b 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -83,6 +83,7 @@ func testShutdown(t *testing.T, router *tcprouter.Router) { Address: "127.0.0.1:0", Transport: epConfig, ForwardedHeaders: &static.ForwardedHeaders{}, + HTTP2: &static.HTTP2Config{}, }, nil) require.NoError(t, err) @@ -166,6 +167,7 @@ func TestReadTimeoutWithoutFirstByte(t *testing.T) { Address: ":0", Transport: epConfig, ForwardedHeaders: &static.ForwardedHeaders{}, + HTTP2: &static.HTTP2Config{}, }, nil) require.NoError(t, err) @@ -202,6 +204,7 @@ func TestReadTimeoutWithFirstByte(t *testing.T) { Address: ":0", Transport: epConfig, ForwardedHeaders: &static.ForwardedHeaders{}, + HTTP2: &static.HTTP2Config{}, }, nil) require.NoError(t, err) From 71150bcaaf8d2361f6e5954744b327f0f5d065d8 Mon Sep 17 00:00:00 2001 From: Adrian Lai Date: Tue, 5 Apr 2022 11:30:08 +0100 Subject: [PATCH 3/9] Allow config of additonal CircuitBreaker params --- .../middlewares/http/circuitbreaker.md | 15 ++++--- .../dynamic-configuration/docker-labels.yml | 3 ++ .../reference/dynamic-configuration/file.toml | 3 ++ .../reference/dynamic-configuration/file.yaml | 3 ++ .../reference/dynamic-configuration/kv-ref.md | 3 ++ .../marathon-labels.json | 3 ++ .../traefik.containo.us_middlewares.yaml | 24 ++++++++++ integration/fixtures/k8s/01-traefik-crd.yml | 24 ++++++++++ pkg/config/dynamic/middlewares.go | 14 ++++++ pkg/config/label/label_test.go | 16 ++++++- .../circuitbreaker/circuit_breaker.go | 44 ++++++++++++------- pkg/provider/kubernetes/crd/kubernetes.go | 37 +++++++++++++++- .../crd/traefik/v1alpha1/middleware.go | 16 ++++++- .../traefik/v1alpha1/zz_generated.deepcopy.go | 35 ++++++++++++++- pkg/provider/kv/kv_test.go | 8 +++- 15 files changed, 218 insertions(+), 30 deletions(-) diff --git a/docs/content/middlewares/http/circuitbreaker.md b/docs/content/middlewares/http/circuitbreaker.md index daa3c5a0c..f1571b870 100644 --- a/docs/content/middlewares/http/circuitbreaker.md +++ b/docs/content/middlewares/http/circuitbreaker.md @@ -171,15 +171,18 @@ This behavior cannot be configured. ### `CheckPeriod` -The interval used to evaluate `expression` and decide if the state of the circuit breaker must change. -By default, `CheckPeriod` is 100ms. This value cannot be configured. +_Optional, Default="100ms"_ + +The interval between successive checks of the circuit breaker condition (when in standby state). ### `FallbackDuration` -By default, `FallbackDuration` is 10 seconds. This value cannot be configured. +_Optional, Default="10s"_ -### `RecoveringDuration` +The duration for which the circuit breaker will wait before trying to recover (from a tripped state). -The duration of the recovering mode (recovering state). +### `RecoveryDuration` -By default, `RecoveringDuration` is 10 seconds. This value cannot be configured. +_Optional, Default="10s"_ + +The duration for which the circuit breaker will try to recover (as soon as it is in recovering state). diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index a28358558..6b0797408 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -11,6 +11,9 @@ - "traefik.http.middlewares.middleware02.buffering.retryexpression=foobar" - "traefik.http.middlewares.middleware03.chain.middlewares=foobar, foobar" - "traefik.http.middlewares.middleware04.circuitbreaker.expression=foobar" +- "traefik.http.middlewares.middleware04.circuitbreaker.checkperiod=42s" +- "traefik.http.middlewares.middleware04.circuitbreaker.fallbackduration=42s" +- "traefik.http.middlewares.middleware04.circuitbreaker.recoveryduration=42s" - "traefik.http.middlewares.middleware05.compress=true" - "traefik.http.middlewares.middleware05.compress.excludedcontenttypes=foobar, foobar" - "traefik.http.middlewares.middleware05.compress.minresponsebodybytes=42" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 87ce97c27..a0eddd445 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -125,6 +125,9 @@ [http.middlewares.Middleware04] [http.middlewares.Middleware04.circuitBreaker] expression = "foobar" + checkPeriod = "42s" + fallbackDuration = "42s" + recoveryDuration = "42s" [http.middlewares.Middleware05] [http.middlewares.Middleware05.compress] excludedContentTypes = ["foobar", "foobar"] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index b5345b4b2..59266040f 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -128,6 +128,9 @@ http: Middleware04: circuitBreaker: expression: foobar + checkPeriod: 42s + fallbackDuration: 42s + recoveryDuration: 42s Middleware05: compress: excludedContentTypes: diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 959980b9f..fe3b72a8c 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -12,7 +12,10 @@ | `traefik/http/middlewares/Middleware02/buffering/retryExpression` | `foobar` | | `traefik/http/middlewares/Middleware03/chain/middlewares/0` | `foobar` | | `traefik/http/middlewares/Middleware03/chain/middlewares/1` | `foobar` | +| `traefik/http/middlewares/Middleware04/circuitBreaker/checkPeriod` | `42s` | | `traefik/http/middlewares/Middleware04/circuitBreaker/expression` | `foobar` | +| `traefik/http/middlewares/Middleware04/circuitBreaker/fallbackDuration` | `42s` | +| `traefik/http/middlewares/Middleware04/circuitBreaker/recoveryDuration` | `42s` | | `traefik/http/middlewares/Middleware05/compress/excludedContentTypes/0` | `foobar` | | `traefik/http/middlewares/Middleware05/compress/excludedContentTypes/1` | `foobar` | | `traefik/http/middlewares/Middleware05/compress/minResponseBodyBytes` | `42` | diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json index 900c50727..ce68ed9e6 100644 --- a/docs/content/reference/dynamic-configuration/marathon-labels.json +++ b/docs/content/reference/dynamic-configuration/marathon-labels.json @@ -11,6 +11,9 @@ "traefik.http.middlewares.middleware02.buffering.retryexpression": "foobar", "traefik.http.middlewares.middleware03.chain.middlewares": "foobar, foobar", "traefik.http.middlewares.middleware04.circuitbreaker.expression": "foobar", +"traefik.http.middlewares.middleware04.circuitbreaker.checkperiod": "42s", +"traefik.http.middlewares.middleware04.circuitbreaker.fallbackduration": "42s", +"traefik.http.middlewares.middleware04.circuitbreaker.recoveryduration": "42s", "traefik.http.middlewares.middleware05.compress": "true", "traefik.http.middlewares.middleware05.compress.excludedcontenttypes": "foobar, foobar", "traefik.http.middlewares.middleware05.compress.minresponsebodybytes": "42", diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml index 932e54010..d9a4cfd2a 100644 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml @@ -91,8 +91,32 @@ spec: circuitBreaker: description: CircuitBreaker holds the circuit breaker configuration. properties: + checkPeriod: + anyOf: + - type: integer + - type: string + description: CheckPeriod is the interval between successive checks + of the circuit breaker condition (when in standby state). + x-kubernetes-int-or-string: true expression: + description: Expression is the condition that triggers the tripped + state. type: string + fallbackDuration: + anyOf: + - type: integer + - type: string + description: FallbackDuration is the duration for which the circuit + breaker will wait before trying to recover (from a tripped state). + x-kubernetes-int-or-string: true + recoveryDuration: + anyOf: + - type: integer + - type: string + description: RecoveryDuration is the duration for which the circuit + breaker will try to recover (as soon as it is in recovering + state). + x-kubernetes-int-or-string: true type: object compress: description: Compress holds the compress configuration. diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 51c471a3d..e1c738b5a 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -535,8 +535,32 @@ spec: circuitBreaker: description: CircuitBreaker holds the circuit breaker configuration. properties: + checkPeriod: + anyOf: + - type: integer + - type: string + description: CheckPeriod is the interval between successive checks + of the circuit breaker condition (when in standby state). + x-kubernetes-int-or-string: true expression: + description: Expression is the condition that triggers the tripped + state. type: string + fallbackDuration: + anyOf: + - type: integer + - type: string + description: FallbackDuration is the duration for which the circuit + breaker will wait before trying to recover (from a tripped state). + x-kubernetes-int-or-string: true + recoveryDuration: + anyOf: + - type: integer + - type: string + description: RecoveryDuration is the duration for which the circuit + breaker will try to recover (as soon as it is in recovering + state). + x-kubernetes-int-or-string: true type: object compress: description: Compress holds the compress configuration. diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index d4e7d11fd..f8c9cfab2 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -93,7 +93,21 @@ type Chain struct { // CircuitBreaker holds the circuit breaker configuration. type CircuitBreaker struct { + // Expression is the condition that triggers the tripped state. Expression string `json:"expression,omitempty" toml:"expression,omitempty" yaml:"expression,omitempty" export:"true"` + // CheckPeriod is the interval between successive checks of the circuit breaker condition (when in standby state). + CheckPeriod ptypes.Duration `json:"checkPeriod,omitempty" toml:"checkPeriod,omitempty" yaml:"checkPeriod,omitempty" export:"true"` + // FallbackDuration is the duration for which the circuit breaker will wait before trying to recover (from a tripped state). + FallbackDuration ptypes.Duration `json:"fallbackDuration,omitempty" toml:"fallbackDuration,omitempty" yaml:"fallbackDuration,omitempty" export:"true"` + // RecoveryDuration is the duration for which the circuit breaker will try to recover (as soon as it is in recovering state). + RecoveryDuration ptypes.Duration `json:"recoveryDuration,omitempty" toml:"recoveryDuration,omitempty" yaml:"recoveryDuration,omitempty" export:"true"` +} + +// SetDefaults sets the default values on a RateLimit. +func (c *CircuitBreaker) SetDefaults() { + c.CheckPeriod = ptypes.Duration(100 * time.Millisecond) + c.FallbackDuration = ptypes.Duration(10 * time.Second) + c.RecoveryDuration = ptypes.Duration(10 * time.Second) } // +k8s:deepcopy-gen=true diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 612120fb0..4cda9c177 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -27,6 +27,9 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware2.buffering.retryexpression": "foobar", "traefik.http.middlewares.Middleware3.chain.middlewares": "foobar, fiibar", "traefik.http.middlewares.Middleware4.circuitbreaker.expression": "foobar", + "traefik.HTTP.Middlewares.Middleware4.circuitbreaker.checkperiod": "1s", + "traefik.HTTP.Middlewares.Middleware4.circuitbreaker.fallbackduration": "1s", + "traefik.HTTP.Middlewares.Middleware4.circuitbreaker.recoveryduration": "1s", "traefik.http.middlewares.Middleware5.digestauth.headerfield": "foobar", "traefik.http.middlewares.Middleware5.digestauth.realm": "foobar", "traefik.http.middlewares.Middleware5.digestauth.removeheader": "true", @@ -488,7 +491,10 @@ func TestDecodeConfiguration(t *testing.T) { }, "Middleware4": { CircuitBreaker: &dynamic.CircuitBreaker{ - Expression: "foobar", + Expression: "foobar", + CheckPeriod: ptypes.Duration(time.Second), + FallbackDuration: ptypes.Duration(time.Second), + RecoveryDuration: ptypes.Duration(time.Second), }, }, "Middleware5": { @@ -983,7 +989,10 @@ func TestEncodeConfiguration(t *testing.T) { }, "Middleware4": { CircuitBreaker: &dynamic.CircuitBreaker{ - Expression: "foobar", + Expression: "foobar", + CheckPeriod: ptypes.Duration(time.Second), + FallbackDuration: ptypes.Duration(time.Second), + RecoveryDuration: ptypes.Duration(time.Second), }, }, "Middleware5": { @@ -1191,6 +1200,9 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware2.Buffering.RetryExpression": "foobar", "traefik.HTTP.Middlewares.Middleware3.Chain.Middlewares": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware4.CircuitBreaker.Expression": "foobar", + "traefik.HTTP.Middlewares.Middleware4.CircuitBreaker.CheckPeriod": "1000000000", + "traefik.HTTP.Middlewares.Middleware4.CircuitBreaker.FallbackDuration": "1000000000", + "traefik.HTTP.Middlewares.Middleware4.CircuitBreaker.RecoveryDuration": "1000000000", "traefik.HTTP.Middlewares.Middleware5.DigestAuth.HeaderField": "foobar", "traefik.HTTP.Middlewares.Middleware5.DigestAuth.Realm": "foobar", "traefik.HTTP.Middlewares.Middleware5.DigestAuth.RemoveHeader": "true", diff --git a/pkg/middlewares/circuitbreaker/circuit_breaker.go b/pkg/middlewares/circuitbreaker/circuit_breaker.go index 981a3381f..77b489e7b 100644 --- a/pkg/middlewares/circuitbreaker/circuit_breaker.go +++ b/pkg/middlewares/circuitbreaker/circuit_breaker.go @@ -3,6 +3,7 @@ package circuitbreaker import ( "context" "net/http" + "time" "github.com/opentracing/opentracing-go/ext" "github.com/traefik/traefik/v2/pkg/config/dynamic" @@ -12,9 +13,7 @@ import ( "github.com/vulcand/oxy/cbreaker" ) -const ( - typeName = "CircuitBreaker" -) +const typeName = "CircuitBreaker" type circuitBreaker struct { circuitBreaker *cbreaker.CircuitBreaker @@ -27,9 +26,32 @@ func New(ctx context.Context, next http.Handler, confCircuitBreaker dynamic.Circ logger := log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName)) logger.Debug("Creating middleware") - logger.Debug("Setting up with expression: %s", expression) + logger.Debugf("Setting up with expression: %s", expression) - oxyCircuitBreaker, err := cbreaker.New(next, expression, createCircuitBreakerOptions(expression)) + cbOpts := []cbreaker.CircuitBreakerOption{ + cbreaker.Fallback(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + tracing.SetErrorWithEvent(req, "blocked by circuit-breaker (%q)", expression) + rw.WriteHeader(http.StatusServiceUnavailable) + + if _, err := rw.Write([]byte(http.StatusText(http.StatusServiceUnavailable))); err != nil { + log.FromContext(req.Context()).Error(err) + } + })), + } + + if confCircuitBreaker.CheckPeriod > 0 { + cbOpts = append(cbOpts, cbreaker.CheckPeriod(time.Duration(confCircuitBreaker.CheckPeriod))) + } + + if confCircuitBreaker.FallbackDuration > 0 { + cbOpts = append(cbOpts, cbreaker.FallbackDuration(time.Duration(confCircuitBreaker.FallbackDuration))) + } + + if confCircuitBreaker.RecoveryDuration > 0 { + cbOpts = append(cbOpts, cbreaker.RecoveryDuration(time.Duration(confCircuitBreaker.RecoveryDuration))) + } + + oxyCircuitBreaker, err := cbreaker.New(next, expression, cbOpts...) if err != nil { return nil, err } @@ -39,18 +61,6 @@ func New(ctx context.Context, next http.Handler, confCircuitBreaker dynamic.Circ }, nil } -// NewCircuitBreakerOptions returns a new CircuitBreakerOption. -func createCircuitBreakerOptions(expression string) cbreaker.CircuitBreakerOption { - return cbreaker.Fallback(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - tracing.SetErrorWithEvent(req, "blocked by circuit-breaker (%q)", expression) - rw.WriteHeader(http.StatusServiceUnavailable) - - if _, err := rw.Write([]byte(http.StatusText(http.StatusServiceUnavailable))); err != nil { - log.FromContext(req.Context()).Error(err) - } - })) -} - func (c *circuitBreaker) GetTracingInformation() (string, ext.SpanKindEnum) { return c.name, tracing.SpanKindNoneEnum } diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index ebbaf4963..2855c0bdc 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -243,6 +243,12 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) continue } + circuitBreaker, err := createCircuitBreakerMiddleware(middleware.Spec.CircuitBreaker) + if err != nil { + log.FromContext(ctxMid).Errorf("Error while reading circuit breaker middleware: %v", err) + continue + } + conf.HTTP.Middlewares[id] = &dynamic.Middleware{ AddPrefix: middleware.Spec.AddPrefix, StripPrefix: middleware.Spec.StripPrefix, @@ -261,7 +267,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) ForwardAuth: forwardAuth, InFlightReq: middleware.Spec.InFlightReq, Buffering: middleware.Spec.Buffering, - CircuitBreaker: middleware.Spec.CircuitBreaker, + CircuitBreaker: circuitBreaker, Compress: middleware.Spec.Compress, PassTLSClientCert: middleware.Spec.PassTLSClientCert, Retry: retry, @@ -425,6 +431,35 @@ func createPluginMiddleware(plugins map[string]apiextensionv1.JSON) (map[string] return pc, nil } +func createCircuitBreakerMiddleware(circuitBreaker *v1alpha1.CircuitBreaker) (*dynamic.CircuitBreaker, error) { + if circuitBreaker == nil { + return nil, nil + } + + cb := &dynamic.CircuitBreaker{Expression: circuitBreaker.Expression} + cb.SetDefaults() + + if circuitBreaker.CheckPeriod != nil { + if err := cb.CheckPeriod.Set(circuitBreaker.CheckPeriod.String()); err != nil { + return nil, err + } + } + + if circuitBreaker.FallbackDuration != nil { + if err := cb.FallbackDuration.Set(circuitBreaker.FallbackDuration.String()); err != nil { + return nil, err + } + } + + if circuitBreaker.RecoveryDuration != nil { + if err := cb.RecoveryDuration.Set(circuitBreaker.RecoveryDuration.String()); err != nil { + return nil, err + } + } + + return cb, nil +} + func createRateLimitMiddleware(rateLimit *v1alpha1.RateLimit) (*dynamic.RateLimit, error) { if rateLimit == nil { return nil, nil diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go index 2fff164a5..a4ae8b2ce 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go @@ -40,7 +40,7 @@ type MiddlewareSpec struct { ForwardAuth *ForwardAuth `json:"forwardAuth,omitempty"` InFlightReq *dynamic.InFlightReq `json:"inFlightReq,omitempty"` Buffering *dynamic.Buffering `json:"buffering,omitempty"` - CircuitBreaker *dynamic.CircuitBreaker `json:"circuitBreaker,omitempty"` + CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty"` Compress *dynamic.Compress `json:"compress,omitempty"` PassTLSClientCert *dynamic.PassTLSClientCert `json:"passTLSClientCert,omitempty"` Retry *Retry `json:"retry,omitempty"` @@ -59,6 +59,20 @@ type ErrorPage struct { // +k8s:deepcopy-gen=true +// CircuitBreaker holds the circuit breaker configuration. +type CircuitBreaker struct { + // Expression is the condition that triggers the tripped state. + Expression string `json:"expression,omitempty" toml:"expression,omitempty" yaml:"expression,omitempty" export:"true"` + // CheckPeriod is the interval between successive checks of the circuit breaker condition (when in standby state). + CheckPeriod *intstr.IntOrString `json:"checkPeriod,omitempty" toml:"checkPeriod,omitempty" yaml:"checkPeriod,omitempty" export:"true"` + // FallbackDuration is the duration for which the circuit breaker will wait before trying to recover (from a tripped state). + FallbackDuration *intstr.IntOrString `json:"fallbackDuration,omitempty" toml:"fallbackDuration,omitempty" yaml:"fallbackDuration,omitempty" export:"true"` + // RecoveryDuration is the duration for which the circuit breaker will try to recover (as soon as it is in recovering state). + RecoveryDuration *intstr.IntOrString `json:"recoveryDuration,omitempty" toml:"recoveryDuration,omitempty" yaml:"recoveryDuration,omitempty" export:"true"` +} + +// +k8s:deepcopy-gen=true + // Chain holds a chain of middlewares. type Chain struct { Middlewares []MiddlewareRef `json:"middlewares,omitempty"` diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index 9628ab921..12040e08e 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -74,6 +74,37 @@ func (in *Chain) DeepCopy() *Chain { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CircuitBreaker) DeepCopyInto(out *CircuitBreaker) { + *out = *in + if in.CheckPeriod != nil { + in, out := &in.CheckPeriod, &out.CheckPeriod + *out = new(intstr.IntOrString) + **out = **in + } + if in.FallbackDuration != nil { + in, out := &in.FallbackDuration, &out.FallbackDuration + *out = new(intstr.IntOrString) + **out = **in + } + if in.RecoveryDuration != nil { + in, out := &in.RecoveryDuration, &out.RecoveryDuration + *out = new(intstr.IntOrString) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CircuitBreaker. +func (in *CircuitBreaker) DeepCopy() *CircuitBreaker { + if in == nil { + return nil + } + out := new(CircuitBreaker) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClientAuth) DeepCopyInto(out *ClientAuth) { *out = *in @@ -714,8 +745,8 @@ func (in *MiddlewareSpec) DeepCopyInto(out *MiddlewareSpec) { } if in.CircuitBreaker != nil { in, out := &in.CircuitBreaker, &out.CircuitBreaker - *out = new(dynamic.CircuitBreaker) - **out = **in + *out = new(CircuitBreaker) + (*in).DeepCopyInto(*out) } if in.Compress != nil { in, out := &in.Compress, &out.Compress diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index b41fada2a..a7097755f 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -173,6 +173,9 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware03/chain/middlewares/0": "foobar", "traefik/http/middlewares/Middleware03/chain/middlewares/1": "foobar", "traefik/http/middlewares/Middleware04/circuitBreaker/expression": "foobar", + "traefik/http/middlewares/Middleware04/circuitBreaker/checkPeriod": "1s", + "traefik/http/middlewares/Middleware04/circuitBreaker/fallbackDuration": "1s", + "traefik/http/middlewares/Middleware04/circuitBreaker/recoveryDuration": "1s", "traefik/http/middlewares/Middleware07/errors/status/0": "foobar", "traefik/http/middlewares/Middleware07/errors/status/1": "foobar", "traefik/http/middlewares/Middleware07/errors/service": "foobar", @@ -393,7 +396,10 @@ func Test_buildConfiguration(t *testing.T) { }, "Middleware04": { CircuitBreaker: &dynamic.CircuitBreaker{ - Expression: "foobar", + Expression: "foobar", + CheckPeriod: ptypes.Duration(time.Second), + FallbackDuration: ptypes.Duration(time.Second), + RecoveryDuration: ptypes.Duration(time.Second), }, }, "Middleware05": { From 883422dc217ac23ab51d924cdb7cbfb98049fece Mon Sep 17 00:00:00 2001 From: Tom Moulard Date: Wed, 6 Apr 2022 10:06:13 +0200 Subject: [PATCH 4/9] Upgrade quic-go to v0.27.0 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e8a4c440e..07b3693c6 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/instana/go-sensor v1.38.3 github.com/klauspost/compress v1.13.0 github.com/kvtools/valkeyrie v0.4.0 - github.com/lucas-clemente/quic-go v0.26.0 + github.com/lucas-clemente/quic-go v0.27.0 github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f github.com/miekg/dns v1.1.45 github.com/mitchellh/copystructure v1.0.0 diff --git a/go.sum b/go.sum index e1b0a8327..5ca0d1a64 100644 --- a/go.sum +++ b/go.sum @@ -1124,8 +1124,8 @@ github.com/liquidweb/liquidweb-go v1.6.3/go.mod h1:SuXXp+thr28LnjEw18AYtWwIbWMHS github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/looplab/fsm v0.1.0 h1:Qte7Zdn/5hBNbXzP7yxVU4OIFHWXBovyTT2LaBTyC20= github.com/looplab/fsm v0.1.0/go.mod h1:m2VaOfDHxqXBBMgc26m6yUOwkFn8H2AlJDE+jd/uafI= -github.com/lucas-clemente/quic-go v0.26.0 h1:ALBQXr9UJ8A1LyzvceX4jd9QFsHvlI0RR6BkV16o00A= -github.com/lucas-clemente/quic-go v0.26.0/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI= +github.com/lucas-clemente/quic-go v0.27.0 h1:v6WY87q9zD4dKASbG8hy/LpzAVNzEQzw8sEIeloJsc4= +github.com/lucas-clemente/quic-go v0.27.0/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= From 7d274e80889645e880df51193b43f267515c48b8 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Thu, 28 Apr 2022 14:58:08 +0200 Subject: [PATCH 5/9] Deprecate caOptional option in client TLS configuration --- docs/content/middlewares/http/forwardauth.md | 64 -------------------- docs/content/migration/v2.md | 5 ++ docs/content/providers/consul-catalog.md | 31 ---------- docs/content/providers/consul.md | 30 --------- docs/content/providers/docker.md | 30 --------- docs/content/providers/etcd.md | 30 --------- docs/content/providers/http.md | 30 --------- docs/content/providers/marathon.md | 30 --------- docs/content/providers/redis.md | 30 --------- docs/content/providers/zookeeper.md | 30 --------- pkg/types/tls.go | 16 ++--- 11 files changed, 11 insertions(+), 315 deletions(-) diff --git a/docs/content/middlewares/http/forwardauth.md b/docs/content/middlewares/http/forwardauth.md index 0d2d566f5..fdcff0321 100644 --- a/docs/content/middlewares/http/forwardauth.md +++ b/docs/content/middlewares/http/forwardauth.md @@ -426,70 +426,6 @@ http: ca = "path/to/local.crt" ``` -#### `caOptional` - -_Optional_ - -The value of `caOptional` defines which policy should be used for the secure connection with TLS Client Authentication to the authentication server. - -!!! warning "" - - If `ca` is undefined, this option will be ignored, and no client certificate will be requested during the handshake. Any provided certificate will thus never be verified. - -When this option is set to `true`, a client certificate is requested during the handshake but is not required. If a certificate is sent, it is required to be valid. - -When this option is set to `false`, a client certificate is requested during the handshake, and at least one valid certificate should be sent by the client. - -```yaml tab="Docker" -labels: - - "traefik.http.middlewares.test-auth.forwardauth.tls.caOptional=true" -``` - -```yaml tab="Kubernetes" -apiVersion: traefik.containo.us/v1alpha1 -kind: Middleware -metadata: - name: test-auth -spec: - forwardAuth: - address: https://example.com/auth - tls: - caOptional: true -``` - -```yaml tab="Consul Catalog" -- "traefik.http.middlewares.test-auth.forwardauth.tls.caOptional=true" -``` - -```json tab="Marathon" -"labels": { - "traefik.http.middlewares.test-auth.forwardauth.tls.caOptional": "true" -} -``` - -```yaml tab="Rancher" -labels: - - "traefik.http.middlewares.test-auth.forwardauth.tls.caOptional=true" -``` - -```yaml tab="File (YAML)" -http: - middlewares: - test-auth: - forwardAuth: - address: "https://example.com/auth" - tls: - caOptional: true -``` - -```toml tab="File (TOML)" -[http.middlewares] - [http.middlewares.test-auth.forwardAuth] - address = "https://example.com/auth" - [http.middlewares.test-auth.forwardAuth.tls] - caOptional = true -``` - #### `cert` _Optional_ diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index d3f80dcde..783d08cac 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -464,3 +464,8 @@ In `v2.6.1`, the Datadog tags added to a span changed from `service.name` to `tr In `v2.7`, the `pilot.token` and `pilot.dashboard` options are deprecated. Please check the [feature deprecation page](../deprecation/features.md) and our Blog for migration instructions later this year. + +## v2.8 + +In `v2.8`, the `caOptional` option is deprecated as TLS client authentication is a server side option. +This option available in the ForwardAuth middleware, as well as in the HTTP, Consul, Etcd, Redis, ZooKeeper, Marathon, Consul Catalog, and Docker providers has no effect and must not be used anymore. diff --git a/docs/content/providers/consul-catalog.md b/docs/content/providers/consul-catalog.md index af03bb2a2..f9db6f119 100644 --- a/docs/content/providers/consul-catalog.md +++ b/docs/content/providers/consul-catalog.md @@ -393,37 +393,6 @@ providers: --providers.consulcatalog.endpoint.tls.ca=path/to/ca.crt ``` -##### `caOptional` - -_Optional_ - -The value of `caOptional` defines which policy should be used for the secure connection with TLS Client Authentication to Consul Catalog. - -!!! warning "" - - If `ca` is undefined, this option will be ignored, and no client certificate will be requested during the handshake. Any provided certificate will thus never be verified. - -When this option is set to `true`, a client certificate is requested during the handshake but is not required. If a certificate is sent, it is required to be valid. - -When this option is set to `false`, a client certificate is requested during the handshake, and at least one valid certificate should be sent by the client. - -```yaml tab="File (YAML)" -providers: - consulCatalog: - endpoint: - tls: - caOptional: true -``` - -```toml tab="File (TOML)" -[providers.consulCatalog.endpoint.tls] - caOptional = true -``` - -```bash tab="CLI" ---providers.consulcatalog.endpoint.tls.caoptional=true -``` - ##### `cert` _Optional_ diff --git a/docs/content/providers/consul.md b/docs/content/providers/consul.md index 289991062..25e84abbe 100644 --- a/docs/content/providers/consul.md +++ b/docs/content/providers/consul.md @@ -185,36 +185,6 @@ providers: --providers.consul.tls.ca=path/to/ca.crt ``` -#### `caOptional` - -_Optional_ - -The value of `caOptional` defines which policy should be used for the secure connection with TLS Client Authentication to Consul. - -!!! warning "" - - If `ca` is undefined, this option will be ignored, and no client certificate will be requested during the handshake. Any provided certificate will thus never be verified. - -When this option is set to `true`, a client certificate is requested during the handshake but is not required. If a certificate is sent, it is required to be valid. - -When this option is set to `false`, a client certificate is requested during the handshake, and at least one valid certificate should be sent by the client. - -```yaml tab="File (YAML)" -providers: - consul: - tls: - caOptional: true -``` - -```toml tab="File (TOML)" -[providers.consul.tls] - caOptional = true -``` - -```bash tab="CLI" ---providers.consul.tls.caOptional=true -``` - #### `cert` _Optional_ diff --git a/docs/content/providers/docker.md b/docs/content/providers/docker.md index 3ff5cc556..9b55f0668 100644 --- a/docs/content/providers/docker.md +++ b/docs/content/providers/docker.md @@ -643,36 +643,6 @@ providers: --providers.docker.tls.ca=path/to/ca.crt ``` -#### `caOptional` - -_Optional_ - -The value of `caOptional` defines which policy should be used for the secure connection with TLS Client Authentication to Docker. - -!!! warning "" - - If `ca` is undefined, this option will be ignored, and no client certificate will be requested during the handshake. Any provided certificate will thus never be verified. - -When this option is set to `true`, a client certificate is requested during the handshake but is not required. If a certificate is sent, it is required to be valid. - -When this option is set to `false`, a client certificate is requested during the handshake, and at least one valid certificate should be sent by the client. - -```yaml tab="File (YAML)" -providers: - docker: - tls: - caOptional: true -``` - -```toml tab="File (TOML)" -[providers.docker.tls] - caOptional = true -``` - -```bash tab="CLI" ---providers.docker.tls.caOptional=true -``` - #### `cert` `cert` is the path to the public certificate used for the secure connection to Docker. diff --git a/docs/content/providers/etcd.md b/docs/content/providers/etcd.md index 82909eedd..6bfaef7b7 100644 --- a/docs/content/providers/etcd.md +++ b/docs/content/providers/etcd.md @@ -134,36 +134,6 @@ providers: --providers.etcd.tls.ca=path/to/ca.crt ``` -#### `caOptional` - -_Optional_ - -The value of `caOptional` defines which policy should be used for the secure connection with TLS Client Authentication to etcd. - -!!! warning "" - - If `ca` is undefined, this option will be ignored, and no client certificate will be requested during the handshake. Any provided certificate will thus never be verified. - -When this option is set to `true`, a client certificate is requested during the handshake but is not required. If a certificate is sent, it is required to be valid. - -When this option is set to `false`, a client certificate is requested during the handshake, and at least one valid certificate should be sent by the client. - -```yaml tab="File (YAML)" -providers: - etcd: - tls: - caOptional: true -``` - -```toml tab="File (TOML)" -[providers.etcd.tls] - caOptional = true -``` - -```bash tab="CLI" ---providers.etcd.tls.caOptional=true -``` - #### `cert` _Optional_ diff --git a/docs/content/providers/http.md b/docs/content/providers/http.md index 5868e7a4b..f99e90a7c 100644 --- a/docs/content/providers/http.md +++ b/docs/content/providers/http.md @@ -105,36 +105,6 @@ providers: --providers.http.tls.ca=path/to/ca.crt ``` -#### `caOptional` - -_Optional_ - -The value of `caOptional` defines which policy should be used for the secure connection with TLS Client Authentication to the endpoint. - -!!! warning "" - - If `ca` is undefined, this option will be ignored, and no client certificate will be requested during the handshake. Any provided certificate will thus never be verified. - -When this option is set to `true`, a client certificate is requested during the handshake but is not required. If a certificate is sent, it is required to be valid. - -When this option is set to `false`, a client certificate is requested during the handshake, and at least one valid certificate should be sent by the client. - -```yaml tab="File (YAML)" -providers: - http: - tls: - caOptional: true -``` - -```toml tab="File (TOML)" -[providers.http.tls] - caOptional = true -``` - -```bash tab="CLI" ---providers.http.tls.caOptional=true -``` - #### `cert` _Optional_ diff --git a/docs/content/providers/marathon.md b/docs/content/providers/marathon.md index ec1490e1f..a0ab85ccb 100644 --- a/docs/content/providers/marathon.md +++ b/docs/content/providers/marathon.md @@ -432,36 +432,6 @@ providers: --providers.marathon.tls.ca=path/to/ca.crt ``` -#### `caOptional` - -_Optional_ - -The value of `caOptional` defines which policy should be used for the secure connection with TLS Client Authentication to Marathon. - -!!! warning "" - - If `ca` is undefined, this option will be ignored, and no client certificate will be requested during the handshake. Any provided certificate will thus never be verified. - -When this option is set to `true`, a client certificate is requested during the handshake but is not required. If a certificate is sent, it is required to be valid. - -When this option is set to `false`, a client certificate is requested during the handshake, and at least one valid certificate should be sent by the client. - -```yaml tab="File (YAML)" -providers: - marathon: - tls: - caOptional: true -``` - -```toml tab="File (TOML)" -[providers.marathon.tls] - caOptional = true -``` - -```bash tab="CLI" ---providers.marathon.tls.caOptional=true -``` - #### `cert` _Optional_ diff --git a/docs/content/providers/redis.md b/docs/content/providers/redis.md index 762d9aab6..505e612b6 100644 --- a/docs/content/providers/redis.md +++ b/docs/content/providers/redis.md @@ -134,36 +134,6 @@ providers: --providers.redis.tls.ca=path/to/ca.crt ``` -#### `caOptional` - -_Optional_ - -The value of `caOptional` defines which policy should be used for the secure connection with TLS Client Authentication to Redis. - -!!! warning "" - - If `ca` is undefined, this option will be ignored, and no client certificate will be requested during the handshake. Any provided certificate will thus never be verified. - -When this option is set to `true`, a client certificate is requested during the handshake but is not required. If a certificate is sent, it is required to be valid. - -When this option is set to `false`, a client certificate is requested during the handshake, and at least one valid certificate should be sent by the client. - -```yaml tab="File (YAML)" -providers: - redis: - tls: - caOptional: true -``` - -```toml tab="File (TOML)" -[providers.redis.tls] - caOptional = true -``` - -```bash tab="CLI" ---providers.redis.tls.caOptional=true -``` - #### `cert` _Optional_ diff --git a/docs/content/providers/zookeeper.md b/docs/content/providers/zookeeper.md index 8ebc94859..7b7483f98 100644 --- a/docs/content/providers/zookeeper.md +++ b/docs/content/providers/zookeeper.md @@ -134,36 +134,6 @@ providers: --providers.zookeeper.tls.ca=path/to/ca.crt ``` -#### `caOptional` - -_Optional_ - -The value of `caOptional` defines which policy should be used for the secure connection with TLS Client Authentication to Zookeeper. - -!!! warning "" - - If `ca` is undefined, this option will be ignored, and no client certificate will be requested during the handshake. Any provided certificate will thus never be verified. - -When this option is set to `true`, a client certificate is requested during the handshake but is not required. If a certificate is sent, it is required to be valid. - -When this option is set to `false`, a client certificate is requested during the handshake, and at least one valid certificate should be sent by the client. - -```yaml tab="File (YAML)" -providers: - zooKeeper: - tls: - caOptional: true -``` - -```toml tab="File (TOML)" -[providers.zooKeeper.tls] - caOptional = true -``` - -```bash tab="CLI" ---providers.zookeeper.tls.caOptional=true -``` - #### `cert` _Optional_ diff --git a/pkg/types/tls.go b/pkg/types/tls.go index f064f4e4b..821ac0c78 100644 --- a/pkg/types/tls.go +++ b/pkg/types/tls.go @@ -16,7 +16,8 @@ import ( // ClientTLS holds TLS specific configurations as client // CA, Cert and Key can be either path or file contents. type ClientTLS struct { - CA string `description:"TLS CA" json:"ca,omitempty" toml:"ca,omitempty" yaml:"ca,omitempty"` + CA string `description:"TLS CA" json:"ca,omitempty" toml:"ca,omitempty" yaml:"ca,omitempty"` + // Deprecated: TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634). CAOptional bool `description:"TLS CA.Optional" json:"caOptional,omitempty" toml:"caOptional,omitempty" yaml:"caOptional,omitempty" export:"true"` Cert string `description:"TLS cert" json:"cert,omitempty" toml:"cert,omitempty" yaml:"cert,omitempty"` Key string `description:"TLS key" json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty" loggable:"false"` @@ -30,10 +31,13 @@ func (clientTLS *ClientTLS) CreateTLSConfig(ctx context.Context) (*tls.Config, e return nil, nil } + if clientTLS.CAOptional { + log.FromContext(ctx).Warn("CAOptional is deprecated, TLS client authentication is a server side option.") + } + // Not initialized, to rely on system bundle. var caPool *x509.CertPool - clientAuth := tls.NoClientCert if clientTLS.CA != "" { var ca []byte if _, errCA := os.Stat(clientTLS.CA); errCA == nil { @@ -50,12 +54,6 @@ func (clientTLS *ClientTLS) CreateTLSConfig(ctx context.Context) (*tls.Config, e if !caPool.AppendCertsFromPEM(ca) { return nil, errors.New("failed to parse CA") } - - if clientTLS.CAOptional { - clientAuth = tls.VerifyClientCertIfGiven - } else { - clientAuth = tls.RequireAndVerifyClientCert - } } hasCert := len(clientTLS.Cert) > 0 @@ -69,7 +67,6 @@ func (clientTLS *ClientTLS) CreateTLSConfig(ctx context.Context) (*tls.Config, e return &tls.Config{ RootCAs: caPool, InsecureSkipVerify: clientTLS.InsecureSkipVerify, - ClientAuth: clientAuth, }, nil } @@ -82,7 +79,6 @@ func (clientTLS *ClientTLS) CreateTLSConfig(ctx context.Context) (*tls.Config, e Certificates: []tls.Certificate{cert}, RootCAs: caPool, InsecureSkipVerify: clientTLS.InsecureSkipVerify, - ClientAuth: clientAuth, }, nil } From ae6e84414360a8ac2aac488349cdbceaf2bf12b2 Mon Sep 17 00:00:00 2001 From: Tom Moulard Date: Tue, 10 May 2022 11:00:09 +0200 Subject: [PATCH 6/9] Support URL replacement in errors middleware --- docs/content/middlewares/http/errorpages.md | 53 +++++++++++-------- pkg/middlewares/customerrors/custom_errors.go | 1 + .../customerrors/custom_errors_test.go | 20 ++++++- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/docs/content/middlewares/http/errorpages.md b/docs/content/middlewares/http/errorpages.md index 25d3e508d..81986d667 100644 --- a/docs/content/middlewares/http/errorpages.md +++ b/docs/content/middlewares/http/errorpages.md @@ -1,16 +1,16 @@ --- -title: "Traefik ErrorPage Documentation" -description: "In Traefik Proxy, the ErrorPage middleware returns custom pages according to configured ranges of HTTP Status codes. Read the technical documentation." +title: "Traefik Errors Documentation" +description: "In Traefik Proxy, the Errors middleware returns custom pages according to configured ranges of HTTP Status codes. Read the technical documentation." --- -# ErrorPage +# Errors It Has Never Been Easier to Say That Something Went Wrong {: .subtitle } -![ErrorPages](../../assets/img/middleware/errorpages.png) +![Errors](../../assets/img/middleware/errorpages.png) -The ErrorPage middleware returns a custom page in lieu of the default, according to configured ranges of HTTP Status codes. +The Errors middleware returns a custom page in lieu of the default, according to configured ranges of HTTP Status codes. !!! important @@ -21,16 +21,16 @@ The ErrorPage middleware returns a custom page in lieu of the default, according ```yaml tab="Docker" # Dynamic Custom Error Page for 5XX Status Code labels: - - "traefik.http.middlewares.test-errorpage.errors.status=500-599" - - "traefik.http.middlewares.test-errorpage.errors.service=serviceError" - - "traefik.http.middlewares.test-errorpage.errors.query=/{status}.html" + - "traefik.http.middlewares.test-errors.errors.status=500-599" + - "traefik.http.middlewares.test-errors.errors.service=serviceError" + - "traefik.http.middlewares.test-errors.errors.query=/{status}.html" ``` ```yaml tab="Kubernetes" apiVersion: traefik.containo.us/v1alpha1 kind: Middleware metadata: - name: test-errorpage + name: test-errors spec: errors: status: @@ -43,32 +43,32 @@ spec: ```yaml tab="Consul Catalog" # Dynamic Custom Error Page for 5XX Status Code -- "traefik.http.middlewares.test-errorpage.errors.status=500-599" -- "traefik.http.middlewares.test-errorpage.errors.service=serviceError" -- "traefik.http.middlewares.test-errorpage.errors.query=/{status}.html" +- "traefik.http.middlewares.test-errors.errors.status=500-599" +- "traefik.http.middlewares.test-errors.errors.service=serviceError" +- "traefik.http.middlewares.test-errors.errors.query=/{status}.html" ``` ```json tab="Marathon" "labels": { - "traefik.http.middlewares.test-errorpage.errors.status": "500-599", - "traefik.http.middlewares.test-errorpage.errors.service": "serviceError", - "traefik.http.middlewares.test-errorpage.errors.query": "/{status}.html" + "traefik.http.middlewares.test-errors.errors.status": "500-599", + "traefik.http.middlewares.test-errors.errors.service": "serviceError", + "traefik.http.middlewares.test-errors.errors.query": "/{status}.html" } ``` ```yaml tab="Rancher" # Dynamic Custom Error Page for 5XX Status Code labels: - - "traefik.http.middlewares.test-errorpage.errors.status=500-599" - - "traefik.http.middlewares.test-errorpage.errors.service=serviceError" - - "traefik.http.middlewares.test-errorpage.errors.query=/{status}.html" + - "traefik.http.middlewares.test-errors.errors.status=500-599" + - "traefik.http.middlewares.test-errors.errors.service=serviceError" + - "traefik.http.middlewares.test-errors.errors.query=/{status}.html" ``` ```yaml tab="File (YAML)" # Custom Error Page for 5XX http: middlewares: - test-errorpage: + test-errors: errors: status: - "500-599" @@ -82,7 +82,7 @@ http: ```toml tab="File (TOML)" # Custom Error Page for 5XX [http.middlewares] - [http.middlewares.test-errorpage.errors] + [http.middlewares.test-errors.errors] status = ["500-599"] service = "serviceError" query = "/{status}.html" @@ -121,8 +121,17 @@ The service that will serve the new requested error page. !!! info "Host Header" By default, the client `Host` header value is forwarded to the configured error [service](#service). - To forward the `Host` value corresponding to the configured error service URL, the [passHostHeader](../../../routing/services/#pass-host-header) option must be set to `false`. + To forward the `Host` value corresponding to the configured error service URL, the [passHostHeader](../../../routing/services/#pass-host-header) option must be set to `false`. ### `query` -The URL for the error page (hosted by `service`). You can use the `{status}` variable in the `query` option in order to insert the status code in the URL. +The URL for the error page (hosted by [`service`](#service))). + +There are multiple variables that can be placed in the `query` option to insert values in the URL. + +The table below lists all the available variables and their associated values. + +| Variable | Value | +|------------|--------------------------------------------------------------------| +| `{status}` | The response status code. | +| `{url}` | The [escaped](https://pkg.go.dev/net/url#QueryEscape) request URL. | diff --git a/pkg/middlewares/customerrors/custom_errors.go b/pkg/middlewares/customerrors/custom_errors.go index 8a4431e8d..dd28aeccd 100644 --- a/pkg/middlewares/customerrors/custom_errors.go +++ b/pkg/middlewares/customerrors/custom_errors.go @@ -93,6 +93,7 @@ func (c *customErrors) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if len(c.backendQuery) > 0 { query = "/" + strings.TrimPrefix(c.backendQuery, "/") query = strings.ReplaceAll(query, "{status}", strconv.Itoa(code)) + query = strings.ReplaceAll(query, "{url}", url.QueryEscape(req.URL.String())) } pageReq, err := newRequest("http://" + req.Host + query) diff --git a/pkg/middlewares/customerrors/custom_errors_test.go b/pkg/middlewares/customerrors/custom_errors_test.go index 7b0a060ec..c291b15d5 100644 --- a/pkg/middlewares/customerrors/custom_errors_test.go +++ b/pkg/middlewares/customerrors/custom_errors_test.go @@ -133,6 +133,24 @@ func TestHandler(t *testing.T) { assert.Contains(t, recorder.Body.String(), "localhost") }, }, + { + desc: "full query replacement", + errorPage: &dynamic.ErrorPage{Service: "error", Query: "/?status={status}&url={url}", Status: []string{"503"}}, + backendCode: http.StatusServiceUnavailable, + backendErrorHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI != "/?status=503&url=http%3A%2F%2Flocalhost%2Ftest%3Ffoo%3Dbar%26baz%3Dbuz" { + t.Log(r.RequestURI) + return + } + + _, _ = fmt.Fprintln(w, "My 503 page.") + }), + validate: func(t *testing.T, recorder *httptest.ResponseRecorder) { + t.Helper() + assert.Equal(t, http.StatusServiceUnavailable, recorder.Code, "HTTP status") + assert.Contains(t, recorder.Body.String(), "My 503 page.") + }, + }, } for _, test := range testCases { @@ -153,7 +171,7 @@ func TestHandler(t *testing.T) { errorPageHandler, err := New(context.Background(), handler, *test.errorPage, serviceBuilderMock, "test") require.NoError(t, err) - req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost/test", nil) + req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost/test?foo=bar&baz=buz", nil) recorder := httptest.NewRecorder() errorPageHandler.ServeHTTP(recorder, req) From d5ff301d90709c0625363a5c95e0298ed8e96e47 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Thu, 19 May 2022 16:42:09 +0200 Subject: [PATCH 7/9] Support certificates configuration in TLSStore CRD Co-authored-by: Romain --- docs/content/https/tls.md | 5 +- .../traefik.containo.us_tlsstores.yaml | 20 +++- .../routing/providers/kubernetes-crd.md | 18 +-- integration/fixtures/k8s/01-traefik-crd.yml | 20 +++- .../fixtures/with_tls_store_certificates.yml | 43 +++++++ pkg/provider/kubernetes/crd/kubernetes.go | 108 ++++++++++++------ .../kubernetes/crd/kubernetes_http.go | 1 + pkg/provider/kubernetes/crd/kubernetes_tcp.go | 1 + .../kubernetes/crd/kubernetes_test.go | 57 +++++++++ .../crd/traefik/v1alpha1/tlsstore.go | 9 +- .../traefik/v1alpha1/zz_generated.deepcopy.go | 45 +++++--- 11 files changed, 252 insertions(+), 75 deletions(-) create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_tls_store_certificates.yml diff --git a/docs/content/https/tls.md b/docs/content/https/tls.md index f49d1f0e9..448f0ca5b 100644 --- a/docs/content/https/tls.md +++ b/docs/content/https/tls.md @@ -364,8 +364,9 @@ spec: ### Strict SNI Checking -With strict SNI checking enabled, Traefik won't allow connections from clients -that do not specify a server_name extension or don't match any certificate configured on the tlsOption. +With strict SNI checking enabled, Traefik won't allow connections from clients that do not specify a server_name extension +or don't match any of the configured certificates. +The default certificate is irrelevant on that matter. ```yaml tab="File (YAML)" # Dynamic configuration diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsstores.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsstores.yaml index eef5f1f77..57e7ef582 100644 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsstores.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsstores.yaml @@ -36,9 +36,23 @@ spec: spec: description: TLSStoreSpec configures a TLSStore resource. properties: + certificates: + description: Certificates is a list of secret names, each secret holding + a key/certificate pair to add to the store. + items: + description: Certificate holds a secret name for the TLSStore resource. + properties: + secretName: + description: SecretName is the name of the referenced Kubernetes + Secret to specify the certificate details. + type: string + required: + - secretName + type: object + type: array defaultCertificate: - description: DefaultCertificate holds a secret name for the TLSOption - resource. + description: DefaultCertificate is the name of the secret holding + the default key/certificate pair for the store. properties: secretName: description: SecretName is the name of the referenced Kubernetes @@ -47,8 +61,6 @@ spec: required: - secretName type: object - required: - - defaultCertificate type: object required: - metadata diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index 809b51637..891bb7135 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -1618,25 +1618,27 @@ or referencing TLS stores in the [`IngressRoute`](#kind-ingressroute) / [`Ingres Traefik currently only uses the [TLS Store named "default"](../../https/tls.md#certificates-stores). This means that if you have two stores that are named default in different kubernetes namespaces, they may be randomly chosen. - For the time being, please only configure one TLSSTore named default. + For the time being, please only configure one TLSStore named default. !!! info "TLSStore Attributes" - ```yaml tab="TLSStore" apiVersion: traefik.containo.us/v1alpha1 kind: TLSStore metadata: name: default namespace: default - spec: - defaultCertificate: - secretName: my-secret # [1] + certificates: # [1] + - secretName: foo + - secretName: bar + defaultCertificate: # [2] + secretName: secret ``` -| Ref | Attribute | Purpose | -|-----|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [1] | `secretName` | The name of the referenced Kubernetes [Secret](https://kubernetes.io/docs/concepts/configuration/secret/) that holds the default certificate for the store. | +| Ref | Attribute | Purpose | +|-----|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| [1] | `certificates` | List of Kubernetes [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/), each of them holding a key/certificate pair to add to the store. | +| [2] | `defaultCertificate` | Name of a Kubernetes [Secret](https://kubernetes.io/docs/concepts/configuration/secret/) that holds the default key/certificate pair for the store. | ??? example "Declaring and referencing a TLSStore" diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index e1c738b5a..5f963d898 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -1356,9 +1356,23 @@ spec: spec: description: TLSStoreSpec configures a TLSStore resource. properties: + certificates: + description: Certificates is a list of secret names, each secret holding + a key/certificate pair to add to the store. + items: + description: Certificate holds a secret name for the TLSStore resource. + properties: + secretName: + description: SecretName is the name of the referenced Kubernetes + Secret to specify the certificate details. + type: string + required: + - secretName + type: object + type: array defaultCertificate: - description: DefaultCertificate holds a secret name for the TLSOption - resource. + description: DefaultCertificate is the name of the secret holding + the default key/certificate pair for the store. properties: secretName: description: SecretName is the name of the referenced Kubernetes @@ -1367,8 +1381,6 @@ spec: required: - secretName type: object - required: - - defaultCertificate type: object required: - metadata diff --git a/pkg/provider/kubernetes/crd/fixtures/with_tls_store_certificates.yml b/pkg/provider/kubernetes/crd/fixtures/with_tls_store_certificates.yml new file mode 100644 index 000000000..2691ac79b --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_tls_store_certificates.yml @@ -0,0 +1,43 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: TLSStore +metadata: + name: default + namespace: default + +spec: + certificates: + - secretName: supersecret + +--- +apiVersion: v1 +kind: Secret +metadata: + name: supersecret + namespace: default + +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/bar`) + kind: Rule + priority: 12 + services: + - name: whoami + port: 80 + + tls: + store: + name: default diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 2855c0bdc..74485b078 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -179,18 +179,25 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. } func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) *dynamic.Configuration { - tlsConfigs := make(map[string]*tls.CertAndStores) + stores, tlsConfigs := buildTLSStores(ctx, client) + if tlsConfigs == nil { + tlsConfigs = make(map[string]*tls.CertAndStores) + } + conf := &dynamic.Configuration{ + // TODO: choose between mutating and returning tlsConfigs HTTP: p.loadIngressRouteConfiguration(ctx, client, tlsConfigs), TCP: p.loadIngressRouteTCPConfiguration(ctx, client, tlsConfigs), UDP: p.loadIngressRouteUDPConfiguration(ctx, client), TLS: &dynamic.TLSConfiguration{ - Certificates: getTLSConfig(tlsConfigs), - Options: buildTLSOptions(ctx, client), - Stores: buildTLSStores(ctx, client), + Options: buildTLSOptions(ctx, client), + Stores: stores, }, } + // Done after because tlsConfigs is mutated by the others above. + conf.TLS.Certificates = getTLSConfig(tlsConfigs) + for _, middleware := range client.GetMiddlewares() { id := provider.Normalize(makeID(middleware.Namespace, middleware.Name)) ctxMid := log.With(ctx, log.Str(log.MiddlewareName, id)) @@ -828,49 +835,60 @@ func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options return tlsOptions } -func buildTLSStores(ctx context.Context, client Client) map[string]tls.Store { +func buildTLSStores(ctx context.Context, client Client) (map[string]tls.Store, map[string]*tls.CertAndStores) { tlsStoreCRD := client.GetTLSStores() - var tlsStores map[string]tls.Store - if len(tlsStoreCRD) == 0 { - return tlsStores + return nil, nil } - tlsStores = make(map[string]tls.Store) + var nsDefault []string + tlsStores := make(map[string]tls.Store) + tlsConfigs := make(map[string]*tls.CertAndStores) - for _, tlsStore := range tlsStoreCRD { - namespace := tlsStore.Namespace - secretName := tlsStore.Spec.DefaultCertificate.SecretName - logger := log.FromContext(log.With(ctx, log.Str("tlsStore", tlsStore.Name), log.Str("namespace", namespace), log.Str("secretName", secretName))) + for _, t := range tlsStoreCRD { + logger := log.FromContext(log.With(ctx, log.Str("TLSStore", t.Name), log.Str("namespace", t.Namespace))) - secret, exists, err := client.GetSecret(namespace, secretName) - if err != nil { - logger.Errorf("Failed to fetch secret %s/%s: %v", namespace, secretName, err) - continue - } - if !exists { - logger.Errorf("Secret %s/%s does not exist", namespace, secretName) - continue - } + id := makeID(t.Namespace, t.Name) - cert, key, err := getCertificateBlocks(secret, namespace, secretName) - if err != nil { - logger.Errorf("Could not get certificate blocks: %v", err) - continue - } - - id := makeID(tlsStore.Namespace, tlsStore.Name) // If the name is default, we override the default config. - if tlsStore.Name == tls.DefaultTLSStoreName { - id = tlsStore.Name - nsDefault = append(nsDefault, tlsStore.Namespace) + if t.Name == tls.DefaultTLSStoreName { + id = t.Name + nsDefault = append(nsDefault, t.Namespace) } - tlsStores[id] = tls.Store{ - DefaultCertificate: &tls.Certificate{ + + var tlsStore tls.Store + + if t.Spec.DefaultCertificate != nil { + secretName := t.Spec.DefaultCertificate.SecretName + + secret, exists, err := client.GetSecret(t.Namespace, secretName) + if err != nil { + logger.Errorf("Failed to fetch secret %s/%s: %v", t.Namespace, secretName, err) + continue + } + if !exists { + logger.Errorf("Secret %s/%s does not exist", t.Namespace, secretName) + continue + } + + cert, key, err := getCertificateBlocks(secret, t.Namespace, secretName) + if err != nil { + logger.Errorf("Could not get certificate blocks: %v", err) + continue + } + + tlsStore.DefaultCertificate = &tls.Certificate{ CertFile: tls.FileOrContent(cert), KeyFile: tls.FileOrContent(key), - }, + } } + + if err := buildCertificates(client, id, t.Namespace, t.Spec.Certificates, tlsConfigs); err != nil { + logger.Errorf("Failed to load certificates: %v", err) + continue + } + + tlsStores[id] = tlsStore } if len(nsDefault) > 1 { @@ -878,7 +896,25 @@ func buildTLSStores(ctx context.Context, client Client) map[string]tls.Store { log.FromContext(ctx).Errorf("Default TLS Stores defined in multiple namespaces: %v", nsDefault) } - return tlsStores + return tlsStores, tlsConfigs +} + +// buildCertificates loads TLSStore certificates from secrets and sets them into tlsConfigs. +func buildCertificates(client Client, tlsStore, namespace string, certificates []v1alpha1.Certificate, tlsConfigs map[string]*tls.CertAndStores) error { + for _, c := range certificates { + configKey := namespace + "/" + c.SecretName + if _, tlsExists := tlsConfigs[configKey]; !tlsExists { + certAndStores, err := getTLS(client, c.SecretName, namespace) + if err != nil { + return fmt.Errorf("unable to read secret %s: %w", configKey, err) + } + + certAndStores.Stores = []string{tlsStore} + tlsConfigs[configKey] = certAndStores + } + } + + return nil } func makeServiceKey(rule, ingressName string) (string, error) { diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 333ce148c..4e08d90e9 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -495,6 +495,7 @@ func namespaceOrFallback(lb v1alpha1.LoadBalancerSpec, fallback string) string { return fallback } +// getTLSHTTP mutates tlsConfigs. func getTLSHTTP(ctx context.Context, ingressRoute *v1alpha1.IngressRoute, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { if ingressRoute.Spec.TLS == nil { return nil diff --git a/pkg/provider/kubernetes/crd/kubernetes_tcp.go b/pkg/provider/kubernetes/crd/kubernetes_tcp.go index a9c134de5..cb3be3f3a 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -269,6 +269,7 @@ func (p *Provider) loadTCPServers(client Client, namespace string, svc v1alpha1. return servers, nil } +// getTLSTCP mutates tlsConfigs. func getTLSTCP(ctx context.Context, ingressRoute *v1alpha1.IngressRouteTCP, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { if ingressRoute.Spec.TLS == nil { return nil diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 5b4914e11..6c081f0b1 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -3480,6 +3480,63 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, }, + { + desc: "TLS with tls store containing certificates", + paths: []string{"services.yml", "with_tls_store_certificates.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{ + CertFile: tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), + KeyFile: tls.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"), + }, + Stores: []string{"default"}, + }, + }, + Stores: map[string]tls.Store{ + "default": {}, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-test-route-6b204d94623b3df4370c": { + EntryPoints: []string{"web"}, + Service: "default-test-route-6b204d94623b3df4370c", + Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", + Priority: 12, + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-test-route-6b204d94623b3df4370c": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, { desc: "TLS with tls store default two times", paths: []string{"services.yml", "with_tls_store.yml", "with_default_tls_store.yml"}, diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsstore.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsstore.go index 404f07f96..06c1d8a6e 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsstore.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsstore.go @@ -20,13 +20,16 @@ type TLSStore struct { // TLSStoreSpec configures a TLSStore resource. type TLSStoreSpec struct { - DefaultCertificate DefaultCertificate `json:"defaultCertificate"` + // DefaultCertificate is the name of the secret holding the default key/certificate pair for the store. + DefaultCertificate *Certificate `json:"defaultCertificate,omitempty"` + // Certificates is a list of secret names, each secret holding a key/certificate pair to add to the store. + Certificates []Certificate `json:"certificates,omitempty"` } // +k8s:deepcopy-gen=true -// DefaultCertificate holds a secret name for the TLSOption resource. -type DefaultCertificate struct { +// Certificate holds a secret name for the TLSStore resource. +type Certificate struct { // SecretName is the name of the referenced Kubernetes Secret to specify the certificate details. SecretName string `json:"secretName"` } diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index 12040e08e..120ad8dfc 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -53,6 +53,22 @@ func (in *BasicAuth) DeepCopy() *BasicAuth { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Certificate) DeepCopyInto(out *Certificate) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Certificate. +func (in *Certificate) DeepCopy() *Certificate { + if in == nil { + return nil + } + out := new(Certificate) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Chain) DeepCopyInto(out *Chain) { *out = *in @@ -142,22 +158,6 @@ func (in *ClientTLS) DeepCopy() *ClientTLS { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DefaultCertificate) DeepCopyInto(out *DefaultCertificate) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultCertificate. -func (in *DefaultCertificate) DeepCopy() *DefaultCertificate { - if in == nil { - return nil - } - out := new(DefaultCertificate) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DigestAuth) DeepCopyInto(out *DigestAuth) { *out = *in @@ -1413,7 +1413,7 @@ func (in *TLSStore) DeepCopyInto(out *TLSStore) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) return } @@ -1487,7 +1487,16 @@ func (in *TLSStoreRef) DeepCopy() *TLSStoreRef { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSStoreSpec) DeepCopyInto(out *TLSStoreSpec) { *out = *in - out.DefaultCertificate = in.DefaultCertificate + if in.DefaultCertificate != nil { + in, out := &in.DefaultCertificate, &out.DefaultCertificate + *out = new(Certificate) + **out = **in + } + if in.Certificates != nil { + in, out := &in.Certificates, &out.Certificates + *out = make([]Certificate, len(*in)) + copy(*out, *in) + } return } From ec25bdb9f915ead01f416cae5c8683e19ed31f46 Mon Sep 17 00:00:00 2001 From: Qi Date: Mon, 30 May 2022 17:14:09 +0800 Subject: [PATCH 8/9] Add destination address to debug log --- pkg/tcp/proxy.go | 2 +- pkg/udp/proxy.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tcp/proxy.go b/pkg/tcp/proxy.go index 06dc2884a..038062c68 100644 --- a/pkg/tcp/proxy.go +++ b/pkg/tcp/proxy.go @@ -48,7 +48,7 @@ func NewProxy(address string, terminationDelay time.Duration, proxyProtocol *dyn // ServeTCP forwards the connection to a service. func (p *Proxy) ServeTCP(conn WriteCloser) { - log.WithoutContext().Debugf("Handling connection from %s", conn.RemoteAddr()) + log.WithoutContext().Debugf("Handling connection from %s to %s", conn.RemoteAddr(), p.address) // needed because of e.g. server.trackedConnection defer conn.Close() diff --git a/pkg/udp/proxy.go b/pkg/udp/proxy.go index a69aafd93..8ccbfda4b 100644 --- a/pkg/udp/proxy.go +++ b/pkg/udp/proxy.go @@ -20,7 +20,7 @@ func NewProxy(address string) (*Proxy, error) { // ServeUDP implements the Handler interface. func (p *Proxy) ServeUDP(conn *Conn) { - log.WithoutContext().Debugf("Handling connection from %s", conn.rAddr) + log.WithoutContext().Debugf("Handling connection from %s to %s", conn.rAddr, p.target) // needed because of e.g. server.trackedConnection defer conn.Close() From f90e3817e82c49e85994eebb4ba0594402909b62 Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 3 Jun 2022 12:00:09 +0200 Subject: [PATCH 9/9] Support multiple namespaces for Consul and ConsulCatalog providers Co-authored-by: Kevin Pollet --- .golangci.toml | 2 + docs/content/deprecation/features.md | 14 ++- docs/content/migration/v2.md | 6 +- docs/content/providers/consul-catalog.md | 58 ++++++++++-- docs/content/providers/consul.md | 56 ++++++++++- .../reference/static-configuration/cli-ref.md | 21 ++--- .../reference/static-configuration/env-ref.md | 21 ++--- .../reference/static-configuration/file.toml | 13 +-- .../reference/static-configuration/file.yaml | 17 ++-- pkg/config/static/static_config.go | 40 ++++---- pkg/provider/aggregator/aggregator.go | 8 +- pkg/provider/consulcatalog/config_test.go | 70 ++++++++++++-- pkg/provider/consulcatalog/consul_catalog.go | 93 ++++++++++++++----- pkg/provider/kv/consul/consul.go | 60 +++++++++++- pkg/provider/kv/consul/consul_test.go | 59 ++++++++++++ pkg/provider/kv/etcd/etcd.go | 2 +- pkg/provider/kv/kv.go | 11 ++- pkg/provider/kv/redis/redis.go | 2 +- pkg/provider/kv/zk/zk.go | 2 +- pkg/redactor/redactor_config_test.go | 58 ++++++------ .../testdata/anonymized-static-config.json | 14 ++- .../components/_commons/PanelMiddlewares.vue | 8 +- .../_commons/PanelMirroringServices.vue | 8 +- .../_commons/PanelRouterDetails.vue | 8 +- .../_commons/PanelServiceDetails.vue | 8 +- .../_commons/PanelWeightedServices.vue | 8 +- .../src/components/_commons/ProviderIcon.vue | 8 +- .../components/dashboard/PanelProvider.vue | 8 +- 28 files changed, 531 insertions(+), 152 deletions(-) create mode 100644 pkg/provider/kv/consul/consul_test.go diff --git a/.golangci.toml b/.golangci.toml index 43f02ce28..93777cec0 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -174,6 +174,8 @@ "SA1019: cfg.SSLHost is deprecated", "SA1019: cfg.SSLForceHost is deprecated", "SA1019: cfg.FeaturePolicy is deprecated", + "SA1019: c.Providers.ConsulCatalog.Namespace is deprecated", + "SA1019: c.Providers.Consul.Namespace is deprecated", ] [[issues.exclude-rules]] path = "(.+)_test.go" diff --git a/docs/content/deprecation/features.md b/docs/content/deprecation/features.md index 6c277c841..fb400c166 100644 --- a/docs/content/deprecation/features.md +++ b/docs/content/deprecation/features.md @@ -2,10 +2,11 @@ This page is maintained and updated periodically to reflect our roadmap and any decisions around feature deprecation. -| Feature | Deprecated | End of Support | Removal | -|-------------------------------------------------------|------------|----------------|---------| -| [Pilot Dashboard (Metrics)](#pilot-dashboard-metrics) | 2.7 | 2.8 | 2.9 | -| [Pilot Plugins](#pilot-plugins) | 2.7 | 2.8 | 2.9 | +| Feature | Deprecated | End of Support | Removal | +|---------------------------------------------------------------|------------|----------------|---------| +| [Pilot Dashboard (Metrics)](#pilot-dashboard-metrics) | 2.7 | 2.8 | 2.9 | +| [Pilot Plugins](#pilot-plugins) | 2.7 | 2.8 | 2.9 | +| [Consul Enterprise Namespaces](#consul-enterprise-namespaces) | 2.8 | TBD | TBD | ## Impact @@ -18,3 +19,8 @@ In 2.9, the Pilot platform and all Traefik integration code will be permanently Starting on 2.7 the pilot token will not be a requirement anymore. At 2.9, a new plugin catalog home should be available, decoupled from pilot. + +### Consul Enterprise Namespaces + +Starting on 2.8 the `namespace` option of Consul and Consul Catalog providers is deprecated, +please use the `namespaces` options instead. diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index c32f28987..822828145 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -460,7 +460,11 @@ In `v2.6.1`, the Datadog tags added to a span changed from `service.name` to `tr ## v2.8 -### TLS client authentication +### TLS client authentication In `v2.8`, the `caOptional` option is deprecated as TLS client authentication is a server side option. This option available in the ForwardAuth middleware, as well as in the HTTP, Consul, Etcd, Redis, ZooKeeper, Marathon, Consul Catalog, and Docker providers has no effect and must not be used anymore. + +### Consul Enterprise Namespaces + +In `v2.8`, the `namespace` option of Consul and Consul Catalog providers is deprecated, please use the `namespaces` options instead. diff --git a/docs/content/providers/consul-catalog.md b/docs/content/providers/consul-catalog.md index f9db6f119..d177df93c 100644 --- a/docs/content/providers/consul-catalog.md +++ b/docs/content/providers/consul-catalog.md @@ -525,7 +525,7 @@ providers: ``` ```bash tab="CLI" ---providers.consulcatalog.defaultRule="Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)" +--providers.consulcatalog.defaultRule=Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`) # ... ``` @@ -669,30 +669,76 @@ For additional information, refer to [Restrict the Scope of Service Discovery](. ### `namespace` +??? warning "Deprecated in favor of the [`namespaces`](#namespaces) option." + + _Optional, Default=""_ + + The `namespace` option defines the namespace in which the consul catalog services will be discovered. + + !!! warning + + The namespace option only works with [Consul Enterprise](https://www.consul.io/docs/enterprise), + which provides the [Namespaces](https://www.consul.io/docs/enterprise/namespaces) feature. + + !!! warning + + One should only define either the `namespaces` option or the `namespace` option. + + ```yaml tab="File (YAML)" + providers: + consulCatalog: + namespace: "production" + # ... + ``` + + ```toml tab="File (TOML)" + [providers.consulCatalog] + namespace = "production" + # ... + ``` + + ```bash tab="CLI" + --providers.consulcatalog.namespace=production + # ... + ``` + +### `namespaces` + _Optional, Default=""_ -The `namespace` option defines the namespace in which the consul catalog services will be discovered. +The `namespaces` option defines the namespaces in which the consul catalog services will be discovered. +When using the `namespaces` option, the discovered configuration object names will be suffixed as shown below: + +```text +@consulcatalog- +``` !!! warning - The namespace option only works with [Consul Enterprise](https://www.consul.io/docs/enterprise), + The namespaces option only works with [Consul Enterprise](https://www.consul.io/docs/enterprise), which provides the [Namespaces](https://www.consul.io/docs/enterprise/namespaces) feature. +!!! warning + + One should only define either the `namespaces` option or the `namespace` option. + ```yaml tab="File (YAML)" providers: consulCatalog: - namespace: "production" + namespaces: + - "ns1" + - "ns2" # ... ``` ```toml tab="File (TOML)" [providers.consulCatalog] - namespace = "production" + namespaces = ["ns1", "ns2"] # ... ``` ```bash tab="CLI" ---providers.consulcatalog.namespace=production +--providers.consulcatalog.namespaces=ns1,ns2 # ... ``` diff --git a/docs/content/providers/consul.md b/docs/content/providers/consul.md index 25e84abbe..cd82fd065 100644 --- a/docs/content/providers/consul.md +++ b/docs/content/providers/consul.md @@ -61,30 +61,76 @@ providers: ### `namespace` +??? warning "Deprecated in favor of the [`namespaces`](#namespaces) option." + + _Optional, Default=""_ + + The `namespace` option defines the namespace to query. + + !!! warning + + The namespace option only works with [Consul Enterprise](https://www.consul.io/docs/enterprise), + which provides the [Namespaces](https://www.consul.io/docs/enterprise/namespaces) feature. + + !!! warning + + One should only define either the `namespaces` option or the `namespace` option. + + ```yaml tab="File (YAML)" + providers: + consul: + # ... + namespace: "production" + ``` + + ```toml tab="File (TOML)" + [providers.consul] + # ... + namespace = "production" + ``` + + ```bash tab="CLI" + --providers.consul.namespace=production + ``` + +### `namespaces` + _Optional, Default=""_ -The `namespace` option defines the namespace to query. +The `namespaces` option defines the namespaces to query. +When using the `namespaces` option, the discovered configuration object names will be suffixed as shown below: + +```text +@consul- +``` !!! warning - The namespace option only works with [Consul Enterprise](https://www.consul.io/docs/enterprise), + The namespaces option only works with [Consul Enterprise](https://www.consul.io/docs/enterprise), which provides the [Namespaces](https://www.consul.io/docs/enterprise/namespaces) feature. +!!! warning + + One should only define either the `namespaces` option or the `namespace` option. + ```yaml tab="File (YAML)" providers: consul: + namespaces: + - "ns1" + - "ns2" # ... - namespace: "production" ``` ```toml tab="File (TOML)" [providers.consul] + namespaces = ["ns1", "ns2"] # ... - namespace = "production" ``` ```bash tab="CLI" ---providers.consul.namespace=production +--providers.consul.namespaces=ns1,ns2 +# ... ``` ### `username` diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 9dcc60921..1f7fba475 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -403,7 +403,10 @@ Enable Consul backend with default settings. (Default: ```false```) KV store endpoints (Default: ```127.0.0.1:8500```) `--providers.consul.namespace`: -KV Namespace +Sets the namespace used to discover the configuration (Consul Enterprise only). + +`--providers.consul.namespaces`: +Sets the namespaces used to discover the configuration (Consul Enterprise only). `--providers.consul.password`: KV Password @@ -492,11 +495,14 @@ Expose containers by default. (Default: ```true```) `--providers.consulcatalog.namespace`: Sets the namespace used to discover services (Consul Enterprise only). +`--providers.consulcatalog.namespaces`: +Sets the namespaces used to discover services (Consul Enterprise only). + `--providers.consulcatalog.prefix`: -Prefix for consul service tags. Default 'traefik' (Default: ```traefik```) +Prefix for consul service tags. (Default: ```traefik```) `--providers.consulcatalog.refreshinterval`: -Interval for check Consul API. Default 15s (Default: ```15```) +Interval for check Consul API. (Default: ```15```) `--providers.consulcatalog.requireconsistent`: Forces the read to be fully consistent. (Default: ```false```) @@ -594,9 +600,6 @@ Enable Etcd backend with default settings. (Default: ```false```) `--providers.etcd.endpoints`: KV store endpoints (Default: ```127.0.0.1:2379```) -`--providers.etcd.namespace`: -KV Namespace - `--providers.etcd.password`: KV Password @@ -858,9 +861,6 @@ Enable Redis backend with default settings. (Default: ```false```) `--providers.redis.endpoints`: KV store endpoints (Default: ```127.0.0.1:6379```) -`--providers.redis.namespace`: -KV Namespace - `--providers.redis.password`: KV Password @@ -900,9 +900,6 @@ Enable ZooKeeper backend with default settings. (Default: ```false```) `--providers.zookeeper.endpoints`: KV store endpoints (Default: ```127.0.0.1:2181```) -`--providers.zookeeper.namespace`: -KV Namespace - `--providers.zookeeper.password`: KV Password diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 44a976c37..861ac7c87 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -459,11 +459,14 @@ Expose containers by default. (Default: ```true```) `TRAEFIK_PROVIDERS_CONSULCATALOG_NAMESPACE`: Sets the namespace used to discover services (Consul Enterprise only). +`TRAEFIK_PROVIDERS_CONSULCATALOG_NAMESPACES`: +Sets the namespaces used to discover services (Consul Enterprise only). + `TRAEFIK_PROVIDERS_CONSULCATALOG_PREFIX`: -Prefix for consul service tags. Default 'traefik' (Default: ```traefik```) +Prefix for consul service tags. (Default: ```traefik```) `TRAEFIK_PROVIDERS_CONSULCATALOG_REFRESHINTERVAL`: -Interval for check Consul API. Default 15s (Default: ```15```) +Interval for check Consul API. (Default: ```15```) `TRAEFIK_PROVIDERS_CONSULCATALOG_REQUIRECONSISTENT`: Forces the read to be fully consistent. (Default: ```false```) @@ -481,7 +484,10 @@ Watch Consul API events. (Default: ```false```) KV store endpoints (Default: ```127.0.0.1:8500```) `TRAEFIK_PROVIDERS_CONSUL_NAMESPACE`: -KV Namespace +Sets the namespace used to discover the configuration (Consul Enterprise only). + +`TRAEFIK_PROVIDERS_CONSUL_NAMESPACES`: +Sets the namespaces used to discover the configuration (Consul Enterprise only). `TRAEFIK_PROVIDERS_CONSUL_PASSWORD`: KV Password @@ -594,9 +600,6 @@ Enable Etcd backend with default settings. (Default: ```false```) `TRAEFIK_PROVIDERS_ETCD_ENDPOINTS`: KV store endpoints (Default: ```127.0.0.1:2379```) -`TRAEFIK_PROVIDERS_ETCD_NAMESPACE`: -KV Namespace - `TRAEFIK_PROVIDERS_ETCD_PASSWORD`: KV Password @@ -858,9 +861,6 @@ Enable Redis backend with default settings. (Default: ```false```) `TRAEFIK_PROVIDERS_REDIS_ENDPOINTS`: KV store endpoints (Default: ```127.0.0.1:6379```) -`TRAEFIK_PROVIDERS_REDIS_NAMESPACE`: -KV Namespace - `TRAEFIK_PROVIDERS_REDIS_PASSWORD`: KV Password @@ -900,9 +900,6 @@ Enable ZooKeeper backend with default settings. (Default: ```false```) `TRAEFIK_PROVIDERS_ZOOKEEPER_ENDPOINTS`: KV store endpoints (Default: ```127.0.0.1:2181```) -`TRAEFIK_PROVIDERS_ZOOKEEPER_NAMESPACE`: -KV Namespace - `TRAEFIK_PROVIDERS_ZOOKEEPER_PASSWORD`: KV Password diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index aad84318d..7f30c61f8 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -28,12 +28,6 @@ [entryPoints.EntryPoint0.forwardedHeaders] insecure = true trustedIPs = ["foobar", "foobar"] - [entryPoints.EntryPoint0.udp] - timeout = 42 - [entryPoints.EntryPoint0.http2] - maxConcurrentStreams = 42 - [entryPoints.EntryPoint0.http3] - advertisedPort = 42 [entryPoints.EntryPoint0.http] middlewares = ["foobar", "foobar"] [entryPoints.EntryPoint0.http.redirections] @@ -53,6 +47,8 @@ [[entryPoints.EntryPoint0.http.tls.domains]] main = "foobar" sans = ["foobar", "foobar"] + [entryPoints.EntryPoint0.http2] + maxConcurrentStreams = 42 [entryPoints.EntryPoint0.http3] advertisedPort = 42 [entryPoints.EntryPoint0.udp] @@ -161,6 +157,7 @@ connectByDefault = true serviceName = "foobar" namespace = "foobar" + namespaces = ["foobar", "foobar"] watch = true [providers.consulCatalog.endpoint] address = "foobar" @@ -194,6 +191,7 @@ password = "foobar" token = "foobar" namespace = "foobar" + namespaces = ["foobar", "foobar"] [providers.consul.tls] ca = "foobar" caOptional = true @@ -206,7 +204,6 @@ username = "foobar" password = "foobar" token = "foobar" - namespace = "foobar" [providers.etcd.tls] ca = "foobar" caOptional = true @@ -219,7 +216,6 @@ username = "foobar" password = "foobar" token = "foobar" - namespace = "foobar" [providers.zooKeeper.tls] ca = "foobar" caOptional = true @@ -232,7 +228,6 @@ username = "foobar" password = "foobar" token = "foobar" - namespace = "foobar" [providers.redis.tls] ca = "foobar" caOptional = true diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 3887876e3..656909d9b 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -32,12 +32,6 @@ entryPoints: trustedIPs: - foobar - foobar - http2: - maxConcurrentStreams: 42 - http3: - advertisedPort: 42 - udp: - timeout: 42 http: redirections: entryPoint: @@ -60,6 +54,8 @@ entryPoints: sans: - foobar - foobar + http2: + maxConcurrentStreams: 42 http3: advertisedPort: 42 udp: @@ -173,6 +169,9 @@ providers: connectByDefault: true serviceName: foobar namespace: foobar + namespaces: + - foobar + - foobar watch: true endpoint: address: foobar @@ -210,6 +209,9 @@ providers: password: foobar token: foobar namespace: foobar + namespaces: + - foobar + - foobar tls: ca: foobar caOptional: true @@ -224,7 +226,6 @@ providers: username: foobar password: foobar token: foobar - namespace: foobar tls: ca: foobar caOptional: true @@ -239,7 +240,6 @@ providers: username: foobar password: foobar token: foobar - namespace: foobar tls: ca: foobar caOptional: true @@ -254,7 +254,6 @@ providers: username: foobar password: foobar token: foobar - namespace: foobar tls: ca: foobar caOptional: true diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index b285996f8..095ce08ff 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -175,22 +175,22 @@ func (t *Tracing) SetDefaults() { type Providers struct { ProvidersThrottleDuration ptypes.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"` - Docker *docker.Provider `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` - File *file.Provider `description:"Enable File backend with default settings." json:"file,omitempty" toml:"file,omitempty" yaml:"file,omitempty" export:"true"` - Marathon *marathon.Provider `description:"Enable Marathon backend with default settings." json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` - KubernetesIngress *ingress.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` - KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` - KubernetesGateway *gateway.Provider `description:"Enable Kubernetes gateway api provider with default settings." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` - Rest *rest.Provider `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` - Rancher *rancher.Provider `description:"Enable Rancher backend with default settings." json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` - ConsulCatalog *consulcatalog.Provider `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - Ecs *ecs.Provider `description:"Enable AWS ECS backend with default settings." json:"ecs,omitempty" toml:"ecs,omitempty" yaml:"ecs,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Docker *docker.Provider `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` + File *file.Provider `description:"Enable File backend with default settings." json:"file,omitempty" toml:"file,omitempty" yaml:"file,omitempty" export:"true"` + Marathon *marathon.Provider `description:"Enable Marathon backend with default settings." json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` + KubernetesIngress *ingress.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` + KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` + KubernetesGateway *gateway.Provider `description:"Enable Kubernetes gateway api provider with default settings." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` + Rest *rest.Provider `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` + Rancher *rancher.Provider `description:"Enable Rancher backend with default settings." json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` + ConsulCatalog *consulcatalog.ProviderBuilder `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Ecs *ecs.Provider `description:"Enable AWS ECS backend with default settings." json:"ecs,omitempty" toml:"ecs,omitempty" yaml:"ecs,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - Consul *consul.Provider `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - ZooKeeper *zk.Provider `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - Redis *redis.Provider `description:"Enable Redis backend with default settings." json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - HTTP *http.Provider `description:"Enable HTTP backend with default settings." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Consul *consul.ProviderBuilder `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + ZooKeeper *zk.Provider `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Redis *redis.Provider `description:"Enable Redis backend with default settings." json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + HTTP *http.Provider `description:"Enable HTTP backend with default settings." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` Plugin map[string]PluginConf `description:"Plugins configuration." json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty"` } @@ -257,7 +257,7 @@ func (c *Configuration) SetEffectiveConfiguration() { c.Pilot.SetDefaults() } - // Disable Gateway API provider if not enabled in experimental + // Disable Gateway API provider if not enabled in experimental. if c.Experimental == nil || !c.Experimental.KubernetesGateway { c.Providers.KubernetesGateway = nil } @@ -328,6 +328,14 @@ func (c *Configuration) ValidateConfiguration() error { acmeEmail = resolver.ACME.Email } + if c.Providers.ConsulCatalog != nil && c.Providers.ConsulCatalog.Namespace != "" && len(c.Providers.ConsulCatalog.Namespaces) > 0 { + return fmt.Errorf("consul catalog provider cannot have both namespace and namespaces options configured") + } + + if c.Providers.Consul != nil && c.Providers.Consul.Namespace != "" && len(c.Providers.Consul.Namespaces) > 0 { + return fmt.Errorf("consul provider cannot have both namespace and namespaces options configured") + } + return nil } diff --git a/pkg/provider/aggregator/aggregator.go b/pkg/provider/aggregator/aggregator.go index 21109a184..16a32003f 100644 --- a/pkg/provider/aggregator/aggregator.go +++ b/pkg/provider/aggregator/aggregator.go @@ -109,11 +109,15 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator { } if conf.ConsulCatalog != nil { - p.quietAddProvider(conf.ConsulCatalog) + for _, pvd := range conf.ConsulCatalog.BuildProviders() { + p.quietAddProvider(pvd) + } } if conf.Consul != nil { - p.quietAddProvider(conf.Consul) + for _, pvd := range conf.Consul.BuildProviders() { + p.quietAddProvider(pvd) + } } if conf.Etcd != nil { diff --git a/pkg/provider/consulcatalog/config_test.go b/pkg/provider/consulcatalog/config_test.go index ca3d75d14..a63d916d0 100644 --- a/pkg/provider/consulcatalog/config_test.go +++ b/pkg/provider/consulcatalog/config_test.go @@ -219,7 +219,7 @@ func TestDefaultRule(t *testing.T) { Status: api.HealthPassing, }, }, - defaultRule: DefaultTemplateRule, + defaultRule: defaultTemplateRule, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{}, @@ -262,8 +262,10 @@ func TestDefaultRule(t *testing.T) { t.Parallel() p := Provider{ - ExposedByDefault: true, - DefaultRule: test.defaultRule, + Configuration: Configuration{ + ExposedByDefault: true, + DefaultRule: test.defaultRule, + }, } err := p.Init() @@ -2618,10 +2620,12 @@ func Test_buildConfiguration(t *testing.T) { t.Parallel() p := Provider{ - ExposedByDefault: true, - DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)", - ConnectAware: test.ConnectAware, - Constraints: test.constraints, + Configuration: Configuration{ + ExposedByDefault: true, + DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)", + ConnectAware: test.ConnectAware, + Constraints: test.constraints, + }, } err := p.Init() @@ -2651,3 +2655,55 @@ func Test_buildConfiguration(t *testing.T) { }) } } + +func TestNamespaces(t *testing.T) { + testCases := []struct { + desc string + namespace string + namespaces []string + expectedNamespaces []string + }{ + { + desc: "no defined namespaces", + expectedNamespaces: []string{""}, + }, + { + desc: "deprecated: use of defined namespace", + namespace: "test-ns", + expectedNamespaces: []string{"test-ns"}, + }, + { + desc: "use of 1 defined namespaces", + namespaces: []string{"test-ns"}, + expectedNamespaces: []string{"test-ns"}, + }, + { + desc: "use of multiple defined namespaces", + namespaces: []string{"test-ns1", "test-ns2", "test-ns3", "test-ns4"}, + expectedNamespaces: []string{"test-ns1", "test-ns2", "test-ns3", "test-ns4"}, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + pb := &ProviderBuilder{ + Namespace: test.namespace, + Namespaces: test.namespaces, + } + + assert.Equal(t, test.expectedNamespaces, extractNSFromProvider(pb.BuildProviders())) + }) + } +} + +func extractNSFromProvider(providers []*Provider) []string { + res := make([]string, len(providers)) + for i, p := range providers { + res[i] = p.namespace + } + return res +} diff --git a/pkg/provider/consulcatalog/consul_catalog.go b/pkg/provider/consulcatalog/consul_catalog.go index a51127d5a..1bd0dab76 100644 --- a/pkg/provider/consulcatalog/consul_catalog.go +++ b/pkg/provider/consulcatalog/consul_catalog.go @@ -22,8 +22,11 @@ import ( "github.com/traefik/traefik/v2/pkg/types" ) -// DefaultTemplateRule The default template for the default rule. -const DefaultTemplateRule = "Host(`{{ normalize .Name }}`)" +// defaultTemplateRule is the default template for the default rule. +const defaultTemplateRule = "Host(`{{ normalize .Name }}`)" + +// providerName is the Consul Catalog provider name. +const providerName = "consulcatalog" var _ provider.Provider = (*Provider)(nil) @@ -41,12 +44,50 @@ type itemData struct { ExtraConf configuration } -// Provider holds configurations of the provider. -type Provider struct { +// ProviderBuilder is responsible for constructing namespaced instances of the Consul Catalog provider. +type ProviderBuilder struct { + Configuration `export:"true"` + + // Deprecated: use Namespaces option instead. + Namespace string `description:"Sets the namespace used to discover services (Consul Enterprise only)." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` + Namespaces []string `description:"Sets the namespaces used to discover services (Consul Enterprise only)." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty"` +} + +// BuildProviders builds Consul Catalog provider instances for the given namespaces configuration. +func (p *ProviderBuilder) BuildProviders() []*Provider { + // We can warn about that, because we've already made sure before that + // Namespace and Namespaces are mutually exclusive. + if p.Namespace != "" { + log.WithoutContext().Warnf("Namespace option is deprecated, please use the Namespaces option instead.") + } + + if len(p.Namespaces) == 0 { + return []*Provider{{ + Configuration: p.Configuration, + name: providerName, + // p.Namespace could very well be empty. + namespace: p.Namespace, + }} + } + + var providers []*Provider + for _, namespace := range p.Namespaces { + providers = append(providers, &Provider{ + Configuration: p.Configuration, + name: providerName + "-" + namespace, + namespace: namespace, + }) + } + + return providers +} + +// Configuration represents the Consul Catalog provider configuration. +type Configuration struct { Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"` Endpoint *EndpointConfig `description:"Consul endpoint settings" json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" export:"true"` - Prefix string `description:"Prefix for consul service tags. Default 'traefik'" json:"prefix,omitempty" toml:"prefix,omitempty" yaml:"prefix,omitempty" export:"true"` - RefreshInterval ptypes.Duration `description:"Interval for check Consul API. Default 15s" json:"refreshInterval,omitempty" toml:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty" export:"true"` + Prefix string `description:"Prefix for consul service tags." json:"prefix,omitempty" toml:"prefix,omitempty" yaml:"prefix,omitempty" export:"true"` + RefreshInterval ptypes.Duration `description:"Interval for check Consul API." json:"refreshInterval,omitempty" toml:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty" export:"true"` RequireConsistent bool `description:"Forces the read to be fully consistent." json:"requireConsistent,omitempty" toml:"requireConsistent,omitempty" yaml:"requireConsistent,omitempty" export:"true"` Stale bool `description:"Use stale consistency for catalog reads." json:"stale,omitempty" toml:"stale,omitempty" yaml:"stale,omitempty" export:"true"` Cache bool `description:"Use local agent caching for catalog reads." json:"cache,omitempty" toml:"cache,omitempty" yaml:"cache,omitempty" export:"true"` @@ -55,9 +96,25 @@ type Provider struct { ConnectAware bool `description:"Enable Consul Connect support." json:"connectAware,omitempty" toml:"connectAware,omitempty" yaml:"connectAware,omitempty" export:"true"` 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"` - Namespace string `description:"Sets the namespace used to discover services (Consul Enterprise only)." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty" export:"true"` Watch bool `description:"Watch Consul API events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"` +} +// SetDefaults sets the default values. +func (c *Configuration) SetDefaults() { + c.Endpoint = &EndpointConfig{} + c.RefreshInterval = ptypes.Duration(15 * time.Second) + c.Prefix = "traefik" + c.ExposedByDefault = true + c.DefaultRule = defaultTemplateRule + c.ServiceName = "traefik" +} + +// Provider is the Consul Catalog provider implementation. +type Provider struct { + Configuration + + name string + namespace string client *api.Client defaultRuleTpl *template.Template certChan chan *connectCert @@ -81,17 +138,6 @@ type EndpointHTTPAuthConfig struct { Password string `description:"Basic Auth password" json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty" loggable:"false"` } -// SetDefaults sets the default values. -func (p *Provider) SetDefaults() { - endpoint := &EndpointConfig{} - p.Endpoint = endpoint - p.RefreshInterval = ptypes.Duration(15 * time.Second) - p.Prefix = "traefik" - p.ExposedByDefault = true - p.DefaultRule = DefaultTemplateRule - p.ServiceName = "traefik" -} - // Init the provider. func (p *Provider) Init() error { defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil) @@ -103,19 +149,24 @@ func (p *Provider) Init() error { p.certChan = make(chan *connectCert, 1) p.watchServicesChan = make(chan struct{}, 1) + // In case they didn't initialize Provider with BuildProviders. + if p.name == "" { + p.name = providerName + } + return nil } // Provide allows the consul catalog provider to provide configurations to traefik using the given configuration channel. func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { var err error - p.client, err = createClient(p.Namespace, p.Endpoint) + p.client, err = createClient(p.namespace, p.Endpoint) if err != nil { return fmt.Errorf("failed to create consul client: %w", err) } pool.GoCtx(func(routineCtx context.Context) { - ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "consulcatalog")) + ctxLog := log.With(routineCtx, log.Str(log.ProviderName, p.name)) logger := log.FromContext(ctxLog) operation := func() error { @@ -210,7 +261,7 @@ func (p *Provider) loadConfiguration(ctx context.Context, certInfo *connectCert, } configurationChan <- dynamic.Message{ - ProviderName: "consulcatalog", + ProviderName: p.name, Configuration: p.buildConfiguration(ctx, data, certInfo), } diff --git a/pkg/provider/kv/consul/consul.go b/pkg/provider/kv/consul/consul.go index 6f3860955..492652c36 100644 --- a/pkg/provider/kv/consul/consul.go +++ b/pkg/provider/kv/consul/consul.go @@ -4,30 +4,80 @@ import ( "errors" "github.com/kvtools/valkeyrie/store" + "github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/provider" "github.com/traefik/traefik/v2/pkg/provider/kv" ) +// providerName is the Consul provider name. +const providerName = "consul" + var _ provider.Provider = (*Provider)(nil) -// Provider holds configurations of the provider. -type Provider struct { +// ProviderBuilder is responsible for constructing namespaced instances of the Consul provider. +type ProviderBuilder struct { kv.Provider `export:"true"` + + // Deprecated: use Namespaces instead. + Namespace string `description:"Sets the namespace used to discover the configuration (Consul Enterprise only)." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` + Namespaces []string `description:"Sets the namespaces used to discover the configuration (Consul Enterprise only)." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty"` } // SetDefaults sets the default values. -func (p *Provider) SetDefaults() { +func (p *ProviderBuilder) SetDefaults() { p.Provider.SetDefaults() p.Endpoints = []string{"127.0.0.1:8500"} } +// BuildProviders builds Consul provider instances for the given namespaces configuration. +func (p *ProviderBuilder) BuildProviders() []*Provider { + // We can warn about that, because we've already made sure before that + // Namespace and Namespaces are mutually exclusive. + if p.Namespace != "" { + log.WithoutContext().Warnf("Namespace option is deprecated, please use the Namespaces option instead.") + } + + if len(p.Namespaces) == 0 { + return []*Provider{{ + Provider: p.Provider, + name: providerName, + // p.Namespace could very well be empty. + namespace: p.Namespace, + }} + } + + var providers []*Provider + for _, namespace := range p.Namespaces { + providers = append(providers, &Provider{ + Provider: p.Provider, + name: providerName + "-" + namespace, + namespace: namespace, + }) + } + + return providers +} + +// Provider holds configurations of the provider. +type Provider struct { + kv.Provider + + name string + namespace string +} + // Init the provider. func (p *Provider) Init() error { // Wildcard namespace allows fetching KV values from any namespace for recursive requests (see https://www.consul.io/api/kv#ns). // As we are not supporting multiple namespaces at the same time, wildcard namespace is not allowed. - if p.Namespace == "*" { + if p.namespace == "*" { return errors.New("wildcard namespace is not supported") } - return p.Provider.Init(store.CONSUL, "consul") + // In case they didn't initialize with BuildProviders. + if p.name == "" { + p.name = providerName + } + + return p.Provider.Init(store.CONSUL, p.name, p.namespace) } diff --git a/pkg/provider/kv/consul/consul_test.go b/pkg/provider/kv/consul/consul_test.go new file mode 100644 index 000000000..286c0cd9c --- /dev/null +++ b/pkg/provider/kv/consul/consul_test.go @@ -0,0 +1,59 @@ +package consul + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNamespaces(t *testing.T) { + testCases := []struct { + desc string + namespace string + namespaces []string + expectedNamespaces []string + }{ + { + desc: "no defined namespaces", + expectedNamespaces: []string{""}, + }, + { + desc: "deprecated: use of defined namespace", + namespace: "test-ns", + expectedNamespaces: []string{"test-ns"}, + }, + { + desc: "use of 1 defined namespaces", + namespaces: []string{"test-ns"}, + expectedNamespaces: []string{"test-ns"}, + }, + { + desc: "use of multiple defined namespaces", + namespaces: []string{"test-ns1", "test-ns2", "test-ns3", "test-ns4"}, + expectedNamespaces: []string{"test-ns1", "test-ns2", "test-ns3", "test-ns4"}, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + pb := &ProviderBuilder{ + Namespace: test.namespace, + Namespaces: test.namespaces, + } + + assert.Equal(t, test.expectedNamespaces, extractNSFromProvider(pb.BuildProviders())) + }) + } +} + +func extractNSFromProvider(providers []*Provider) []string { + res := make([]string, len(providers)) + for i, p := range providers { + res[i] = p.namespace + } + return res +} diff --git a/pkg/provider/kv/etcd/etcd.go b/pkg/provider/kv/etcd/etcd.go index a06f1b1a1..a4b83bfbf 100644 --- a/pkg/provider/kv/etcd/etcd.go +++ b/pkg/provider/kv/etcd/etcd.go @@ -21,5 +21,5 @@ func (p *Provider) SetDefaults() { // Init the provider. func (p *Provider) Init() error { - return p.Provider.Init(store.ETCDV3, "etcd") + return p.Provider.Init(store.ETCDV3, "etcd", "") } diff --git a/pkg/provider/kv/kv.go b/pkg/provider/kv/kv.go index 50279e6bb..173c49698 100644 --- a/pkg/provider/kv/kv.go +++ b/pkg/provider/kv/kv.go @@ -30,12 +30,12 @@ type Provider struct { Username string `description:"KV Username" json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty" loggable:"false"` Password string `description:"KV Password" json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty" loggable:"false"` Token string `description:"KV Token" json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"` - Namespace string `description:"KV Namespace" json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` TLS *types.ClientTLS `description:"Enable TLS support" json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true" ` + name string + namespace string storeType store.Backend kvClient store.Store - name string } // SetDefaults sets the default values. @@ -44,11 +44,12 @@ func (p *Provider) SetDefaults() { } // Init the provider. -func (p *Provider) Init(storeType store.Backend, name string) error { +func (p *Provider) Init(storeType store.Backend, name, namespace string) error { ctx := log.With(context.Background(), log.Str(log.ProviderName, name)) - p.storeType = storeType p.name = name + p.namespace = namespace + p.storeType = storeType kvClient, err := p.createKVClient(ctx) if err != nil { @@ -167,7 +168,7 @@ func (p *Provider) createKVClient(ctx context.Context) (store.Store, error) { Username: p.Username, Password: p.Password, Token: p.Token, - Namespace: p.Namespace, + Namespace: p.namespace, } if p.TLS != nil { diff --git a/pkg/provider/kv/redis/redis.go b/pkg/provider/kv/redis/redis.go index f24b94e8e..96e48380e 100644 --- a/pkg/provider/kv/redis/redis.go +++ b/pkg/provider/kv/redis/redis.go @@ -21,5 +21,5 @@ func (p *Provider) SetDefaults() { // Init the provider. func (p *Provider) Init() error { - return p.Provider.Init(store.REDIS, "redis") + return p.Provider.Init(store.REDIS, "redis", "") } diff --git a/pkg/provider/kv/zk/zk.go b/pkg/provider/kv/zk/zk.go index 7f21219ef..01e5c9698 100644 --- a/pkg/provider/kv/zk/zk.go +++ b/pkg/provider/kv/zk/zk.go @@ -21,5 +21,5 @@ func (p *Provider) SetDefaults() { // Init the provider. func (p *Provider) Init() error { - return p.Provider.Init(store.ZK, "zookeeper") + return p.Provider.Init(store.ZK, "zookeeper", "") } diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index 1241a4f5f..a8fa09e50 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -682,33 +682,37 @@ func TestDo_staticConfiguration(t *testing.T) { Prefix: "MyPrefix", } - config.Providers.ConsulCatalog = &consulcatalog.Provider{ - Constraints: `Label("foo", "bar")`, - Endpoint: &consulcatalog.EndpointConfig{ - Address: "MyAddress", - Scheme: "MyScheme", - DataCenter: "MyDatacenter", - Token: "MyToken", - TLS: &types.ClientTLS{ - CA: "myCa", - CAOptional: true, - Cert: "mycert.pem", - Key: "mycert.key", - InsecureSkipVerify: true, + config.Providers.ConsulCatalog = &consulcatalog.ProviderBuilder{ + Configuration: consulcatalog.Configuration{ + Constraints: `Label("foo", "bar")`, + Endpoint: &consulcatalog.EndpointConfig{ + Address: "MyAddress", + Scheme: "MyScheme", + DataCenter: "MyDatacenter", + Token: "MyToken", + TLS: &types.ClientTLS{ + CA: "myCa", + CAOptional: true, + Cert: "mycert.pem", + Key: "mycert.key", + InsecureSkipVerify: true, + }, + HTTPAuth: &consulcatalog.EndpointHTTPAuthConfig{ + Username: "MyUsername", + Password: "MyPassword", + }, + EndpointWaitTime: 42, }, - HTTPAuth: &consulcatalog.EndpointHTTPAuthConfig{ - Username: "MyUsername", - Password: "MyPassword", - }, - EndpointWaitTime: 42, + Prefix: "MyPrefix", + RefreshInterval: 42, + RequireConsistent: true, + Stale: true, + Cache: true, + ExposedByDefault: true, + DefaultRule: "PathPrefix(`/`)", }, - Prefix: "MyPrefix", - RefreshInterval: 42, - RequireConsistent: true, - Stale: true, - Cache: true, - ExposedByDefault: true, - DefaultRule: "PathPrefix(`/`)", + Namespace: "ns", + Namespaces: []string{"ns1", "ns2"}, } config.Providers.Ecs = &ecs.Provider{ @@ -723,7 +727,7 @@ func TestDo_staticConfiguration(t *testing.T) { SecretAccessKey: "AwsSecretAccessKey", } - config.Providers.Consul = &consul.Provider{ + config.Providers.Consul = &consul.ProviderBuilder{ Provider: kv.Provider{ RootKey: "RootKey", Endpoints: nil, @@ -737,6 +741,8 @@ func TestDo_staticConfiguration(t *testing.T) { InsecureSkipVerify: true, }, }, + Namespace: "ns", + Namespaces: []string{"ns1", "ns2"}, } config.Providers.Etcd = &etcd.Provider{ diff --git a/pkg/redactor/testdata/anonymized-static-config.json b/pkg/redactor/testdata/anonymized-static-config.json index a525c388a..6773b047a 100644 --- a/pkg/redactor/testdata/anonymized-static-config.json +++ b/pkg/redactor/testdata/anonymized-static-config.json @@ -206,7 +206,12 @@ "stale": true, "cache": true, "exposedByDefault": true, - "defaultRule": "xxxx" + "defaultRule": "xxxx", + "namespace": "xxxx", + "namespaces": [ + "xxxx", + "xxxx" + ] }, "ecs": { "constraints": "Label(\"foo\", \"bar\")", @@ -232,7 +237,12 @@ "cert": "xxxx", "key": "xxxx", "insecureSkipVerify": true - } + }, + "namespace": "xxxx", + "namespaces": [ + "xxxx", + "xxxx" + ] }, "etcd": { "rootKey": "xxxx", diff --git a/webui/src/components/_commons/PanelMiddlewares.vue b/webui/src/components/_commons/PanelMiddlewares.vue index 20243462e..3437f8bef 100644 --- a/webui/src/components/_commons/PanelMiddlewares.vue +++ b/webui/src/components/_commons/PanelMiddlewares.vue @@ -1142,9 +1142,15 @@ export default { getProviderLogoPath (provider) { const name = provider.toLowerCase() - if (name.includes('plugin-')) { + if (name.startsWith('plugin-')) { return 'statics/providers/plugin.svg' } + if (name.startsWith('consul-')) { + return `statics/providers/consul.svg` + } + if (name.startsWith('consulcatalog-')) { + return `statics/providers/consulcatalog.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/_commons/PanelMirroringServices.vue b/webui/src/components/_commons/PanelMirroringServices.vue index d0d748fb7..849d99559 100644 --- a/webui/src/components/_commons/PanelMirroringServices.vue +++ b/webui/src/components/_commons/PanelMirroringServices.vue @@ -66,9 +66,15 @@ export default { const provider = this.getProvider(service) const name = provider.toLowerCase() - if (name.includes('plugin-')) { + if (name.startsWith('plugin-')) { return 'statics/providers/plugin.svg' } + if (name.startsWith('consul-')) { + return `statics/providers/consul.svg` + } + if (name.startsWith('consulcatalog-')) { + return `statics/providers/consulcatalog.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/_commons/PanelRouterDetails.vue b/webui/src/components/_commons/PanelRouterDetails.vue index 41eaca57e..582d13133 100644 --- a/webui/src/components/_commons/PanelRouterDetails.vue +++ b/webui/src/components/_commons/PanelRouterDetails.vue @@ -132,9 +132,15 @@ export default { getProviderLogoPath () { const name = this.data.provider.toLowerCase() - if (name.includes('plugin-')) { + if (name.startsWith('plugin-')) { return 'statics/providers/plugin.svg' } + if (name.startsWith('consul-')) { + return `statics/providers/consul.svg` + } + if (name.startsWith('consulcatalog-')) { + return `statics/providers/consulcatalog.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/_commons/PanelServiceDetails.vue b/webui/src/components/_commons/PanelServiceDetails.vue index 71a3c54dd..6d8de0857 100644 --- a/webui/src/components/_commons/PanelServiceDetails.vue +++ b/webui/src/components/_commons/PanelServiceDetails.vue @@ -146,9 +146,15 @@ export default { getProviderLogoPath () { const name = this.data.provider.toLowerCase() - if (name.includes('plugin-')) { + if (name.startsWith('plugin-')) { return 'statics/providers/plugin.svg' } + if (name.startsWith('consul-')) { + return `statics/providers/consul.svg` + } + if (name.startsWith('consulcatalog-')) { + return `statics/providers/consulcatalog.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/_commons/PanelWeightedServices.vue b/webui/src/components/_commons/PanelWeightedServices.vue index 3f23ed697..2b6f539d6 100644 --- a/webui/src/components/_commons/PanelWeightedServices.vue +++ b/webui/src/components/_commons/PanelWeightedServices.vue @@ -66,9 +66,15 @@ export default { const provider = this.getProvider(service) const name = provider.toLowerCase() - if (name.includes('plugin-')) { + if (name.startsWith('plugin-')) { return 'statics/providers/plugin.svg' } + if (name.startsWith('consul-')) { + return `statics/providers/consul.svg` + } + if (name.startsWith('consulcatalog-')) { + return `statics/providers/consulcatalog.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/_commons/ProviderIcon.vue b/webui/src/components/_commons/ProviderIcon.vue index 035a548a4..6b3bac409 100644 --- a/webui/src/components/_commons/ProviderIcon.vue +++ b/webui/src/components/_commons/ProviderIcon.vue @@ -11,9 +11,15 @@ export default { getLogoPath () { const name = this.name.toLowerCase() - if (name.includes('plugin-')) { + if (name.startsWith('plugin-')) { return 'statics/providers/plugin.svg' } + if (name.startsWith('consul-')) { + return `statics/providers/consul.svg` + } + if (name.startsWith('consulcatalog-')) { + return `statics/providers/consulcatalog.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/dashboard/PanelProvider.vue b/webui/src/components/dashboard/PanelProvider.vue index dba854c35..a4d451866 100644 --- a/webui/src/components/dashboard/PanelProvider.vue +++ b/webui/src/components/dashboard/PanelProvider.vue @@ -28,9 +28,15 @@ export default { getLogoPath () { const name = this.getName.toLowerCase() - if (name.includes('plugin-')) { + if (name.startsWith('plugin-')) { return 'statics/providers/plugin.svg' } + if (name.startsWith('consul-')) { + return `statics/providers/consul.svg` + } + if (name.startsWith('consulcatalog-')) { + return `statics/providers/consulcatalog.svg` + } return `statics/providers/${name}.svg` }