diff --git a/apis/projectcontour/v1alpha1/contourconfig.go b/apis/projectcontour/v1alpha1/contourconfig.go index 852cdb6326e..3e50f1efc24 100644 --- a/apis/projectcontour/v1alpha1/contourconfig.go +++ b/apis/projectcontour/v1alpha1/contourconfig.go @@ -107,6 +107,21 @@ const ( EnvoyServerType XDSServerType = "envoy" ) +type GlobalCircuitBreakerDefaults struct { + // The maximum number of connections that a single Envoy instance allows to the Kubernetes Service; defaults to 1024. + // +optional + MaxConnections uint32 `json:"maxConnections,omitempty" yaml:"max-connections,omitempty"` + // The maximum number of pending requests that a single Envoy instance allows to the Kubernetes Service; defaults to 1024. + // +optional + MaxPendingRequests uint32 `json:"maxPendingRequests,omitempty" yaml:"max-pending-requests,omitempty"` + // The maximum parallel requests a single Envoy instance allows to the Kubernetes Service; defaults to 1024 + // +optional + MaxRequests uint32 `json:"maxRequests,omitempty" yaml:"max-requests,omitempty"` + // The maximum number of parallel retries a single Envoy instance allows to the Kubernetes Service; defaults to 3. + // +optional + MaxRetries uint32 `json:"maxRetries,omitempty" yaml:"max-retries,omitempty"` +} + // XDSServerConfig holds the config for the Contour xDS server. type XDSServerConfig struct { // Defines the XDSServer to use for `contour serve`. @@ -689,6 +704,12 @@ type ClusterParameters struct { // +optional PerConnectionBufferLimitBytes *uint32 `json:"per-connection-buffer-limit-bytes,omitempty"` + // GlobalCircuitBreakerDefaults specifies default circuit breaker budget across all services. + // If defined, this will be used as the default for all services. + // + // +optional + GlobalCircuitBreakerDefaults *GlobalCircuitBreakerDefaults `json:"circuitBreakers,omitempty"` + // UpstreamTLS contains the TLS policy parameters for upstream connections // // +optional diff --git a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go index 832bf49e2a8..1727df16bd9 100644 --- a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go +++ b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go @@ -60,6 +60,11 @@ func (in *ClusterParameters) DeepCopyInto(out *ClusterParameters) { *out = new(uint32) **out = **in } + if in.GlobalCircuitBreakerDefaults != nil { + in, out := &in.GlobalCircuitBreakerDefaults, &out.GlobalCircuitBreakerDefaults + *out = new(GlobalCircuitBreakerDefaults) + **out = **in + } if in.UpstreamTLS != nil { in, out := &in.UpstreamTLS, &out.UpstreamTLS *out = new(EnvoyTLS) @@ -886,6 +891,21 @@ func (in *GatewayConfig) DeepCopy() *GatewayConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GlobalCircuitBreakerDefaults) DeepCopyInto(out *GlobalCircuitBreakerDefaults) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GlobalCircuitBreakerDefaults. +func (in *GlobalCircuitBreakerDefaults) DeepCopy() *GlobalCircuitBreakerDefaults { + if in == nil { + return nil + } + out := new(GlobalCircuitBreakerDefaults) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPProxyConfig) DeepCopyInto(out *HTTPProxyConfig) { *out = *in diff --git a/changelogs/unreleased/6013-davinci26-minor.md b/changelogs/unreleased/6013-davinci26-minor.md new file mode 100644 index 00000000000..df19a22dc78 --- /dev/null +++ b/changelogs/unreleased/6013-davinci26-minor.md @@ -0,0 +1,3 @@ +## Support for Global Circuit Breaker Policy + +The way [circuit-breaker-annotations](https://projectcontour.io/docs/1.27/config/annotations/) work currently is that when not present they are being defaulted to Envoy defaults. The Envoy defaults can be quite low for larger clusters with more traffic so if a user accidentally deletes them or unset them this cause an issue. With this change we are providing contour administrators the ability to provide global defaults that are good. In that case even if the user forgets to set them or deletes them they can have the safety net of good defaults. They can be configured via [cluster.circuit-breakers](https://projectcontour.io/docs/1.28/configuration/#circuit-breakers) or via `ContourConfiguration`` CRD in [spec.envoy.cluster.circuitBreakers](https://projectcontour.io/docs/1.28/config/api/#projectcontour.io/v1alpha1.GlobalCircuitBreakerDefaults) diff --git a/cmd/contour/serve.go b/cmd/contour/serve.go index e9abde903be..333f573bb32 100644 --- a/cmd/contour/serve.go +++ b/cmd/contour/serve.go @@ -560,6 +560,7 @@ func (s *Server) doServe() error { globalRateLimitService: contourConfiguration.RateLimitService, maxRequestsPerConnection: contourConfiguration.Envoy.Cluster.MaxRequestsPerConnection, perConnectionBufferLimitBytes: contourConfiguration.Envoy.Cluster.PerConnectionBufferLimitBytes, + globalCircuitBreakerDefaults: contourConfiguration.Envoy.Cluster.GlobalCircuitBreakerDefaults, upstreamTLS: &dag.UpstreamTLS{ MinimumProtocolVersion: annotation.TLSVersion(contourConfiguration.Envoy.Cluster.UpstreamTLS.MinimumProtocolVersion, "1.2"), MaximumProtocolVersion: annotation.TLSVersion(contourConfiguration.Envoy.Cluster.UpstreamTLS.MaximumProtocolVersion, "1.3"), @@ -1122,6 +1123,7 @@ type dagBuilderConfig struct { maxRequestsPerConnection *uint32 perConnectionBufferLimitBytes *uint32 globalRateLimitService *contour_api_v1alpha1.RateLimitServiceConfig + globalCircuitBreakerDefaults *contour_api_v1alpha1.GlobalCircuitBreakerDefaults upstreamTLS *dag.UpstreamTLS } @@ -1192,6 +1194,7 @@ func (s *Server) getDAGBuilder(dbc dagBuilderConfig) *dag.Builder { ConnectTimeout: dbc.connectTimeout, MaxRequestsPerConnection: dbc.maxRequestsPerConnection, PerConnectionBufferLimitBytes: dbc.perConnectionBufferLimitBytes, + GlobalCircuitBreakerDefaults: dbc.globalCircuitBreakerDefaults, SetSourceMetadataOnRoutes: true, UpstreamTLS: dbc.upstreamTLS, }, @@ -1217,6 +1220,7 @@ func (s *Server) getDAGBuilder(dbc dagBuilderConfig) *dag.Builder { GlobalRateLimitService: dbc.globalRateLimitService, PerConnectionBufferLimitBytes: dbc.perConnectionBufferLimitBytes, SetSourceMetadataOnRoutes: true, + GlobalCircuitBreakerDefaults: dbc.globalCircuitBreakerDefaults, UpstreamTLS: dbc.upstreamTLS, }, } @@ -1229,6 +1233,7 @@ func (s *Server) getDAGBuilder(dbc dagBuilderConfig) *dag.Builder { MaxRequestsPerConnection: dbc.maxRequestsPerConnection, PerConnectionBufferLimitBytes: dbc.perConnectionBufferLimitBytes, SetSourceMetadataOnRoutes: true, + GlobalCircuitBreakerDefaults: dbc.globalCircuitBreakerDefaults, }) } diff --git a/cmd/contour/serve_test.go b/cmd/contour/serve_test.go index 8f6ac4234b1..4bf95257e73 100644 --- a/cmd/contour/serve_test.go +++ b/cmd/contour/serve_test.go @@ -123,6 +123,31 @@ func TestGetDAGBuilder(t *testing.T) { assert.ElementsMatch(t, map[string]string(nil), ingressProcessor.ResponseHeadersPolicy.Remove) }) + t.Run("GlobalCircuitBreakerDefaults specified for all processors", func(t *testing.T) { + g := contour_api_v1alpha1.GlobalCircuitBreakerDefaults{ + MaxConnections: 100, + } + + serve := &Server{ + log: logrus.StandardLogger(), + } + got := serve.getDAGBuilder(dagBuilderConfig{ + gatewayControllerName: "projectcontour.io/gateway-controller", + rootNamespaces: []string{}, + dnsLookupFamily: contour_api_v1alpha1.AutoClusterDNSFamily, + globalCircuitBreakerDefaults: &g, + }) + + iProcessor := mustGetIngressProcessor(t, got) + assert.EqualValues(t, iProcessor.GlobalCircuitBreakerDefaults, &g) + + hProcessor := mustGetHTTPProxyProcessor(t, got) + assert.EqualValues(t, hProcessor.GlobalCircuitBreakerDefaults, &g) + + gProcessor := mustGetGatewayAPIProcessor(t, got) + assert.EqualValues(t, gProcessor.GlobalCircuitBreakerDefaults, &g) + }) + t.Run("request and response headers policy specified for ingress", func(t *testing.T) { policy := &contour_api_v1alpha1.PolicyConfig{ RequestHeadersPolicy: &contour_api_v1alpha1.HeadersPolicy{ @@ -192,6 +217,19 @@ func TestGetDAGBuilder(t *testing.T) { // TODO(3453): test additional properties of the DAG builder (processor fields, cache fields, Gateway tests (requires a client fake)) } +func mustGetGatewayAPIProcessor(t *testing.T, builder *dag.Builder) *dag.GatewayAPIProcessor { + t.Helper() + for i := range builder.Processors { + found, ok := builder.Processors[i].(*dag.GatewayAPIProcessor) + if ok { + return found + } + } + + require.FailNow(t, "GatewayAPIProcessor not found in list of DAG builder's processors") + return nil +} + func mustGetHTTPProxyProcessor(t *testing.T, builder *dag.Builder) *dag.HTTPProxyProcessor { t.Helper() for i := range builder.Processors { diff --git a/cmd/contour/servecontext.go b/cmd/contour/servecontext.go index 8994050b617..c6b71d0791a 100644 --- a/cmd/contour/servecontext.go +++ b/cmd/contour/servecontext.go @@ -572,6 +572,7 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_api_v1alpha DNSLookupFamily: dnsLookupFamily, MaxRequestsPerConnection: ctx.Config.Cluster.MaxRequestsPerConnection, PerConnectionBufferLimitBytes: ctx.Config.Cluster.PerConnectionBufferLimitBytes, + GlobalCircuitBreakerDefaults: ctx.Config.Cluster.GlobalCircuitBreakerDefaults, UpstreamTLS: &contour_api_v1alpha1.EnvoyTLS{ MinimumProtocolVersion: ctx.Config.Cluster.UpstreamTLS.MinimumProtocolVersion, MaximumProtocolVersion: ctx.Config.Cluster.UpstreamTLS.MaximumProtocolVersion, diff --git a/cmd/contour/servecontext_test.go b/cmd/contour/servecontext_test.go index 2ff36826417..b3914e0999d 100644 --- a/cmd/contour/servecontext_test.go +++ b/cmd/contour/servecontext_test.go @@ -473,7 +473,8 @@ func TestConvertServeContext(t *testing.T) { ConnectTimeout: ref.To("2s"), }, Cluster: &contour_api_v1alpha1.ClusterParameters{ - DNSLookupFamily: contour_api_v1alpha1.AutoClusterDNSFamily, + DNSLookupFamily: contour_api_v1alpha1.AutoClusterDNSFamily, + GlobalCircuitBreakerDefaults: nil, UpstreamTLS: &contour_api_v1alpha1.EnvoyTLS{ MinimumProtocolVersion: "", MaximumProtocolVersion: "", @@ -776,6 +777,26 @@ func TestConvertServeContext(t *testing.T) { return cfg }, }, + "global circuit breaker defaults": { + getServeContext: func(ctx *serveContext) *serveContext { + ctx.Config.Cluster.GlobalCircuitBreakerDefaults = &contour_api_v1alpha1.GlobalCircuitBreakerDefaults{ + MaxConnections: 4, + MaxPendingRequests: 5, + MaxRequests: 6, + MaxRetries: 7, + } + return ctx + }, + getContourConfiguration: func(cfg contour_api_v1alpha1.ContourConfigurationSpec) contour_api_v1alpha1.ContourConfigurationSpec { + cfg.Envoy.Cluster.GlobalCircuitBreakerDefaults = &contour_api_v1alpha1.GlobalCircuitBreakerDefaults{ + MaxConnections: 4, + MaxPendingRequests: 5, + MaxRequests: 6, + MaxRetries: 7, + } + return cfg + }, + }, "global external authorization": { getServeContext: func(ctx *serveContext) *serveContext { ctx.Config.GlobalExternalAuthorization = config.GlobalExternalAuthorization{ diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index f90d9226e4d..6f3f1f4aa65 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -78,6 +78,36 @@ spec: description: Cluster holds various configurable Envoy cluster values that can be set in the config file. properties: + circuitBreakers: + description: GlobalCircuitBreakerDefaults specifies default + circuit breaker budget across all services. If defined, + this will be used as the default for all services. + properties: + maxConnections: + description: The maximum number of connections that a + single Envoy instance allows to the Kubernetes Service; + defaults to 1024. + format: int32 + type: integer + maxPendingRequests: + description: The maximum number of pending requests that + a single Envoy instance allows to the Kubernetes Service; + defaults to 1024. + format: int32 + type: integer + maxRequests: + description: The maximum parallel requests a single Envoy + instance allows to the Kubernetes Service; defaults + to 1024 + format: int32 + type: integer + maxRetries: + description: The maximum number of parallel retries a + single Envoy instance allows to the Kubernetes Service; + defaults to 3. + format: int32 + type: integer + type: object dnsLookupFamily: description: "DNSLookupFamily defines how external names are looked up When configured as V4, the DNS resolver will only @@ -3684,6 +3714,36 @@ spec: description: Cluster holds various configurable Envoy cluster values that can be set in the config file. properties: + circuitBreakers: + description: GlobalCircuitBreakerDefaults specifies default + circuit breaker budget across all services. If defined, + this will be used as the default for all services. + properties: + maxConnections: + description: The maximum number of connections that + a single Envoy instance allows to the Kubernetes + Service; defaults to 1024. + format: int32 + type: integer + maxPendingRequests: + description: The maximum number of pending requests + that a single Envoy instance allows to the Kubernetes + Service; defaults to 1024. + format: int32 + type: integer + maxRequests: + description: The maximum parallel requests a single + Envoy instance allows to the Kubernetes Service; + defaults to 1024 + format: int32 + type: integer + maxRetries: + description: The maximum number of parallel retries + a single Envoy instance allows to the Kubernetes + Service; defaults to 3. + format: int32 + type: integer + type: object dnsLookupFamily: description: "DNSLookupFamily defines how external names are looked up When configured as V4, the DNS resolver diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml index b40c20ba752..5acc615ffaa 100644 --- a/examples/render/contour-deployment.yaml +++ b/examples/render/contour-deployment.yaml @@ -297,6 +297,36 @@ spec: description: Cluster holds various configurable Envoy cluster values that can be set in the config file. properties: + circuitBreakers: + description: GlobalCircuitBreakerDefaults specifies default + circuit breaker budget across all services. If defined, + this will be used as the default for all services. + properties: + maxConnections: + description: The maximum number of connections that a + single Envoy instance allows to the Kubernetes Service; + defaults to 1024. + format: int32 + type: integer + maxPendingRequests: + description: The maximum number of pending requests that + a single Envoy instance allows to the Kubernetes Service; + defaults to 1024. + format: int32 + type: integer + maxRequests: + description: The maximum parallel requests a single Envoy + instance allows to the Kubernetes Service; defaults + to 1024 + format: int32 + type: integer + maxRetries: + description: The maximum number of parallel retries a + single Envoy instance allows to the Kubernetes Service; + defaults to 3. + format: int32 + type: integer + type: object dnsLookupFamily: description: "DNSLookupFamily defines how external names are looked up When configured as V4, the DNS resolver will only @@ -3903,6 +3933,36 @@ spec: description: Cluster holds various configurable Envoy cluster values that can be set in the config file. properties: + circuitBreakers: + description: GlobalCircuitBreakerDefaults specifies default + circuit breaker budget across all services. If defined, + this will be used as the default for all services. + properties: + maxConnections: + description: The maximum number of connections that + a single Envoy instance allows to the Kubernetes + Service; defaults to 1024. + format: int32 + type: integer + maxPendingRequests: + description: The maximum number of pending requests + that a single Envoy instance allows to the Kubernetes + Service; defaults to 1024. + format: int32 + type: integer + maxRequests: + description: The maximum parallel requests a single + Envoy instance allows to the Kubernetes Service; + defaults to 1024 + format: int32 + type: integer + maxRetries: + description: The maximum number of parallel retries + a single Envoy instance allows to the Kubernetes + Service; defaults to 3. + format: int32 + type: integer + type: object dnsLookupFamily: description: "DNSLookupFamily defines how external names are looked up When configured as V4, the DNS resolver diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml index 4ccde1b91f6..526c7b2ba5a 100644 --- a/examples/render/contour-gateway-provisioner.yaml +++ b/examples/render/contour-gateway-provisioner.yaml @@ -89,6 +89,36 @@ spec: description: Cluster holds various configurable Envoy cluster values that can be set in the config file. properties: + circuitBreakers: + description: GlobalCircuitBreakerDefaults specifies default + circuit breaker budget across all services. If defined, + this will be used as the default for all services. + properties: + maxConnections: + description: The maximum number of connections that a + single Envoy instance allows to the Kubernetes Service; + defaults to 1024. + format: int32 + type: integer + maxPendingRequests: + description: The maximum number of pending requests that + a single Envoy instance allows to the Kubernetes Service; + defaults to 1024. + format: int32 + type: integer + maxRequests: + description: The maximum parallel requests a single Envoy + instance allows to the Kubernetes Service; defaults + to 1024 + format: int32 + type: integer + maxRetries: + description: The maximum number of parallel retries a + single Envoy instance allows to the Kubernetes Service; + defaults to 3. + format: int32 + type: integer + type: object dnsLookupFamily: description: "DNSLookupFamily defines how external names are looked up When configured as V4, the DNS resolver will only @@ -3695,6 +3725,36 @@ spec: description: Cluster holds various configurable Envoy cluster values that can be set in the config file. properties: + circuitBreakers: + description: GlobalCircuitBreakerDefaults specifies default + circuit breaker budget across all services. If defined, + this will be used as the default for all services. + properties: + maxConnections: + description: The maximum number of connections that + a single Envoy instance allows to the Kubernetes + Service; defaults to 1024. + format: int32 + type: integer + maxPendingRequests: + description: The maximum number of pending requests + that a single Envoy instance allows to the Kubernetes + Service; defaults to 1024. + format: int32 + type: integer + maxRequests: + description: The maximum parallel requests a single + Envoy instance allows to the Kubernetes Service; + defaults to 1024 + format: int32 + type: integer + maxRetries: + description: The maximum number of parallel retries + a single Envoy instance allows to the Kubernetes + Service; defaults to 3. + format: int32 + type: integer + type: object dnsLookupFamily: description: "DNSLookupFamily defines how external names are looked up When configured as V4, the DNS resolver diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml index 1a6894f5c67..8b16a4a46a1 100644 --- a/examples/render/contour-gateway.yaml +++ b/examples/render/contour-gateway.yaml @@ -300,6 +300,36 @@ spec: description: Cluster holds various configurable Envoy cluster values that can be set in the config file. properties: + circuitBreakers: + description: GlobalCircuitBreakerDefaults specifies default + circuit breaker budget across all services. If defined, + this will be used as the default for all services. + properties: + maxConnections: + description: The maximum number of connections that a + single Envoy instance allows to the Kubernetes Service; + defaults to 1024. + format: int32 + type: integer + maxPendingRequests: + description: The maximum number of pending requests that + a single Envoy instance allows to the Kubernetes Service; + defaults to 1024. + format: int32 + type: integer + maxRequests: + description: The maximum parallel requests a single Envoy + instance allows to the Kubernetes Service; defaults + to 1024 + format: int32 + type: integer + maxRetries: + description: The maximum number of parallel retries a + single Envoy instance allows to the Kubernetes Service; + defaults to 3. + format: int32 + type: integer + type: object dnsLookupFamily: description: "DNSLookupFamily defines how external names are looked up When configured as V4, the DNS resolver will only @@ -3906,6 +3936,36 @@ spec: description: Cluster holds various configurable Envoy cluster values that can be set in the config file. properties: + circuitBreakers: + description: GlobalCircuitBreakerDefaults specifies default + circuit breaker budget across all services. If defined, + this will be used as the default for all services. + properties: + maxConnections: + description: The maximum number of connections that + a single Envoy instance allows to the Kubernetes + Service; defaults to 1024. + format: int32 + type: integer + maxPendingRequests: + description: The maximum number of pending requests + that a single Envoy instance allows to the Kubernetes + Service; defaults to 1024. + format: int32 + type: integer + maxRequests: + description: The maximum parallel requests a single + Envoy instance allows to the Kubernetes Service; + defaults to 1024 + format: int32 + type: integer + maxRetries: + description: The maximum number of parallel retries + a single Envoy instance allows to the Kubernetes + Service; defaults to 3. + format: int32 + type: integer + type: object dnsLookupFamily: description: "DNSLookupFamily defines how external names are looked up When configured as V4, the DNS resolver diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index 0099e4fad8c..c7dc925b86d 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -297,6 +297,36 @@ spec: description: Cluster holds various configurable Envoy cluster values that can be set in the config file. properties: + circuitBreakers: + description: GlobalCircuitBreakerDefaults specifies default + circuit breaker budget across all services. If defined, + this will be used as the default for all services. + properties: + maxConnections: + description: The maximum number of connections that a + single Envoy instance allows to the Kubernetes Service; + defaults to 1024. + format: int32 + type: integer + maxPendingRequests: + description: The maximum number of pending requests that + a single Envoy instance allows to the Kubernetes Service; + defaults to 1024. + format: int32 + type: integer + maxRequests: + description: The maximum parallel requests a single Envoy + instance allows to the Kubernetes Service; defaults + to 1024 + format: int32 + type: integer + maxRetries: + description: The maximum number of parallel retries a + single Envoy instance allows to the Kubernetes Service; + defaults to 3. + format: int32 + type: integer + type: object dnsLookupFamily: description: "DNSLookupFamily defines how external names are looked up When configured as V4, the DNS resolver will only @@ -3903,6 +3933,36 @@ spec: description: Cluster holds various configurable Envoy cluster values that can be set in the config file. properties: + circuitBreakers: + description: GlobalCircuitBreakerDefaults specifies default + circuit breaker budget across all services. If defined, + this will be used as the default for all services. + properties: + maxConnections: + description: The maximum number of connections that + a single Envoy instance allows to the Kubernetes + Service; defaults to 1024. + format: int32 + type: integer + maxPendingRequests: + description: The maximum number of pending requests + that a single Envoy instance allows to the Kubernetes + Service; defaults to 1024. + format: int32 + type: integer + maxRequests: + description: The maximum parallel requests a single + Envoy instance allows to the Kubernetes Service; + defaults to 1024 + format: int32 + type: integer + maxRetries: + description: The maximum number of parallel retries + a single Envoy instance allows to the Kubernetes + Service; defaults to 3. + format: int32 + type: integer + type: object dnsLookupFamily: description: "DNSLookupFamily defines how external names are looked up When configured as V4, the DNS resolver diff --git a/internal/dag/gatewayapi_processor.go b/internal/dag/gatewayapi_processor.go index b76ef7f07a6..3b685bdc438 100644 --- a/internal/dag/gatewayapi_processor.go +++ b/internal/dag/gatewayapi_processor.go @@ -21,6 +21,7 @@ import ( "strings" "time" + contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" "github.com/projectcontour/contour/internal/gatewayapi" "github.com/projectcontour/contour/internal/k8s" "github.com/projectcontour/contour/internal/ref" @@ -74,6 +75,9 @@ type GatewayAPIProcessor struct { // configurable and off by default in order to support the feature // without requiring all existing test cases to change. SetSourceMetadataOnRoutes bool + + // GlobalCircuitBreakerDefaults defines global circuit breaker defaults. + GlobalCircuitBreakerDefaults *contour_api_v1alpha1.GlobalCircuitBreakerDefaults } // matchConditions holds match rules. @@ -1810,6 +1814,7 @@ func (p *GatewayAPIProcessor) validateBackendObjectRef( return nil, ref.To(resolvedRefsFalse(gatewayapi_v1beta1.RouteReasonBackendNotFound, fmt.Sprintf("service %q is invalid: %s", meta.Name, err))) } + service = serviceCircuitBreakerPolicy(service, p.GlobalCircuitBreakerDefaults) if err = validateAppProtocol(&service.Weighted.ServicePort); err != nil { return nil, ref.To(resolvedRefsFalse(gatewayapi_v1.RouteReasonUnsupportedProtocol, err.Error())) } diff --git a/internal/dag/httpproxy_processor.go b/internal/dag/httpproxy_processor.go index 3f3e5042c9e..29278ea5328 100644 --- a/internal/dag/httpproxy_processor.go +++ b/internal/dag/httpproxy_processor.go @@ -113,6 +113,9 @@ type HTTPProxyProcessor struct { // without requiring all existing test cases to change. SetSourceMetadataOnRoutes bool + // GlobalCircuitBreakerDefaults defines global circuit breaker defaults. + GlobalCircuitBreakerDefaults *contour_api_v1alpha1.GlobalCircuitBreakerDefaults + // UpstreamTLS defines the TLS settings like min/max version // and cipher suites for upstream connections. UpstreamTLS *UpstreamTLS @@ -938,6 +941,7 @@ func (p *HTTPProxyProcessor) computeRoutes( "Spec.Routes unresolved service reference: %s", err) continue } + s = serviceCircuitBreakerPolicy(s, p.GlobalCircuitBreakerDefaults) // Determine the protocol to use to speak to this Cluster. protocol, err := getProtocol(service, s) diff --git a/internal/dag/ingress_processor.go b/internal/dag/ingress_processor.go index c39def3720a..c15a1d8ae4c 100644 --- a/internal/dag/ingress_processor.go +++ b/internal/dag/ingress_processor.go @@ -20,6 +20,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" + contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" "github.com/projectcontour/contour/internal/annotation" "github.com/projectcontour/contour/internal/k8s" "github.com/projectcontour/contour/internal/ref" @@ -66,6 +67,9 @@ type IngressProcessor struct { // without requiring all existing test cases to change. SetSourceMetadataOnRoutes bool + // GlobalCircuitBreakerDefaults defines global circuit breaker defaults. + GlobalCircuitBreakerDefaults *contour_api_v1alpha1.GlobalCircuitBreakerDefaults + // UpstreamTLS defines the TLS settings like min/max version // and cipher suites for upstream connections. UpstreamTLS *UpstreamTLS @@ -217,6 +221,7 @@ func (p *IngressProcessor) computeIngressRule(ing *networking_v1.Ingress, rule n Error("unresolved service reference") continue } + s = serviceCircuitBreakerPolicy(s, p.GlobalCircuitBreakerDefaults) r, err := p.route(ing, rule.Host, path, pathType, s, clientCertSecret, be.Service.Name, be.Service.Port.Number, p.FieldLogger) if err != nil { diff --git a/internal/dag/policy.go b/internal/dag/policy.go index 97c57d2cf08..4c237b96a5f 100644 --- a/internal/dag/policy.go +++ b/internal/dag/policy.go @@ -27,6 +27,7 @@ import ( gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" + contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" "github.com/projectcontour/contour/internal/annotation" "github.com/projectcontour/contour/internal/ref" "github.com/projectcontour/contour/internal/timeout" @@ -809,3 +810,27 @@ func loadBalancerRequestHashPolicies(lbp *contour_api_v1.LoadBalancerPolicy, val } } + +func serviceCircuitBreakerPolicy(s *Service, cb *contour_api_v1alpha1.GlobalCircuitBreakerDefaults) *Service { + if s == nil { + return nil + } + + if s.MaxConnections == 0 && cb != nil { + s.MaxConnections = cb.MaxConnections + } + + if s.MaxPendingRequests == 0 && cb != nil { + s.MaxPendingRequests = cb.MaxPendingRequests + } + + if s.MaxRequests == 0 && cb != nil { + s.MaxRequests = cb.MaxRequests + } + + if s.MaxRetries == 0 && cb != nil { + s.MaxRetries = cb.MaxRetries + } + + return s +} diff --git a/internal/dag/policy_test.go b/internal/dag/policy_test.go index 6daf9e7a969..018b386d8b2 100644 --- a/internal/dag/policy_test.go +++ b/internal/dag/policy_test.go @@ -21,6 +21,7 @@ import ( "time" contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" + contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" "github.com/projectcontour/contour/internal/timeout" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -1271,6 +1272,76 @@ func TestValidateHeaderAlteration(t *testing.T) { } } +func TestServiceCircuitBreakerPolicy(t *testing.T) { + tests := map[string]struct { + in *Service + globalDefault *contour_api_v1alpha1.GlobalCircuitBreakerDefaults + want *Service + }{ + "service is nil and globalDefault is nil": { + in: nil, + globalDefault: nil, + want: nil, + }, + "service is nil and globalDefault is not nil": { + in: nil, + globalDefault: &contour_api_v1alpha1.GlobalCircuitBreakerDefaults{}, + want: nil, + }, + "service is not nil and globalDefault is nil": { + in: &Service{ + MaxConnections: 42, + MaxPendingRequests: 73, + MaxRequests: 89, + MaxRetries: 13, + }, + globalDefault: nil, + want: &Service{ + MaxConnections: 42, + MaxPendingRequests: 73, + MaxRequests: 89, + MaxRetries: 13, + }, + }, + "service is not set but global is": { + in: &Service{}, + globalDefault: &contour_api_v1alpha1.GlobalCircuitBreakerDefaults{ + MaxConnections: 42, + MaxPendingRequests: 73, + MaxRequests: 89, + MaxRetries: 13, + }, + want: &Service{ + MaxConnections: 42, + MaxPendingRequests: 73, + MaxRequests: 89, + MaxRetries: 13, + }, + }, + "service is not set but global is partial": { + in: &Service{}, + globalDefault: &contour_api_v1alpha1.GlobalCircuitBreakerDefaults{ + MaxConnections: 42, + MaxPendingRequests: 73, + MaxRequests: 89, + }, + want: &Service{ + MaxConnections: 42, + MaxPendingRequests: 73, + MaxRequests: 89, + MaxRetries: 0, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := serviceCircuitBreakerPolicy(tc.in, tc.globalDefault) + assert.Equal(t, tc.want, got) + }) + } +} + func TestExtractHeaderValue(t *testing.T) { tests := map[string]string{ "%REQ(X-Header-Name)%": "X-Header-Name", diff --git a/internal/featuretests/v3/cluster_test.go b/internal/featuretests/v3/cluster_test.go index 0c9c3dd0ae5..556c2d2de3f 100644 --- a/internal/featuretests/v3/cluster_test.go +++ b/internal/featuretests/v3/cluster_test.go @@ -20,14 +20,21 @@ import ( envoy_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" + contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" + "github.com/projectcontour/contour/internal/dag" envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" "github.com/projectcontour/contour/internal/featuretests" "github.com/projectcontour/contour/internal/fixture" + "github.com/projectcontour/contour/internal/gatewayapi" + "github.com/projectcontour/contour/internal/ref" + "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/wrapperspb" v1 "k8s.io/api/core/v1" networking_v1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + gatewayapi_v1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) // projectcontour/contour#186 @@ -383,8 +390,36 @@ func TestCDSResourceFiltering(t *testing.T) { }) } -func TestClusterCircuitbreakerAnnotations(t *testing.T) { - rh, c, done := setup(t) +func circuitBreakerGlobalOpt(t *testing.T, g *contour_api_v1alpha1.GlobalCircuitBreakerDefaults) func(*dag.Builder) { + return func(b *dag.Builder) { + log := fixture.NewTestLogger(t) + log.SetLevel(logrus.DebugLevel) + + b.Processors = []dag.Processor{ + &dag.ListenerProcessor{}, + &dag.IngressProcessor{ + FieldLogger: log.WithField("context", "IngressProcessor"), + GlobalCircuitBreakerDefaults: g, + }, + &dag.HTTPProxyProcessor{ + GlobalCircuitBreakerDefaults: g, + }, + &dag.GatewayAPIProcessor{ + FieldLogger: log.WithField("context", "GatewayAPIProcessor"), + GlobalCircuitBreakerDefaults: g, + }, + } + } +} + +func TestClusterCircuitbreakerAnnotationsIngress(t *testing.T) { + g := &contour_api_v1alpha1.GlobalCircuitBreakerDefaults{ + MaxConnections: 13, + MaxPendingRequests: 14, + MaxRequests: 15, + MaxRetries: 17, + } + rh, c, done := setup(t, circuitBreakerGlobalOpt(t, g)) defer done() s1 := fixture.NewService("kuard"). @@ -454,6 +489,347 @@ func TestClusterCircuitbreakerAnnotations(t *testing.T) { CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{ Thresholds: []*envoy_cluster_v3.CircuitBreakers_Thresholds{{ MaxPendingRequests: wrapperspb.UInt32(9999), + MaxConnections: wrapperspb.UInt32(13), + MaxRequests: wrapperspb.UInt32(15), + MaxRetries: wrapperspb.UInt32(17), + }}, + }, + }), + ), + TypeUrl: clusterType, + }) + + s3 := fixture.NewService("kuard"). + Annotate("projectcontour.io/max-connections", "0"). + Annotate("projectcontour.io/max-pending-requests", "0"). + Annotate("projectcontour.io/max-requests", "0"). + Annotate("projectcontour.io/max-retries", "0"). + WithPorts(v1.ServicePort{Port: 8080, TargetPort: intstr.FromString("8080")}) + + rh.OnUpdate(s2, s3) + + // check that it's been translated correctly. + c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + DefaultCluster(&envoy_cluster_v3.Cluster{ + Name: "default/kuard/8080/da39a3ee5e", + AltStatName: "default_kuard_8080", + ClusterDiscoveryType: envoy_v3.ClusterDiscoveryType(envoy_cluster_v3.Cluster_EDS), + EdsClusterConfig: &envoy_cluster_v3.Cluster_EdsClusterConfig{ + EdsConfig: envoy_v3.ConfigSource("contour"), + ServiceName: "default/kuard", + }, + CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{ + Thresholds: []*envoy_cluster_v3.CircuitBreakers_Thresholds{{ + MaxConnections: wrapperspb.UInt32(13), + MaxPendingRequests: wrapperspb.UInt32(14), + MaxRequests: wrapperspb.UInt32(15), + MaxRetries: wrapperspb.UInt32(17), + }}, + }, + }), + ), + TypeUrl: clusterType, + }) +} + +func TestClusterCircuitbreakerAnnotationsHTTPProxy(t *testing.T) { + g := &contour_api_v1alpha1.GlobalCircuitBreakerDefaults{ + MaxConnections: 13, + MaxPendingRequests: 14, + MaxRequests: 15, + MaxRetries: 17, + } + rh, c, done := setup(t, circuitBreakerGlobalOpt(t, g)) + defer done() + + s1 := fixture.NewService("kuard"). + Annotate("projectcontour.io/max-connections", "9000"). + Annotate("projectcontour.io/max-pending-requests", "4096"). + Annotate("projectcontour.io/max-requests", "404"). + Annotate("projectcontour.io/max-retries", "7"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromString("8080")}) + + rh.OnAdd( + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{Fqdn: "www.example.com"}, + Routes: []contour_api_v1.Route{ + { + Services: []contour_api_v1.Service{ + { + Name: "kuard", + Port: 80, + }, + }, + }, + }, + }, + }) + + rh.OnAdd(s1) + + // check that it's been translated correctly. + c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + DefaultCluster(&envoy_cluster_v3.Cluster{ + Name: "default/kuard/80/da39a3ee5e", + AltStatName: "default_kuard_80", + ClusterDiscoveryType: envoy_v3.ClusterDiscoveryType(envoy_cluster_v3.Cluster_EDS), + EdsClusterConfig: &envoy_cluster_v3.Cluster_EdsClusterConfig{ + EdsConfig: envoy_v3.ConfigSource("contour"), + ServiceName: "default/kuard", + }, + CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{ + Thresholds: []*envoy_cluster_v3.CircuitBreakers_Thresholds{{ + MaxConnections: wrapperspb.UInt32(9000), + MaxPendingRequests: wrapperspb.UInt32(4096), + MaxRequests: wrapperspb.UInt32(404), + MaxRetries: wrapperspb.UInt32(7), + }}, + }, + }), + ), + TypeUrl: clusterType, + }) + + // update s1 with slightly weird values + s2 := fixture.NewService("kuard"). + Annotate("projectcontour.io/max-pending-requests", "9999"). + Annotate("projectcontour.io/max-requests", "1e6"). + Annotate("projectcontour.io/max-retries", "0"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromString("8080")}) + + rh.OnUpdate(s1, s2) + + // check that it's been translated correctly. + c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + DefaultCluster(&envoy_cluster_v3.Cluster{ + Name: "default/kuard/80/da39a3ee5e", + AltStatName: "default_kuard_80", + ClusterDiscoveryType: envoy_v3.ClusterDiscoveryType(envoy_cluster_v3.Cluster_EDS), + EdsClusterConfig: &envoy_cluster_v3.Cluster_EdsClusterConfig{ + EdsConfig: envoy_v3.ConfigSource("contour"), + ServiceName: "default/kuard", + }, + CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{ + Thresholds: []*envoy_cluster_v3.CircuitBreakers_Thresholds{{ + MaxPendingRequests: wrapperspb.UInt32(9999), + MaxConnections: wrapperspb.UInt32(13), + MaxRequests: wrapperspb.UInt32(15), + MaxRetries: wrapperspb.UInt32(17), + }}, + }, + }), + ), + TypeUrl: clusterType, + }) + + s3 := fixture.NewService("kuard"). + Annotate("projectcontour.io/max-connections", "0"). + Annotate("projectcontour.io/max-pending-requests", "0"). + Annotate("projectcontour.io/max-requests", "0"). + Annotate("projectcontour.io/max-retries", "0"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromString("8080")}) + + rh.OnUpdate(s2, s3) + + // check that it's been translated correctly. + c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + DefaultCluster(&envoy_cluster_v3.Cluster{ + Name: "default/kuard/80/da39a3ee5e", + AltStatName: "default_kuard_80", + ClusterDiscoveryType: envoy_v3.ClusterDiscoveryType(envoy_cluster_v3.Cluster_EDS), + EdsClusterConfig: &envoy_cluster_v3.Cluster_EdsClusterConfig{ + EdsConfig: envoy_v3.ConfigSource("contour"), + ServiceName: "default/kuard", + }, + CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{ + Thresholds: []*envoy_cluster_v3.CircuitBreakers_Thresholds{{ + MaxConnections: wrapperspb.UInt32(13), + MaxPendingRequests: wrapperspb.UInt32(14), + MaxRequests: wrapperspb.UInt32(15), + MaxRetries: wrapperspb.UInt32(17), + }}, + }, + }), + ), + TypeUrl: clusterType, + }) +} + +func TestClusterCircuitbreakerAnnotationsGateway(t *testing.T) { + g := &contour_api_v1alpha1.GlobalCircuitBreakerDefaults{ + MaxConnections: 13, + MaxPendingRequests: 14, + MaxRequests: 15, + MaxRetries: 17, + } + rh, c, done := setup(t, circuitBreakerGlobalOpt(t, g)) + defer done() + + s1 := fixture.NewService("kuard"). + Annotate("projectcontour.io/max-connections", "9000"). + Annotate("projectcontour.io/max-pending-requests", "4096"). + Annotate("projectcontour.io/max-requests", "404"). + Annotate("projectcontour.io/max-retries", "7"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromString("8080")}) + + gc := &gatewayapi_v1beta1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "contour", + }, + Spec: gatewayapi_v1beta1.GatewayClassSpec{ + ControllerName: "projectcontour.io/contour", + }, + Status: gatewayapi_v1beta1.GatewayClassStatus{ + Conditions: []metav1.Condition{ + { + Type: string(gatewayapi_v1.GatewayClassConditionStatusAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + } + + gt := &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "contour", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: gatewayapi_v1beta1.ObjectName(gc.Name), + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "http", + Port: 80, + Protocol: gatewayapi_v1.HTTPProtocolType, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1.NamespacesFromAll), + }, + }, + }, + }, + }, + } + rh.OnAdd(gc) + rh.OnAdd(gt) + + rh.OnAdd(&gatewayapi_v1beta1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + }, + Spec: gatewayapi_v1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1beta1.ParentReference{ + gatewayapi.GatewayParentRef("projectcontour", "contour"), + }, + }, + Hostnames: []gatewayapi_v1beta1.Hostname{ + "test.projectcontour.io", + }, + Rules: []gatewayapi_v1beta1.HTTPRouteRule{ + { + Matches: gatewayapi.HTTPRouteMatch(gatewayapi_v1.PathMatchPathPrefix, "/"), + BackendRefs: gatewayapi.HTTPBackendRef("kuard", 80, 1), + }, + }, + }, + }) + + rh.OnAdd(s1) + + // check that it's been translated correctly. + c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + DefaultCluster(&envoy_cluster_v3.Cluster{ + Name: "default/kuard/80/da39a3ee5e", + AltStatName: "default_kuard_80", + ClusterDiscoveryType: envoy_v3.ClusterDiscoveryType(envoy_cluster_v3.Cluster_EDS), + EdsClusterConfig: &envoy_cluster_v3.Cluster_EdsClusterConfig{ + EdsConfig: envoy_v3.ConfigSource("contour"), + ServiceName: "default/kuard", + }, + CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{ + Thresholds: []*envoy_cluster_v3.CircuitBreakers_Thresholds{{ + MaxConnections: wrapperspb.UInt32(9000), + MaxPendingRequests: wrapperspb.UInt32(4096), + MaxRequests: wrapperspb.UInt32(404), + MaxRetries: wrapperspb.UInt32(7), + }}, + }, + }), + ), + TypeUrl: clusterType, + }) + + // update s1 with slightly weird values + s2 := fixture.NewService("kuard"). + Annotate("projectcontour.io/max-pending-requests", "9999"). + Annotate("projectcontour.io/max-requests", "1e6"). + Annotate("projectcontour.io/max-retries", "0"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromString("8080")}) + + rh.OnUpdate(s1, s2) + + // check that it's been translated correctly. + c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + DefaultCluster(&envoy_cluster_v3.Cluster{ + Name: "default/kuard/80/da39a3ee5e", + AltStatName: "default_kuard_80", + ClusterDiscoveryType: envoy_v3.ClusterDiscoveryType(envoy_cluster_v3.Cluster_EDS), + EdsClusterConfig: &envoy_cluster_v3.Cluster_EdsClusterConfig{ + EdsConfig: envoy_v3.ConfigSource("contour"), + ServiceName: "default/kuard", + }, + CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{ + Thresholds: []*envoy_cluster_v3.CircuitBreakers_Thresholds{{ + MaxPendingRequests: wrapperspb.UInt32(9999), + MaxConnections: wrapperspb.UInt32(13), + MaxRequests: wrapperspb.UInt32(15), + MaxRetries: wrapperspb.UInt32(17), + }}, + }, + }), + ), + TypeUrl: clusterType, + }) + + s3 := fixture.NewService("kuard"). + Annotate("projectcontour.io/max-connections", "0"). + Annotate("projectcontour.io/max-pending-requests", "0"). + Annotate("projectcontour.io/max-requests", "0"). + Annotate("projectcontour.io/max-retries", "0"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromString("8080")}) + + rh.OnUpdate(s2, s3) + + // check that it's been translated correctly. + c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + DefaultCluster(&envoy_cluster_v3.Cluster{ + Name: "default/kuard/80/da39a3ee5e", + AltStatName: "default_kuard_80", + ClusterDiscoveryType: envoy_v3.ClusterDiscoveryType(envoy_cluster_v3.Cluster_EDS), + EdsClusterConfig: &envoy_cluster_v3.Cluster_EdsClusterConfig{ + EdsConfig: envoy_v3.ConfigSource("contour"), + ServiceName: "default/kuard", + }, + CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{ + Thresholds: []*envoy_cluster_v3.CircuitBreakers_Thresholds{{ + MaxConnections: wrapperspb.UInt32(13), + MaxPendingRequests: wrapperspb.UInt32(14), + MaxRequests: wrapperspb.UInt32(15), + MaxRetries: wrapperspb.UInt32(17), }}, }, }), diff --git a/pkg/config/parameters.go b/pkg/config/parameters.go index 2c673e92cf6..87574eac345 100644 --- a/pkg/config/parameters.go +++ b/pkg/config/parameters.go @@ -444,6 +444,11 @@ type ClusterParameters struct { // +optional PerConnectionBufferLimitBytes *uint32 `yaml:"per-connection-buffer-limit-bytes,omitempty"` + // GlobalCircuitBreakerDefaults holds configurable global defaults for the circuit breakers. + // + // +optional + GlobalCircuitBreakerDefaults *contour_api_v1alpha1.GlobalCircuitBreakerDefaults `yaml:"circuit-breakers,omitempty"` + // UpstreamTLS contains the TLS policy parameters for upstream connections UpstreamTLS ProtocolParameters `yaml:"upstream-tls,omitempty"` } diff --git a/pkg/config/parameters_test.go b/pkg/config/parameters_test.go index 51b976bf147..52e31491d79 100644 --- a/pkg/config/parameters_test.go +++ b/pkg/config/parameters_test.go @@ -488,6 +488,20 @@ listener: cluster: max-requests-per-connection: 1 `) + + check(func(t *testing.T, conf *Parameters) { + assert.Equal(t, uint32(42), conf.Cluster.GlobalCircuitBreakerDefaults.MaxConnections) + assert.Equal(t, uint32(43), conf.Cluster.GlobalCircuitBreakerDefaults.MaxPendingRequests) + assert.Equal(t, uint32(44), conf.Cluster.GlobalCircuitBreakerDefaults.MaxRequests) + assert.Equal(t, uint32(0), conf.Cluster.GlobalCircuitBreakerDefaults.MaxRetries) + }, ` +cluster: + circuit-breakers: + max-connections: 42 + max-pending-requests: 43 + max-requests: 44 +`) + } func TestMetricsParametersValidation(t *testing.T) { diff --git a/site/content/docs/main/config/api-reference.html b/site/content/docs/main/config/api-reference.html index a518abd6843..a78277849ba 100644 --- a/site/content/docs/main/config/api-reference.html +++ b/site/content/docs/main/config/api-reference.html @@ -5675,6 +5675,22 @@

ClusterParameters +circuitBreakers +
+ + +GlobalCircuitBreakerDefaults + + + + +(Optional) +

GlobalCircuitBreakerDefaults specifies default circuit breaker budget across all services. +If defined, this will be used as the default for all services.

+ + + + upstreamTLS
@@ -7584,6 +7600,76 @@

GatewayConfig +

GlobalCircuitBreakerDefaults +

+

+(Appears on: +ClusterParameters) +

+

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+maxConnections +
+ +uint32 + +
+(Optional) +

The maximum number of connections that a single Envoy instance allows to the Kubernetes Service; defaults to 1024.

+
+maxPendingRequests +
+ +uint32 + +
+(Optional) +

The maximum number of pending requests that a single Envoy instance allows to the Kubernetes Service; defaults to 1024.

+
+maxRequests +
+ +uint32 + +
+(Optional) +

The maximum parallel requests a single Envoy instance allows to the Kubernetes Service; defaults to 1024

+
+maxRetries +
+ +uint32 + +
+(Optional) +

The maximum number of parallel retries a single Envoy instance allows to the Kubernetes Service; defaults to 3.

+

HTTPProxyConfig

diff --git a/site/content/docs/main/configuration.md b/site/content/docs/main/configuration.md index eecfeb682f6..f0e98c97ae3 100644 --- a/site/content/docs/main/configuration.md +++ b/site/content/docs/main/configuration.md @@ -167,11 +167,15 @@ The cluster configuration block can be used to configure various parameters for |-----------------------------------|--------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | dns-lookup-family | string | auto | This field specifies the dns-lookup-family to use for upstream requests to externalName type Kubernetes services from an HTTPProxy route. Values are: `auto`, `v4`, `v6`, `all` | | max-requests-per-connection | int | none | This field specifies the maximum requests for upstream connections. If not specified, there is no limit | +| circuit-breakers | [CircuitBreakers](#circuit-breakers) | none | This field specifies the default value for [circuit-breaker-annotations](https://projectcontour.io/docs/main/config/annotations/) for services that don't specify them. | | per-connection-buffer-limit-bytes | int | 1MiB* | This field specifies the soft limit on size of the cluster’s new connection read and write buffer. If not specified, Envoy defaults of 1MiB apply | | upstream-tls | UpstreamTLS | | [Upstream TLS configuration](#upstream-tls) | _This is Envoy's default setting value and is not explicitly configured by Contour._ + + + ### Network Configuration The network configuration block can be used to configure various parameters network connections. @@ -288,6 +292,16 @@ Metrics and health endpoints cannot have the same port number when metrics are s | tos | int | 0 | Defines the value for IPv4 TOS field (including 6 bit DSCP field) for IP packets originating from Envoy listeners. Single value is applied to all listeners. The value must be in the range 0-255, 0 means socket option is not set. If listeners are bound to IPv6-only addresses, setting this option will cause an error. | | traffic-class | int | 0 | Defines the value for IPv6 Traffic Class field (including 6 bit DSCP field) for IP packets originating from the Envoy listeners. Single value is applied to all listeners. The value must be in the range 0-255, 0 means socket option is not set. If listeners are bound to IPv4-only addresses, setting this option will cause an error. | + +### Circuit Breakers + +| Field Name | Type | Default | Description | +| --------------- | ------ | ------- | ----------------------------------------------------------------------------- | +| max-connections | int | 0 | The maximum number of connections that a single Envoy instance allows to the Kubernetes Service; defaults to 1024. | +| max-pending-requests | int | 0 | The maximum number of pending requests that a single Envoy instance allows to the Kubernetes Service; defaults to 1024. | +| max-requests | int | 0 | The maximum parallel requests a single Envoy instance allows to the Kubernetes Service; defaults to 1024 | +| max-retries | int | 0 | The maximum number of parallel retries a single Envoy instance allows to the Kubernetes Service; defaults to 3. This setting only makes sense if the cluster is configured to do retries.| + ### Configuration Example The following is an example ConfigMap with configuration file included: