From 7abb9631324a7907235ef5d9ef69149777c5b9d6 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sat, 15 Nov 2025 10:17:29 -0800 Subject: [PATCH 1/3] add admission control functionality Signed-off-by: Adam Buran --- api/v1alpha1/admission_control.go | 110 +++++++ api/v1alpha1/backendtrafficpolicy_types.go | 6 + api/v1alpha1/envoyproxy_types.go | 3 + api/v1alpha1/zz_generated.deepcopy.go | 297 +++++++++++++----- ....envoyproxy.io_backendtrafficpolicies.yaml | 94 ++++++ ....envoyproxy.io_backendtrafficpolicies.yaml | 94 ++++++ examples/kubernetes/admission-control.yaml | 64 ++++ internal/gatewayapi/backendtrafficpolicy.go | 42 +++ internal/ir/xds.go | 60 ++++ internal/ir/zz_generated.deepcopy.go | 135 ++++++++ internal/xds/translator/admission_control.go | 252 +++++++++++++++ .../xds/translator/admission_control_test.go | 95 ++++++ internal/xds/translator/httpfilters.go | 22 +- site/content/en/latest/api/extension_types.md | 82 +++++ 14 files changed, 1265 insertions(+), 91 deletions(-) create mode 100644 api/v1alpha1/admission_control.go create mode 100644 examples/kubernetes/admission-control.yaml create mode 100644 internal/xds/translator/admission_control.go create mode 100644 internal/xds/translator/admission_control_test.go diff --git a/api/v1alpha1/admission_control.go b/api/v1alpha1/admission_control.go new file mode 100644 index 00000000000..46ecd44fa96 --- /dev/null +++ b/api/v1alpha1/admission_control.go @@ -0,0 +1,110 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// AdmissionControl defines the admission control policy to be applied. +// This configuration probabilistically rejects requests based on the success rate +// of previous requests in a configurable sliding time window. +type AdmissionControl struct { + // Enabled enables or disables the admission control filter. + // Defaults to true if not specified. + // + // +optional + Enabled *bool `json:"enabled,omitempty"` + + // SamplingWindow defines the time window over which request success rates are calculated. + // Defaults to 60s if not specified. + // + // +optional + SamplingWindow *metav1.Duration `json:"samplingWindow,omitempty"` + + // SuccessRateThreshold defines the lowest request success rate at which the filter + // will not reject requests. The value should be in the range [0.0, 1.0]. + // Defaults to 0.95 (95%) if not specified. + // + // +optional + // +kubebuilder:validation:Minimum=0.0 + // +kubebuilder:validation:Maximum=1.0 + SuccessRateThreshold *float64 `json:"successRateThreshold,omitempty"` + + // Aggression controls the rejection probability curve. A value of 1.0 means a linear + // increase in rejection probability as the success rate decreases. Higher values + // result in more aggressive rejection at higher success rates. + // Defaults to 1.0 if not specified. + // + // +optional + // +kubebuilder:validation:Minimum=0.0 + Aggression *float64 `json:"aggression,omitempty"` + + // RPSThreshold defines the minimum requests per second below which requests will + // pass through the filter without rejection. Defaults to 1 if not specified. + // + // +optional + // +kubebuilder:validation:Minimum=0 + RPSThreshold *uint32 `json:"rpsThreshold,omitempty"` + + // MaxRejectionProbability represents the upper limit of the rejection probability. + // The value should be in the range [0.0, 1.0]. Defaults to 0.95 (95%) if not specified. + // + // +optional + // +kubebuilder:validation:Minimum=0.0 + // +kubebuilder:validation:Maximum=1.0 + MaxRejectionProbability *float64 `json:"maxRejectionProbability,omitempty"` + + // SuccessCriteria defines what constitutes a successful request for both HTTP and gRPC. + // + // +optional + SuccessCriteria *AdmissionControlSuccessCriteria `json:"successCriteria,omitempty"` +} + +// AdmissionControlSuccessCriteria defines the criteria for determining successful requests. +type AdmissionControlSuccessCriteria struct { + // HTTP defines success criteria for HTTP requests. + // + // +optional + HTTP *HTTPSuccessCriteria `json:"http,omitempty"` + + // GRPC defines success criteria for gRPC requests. + // + // +optional + GRPC *GRPCSuccessCriteria `json:"grpc,omitempty"` +} + +// HTTPSuccessCriteria defines success criteria for HTTP requests. +type HTTPSuccessCriteria struct { + // HTTPSuccessStatus defines ranges of HTTP status codes that are considered successful. + // Each range is inclusive on both ends. + // + // +optional + HTTPSuccessStatus []HTTPStatusRange `json:"httpSuccessStatus,omitempty"` +} + +// HTTPStatusRange defines a range of HTTP status codes. +type HTTPStatusRange struct { + // Start is the inclusive start of the status code range (100-600). + // + // +kubebuilder:validation:Minimum=100 + // +kubebuilder:validation:Maximum=600 + Start int32 `json:"start"` + + // End is the inclusive end of the status code range (100-600). + // + // +kubebuilder:validation:Minimum=100 + // +kubebuilder:validation:Maximum=600 + End int32 `json:"end"` +} + +// GRPCSuccessCriteria defines success criteria for gRPC requests. +type GRPCSuccessCriteria struct { + // GRPCSuccessStatus defines gRPC status codes that are considered successful. + // + // +optional + GRPCSuccessStatus []int32 `json:"grpcSuccessStatus,omitempty"` +} diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index 967985426bf..c303557f24e 100644 --- a/api/v1alpha1/backendtrafficpolicy_types.go +++ b/api/v1alpha1/backendtrafficpolicy_types.go @@ -64,6 +64,12 @@ type BackendTrafficPolicySpec struct { // +optional FaultInjection *FaultInjection `json:"faultInjection,omitempty"` + // AdmissionControl defines the admission control policy to be applied. This configuration + // probabilistically rejects requests based on the success rate of previous requests in a + // configurable sliding time window. + // +optional + AdmissionControl *AdmissionControl `json:"admissionControl,omitempty"` + // UseClientProtocol configures Envoy to prefer sending requests to backends using // the same HTTP protocol that the incoming request used. Defaults to false, which means // that Envoy will use the protocol indicated by the attached BackendRef. diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index 0db874ef3a8..48d0596232c 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -244,6 +244,9 @@ const ( // EnvoyFilterFault defines the Envoy HTTP fault filter. EnvoyFilterFault EnvoyFilter = "envoy.filters.http.fault" + // EnvoyFilterAdmissionControl defines the Envoy HTTP admission control filter. + EnvoyFilterAdmissionControl EnvoyFilter = "envoy.filters.http.admission_control" + // EnvoyFilterCORS defines the Envoy HTTP CORS filter. EnvoyFilterCORS EnvoyFilter = "envoy.filters.http.cors" diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2dd26de2064..6f85aa472ab 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -14,10 +14,10 @@ import ( "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" - "sigs.k8s.io/gateway-api/apis/v1" + apisv1 "sigs.k8s.io/gateway-api/apis/v1" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -81,7 +81,7 @@ func (in *APIKeyAuth) DeepCopyInto(out *APIKeyAuth) { *out = *in if in.CredentialRefs != nil { in, out := &in.CredentialRefs, &out.CredentialRefs - *out = make([]v1.SecretObjectReference, len(*in)) + *out = make([]apisv1.SecretObjectReference, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -124,17 +124,17 @@ func (in *ActiveHealthCheck) DeepCopyInto(out *ActiveHealthCheck) { *out = *in if in.Timeout != nil { in, out := &in.Timeout, &out.Timeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.Interval != nil { in, out := &in.Interval, &out.Interval - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.InitialJitter != nil { in, out := &in.InitialJitter, &out.InitialJitter - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.UnhealthyThreshold != nil { @@ -199,6 +199,81 @@ func (in *ActiveHealthCheckPayload) DeepCopy() *ActiveHealthCheckPayload { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdmissionControl) DeepCopyInto(out *AdmissionControl) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.SamplingWindow != nil { + in, out := &in.SamplingWindow, &out.SamplingWindow + *out = new(v1.Duration) + **out = **in + } + if in.SuccessRateThreshold != nil { + in, out := &in.SuccessRateThreshold, &out.SuccessRateThreshold + *out = new(float64) + **out = **in + } + if in.Aggression != nil { + in, out := &in.Aggression, &out.Aggression + *out = new(float64) + **out = **in + } + if in.RPSThreshold != nil { + in, out := &in.RPSThreshold, &out.RPSThreshold + *out = new(uint32) + **out = **in + } + if in.MaxRejectionProbability != nil { + in, out := &in.MaxRejectionProbability, &out.MaxRejectionProbability + *out = new(float64) + **out = **in + } + if in.SuccessCriteria != nil { + in, out := &in.SuccessCriteria, &out.SuccessCriteria + *out = new(AdmissionControlSuccessCriteria) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmissionControl. +func (in *AdmissionControl) DeepCopy() *AdmissionControl { + if in == nil { + return nil + } + out := new(AdmissionControl) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdmissionControlSuccessCriteria) DeepCopyInto(out *AdmissionControlSuccessCriteria) { + *out = *in + if in.HTTP != nil { + in, out := &in.HTTP, &out.HTTP + *out = new(HTTPSuccessCriteria) + (*in).DeepCopyInto(*out) + } + if in.GRPC != nil { + in, out := &in.GRPC, &out.GRPC + *out = new(GRPCSuccessCriteria) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmissionControlSuccessCriteria. +func (in *AdmissionControlSuccessCriteria) DeepCopy() *AdmissionControlSuccessCriteria { + if in == nil { + return nil + } + out := new(AdmissionControlSuccessCriteria) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Authorization) DeepCopyInto(out *Authorization) { *out = *in @@ -277,12 +352,12 @@ func (in *BackOffPolicy) DeepCopyInto(out *BackOffPolicy) { *out = *in if in.BaseInterval != nil { in, out := &in.BaseInterval, &out.BaseInterval - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.MaxInterval != nil { in, out := &in.MaxInterval, &out.MaxInterval - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -329,7 +404,7 @@ func (in *BackendCluster) DeepCopyInto(out *BackendCluster) { *out = *in if in.BackendRef != nil { in, out := &in.BackendRef, &out.BackendRef - *out = new(v1.BackendObjectReference) + *out = new(apisv1.BackendObjectReference) (*in).DeepCopyInto(*out) } if in.BackendRefs != nil { @@ -526,7 +601,7 @@ func (in *BackendStatus) DeepCopyInto(out *BackendStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -548,7 +623,7 @@ func (in *BackendTLSConfig) DeepCopyInto(out *BackendTLSConfig) { *out = *in if in.ClientCertificateRef != nil { in, out := &in.ClientCertificateRef, &out.ClientCertificateRef - *out = new(v1.SecretObjectReference) + *out = new(apisv1.SecretObjectReference) (*in).DeepCopyInto(*out) } in.TLSSettings.DeepCopyInto(&out.TLSSettings) @@ -569,12 +644,12 @@ func (in *BackendTLSSettings) DeepCopyInto(out *BackendTLSSettings) { *out = *in if in.CACertificateRefs != nil { in, out := &in.CACertificateRefs, &out.CACertificateRefs - *out = make([]v1.LocalObjectReference, len(*in)) + *out = make([]apisv1.LocalObjectReference, len(*in)) copy(*out, *in) } if in.WellKnownCACertificates != nil { in, out := &in.WellKnownCACertificates, &out.WellKnownCACertificates - *out = new(v1.WellKnownCACertificatesType) + *out = new(apisv1.WellKnownCACertificatesType) **out = **in } if in.InsecureSkipVerify != nil { @@ -584,7 +659,7 @@ func (in *BackendTLSSettings) DeepCopyInto(out *BackendTLSSettings) { } if in.SNI != nil { in, out := &in.SNI, &out.SNI - *out = new(v1.PreciseHostname) + *out = new(apisv1.PreciseHostname) **out = **in } if in.BackendTLSConfig != nil { @@ -703,6 +778,11 @@ func (in *BackendTrafficPolicySpec) DeepCopyInto(out *BackendTrafficPolicySpec) *out = new(FaultInjection) (*in).DeepCopyInto(*out) } + if in.AdmissionControl != nil { + in, out := &in.AdmissionControl, &out.AdmissionControl + *out = new(AdmissionControl) + (*in).DeepCopyInto(*out) + } if in.UseClientProtocol != nil { in, out := &in.UseClientProtocol, &out.UseClientProtocol *out = new(bool) @@ -850,7 +930,7 @@ func (in *CORS) DeepCopyInto(out *CORS) { } if in.MaxAge != nil { in, out := &in.MaxAge, &out.MaxAge - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.AllowCredentials != nil { @@ -1186,7 +1266,7 @@ func (in *ClientValidationContext) DeepCopyInto(out *ClientValidationContext) { *out = *in if in.CACertificateRefs != nil { in, out := &in.CACertificateRefs, &out.CACertificateRefs - *out = make([]v1.SecretObjectReference, len(*in)) + *out = make([]apisv1.SecretObjectReference, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1363,12 +1443,12 @@ func (in *ConnectionLimit) DeepCopyInto(out *ConnectionLimit) { *out = *in if in.CloseDelay != nil { in, out := &in.CloseDelay, &out.CloseDelay - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.MaxConnectionDuration != nil { in, out := &in.MaxConnectionDuration, &out.MaxConnectionDuration - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.MaxRequestsPerConnection != nil { @@ -1378,7 +1458,7 @@ func (in *ConnectionLimit) DeepCopyInto(out *ConnectionLimit) { } if in.MaxStreamDuration != nil { in, out := &in.MaxStreamDuration, &out.MaxStreamDuration - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -1439,7 +1519,7 @@ func (in *Cookie) DeepCopyInto(out *Cookie) { *out = *in if in.TTL != nil { in, out := &in.TTL, &out.TTL - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.Attributes != nil { @@ -1466,7 +1546,7 @@ func (in *CrlContext) DeepCopyInto(out *CrlContext) { *out = *in if in.Refs != nil { in, out := &in.Refs, &out.Refs - *out = make([]v1.SecretObjectReference, len(*in)) + *out = make([]apisv1.SecretObjectReference, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1518,17 +1598,17 @@ func (in *CustomRedirect) DeepCopyInto(out *CustomRedirect) { } if in.Hostname != nil { in, out := &in.Hostname, &out.Hostname - *out = new(v1.PreciseHostname) + *out = new(apisv1.PreciseHostname) **out = **in } if in.Path != nil { in, out := &in.Path, &out.Path - *out = new(v1.HTTPPathModifier) + *out = new(apisv1.HTTPPathModifier) (*in).DeepCopyInto(*out) } if in.Port != nil { in, out := &in.Port, &out.Port - *out = new(v1.PortNumber) + *out = new(apisv1.PortNumber) **out = **in } if in.StatusCode != nil { @@ -1568,7 +1648,7 @@ func (in *CustomResponse) DeepCopyInto(out *CustomResponse) { } if in.Header != nil { in, out := &in.Header, &out.Header - *out = new(v1.HTTPHeaderFilter) + *out = new(apisv1.HTTPHeaderFilter) (*in).DeepCopyInto(*out) } } @@ -1598,7 +1678,7 @@ func (in *CustomResponseBody) DeepCopyInto(out *CustomResponseBody) { } if in.ValueRef != nil { in, out := &in.ValueRef, &out.ValueRef - *out = new(v1.LocalObjectReference) + *out = new(apisv1.LocalObjectReference) **out = **in } } @@ -1670,7 +1750,7 @@ func (in *DNS) DeepCopyInto(out *DNS) { *out = *in if in.DNSRefreshRate != nil { in, out := &in.DNSRefreshRate, &out.DNSRefreshRate - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.RespectDNSTTL != nil { @@ -2059,7 +2139,7 @@ func (in *EnvoyGatewayKubernetesProvider) DeepCopyInto(out *EnvoyGatewayKubernet } if in.CacheSyncPeriod != nil { in, out := &in.CacheSyncPeriod, &out.CacheSyncPeriod - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -2153,12 +2233,12 @@ func (in *EnvoyGatewayOpenTelemetrySink) DeepCopyInto(out *EnvoyGatewayOpenTelem *out = *in if in.ExportInterval != nil { in, out := &in.ExportInterval, &out.ExportInterval - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.ExportTimeout != nil { in, out := &in.ExportTimeout, &out.ExportTimeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -2713,7 +2793,7 @@ func (in *ExtAuth) DeepCopyInto(out *ExtAuth) { } if in.Timeout != nil { in, out := &in.Timeout, &out.Timeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.FailOpen != nil { @@ -2744,7 +2824,7 @@ func (in *ExtProc) DeepCopyInto(out *ExtProc) { in.BackendCluster.DeepCopyInto(&out.BackendCluster) if in.MessageTimeout != nil { in, out := &in.MessageTimeout, &out.MessageTimeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.FailOpen != nil { @@ -2940,17 +3020,17 @@ func (in *ExtensionServiceRetry) DeepCopyInto(out *ExtensionServiceRetry) { } if in.InitialBackoff != nil { in, out := &in.InitialBackoff, &out.InitialBackoff - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.MaxBackoff != nil { in, out := &in.MaxBackoff, &out.MaxBackoff - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.BackoffMultiplier != nil { in, out := &in.BackoffMultiplier, &out.BackoffMultiplier - *out = new(v1.Fraction) + *out = new(apisv1.Fraction) (*in).DeepCopyInto(*out) } if in.RetryableStatusCodes != nil { @@ -2976,7 +3056,7 @@ func (in *ExtensionTLS) DeepCopyInto(out *ExtensionTLS) { in.CertificateRef.DeepCopyInto(&out.CertificateRef) if in.ClientCertificateRef != nil { in, out := &in.ClientCertificateRef, &out.ClientCertificateRef - *out = new(v1.SecretObjectReference) + *out = new(apisv1.SecretObjectReference) (*in).DeepCopyInto(*out) } } @@ -3096,7 +3176,7 @@ func (in *FaultInjectionDelay) DeepCopyInto(out *FaultInjectionDelay) { *out = *in if in.FixedDelay != nil { in, out := &in.FixedDelay, &out.FixedDelay - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.Percentage != nil { @@ -3212,6 +3292,26 @@ func (in *GRPCExtAuthService) DeepCopy() *GRPCExtAuthService { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GRPCSuccessCriteria) DeepCopyInto(out *GRPCSuccessCriteria) { + *out = *in + if in.GRPCSuccessStatus != nil { + in, out := &in.GRPCSuccessStatus, &out.GRPCSuccessStatus + *out = make([]int32, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GRPCSuccessCriteria. +func (in *GRPCSuccessCriteria) DeepCopy() *GRPCSuccessCriteria { + if in == nil { + return nil + } + out := new(GRPCSuccessCriteria) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Gateway) DeepCopyInto(out *Gateway) { *out = *in @@ -3424,17 +3524,17 @@ func (in *HTTPClientTimeout) DeepCopyInto(out *HTTPClientTimeout) { *out = *in if in.RequestReceivedTimeout != nil { in, out := &in.RequestReceivedTimeout, &out.RequestReceivedTimeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.IdleTimeout != nil { in, out := &in.IdleTimeout, &out.IdleTimeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.StreamIdleTimeout != nil { in, out := &in.StreamIdleTimeout, &out.StreamIdleTimeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -3495,7 +3595,7 @@ func (in *HTTPDirectResponseFilter) DeepCopyInto(out *HTTPDirectResponseFilter) } if in.Header != nil { in, out := &in.Header, &out.Header - *out = new(v1.HTTPHeaderFilter) + *out = new(apisv1.HTTPHeaderFilter) (*in).DeepCopyInto(*out) } } @@ -3541,12 +3641,12 @@ func (in *HTTPHeaderFilter) DeepCopyInto(out *HTTPHeaderFilter) { *out = *in if in.Set != nil { in, out := &in.Set, &out.Set - *out = make([]v1.HTTPHeader, len(*in)) + *out = make([]apisv1.HTTPHeader, len(*in)) copy(*out, *in) } if in.Add != nil { in, out := &in.Add, &out.Add - *out = make([]v1.HTTPHeader, len(*in)) + *out = make([]apisv1.HTTPHeader, len(*in)) copy(*out, *in) } if in.Remove != nil { @@ -3694,27 +3794,62 @@ func (in *HTTPRouteFilterSpec) DeepCopy() *HTTPRouteFilterSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPStatusRange) DeepCopyInto(out *HTTPStatusRange) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPStatusRange. +func (in *HTTPStatusRange) DeepCopy() *HTTPStatusRange { + if in == nil { + return nil + } + out := new(HTTPStatusRange) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPSuccessCriteria) DeepCopyInto(out *HTTPSuccessCriteria) { + *out = *in + if in.HTTPSuccessStatus != nil { + in, out := &in.HTTPSuccessStatus, &out.HTTPSuccessStatus + *out = make([]HTTPStatusRange, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPSuccessCriteria. +func (in *HTTPSuccessCriteria) DeepCopy() *HTTPSuccessCriteria { + if in == nil { + return nil + } + out := new(HTTPSuccessCriteria) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPTimeout) DeepCopyInto(out *HTTPTimeout) { *out = *in if in.ConnectionIdleTimeout != nil { in, out := &in.ConnectionIdleTimeout, &out.ConnectionIdleTimeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.MaxConnectionDuration != nil { in, out := &in.MaxConnectionDuration, &out.MaxConnectionDuration - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.RequestTimeout != nil { in, out := &in.RequestTimeout, &out.RequestTimeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.MaxStreamDuration != nil { in, out := &in.MaxStreamDuration, &out.MaxStreamDuration - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -3949,7 +4084,7 @@ func (in *ImageWasmCodeSource) DeepCopyInto(out *ImageWasmCodeSource) { } if in.PullSecretRef != nil { in, out := &in.PullSecretRef, &out.PullSecretRef - *out = new(v1.SecretObjectReference) + *out = new(apisv1.SecretObjectReference) (*in).DeepCopyInto(*out) } if in.TLS != nil { @@ -4676,7 +4811,7 @@ func (in *KubernetesWatchMode) DeepCopyInto(out *KubernetesWatchMode) { } if in.NamespaceSelector != nil { in, out := &in.NamespaceSelector, &out.NamespaceSelector - *out = new(metav1.LabelSelector) + *out = new(v1.LabelSelector) (*in).DeepCopyInto(*out) } } @@ -4696,17 +4831,17 @@ func (in *LeaderElection) DeepCopyInto(out *LeaderElection) { *out = *in if in.LeaseDuration != nil { in, out := &in.LeaseDuration, &out.LeaseDuration - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.RenewDeadline != nil { in, out := &in.RenewDeadline, &out.RenewDeadline - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.RetryPeriod != nil { in, out := &in.RetryPeriod, &out.RetryPeriod - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.Disable != nil { @@ -4811,7 +4946,7 @@ func (in *LocalJWKS) DeepCopyInto(out *LocalJWKS) { } if in.ValueRef != nil { in, out := &in.ValueRef, &out.ValueRef - *out = new(v1.LocalObjectReference) + *out = new(apisv1.LocalObjectReference) **out = **in } } @@ -4858,7 +4993,7 @@ func (in *Lua) DeepCopyInto(out *Lua) { } if in.ValueRef != nil { in, out := &in.ValueRef, &out.ValueRef - *out = new(v1.LocalObjectReference) + *out = new(apisv1.LocalObjectReference) **out = **in } } @@ -4904,7 +5039,7 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { } if in.ClientIDRef != nil { in, out := &in.ClientIDRef, &out.ClientIDRef - *out = new(v1.SecretObjectReference) + *out = new(apisv1.SecretObjectReference) (*in).DeepCopyInto(*out) } in.ClientSecret.DeepCopyInto(&out.ClientSecret) @@ -4955,7 +5090,7 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { } if in.DefaultTokenTTL != nil { in, out := &in.DefaultTokenTTL, &out.DefaultTokenTTL - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.RefreshToken != nil { @@ -4965,12 +5100,12 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { } if in.DefaultRefreshTokenTTL != nil { in, out := &in.DefaultRefreshTokenTTL, &out.DefaultRefreshTokenTTL - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.CSRFTokenTTL != nil { in, out := &in.CSRFTokenTTL, &out.CSRFTokenTTL - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.DisableTokenEncryption != nil { @@ -5142,7 +5277,7 @@ func (in *Operation) DeepCopyInto(out *Operation) { *out = *in if in.Methods != nil { in, out := &in.Methods, &out.Methods - *out = make([]v1.HTTPMethod, len(*in)) + *out = make([]apisv1.HTTPMethod, len(*in)) copy(*out, *in) } } @@ -5183,7 +5318,7 @@ func (in *PassiveHealthCheck) DeepCopyInto(out *PassiveHealthCheck) { } if in.Interval != nil { in, out := &in.Interval, &out.Interval - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.ConsecutiveLocalOriginFailures != nil { @@ -5203,7 +5338,7 @@ func (in *PassiveHealthCheck) DeepCopyInto(out *PassiveHealthCheck) { } if in.BaseEjectionTime != nil { in, out := &in.BaseEjectionTime, &out.BaseEjectionTime - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.MaxEjectionPercent != nil { @@ -5233,7 +5368,7 @@ func (in *PathMatch) DeepCopyInto(out *PathMatch) { *out = *in if in.Type != nil { in, out := &in.Type, &out.Type - *out = new(v1.PathMatchType) + *out = new(apisv1.PathMatchType) **out = **in } if in.Invert != nil { @@ -5303,7 +5438,7 @@ func (in *PerRetryPolicy) DeepCopyInto(out *PerRetryPolicy) { *out = *in if in.Timeout != nil { in, out := &in.Timeout, &out.Timeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.BackOff != nil { @@ -5328,12 +5463,12 @@ func (in *PolicyTargetReferences) DeepCopyInto(out *PolicyTargetReferences) { *out = *in if in.TargetRef != nil { in, out := &in.TargetRef, &out.TargetRef - *out = new(v1.LocalPolicyTargetReferenceWithSectionName) + *out = new(apisv1.LocalPolicyTargetReferenceWithSectionName) (*in).DeepCopyInto(*out) } if in.TargetRefs != nil { in, out := &in.TargetRefs, &out.TargetRefs - *out = make([]v1.LocalPolicyTargetReferenceWithSectionName, len(*in)) + *out = make([]apisv1.LocalPolicyTargetReferenceWithSectionName, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -5854,7 +5989,7 @@ func (in *ProxyTracing) DeepCopyInto(out *ProxyTracing) { } if in.SamplingFraction != nil { in, out := &in.SamplingFraction, &out.SamplingFraction - *out = new(v1.Fraction) + *out = new(apisv1.Fraction) (*in).DeepCopyInto(*out) } if in.CustomTags != nil { @@ -5883,7 +6018,7 @@ func (in *RateLimit) DeepCopyInto(out *RateLimit) { in.Backend.DeepCopyInto(&out.Backend) if in.Timeout != nil { in, out := &in.Timeout, &out.Timeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.Telemetry != nil { @@ -6235,7 +6370,7 @@ func (in *RedisTLSSettings) DeepCopyInto(out *RedisTLSSettings) { *out = *in if in.CertificateRef != nil { in, out := &in.CertificateRef, &out.CertificateRef - *out = new(v1.SecretObjectReference) + *out = new(apisv1.SecretObjectReference) (*in).DeepCopyInto(*out) } } @@ -6256,7 +6391,7 @@ func (in *RemoteJWKS) DeepCopyInto(out *RemoteJWKS) { in.BackendCluster.DeepCopyInto(&out.BackendCluster) if in.CacheDuration != nil { in, out := &in.CacheDuration, &out.CacheDuration - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -6633,12 +6768,12 @@ func (in *ShutdownConfig) DeepCopyInto(out *ShutdownConfig) { *out = *in if in.DrainTimeout != nil { in, out := &in.DrainTimeout, &out.DrainTimeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.MinDrainDuration != nil { in, out := &in.MinDrainDuration, &out.MinDrainDuration - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -6678,7 +6813,7 @@ func (in *SlowStart) DeepCopyInto(out *SlowStart) { *out = *in if in.Window != nil { in, out := &in.Window, &out.Window - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -6888,7 +7023,7 @@ func (in *TCPClientTimeout) DeepCopyInto(out *TCPClientTimeout) { *out = *in if in.IdleTimeout != nil { in, out := &in.IdleTimeout, &out.IdleTimeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -6913,12 +7048,12 @@ func (in *TCPKeepalive) DeepCopyInto(out *TCPKeepalive) { } if in.IdleTime != nil { in, out := &in.IdleTime, &out.IdleTime - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.Interval != nil { in, out := &in.Interval, &out.Interval - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -6938,7 +7073,7 @@ func (in *TCPTimeout) DeepCopyInto(out *TCPTimeout) { *out = *in if in.ConnectTimeout != nil { in, out := &in.ConnectTimeout, &out.ConnectTimeout - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } @@ -7003,7 +7138,7 @@ func (in *TargetSelector) DeepCopyInto(out *TargetSelector) { *out = *in if in.Group != nil { in, out := &in.Group, &out.Group - *out = new(v1.Group) + *out = new(apisv1.Group) **out = **in } if in.MatchLabels != nil { @@ -7015,7 +7150,7 @@ func (in *TargetSelector) DeepCopyInto(out *TargetSelector) { } if in.MatchExpressions != nil { in, out := &in.MatchExpressions, &out.MatchExpressions - *out = make([]metav1.LabelSelectorRequirement, len(*in)) + *out = make([]v1.LabelSelectorRequirement, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -7062,7 +7197,7 @@ func (in *Tracing) DeepCopyInto(out *Tracing) { *out = *in if in.SamplingFraction != nil { in, out := &in.SamplingFraction, &out.SamplingFraction - *out = new(v1.Fraction) + *out = new(apisv1.Fraction) (*in).DeepCopyInto(*out) } if in.CustomTags != nil { @@ -7277,12 +7412,12 @@ func (in *XDSServer) DeepCopyInto(out *XDSServer) { *out = *in if in.MaxConnectionAge != nil { in, out := &in.MaxConnectionAge, &out.MaxConnectionAge - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } if in.MaxConnectionAgeGrace != nil { in, out := &in.MaxConnectionAgeGrace, &out.MaxConnectionAgeGrace - *out = new(v1.Duration) + *out = new(apisv1.Duration) **out = **in } } diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index be726b94603..4c059f20b27 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -50,6 +50,100 @@ spec: spec: description: spec defines the desired state of BackendTrafficPolicy. properties: + admissionControl: + description: |- + AdmissionControl defines the admission control policy to be applied. This configuration + probabilistically rejects requests based on the success rate of previous requests in a + configurable sliding time window. + properties: + aggression: + description: |- + Aggression controls the rejection probability curve. A value of 1.0 means a linear + increase in rejection probability as the success rate decreases. Higher values + result in more aggressive rejection at higher success rates. + Defaults to 1.0 if not specified. + minimum: 0 + type: number + enabled: + description: |- + Enabled enables or disables the admission control filter. + Defaults to true if not specified. + type: boolean + maxRejectionProbability: + description: |- + MaxRejectionProbability represents the upper limit of the rejection probability. + The value should be in the range [0.0, 1.0]. Defaults to 0.95 (95%) if not specified. + maximum: 1 + minimum: 0 + type: number + rpsThreshold: + description: |- + RPSThreshold defines the minimum requests per second below which requests will + pass through the filter without rejection. Defaults to 1 if not specified. + format: int32 + minimum: 0 + type: integer + samplingWindow: + description: |- + SamplingWindow defines the time window over which request success rates are calculated. + Defaults to 60s if not specified. + type: string + successCriteria: + description: SuccessCriteria defines what constitutes a successful + request for both HTTP and gRPC. + properties: + grpc: + description: GRPC defines success criteria for gRPC requests. + properties: + grpcSuccessStatus: + description: GRPCSuccessStatus defines gRPC status codes + that are considered successful. + items: + format: int32 + type: integer + type: array + type: object + http: + description: HTTP defines success criteria for HTTP requests. + properties: + httpSuccessStatus: + description: |- + HTTPSuccessStatus defines ranges of HTTP status codes that are considered successful. + Each range is inclusive on both ends. + items: + description: HTTPStatusRange defines a range of HTTP + status codes. + properties: + end: + description: End is the inclusive end of the status + code range (100-600). + format: int32 + maximum: 600 + minimum: 100 + type: integer + start: + description: Start is the inclusive start of the + status code range (100-600). + format: int32 + maximum: 600 + minimum: 100 + type: integer + required: + - end + - start + type: object + type: array + type: object + type: object + successRateThreshold: + description: |- + SuccessRateThreshold defines the lowest request success rate at which the filter + will not reject requests. The value should be in the range [0.0, 1.0]. + Defaults to 0.95 (95%) if not specified. + maximum: 1 + minimum: 0 + type: number + type: object circuitBreaker: description: |- Circuit Breaker settings for the upstream connections and requests. diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index f0fa3b569ce..7369ddd10ee 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -49,6 +49,100 @@ spec: spec: description: spec defines the desired state of BackendTrafficPolicy. properties: + admissionControl: + description: |- + AdmissionControl defines the admission control policy to be applied. This configuration + probabilistically rejects requests based on the success rate of previous requests in a + configurable sliding time window. + properties: + aggression: + description: |- + Aggression controls the rejection probability curve. A value of 1.0 means a linear + increase in rejection probability as the success rate decreases. Higher values + result in more aggressive rejection at higher success rates. + Defaults to 1.0 if not specified. + minimum: 0 + type: number + enabled: + description: |- + Enabled enables or disables the admission control filter. + Defaults to true if not specified. + type: boolean + maxRejectionProbability: + description: |- + MaxRejectionProbability represents the upper limit of the rejection probability. + The value should be in the range [0.0, 1.0]. Defaults to 0.95 (95%) if not specified. + maximum: 1 + minimum: 0 + type: number + rpsThreshold: + description: |- + RPSThreshold defines the minimum requests per second below which requests will + pass through the filter without rejection. Defaults to 1 if not specified. + format: int32 + minimum: 0 + type: integer + samplingWindow: + description: |- + SamplingWindow defines the time window over which request success rates are calculated. + Defaults to 60s if not specified. + type: string + successCriteria: + description: SuccessCriteria defines what constitutes a successful + request for both HTTP and gRPC. + properties: + grpc: + description: GRPC defines success criteria for gRPC requests. + properties: + grpcSuccessStatus: + description: GRPCSuccessStatus defines gRPC status codes + that are considered successful. + items: + format: int32 + type: integer + type: array + type: object + http: + description: HTTP defines success criteria for HTTP requests. + properties: + httpSuccessStatus: + description: |- + HTTPSuccessStatus defines ranges of HTTP status codes that are considered successful. + Each range is inclusive on both ends. + items: + description: HTTPStatusRange defines a range of HTTP + status codes. + properties: + end: + description: End is the inclusive end of the status + code range (100-600). + format: int32 + maximum: 600 + minimum: 100 + type: integer + start: + description: Start is the inclusive start of the + status code range (100-600). + format: int32 + maximum: 600 + minimum: 100 + type: integer + required: + - end + - start + type: object + type: array + type: object + type: object + successRateThreshold: + description: |- + SuccessRateThreshold defines the lowest request success rate at which the filter + will not reject requests. The value should be in the range [0.0, 1.0]. + Defaults to 0.95 (95%) if not specified. + maximum: 1 + minimum: 0 + type: number + type: object circuitBreaker: description: |- Circuit Breaker settings for the upstream connections and requests. diff --git a/examples/kubernetes/admission-control.yaml b/examples/kubernetes/admission-control.yaml new file mode 100644 index 00000000000..d386768cf27 --- /dev/null +++ b/examples/kubernetes/admission-control.yaml @@ -0,0 +1,64 @@ +# Copyright Envoy Gateway Authors +# SPDX-License-Identifier: Apache-2.0 +# The full text of the Apache license is available in the LICENSE file at +# the root of the repo. + +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: admission-control-policy + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: example-route + namespace: default + admissionControl: + enabled: true + samplingWindow: 30s + srThreshold: 0.95 + aggression: 1.0 + rpsThreshold: 5.0 + maxRejectionProbability: 0.8 + successCriteria: + http: + httpSuccessStatus: + - start: 200 + end: 299 + grpc: + grpcSuccessStatus: + - start: 0 + end: 0 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: example-route + namespace: default +spec: + parentRefs: + - name: eg + namespace: default + hostnames: + - "example.com" + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: backend-service + port: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: backend-service + namespace: default +spec: + ports: + - port: 80 + targetPort: 8080 + selector: + app: backend diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index a339d3b8c8d..372a788f129 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -822,6 +822,7 @@ func (t *Translator) buildTrafficFeatures(policy *egv1a1.BackendTrafficPolicy, r hc *ir.HealthCheck cb *ir.CircuitBreaker fi *ir.FaultInjection + ac *ir.AdmissionControl to *ir.Timeout ka *ir.TCPKeepalive rt *ir.Retry @@ -854,6 +855,9 @@ func (t *Translator) buildTrafficFeatures(policy *egv1a1.BackendTrafficPolicy, r if policy.Spec.FaultInjection != nil { fi = t.buildFaultInjection(policy) } + if policy.Spec.AdmissionControl != nil { + ac = t.buildAdmissionControl(policy) + } if ka, err = buildTCPKeepAlive(&policy.Spec.ClusterSettings); err != nil { err = perr.WithMessage(err, "TCPKeepalive") errs = errors.Join(errs, err) @@ -901,6 +905,7 @@ func (t *Translator) buildTrafficFeatures(policy *egv1a1.BackendTrafficPolicy, r HealthCheck: hc, CircuitBreaker: cb, FaultInjection: fi, + AdmissionControl: ac, TCPKeepalive: ka, Retry: rt, BackendConnection: bc, @@ -1362,6 +1367,43 @@ func (t *Translator) buildFaultInjection(policy *egv1a1.BackendTrafficPolicy) *i return fi } +func (t *Translator) buildAdmissionControl(policy *egv1a1.BackendTrafficPolicy) *ir.AdmissionControl { + if policy.Spec.AdmissionControl == nil { + return nil + } + + ac := &ir.AdmissionControl{ + Enabled: policy.Spec.AdmissionControl.Enabled, + SamplingWindow: policy.Spec.AdmissionControl.SamplingWindow, + SuccessRateThreshold: policy.Spec.AdmissionControl.SuccessRateThreshold, + Aggression: policy.Spec.AdmissionControl.Aggression, + RPSThreshold: policy.Spec.AdmissionControl.RPSThreshold, + MaxRejectionProbability: policy.Spec.AdmissionControl.MaxRejectionProbability, + } + + if policy.Spec.AdmissionControl.SuccessCriteria != nil { + ac.SuccessCriteria = &ir.AdmissionControlSuccessCriteria{} + + if policy.Spec.AdmissionControl.SuccessCriteria.HTTP != nil { + ac.SuccessCriteria.HTTP = &ir.HTTPSuccessCriteria{} + for _, statusRange := range policy.Spec.AdmissionControl.SuccessCriteria.HTTP.HTTPSuccessStatus { + ac.SuccessCriteria.HTTP.HTTPSuccessStatus = append(ac.SuccessCriteria.HTTP.HTTPSuccessStatus, ir.HTTPStatusRange{ + Start: statusRange.Start, + End: statusRange.End, + }) + } + } + + if policy.Spec.AdmissionControl.SuccessCriteria.GRPC != nil { + ac.SuccessCriteria.GRPC = &ir.GRPCSuccessCriteria{ + GRPCSuccessStatus: policy.Spec.AdmissionControl.SuccessCriteria.GRPC.GRPCSuccessStatus, + } + } + } + + return ac +} + func makeIrStatusSet(in []egv1a1.HTTPStatus) []ir.HTTPStatus { statusSet := sets.NewInt() for _, r := range in { diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 2e801c4dec0..c7e91fbfcdb 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -905,6 +905,8 @@ type TrafficFeatures struct { HealthCheck *HealthCheck `json:"healthCheck,omitempty" yaml:"healthCheck,omitempty"` // FaultInjection defines the schema for injecting faults into HTTP requests. FaultInjection *FaultInjection `json:"faultInjection,omitempty" yaml:"faultInjection,omitempty"` + // AdmissionControl defines the schema for admission control based on success rate. + AdmissionControl *AdmissionControl `json:"admissionControl,omitempty" yaml:"admissionControl,omitempty"` // Circuit Breaker Settings CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty" yaml:"circuitBreaker,omitempty"` // Request and connection timeout settings @@ -1453,6 +1455,64 @@ type FaultInjectionAbort struct { Percentage *float32 `json:"percentage,omitempty" yaml:"percentage,omitempty"` } +// AdmissionControl defines the schema for admission control based on success rate. +// +// +k8s:deepcopy-gen=true +type AdmissionControl struct { + // Enabled enables or disables the admission control filter. + Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + // SamplingWindow defines the time window over which request success rates are calculated. + SamplingWindow *metav1.Duration `json:"samplingWindow,omitempty" yaml:"samplingWindow,omitempty"` + // SuccessRateThreshold defines the lowest request success rate at which the filter + // will not reject requests. The value should be in the range [0.0, 1.0]. + SuccessRateThreshold *float64 `json:"successRateThreshold,omitempty" yaml:"successRateThreshold,omitempty"` + // Aggression controls the rejection probability curve. + Aggression *float64 `json:"aggression,omitempty" yaml:"aggression,omitempty"` + // RPSThreshold defines the minimum requests per second below which requests will + // pass through the filter without rejection. + RPSThreshold *uint32 `json:"rpsThreshold,omitempty" yaml:"rpsThreshold,omitempty"` + // MaxRejectionProbability represents the upper limit of the rejection probability. + MaxRejectionProbability *float64 `json:"maxRejectionProbability,omitempty" yaml:"maxRejectionProbability,omitempty"` + // SuccessCriteria defines what constitutes a successful request for both HTTP and gRPC. + SuccessCriteria *AdmissionControlSuccessCriteria `json:"successCriteria,omitempty" yaml:"successCriteria,omitempty"` +} + +// AdmissionControlSuccessCriteria defines the criteria for determining successful requests. +// +// +k8s:deepcopy-gen=true +type AdmissionControlSuccessCriteria struct { + // HTTP defines success criteria for HTTP requests. + HTTP *HTTPSuccessCriteria `json:"http,omitempty" yaml:"http,omitempty"` + // GRPC defines success criteria for gRPC requests. + GRPC *GRPCSuccessCriteria `json:"grpc,omitempty" yaml:"grpc,omitempty"` +} + +// HTTPSuccessCriteria defines success criteria for HTTP requests. +// +// +k8s:deepcopy-gen=true +type HTTPSuccessCriteria struct { + // HTTPSuccessStatus defines ranges of HTTP status codes that are considered successful. + HTTPSuccessStatus []HTTPStatusRange `json:"httpSuccessStatus,omitempty" yaml:"httpSuccessStatus,omitempty"` +} + +// HTTPStatusRange defines a range of HTTP status codes. +// +// +k8s:deepcopy-gen=true +type HTTPStatusRange struct { + // Start is the inclusive start of the status code range. + Start int32 `json:"start" yaml:"start"` + // End is the inclusive end of the status code range. + End int32 `json:"end" yaml:"end"` +} + +// GRPCSuccessCriteria defines success criteria for gRPC requests. +// +// +k8s:deepcopy-gen=true +type GRPCSuccessCriteria struct { + // GRPCSuccessStatus defines gRPC status codes that are considered successful. + GRPCSuccessStatus []int32 `json:"grpcSuccessStatus,omitempty" yaml:"grpcSuccessStatus,omitempty"` +} + // MirrorPolicy specifies a destination to mirror traffic in addition // to the original destination // diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index f1e4d5addca..2df47610d39 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -280,6 +280,81 @@ func (in *AddHeader) DeepCopy() *AddHeader { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdmissionControl) DeepCopyInto(out *AdmissionControl) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.SamplingWindow != nil { + in, out := &in.SamplingWindow, &out.SamplingWindow + *out = new(v1.Duration) + **out = **in + } + if in.SuccessRateThreshold != nil { + in, out := &in.SuccessRateThreshold, &out.SuccessRateThreshold + *out = new(float64) + **out = **in + } + if in.Aggression != nil { + in, out := &in.Aggression, &out.Aggression + *out = new(float64) + **out = **in + } + if in.RPSThreshold != nil { + in, out := &in.RPSThreshold, &out.RPSThreshold + *out = new(uint32) + **out = **in + } + if in.MaxRejectionProbability != nil { + in, out := &in.MaxRejectionProbability, &out.MaxRejectionProbability + *out = new(float64) + **out = **in + } + if in.SuccessCriteria != nil { + in, out := &in.SuccessCriteria, &out.SuccessCriteria + *out = new(AdmissionControlSuccessCriteria) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmissionControl. +func (in *AdmissionControl) DeepCopy() *AdmissionControl { + if in == nil { + return nil + } + out := new(AdmissionControl) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdmissionControlSuccessCriteria) DeepCopyInto(out *AdmissionControlSuccessCriteria) { + *out = *in + if in.HTTP != nil { + in, out := &in.HTTP, &out.HTTP + *out = new(HTTPSuccessCriteria) + (*in).DeepCopyInto(*out) + } + if in.GRPC != nil { + in, out := &in.GRPC, &out.GRPC + *out = new(GRPCSuccessCriteria) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmissionControlSuccessCriteria. +func (in *AdmissionControlSuccessCriteria) DeepCopy() *AdmissionControlSuccessCriteria { + if in == nil { + return nil + } + out := new(AdmissionControlSuccessCriteria) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Authorization) DeepCopyInto(out *Authorization) { *out = *in @@ -1442,6 +1517,26 @@ func (in *GRPCHealthChecker) DeepCopy() *GRPCHealthChecker { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GRPCSuccessCriteria) DeepCopyInto(out *GRPCSuccessCriteria) { + *out = *in + if in.GRPCSuccessStatus != nil { + in, out := &in.GRPCSuccessStatus, &out.GRPCSuccessStatus + *out = make([]int32, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GRPCSuccessCriteria. +func (in *GRPCSuccessCriteria) DeepCopy() *GRPCSuccessCriteria { + if in == nil { + return nil + } + out := new(GRPCSuccessCriteria) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GlobalRateLimit) DeepCopyInto(out *GlobalRateLimit) { *out = *in @@ -1950,6 +2045,41 @@ func (in *HTTPRoute) DeepCopy() *HTTPRoute { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPStatusRange) DeepCopyInto(out *HTTPStatusRange) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPStatusRange. +func (in *HTTPStatusRange) DeepCopy() *HTTPStatusRange { + if in == nil { + return nil + } + out := new(HTTPStatusRange) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPSuccessCriteria) DeepCopyInto(out *HTTPSuccessCriteria) { + *out = *in + if in.HTTPSuccessStatus != nil { + in, out := &in.HTTPSuccessStatus, &out.HTTPSuccessStatus + *out = make([]HTTPStatusRange, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPSuccessCriteria. +func (in *HTTPSuccessCriteria) DeepCopy() *HTTPSuccessCriteria { + if in == nil { + return nil + } + out := new(HTTPSuccessCriteria) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPTimeout) DeepCopyInto(out *HTTPTimeout) { *out = *in @@ -4187,6 +4317,11 @@ func (in *TrafficFeatures) DeepCopyInto(out *TrafficFeatures) { *out = new(FaultInjection) (*in).DeepCopyInto(*out) } + if in.AdmissionControl != nil { + in, out := &in.AdmissionControl, &out.AdmissionControl + *out = new(AdmissionControl) + (*in).DeepCopyInto(*out) + } if in.CircuitBreaker != nil { in, out := &in.CircuitBreaker, &out.CircuitBreaker *out = new(CircuitBreaker) diff --git a/internal/xds/translator/admission_control.go b/internal/xds/translator/admission_control.go new file mode 100644 index 00000000000..2d2afed3510 --- /dev/null +++ b/internal/xds/translator/admission_control.go @@ -0,0 +1,252 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "errors" + "fmt" + "time" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + admissioncontrolv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/admission_control/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/wrapperspb" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/utils/proto" + "github.com/envoyproxy/gateway/internal/xds/types" +) + +func init() { + registerHTTPFilter(&admissionControl{}) +} + +type admissionControl struct{} + +var _ httpFilter = &admissionControl{} + +// patchHCM builds and appends the admission control filter to the HTTP Connection Manager +// if applicable, and it does not already exist. +func (*admissionControl) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { + if mgr == nil { + return errors.New("hcm is nil") + } + + if irListener == nil { + return errors.New("ir listener is nil") + } + + if !listenerContainsAdmissionControl(irListener) { + return nil + } + + // Return early if the admission control filter already exists. + for _, existingFilter := range mgr.HttpFilters { + if existingFilter.Name == string(egv1a1.EnvoyFilterAdmissionControl) { + return nil + } + } + + admissionControlFilter, err := buildHCMAdmissionControlFilter() + if err != nil { + return err + } + mgr.HttpFilters = append(mgr.HttpFilters, admissionControlFilter) + + return nil +} + +// buildHCMAdmissionControlFilter returns a basic admission control HTTP filter. +func buildHCMAdmissionControlFilter() (*hcmv3.HttpFilter, error) { + // Create a basic admission control configuration + admissionControlProto := &admissioncontrolv3.AdmissionControl{} + + admissionControlAny, err := proto.ToAnyWithValidation(admissionControlProto) + if err != nil { + return nil, err + } + + return &hcmv3.HttpFilter{ + Name: string(egv1a1.EnvoyFilterAdmissionControl), + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: admissionControlAny, + }, + }, nil +} + +// patchRoute patches the provided route with the admission control config if applicable. +func (*admissionControl) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute, httpListener *ir.HTTPListener) error { + if route == nil || irRoute == nil { + return nil + } + + // Check if admission control is configured for this route + if irRoute.Traffic == nil || irRoute.Traffic.AdmissionControl == nil { + return nil + } + + admissionControlConfig := irRoute.Traffic.AdmissionControl + + // Skip if admission control is explicitly disabled + if admissionControlConfig.Enabled != nil && !*admissionControlConfig.Enabled { + return nil + } + + // Build the admission control configuration + routeCfgProto, err := buildAdmissionControlConfig(admissionControlConfig) + if err != nil { + return err + } + + // Add the admission control filter to the route + if route.TypedPerFilterConfig == nil { + route.TypedPerFilterConfig = make(map[string]*anypb.Any) + } + + routeCfgAny, err := proto.ToAnyWithValidation(routeCfgProto) + if err != nil { + return err + } + + route.TypedPerFilterConfig[string(egv1a1.EnvoyFilterAdmissionControl)] = routeCfgAny + + return nil +} + +// buildAdmissionControlConfig builds the admission control configuration from the IR. +func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admissioncontrolv3.AdmissionControl, error) { + if admissionControl == nil { + return nil, errors.New("admissionControl cannot be nil") + } + + config := &admissioncontrolv3.AdmissionControl{} + + // Set enabled (defaults to true if not specified) + enabled := true + if admissionControl.Enabled != nil { + enabled = *admissionControl.Enabled + } + config.Enabled = &corev3.RuntimeFeatureFlag{ + DefaultValue: &wrapperspb.BoolValue{Value: enabled}, + } + + // Set sampling window (defaults to 60s if not specified) + samplingWindow := "60s" + if admissionControl.SamplingWindow != nil { + samplingWindow = admissionControl.SamplingWindow.Duration.String() + } + duration, err := parseDuration(samplingWindow) + if err != nil { + return nil, fmt.Errorf("invalid samplingWindow: %w", err) + } + config.SamplingWindow = durationpb.New(duration) + + // Set success rate threshold (defaults to 0.95 if not specified) + // Note: srThreshold is in range [0.0, 1.0], but Percent expects [0.0, 100.0] + srThreshold := 0.95 + if admissionControl.SuccessRateThreshold != nil { + srThreshold = *admissionControl.SuccessRateThreshold + } + config.SrThreshold = &corev3.RuntimePercent{ + DefaultValue: &typev3.Percent{Value: srThreshold * 100.0}, + } + + // Set aggression (defaults to 1.0 if not specified) + aggression := 1.0 + if admissionControl.Aggression != nil { + aggression = *admissionControl.Aggression + } + config.Aggression = &corev3.RuntimeDouble{ + DefaultValue: aggression, + } + + // Set RPS threshold (defaults to 1 if not specified) + rpsThreshold := uint32(1) + if admissionControl.RPSThreshold != nil { + rpsThreshold = *admissionControl.RPSThreshold + } + config.RpsThreshold = &corev3.RuntimeUInt32{ + DefaultValue: rpsThreshold, + } + + // Set max rejection probability (defaults to 0.95 if not specified) + // Note: maxRejectionProbability is in range [0.0, 1.0], but Percent expects [0.0, 100.0] + maxRejectionProbability := 0.95 + if admissionControl.MaxRejectionProbability != nil { + maxRejectionProbability = *admissionControl.MaxRejectionProbability + } + config.MaxRejectionProbability = &corev3.RuntimePercent{ + DefaultValue: &typev3.Percent{Value: maxRejectionProbability * 100.0}, + } + + // Set success criteria (part of EvaluationCriteria oneof) + if admissionControl.SuccessCriteria != nil { + successCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria{} + + // HTTP success criteria + if admissionControl.SuccessCriteria.HTTP != nil && len(admissionControl.SuccessCriteria.HTTP.HTTPSuccessStatus) > 0 { + httpCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria_HttpCriteria{} + for _, statusRange := range admissionControl.SuccessCriteria.HTTP.HTTPSuccessStatus { + httpCriteria.HttpSuccessStatus = append(httpCriteria.HttpSuccessStatus, &typev3.Int32Range{ + Start: statusRange.Start, + End: statusRange.End, + }) + } + successCriteria.HttpCriteria = httpCriteria + } + + // gRPC success criteria + if admissionControl.SuccessCriteria.GRPC != nil && len(admissionControl.SuccessCriteria.GRPC.GRPCSuccessStatus) > 0 { + grpcCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria_GrpcCriteria{} + for _, status := range admissionControl.SuccessCriteria.GRPC.GRPCSuccessStatus { + grpcCriteria.GrpcSuccessStatus = append(grpcCriteria.GrpcSuccessStatus, uint32(status)) + } + successCriteria.GrpcCriteria = grpcCriteria + } + + // Set as EvaluationCriteria (oneof field) + config.EvaluationCriteria = &admissioncontrolv3.AdmissionControl_SuccessCriteria_{ + SuccessCriteria: successCriteria, + } + } + + return config, nil +} + +// parseDuration parses a duration string and returns a time.Duration. +func parseDuration(s string) (time.Duration, error) { + return time.ParseDuration(s) +} + +// patchResources adds all the other needed resources referenced by this filter. +func (*admissionControl) patchResources(tCtx *types.ResourceVersionTable, routes []*ir.HTTPRoute) error { + // Admission control filter doesn't require additional resources + return nil +} + +// listenerContainsAdmissionControl returns true if the provided listener contains +// any route with admission control configured. +func listenerContainsAdmissionControl(irListener *ir.HTTPListener) bool { + if irListener == nil { + return false + } + + for _, route := range irListener.Routes { + if route.Traffic != nil && route.Traffic.AdmissionControl != nil { + // Check if enabled (defaults to true) + if route.Traffic.AdmissionControl.Enabled == nil || *route.Traffic.AdmissionControl.Enabled { + return true + } + } + } + + return false +} diff --git a/internal/xds/translator/admission_control_test.go b/internal/xds/translator/admission_control_test.go new file mode 100644 index 00000000000..da5a26c77b4 --- /dev/null +++ b/internal/xds/translator/admission_control_test.go @@ -0,0 +1,95 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/envoyproxy/gateway/internal/ir" +) + +func TestAdmissionControlFilter(t *testing.T) { + tests := []struct { + name string + listener *ir.HTTPListener + want bool + }{ + { + name: "listener with admission control", + listener: &ir.HTTPListener{ + Routes: []*ir.HTTPRoute{ + { + Traffic: &ir.TrafficFeatures{ + AdmissionControl: &ir.AdmissionControl{ + Enabled: func() *bool { b := true; return &b }(), + }, + }, + }, + }, + }, + want: true, + }, + { + name: "listener without admission control", + listener: &ir.HTTPListener{ + Routes: []*ir.HTTPRoute{ + { + Traffic: &ir.TrafficFeatures{}, + }, + }, + }, + want: false, + }, + { + name: "nil listener", + listener: nil, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := listenerContainsAdmissionControl(tt.listener) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestBuildAdmissionControlConfig(t *testing.T) { + tests := []struct { + name string + config *ir.AdmissionControl + wantErr bool + }{ + { + name: "valid admission control config", + config: &ir.AdmissionControl{ + Enabled: func() *bool { b := true; return &b }(), + }, + wantErr: false, + }, + { + name: "nil config", + config: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := buildAdmissionControlConfig(tt.config) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotNil(t, got) + }) + } +} diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go index dfcf73bf60f..478c5242fb9 100644 --- a/internal/xds/translator/httpfilters.go +++ b/internal/xds/translator/httpfilters.go @@ -102,27 +102,29 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter { order = 0 case isFilterType(filter, egv1a1.EnvoyFilterFault): order = 1 - case isFilterType(filter, egv1a1.EnvoyFilterCORS): + case isFilterType(filter, egv1a1.EnvoyFilterAdmissionControl): order = 2 + case isFilterType(filter, egv1a1.EnvoyFilterCORS): + order = 3 case isFilterType(filter, egv1a1.EnvoyFilterHeaderMutation): // Ensure header mutation run before ext auth which might consume the header. - order = 3 - case isFilterType(filter, egv1a1.EnvoyFilterExtAuthz): order = 4 - case isFilterType(filter, egv1a1.EnvoyFilterAPIKeyAuth): + case isFilterType(filter, egv1a1.EnvoyFilterExtAuthz): order = 5 - case isFilterType(filter, egv1a1.EnvoyFilterBasicAuth): + case isFilterType(filter, egv1a1.EnvoyFilterAPIKeyAuth): order = 6 - case isFilterType(filter, egv1a1.EnvoyFilterOAuth2): + case isFilterType(filter, egv1a1.EnvoyFilterBasicAuth): order = 7 - case isFilterType(filter, egv1a1.EnvoyFilterJWTAuthn): + case isFilterType(filter, egv1a1.EnvoyFilterOAuth2): order = 8 - case isFilterType(filter, egv1a1.EnvoyFilterSessionPersistence): + case isFilterType(filter, egv1a1.EnvoyFilterJWTAuthn): order = 9 - case isFilterType(filter, egv1a1.EnvoyFilterBuffer): + case isFilterType(filter, egv1a1.EnvoyFilterSessionPersistence): order = 10 + case isFilterType(filter, egv1a1.EnvoyFilterBuffer): + order = 11 case isFilterType(filter, egv1a1.EnvoyFilterLua): - order = 11 + mustGetFilterIndex(filter.Name) + order = 12 + mustGetFilterIndex(filter.Name) case isFilterType(filter, egv1a1.EnvoyFilterExtProc): order = 100 + mustGetFilterIndex(filter.Name) case isFilterType(filter, egv1a1.EnvoyFilterWasm): diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 1d1086fb7dc..25f52bfc1e4 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -188,6 +188,43 @@ _Appears in:_ | `GRPC` | ActiveHealthCheckerTypeGRPC defines the GRPC type of health checking.
| +#### AdmissionControl + + + +AdmissionControl defines the admission control policy to be applied. +This configuration probabilistically rejects requests based on the success rate +of previous requests in a configurable sliding time window. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `enabled` | _boolean_ | false | | Enabled enables or disables the admission control filter.
Defaults to true if not specified. | +| `samplingWindow` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | | SamplingWindow defines the time window over which request success rates are calculated.
Defaults to 60s if not specified. | +| `successRateThreshold` | _float_ | false | | SuccessRateThreshold defines the lowest request success rate at which the filter
will not reject requests. The value should be in the range [0.0, 1.0].
Defaults to 0.95 (95%) if not specified. | +| `aggression` | _float_ | false | | Aggression controls the rejection probability curve. A value of 1.0 means a linear
increase in rejection probability as the success rate decreases. Higher values
result in more aggressive rejection at higher success rates.
Defaults to 1.0 if not specified. | +| `rpsThreshold` | _integer_ | false | | RPSThreshold defines the minimum requests per second below which requests will
pass through the filter without rejection. Defaults to 1 if not specified. | +| `maxRejectionProbability` | _float_ | false | | MaxRejectionProbability represents the upper limit of the rejection probability.
The value should be in the range [0.0, 1.0]. Defaults to 0.95 (95%) if not specified. | +| `successCriteria` | _[AdmissionControlSuccessCriteria](#admissioncontrolsuccesscriteria)_ | false | | SuccessCriteria defines what constitutes a successful request for both HTTP and gRPC. | + + +#### AdmissionControlSuccessCriteria + + + +AdmissionControlSuccessCriteria defines the criteria for determining successful requests. + +_Appears in:_ +- [AdmissionControl](#admissioncontrol) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `http` | _[HTTPSuccessCriteria](#httpsuccesscriteria)_ | false | | HTTP defines success criteria for HTTP requests. | +| `grpc` | _[GRPCSuccessCriteria](#grpcsuccesscriteria)_ | false | | GRPC defines success criteria for gRPC requests. | + + #### AppProtocolType _Underlying type:_ _string_ @@ -525,6 +562,7 @@ _Appears in:_ | `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType determines how this configuration is merged with existing BackendTrafficPolicy
configurations targeting a parent resource. When set, this configuration will be merged
into a parent BackendTrafficPolicy (i.e. the one targeting a Gateway or Listener).
This field cannot be set when targeting a parent resource (Gateway).
If unset, no merging occurs, and only the most specific configuration takes effect. | | `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ | false | | RateLimit allows the user to limit the number of incoming requests
to a predefined value based on attributes within the traffic flow. | | `faultInjection` | _[FaultInjection](#faultinjection)_ | false | | FaultInjection defines the fault injection policy to be applied. This configuration can be used to
inject delays and abort requests to mimic failure scenarios such as service failures and overloads | +| `admissionControl` | _[AdmissionControl](#admissioncontrol)_ | false | | AdmissionControl defines the admission control policy to be applied. This configuration
probabilistically rejects requests based on the success rate of previous requests in a
configurable sliding time window. | | `useClientProtocol` | _boolean_ | false | | UseClientProtocol configures Envoy to prefer sending requests to backends using
the same HTTP protocol that the incoming request used. Defaults to false, which means
that Envoy will use the protocol indicated by the attached BackendRef. | | `compression` | _[Compression](#compression) array_ | false | | The compression config for the http streams.
Deprecated: Use Compressor instead. | | `compressor` | _[Compression](#compression) array_ | false | | The compressor config for the http streams.
This provides more granular control over compression configuration. | @@ -1240,6 +1278,7 @@ _Appears in:_ | ----- | ----------- | | `envoy.filters.http.health_check` | EnvoyFilterHealthCheck defines the Envoy HTTP health check filter.
| | `envoy.filters.http.fault` | EnvoyFilterFault defines the Envoy HTTP fault filter.
| +| `envoy.filters.http.admission_control` | EnvoyFilterAdmissionControl defines the Envoy HTTP admission control filter.
| | `envoy.filters.http.cors` | EnvoyFilterCORS defines the Envoy HTTP CORS filter.
| | `envoy.filters.http.ext_authz` | EnvoyFilterExtAuthz defines the Envoy HTTP external authorization filter.
| | `envoy.filters.http.api_key_auth` | EnvoyFilterAPIKeyAuth defines the Envoy HTTP api key authentication filter.
| @@ -2161,6 +2200,20 @@ _Appears in:_ | `backendSettings` | _[ClusterSettings](#clustersettings)_ | false | | BackendSettings holds configuration for managing the connection
to the backend. | +#### GRPCSuccessCriteria + + + +GRPCSuccessCriteria defines success criteria for gRPC requests. + +_Appears in:_ +- [AdmissionControlSuccessCriteria](#admissioncontrolsuccesscriteria) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `grpcSuccessStatus` | _integer array_ | false | | GRPCSuccessStatus defines gRPC status codes that are considered successful. | + + #### Gateway @@ -2494,6 +2547,35 @@ _Appears in:_ +#### HTTPStatusRange + + + +HTTPStatusRange defines a range of HTTP status codes. + +_Appears in:_ +- [HTTPSuccessCriteria](#httpsuccesscriteria) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `start` | _integer_ | true | | Start is the inclusive start of the status code range (100-600). | +| `end` | _integer_ | true | | End is the inclusive end of the status code range (100-600). | + + +#### HTTPSuccessCriteria + + + +HTTPSuccessCriteria defines success criteria for HTTP requests. + +_Appears in:_ +- [AdmissionControlSuccessCriteria](#admissioncontrolsuccesscriteria) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `httpSuccessStatus` | _[HTTPStatusRange](#httpstatusrange) array_ | false | | HTTPSuccessStatus defines ranges of HTTP status codes that are considered successful.
Each range is inclusive on both ends. | + + #### HTTPTimeout From 03d92f3e6f202517f02d6e8b5cfdae2916d23bdd Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sat, 15 Nov 2025 10:28:56 -0800 Subject: [PATCH 2/3] add admission control functionality crds Signed-off-by: Adam Buran --- test/helm/gateway-crds-helm/all.out.yaml | 94 +++++++++++++++++++ .../envoy-gateway-crds.out.yaml | 94 +++++++++++++++++++ 2 files changed, 188 insertions(+) diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 63dc101fdd8..d0c9847f273 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -21183,6 +21183,100 @@ spec: spec: description: spec defines the desired state of BackendTrafficPolicy. properties: + admissionControl: + description: |- + AdmissionControl defines the admission control policy to be applied. This configuration + probabilistically rejects requests based on the success rate of previous requests in a + configurable sliding time window. + properties: + aggression: + description: |- + Aggression controls the rejection probability curve. A value of 1.0 means a linear + increase in rejection probability as the success rate decreases. Higher values + result in more aggressive rejection at higher success rates. + Defaults to 1.0 if not specified. + minimum: 0 + type: number + enabled: + description: |- + Enabled enables or disables the admission control filter. + Defaults to true if not specified. + type: boolean + maxRejectionProbability: + description: |- + MaxRejectionProbability represents the upper limit of the rejection probability. + The value should be in the range [0.0, 1.0]. Defaults to 0.95 (95%) if not specified. + maximum: 1 + minimum: 0 + type: number + rpsThreshold: + description: |- + RPSThreshold defines the minimum requests per second below which requests will + pass through the filter without rejection. Defaults to 1 if not specified. + format: int32 + minimum: 0 + type: integer + samplingWindow: + description: |- + SamplingWindow defines the time window over which request success rates are calculated. + Defaults to 60s if not specified. + type: string + successCriteria: + description: SuccessCriteria defines what constitutes a successful + request for both HTTP and gRPC. + properties: + grpc: + description: GRPC defines success criteria for gRPC requests. + properties: + grpcSuccessStatus: + description: GRPCSuccessStatus defines gRPC status codes + that are considered successful. + items: + format: int32 + type: integer + type: array + type: object + http: + description: HTTP defines success criteria for HTTP requests. + properties: + httpSuccessStatus: + description: |- + HTTPSuccessStatus defines ranges of HTTP status codes that are considered successful. + Each range is inclusive on both ends. + items: + description: HTTPStatusRange defines a range of HTTP + status codes. + properties: + end: + description: End is the inclusive end of the status + code range (100-600). + format: int32 + maximum: 600 + minimum: 100 + type: integer + start: + description: Start is the inclusive start of the + status code range (100-600). + format: int32 + maximum: 600 + minimum: 100 + type: integer + required: + - end + - start + type: object + type: array + type: object + type: object + successRateThreshold: + description: |- + SuccessRateThreshold defines the lowest request success rate at which the filter + will not reject requests. The value should be in the range [0.0, 1.0]. + Defaults to 0.95 (95%) if not specified. + maximum: 1 + minimum: 0 + type: number + type: object circuitBreaker: description: |- Circuit Breaker settings for the upstream connections and requests. diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index ae2d5c6e125..8fdc7aa095a 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -527,6 +527,100 @@ spec: spec: description: spec defines the desired state of BackendTrafficPolicy. properties: + admissionControl: + description: |- + AdmissionControl defines the admission control policy to be applied. This configuration + probabilistically rejects requests based on the success rate of previous requests in a + configurable sliding time window. + properties: + aggression: + description: |- + Aggression controls the rejection probability curve. A value of 1.0 means a linear + increase in rejection probability as the success rate decreases. Higher values + result in more aggressive rejection at higher success rates. + Defaults to 1.0 if not specified. + minimum: 0 + type: number + enabled: + description: |- + Enabled enables or disables the admission control filter. + Defaults to true if not specified. + type: boolean + maxRejectionProbability: + description: |- + MaxRejectionProbability represents the upper limit of the rejection probability. + The value should be in the range [0.0, 1.0]. Defaults to 0.95 (95%) if not specified. + maximum: 1 + minimum: 0 + type: number + rpsThreshold: + description: |- + RPSThreshold defines the minimum requests per second below which requests will + pass through the filter without rejection. Defaults to 1 if not specified. + format: int32 + minimum: 0 + type: integer + samplingWindow: + description: |- + SamplingWindow defines the time window over which request success rates are calculated. + Defaults to 60s if not specified. + type: string + successCriteria: + description: SuccessCriteria defines what constitutes a successful + request for both HTTP and gRPC. + properties: + grpc: + description: GRPC defines success criteria for gRPC requests. + properties: + grpcSuccessStatus: + description: GRPCSuccessStatus defines gRPC status codes + that are considered successful. + items: + format: int32 + type: integer + type: array + type: object + http: + description: HTTP defines success criteria for HTTP requests. + properties: + httpSuccessStatus: + description: |- + HTTPSuccessStatus defines ranges of HTTP status codes that are considered successful. + Each range is inclusive on both ends. + items: + description: HTTPStatusRange defines a range of HTTP + status codes. + properties: + end: + description: End is the inclusive end of the status + code range (100-600). + format: int32 + maximum: 600 + minimum: 100 + type: integer + start: + description: Start is the inclusive start of the + status code range (100-600). + format: int32 + maximum: 600 + minimum: 100 + type: integer + required: + - end + - start + type: object + type: array + type: object + type: object + successRateThreshold: + description: |- + SuccessRateThreshold defines the lowest request success rate at which the filter + will not reject requests. The value should be in the range [0.0, 1.0]. + Defaults to 0.95 (95%) if not specified. + maximum: 1 + minimum: 0 + type: number + type: object circuitBreaker: description: |- Circuit Breaker settings for the upstream connections and requests. From 1c2a8dcf20426e8f9b00dd0d09ea34be459ca5ce Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sun, 30 Nov 2025 10:55:03 -0800 Subject: [PATCH 3/3] fix the lint errors Signed-off-by: Adam Buran --- examples/kubernetes/admission-control.yaml | 24 +++++++++++----------- tools/make/lint.mk | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/kubernetes/admission-control.yaml b/examples/kubernetes/admission-control.yaml index d386768cf27..2750e9182f2 100644 --- a/examples/kubernetes/admission-control.yaml +++ b/examples/kubernetes/admission-control.yaml @@ -38,18 +38,18 @@ metadata: namespace: default spec: parentRefs: - - name: eg - namespace: default + - name: eg + namespace: default hostnames: - - "example.com" + - "example.com" rules: - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: backend-service - port: 80 + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: backend-service + port: 80 --- apiVersion: v1 kind: Service @@ -58,7 +58,7 @@ metadata: namespace: default spec: ports: - - port: 80 - targetPort: 8080 + - port: 80 + targetPort: 8080 selector: app: backend diff --git a/tools/make/lint.mk b/tools/make/lint.mk index cbe2dc48f3f..a8059bfcf78 100644 --- a/tools/make/lint.mk +++ b/tools/make/lint.mk @@ -46,7 +46,7 @@ lint.codespell: $(tools/codespell) # one shell, this is because we want the ::remove-matcher lines to get # printed whether or not it finds complaints. @PS4=; set -e; { \ - if test -n "$$GITHUB_ACTION"; then \ + if test -n "$${GITHUB_ACTION:-}"; then \ printf '::add-matcher::$(CURDIR)/tools/linter/codespell/matcher.json\n'; \ trap "printf '::remove-matcher owner=codespell-matcher-default::\n::remove-matcher owner=codespell-matcher-specified::\n'" EXIT; \ fi; \