1
0
Fork 0

Merge current branch master into v2.7

This commit is contained in:
Tom Moulard 2022-06-10 16:17:55 +02:00
commit 59e66dfce5
No known key found for this signature in database
GPG key ID: 521ABE0C1A0DEAF6
72 changed files with 1162 additions and 605 deletions

View file

@ -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

View file

@ -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",

View file

@ -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,9 +62,19 @@ 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 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.

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

@ -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)

View file

@ -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 {

View file

@ -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
}

View file

@ -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),
}

View file

@ -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

View file

@ -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))
@ -243,6 +250,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 +274,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 +438,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
@ -793,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 {
@ -843,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) {

View file

@ -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

View file

@ -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

View file

@ -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"},

View file

@ -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"`

View file

@ -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"`
}

View file

@ -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
@ -74,6 +90,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
@ -111,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
@ -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
@ -1382,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
}
@ -1456,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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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", "")
}

View file

@ -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 {

View file

@ -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": {

View file

@ -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", "")
}

View file

@ -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", "")
}

View file

@ -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{

View file

@ -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",

View file

@ -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)

View file

@ -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,

View file

@ -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,
},

View file

@ -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)

View file

@ -46,7 +46,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()

View file

@ -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
}

View file

@ -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()