config: add circuit breaker thresholds (#5650)

## Summary
Add a new `circuit_breaker_thresholds` option:

```yaml
circuit_breaker_thresholds:
  max_connections: 1
  max_pending_requests: 2
  max_requests: 3
  max_retries: 4
  max_connection_pools: 5
```

This option can be set at the global level or at the route level. Each
threshold is optional and when not set a default will be used. For
internal clusters we will disable the circuit breaker. For normal routes
we will use the envoy defaults.

## Related issues
-
[ENG-2310](https://linear.app/pomerium/issue/ENG-2310/add-circuit-breaker-settings-per-route)

## Checklist
- [x] reference any related issues
- [x] updated unit tests
- [x] add appropriate label (`enhancement`, `bug`, `breaking`,
`dependencies`, `ci`)
- [x] ready for review
This commit is contained in:
Caleb Doxsey 2025-06-16 09:38:39 -06:00 committed by GitHub
parent e320a532de
commit 5ac7ae9c26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1571 additions and 1127 deletions

6
.clang-format Normal file
View file

@ -0,0 +1,6 @@
---
Language: Proto
BasedOnStyle: Google
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: true
ColumnLimit: 0

View file

@ -85,7 +85,7 @@ func Test_populateLogEvent(t *testing.T) {
{log.AuthorizeLogFieldQuery, s, `{"query":"a=b"}`}, {log.AuthorizeLogFieldQuery, s, `{"query":"a=b"}`},
{log.AuthorizeLogFieldRemovedGroupsCount, s, `{"removed-groups-count":42}`}, {log.AuthorizeLogFieldRemovedGroupsCount, s, `{"removed-groups-count":42}`},
{log.AuthorizeLogFieldRequestID, s, `{"request-id":"REQUEST-ID"}`}, {log.AuthorizeLogFieldRequestID, s, `{"request-id":"REQUEST-ID"}`},
{log.AuthorizeLogFieldRouteChecksum, s, `{"route-checksum":14294378844626301341}`}, {log.AuthorizeLogFieldRouteChecksum, s, `{"route-checksum":7416256365460802121}`},
{log.AuthorizeLogFieldRouteID, s, `{"route-id":"POLICY-ID"}`}, {log.AuthorizeLogFieldRouteID, s, `{"route-id":"POLICY-ID"}`},
{log.AuthorizeLogFieldServiceAccountID, sa, `{"service-account-id":"SERVICE-ACCOUNT-ID"}`}, {log.AuthorizeLogFieldServiceAccountID, sa, `{"service-account-id":"SERVICE-ACCOUNT-ID"}`},
{log.AuthorizeLogFieldSessionID, s, `{"session-id":"SESSION-ID"}`}, {log.AuthorizeLogFieldSessionID, s, `{"session-id":"SESSION-ID"}`},

View file

@ -0,0 +1,56 @@
package config
import (
"github.com/volatiletech/null/v9"
configpb "github.com/pomerium/pomerium/pkg/grpc/config"
)
// CircuitBreakerThresholds define thresholds for circuit breaking.
type CircuitBreakerThresholds struct {
MaxConnections null.Uint32 `mapstructure:"max_connections" yaml:"max_connections,omitempty" json:"max_connections,omitempty"`
MaxPendingRequests null.Uint32 `mapstructure:"max_pending_requests" yaml:"max_pending_requests,omitempty" json:"max_pending_requests,omitempty"`
MaxRequests null.Uint32 `mapstructure:"max_requests" yaml:"max_requests,omitempty" json:"max_requests,omitempty"`
MaxRetries null.Uint32 `mapstructure:"max_retries" yaml:"max_retries,omitempty" json:"max_retries,omitempty"`
MaxConnectionPools null.Uint32 `mapstructure:"max_connection_pools" yaml:"max_connection_pools,omitempty" json:"max_connection_pools,omitempty"`
}
// CircuitBreakerThresholdsFromPB converts the CircuitBreakerThresholds from a protobuf type.
func CircuitBreakerThresholdsFromPB(src *configpb.CircuitBreakerThresholds) *CircuitBreakerThresholds {
if src == nil {
return nil
}
dst := &CircuitBreakerThresholds{}
if src.MaxConnections != nil {
dst.MaxConnections = null.Uint32From(*src.MaxConnections)
}
if src.MaxPendingRequests != nil {
dst.MaxPendingRequests = null.Uint32From(*src.MaxPendingRequests)
}
if src.MaxRequests != nil {
dst.MaxRequests = null.Uint32From(*src.MaxRequests)
}
if src.MaxRetries != nil {
dst.MaxRetries = null.Uint32From(*src.MaxRetries)
}
if src.MaxConnectionPools != nil {
dst.MaxConnectionPools = null.Uint32From(*src.MaxConnectionPools)
}
return dst
}
// CircuitBreakerThresholdsToPB converts the CircuitBreakerThresholds into a protobuf type.
func CircuitBreakerThresholdsToPB(src *CircuitBreakerThresholds) *configpb.CircuitBreakerThresholds {
if src == nil {
return nil
}
return &configpb.CircuitBreakerThresholds{
MaxConnections: src.MaxConnections.Ptr(),
MaxPendingRequests: src.MaxPendingRequests.Ptr(),
MaxRequests: src.MaxRequests.Ptr(),
MaxRetries: src.MaxRetries.Ptr(),
MaxConnectionPools: src.MaxConnectionPools.Ptr(),
}
}

View file

@ -37,6 +37,7 @@ var ViperPolicyHooks = viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
// parse base-64 encoded POLICY that is bound to environment variable // parse base-64 encoded POLICY that is bound to environment variable
DecodePolicyBase64Hook(), DecodePolicyBase64Hook(),
decodeNullBoolHookFunc(), decodeNullBoolHookFunc(),
decodeNullUint32HookFunc(),
decodeJWTClaimHeadersHookFunc(), decodeJWTClaimHeadersHookFunc(),
decodeBearerTokenFormatHookFunc(), decodeBearerTokenFormatHookFunc(),
decodeCodecTypeHookFunc(), decodeCodecTypeHookFunc(),

View file

@ -46,6 +46,25 @@ func decodeNullBoolHookFunc() mapstructure.DecodeHookFunc {
} }
} }
func decodeNullUint32HookFunc() mapstructure.DecodeHookFunc {
return func(_, t reflect.Type, data any) (any, error) {
if t != reflect.TypeOf(null.Uint32{}) {
return data, nil
}
bs, err := json.Marshal(data)
if err != nil {
return nil, err
}
var value null.Uint32
err = json.Unmarshal(bs, &value)
if err != nil {
return nil, err
}
return value, nil
}
}
// JWTClaimHeaders are headers to add to a request based on IDP claims. // JWTClaimHeaders are headers to add to a request based on IDP claims.
type JWTClaimHeaders map[string]string type JWTClaimHeaders map[string]string

View file

@ -260,6 +260,7 @@ func (b *Builder) BuildBootstrapStaticResources(
}, },
}, },
TypedExtensionProtocolOptions: buildTypedExtensionProtocolOptions(nil, upstreamProtocolHTTP2, Keepalive(false)), TypedExtensionProtocolOptions: buildTypedExtensionProtocolOptions(nil, upstreamProtocolHTTP2, Keepalive(false)),
CircuitBreakers: buildInternalCircuitBreakers(cfg),
} }
staticResources.Clusters = append(staticResources.Clusters, controlPlaneCluster) staticResources.Clusters = append(staticResources.Clusters, controlPlaneCluster)

View file

@ -71,6 +71,14 @@ func TestBuilder_BuildBootstrapStaticResources(t *testing.T) {
"name": "pomerium-control-plane-grpc", "name": "pomerium-control-plane-grpc",
"type": "STATIC", "type": "STATIC",
"connectTimeout": "5s", "connectTimeout": "5s",
"circuitBreakers": {
"thresholds": [{
"maxConnectionPools": 4294967295,
"maxConnections": 4294967295,
"maxPendingRequests": 4294967295,
"maxRequests": 4294967295
}]
},
"loadAssignment": { "loadAssignment": {
"clusterName": "pomerium-control-plane-grpc", "clusterName": "pomerium-control-plane-grpc",
"endpoints": [{ "endpoints": [{

View file

@ -0,0 +1,83 @@
package envoyconfig
import (
"math"
envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/pomerium/pomerium/config"
)
// unlimitedCircuitBreakersThreshold sets the circuit breaking thresholds to the maximum value, effectively disabling them
var unlimitedCircuitBreakersThreshold = &envoy_config_cluster_v3.CircuitBreakers_Thresholds{
Priority: envoy_config_core_v3.RoutingPriority_DEFAULT,
MaxConnections: wrapperspb.UInt32(math.MaxUint32),
MaxPendingRequests: wrapperspb.UInt32(math.MaxUint32),
MaxRequests: wrapperspb.UInt32(math.MaxUint32),
MaxConnectionPools: wrapperspb.UInt32(math.MaxUint32),
}
func buildInternalCircuitBreakers(cfg *config.Config) *envoy_config_cluster_v3.CircuitBreakers {
threshold := unlimitedCircuitBreakersThreshold
if cfg != nil && cfg.Options != nil {
threshold = buildCircuitBreakersThreshold(threshold, cfg.Options.CircuitBreakerThresholds)
}
if threshold == nil {
return nil
}
return &envoy_config_cluster_v3.CircuitBreakers{
Thresholds: []*envoy_config_cluster_v3.CircuitBreakers_Thresholds{threshold},
}
}
func buildRouteCircuitBreakers(cfg *config.Config, policy *config.Policy) *envoy_config_cluster_v3.CircuitBreakers {
threshold := (*envoy_config_cluster_v3.CircuitBreakers_Thresholds)(nil)
if cfg != nil && cfg.Options != nil {
threshold = buildCircuitBreakersThreshold(threshold, cfg.Options.CircuitBreakerThresholds)
}
if policy != nil {
threshold = buildCircuitBreakersThreshold(threshold, policy.CircuitBreakerThresholds)
}
if threshold == nil {
return nil
}
return &envoy_config_cluster_v3.CircuitBreakers{
Thresholds: []*envoy_config_cluster_v3.CircuitBreakers_Thresholds{threshold},
}
}
func buildCircuitBreakersThreshold(dst *envoy_config_cluster_v3.CircuitBreakers_Thresholds, src *config.CircuitBreakerThresholds) *envoy_config_cluster_v3.CircuitBreakers_Thresholds {
if src == nil {
return dst
}
if dst == nil {
dst = new(envoy_config_cluster_v3.CircuitBreakers_Thresholds)
} else {
dst = proto.CloneOf(dst)
}
if src.MaxConnections.IsSet() {
dst.MaxConnections = wrapperspb.UInt32(src.MaxConnections.Uint32)
}
if src.MaxPendingRequests.IsSet() {
dst.MaxPendingRequests = wrapperspb.UInt32(src.MaxPendingRequests.Uint32)
}
if src.MaxRequests.IsSet() {
dst.MaxRequests = wrapperspb.UInt32(src.MaxRequests.Uint32)
}
if src.MaxRetries.IsSet() {
dst.MaxRetries = wrapperspb.UInt32(src.MaxRetries.Uint32)
}
if src.MaxConnectionPools.IsSet() {
dst.MaxConnectionPools = wrapperspb.UInt32(src.MaxConnectionPools.Uint32)
}
return dst
}

View file

@ -152,6 +152,7 @@ func (b *Builder) buildInternalCluster(
if err := b.buildCluster(cluster, name, endpoints, upstreamProtocol, keepalive); err != nil { if err := b.buildCluster(cluster, name, endpoints, upstreamProtocol, keepalive); err != nil {
return nil, err return nil, err
} }
cluster.CircuitBreakers = buildInternalCircuitBreakers(cfg)
return cluster, nil return cluster, nil
} }
@ -205,6 +206,7 @@ func (b *Builder) buildPolicyCluster(ctx context.Context, cfg *config.Config, po
if err := b.buildCluster(cluster, name, endpoints, upstreamProtocol, Keepalive(false)); err != nil { if err := b.buildCluster(cluster, name, endpoints, upstreamProtocol, Keepalive(false)); err != nil {
return nil, err return nil, err
} }
cluster.CircuitBreakers = buildRouteCircuitBreakers(cfg, policy)
return cluster, nil return cluster, nil
} }

View file

@ -100,7 +100,7 @@ func TestBuilder_buildMainRouteConfiguration(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "4844561823473827050", "route_checksum": "3842393772597897044",
"route_id": "5fbd81d8f19363f4" "route_id": "5fbd81d8f19363f4"
} }
} }
@ -158,7 +158,7 @@ func TestBuilder_buildMainRouteConfiguration(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "4844561823473827050", "route_checksum": "3842393772597897044",
"route_id": "5fbd81d8f19363f4" "route_id": "5fbd81d8f19363f4"
} }
} }

View file

@ -405,14 +405,14 @@ func Test_buildPolicyRoutes(t *testing.T) {
8: "301084c3bd94c1ed", 8: "301084c3bd94c1ed",
} }
routeChecksums := []string{ routeChecksums := []string{
1: "1550825365439203068", 1: "12033848814082772143",
2: "1743754064510868859", 2: "17258478527403939037",
3: "5973469357905660470", 3: "8444406770556299357",
4: "10629393024495644652", 4: "4105318140299980592",
5: "17050190873730880526", 5: "10806704411058754499",
6: "4829848755825381466", 6: "3851124977091699412",
7: "7941222915300424536", 7: "4628039018923516807",
8: "8084661313119959810", 8: "2191103304818138823",
} }
b := &Builder{filemgr: filemgr.NewManager(), reproxy: reproxy.New()} b := &Builder{filemgr: filemgr.NewManager(), reproxy: reproxy.New()}
@ -1217,7 +1217,7 @@ func Test_buildPolicyRoutes(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "16194904053254305772", "route_checksum": "6606147546948811510",
"route_id": "98f90d58022ca963" "route_id": "98f90d58022ca963"
} }
} }
@ -1294,7 +1294,7 @@ func Test_buildPolicyRoutes(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "10734663134999137402", "route_checksum": "17040385240945756229",
"route_id": "81175a3a9df11dd8" "route_id": "81175a3a9df11dd8"
} }
} }
@ -1392,7 +1392,7 @@ func Test_buildPolicyRoutes(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "6431934433938620139", "route_checksum": "8569669228737044894",
"route_id": "ad0a23467bbdb773" "route_id": "ad0a23467bbdb773"
} }
} }
@ -1495,7 +1495,7 @@ func Test_buildPolicyRoutes(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "185312241489123079", "route_checksum": "17193711666127451236",
"route_id": "1013c6be524d7fbd" "route_id": "1013c6be524d7fbd"
} }
} }
@ -1611,7 +1611,7 @@ func Test_buildPolicyRoutes(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "10135716377738705841", "route_checksum": "5598645225002145886",
"route_id": "a81e6b1e66c1e2cd" "route_id": "a81e6b1e66c1e2cd"
} }
} }
@ -1746,7 +1746,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "14883526649905267120", "route_checksum": "12006715121823499564",
"route_id": "4d5ee69fcc359f45" "route_id": "4d5ee69fcc359f45"
} }
} }
@ -1822,7 +1822,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "10046758419081543299", "route_checksum": "16841768668336313196",
"route_id": "4d5ee69fcc359f45" "route_id": "4d5ee69fcc359f45"
} }
} }
@ -1903,7 +1903,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "2754443979682419848", "route_checksum": "4748097581015346493",
"route_id": "4d5ee69fcc359f45" "route_id": "4d5ee69fcc359f45"
} }
} }
@ -1979,7 +1979,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "1546259218106347577", "route_checksum": "17768398617143399322",
"route_id": "4d5ee69fcc359f45" "route_id": "4d5ee69fcc359f45"
} }
} }
@ -2055,7 +2055,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "8023363204239692999", "route_checksum": "16285305135509499281",
"route_id": "4d5ee69fcc359f45" "route_id": "4d5ee69fcc359f45"
} }
} }
@ -2136,7 +2136,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) {
"checkSettings": { "checkSettings": {
"contextExtensions": { "contextExtensions": {
"internal": "false", "internal": "false",
"route_checksum": "5173412791633652167", "route_checksum": "5072687206675243600",
"route_id": "4d5ee69fcc359f45" "route_id": "4d5ee69fcc359f45"
} }
} }

View file

@ -23,6 +23,16 @@
}, },
{ {
"connectTimeout": "10s", "connectTimeout": "10s",
"circuitBreakers": {
"thresholds": [
{
"maxConnectionPools": 4294967295,
"maxConnections": 4294967295,
"maxPendingRequests": 4294967295,
"maxRequests": 4294967295
}
]
},
"dnsLookupFamily": "V4_PREFERRED", "dnsLookupFamily": "V4_PREFERRED",
"loadAssignment": { "loadAssignment": {
"clusterName": "pomerium-control-plane-grpc", "clusterName": "pomerium-control-plane-grpc",
@ -71,6 +81,16 @@
}, },
{ {
"connectTimeout": "10s", "connectTimeout": "10s",
"circuitBreakers": {
"thresholds": [
{
"maxConnectionPools": 4294967295,
"maxConnections": 4294967295,
"maxPendingRequests": 4294967295,
"maxRequests": 4294967295
}
]
},
"dnsLookupFamily": "V4_PREFERRED", "dnsLookupFamily": "V4_PREFERRED",
"loadAssignment": { "loadAssignment": {
"clusterName": "pomerium-control-plane-http", "clusterName": "pomerium-control-plane-http",
@ -123,6 +143,16 @@
}, },
{ {
"connectTimeout": "10s", "connectTimeout": "10s",
"circuitBreakers": {
"thresholds": [
{
"maxConnectionPools": 4294967295,
"maxConnections": 4294967295,
"maxPendingRequests": 4294967295,
"maxRequests": 4294967295
}
]
},
"dnsLookupFamily": "V4_PREFERRED", "dnsLookupFamily": "V4_PREFERRED",
"loadAssignment": { "loadAssignment": {
"clusterName": "pomerium-control-plane-metrics", "clusterName": "pomerium-control-plane-metrics",
@ -175,6 +205,16 @@
}, },
{ {
"connectTimeout": "10s", "connectTimeout": "10s",
"circuitBreakers": {
"thresholds": [
{
"maxConnectionPools": 4294967295,
"maxConnections": 4294967295,
"maxPendingRequests": 4294967295,
"maxRequests": 4294967295
}
]
},
"dnsLookupFamily": "V4_PREFERRED", "dnsLookupFamily": "V4_PREFERRED",
"loadAssignment": { "loadAssignment": {
"clusterName": "pomerium-authorize", "clusterName": "pomerium-authorize",
@ -223,6 +263,16 @@
}, },
{ {
"connectTimeout": "10s", "connectTimeout": "10s",
"circuitBreakers": {
"thresholds": [
{
"maxConnectionPools": 4294967295,
"maxConnections": 4294967295,
"maxPendingRequests": 4294967295,
"maxRequests": 4294967295
}
]
},
"dnsLookupFamily": "V4_PREFERRED", "dnsLookupFamily": "V4_PREFERRED",
"loadAssignment": { "loadAssignment": {
"clusterName": "pomerium-databroker", "clusterName": "pomerium-databroker",

View file

@ -292,6 +292,7 @@ type Options struct {
RuntimeFlags RuntimeFlags `mapstructure:"runtime_flags" yaml:"runtime_flags,omitempty"` RuntimeFlags RuntimeFlags `mapstructure:"runtime_flags" yaml:"runtime_flags,omitempty"`
HTTP3AdvertisePort null.Uint32 `mapstructure:"-" yaml:"-" json:"-"` HTTP3AdvertisePort null.Uint32 `mapstructure:"-" yaml:"-" json:"-"`
CircuitBreakerThresholds *CircuitBreakerThresholds `mapstructure:"circuit_breaker_thresholds" yaml:"circuit_breaker_thresholds" json:"circuit_breaker_thresholds"`
} }
type certificateFilePair struct { type certificateFilePair struct {
@ -1593,6 +1594,9 @@ func (o *Options) ApplySettings(ctx context.Context, certsIndex *cryptutil.Certi
return RuntimeFlag(k), v return RuntimeFlag(k), v
}) })
o.HTTP3AdvertisePort = null.Uint32FromPtr(settings.Http3AdvertisePort) o.HTTP3AdvertisePort = null.Uint32FromPtr(settings.Http3AdvertisePort)
if settings.CircuitBreakerThresholds != nil {
o.CircuitBreakerThresholds = CircuitBreakerThresholdsFromPB(settings.CircuitBreakerThresholds)
}
} }
func (o *Options) ToProto() *config.Config { func (o *Options) ToProto() *config.Config {
@ -1720,6 +1724,9 @@ func (o *Options) ToProto() *config.Config {
return string(k), v return string(k), v
}) })
settings.Http3AdvertisePort = o.HTTP3AdvertisePort.Ptr() settings.Http3AdvertisePort = o.HTTP3AdvertisePort.Ptr()
if o.CircuitBreakerThresholds != nil {
settings.CircuitBreakerThresholds = CircuitBreakerThresholdsToPB(o.CircuitBreakerThresholds)
}
routes := make([]*config.Route, 0, o.NumPolicies()) routes := make([]*config.Route, 0, o.NumPolicies())
for p := range o.GetAllPolicies() { for p := range o.GetAllPolicies() {

View file

@ -32,6 +32,7 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/volatiletech/null/v9"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/fieldmaskpb" "google.golang.org/protobuf/types/known/fieldmaskpb"
@ -1035,6 +1036,21 @@ func TestOptions_ApplySettings(t *testing.T) {
assert.Equal(t, ptr([]string{"x", "y", "z"}), options.IDPAccessTokenAllowedAudiences, assert.Equal(t, ptr([]string{"x", "y", "z"}), options.IDPAccessTokenAllowedAudiences,
"should preserve idp access token allowed audiences") "should preserve idp access token allowed audiences")
}) })
t.Run("circuit_breaker_thresholds", func(t *testing.T) {
t.Parallel()
options := NewDefaultOptions()
assert.Nil(t, options.CircuitBreakerThresholds)
options.ApplySettings(ctx, nil, &configpb.Settings{
CircuitBreakerThresholds: &configpb.CircuitBreakerThresholds{
MaxConnections: proto.Uint32(3),
},
})
assert.Equal(t, &CircuitBreakerThresholds{MaxConnections: null.Uint32From(3)}, options.CircuitBreakerThresholds)
options.ApplySettings(ctx, nil, &configpb.Settings{})
assert.Equal(t, &CircuitBreakerThresholds{MaxConnections: null.Uint32From(3)}, options.CircuitBreakerThresholds,
"should not erase existing circuit breaker thresholds")
})
} }
func TestOptions_GetSetResponseHeaders(t *testing.T) { func TestOptions_GetSetResponseHeaders(t *testing.T) {

View file

@ -207,6 +207,7 @@ type Policy struct {
// MCP is an experimental support for Model Context Protocol upstreams // MCP is an experimental support for Model Context Protocol upstreams
MCP *MCP `mapstructure:"mcp" yaml:"mcp,omitempty" json:"mcp,omitempty"` MCP *MCP `mapstructure:"mcp" yaml:"mcp,omitempty" json:"mcp,omitempty"`
CircuitBreakerThresholds *CircuitBreakerThresholds `mapstructure:"circuit_breaker_thresholds" yaml:"circuit_breaker_thresholds,omitempty" json:"circuit_breaker_thresholds,omitempty"`
} }
// MCP is an experimental support for Model Context Protocol upstreams configuration // MCP is an experimental support for Model Context Protocol upstreams configuration
@ -345,6 +346,7 @@ func NewPolicyFromProto(pb *configpb.Route) (*Policy, error) {
AllowPublicUnauthenticatedAccess: pb.GetAllowPublicUnauthenticatedAccess(), AllowPublicUnauthenticatedAccess: pb.GetAllowPublicUnauthenticatedAccess(),
AllowSPDY: pb.GetAllowSpdy(), AllowSPDY: pb.GetAllowSpdy(),
AllowWebsockets: pb.GetAllowWebsockets(), AllowWebsockets: pb.GetAllowWebsockets(),
CircuitBreakerThresholds: CircuitBreakerThresholdsFromPB(pb.CircuitBreakerThresholds),
CORSAllowPreflight: pb.GetCorsAllowPreflight(), CORSAllowPreflight: pb.GetCorsAllowPreflight(),
Description: pb.GetDescription(), Description: pb.GetDescription(),
DependsOn: pb.GetDependsOn(), DependsOn: pb.GetDependsOn(),
@ -502,6 +504,7 @@ func (p *Policy) ToProto() (*configpb.Route, error) {
AllowPublicUnauthenticatedAccess: p.AllowPublicUnauthenticatedAccess, AllowPublicUnauthenticatedAccess: p.AllowPublicUnauthenticatedAccess,
AllowSpdy: p.AllowSPDY, AllowSpdy: p.AllowSPDY,
AllowWebsockets: p.AllowWebsockets, AllowWebsockets: p.AllowWebsockets,
CircuitBreakerThresholds: CircuitBreakerThresholdsToPB(p.CircuitBreakerThresholds),
CorsAllowPreflight: p.CORSAllowPreflight, CorsAllowPreflight: p.CORSAllowPreflight,
Description: p.Description, Description: p.Description,
DependsOn: p.DependsOn, DependsOn: p.DependsOn,

File diff suppressed because it is too large Load diff

View file

@ -55,7 +55,29 @@ enum BearerTokenFormat {
BEARER_TOKEN_FORMAT_IDP_IDENTITY_TOKEN = 3; BEARER_TOKEN_FORMAT_IDP_IDENTITY_TOKEN = 3;
} }
// Next ID: 73. // CircuitBreakerThresholds defines CircuitBreaker settings.
message CircuitBreakerThresholds {
// The maximum number of connections that Envoy will make to the upstream
// cluster. If not specified, the default is 1024.
optional uint32 max_connections = 1;
// The maximum number of pending requests that Envoy will allow to the
// upstream cluster. If not specified, the default is 1024. This limit is
// applied as a connection limit for non-HTTP traffic.
optional uint32 max_pending_requests = 2;
// The maximum number of parallel requests that Envoy will make to the
// upstream cluster. If not specified, the default is 1024. This limit does
// not apply to non-HTTP traffic.
optional uint32 max_requests = 3;
// The maximum number of parallel retries that Envoy will allow to the
// upstream cluster. If not specified, the default is 3.
optional uint32 max_retries = 4;
// The maximum number of connection pools per cluster that Envoy will
// concurrently support at once. If not specified, the default is unlimited.
// Set this for clusters which create a large number of connection pools.
optional uint32 max_connection_pools = 5;
}
// Next ID: 74.
message Route { message Route {
message StringList { message StringList {
repeated string values = 1; repeated string values = 1;
@ -149,6 +171,7 @@ message Route {
bool show_error_details = 59; bool show_error_details = 59;
optional MCP mcp = 72; optional MCP mcp = 72;
optional CircuitBreakerThresholds circuit_breaker_thresholds = 73;
} }
message MCP { message MCP {
@ -202,7 +225,7 @@ message Policy {
string remediation = 9; string remediation = 9;
} }
// Next ID: 140. // Next ID: 141.
message Settings { message Settings {
message Certificate { message Certificate {
bytes cert_bytes = 3; bytes cert_bytes = 3;
@ -314,7 +337,9 @@ message Settings {
optional string envoy_bind_config_source_address = 111; optional string envoy_bind_config_source_address = 111;
optional bool envoy_bind_config_freebind = 112; optional bool envoy_bind_config_freebind = 112;
repeated string programmatic_redirect_domain_whitelist = 68; repeated string programmatic_redirect_domain_whitelist = 68;
optional envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager.CodecType codec_type = 73; optional envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager.CodecType codec_type = 73;
// optional pomerium.crypt.PublicKeyEncryptionKey audit_key = 72; // optional pomerium.crypt.PublicKeyEncryptionKey audit_key = 72;
optional string primary_color = 85; optional string primary_color = 85;
optional string secondary_color = 86; optional string secondary_color = 86;
@ -326,6 +351,7 @@ message Settings {
optional bool pass_identity_headers = 117; optional bool pass_identity_headers = 117;
map<string, bool> runtime_flags = 118; map<string, bool> runtime_flags = 118;
optional uint32 http3_advertise_port = 136; optional uint32 http3_advertise_port = 136;
optional CircuitBreakerThresholds circuit_breaker_thresholds = 140;
} }
message DownstreamMtlsSettings { message DownstreamMtlsSettings {