From ae64948d2b98c47d635d54d9e54bccc430af47e4 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sat, 15 Nov 2025 10:17:29 -0800 Subject: [PATCH 01/22] 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 | 305 +++++++++++++----- ....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, 1275 insertions(+), 89 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 0000000000..46ecd44fa9 --- /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 b548416d86..dfc972e467 100644 --- a/api/v1alpha1/backendtrafficpolicy_types.go +++ b/api/v1alpha1/backendtrafficpolicy_types.go @@ -67,6 +67,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 9a98b20093..aef415d180 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -290,6 +290,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 2b2c50ac1d..a745c238f5 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -17,7 +17,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "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 { @@ -204,6 +204,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 @@ -282,12 +357,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 } } @@ -334,7 +409,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 { @@ -556,7 +631,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]) } @@ -578,7 +653,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) @@ -599,12 +674,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 { @@ -614,7 +689,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 { @@ -738,6 +813,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) @@ -935,7 +1015,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 { @@ -1336,7 +1416,7 @@ func (in *ClientValidationContext) DeepCopyInto(out *ClientValidationContext) { } 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]) } @@ -1523,12 +1603,12 @@ func (in *ConnectionLimit) DeepCopyInto(out *ConnectionLimit) { } 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 { @@ -1538,7 +1618,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 } } @@ -1635,7 +1715,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 { @@ -1662,7 +1742,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]) } @@ -1714,17 +1794,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 { @@ -1764,7 +1844,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) } } @@ -1794,7 +1874,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 } } @@ -1866,7 +1946,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 { @@ -2348,7 +2428,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 } } @@ -2442,12 +2522,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 } } @@ -3070,7 +3150,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 { @@ -3117,7 +3197,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 { @@ -3313,17 +3393,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 { @@ -3349,7 +3429,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) } } @@ -3469,7 +3549,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 { @@ -3605,6 +3685,26 @@ func (in *GRPCSettings) DeepCopy() *GRPCSettings { 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 @@ -3998,17 +4098,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 } } @@ -4089,7 +4189,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) } } @@ -4135,12 +4235,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.AddIfAbsent != nil { @@ -4329,27 +4429,62 @@ func (in *HTTPRouteMatchFilter) DeepCopy() *HTTPRouteMatchFilter { 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 } if in.StreamIdleTimeout != nil { @@ -4604,7 +4739,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 { @@ -5336,7 +5471,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) } } @@ -5356,17 +5491,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 { @@ -5506,7 +5641,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 } } @@ -5569,7 +5704,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 } } @@ -5615,7 +5750,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) @@ -5666,7 +5801,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 { @@ -5676,12 +5811,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 { @@ -5892,7 +6027,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) } } @@ -5933,7 +6068,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 { @@ -5953,7 +6088,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 { @@ -5988,7 +6123,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 { @@ -6058,7 +6193,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 { @@ -6083,12 +6218,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]) } @@ -6647,6 +6782,18 @@ func (in *ProxyTracing) DeepCopyInto(out *ProxyTracing) { *out = new(uint32) **out = **in } + if in.SamplingFraction != nil { + in, out := &in.SamplingFraction, &out.SamplingFraction + *out = new(apisv1.Fraction) + (*in).DeepCopyInto(*out) + } + if in.CustomTags != nil { + in, out := &in.CustomTags, &out.CustomTags + *out = make(map[string]CustomTag, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } in.Provider.DeepCopyInto(&out.Provider) } @@ -6711,7 +6858,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 { @@ -7075,7 +7222,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) } } @@ -7111,7 +7258,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 } } @@ -7534,12 +7681,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 } } @@ -7579,7 +7726,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 } } @@ -7794,7 +7941,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 } } @@ -7819,12 +7966,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 } } @@ -7844,7 +7991,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 } } @@ -7914,7 +8061,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 { @@ -7926,7 +8073,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]) } @@ -7973,7 +8120,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 { @@ -8235,12 +8382,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 a8096a4a43..16c682d81c 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/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index c096366836..db5e0e9458 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/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 0000000000..d386768cf2 --- /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 c570f764eb..4ea2ae590b 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -1034,6 +1034,7 @@ func (t *Translator) buildTrafficFeatures(policy *egv1a1.BackendTrafficPolicy) ( hc *ir.HealthCheck cb *ir.CircuitBreaker fi *ir.FaultInjection + ac *ir.AdmissionControl to *ir.Timeout ka *ir.TCPKeepalive rt *ir.Retry @@ -1066,6 +1067,9 @@ func (t *Translator) buildTrafficFeatures(policy *egv1a1.BackendTrafficPolicy) ( 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) @@ -1123,6 +1127,7 @@ func (t *Translator) buildTrafficFeatures(policy *egv1a1.BackendTrafficPolicy) ( HealthCheck: hc, CircuitBreaker: cb, FaultInjection: fi, + AdmissionControl: ac, TCPKeepalive: ka, Retry: rt, BackendConnection: bc, @@ -1698,6 +1703,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 b4f59c7323..e0ad76ebc0 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -1005,6 +1005,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 @@ -1654,6 +1656,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 4539825b7a..45e19ab68d 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -299,6 +299,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 @@ -1743,6 +1818,26 @@ func (in *GRPCSettings) DeepCopy() *GRPCSettings { 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 *GeoIPMaxMindProvider) DeepCopyInto(out *GeoIPMaxMindProvider) { *out = *in @@ -2387,6 +2482,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 @@ -4823,6 +4953,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 0000000000..2d2afed351 --- /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 0000000000..da5a26c77b --- /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 b51ccc5283..113fa33397 100644 --- a/internal/xds/translator/httpfilters.go +++ b/internal/xds/translator/httpfilters.go @@ -104,27 +104,29 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter { order = 1 case isFilterType(filter, egv1a1.EnvoyFilterFault): order = 2 - case isFilterType(filter, egv1a1.EnvoyFilterCORS): + case isFilterType(filter, egv1a1.EnvoyFilterAdmissionControl): order = 3 + case isFilterType(filter, egv1a1.EnvoyFilterCORS): + order = 4 case isFilterType(filter, egv1a1.EnvoyFilterHeaderMutation): // Ensure header mutation run before ext auth which might consume the header. - order = 4 - case isFilterType(filter, egv1a1.EnvoyFilterExtAuthz): order = 5 - case isFilterType(filter, egv1a1.EnvoyFilterAPIKeyAuth): + case isFilterType(filter, egv1a1.EnvoyFilterExtAuthz): order = 6 - case isFilterType(filter, egv1a1.EnvoyFilterBasicAuth): + case isFilterType(filter, egv1a1.EnvoyFilterAPIKeyAuth): order = 7 - case isFilterType(filter, egv1a1.EnvoyFilterOAuth2): + case isFilterType(filter, egv1a1.EnvoyFilterBasicAuth): order = 8 - case isFilterType(filter, egv1a1.EnvoyFilterJWTAuthn): + case isFilterType(filter, egv1a1.EnvoyFilterOAuth2): order = 9 - case isFilterType(filter, egv1a1.EnvoyFilterSessionPersistence): + case isFilterType(filter, egv1a1.EnvoyFilterJWTAuthn): order = 10 - case isFilterType(filter, egv1a1.EnvoyFilterBuffer): + case isFilterType(filter, egv1a1.EnvoyFilterSessionPersistence): order = 11 + case isFilterType(filter, egv1a1.EnvoyFilterBuffer): + order = 12 case isFilterType(filter, egv1a1.EnvoyFilterLua): - order = 12 + mustGetFilterIndex(filter.Name) + order = 13 + 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 179166ef5e..799e2a244f 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -189,6 +189,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_ @@ -543,6 +580,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.
Order matters: The first compressor in the list is preferred when q-values in Accept-Encoding are equal. | @@ -1458,6 +1496,7 @@ _Appears in:_ | `envoy.filters.http.custom_response` | EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter.
| | `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.header_mutation` | EnvoyFilterHeaderMutation defines the Envoy HTTP header mutation filter
| | `envoy.filters.http.ext_authz` | EnvoyFilterExtAuthz defines the Envoy HTTP external authorization filter.
| @@ -2443,6 +2482,20 @@ _Appears in:_ | `enableWeb` | _boolean_ | false | | EnableWeb configures the gRPC-web filter on the listener.
The gRPC-web filter allows clients (typically browsers) to make gRPC calls
using HTTP/1.1 or HTTP/2.
This is enabled by default for GRPCRoute and opt-in for HTTPRoute.
In general, gRPC traffic should be handled via GRPCRoute, but there are cases where
users want to route gRPC using HTTPRoute for its richer matching capabilities.
Therefore, we enable this behavior only when it is explicitly opted in. | +#### 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 @@ -2940,6 +2993,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 fead37022b7578cd7e90a00b8b05f7ed21b7a7c2 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sat, 15 Nov 2025 10:28:56 -0800 Subject: [PATCH 02/22] 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 e76a32c3e7..a0db06153b 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -22577,6 +22577,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 c3a33d575b..9309af166d 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -550,6 +550,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 ed7451dff71c91cafcabdda443983656ed0bacb8 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sun, 30 Nov 2025 10:55:03 -0800 Subject: [PATCH 03/22] fix the lint errors Signed-off-by: Adam Buran --- examples/kubernetes/admission-control.yaml | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/kubernetes/admission-control.yaml b/examples/kubernetes/admission-control.yaml index d386768cf2..2750e9182f 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 From 31febb0311ad6cad02cc748d92453b2f66be56f3 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Fri, 13 Feb 2026 12:43:35 -0800 Subject: [PATCH 04/22] fix: address CI lint and gen-check failures for admission control - Change SamplingWindow from *metav1.Duration to *gwapiv1.Duration to satisfy kube-api-linter (nodurations rule) - Add parseSamplingWindow converter in gateway API translator to convert gwapiv1.Duration to metav1.Duration for the IR - Fix unused-parameter lint errors in patchRoute and patchResources by renaming unused params to _ - Regenerate deepcopy, CRD manifests, helm test outputs, and API docs Co-authored-by: Cursor Signed-off-by: Adam Buran --- api/v1alpha1/admission_control.go | 4 +- api/v1alpha1/zz_generated.deepcopy.go | 158 +++++++++--------- ....envoyproxy.io_backendtrafficpolicies.yaml | 1 + ....envoyproxy.io_backendtrafficpolicies.yaml | 1 + internal/gatewayapi/backendtrafficpolicy.go | 14 +- internal/xds/translator/admission_control.go | 4 +- site/content/en/latest/api/extension_types.md | 2 +- test/helm/gateway-crds-helm/all.out.yaml | 1 + .../envoy-gateway-crds.out.yaml | 1 + 9 files changed, 101 insertions(+), 85 deletions(-) diff --git a/api/v1alpha1/admission_control.go b/api/v1alpha1/admission_control.go index 46ecd44fa9..b2fc4af8ca 100644 --- a/api/v1alpha1/admission_control.go +++ b/api/v1alpha1/admission_control.go @@ -6,7 +6,7 @@ package v1alpha1 import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" ) // AdmissionControl defines the admission control policy to be applied. @@ -23,7 +23,7 @@ type AdmissionControl struct { // Defaults to 60s if not specified. // // +optional - SamplingWindow *metav1.Duration `json:"samplingWindow,omitempty"` + SamplingWindow *gwapiv1.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]. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a745c238f5..e19ce5359e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -17,7 +17,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" - apisv1 "sigs.k8s.io/gateway-api/apis/v1" + "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([]apisv1.SecretObjectReference, len(*in)) + *out = make([]v1.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(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.Interval != nil { in, out := &in.Interval, &out.Interval - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.InitialJitter != nil { in, out := &in.InitialJitter, &out.InitialJitter - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.UnhealthyThreshold != nil { @@ -357,12 +357,12 @@ func (in *BackOffPolicy) DeepCopyInto(out *BackOffPolicy) { *out = *in if in.BaseInterval != nil { in, out := &in.BaseInterval, &out.BaseInterval - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.MaxInterval != nil { in, out := &in.MaxInterval, &out.MaxInterval - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } } @@ -409,7 +409,7 @@ func (in *BackendCluster) DeepCopyInto(out *BackendCluster) { *out = *in if in.BackendRef != nil { in, out := &in.BackendRef, &out.BackendRef - *out = new(apisv1.BackendObjectReference) + *out = new(v1.BackendObjectReference) (*in).DeepCopyInto(*out) } if in.BackendRefs != nil { @@ -631,7 +631,7 @@ func (in *BackendStatus) DeepCopyInto(out *BackendStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -653,7 +653,7 @@ func (in *BackendTLSConfig) DeepCopyInto(out *BackendTLSConfig) { *out = *in if in.ClientCertificateRef != nil { in, out := &in.ClientCertificateRef, &out.ClientCertificateRef - *out = new(apisv1.SecretObjectReference) + *out = new(v1.SecretObjectReference) (*in).DeepCopyInto(*out) } in.TLSSettings.DeepCopyInto(&out.TLSSettings) @@ -674,12 +674,12 @@ func (in *BackendTLSSettings) DeepCopyInto(out *BackendTLSSettings) { *out = *in if in.CACertificateRefs != nil { in, out := &in.CACertificateRefs, &out.CACertificateRefs - *out = make([]apisv1.LocalObjectReference, len(*in)) + *out = make([]v1.LocalObjectReference, len(*in)) copy(*out, *in) } if in.WellKnownCACertificates != nil { in, out := &in.WellKnownCACertificates, &out.WellKnownCACertificates - *out = new(apisv1.WellKnownCACertificatesType) + *out = new(v1.WellKnownCACertificatesType) **out = **in } if in.InsecureSkipVerify != nil { @@ -689,7 +689,7 @@ func (in *BackendTLSSettings) DeepCopyInto(out *BackendTLSSettings) { } if in.SNI != nil { in, out := &in.SNI, &out.SNI - *out = new(apisv1.PreciseHostname) + *out = new(v1.PreciseHostname) **out = **in } if in.BackendTLSConfig != nil { @@ -1015,7 +1015,7 @@ func (in *CORS) DeepCopyInto(out *CORS) { } if in.MaxAge != nil { in, out := &in.MaxAge, &out.MaxAge - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.AllowCredentials != nil { @@ -1416,7 +1416,7 @@ func (in *ClientValidationContext) DeepCopyInto(out *ClientValidationContext) { } if in.CACertificateRefs != nil { in, out := &in.CACertificateRefs, &out.CACertificateRefs - *out = make([]apisv1.SecretObjectReference, len(*in)) + *out = make([]v1.SecretObjectReference, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1603,12 +1603,12 @@ func (in *ConnectionLimit) DeepCopyInto(out *ConnectionLimit) { } if in.CloseDelay != nil { in, out := &in.CloseDelay, &out.CloseDelay - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.MaxConnectionDuration != nil { in, out := &in.MaxConnectionDuration, &out.MaxConnectionDuration - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.MaxRequestsPerConnection != nil { @@ -1618,7 +1618,7 @@ func (in *ConnectionLimit) DeepCopyInto(out *ConnectionLimit) { } if in.MaxStreamDuration != nil { in, out := &in.MaxStreamDuration, &out.MaxStreamDuration - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } } @@ -1715,7 +1715,7 @@ func (in *Cookie) DeepCopyInto(out *Cookie) { *out = *in if in.TTL != nil { in, out := &in.TTL, &out.TTL - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.Attributes != nil { @@ -1742,7 +1742,7 @@ func (in *CrlContext) DeepCopyInto(out *CrlContext) { *out = *in if in.Refs != nil { in, out := &in.Refs, &out.Refs - *out = make([]apisv1.SecretObjectReference, len(*in)) + *out = make([]v1.SecretObjectReference, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1794,17 +1794,17 @@ func (in *CustomRedirect) DeepCopyInto(out *CustomRedirect) { } if in.Hostname != nil { in, out := &in.Hostname, &out.Hostname - *out = new(apisv1.PreciseHostname) + *out = new(v1.PreciseHostname) **out = **in } if in.Path != nil { in, out := &in.Path, &out.Path - *out = new(apisv1.HTTPPathModifier) + *out = new(v1.HTTPPathModifier) (*in).DeepCopyInto(*out) } if in.Port != nil { in, out := &in.Port, &out.Port - *out = new(apisv1.PortNumber) + *out = new(v1.PortNumber) **out = **in } if in.StatusCode != nil { @@ -1844,7 +1844,7 @@ func (in *CustomResponse) DeepCopyInto(out *CustomResponse) { } if in.Header != nil { in, out := &in.Header, &out.Header - *out = new(apisv1.HTTPHeaderFilter) + *out = new(v1.HTTPHeaderFilter) (*in).DeepCopyInto(*out) } } @@ -1874,7 +1874,7 @@ func (in *CustomResponseBody) DeepCopyInto(out *CustomResponseBody) { } if in.ValueRef != nil { in, out := &in.ValueRef, &out.ValueRef - *out = new(apisv1.LocalObjectReference) + *out = new(v1.LocalObjectReference) **out = **in } } @@ -1946,7 +1946,7 @@ func (in *DNS) DeepCopyInto(out *DNS) { *out = *in if in.DNSRefreshRate != nil { in, out := &in.DNSRefreshRate, &out.DNSRefreshRate - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.RespectDNSTTL != nil { @@ -2428,7 +2428,7 @@ func (in *EnvoyGatewayKubernetesProvider) DeepCopyInto(out *EnvoyGatewayKubernet } if in.CacheSyncPeriod != nil { in, out := &in.CacheSyncPeriod, &out.CacheSyncPeriod - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } } @@ -2522,12 +2522,12 @@ func (in *EnvoyGatewayOpenTelemetrySink) DeepCopyInto(out *EnvoyGatewayOpenTelem *out = *in if in.ExportInterval != nil { in, out := &in.ExportInterval, &out.ExportInterval - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.ExportTimeout != nil { in, out := &in.ExportTimeout, &out.ExportTimeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } } @@ -3150,7 +3150,7 @@ func (in *ExtAuth) DeepCopyInto(out *ExtAuth) { } if in.Timeout != nil { in, out := &in.Timeout, &out.Timeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.FailOpen != nil { @@ -3197,7 +3197,7 @@ func (in *ExtProc) DeepCopyInto(out *ExtProc) { in.BackendCluster.DeepCopyInto(&out.BackendCluster) if in.MessageTimeout != nil { in, out := &in.MessageTimeout, &out.MessageTimeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.FailOpen != nil { @@ -3393,17 +3393,17 @@ func (in *ExtensionServiceRetry) DeepCopyInto(out *ExtensionServiceRetry) { } if in.InitialBackoff != nil { in, out := &in.InitialBackoff, &out.InitialBackoff - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.MaxBackoff != nil { in, out := &in.MaxBackoff, &out.MaxBackoff - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.BackoffMultiplier != nil { in, out := &in.BackoffMultiplier, &out.BackoffMultiplier - *out = new(apisv1.Fraction) + *out = new(v1.Fraction) (*in).DeepCopyInto(*out) } if in.RetryableStatusCodes != nil { @@ -3429,7 +3429,7 @@ func (in *ExtensionTLS) DeepCopyInto(out *ExtensionTLS) { in.CertificateRef.DeepCopyInto(&out.CertificateRef) if in.ClientCertificateRef != nil { in, out := &in.ClientCertificateRef, &out.ClientCertificateRef - *out = new(apisv1.SecretObjectReference) + *out = new(v1.SecretObjectReference) (*in).DeepCopyInto(*out) } } @@ -3549,7 +3549,7 @@ func (in *FaultInjectionDelay) DeepCopyInto(out *FaultInjectionDelay) { *out = *in if in.FixedDelay != nil { in, out := &in.FixedDelay, &out.FixedDelay - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.Percentage != nil { @@ -4098,17 +4098,17 @@ func (in *HTTPClientTimeout) DeepCopyInto(out *HTTPClientTimeout) { *out = *in if in.RequestReceivedTimeout != nil { in, out := &in.RequestReceivedTimeout, &out.RequestReceivedTimeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.IdleTimeout != nil { in, out := &in.IdleTimeout, &out.IdleTimeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.StreamIdleTimeout != nil { in, out := &in.StreamIdleTimeout, &out.StreamIdleTimeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } } @@ -4189,7 +4189,7 @@ func (in *HTTPDirectResponseFilter) DeepCopyInto(out *HTTPDirectResponseFilter) } if in.Header != nil { in, out := &in.Header, &out.Header - *out = new(apisv1.HTTPHeaderFilter) + *out = new(v1.HTTPHeaderFilter) (*in).DeepCopyInto(*out) } } @@ -4235,12 +4235,12 @@ func (in *HTTPHeaderFilter) DeepCopyInto(out *HTTPHeaderFilter) { *out = *in if in.Set != nil { in, out := &in.Set, &out.Set - *out = make([]apisv1.HTTPHeader, len(*in)) + *out = make([]v1.HTTPHeader, len(*in)) copy(*out, *in) } if in.Add != nil { in, out := &in.Add, &out.Add - *out = make([]apisv1.HTTPHeader, len(*in)) + *out = make([]v1.HTTPHeader, len(*in)) copy(*out, *in) } if in.AddIfAbsent != nil { @@ -4469,22 +4469,22 @@ func (in *HTTPTimeout) DeepCopyInto(out *HTTPTimeout) { *out = *in if in.ConnectionIdleTimeout != nil { in, out := &in.ConnectionIdleTimeout, &out.ConnectionIdleTimeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.MaxConnectionDuration != nil { in, out := &in.MaxConnectionDuration, &out.MaxConnectionDuration - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.RequestTimeout != nil { in, out := &in.RequestTimeout, &out.RequestTimeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.MaxStreamDuration != nil { in, out := &in.MaxStreamDuration, &out.MaxStreamDuration - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.StreamIdleTimeout != nil { @@ -4739,7 +4739,7 @@ func (in *ImageWasmCodeSource) DeepCopyInto(out *ImageWasmCodeSource) { } if in.PullSecretRef != nil { in, out := &in.PullSecretRef, &out.PullSecretRef - *out = new(apisv1.SecretObjectReference) + *out = new(v1.SecretObjectReference) (*in).DeepCopyInto(*out) } if in.TLS != nil { @@ -5471,7 +5471,7 @@ func (in *KubernetesWatchMode) DeepCopyInto(out *KubernetesWatchMode) { } if in.NamespaceSelector != nil { in, out := &in.NamespaceSelector, &out.NamespaceSelector - *out = new(v1.LabelSelector) + *out = new(metav1.LabelSelector) (*in).DeepCopyInto(*out) } } @@ -5491,17 +5491,17 @@ func (in *LeaderElection) DeepCopyInto(out *LeaderElection) { *out = *in if in.LeaseDuration != nil { in, out := &in.LeaseDuration, &out.LeaseDuration - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.RenewDeadline != nil { in, out := &in.RenewDeadline, &out.RenewDeadline - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.RetryPeriod != nil { in, out := &in.RetryPeriod, &out.RetryPeriod - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.Disable != nil { @@ -5641,7 +5641,7 @@ func (in *LocalJWKS) DeepCopyInto(out *LocalJWKS) { } if in.ValueRef != nil { in, out := &in.ValueRef, &out.ValueRef - *out = new(apisv1.LocalObjectReference) + *out = new(v1.LocalObjectReference) **out = **in } } @@ -5704,7 +5704,7 @@ func (in *Lua) DeepCopyInto(out *Lua) { } if in.ValueRef != nil { in, out := &in.ValueRef, &out.ValueRef - *out = new(apisv1.LocalObjectReference) + *out = new(v1.LocalObjectReference) **out = **in } } @@ -5750,7 +5750,7 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { } if in.ClientIDRef != nil { in, out := &in.ClientIDRef, &out.ClientIDRef - *out = new(apisv1.SecretObjectReference) + *out = new(v1.SecretObjectReference) (*in).DeepCopyInto(*out) } in.ClientSecret.DeepCopyInto(&out.ClientSecret) @@ -5801,7 +5801,7 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { } if in.DefaultTokenTTL != nil { in, out := &in.DefaultTokenTTL, &out.DefaultTokenTTL - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.RefreshToken != nil { @@ -5811,12 +5811,12 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { } if in.DefaultRefreshTokenTTL != nil { in, out := &in.DefaultRefreshTokenTTL, &out.DefaultRefreshTokenTTL - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.CSRFTokenTTL != nil { in, out := &in.CSRFTokenTTL, &out.CSRFTokenTTL - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.DisableTokenEncryption != nil { @@ -6027,7 +6027,7 @@ func (in *Operation) DeepCopyInto(out *Operation) { *out = *in if in.Methods != nil { in, out := &in.Methods, &out.Methods - *out = make([]apisv1.HTTPMethod, len(*in)) + *out = make([]v1.HTTPMethod, len(*in)) copy(*out, *in) } } @@ -6068,7 +6068,7 @@ func (in *PassiveHealthCheck) DeepCopyInto(out *PassiveHealthCheck) { } if in.Interval != nil { in, out := &in.Interval, &out.Interval - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.ConsecutiveLocalOriginFailures != nil { @@ -6088,7 +6088,7 @@ func (in *PassiveHealthCheck) DeepCopyInto(out *PassiveHealthCheck) { } if in.BaseEjectionTime != nil { in, out := &in.BaseEjectionTime, &out.BaseEjectionTime - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.MaxEjectionPercent != nil { @@ -6123,7 +6123,7 @@ func (in *PathMatch) DeepCopyInto(out *PathMatch) { *out = *in if in.Type != nil { in, out := &in.Type, &out.Type - *out = new(apisv1.PathMatchType) + *out = new(v1.PathMatchType) **out = **in } if in.Invert != nil { @@ -6193,7 +6193,7 @@ func (in *PerRetryPolicy) DeepCopyInto(out *PerRetryPolicy) { *out = *in if in.Timeout != nil { in, out := &in.Timeout, &out.Timeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.BackOff != nil { @@ -6218,12 +6218,12 @@ func (in *PolicyTargetReferences) DeepCopyInto(out *PolicyTargetReferences) { *out = *in if in.TargetRef != nil { in, out := &in.TargetRef, &out.TargetRef - *out = new(apisv1.LocalPolicyTargetReferenceWithSectionName) + *out = new(v1.LocalPolicyTargetReferenceWithSectionName) (*in).DeepCopyInto(*out) } if in.TargetRefs != nil { in, out := &in.TargetRefs, &out.TargetRefs - *out = make([]apisv1.LocalPolicyTargetReferenceWithSectionName, len(*in)) + *out = make([]v1.LocalPolicyTargetReferenceWithSectionName, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -6858,7 +6858,7 @@ func (in *RateLimit) DeepCopyInto(out *RateLimit) { in.Backend.DeepCopyInto(&out.Backend) if in.Timeout != nil { in, out := &in.Timeout, &out.Timeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.Telemetry != nil { @@ -7222,7 +7222,7 @@ func (in *RedisTLSSettings) DeepCopyInto(out *RedisTLSSettings) { *out = *in if in.CertificateRef != nil { in, out := &in.CertificateRef, &out.CertificateRef - *out = new(apisv1.SecretObjectReference) + *out = new(v1.SecretObjectReference) (*in).DeepCopyInto(*out) } } @@ -7258,7 +7258,7 @@ func (in *RemoteJWKS) DeepCopyInto(out *RemoteJWKS) { in.BackendCluster.DeepCopyInto(&out.BackendCluster) if in.CacheDuration != nil { in, out := &in.CacheDuration, &out.CacheDuration - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } } @@ -7681,12 +7681,12 @@ func (in *ShutdownConfig) DeepCopyInto(out *ShutdownConfig) { *out = *in if in.DrainTimeout != nil { in, out := &in.DrainTimeout, &out.DrainTimeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.MinDrainDuration != nil { in, out := &in.MinDrainDuration, &out.MinDrainDuration - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } } @@ -7726,7 +7726,7 @@ func (in *SlowStart) DeepCopyInto(out *SlowStart) { *out = *in if in.Window != nil { in, out := &in.Window, &out.Window - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } } @@ -7941,7 +7941,7 @@ func (in *TCPClientTimeout) DeepCopyInto(out *TCPClientTimeout) { *out = *in if in.IdleTimeout != nil { in, out := &in.IdleTimeout, &out.IdleTimeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } } @@ -7966,12 +7966,12 @@ func (in *TCPKeepalive) DeepCopyInto(out *TCPKeepalive) { } if in.IdleTime != nil { in, out := &in.IdleTime, &out.IdleTime - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.Interval != nil { in, out := &in.Interval, &out.Interval - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } } @@ -7991,7 +7991,7 @@ func (in *TCPTimeout) DeepCopyInto(out *TCPTimeout) { *out = *in if in.ConnectTimeout != nil { in, out := &in.ConnectTimeout, &out.ConnectTimeout - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } } @@ -8061,7 +8061,7 @@ func (in *TargetSelector) DeepCopyInto(out *TargetSelector) { *out = *in if in.Group != nil { in, out := &in.Group, &out.Group - *out = new(apisv1.Group) + *out = new(v1.Group) **out = **in } if in.MatchLabels != nil { @@ -8073,7 +8073,7 @@ func (in *TargetSelector) DeepCopyInto(out *TargetSelector) { } if in.MatchExpressions != nil { in, out := &in.MatchExpressions, &out.MatchExpressions - *out = make([]v1.LabelSelectorRequirement, len(*in)) + *out = make([]metav1.LabelSelectorRequirement, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -8120,7 +8120,7 @@ func (in *Tracing) DeepCopyInto(out *Tracing) { *out = *in if in.SamplingFraction != nil { in, out := &in.SamplingFraction, &out.SamplingFraction - *out = new(apisv1.Fraction) + *out = new(v1.Fraction) (*in).DeepCopyInto(*out) } if in.CustomTags != nil { @@ -8382,12 +8382,12 @@ func (in *XDSServer) DeepCopyInto(out *XDSServer) { *out = *in if in.MaxConnectionAge != nil { in, out := &in.MaxConnectionAge, &out.MaxConnectionAge - *out = new(apisv1.Duration) + *out = new(v1.Duration) **out = **in } if in.MaxConnectionAgeGrace != nil { in, out := &in.MaxConnectionAgeGrace, &out.MaxConnectionAgeGrace - *out = new(apisv1.Duration) + *out = new(v1.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 16c682d81c..1e95c57e2a 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 @@ -87,6 +87,7 @@ spec: description: |- SamplingWindow defines the time window over which request success rates are calculated. Defaults to 60s if not specified. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string successCriteria: description: SuccessCriteria defines what constitutes a successful diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index db5e0e9458..2d134f82a3 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -86,6 +86,7 @@ spec: description: |- SamplingWindow defines the time window over which request success rates are calculated. Defaults to 60s if not specified. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string successCriteria: description: SuccessCriteria defines what constitutes a successful diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index 4ea2ae590b..16defc9ad8 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -1710,7 +1710,7 @@ func (t *Translator) buildAdmissionControl(policy *egv1a1.BackendTrafficPolicy) ac := &ir.AdmissionControl{ Enabled: policy.Spec.AdmissionControl.Enabled, - SamplingWindow: policy.Spec.AdmissionControl.SamplingWindow, + SamplingWindow: parseSamplingWindow(policy.Spec.AdmissionControl.SamplingWindow), SuccessRateThreshold: policy.Spec.AdmissionControl.SuccessRateThreshold, Aggression: policy.Spec.AdmissionControl.Aggression, RPSThreshold: policy.Spec.AdmissionControl.RPSThreshold, @@ -1740,6 +1740,18 @@ func (t *Translator) buildAdmissionControl(policy *egv1a1.BackendTrafficPolicy) return ac } +// parseSamplingWindow converts a gwapiv1.Duration to a metav1.Duration for the IR. +func parseSamplingWindow(d *gwapiv1.Duration) *metav1.Duration { + if d == nil { + return nil + } + duration, err := time.ParseDuration(string(*d)) + if err != nil { + return nil + } + return &metav1.Duration{Duration: duration} +} + func makeIrStatusSet(in []egv1a1.HTTPStatus) []ir.HTTPStatus { statusSet := sets.NewInt() for _, r := range in { diff --git a/internal/xds/translator/admission_control.go b/internal/xds/translator/admission_control.go index 2d2afed351..d58be22307 100644 --- a/internal/xds/translator/admission_control.go +++ b/internal/xds/translator/admission_control.go @@ -83,7 +83,7 @@ func buildHCMAdmissionControlFilter() (*hcmv3.HttpFilter, error) { } // 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 { +func (*admissionControl) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute, _ *ir.HTTPListener) error { if route == nil || irRoute == nil { return nil } @@ -227,7 +227,7 @@ func parseDuration(s string) (time.Duration, error) { } // patchResources adds all the other needed resources referenced by this filter. -func (*admissionControl) patchResources(tCtx *types.ResourceVersionTable, routes []*ir.HTTPRoute) error { +func (*admissionControl) patchResources(_ *types.ResourceVersionTable, _ []*ir.HTTPRoute) error { // Admission control filter doesn't require additional resources return nil } diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 799e2a244f..659d5a15f4 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -203,7 +203,7 @@ _Appears in:_ | 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. | +| `samplingWindow` | _[Duration](https://gateway-api.sigs.k8s.io/reference/1.4/spec/#duration)_ | 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. | diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index a0db06153b..a121613b09 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -22614,6 +22614,7 @@ spec: description: |- SamplingWindow defines the time window over which request success rates are calculated. Defaults to 60s if not specified. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string successCriteria: description: SuccessCriteria defines what constitutes a successful 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 9309af166d..2aa1adf030 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -587,6 +587,7 @@ spec: description: |- SamplingWindow defines the time window over which request success rates are calculated. Defaults to 60s if not specified. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string successCriteria: description: SuccessCriteria defines what constitutes a successful From 0fca7f5cfd636401939e26e599228c70aeb8a097 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Mon, 23 Feb 2026 09:28:16 -0800 Subject: [PATCH 05/22] incorporate some feedback Signed-off-by: Adam Buran --- api/v1alpha1/admission_control.go | 45 +- api/v1alpha1/zz_generated.deepcopy.go | 19 +- ....envoyproxy.io_backendtrafficpolicies.yaml | 59 +-- ....envoyproxy.io_backendtrafficpolicies.yaml | 59 +-- examples/kubernetes/admission-control.yaml | 8 +- internal/gatewayapi/backendtrafficpolicy.go | 18 +- internal/ir/xds.go | 18 +- internal/ir/zz_generated.deepcopy.go | 19 +- internal/xds/translator/admission_control.go | 40 +- .../xds/translator/admission_control_test.go | 431 +++++++++++++++++- .../testdata/in/xds-ir/admission-control.yaml | 70 +++ .../out/xds-ir/admission-control.routes.yaml | 93 ++++ site/content/en/latest/api/extension_types.md | 52 ++- 13 files changed, 775 insertions(+), 156 deletions(-) create mode 100644 internal/xds/translator/testdata/in/xds-ir/admission-control.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml diff --git a/api/v1alpha1/admission_control.go b/api/v1alpha1/admission_control.go index b2fc4af8ca..12a75ff84a 100644 --- a/api/v1alpha1/admission_control.go +++ b/api/v1alpha1/admission_control.go @@ -12,6 +12,7 @@ import ( // 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. +// All fields are optional and will use Envoy's defaults when not specified. type AdmissionControl struct { // Enabled enables or disables the admission control filter. // Defaults to true if not specified. @@ -79,32 +80,42 @@ type AdmissionControlSuccessCriteria struct { // 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. + // HTTPSuccessStatus defines HTTP status codes that are considered successful. // // +optional - HTTPSuccessStatus []HTTPStatusRange `json:"httpSuccessStatus,omitempty"` + HTTPSuccessStatus []HTTPStatus `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"` +// GRPCSuccessCode defines gRPC status codes as defined in +// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. +// +kubebuilder:validation:Enum=OK;CANCELLED;UNKNOWN;INVALID_ARGUMENT;DEADLINE_EXCEEDED;NOT_FOUND;ALREADY_EXISTS;PERMISSION_DENIED;RESOURCE_EXHAUSTED;FAILED_PRECONDITION;ABORTED;OUT_OF_RANGE;UNIMPLEMENTED;INTERNAL;UNAVAILABLE;DATA_LOSS;UNAUTHENTICATED +type GRPCSuccessCode string - // End is the inclusive end of the status code range (100-600). - // - // +kubebuilder:validation:Minimum=100 - // +kubebuilder:validation:Maximum=600 - End int32 `json:"end"` -} +const ( + GRPCSuccessCodeOK GRPCSuccessCode = "OK" + GRPCSuccessCodeCancelled GRPCSuccessCode = "CANCELLED" + GRPCSuccessCodeUnknown GRPCSuccessCode = "UNKNOWN" + GRPCSuccessCodeInvalidArgument GRPCSuccessCode = "INVALID_ARGUMENT" + GRPCSuccessCodeDeadlineExceeded GRPCSuccessCode = "DEADLINE_EXCEEDED" + GRPCSuccessCodeNotFound GRPCSuccessCode = "NOT_FOUND" + GRPCSuccessCodeAlreadyExists GRPCSuccessCode = "ALREADY_EXISTS" + GRPCSuccessCodePermissionDenied GRPCSuccessCode = "PERMISSION_DENIED" + GRPCSuccessCodeResourceExhausted GRPCSuccessCode = "RESOURCE_EXHAUSTED" + GRPCSuccessCodeFailedPrecondition GRPCSuccessCode = "FAILED_PRECONDITION" + GRPCSuccessCodeAborted GRPCSuccessCode = "ABORTED" + GRPCSuccessCodeOutOfRange GRPCSuccessCode = "OUT_OF_RANGE" + GRPCSuccessCodeUnimplemented GRPCSuccessCode = "UNIMPLEMENTED" + GRPCSuccessCodeInternal GRPCSuccessCode = "INTERNAL" + GRPCSuccessCodeUnavailable GRPCSuccessCode = "UNAVAILABLE" + GRPCSuccessCodeDataLoss GRPCSuccessCode = "DATA_LOSS" + GRPCSuccessCodeUnauthenticated GRPCSuccessCode = "UNAUTHENTICATED" +) // GRPCSuccessCriteria defines success criteria for gRPC requests. type GRPCSuccessCriteria struct { // GRPCSuccessStatus defines gRPC status codes that are considered successful. + // Status codes are defined in https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. // // +optional - GRPCSuccessStatus []int32 `json:"grpcSuccessStatus,omitempty"` + GRPCSuccessStatus []GRPCSuccessCode `json:"grpcSuccessStatus,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e19ce5359e..71e6ad4493 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -3690,7 +3690,7 @@ func (in *GRPCSuccessCriteria) DeepCopyInto(out *GRPCSuccessCriteria) { *out = *in if in.GRPCSuccessStatus != nil { in, out := &in.GRPCSuccessStatus, &out.GRPCSuccessStatus - *out = make([]int32, len(*in)) + *out = make([]GRPCSuccessCode, len(*in)) copy(*out, *in) } } @@ -4429,27 +4429,12 @@ func (in *HTTPRouteMatchFilter) DeepCopy() *HTTPRouteMatchFilter { 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)) + *out = make([]HTTPStatus, len(*in)) copy(*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 1e95c57e2a..602aaf1efc 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 @@ -97,42 +97,45 @@ spec: description: GRPC defines success criteria for gRPC requests. properties: grpcSuccessStatus: - description: GRPCSuccessStatus defines gRPC status codes - that are considered successful. + description: |- + GRPCSuccessStatus defines gRPC status codes that are considered successful. + Status codes are defined in https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. items: - format: int32 - type: integer + description: |- + GRPCSuccessCode defines gRPC status codes as defined in + https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. + enum: + - OK + - CANCELLED + - UNKNOWN + - INVALID_ARGUMENT + - DEADLINE_EXCEEDED + - NOT_FOUND + - ALREADY_EXISTS + - PERMISSION_DENIED + - RESOURCE_EXHAUSTED + - FAILED_PRECONDITION + - ABORTED + - OUT_OF_RANGE + - UNIMPLEMENTED + - INTERNAL + - UNAVAILABLE + - DATA_LOSS + - UNAUTHENTICATED + type: string 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. + description: HTTPSuccessStatus defines HTTP status codes + that are considered successful. 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 + description: HTTPStatus defines the http status code. + maximum: 599 + minimum: 100 + type: integer type: array type: object type: object diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index 2d134f82a3..fee5023269 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -96,42 +96,45 @@ spec: description: GRPC defines success criteria for gRPC requests. properties: grpcSuccessStatus: - description: GRPCSuccessStatus defines gRPC status codes - that are considered successful. + description: |- + GRPCSuccessStatus defines gRPC status codes that are considered successful. + Status codes are defined in https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. items: - format: int32 - type: integer + description: |- + GRPCSuccessCode defines gRPC status codes as defined in + https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. + enum: + - OK + - CANCELLED + - UNKNOWN + - INVALID_ARGUMENT + - DEADLINE_EXCEEDED + - NOT_FOUND + - ALREADY_EXISTS + - PERMISSION_DENIED + - RESOURCE_EXHAUSTED + - FAILED_PRECONDITION + - ABORTED + - OUT_OF_RANGE + - UNIMPLEMENTED + - INTERNAL + - UNAVAILABLE + - DATA_LOSS + - UNAUTHENTICATED + type: string 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. + description: HTTPSuccessStatus defines HTTP status codes + that are considered successful. 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 + description: HTTPStatus defines the http status code. + maximum: 599 + minimum: 100 + type: integer type: array type: object type: object diff --git a/examples/kubernetes/admission-control.yaml b/examples/kubernetes/admission-control.yaml index 2750e9182f..112bec984d 100644 --- a/examples/kubernetes/admission-control.yaml +++ b/examples/kubernetes/admission-control.yaml @@ -24,12 +24,12 @@ spec: successCriteria: http: httpSuccessStatus: - - start: 200 - end: 299 + - 200 + - 201 + - 204 grpc: grpcSuccessStatus: - - start: 0 - end: 0 + - OK --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index 16defc9ad8..6375410704 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -1721,18 +1721,22 @@ func (t *Translator) buildAdmissionControl(policy *egv1a1.BackendTrafficPolicy) 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, - }) + httpStatuses := make([]int32, len(policy.Spec.AdmissionControl.SuccessCriteria.HTTP.HTTPSuccessStatus)) + for i, s := range policy.Spec.AdmissionControl.SuccessCriteria.HTTP.HTTPSuccessStatus { + httpStatuses[i] = int32(s) + } + ac.SuccessCriteria.HTTP = &ir.HTTPSuccessCriteria{ + HTTPSuccessStatus: httpStatuses, } } if policy.Spec.AdmissionControl.SuccessCriteria.GRPC != nil { + grpcStatuses := make([]string, len(policy.Spec.AdmissionControl.SuccessCriteria.GRPC.GRPCSuccessStatus)) + for i, s := range policy.Spec.AdmissionControl.SuccessCriteria.GRPC.GRPCSuccessStatus { + grpcStatuses[i] = string(s) + } ac.SuccessCriteria.GRPC = &ir.GRPCSuccessCriteria{ - GRPCSuccessStatus: policy.Spec.AdmissionControl.SuccessCriteria.GRPC.GRPCSuccessStatus, + GRPCSuccessStatus: grpcStatuses, } } } diff --git a/internal/ir/xds.go b/internal/ir/xds.go index e0ad76ebc0..bf0ff7b88f 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -1692,26 +1692,16 @@ type AdmissionControlSuccessCriteria struct { // // +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"` + // HTTPSuccessStatus defines HTTP status codes that are considered successful. + HTTPSuccessStatus []int32 `json:"httpSuccessStatus,omitempty" yaml:"httpSuccessStatus,omitempty"` } // 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"` + // GRPCSuccessStatus defines gRPC status codes that are considered successful (string enum names). + GRPCSuccessStatus []string `json:"grpcSuccessStatus,omitempty" yaml:"grpcSuccessStatus,omitempty"` } // MirrorPolicy specifies a destination to mirror traffic in addition diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 45e19ab68d..5576333ae5 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -1823,7 +1823,7 @@ func (in *GRPCSuccessCriteria) DeepCopyInto(out *GRPCSuccessCriteria) { *out = *in if in.GRPCSuccessStatus != nil { in, out := &in.GRPCSuccessStatus, &out.GRPCSuccessStatus - *out = make([]int32, len(*in)) + *out = make([]string, len(*in)) copy(*out, *in) } } @@ -2482,27 +2482,12 @@ 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)) + *out = make([]int32, len(*in)) copy(*out, *in) } } diff --git a/internal/xds/translator/admission_control.go b/internal/xds/translator/admission_control.go index d58be22307..f832a70139 100644 --- a/internal/xds/translator/admission_control.go +++ b/internal/xds/translator/admission_control.go @@ -191,23 +191,25 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss if admissionControl.SuccessCriteria != nil { successCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria{} - // HTTP success criteria + // HTTP success criteria: each individual status code becomes a single-element range [code, code+1) if admissionControl.SuccessCriteria.HTTP != nil && len(admissionControl.SuccessCriteria.HTTP.HTTPSuccessStatus) > 0 { httpCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria_HttpCriteria{} - for _, statusRange := range admissionControl.SuccessCriteria.HTTP.HTTPSuccessStatus { + for _, code := range admissionControl.SuccessCriteria.HTTP.HTTPSuccessStatus { httpCriteria.HttpSuccessStatus = append(httpCriteria.HttpSuccessStatus, &typev3.Int32Range{ - Start: statusRange.Start, - End: statusRange.End, + Start: code, + End: code + 1, }) } successCriteria.HttpCriteria = httpCriteria } - // gRPC success criteria + // gRPC success criteria: map string enum names to numeric codes 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)) + if code, ok := grpcStatusCodeToUint32(status); ok { + grpcCriteria.GrpcSuccessStatus = append(grpcCriteria.GrpcSuccessStatus, code) + } } successCriteria.GrpcCriteria = grpcCriteria } @@ -226,6 +228,32 @@ func parseDuration(s string) (time.Duration, error) { return time.ParseDuration(s) } +// grpcStatusCodeToUint32 maps a gRPC status code string name to its numeric value. +// See https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc +func grpcStatusCodeToUint32(name string) (uint32, bool) { + codes := map[string]uint32{ + "OK": 0, + "CANCELLED": 1, + "UNKNOWN": 2, + "INVALID_ARGUMENT": 3, + "DEADLINE_EXCEEDED": 4, + "NOT_FOUND": 5, + "ALREADY_EXISTS": 6, + "PERMISSION_DENIED": 7, + "RESOURCE_EXHAUSTED": 8, + "FAILED_PRECONDITION": 9, + "ABORTED": 10, + "OUT_OF_RANGE": 11, + "UNIMPLEMENTED": 12, + "INTERNAL": 13, + "UNAVAILABLE": 14, + "DATA_LOSS": 15, + "UNAUTHENTICATED": 16, + } + code, ok := codes[name] + return code, ok +} + // patchResources adds all the other needed resources referenced by this filter. func (*admissionControl) patchResources(_ *types.ResourceVersionTable, _ []*ir.HTTPRoute) error { // Admission control filter doesn't require additional resources diff --git a/internal/xds/translator/admission_control_test.go b/internal/xds/translator/admission_control_test.go index da5a26c77b..75d285e567 100644 --- a/internal/xds/translator/admission_control_test.go +++ b/internal/xds/translator/admission_control_test.go @@ -79,6 +79,90 @@ func TestBuildAdmissionControlConfig(t *testing.T) { config: nil, wantErr: true, }, + { + name: "empty config with defaults", + config: &ir.AdmissionControl{}, + wantErr: false, + }, + { + name: "full config with all fields", + config: &ir.AdmissionControl{ + SamplingWindow: &metav1.Duration{ + Duration: 30 * time.Second, + }, + SuccessRateThreshold: ptr.To(0.90), + Aggression: ptr.To(2.0), + RPSThreshold: ptr.To(uint32(10)), + MaxRejectionProbability: ptr.To(0.80), + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ + HTTP: &ir.HTTPSuccessCriteria{ + HTTPSuccessStatus: []int32{200, 201, 300, 301}, + }, + GRPC: &ir.GRPCSuccessCriteria{ + GRPCSuccessStatus: []string{"OK", "CANCELLED"}, + }, + }, + }, + wantErr: false, + }, + { + name: "config with only HTTP success criteria", + config: &ir.AdmissionControl{ + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ + HTTP: &ir.HTTPSuccessCriteria{ + HTTPSuccessStatus: []int32{200, 201, 202}, + }, + }, + }, + wantErr: false, + }, + { + name: "config with only gRPC success criteria", + config: &ir.AdmissionControl{ + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ + GRPC: &ir.GRPCSuccessCriteria{ + GRPCSuccessStatus: []string{"OK"}, + }, + }, + }, + wantErr: false, + }, + { + name: "config with empty success criteria", + config: &ir.AdmissionControl{ + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{}, + }, + wantErr: false, + }, + { + name: "config with custom sampling window", + config: &ir.AdmissionControl{ + SamplingWindow: &metav1.Duration{ + Duration: 120 * time.Second, + }, + }, + wantErr: false, + }, + { + name: "config with zero thresholds", + config: &ir.AdmissionControl{ + SuccessRateThreshold: ptr.To(0.0), + Aggression: ptr.To(0.0), + RPSThreshold: ptr.To(uint32(0)), + MaxRejectionProbability: ptr.To(0.0), + }, + wantErr: false, + }, + { + name: "config with max thresholds", + config: &ir.AdmissionControl{ + SuccessRateThreshold: ptr.To(1.0), + Aggression: ptr.To(10.0), + RPSThreshold: ptr.To(uint32(1000)), + MaxRejectionProbability: ptr.To(1.0), + }, + wantErr: false, + }, } for _, tt := range tests { @@ -89,7 +173,352 @@ func TestBuildAdmissionControlConfig(t *testing.T) { return } require.NoError(t, err) - assert.NotNil(t, got) + require.NotNil(t, got) + + // Verify enabled is always true + require.NotNil(t, got.Enabled) + require.NotNil(t, got.Enabled.DefaultValue) + assert.True(t, got.Enabled.DefaultValue.Value) + + // Verify sampling window is set + require.NotNil(t, got.SamplingWindow) + + // Verify sr threshold is set + require.NotNil(t, got.SrThreshold) + require.NotNil(t, got.SrThreshold.DefaultValue) + + // Verify aggression is set + require.NotNil(t, got.Aggression) + + // Verify RPS threshold is set + require.NotNil(t, got.RpsThreshold) + + // Verify max rejection probability is set + require.NotNil(t, got.MaxRejectionProbability) + require.NotNil(t, got.MaxRejectionProbability.DefaultValue) + + // Verify evaluation criteria is always set + require.NotNil(t, got.EvaluationCriteria) + }) + } +} + +func TestBuildAdmissionControlConfigValues(t *testing.T) { + // Test that specific values are correctly translated + config := &ir.AdmissionControl{ + SamplingWindow: &metav1.Duration{ + Duration: 45 * time.Second, + }, + SuccessRateThreshold: ptr.To(0.85), + Aggression: ptr.To(1.5), + RPSThreshold: ptr.To(uint32(20)), + MaxRejectionProbability: ptr.To(0.75), + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ + HTTP: &ir.HTTPSuccessCriteria{ + HTTPSuccessStatus: []int32{200, 201, 202}, + }, + GRPC: &ir.GRPCSuccessCriteria{ + GRPCSuccessStatus: []string{"OK", "CANCELLED", "UNKNOWN"}, + }, + }, + } + + got, err := buildAdmissionControlConfig(config) + require.NoError(t, err) + require.NotNil(t, got) + + // Verify sampling window (45 seconds) + assert.Equal(t, int64(45), got.SamplingWindow.Seconds) + + // Verify success rate threshold (0.85 * 100 = 85.0) + assert.Equal(t, 85.0, got.SrThreshold.DefaultValue.Value) + + // Verify aggression + assert.Equal(t, 1.5, got.Aggression.DefaultValue) + + // Verify RPS threshold + assert.Equal(t, uint32(20), got.RpsThreshold.DefaultValue) + + // Verify max rejection probability (0.75 * 100 = 75.0) + assert.Equal(t, 75.0, got.MaxRejectionProbability.DefaultValue.Value) + + // Verify evaluation criteria is set (detailed verification done via testdata tests) + require.NotNil(t, got.EvaluationCriteria) +} + +func TestBuildAdmissionControlConfigDefaults(t *testing.T) { + // Test that defaults are correctly applied when no values are set + config := &ir.AdmissionControl{} + + got, err := buildAdmissionControlConfig(config) + require.NoError(t, err) + require.NotNil(t, got) + + // Default sampling window is 60s + assert.Equal(t, int64(60), got.SamplingWindow.Seconds) + + // Default success rate threshold is 0.95 * 100 = 95.0 + assert.Equal(t, 95.0, got.SrThreshold.DefaultValue.Value) + + // Default aggression is 1.0 + assert.Equal(t, 1.0, got.Aggression.DefaultValue) + + // Default RPS threshold is 1 + assert.Equal(t, uint32(1), got.RpsThreshold.DefaultValue) + + // Default max rejection probability is 0.95 * 100 = 95.0 + assert.Equal(t, 95.0, got.MaxRejectionProbability.DefaultValue.Value) +} + +func TestAdmissionControlPatchHCM(t *testing.T) { + ac := &admissionControl{} + + tests := []struct { + name string + mgr *hcmv3.HttpConnectionManager + listener *ir.HTTPListener + wantErr bool + wantLen int // expected number of http filters after patching + }{ + { + name: "nil hcm", + mgr: nil, + listener: &ir.HTTPListener{}, + wantErr: true, + }, + { + name: "nil listener", + mgr: &hcmv3.HttpConnectionManager{}, + listener: nil, + wantErr: true, + }, + { + name: "no routes with admission control", + mgr: &hcmv3.HttpConnectionManager{}, + listener: &ir.HTTPListener{ + Routes: []*ir.HTTPRoute{ + {Traffic: &ir.TrafficFeatures{}}, + }, + }, + wantErr: false, + wantLen: 0, + }, + { + name: "route with admission control", + mgr: &hcmv3.HttpConnectionManager{}, + listener: &ir.HTTPListener{ + Routes: []*ir.HTTPRoute{ + { + Traffic: &ir.TrafficFeatures{ + AdmissionControl: &ir.AdmissionControl{}, + }, + }, + }, + }, + wantErr: false, + wantLen: 1, + }, + { + name: "filter already exists", + mgr: &hcmv3.HttpConnectionManager{ + HttpFilters: []*hcmv3.HttpFilter{ + {Name: string(egv1a1.EnvoyFilterAdmissionControl)}, + }, + }, + listener: &ir.HTTPListener{ + Routes: []*ir.HTTPRoute{ + { + Traffic: &ir.TrafficFeatures{ + AdmissionControl: &ir.AdmissionControl{}, + }, + }, + }, + }, + wantErr: false, + wantLen: 1, // Should not add duplicate + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ac.patchHCM(tt.mgr, tt.listener) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Len(t, tt.mgr.HttpFilters, tt.wantLen) + }) + } +} + +func TestAdmissionControlPatchRoute(t *testing.T) { + ac := &admissionControl{} + + tests := []struct { + name string + route *routev3.Route + irRoute *ir.HTTPRoute + listener *ir.HTTPListener + wantErr bool + wantCfg bool // whether typedPerFilterConfig should be set + }{ + { + name: "nil route", + route: nil, + irRoute: &ir.HTTPRoute{}, + listener: &ir.HTTPListener{}, + wantErr: false, + wantCfg: false, + }, + { + name: "nil ir route", + route: &routev3.Route{}, + irRoute: nil, + listener: &ir.HTTPListener{}, + wantErr: false, + wantCfg: false, + }, + { + name: "route without traffic features", + route: &routev3.Route{}, + irRoute: &ir.HTTPRoute{ + Traffic: nil, + }, + listener: &ir.HTTPListener{}, + wantErr: false, + wantCfg: false, + }, + { + name: "route without admission control", + route: &routev3.Route{}, + irRoute: &ir.HTTPRoute{ + Traffic: &ir.TrafficFeatures{}, + }, + listener: &ir.HTTPListener{}, + wantErr: false, + wantCfg: false, + }, + { + name: "route with admission control", + route: &routev3.Route{}, + irRoute: &ir.HTTPRoute{ + Traffic: &ir.TrafficFeatures{ + AdmissionControl: &ir.AdmissionControl{}, + }, + }, + listener: &ir.HTTPListener{}, + wantErr: false, + wantCfg: true, + }, + { + name: "route with full admission control config", + route: &routev3.Route{}, + irRoute: &ir.HTTPRoute{ + Traffic: &ir.TrafficFeatures{ + AdmissionControl: &ir.AdmissionControl{ + SamplingWindow: &metav1.Duration{ + Duration: 30 * time.Second, + }, + SuccessRateThreshold: ptr.To(0.90), + Aggression: ptr.To(2.0), + RPSThreshold: ptr.To(uint32(10)), + MaxRejectionProbability: ptr.To(0.80), + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ + HTTP: &ir.HTTPSuccessCriteria{ + HTTPSuccessStatus: []int32{200, 201, 202}, + }, + }, + }, + }, + }, + listener: &ir.HTTPListener{}, + wantErr: false, + wantCfg: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ac.patchRoute(tt.route, tt.irRoute, tt.listener) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + if tt.wantCfg { + require.NotNil(t, tt.route.TypedPerFilterConfig) + assert.Contains(t, tt.route.TypedPerFilterConfig, string(egv1a1.EnvoyFilterAdmissionControl)) + } + }) + } +} + +func TestAdmissionControlPatchResources(t *testing.T) { + ac := &admissionControl{} + tCtx := &types.ResourceVersionTable{} + routes := []*ir.HTTPRoute{ + { + Traffic: &ir.TrafficFeatures{ + AdmissionControl: &ir.AdmissionControl{}, + }, + }, + } + + // patchResources should always return nil since admission control doesn't need additional resources + err := ac.patchResources(tCtx, routes) + require.NoError(t, err) +} + +func TestBuildHCMAdmissionControlFilter(t *testing.T) { + filter, err := buildHCMAdmissionControlFilter() + require.NoError(t, err) + require.NotNil(t, filter) + assert.Equal(t, string(egv1a1.EnvoyFilterAdmissionControl), filter.Name) + assert.NotNil(t, filter.ConfigType) +} + +func TestParseDuration(t *testing.T) { + tests := []struct { + name string + input string + want time.Duration + wantErr bool + }{ + { + name: "valid duration 60s", + input: "60s", + want: 60 * time.Second, + wantErr: false, + }, + { + name: "valid duration 30s", + input: "30s", + want: 30 * time.Second, + wantErr: false, + }, + { + name: "valid duration 1m", + input: "1m0s", + want: time.Minute, + wantErr: false, + }, + { + name: "invalid duration", + input: "invalid", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseDuration(tt.input) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) }) } } diff --git a/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml b/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml new file mode 100644 index 0000000000..1515748e03 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml @@ -0,0 +1,70 @@ +http: +- name: "first-listener" + address: "::" + port: 10080 + path: + mergeSlashes: true + escapedSlashesAction: UnescapeAndRedirect + hostnames: + - "*" + routes: + - name: "first-route" + hostname: "*" + traffic: + admissionControl: + samplingWindow: 30s + successRateThreshold: 0.95 + aggression: 1.0 + rpsThreshold: 5 + maxRejectionProbability: 0.8 + successCriteria: + http: + httpSuccessStatus: + - 200 + - 201 + - 202 + - 204 + pathMatch: + exact: "foo/bar" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + name: "first-route-dest/backend/0" + - name: "second-route" + hostname: "*" + traffic: + admissionControl: + successRateThreshold: 0.80 + aggression: 2.0 + rpsThreshold: 10 + maxRejectionProbability: 0.95 + successCriteria: + grpc: + grpcSuccessStatus: + - "OK" + - "CANCELLED" + pathMatch: + exact: "example" + destination: + name: "second-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + name: "second-route-dest/backend/0" + - name: "third-route" + hostname: "*" + traffic: + admissionControl: {} + pathMatch: + exact: "test" + destination: + name: "third-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + name: "third-route-dest/backend/0" diff --git a/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml new file mode 100644 index 0000000000..c62285ff8b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml @@ -0,0 +1,93 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + path: foo/bar + name: first-route + route: + cluster: first-route-dest + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.admission_control: + '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl + aggression: + defaultValue: 1 + enabled: + defaultValue: true + maxRejectionProbability: + defaultValue: + value: 80 + rpsThreshold: + defaultValue: 5 + samplingWindow: 30s + srThreshold: + defaultValue: + value: 95 + successCriteria: + httpCriteria: + httpSuccessStatus: + - end: 201 + start: 200 + - end: 202 + start: 201 + - end: 203 + start: 202 + - end: 205 + start: 204 + - match: + path: example + name: second-route + route: + cluster: second-route-dest + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.admission_control: + '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl + aggression: + defaultValue: 2 + enabled: + defaultValue: true + maxRejectionProbability: + defaultValue: + value: 95 + rpsThreshold: + defaultValue: 10 + samplingWindow: 60s + srThreshold: + defaultValue: + value: 80 + successCriteria: + grpcCriteria: + grpcSuccessStatus: + - 0 + - 1 + - match: + path: test + name: third-route + route: + cluster: third-route-dest + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.admission_control: + '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl + aggression: + defaultValue: 1 + enabled: + defaultValue: true + maxRejectionProbability: + defaultValue: + value: 95 + rpsThreshold: + defaultValue: 1 + samplingWindow: 60s + srThreshold: + defaultValue: + value: 95 + successCriteria: {} diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 659d5a15f4..0e73bd7a28 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -196,6 +196,7 @@ _Appears in:_ 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. +All fields are optional and will use Envoy's defaults when not specified. _Appears in:_ - [BackendTrafficPolicySpec](#backendtrafficpolicyspec) @@ -2482,6 +2483,37 @@ _Appears in:_ | `enableWeb` | _boolean_ | false | | EnableWeb configures the gRPC-web filter on the listener.
The gRPC-web filter allows clients (typically browsers) to make gRPC calls
using HTTP/1.1 or HTTP/2.
This is enabled by default for GRPCRoute and opt-in for HTTPRoute.
In general, gRPC traffic should be handled via GRPCRoute, but there are cases where
users want to route gRPC using HTTPRoute for its richer matching capabilities.
Therefore, we enable this behavior only when it is explicitly opted in. | +#### GRPCSuccessCode + +_Underlying type:_ _string_ + +GRPCSuccessCode defines gRPC status codes as defined in +https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. + +_Appears in:_ +- [GRPCSuccessCriteria](#grpcsuccesscriteria) + +| Value | Description | +| ----- | ----------- | +| `OK` | | +| `CANCELLED` | | +| `UNKNOWN` | | +| `INVALID_ARGUMENT` | | +| `DEADLINE_EXCEEDED` | | +| `NOT_FOUND` | | +| `ALREADY_EXISTS` | | +| `PERMISSION_DENIED` | | +| `RESOURCE_EXHAUSTED` | | +| `FAILED_PRECONDITION` | | +| `ABORTED` | | +| `OUT_OF_RANGE` | | +| `UNIMPLEMENTED` | | +| `INTERNAL` | | +| `UNAVAILABLE` | | +| `DATA_LOSS` | | +| `UNAUTHENTICATED` | | + + #### GRPCSuccessCriteria @@ -2493,7 +2525,7 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `grpcSuccessStatus` | _integer array_ | false | | GRPCSuccessStatus defines gRPC status codes that are considered successful. | +| `grpcSuccessStatus` | _[GRPCSuccessCode](#grpcsuccesscode) array_ | false | | GRPCSuccessStatus defines gRPC status codes that are considered successful.
Status codes are defined in https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. | #### Gateway @@ -2989,23 +3021,9 @@ HTTPStatus defines the http status code. _Appears in:_ - [HTTPActiveHealthChecker](#httpactivehealthchecker) -- [RetryOn](#retryon) - - - -#### HTTPStatusRange - - - -HTTPStatusRange defines a range of HTTP status codes. - -_Appears in:_ - [HTTPSuccessCriteria](#httpsuccesscriteria) +- [RetryOn](#retryon) -| 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 @@ -3019,7 +3037,7 @@ _Appears in:_ | 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. | +| `httpSuccessStatus` | _[HTTPStatus](#httpstatus) array_ | false | | HTTPSuccessStatus defines HTTP status codes that are considered successful. | #### HTTPTimeout From ce38d07480b0ff38146c51aeb0ce37b3d0cc33e3 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sat, 28 Feb 2026 15:35:21 -0800 Subject: [PATCH 06/22] update helm template test outputs after merge with upstream/main Signed-off-by: Adam Buran --- test/helm/gateway-crds-helm/all.out.yaml | 59 ++++++++++--------- .../envoy-gateway-crds.out.yaml | 59 ++++++++++--------- 2 files changed, 62 insertions(+), 56 deletions(-) diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index a121613b09..6af0cb4f6a 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -22624,42 +22624,45 @@ spec: description: GRPC defines success criteria for gRPC requests. properties: grpcSuccessStatus: - description: GRPCSuccessStatus defines gRPC status codes - that are considered successful. + description: |- + GRPCSuccessStatus defines gRPC status codes that are considered successful. + Status codes are defined in https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. items: - format: int32 - type: integer + description: |- + GRPCSuccessCode defines gRPC status codes as defined in + https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. + enum: + - OK + - CANCELLED + - UNKNOWN + - INVALID_ARGUMENT + - DEADLINE_EXCEEDED + - NOT_FOUND + - ALREADY_EXISTS + - PERMISSION_DENIED + - RESOURCE_EXHAUSTED + - FAILED_PRECONDITION + - ABORTED + - OUT_OF_RANGE + - UNIMPLEMENTED + - INTERNAL + - UNAVAILABLE + - DATA_LOSS + - UNAUTHENTICATED + type: string 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. + description: HTTPSuccessStatus defines HTTP status codes + that are considered successful. 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 + description: HTTPStatus defines the http status code. + maximum: 599 + minimum: 100 + type: integer type: array type: object type: object 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 2aa1adf030..70a40640c1 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -597,42 +597,45 @@ spec: description: GRPC defines success criteria for gRPC requests. properties: grpcSuccessStatus: - description: GRPCSuccessStatus defines gRPC status codes - that are considered successful. + description: |- + GRPCSuccessStatus defines gRPC status codes that are considered successful. + Status codes are defined in https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. items: - format: int32 - type: integer + description: |- + GRPCSuccessCode defines gRPC status codes as defined in + https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. + enum: + - OK + - CANCELLED + - UNKNOWN + - INVALID_ARGUMENT + - DEADLINE_EXCEEDED + - NOT_FOUND + - ALREADY_EXISTS + - PERMISSION_DENIED + - RESOURCE_EXHAUSTED + - FAILED_PRECONDITION + - ABORTED + - OUT_OF_RANGE + - UNIMPLEMENTED + - INTERNAL + - UNAVAILABLE + - DATA_LOSS + - UNAUTHENTICATED + type: string 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. + description: HTTPSuccessStatus defines HTTP status codes + that are considered successful. 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 + description: HTTPStatus defines the http status code. + maximum: 599 + minimum: 100 + type: integer type: array type: object type: object From eef4e1ad78771f0d7a00be888c4f7364a2ae7273 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Mon, 2 Mar 2026 08:32:01 -0800 Subject: [PATCH 07/22] use envoy defaults Signed-off-by: Adam Buran --- api/v1alpha1/admission_control.go | 42 ++++----- internal/xds/translator/admission_control.go | 87 ++++++++----------- .../xds/translator/admission_control_test.go | 45 +++------- .../testdata/in/xds-ir/admission-control.yaml | 4 +- .../out/xds-ir/admission-control.routes.yaml | 12 --- 5 files changed, 71 insertions(+), 119 deletions(-) diff --git a/api/v1alpha1/admission_control.go b/api/v1alpha1/admission_control.go index 12a75ff84a..24c635465d 100644 --- a/api/v1alpha1/admission_control.go +++ b/api/v1alpha1/admission_control.go @@ -21,7 +21,7 @@ type AdmissionControl struct { Enabled *bool `json:"enabled,omitempty"` // SamplingWindow defines the time window over which request success rates are calculated. - // Defaults to 60s if not specified. + // Defaults to 30s if not specified. // // +optional SamplingWindow *gwapiv1.Duration `json:"samplingWindow,omitempty"` @@ -45,14 +45,14 @@ type AdmissionControl struct { 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. + // pass through the filter without rejection. Defaults to 0 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. + // The value should be in the range [0.0, 1.0]. Defaults to 0.80 (80%) if not specified. // // +optional // +kubebuilder:validation:Minimum=0.0 @@ -88,27 +88,27 @@ type HTTPSuccessCriteria struct { // GRPCSuccessCode defines gRPC status codes as defined in // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. -// +kubebuilder:validation:Enum=OK;CANCELLED;UNKNOWN;INVALID_ARGUMENT;DEADLINE_EXCEEDED;NOT_FOUND;ALREADY_EXISTS;PERMISSION_DENIED;RESOURCE_EXHAUSTED;FAILED_PRECONDITION;ABORTED;OUT_OF_RANGE;UNIMPLEMENTED;INTERNAL;UNAVAILABLE;DATA_LOSS;UNAUTHENTICATED +// +kubebuilder:validation:Enum=Ok;Cancelled;Unknown;InvalidArgument;DeadlineExceeded;NotFound;AlreadyExists;PermissionDenied;ResourceExhausted;FailedPrecondition;Aborted;OutOfRange;Unimplemented;Internal;Unavailable;DataLoss;Unauthenticated type GRPCSuccessCode string const ( - GRPCSuccessCodeOK GRPCSuccessCode = "OK" - GRPCSuccessCodeCancelled GRPCSuccessCode = "CANCELLED" - GRPCSuccessCodeUnknown GRPCSuccessCode = "UNKNOWN" - GRPCSuccessCodeInvalidArgument GRPCSuccessCode = "INVALID_ARGUMENT" - GRPCSuccessCodeDeadlineExceeded GRPCSuccessCode = "DEADLINE_EXCEEDED" - GRPCSuccessCodeNotFound GRPCSuccessCode = "NOT_FOUND" - GRPCSuccessCodeAlreadyExists GRPCSuccessCode = "ALREADY_EXISTS" - GRPCSuccessCodePermissionDenied GRPCSuccessCode = "PERMISSION_DENIED" - GRPCSuccessCodeResourceExhausted GRPCSuccessCode = "RESOURCE_EXHAUSTED" - GRPCSuccessCodeFailedPrecondition GRPCSuccessCode = "FAILED_PRECONDITION" - GRPCSuccessCodeAborted GRPCSuccessCode = "ABORTED" - GRPCSuccessCodeOutOfRange GRPCSuccessCode = "OUT_OF_RANGE" - GRPCSuccessCodeUnimplemented GRPCSuccessCode = "UNIMPLEMENTED" - GRPCSuccessCodeInternal GRPCSuccessCode = "INTERNAL" - GRPCSuccessCodeUnavailable GRPCSuccessCode = "UNAVAILABLE" - GRPCSuccessCodeDataLoss GRPCSuccessCode = "DATA_LOSS" - GRPCSuccessCodeUnauthenticated GRPCSuccessCode = "UNAUTHENTICATED" + GRPCSuccessCodeOk GRPCSuccessCode = "Ok" + GRPCSuccessCodeCancelled GRPCSuccessCode = "Cancelled" + GRPCSuccessCodeUnknown GRPCSuccessCode = "Unknown" + GRPCSuccessCodeInvalidArgument GRPCSuccessCode = "InvalidArgument" + GRPCSuccessCodeDeadlineExceeded GRPCSuccessCode = "DeadlineExceeded" + GRPCSuccessCodeNotFound GRPCSuccessCode = "NotFound" + GRPCSuccessCodeAlreadyExists GRPCSuccessCode = "AlreadyExists" + GRPCSuccessCodePermissionDenied GRPCSuccessCode = "PermissionDenied" + GRPCSuccessCodeResourceExhausted GRPCSuccessCode = "ResourceExhausted" + GRPCSuccessCodeFailedPrecondition GRPCSuccessCode = "FailedPrecondition" + GRPCSuccessCodeAborted GRPCSuccessCode = "Aborted" + GRPCSuccessCodeOutOfRange GRPCSuccessCode = "OutOfRange" + GRPCSuccessCodeUnimplemented GRPCSuccessCode = "Unimplemented" + GRPCSuccessCodeInternal GRPCSuccessCode = "Internal" + GRPCSuccessCodeUnavailable GRPCSuccessCode = "Unavailable" + GRPCSuccessCodeDataLoss GRPCSuccessCode = "DataLoss" + GRPCSuccessCodeUnauthenticated GRPCSuccessCode = "Unauthenticated" ) // GRPCSuccessCriteria defines success criteria for gRPC requests. diff --git a/internal/xds/translator/admission_control.go b/internal/xds/translator/admission_control.go index f832a70139..8331844ded 100644 --- a/internal/xds/translator/admission_control.go +++ b/internal/xds/translator/admission_control.go @@ -138,53 +138,38 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss DefaultValue: &wrapperspb.BoolValue{Value: enabled}, } - // Set sampling window (defaults to 60s if not specified) - samplingWindow := "60s" + // Only set fields the user explicitly configured; Envoy applies its own defaults + // (sampling_window=30s, sr_threshold=95%, aggression=1.0, rps_threshold=0, max_rejection_probability=80%). if admissionControl.SamplingWindow != nil { - samplingWindow = admissionControl.SamplingWindow.Duration.String() - } - duration, err := parseDuration(samplingWindow) - if err != nil { - return nil, fmt.Errorf("invalid samplingWindow: %w", err) + duration, err := parseDuration(admissionControl.SamplingWindow.Duration.String()) + if err != nil { + return nil, fmt.Errorf("invalid samplingWindow: %w", err) + } + config.SamplingWindow = durationpb.New(duration) } - 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}, + config.SrThreshold = &corev3.RuntimePercent{ + DefaultValue: &typev3.Percent{Value: *admissionControl.SuccessRateThreshold * 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, + config.Aggression = &corev3.RuntimeDouble{ + DefaultValue: *admissionControl.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, + config.RpsThreshold = &corev3.RuntimeUInt32{ + DefaultValue: *admissionControl.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}, + config.MaxRejectionProbability = &corev3.RuntimePercent{ + DefaultValue: &typev3.Percent{Value: *admissionControl.MaxRejectionProbability * 100.0}, + } } // Set success criteria (part of EvaluationCriteria oneof) @@ -232,23 +217,23 @@ func parseDuration(s string) (time.Duration, error) { // See https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc func grpcStatusCodeToUint32(name string) (uint32, bool) { codes := map[string]uint32{ - "OK": 0, - "CANCELLED": 1, - "UNKNOWN": 2, - "INVALID_ARGUMENT": 3, - "DEADLINE_EXCEEDED": 4, - "NOT_FOUND": 5, - "ALREADY_EXISTS": 6, - "PERMISSION_DENIED": 7, - "RESOURCE_EXHAUSTED": 8, - "FAILED_PRECONDITION": 9, - "ABORTED": 10, - "OUT_OF_RANGE": 11, - "UNIMPLEMENTED": 12, - "INTERNAL": 13, - "UNAVAILABLE": 14, - "DATA_LOSS": 15, - "UNAUTHENTICATED": 16, + "Ok": 0, + "Cancelled": 1, + "Unknown": 2, + "InvalidArgument": 3, + "DeadlineExceeded": 4, + "NotFound": 5, + "AlreadyExists": 6, + "PermissionDenied": 7, + "ResourceExhausted": 8, + "FailedPrecondition": 9, + "Aborted": 10, + "OutOfRange": 11, + "Unimplemented": 12, + "Internal": 13, + "Unavailable": 14, + "DataLoss": 15, + "Unauthenticated": 16, } code, ok := codes[name] return code, ok diff --git a/internal/xds/translator/admission_control_test.go b/internal/xds/translator/admission_control_test.go index 75d285e567..40e2151e8b 100644 --- a/internal/xds/translator/admission_control_test.go +++ b/internal/xds/translator/admission_control_test.go @@ -99,7 +99,7 @@ func TestBuildAdmissionControlConfig(t *testing.T) { HTTPSuccessStatus: []int32{200, 201, 300, 301}, }, GRPC: &ir.GRPCSuccessCriteria{ - GRPCSuccessStatus: []string{"OK", "CANCELLED"}, + GRPCSuccessStatus: []string{"Ok", "Cancelled"}, }, }, }, @@ -180,23 +180,6 @@ func TestBuildAdmissionControlConfig(t *testing.T) { require.NotNil(t, got.Enabled.DefaultValue) assert.True(t, got.Enabled.DefaultValue.Value) - // Verify sampling window is set - require.NotNil(t, got.SamplingWindow) - - // Verify sr threshold is set - require.NotNil(t, got.SrThreshold) - require.NotNil(t, got.SrThreshold.DefaultValue) - - // Verify aggression is set - require.NotNil(t, got.Aggression) - - // Verify RPS threshold is set - require.NotNil(t, got.RpsThreshold) - - // Verify max rejection probability is set - require.NotNil(t, got.MaxRejectionProbability) - require.NotNil(t, got.MaxRejectionProbability.DefaultValue) - // Verify evaluation criteria is always set require.NotNil(t, got.EvaluationCriteria) }) @@ -218,7 +201,7 @@ func TestBuildAdmissionControlConfigValues(t *testing.T) { HTTPSuccessStatus: []int32{200, 201, 202}, }, GRPC: &ir.GRPCSuccessCriteria{ - GRPCSuccessStatus: []string{"OK", "CANCELLED", "UNKNOWN"}, + GRPCSuccessStatus: []string{"Ok", "Cancelled", "Unknown"}, }, }, } @@ -247,27 +230,23 @@ func TestBuildAdmissionControlConfigValues(t *testing.T) { } func TestBuildAdmissionControlConfigDefaults(t *testing.T) { - // Test that defaults are correctly applied when no values are set + // When no values are set, fields should be nil so Envoy applies its own defaults + // (sampling_window=30s, sr_threshold=95%, aggression=1.0, rps_threshold=0, max_rejection_probability=80%). config := &ir.AdmissionControl{} got, err := buildAdmissionControlConfig(config) require.NoError(t, err) require.NotNil(t, got) - // Default sampling window is 60s - assert.Equal(t, int64(60), got.SamplingWindow.Seconds) + assert.Nil(t, got.SamplingWindow) + assert.Nil(t, got.SrThreshold) + assert.Nil(t, got.Aggression) + assert.Nil(t, got.RpsThreshold) + assert.Nil(t, got.MaxRejectionProbability) - // Default success rate threshold is 0.95 * 100 = 95.0 - assert.Equal(t, 95.0, got.SrThreshold.DefaultValue.Value) - - // Default aggression is 1.0 - assert.Equal(t, 1.0, got.Aggression.DefaultValue) - - // Default RPS threshold is 1 - assert.Equal(t, uint32(1), got.RpsThreshold.DefaultValue) - - // Default max rejection probability is 0.95 * 100 = 95.0 - assert.Equal(t, 95.0, got.MaxRejectionProbability.DefaultValue.Value) + // Enabled and EvaluationCriteria are always set + require.NotNil(t, got.Enabled) + require.NotNil(t, got.EvaluationCriteria) } func TestAdmissionControlPatchHCM(t *testing.T) { diff --git a/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml b/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml index 1515748e03..42cebd5f79 100644 --- a/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml @@ -44,8 +44,8 @@ http: successCriteria: grpc: grpcSuccessStatus: - - "OK" - - "CANCELLED" + - "Ok" + - "Cancelled" pathMatch: exact: "example" destination: diff --git a/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml index c62285ff8b..90af2cda29 100644 --- a/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml @@ -58,7 +58,6 @@ value: 95 rpsThreshold: defaultValue: 10 - samplingWindow: 60s srThreshold: defaultValue: value: 80 @@ -77,17 +76,6 @@ typedPerFilterConfig: envoy.filters.http.admission_control: '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl - aggression: - defaultValue: 1 enabled: defaultValue: true - maxRejectionProbability: - defaultValue: - value: 95 - rpsThreshold: - defaultValue: 1 - samplingWindow: 60s - srThreshold: - defaultValue: - value: 95 successCriteria: {} From 24f177b6cacb9937a7e8ae2103f4d68144b90404 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Mon, 2 Mar 2026 08:34:09 -0800 Subject: [PATCH 08/22] use envoy defaults Signed-off-by: Adam Buran --- ....envoyproxy.io_backendtrafficpolicies.yaml | 40 +++++++++---------- ....envoyproxy.io_backendtrafficpolicies.yaml | 40 +++++++++---------- site/content/en/latest/api/extension_types.md | 40 +++++++++---------- 3 files changed, 60 insertions(+), 60 deletions(-) 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 602aaf1efc..d9d9a8f0b8 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 @@ -72,21 +72,21 @@ spec: 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. + The value should be in the range [0.0, 1.0]. Defaults to 0.80 (80%) 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. + pass through the filter without rejection. Defaults to 0 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. + Defaults to 30s if not specified. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string successCriteria: @@ -105,23 +105,23 @@ spec: GRPCSuccessCode defines gRPC status codes as defined in https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. enum: - - OK - - CANCELLED - - UNKNOWN - - INVALID_ARGUMENT - - DEADLINE_EXCEEDED - - NOT_FOUND - - ALREADY_EXISTS - - PERMISSION_DENIED - - RESOURCE_EXHAUSTED - - FAILED_PRECONDITION - - ABORTED - - OUT_OF_RANGE - - UNIMPLEMENTED - - INTERNAL - - UNAVAILABLE - - DATA_LOSS - - UNAUTHENTICATED + - Ok + - Cancelled + - Unknown + - InvalidArgument + - DeadlineExceeded + - NotFound + - AlreadyExists + - PermissionDenied + - ResourceExhausted + - FailedPrecondition + - Aborted + - OutOfRange + - Unimplemented + - Internal + - Unavailable + - DataLoss + - Unauthenticated type: string type: array type: object diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index fee5023269..45467d8536 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -71,21 +71,21 @@ spec: 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. + The value should be in the range [0.0, 1.0]. Defaults to 0.80 (80%) 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. + pass through the filter without rejection. Defaults to 0 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. + Defaults to 30s if not specified. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string successCriteria: @@ -104,23 +104,23 @@ spec: GRPCSuccessCode defines gRPC status codes as defined in https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. enum: - - OK - - CANCELLED - - UNKNOWN - - INVALID_ARGUMENT - - DEADLINE_EXCEEDED - - NOT_FOUND - - ALREADY_EXISTS - - PERMISSION_DENIED - - RESOURCE_EXHAUSTED - - FAILED_PRECONDITION - - ABORTED - - OUT_OF_RANGE - - UNIMPLEMENTED - - INTERNAL - - UNAVAILABLE - - DATA_LOSS - - UNAUTHENTICATED + - Ok + - Cancelled + - Unknown + - InvalidArgument + - DeadlineExceeded + - NotFound + - AlreadyExists + - PermissionDenied + - ResourceExhausted + - FailedPrecondition + - Aborted + - OutOfRange + - Unimplemented + - Internal + - Unavailable + - DataLoss + - Unauthenticated type: string type: array type: object diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 0e73bd7a28..86fcce65d2 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -204,11 +204,11 @@ _Appears in:_ | 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://gateway-api.sigs.k8s.io/reference/1.4/spec/#duration)_ | false | | SamplingWindow defines the time window over which request success rates are calculated.
Defaults to 60s if not specified. | +| `samplingWindow` | _[Duration](https://gateway-api.sigs.k8s.io/reference/1.4/spec/#duration)_ | false | | SamplingWindow defines the time window over which request success rates are calculated.
Defaults to 30s 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. | +| `rpsThreshold` | _integer_ | false | | RPSThreshold defines the minimum requests per second below which requests will
pass through the filter without rejection. Defaults to 0 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.80 (80%) if not specified. | | `successCriteria` | _[AdmissionControlSuccessCriteria](#admissioncontrolsuccesscriteria)_ | false | | SuccessCriteria defines what constitutes a successful request for both HTTP and gRPC. | @@ -2495,23 +2495,23 @@ _Appears in:_ | Value | Description | | ----- | ----------- | -| `OK` | | -| `CANCELLED` | | -| `UNKNOWN` | | -| `INVALID_ARGUMENT` | | -| `DEADLINE_EXCEEDED` | | -| `NOT_FOUND` | | -| `ALREADY_EXISTS` | | -| `PERMISSION_DENIED` | | -| `RESOURCE_EXHAUSTED` | | -| `FAILED_PRECONDITION` | | -| `ABORTED` | | -| `OUT_OF_RANGE` | | -| `UNIMPLEMENTED` | | -| `INTERNAL` | | -| `UNAVAILABLE` | | -| `DATA_LOSS` | | -| `UNAUTHENTICATED` | | +| `Ok` | | +| `Cancelled` | | +| `Unknown` | | +| `InvalidArgument` | | +| `DeadlineExceeded` | | +| `NotFound` | | +| `AlreadyExists` | | +| `PermissionDenied` | | +| `ResourceExhausted` | | +| `FailedPrecondition` | | +| `Aborted` | | +| `OutOfRange` | | +| `Unimplemented` | | +| `Internal` | | +| `Unavailable` | | +| `DataLoss` | | +| `Unauthenticated` | | #### GRPCSuccessCriteria From 37b648fab683f93fe8f9bd99472cf5f1ae79d01c Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Fri, 6 Mar 2026 09:43:16 -0800 Subject: [PATCH 09/22] add out files Signed-off-by: Adam Buran --- test/helm/gateway-crds-helm/all.out.yaml | 40 +++++++++---------- .../envoy-gateway-crds.out.yaml | 40 +++++++++---------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 6af0cb4f6a..012b4a8eea 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -22599,21 +22599,21 @@ spec: 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. + The value should be in the range [0.0, 1.0]. Defaults to 0.80 (80%) 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. + pass through the filter without rejection. Defaults to 0 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. + Defaults to 30s if not specified. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string successCriteria: @@ -22632,23 +22632,23 @@ spec: GRPCSuccessCode defines gRPC status codes as defined in https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. enum: - - OK - - CANCELLED - - UNKNOWN - - INVALID_ARGUMENT - - DEADLINE_EXCEEDED - - NOT_FOUND - - ALREADY_EXISTS - - PERMISSION_DENIED - - RESOURCE_EXHAUSTED - - FAILED_PRECONDITION - - ABORTED - - OUT_OF_RANGE - - UNIMPLEMENTED - - INTERNAL - - UNAVAILABLE - - DATA_LOSS - - UNAUTHENTICATED + - Ok + - Cancelled + - Unknown + - InvalidArgument + - DeadlineExceeded + - NotFound + - AlreadyExists + - PermissionDenied + - ResourceExhausted + - FailedPrecondition + - Aborted + - OutOfRange + - Unimplemented + - Internal + - Unavailable + - DataLoss + - Unauthenticated type: string type: array type: object 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 70a40640c1..752f9fca08 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -572,21 +572,21 @@ spec: 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. + The value should be in the range [0.0, 1.0]. Defaults to 0.80 (80%) 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. + pass through the filter without rejection. Defaults to 0 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. + Defaults to 30s if not specified. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string successCriteria: @@ -605,23 +605,23 @@ spec: GRPCSuccessCode defines gRPC status codes as defined in https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. enum: - - OK - - CANCELLED - - UNKNOWN - - INVALID_ARGUMENT - - DEADLINE_EXCEEDED - - NOT_FOUND - - ALREADY_EXISTS - - PERMISSION_DENIED - - RESOURCE_EXHAUSTED - - FAILED_PRECONDITION - - ABORTED - - OUT_OF_RANGE - - UNIMPLEMENTED - - INTERNAL - - UNAVAILABLE - - DATA_LOSS - - UNAUTHENTICATED + - Ok + - Cancelled + - Unknown + - InvalidArgument + - DeadlineExceeded + - NotFound + - AlreadyExists + - PermissionDenied + - ResourceExhausted + - FailedPrecondition + - Aborted + - OutOfRange + - Unimplemented + - Internal + - Unavailable + - DataLoss + - Unauthenticated type: string type: array type: object From a75dfac7c8d1abe13a6099c845a2047125ef2c56 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sat, 7 Mar 2026 18:17:09 -0800 Subject: [PATCH 10/22] add testdata gen Signed-off-by: Adam Buran --- site/content/en/latest/api/extension_types.md | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 86fcce65d2..4d9b80e105 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -2495,23 +2495,23 @@ _Appears in:_ | Value | Description | | ----- | ----------- | -| `Ok` | | -| `Cancelled` | | -| `Unknown` | | -| `InvalidArgument` | | -| `DeadlineExceeded` | | -| `NotFound` | | -| `AlreadyExists` | | -| `PermissionDenied` | | -| `ResourceExhausted` | | -| `FailedPrecondition` | | -| `Aborted` | | -| `OutOfRange` | | -| `Unimplemented` | | -| `Internal` | | -| `Unavailable` | | -| `DataLoss` | | -| `Unauthenticated` | | +| `Ok` | | +| `Cancelled` | | +| `Unknown` | | +| `InvalidArgument` | | +| `DeadlineExceeded` | | +| `NotFound` | | +| `AlreadyExists` | | +| `PermissionDenied` | | +| `ResourceExhausted` | | +| `FailedPrecondition` | | +| `Aborted` | | +| `OutOfRange` | | +| `Unimplemented` | | +| `Internal` | | +| `Unavailable` | | +| `DataLoss` | | +| `Unauthenticated` | | #### GRPCSuccessCriteria From 4988451ba9fd433b4999baaabebfaa6fa76635b4 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sun, 8 Mar 2026 10:35:25 -0700 Subject: [PATCH 11/22] regenerate crds Signed-off-by: Adam Buran --- test/helm/gateway-crds-helm/e2e.out.yaml | 96 ++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 19bf076558..16bedceae5 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -550,6 +550,99 @@ 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 + 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.80 (80%) 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 0 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 30s if not specified. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + 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. + Status codes are defined in https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. + items: + description: |- + GRPCSuccessCode defines gRPC status codes as defined in + https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc. + enum: + - Ok + - Cancelled + - Unknown + - InvalidArgument + - DeadlineExceeded + - NotFound + - AlreadyExists + - PermissionDenied + - ResourceExhausted + - FailedPrecondition + - Aborted + - OutOfRange + - Unimplemented + - Internal + - Unavailable + - DataLoss + - Unauthenticated + type: string + type: array + type: object + http: + description: HTTP defines success criteria for HTTP requests. + properties: + httpSuccessStatus: + description: HTTPSuccessStatus defines HTTP status codes + that are considered successful. + items: + description: HTTPStatus defines the http status code. + maximum: 599 + minimum: 100 + type: integer + 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. @@ -9166,6 +9259,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -9197,6 +9291,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -9226,6 +9321,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz From 3f7d253319ea2f37b2956249742fa6c3213abe3a Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sat, 14 Mar 2026 21:22:09 -0700 Subject: [PATCH 12/22] setup admission control as http upstream filter Signed-off-by: Adam Buran --- examples/kubernetes/admission-control.yaml | 2 +- internal/xds/translator/admission_control.go | 128 +------- .../xds/translator/admission_control_test.go | 297 +++--------------- internal/xds/translator/cluster.go | 16 +- internal/xds/translator/httpfilters.go | 2 - .../out/xds-ir/admission-control.routes.yaml | 53 ---- .../xds-ir/geoip-authorization.clusters.yaml | 51 +++ internal/xds/translator/utils.go | 1 + 8 files changed, 128 insertions(+), 422 deletions(-) diff --git a/examples/kubernetes/admission-control.yaml b/examples/kubernetes/admission-control.yaml index 112bec984d..656f68af4b 100644 --- a/examples/kubernetes/admission-control.yaml +++ b/examples/kubernetes/admission-control.yaml @@ -29,7 +29,7 @@ spec: - 204 grpc: grpcSuccessStatus: - - OK + - Ok --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute diff --git a/internal/xds/translator/admission_control.go b/internal/xds/translator/admission_control.go index 8331844ded..03858daf1f 100644 --- a/internal/xds/translator/admission_control.go +++ b/internal/xds/translator/admission_control.go @@ -11,65 +11,29 @@ import ( "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() +// buildUpstreamAdmissionControlFilter builds an admission control filter for use +// as an upstream HTTP filter on a cluster. Envoy's admission_control filter is a +// "dual filter" that supports both the downstream (HCM) and upstream (cluster) +// extension categories, but does not support per-route typedPerFilterConfig. +// Placing it as an upstream filter gives per-cluster success-rate tracking. +func buildUpstreamAdmissionControlFilter(ac *ir.AdmissionControl) (*hcmv3.HttpFilter, error) { + config, err := buildAdmissionControlConfig(ac) if err != nil { - return err + return nil, 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) + configAny, err := proto.ToAnyWithValidation(config) if err != nil { return nil, err } @@ -77,50 +41,11 @@ func buildHCMAdmissionControlFilter() (*hcmv3.HttpFilter, error) { return &hcmv3.HttpFilter{ Name: string(egv1a1.EnvoyFilterAdmissionControl), ConfigType: &hcmv3.HttpFilter_TypedConfig{ - TypedConfig: admissionControlAny, + TypedConfig: configAny, }, }, nil } -// patchRoute patches the provided route with the admission control config if applicable. -func (*admissionControl) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute, _ *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 { @@ -138,8 +63,6 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss DefaultValue: &wrapperspb.BoolValue{Value: enabled}, } - // Only set fields the user explicitly configured; Envoy applies its own defaults - // (sampling_window=30s, sr_threshold=95%, aggression=1.0, rps_threshold=0, max_rejection_probability=80%). if admissionControl.SamplingWindow != nil { duration, err := parseDuration(admissionControl.SamplingWindow.Duration.String()) if err != nil { @@ -172,10 +95,10 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss } } + successCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria{} + // Set success criteria (part of EvaluationCriteria oneof) if admissionControl.SuccessCriteria != nil { - successCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria{} - // HTTP success criteria: each individual status code becomes a single-element range [code, code+1) if admissionControl.SuccessCriteria.HTTP != nil && len(admissionControl.SuccessCriteria.HTTP.HTTPSuccessStatus) > 0 { httpCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria_HttpCriteria{} @@ -188,7 +111,6 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss successCriteria.HttpCriteria = httpCriteria } - // gRPC success criteria: map string enum names to numeric codes if admissionControl.SuccessCriteria.GRPC != nil && len(admissionControl.SuccessCriteria.GRPC.GRPCSuccessStatus) > 0 { grpcCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria_GrpcCriteria{} for _, status := range admissionControl.SuccessCriteria.GRPC.GRPCSuccessStatus { @@ -208,7 +130,6 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss return config, nil } -// parseDuration parses a duration string and returns a time.Duration. func parseDuration(s string) (time.Duration, error) { return time.ParseDuration(s) } @@ -238,28 +159,3 @@ func grpcStatusCodeToUint32(name string) (uint32, bool) { code, ok := codes[name] return code, ok } - -// patchResources adds all the other needed resources referenced by this filter. -func (*admissionControl) patchResources(_ *types.ResourceVersionTable, _ []*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 index 40e2151e8b..16a296ff84 100644 --- a/internal/xds/translator/admission_control_test.go +++ b/internal/xds/translator/admission_control_test.go @@ -14,53 +14,6 @@ import ( "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 @@ -121,7 +74,7 @@ func TestBuildAdmissionControlConfig(t *testing.T) { config: &ir.AdmissionControl{ SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ GRPC: &ir.GRPCSuccessCriteria{ - GRPCSuccessStatus: []string{"OK"}, + GRPCSuccessStatus: []string{"Ok"}, }, }, }, @@ -175,19 +128,16 @@ func TestBuildAdmissionControlConfig(t *testing.T) { require.NoError(t, err) require.NotNil(t, got) - // Verify enabled is always true require.NotNil(t, got.Enabled) require.NotNil(t, got.Enabled.DefaultValue) assert.True(t, got.Enabled.DefaultValue.Value) - // Verify evaluation criteria is always set require.NotNil(t, got.EvaluationCriteria) }) } } func TestBuildAdmissionControlConfigValues(t *testing.T) { - // Test that specific values are correctly translated config := &ir.AdmissionControl{ SamplingWindow: &metav1.Duration{ Duration: 45 * time.Second, @@ -210,28 +160,15 @@ func TestBuildAdmissionControlConfigValues(t *testing.T) { require.NoError(t, err) require.NotNil(t, got) - // Verify sampling window (45 seconds) assert.Equal(t, int64(45), got.SamplingWindow.Seconds) - - // Verify success rate threshold (0.85 * 100 = 85.0) assert.Equal(t, 85.0, got.SrThreshold.DefaultValue.Value) - - // Verify aggression assert.Equal(t, 1.5, got.Aggression.DefaultValue) - - // Verify RPS threshold assert.Equal(t, uint32(20), got.RpsThreshold.DefaultValue) - - // Verify max rejection probability (0.75 * 100 = 75.0) assert.Equal(t, 75.0, got.MaxRejectionProbability.DefaultValue.Value) - - // Verify evaluation criteria is set (detailed verification done via testdata tests) require.NotNil(t, got.EvaluationCriteria) } func TestBuildAdmissionControlConfigDefaults(t *testing.T) { - // When no values are set, fields should be nil so Envoy applies its own defaults - // (sampling_window=30s, sr_threshold=95%, aggression=1.0, rps_threshold=0, max_rejection_probability=80%). config := &ir.AdmissionControl{} got, err := buildAdmissionControlConfig(config) @@ -244,219 +181,61 @@ func TestBuildAdmissionControlConfigDefaults(t *testing.T) { assert.Nil(t, got.RpsThreshold) assert.Nil(t, got.MaxRejectionProbability) - // Enabled and EvaluationCriteria are always set require.NotNil(t, got.Enabled) require.NotNil(t, got.EvaluationCriteria) } -func TestAdmissionControlPatchHCM(t *testing.T) { - ac := &admissionControl{} - +func TestBuildUpstreamAdmissionControlFilter(t *testing.T) { tests := []struct { - name string - mgr *hcmv3.HttpConnectionManager - listener *ir.HTTPListener - wantErr bool - wantLen int // expected number of http filters after patching + name string + ac *ir.AdmissionControl + wantErr bool }{ { - name: "nil hcm", - mgr: nil, - listener: &ir.HTTPListener{}, - wantErr: true, - }, - { - name: "nil listener", - mgr: &hcmv3.HttpConnectionManager{}, - listener: nil, - wantErr: true, + name: "nil config", + ac: nil, + wantErr: true, }, { - name: "no routes with admission control", - mgr: &hcmv3.HttpConnectionManager{}, - listener: &ir.HTTPListener{ - Routes: []*ir.HTTPRoute{ - {Traffic: &ir.TrafficFeatures{}}, - }, - }, + name: "empty config", + ac: &ir.AdmissionControl{}, wantErr: false, - wantLen: 0, }, { - name: "route with admission control", - mgr: &hcmv3.HttpConnectionManager{}, - listener: &ir.HTTPListener{ - Routes: []*ir.HTTPRoute{ - { - Traffic: &ir.TrafficFeatures{ - AdmissionControl: &ir.AdmissionControl{}, - }, - }, - }, - }, - wantErr: false, - wantLen: 1, - }, - { - name: "filter already exists", - mgr: &hcmv3.HttpConnectionManager{ - HttpFilters: []*hcmv3.HttpFilter{ - {Name: string(egv1a1.EnvoyFilterAdmissionControl)}, + name: "full config", + ac: &ir.AdmissionControl{ + SamplingWindow: &metav1.Duration{ + Duration: 30 * time.Second, }, - }, - listener: &ir.HTTPListener{ - Routes: []*ir.HTTPRoute{ - { - Traffic: &ir.TrafficFeatures{ - AdmissionControl: &ir.AdmissionControl{}, - }, + SuccessRateThreshold: ptr.To(0.95), + Aggression: ptr.To(1.0), + RPSThreshold: ptr.To(uint32(5)), + MaxRejectionProbability: ptr.To(0.80), + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ + HTTP: &ir.HTTPSuccessCriteria{ + HTTPSuccessStatus: []int32{200, 201}, }, }, }, wantErr: false, - wantLen: 1, // Should not add duplicate }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ac.patchHCM(tt.mgr, tt.listener) + filter, err := buildUpstreamAdmissionControlFilter(tt.ac) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) - assert.Len(t, tt.mgr.HttpFilters, tt.wantLen) + require.NotNil(t, filter) + assert.Equal(t, string(egv1a1.EnvoyFilterAdmissionControl), filter.Name) + assert.NotNil(t, filter.ConfigType) }) } } -func TestAdmissionControlPatchRoute(t *testing.T) { - ac := &admissionControl{} - - tests := []struct { - name string - route *routev3.Route - irRoute *ir.HTTPRoute - listener *ir.HTTPListener - wantErr bool - wantCfg bool // whether typedPerFilterConfig should be set - }{ - { - name: "nil route", - route: nil, - irRoute: &ir.HTTPRoute{}, - listener: &ir.HTTPListener{}, - wantErr: false, - wantCfg: false, - }, - { - name: "nil ir route", - route: &routev3.Route{}, - irRoute: nil, - listener: &ir.HTTPListener{}, - wantErr: false, - wantCfg: false, - }, - { - name: "route without traffic features", - route: &routev3.Route{}, - irRoute: &ir.HTTPRoute{ - Traffic: nil, - }, - listener: &ir.HTTPListener{}, - wantErr: false, - wantCfg: false, - }, - { - name: "route without admission control", - route: &routev3.Route{}, - irRoute: &ir.HTTPRoute{ - Traffic: &ir.TrafficFeatures{}, - }, - listener: &ir.HTTPListener{}, - wantErr: false, - wantCfg: false, - }, - { - name: "route with admission control", - route: &routev3.Route{}, - irRoute: &ir.HTTPRoute{ - Traffic: &ir.TrafficFeatures{ - AdmissionControl: &ir.AdmissionControl{}, - }, - }, - listener: &ir.HTTPListener{}, - wantErr: false, - wantCfg: true, - }, - { - name: "route with full admission control config", - route: &routev3.Route{}, - irRoute: &ir.HTTPRoute{ - Traffic: &ir.TrafficFeatures{ - AdmissionControl: &ir.AdmissionControl{ - SamplingWindow: &metav1.Duration{ - Duration: 30 * time.Second, - }, - SuccessRateThreshold: ptr.To(0.90), - Aggression: ptr.To(2.0), - RPSThreshold: ptr.To(uint32(10)), - MaxRejectionProbability: ptr.To(0.80), - SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ - HTTP: &ir.HTTPSuccessCriteria{ - HTTPSuccessStatus: []int32{200, 201, 202}, - }, - }, - }, - }, - }, - listener: &ir.HTTPListener{}, - wantErr: false, - wantCfg: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := ac.patchRoute(tt.route, tt.irRoute, tt.listener) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - if tt.wantCfg { - require.NotNil(t, tt.route.TypedPerFilterConfig) - assert.Contains(t, tt.route.TypedPerFilterConfig, string(egv1a1.EnvoyFilterAdmissionControl)) - } - }) - } -} - -func TestAdmissionControlPatchResources(t *testing.T) { - ac := &admissionControl{} - tCtx := &types.ResourceVersionTable{} - routes := []*ir.HTTPRoute{ - { - Traffic: &ir.TrafficFeatures{ - AdmissionControl: &ir.AdmissionControl{}, - }, - }, - } - - // patchResources should always return nil since admission control doesn't need additional resources - err := ac.patchResources(tCtx, routes) - require.NoError(t, err) -} - -func TestBuildHCMAdmissionControlFilter(t *testing.T) { - filter, err := buildHCMAdmissionControlFilter() - require.NoError(t, err) - require.NotNil(t, filter) - assert.Equal(t, string(egv1a1.EnvoyFilterAdmissionControl), filter.Name) - assert.NotNil(t, filter.ConfigType) -} - func TestParseDuration(t *testing.T) { tests := []struct { name string @@ -501,3 +280,29 @@ func TestParseDuration(t *testing.T) { }) } } + +func TestGRPCStatusCodeToUint32(t *testing.T) { + tests := []struct { + name string + input string + want uint32 + wantOk bool + }{ + {name: "Ok", input: "Ok", want: 0, wantOk: true}, + {name: "Cancelled", input: "Cancelled", want: 1, wantOk: true}, + {name: "Unavailable", input: "Unavailable", want: 14, wantOk: true}, + {name: "Unauthenticated", input: "Unauthenticated", want: 16, wantOk: true}, + {name: "invalid code", input: "Invalid", want: 0, wantOk: false}, + {name: "wrong case OK", input: "OK", want: 0, wantOk: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := grpcStatusCodeToUint32(tt.input) + assert.Equal(t, tt.wantOk, ok) + if tt.wantOk { + assert.Equal(t, tt.want, got) + } + }) + } +} diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go index 674520c4c1..c1b4e1619a 100644 --- a/internal/xds/translator/cluster.go +++ b/internal/xds/translator/cluster.go @@ -75,6 +75,7 @@ type xdsClusterArgs struct { metrics *ir.Metrics backendConnection *ir.BackendConnection dns *ir.DNS + admissionControl *ir.AdmissionControl useClientProtocol bool ipFamily *egv1a1.IPFamily metadata *ir.ResourceMetadata @@ -1057,7 +1058,8 @@ func buildTypedExtensionProtocolOptions(args *xdsClusterArgs, requiresAutoHTTPCo requiresHTTP1Options := args.http1Settings != nil && (args.http1Settings.EnableTrailers || args.http1Settings.PreserveHeaderCase || args.http1Settings.HTTP10 != nil) - requiresHTTPFilters := len(args.settings) > 0 && args.settings[0].Filters != nil && args.settings[0].Filters.CredentialInjection != nil + requiresHTTPFilters := (len(args.settings) > 0 && args.settings[0].Filters != nil && args.settings[0].Filters.CredentialInjection != nil) || + args.admissionControl != nil requiredHTTPProtocolOptions := args.useClientProtocol || requiresAutoHTTPConfig || requiresCommonHTTPOptions || requiresHTTP1Options || requiresHTTP2Options || requiresHTTPFilters || requiresAutoSNI @@ -1165,8 +1167,7 @@ func buildTypedExtensionProtocolOptions(args *xdsClusterArgs, requiresAutoHTTPCo return extensionOptions, secrets, nil } -// buildClusterHTTPFilters builds the HTTP filters for the cluster. -// EG only supports credential injector filter for now, more filters can be added in the future. +// buildClusterHTTPFilters builds the upstream HTTP filters for the cluster. func buildClusterHTTPFilters(args *xdsClusterArgs) ([]*hcmv3.HttpFilter, []*tlsv3.Secret, error) { filters := make([]*hcmv3.HttpFilter, 0) secrets := make([]*tlsv3.Secret, 0) @@ -1185,6 +1186,14 @@ func buildClusterHTTPFilters(args *xdsClusterArgs) ([]*hcmv3.HttpFilter, []*tlsv } } + if args.admissionControl != nil { + filter, err := buildUpstreamAdmissionControlFilter(args.admissionControl) + if err != nil { + return nil, nil, err + } + filters = append(filters, filter) + } + // UpstreamCodec filter is required as the terminal filter for the upstream HTTP filters. if len(filters) > 0 { upstreamCodec, err := buildUpstreamCodecFilter() @@ -1193,7 +1202,6 @@ func buildClusterHTTPFilters(args *xdsClusterArgs) ([]*hcmv3.HttpFilter, []*tlsv } filters = append(filters, upstreamCodec) } - // We may need to add more Cluster filters in the future, so we return a slice of filters. return filters, secrets, nil } diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go index 113fa33397..0c22c69304 100644 --- a/internal/xds/translator/httpfilters.go +++ b/internal/xds/translator/httpfilters.go @@ -104,8 +104,6 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter { order = 1 case isFilterType(filter, egv1a1.EnvoyFilterFault): order = 2 - case isFilterType(filter, egv1a1.EnvoyFilterAdmissionControl): - order = 3 case isFilterType(filter, egv1a1.EnvoyFilterCORS): order = 4 case isFilterType(filter, egv1a1.EnvoyFilterHeaderMutation): diff --git a/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml index 90af2cda29..67715e26e3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml @@ -12,33 +12,6 @@ cluster: first-route-dest upgradeConfigs: - upgradeType: websocket - typedPerFilterConfig: - envoy.filters.http.admission_control: - '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl - aggression: - defaultValue: 1 - enabled: - defaultValue: true - maxRejectionProbability: - defaultValue: - value: 80 - rpsThreshold: - defaultValue: 5 - samplingWindow: 30s - srThreshold: - defaultValue: - value: 95 - successCriteria: - httpCriteria: - httpSuccessStatus: - - end: 201 - start: 200 - - end: 202 - start: 201 - - end: 203 - start: 202 - - end: 205 - start: 204 - match: path: example name: second-route @@ -46,26 +19,6 @@ cluster: second-route-dest upgradeConfigs: - upgradeType: websocket - typedPerFilterConfig: - envoy.filters.http.admission_control: - '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl - aggression: - defaultValue: 2 - enabled: - defaultValue: true - maxRejectionProbability: - defaultValue: - value: 95 - rpsThreshold: - defaultValue: 10 - srThreshold: - defaultValue: - value: 80 - successCriteria: - grpcCriteria: - grpcSuccessStatus: - - 0 - - 1 - match: path: test name: third-route @@ -73,9 +26,3 @@ cluster: third-route-dest upgradeConfigs: - upgradeType: websocket - typedPerFilterConfig: - envoy.filters.http.admission_control: - '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl - enabled: - defaultValue: true - successCriteria: {} diff --git a/internal/xds/translator/testdata/out/xds-ir/geoip-authorization.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/geoip-authorization.clusters.yaml index 64bb1ce74f..d0c7304bb0 100644 --- a/internal/xds/translator/testdata/out/xds-ir/geoip-authorization.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/geoip-authorization.clusters.yaml @@ -29,6 +29,42 @@ name: httproute/default/httproute-plain/rule/0 perConnectionBufferLimitBytes: 32768 type: EDS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicitHttpConfig: + httpProtocolOptions: {} + httpFilters: + - name: envoy.filters.http.admission_control + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl + aggression: + defaultValue: 1 + enabled: + defaultValue: true + maxRejectionProbability: + defaultValue: + value: 80 + rpsThreshold: + defaultValue: 5 + samplingWindow: 30s + srThreshold: + defaultValue: + value: 95 + successCriteria: + httpCriteria: + httpSuccessStatus: + - end: 201 + start: 200 + - end: 202 + start: 201 + - end: 203 + start: 202 + - end: 205 + start: 204 + - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec - circuitBreakers: thresholds: - maxRetries: 1024 @@ -60,3 +96,18 @@ name: httproute/default/httproute-geo/rule/0 perConnectionBufferLimitBytes: 32768 type: EDS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicitHttpConfig: + httpProtocolOptions: {} + httpFilters: + - name: envoy.filters.http.admission_control + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl + enabled: + defaultValue: true + successCriteria: {} + - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec diff --git a/internal/xds/translator/utils.go b/internal/xds/translator/utils.go index 2ee3efb81a..03c928c8a2 100644 --- a/internal/xds/translator/utils.go +++ b/internal/xds/translator/utils.go @@ -220,6 +220,7 @@ func applyTraffic(args *xdsClusterArgs, traffic *ir.TrafficFeatures) { args.backendConnection = traffic.BackendConnection args.dns = traffic.DNS args.http2Settings = traffic.HTTP2 + args.admissionControl = traffic.AdmissionControl } // determineIPFamily determines the IP family based on multiple destination settings From 3696a2ccb1ba64aa4b33c399622a662ad0c6c59e Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sun, 22 Mar 2026 10:35:51 -0700 Subject: [PATCH 13/22] change to aggression Signed-off-by: Adam Buran --- api/v1alpha1/admission_control.go | 3 ++- .../gateway.envoyproxy.io_backendtrafficpolicies.yaml | 3 ++- .../gateway.envoyproxy.io_backendtrafficpolicies.yaml | 3 ++- site/content/en/latest/api/extension_types.md | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/api/v1alpha1/admission_control.go b/api/v1alpha1/admission_control.go index 24c635465d..e896c65bfe 100644 --- a/api/v1alpha1/admission_control.go +++ b/api/v1alpha1/admission_control.go @@ -38,10 +38,11 @@ type AdmissionControl struct { // 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. + // Envoy clamps values below 1.0 to 1.0. // Defaults to 1.0 if not specified. // // +optional - // +kubebuilder:validation:Minimum=0.0 + // +kubebuilder:validation:Minimum=1.0 Aggression *float64 `json:"aggression,omitempty"` // RPSThreshold defines the minimum requests per second below which requests will 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 d9d9a8f0b8..cfc7ea4b54 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 @@ -61,8 +61,9 @@ spec: 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. + Envoy clamps values below 1.0 to 1.0. Defaults to 1.0 if not specified. - minimum: 0 + minimum: 1 type: number enabled: description: |- diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index 45467d8536..3f558fe1c0 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -60,8 +60,9 @@ spec: 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. + Envoy clamps values below 1.0 to 1.0. Defaults to 1.0 if not specified. - minimum: 0 + minimum: 1 type: number enabled: description: |- diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 4d9b80e105..a0edcb2561 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -206,7 +206,7 @@ _Appears in:_ | `enabled` | _boolean_ | false | | Enabled enables or disables the admission control filter.
Defaults to true if not specified. | | `samplingWindow` | _[Duration](https://gateway-api.sigs.k8s.io/reference/1.4/spec/#duration)_ | false | | SamplingWindow defines the time window over which request success rates are calculated.
Defaults to 30s 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. | +| `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.
Envoy clamps values below 1.0 to 1.0.
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 0 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.80 (80%) if not specified. | | `successCriteria` | _[AdmissionControlSuccessCriteria](#admissioncontrolsuccesscriteria)_ | false | | SuccessCriteria defines what constitutes a successful request for both HTTP and gRPC. | From 566c3a9d4eeb3990abdcce9ea8ff354c5bee8d13 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sun, 22 Mar 2026 10:35:51 -0700 Subject: [PATCH 14/22] change to aggression Signed-off-by: Adam Buran --- test/helm/gateway-crds-helm/all.out.yaml | 3 ++- test/helm/gateway-crds-helm/e2e.out.yaml | 3 ++- test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 012b4a8eea..156732b3da 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -22588,8 +22588,9 @@ spec: 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. + Envoy clamps values below 1.0 to 1.0. Defaults to 1.0 if not specified. - minimum: 0 + minimum: 1 type: number enabled: description: |- diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 16bedceae5..e16d71d68a 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -561,8 +561,9 @@ spec: 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. + Envoy clamps values below 1.0 to 1.0. Defaults to 1.0 if not specified. - minimum: 0 + minimum: 1 type: number maxRejectionProbability: description: |- 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 752f9fca08..db3cd69e0f 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -561,8 +561,9 @@ spec: 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. + Envoy clamps values below 1.0 to 1.0. Defaults to 1.0 if not specified. - minimum: 0 + minimum: 1 type: number enabled: description: |- From a16039d917bff555676676281ca49aad1c34e71b Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Mon, 30 Mar 2026 16:07:17 -0700 Subject: [PATCH 15/22] fix: resolve rebase conflicts and regenerate testdata Fix merge conflicts from rebase onto upstream/main, fix missing imports in admission control tests, and ensure EvaluationCriteria is always set in the admission control config. Signed-off-by: Adam Buran --- api/v1alpha1/zz_generated.deepcopy.go | 12 -- internal/ir/zz_generated.deepcopy.go | 2 +- internal/xds/translator/admission_control.go | 9 +- .../xds/translator/admission_control_test.go | 4 + .../xds-ir/admission-control.clusters.yaml | 152 ++++++++++++++++++ .../xds-ir/admission-control.endpoints.yaml | 36 +++++ .../xds-ir/admission-control.listeners.yaml | 35 ++++ .../xds-ir/geoip-authorization.clusters.yaml | 51 ------ test/helm/gateway-crds-helm/e2e.out.yaml | 8 +- 9 files changed, 238 insertions(+), 71 deletions(-) create mode 100644 internal/xds/translator/testdata/out/xds-ir/admission-control.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/admission-control.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/admission-control.listeners.yaml diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 71e6ad4493..294cf7a163 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -6767,18 +6767,6 @@ func (in *ProxyTracing) DeepCopyInto(out *ProxyTracing) { *out = new(uint32) **out = **in } - if in.SamplingFraction != nil { - in, out := &in.SamplingFraction, &out.SamplingFraction - *out = new(apisv1.Fraction) - (*in).DeepCopyInto(*out) - } - if in.CustomTags != nil { - in, out := &in.CustomTags, &out.CustomTags - *out = make(map[string]CustomTag, len(*in)) - for key, val := range *in { - (*out)[key] = *val.DeepCopy() - } - } in.Provider.DeepCopyInto(&out.Provider) } diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 5576333ae5..9ba299c1b3 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -309,7 +309,7 @@ func (in *AdmissionControl) DeepCopyInto(out *AdmissionControl) { } if in.SamplingWindow != nil { in, out := &in.SamplingWindow, &out.SamplingWindow - *out = new(v1.Duration) + *out = new(metav1.Duration) **out = **in } if in.SuccessRateThreshold != nil { diff --git a/internal/xds/translator/admission_control.go b/internal/xds/translator/admission_control.go index 03858daf1f..a4dbfaebbe 100644 --- a/internal/xds/translator/admission_control.go +++ b/internal/xds/translator/admission_control.go @@ -121,10 +121,11 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss successCriteria.GrpcCriteria = grpcCriteria } - // Set as EvaluationCriteria (oneof field) - config.EvaluationCriteria = &admissioncontrolv3.AdmissionControl_SuccessCriteria_{ - SuccessCriteria: successCriteria, - } + } + + // Always set EvaluationCriteria (required field) + config.EvaluationCriteria = &admissioncontrolv3.AdmissionControl_SuccessCriteria_{ + SuccessCriteria: successCriteria, } return config, nil diff --git a/internal/xds/translator/admission_control_test.go b/internal/xds/translator/admission_control_test.go index 16a296ff84..5818984af5 100644 --- a/internal/xds/translator/admission_control_test.go +++ b/internal/xds/translator/admission_control_test.go @@ -7,10 +7,14 @@ package translator import ( "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/ir" ) diff --git a/internal/xds/translator/testdata/out/xds-ir/admission-control.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/admission-control.clusters.yaml new file mode 100644 index 0000000000..19d49650ea --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/admission-control.clusters.yaml @@ -0,0 +1,152 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + ignoreHealthOnHostRemoval: true + lbPolicy: LEAST_REQUEST + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: first-route-dest + perConnectionBufferLimitBytes: 32768 + type: EDS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicitHttpConfig: + httpProtocolOptions: {} + httpFilters: + - name: envoy.filters.http.admission_control + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl + aggression: + defaultValue: 1 + enabled: + defaultValue: true + maxRejectionProbability: + defaultValue: + value: 80 + rpsThreshold: + defaultValue: 5 + samplingWindow: 30s + srThreshold: + defaultValue: + value: 95 + successCriteria: + httpCriteria: + httpSuccessStatus: + - end: 201 + start: 200 + - end: 202 + start: 201 + - end: 203 + start: 202 + - end: 205 + start: 204 + - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: second-route-dest + ignoreHealthOnHostRemoval: true + lbPolicy: LEAST_REQUEST + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: second-route-dest + perConnectionBufferLimitBytes: 32768 + type: EDS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicitHttpConfig: + httpProtocolOptions: {} + httpFilters: + - name: envoy.filters.http.admission_control + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl + aggression: + defaultValue: 2 + enabled: + defaultValue: true + maxRejectionProbability: + defaultValue: + value: 95 + rpsThreshold: + defaultValue: 10 + srThreshold: + defaultValue: + value: 80 + successCriteria: + grpcCriteria: + grpcSuccessStatus: + - 0 + - 1 + - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: third-route-dest + ignoreHealthOnHostRemoval: true + lbPolicy: LEAST_REQUEST + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: third-route-dest + perConnectionBufferLimitBytes: 32768 + type: EDS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicitHttpConfig: + httpProtocolOptions: {} + httpFilters: + - name: envoy.filters.http.admission_control + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl + enabled: + defaultValue: true + successCriteria: {} + - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec diff --git a/internal/xds/translator/testdata/out/xds-ir/admission-control.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/admission-control.endpoints.yaml new file mode 100644 index 0000000000..475b89a087 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/admission-control.endpoints.yaml @@ -0,0 +1,36 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: first-route-dest/backend/0 +- clusterName: second-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: second-route-dest/backend/0 +- clusterName: third-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: third-route-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/admission-control.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/admission-control.listeners.yaml new file mode 100644 index 0000000000..5dd5e46e3c --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/admission-control.listeners.yaml @@ -0,0 +1,35 @@ +- address: + socketAddress: + address: '::' + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-10080 + useRemoteAddress: true + name: first-listener + maxConnectionsToAcceptPerSocketEvent: 1 + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/geoip-authorization.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/geoip-authorization.clusters.yaml index d0c7304bb0..64bb1ce74f 100644 --- a/internal/xds/translator/testdata/out/xds-ir/geoip-authorization.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/geoip-authorization.clusters.yaml @@ -29,42 +29,6 @@ name: httproute/default/httproute-plain/rule/0 perConnectionBufferLimitBytes: 32768 type: EDS - typedExtensionProtocolOptions: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - explicitHttpConfig: - httpProtocolOptions: {} - httpFilters: - - name: envoy.filters.http.admission_control - typedConfig: - '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl - aggression: - defaultValue: 1 - enabled: - defaultValue: true - maxRejectionProbability: - defaultValue: - value: 80 - rpsThreshold: - defaultValue: 5 - samplingWindow: 30s - srThreshold: - defaultValue: - value: 95 - successCriteria: - httpCriteria: - httpSuccessStatus: - - end: 201 - start: 200 - - end: 202 - start: 201 - - end: 203 - start: 202 - - end: 205 - start: 204 - - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec - typedConfig: - '@type': type.googleapis.com/envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec - circuitBreakers: thresholds: - maxRetries: 1024 @@ -96,18 +60,3 @@ name: httproute/default/httproute-geo/rule/0 perConnectionBufferLimitBytes: 32768 type: EDS - typedExtensionProtocolOptions: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - explicitHttpConfig: - httpProtocolOptions: {} - httpFilters: - - name: envoy.filters.http.admission_control - typedConfig: - '@type': type.googleapis.com/envoy.extensions.filters.http.admission_control.v3.AdmissionControl - enabled: - defaultValue: true - successCriteria: {} - - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec - typedConfig: - '@type': type.googleapis.com/envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index e16d71d68a..dde1bf47dc 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -565,6 +565,11 @@ spec: Defaults to 1.0 if not specified. minimum: 1 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. @@ -9260,7 +9265,6 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault - - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -9292,7 +9296,6 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault - - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -9322,7 +9325,6 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault - - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz From 807a476d160dd4fb6c11c3a5208cb2f5bc4ced75 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Fri, 10 Apr 2026 14:36:44 -0700 Subject: [PATCH 16/22] fix: address review feedback for admission control - Remove Enabled field (presence of admissionControl implies enabled) - Switch SuccessRateThreshold, Aggression, MaxRejectionProbability from float64 to uint32 to avoid floats in the API layer - Register envoy.filters.http.admission_control in the EnvoyFilter enum - Fix example manifest (srThreshold -> successRateThreshold, integer values) - Regenerate CRDs, deepcopy, docs, and xds translator testdata Signed-off-by: Adam Buran --- api/v1alpha1/admission_control.go | 38 ++++++++---------- api/v1alpha1/envoyproxy_types.go | 2 +- api/v1alpha1/zz_generated.deepcopy.go | 11 ++--- ....envoyproxy.io_backendtrafficpolicies.yaml | 36 ++++++++--------- .../gateway.envoyproxy.io_envoyproxies.yaml | 3 ++ ....envoyproxy.io_backendtrafficpolicies.yaml | 36 ++++++++--------- .../gateway.envoyproxy.io_envoyproxies.yaml | 3 ++ examples/kubernetes/admission-control.yaml | 9 ++--- internal/gatewayapi/backendtrafficpolicy.go | 1 - internal/ir/xds.go | 15 ++++--- internal/ir/zz_generated.deepcopy.go | 11 ++--- internal/xds/translator/admission_control.go | 20 ++++------ .../xds/translator/admission_control_test.go | 40 +++++++++---------- .../testdata/in/xds-ir/admission-control.yaml | 12 +++--- site/content/en/latest/api/extension_types.md | 7 ++-- 15 files changed, 110 insertions(+), 134 deletions(-) diff --git a/api/v1alpha1/admission_control.go b/api/v1alpha1/admission_control.go index e896c65bfe..4aec5bd441 100644 --- a/api/v1alpha1/admission_control.go +++ b/api/v1alpha1/admission_control.go @@ -14,36 +14,30 @@ import ( // of previous requests in a configurable sliding time window. // All fields are optional and will use Envoy's defaults when not specified. 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 30s if not specified. // // +optional SamplingWindow *gwapiv1.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. + // SuccessRateThreshold is the lowest request success rate, as a percentage in the + // range [1, 100], at which the filter will not reject requests. Defaults to 95 if + // not specified. Envoy rejects values below 1%, so values lower than 1 are not allowed. // // +optional - // +kubebuilder:validation:Minimum=0.0 - // +kubebuilder:validation:Maximum=1.0 - SuccessRateThreshold *float64 `json:"successRateThreshold,omitempty"` + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=100 + SuccessRateThreshold *uint32 `json:"successRateThreshold,omitempty"` - // Aggression controls the rejection probability curve. A value of 1.0 means a linear + // Aggression controls the rejection probability curve. A value of 1 means a linear // increase in rejection probability as the success rate decreases. Higher values // result in more aggressive rejection at higher success rates. - // Envoy clamps values below 1.0 to 1.0. - // Defaults to 1.0 if not specified. + // Envoy requires aggression to be greater than 0 and clamps values below 1 to 1. + // Defaults to 1 if not specified. // // +optional - // +kubebuilder:validation:Minimum=1.0 - Aggression *float64 `json:"aggression,omitempty"` + // +kubebuilder:validation:Minimum=1 + Aggression *uint32 `json:"aggression,omitempty"` // RPSThreshold defines the minimum requests per second below which requests will // pass through the filter without rejection. Defaults to 0 if not specified. @@ -52,13 +46,13 @@ type AdmissionControl struct { // +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.80 (80%) if not specified. + // MaxRejectionProbability represents the upper limit of the rejection probability, + // expressed as a percentage in the range [0, 100]. Defaults to 80 if not specified. // // +optional - // +kubebuilder:validation:Minimum=0.0 - // +kubebuilder:validation:Maximum=1.0 - MaxRejectionProbability *float64 `json:"maxRejectionProbability,omitempty"` + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=100 + MaxRejectionProbability *uint32 `json:"maxRejectionProbability,omitempty"` // SuccessCriteria defines what constitutes a successful request for both HTTP and gRPC. // diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index aef415d180..09f52483cf 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -277,7 +277,7 @@ type FilterPosition struct { } // EnvoyFilter defines the type of Envoy HTTP filter. -// +kubebuilder:validation:Enum=envoy.filters.http.custom_response;envoy.filters.http.health_check;envoy.filters.http.fault;envoy.filters.http.cors;envoy.filters.http.header_mutation;envoy.filters.http.ext_authz;envoy.filters.http.api_key_auth;envoy.filters.http.basic_auth;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.stateful_session;envoy.filters.http.buffer;envoy.filters.http.lua;envoy.filters.http.ext_proc;envoy.filters.http.wasm;envoy.filters.http.dynamic_modules;envoy.filters.http.geoip;envoy.filters.http.rbac;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit;envoy.filters.http.grpc_web;envoy.filters.http.grpc_stats;envoy.filters.http.credential_injector;envoy.filters.http.compressor;envoy.filters.http.dynamic_forward_proxy +// +kubebuilder:validation:Enum=envoy.filters.http.custom_response;envoy.filters.http.health_check;envoy.filters.http.fault;envoy.filters.http.admission_control;envoy.filters.http.cors;envoy.filters.http.header_mutation;envoy.filters.http.ext_authz;envoy.filters.http.api_key_auth;envoy.filters.http.basic_auth;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.stateful_session;envoy.filters.http.buffer;envoy.filters.http.lua;envoy.filters.http.ext_proc;envoy.filters.http.wasm;envoy.filters.http.dynamic_modules;envoy.filters.http.geoip;envoy.filters.http.rbac;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit;envoy.filters.http.grpc_web;envoy.filters.http.grpc_stats;envoy.filters.http.credential_injector;envoy.filters.http.compressor;envoy.filters.http.dynamic_forward_proxy type EnvoyFilter string const ( diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 294cf7a163..21a4251ed3 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -207,11 +207,6 @@ func (in *ActiveHealthCheckPayload) DeepCopy() *ActiveHealthCheckPayload { // 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) @@ -219,12 +214,12 @@ func (in *AdmissionControl) DeepCopyInto(out *AdmissionControl) { } if in.SuccessRateThreshold != nil { in, out := &in.SuccessRateThreshold, &out.SuccessRateThreshold - *out = new(float64) + *out = new(uint32) **out = **in } if in.Aggression != nil { in, out := &in.Aggression, &out.Aggression - *out = new(float64) + *out = new(uint32) **out = **in } if in.RPSThreshold != nil { @@ -234,7 +229,7 @@ func (in *AdmissionControl) DeepCopyInto(out *AdmissionControl) { } if in.MaxRejectionProbability != nil { in, out := &in.MaxRejectionProbability, &out.MaxRejectionProbability - *out = new(float64) + *out = new(uint32) **out = **in } if in.SuccessCriteria != nil { 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 cfc7ea4b54..c00b460d6b 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 @@ -58,25 +58,22 @@ spec: properties: aggression: description: |- - Aggression controls the rejection probability curve. A value of 1.0 means a linear + Aggression controls the rejection probability curve. A value of 1 means a linear increase in rejection probability as the success rate decreases. Higher values result in more aggressive rejection at higher success rates. - Envoy clamps values below 1.0 to 1.0. - Defaults to 1.0 if not specified. + Envoy requires aggression to be greater than 0 and clamps values below 1 to 1. + Defaults to 1 if not specified. + format: int32 minimum: 1 - type: number - enabled: - description: |- - Enabled enables or disables the admission control filter. - Defaults to true if not specified. - type: boolean + type: integer 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.80 (80%) if not specified. - maximum: 1 + MaxRejectionProbability represents the upper limit of the rejection probability, + expressed as a percentage in the range [0, 100]. Defaults to 80 if not specified. + format: int32 + maximum: 100 minimum: 0 - type: number + type: integer rpsThreshold: description: |- RPSThreshold defines the minimum requests per second below which requests will @@ -142,12 +139,13 @@ spec: 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 + SuccessRateThreshold is the lowest request success rate, as a percentage in the + range [1, 100], at which the filter will not reject requests. Defaults to 95 if + not specified. Envoy rejects values below 1%, so values lower than 1 are not allowed. + format: int32 + maximum: 100 + minimum: 1 + type: integer type: object circuitBreaker: description: |- diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml index ef3e316706..d7a6b12311 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -475,6 +475,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -506,6 +507,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -535,6 +537,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index 3f558fe1c0..30c803d13b 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -57,25 +57,22 @@ spec: properties: aggression: description: |- - Aggression controls the rejection probability curve. A value of 1.0 means a linear + Aggression controls the rejection probability curve. A value of 1 means a linear increase in rejection probability as the success rate decreases. Higher values result in more aggressive rejection at higher success rates. - Envoy clamps values below 1.0 to 1.0. - Defaults to 1.0 if not specified. + Envoy requires aggression to be greater than 0 and clamps values below 1 to 1. + Defaults to 1 if not specified. + format: int32 minimum: 1 - type: number - enabled: - description: |- - Enabled enables or disables the admission control filter. - Defaults to true if not specified. - type: boolean + type: integer 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.80 (80%) if not specified. - maximum: 1 + MaxRejectionProbability represents the upper limit of the rejection probability, + expressed as a percentage in the range [0, 100]. Defaults to 80 if not specified. + format: int32 + maximum: 100 minimum: 0 - type: number + type: integer rpsThreshold: description: |- RPSThreshold defines the minimum requests per second below which requests will @@ -141,12 +138,13 @@ spec: 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 + SuccessRateThreshold is the lowest request success rate, as a percentage in the + range [1, 100], at which the filter will not reject requests. Defaults to 95 if + not specified. Envoy rejects values below 1%, so values lower than 1 are not allowed. + format: int32 + maximum: 100 + minimum: 1 + type: integer type: object circuitBreaker: description: |- diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index 0ec6ddd5ca..eec8f75b96 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -474,6 +474,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -505,6 +506,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -534,6 +536,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz diff --git a/examples/kubernetes/admission-control.yaml b/examples/kubernetes/admission-control.yaml index 656f68af4b..91623a5a19 100644 --- a/examples/kubernetes/admission-control.yaml +++ b/examples/kubernetes/admission-control.yaml @@ -15,12 +15,11 @@ spec: name: example-route namespace: default admissionControl: - enabled: true samplingWindow: 30s - srThreshold: 0.95 - aggression: 1.0 - rpsThreshold: 5.0 - maxRejectionProbability: 0.8 + successRateThreshold: 95 + aggression: 1 + rpsThreshold: 5 + maxRejectionProbability: 80 successCriteria: http: httpSuccessStatus: diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index 6375410704..810cdabd3b 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -1709,7 +1709,6 @@ func (t *Translator) buildAdmissionControl(policy *egv1a1.BackendTrafficPolicy) } ac := &ir.AdmissionControl{ - Enabled: policy.Spec.AdmissionControl.Enabled, SamplingWindow: parseSamplingWindow(policy.Spec.AdmissionControl.SamplingWindow), SuccessRateThreshold: policy.Spec.AdmissionControl.SuccessRateThreshold, Aggression: policy.Spec.AdmissionControl.Aggression, diff --git a/internal/ir/xds.go b/internal/ir/xds.go index bf0ff7b88f..3e736b4904 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -1660,20 +1660,19 @@ type FaultInjectionAbort struct { // // +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"` + // SuccessRateThreshold is the lowest request success rate, as a percentage in the + // range [1, 100], at which the filter will not reject requests. + SuccessRateThreshold *uint32 `json:"successRateThreshold,omitempty" yaml:"successRateThreshold,omitempty"` // Aggression controls the rejection probability curve. - Aggression *float64 `json:"aggression,omitempty" yaml:"aggression,omitempty"` + Aggression *uint32 `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"` + // MaxRejectionProbability represents the upper limit of the rejection probability, + // as a percentage in the range [0, 100]. + MaxRejectionProbability *uint32 `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"` } diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 9ba299c1b3..9a65084da7 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -302,11 +302,6 @@ func (in *AddHeader) DeepCopy() *AddHeader { // 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(metav1.Duration) @@ -314,12 +309,12 @@ func (in *AdmissionControl) DeepCopyInto(out *AdmissionControl) { } if in.SuccessRateThreshold != nil { in, out := &in.SuccessRateThreshold, &out.SuccessRateThreshold - *out = new(float64) + *out = new(uint32) **out = **in } if in.Aggression != nil { in, out := &in.Aggression, &out.Aggression - *out = new(float64) + *out = new(uint32) **out = **in } if in.RPSThreshold != nil { @@ -329,7 +324,7 @@ func (in *AdmissionControl) DeepCopyInto(out *AdmissionControl) { } if in.MaxRejectionProbability != nil { in, out := &in.MaxRejectionProbability, &out.MaxRejectionProbability - *out = new(float64) + *out = new(uint32) **out = **in } if in.SuccessCriteria != nil { diff --git a/internal/xds/translator/admission_control.go b/internal/xds/translator/admission_control.go index a4dbfaebbe..eb5ee47917 100644 --- a/internal/xds/translator/admission_control.go +++ b/internal/xds/translator/admission_control.go @@ -52,15 +52,11 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss 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}, + // The filter is enabled whenever the policy is configured. + config := &admissioncontrolv3.AdmissionControl{ + Enabled: &corev3.RuntimeFeatureFlag{ + DefaultValue: &wrapperspb.BoolValue{Value: true}, + }, } if admissionControl.SamplingWindow != nil { @@ -73,13 +69,13 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss if admissionControl.SuccessRateThreshold != nil { config.SrThreshold = &corev3.RuntimePercent{ - DefaultValue: &typev3.Percent{Value: *admissionControl.SuccessRateThreshold * 100.0}, + DefaultValue: &typev3.Percent{Value: float64(*admissionControl.SuccessRateThreshold)}, } } if admissionControl.Aggression != nil { config.Aggression = &corev3.RuntimeDouble{ - DefaultValue: *admissionControl.Aggression, + DefaultValue: float64(*admissionControl.Aggression), } } @@ -91,7 +87,7 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss if admissionControl.MaxRejectionProbability != nil { config.MaxRejectionProbability = &corev3.RuntimePercent{ - DefaultValue: &typev3.Percent{Value: *admissionControl.MaxRejectionProbability * 100.0}, + DefaultValue: &typev3.Percent{Value: float64(*admissionControl.MaxRejectionProbability)}, } } diff --git a/internal/xds/translator/admission_control_test.go b/internal/xds/translator/admission_control_test.go index 5818984af5..39767ea1f2 100644 --- a/internal/xds/translator/admission_control_test.go +++ b/internal/xds/translator/admission_control_test.go @@ -25,10 +25,8 @@ func TestBuildAdmissionControlConfig(t *testing.T) { wantErr bool }{ { - name: "valid admission control config", - config: &ir.AdmissionControl{ - Enabled: func() *bool { b := true; return &b }(), - }, + name: "valid admission control config", + config: &ir.AdmissionControl{}, wantErr: false, }, { @@ -47,10 +45,10 @@ func TestBuildAdmissionControlConfig(t *testing.T) { SamplingWindow: &metav1.Duration{ Duration: 30 * time.Second, }, - SuccessRateThreshold: ptr.To(0.90), - Aggression: ptr.To(2.0), + SuccessRateThreshold: ptr.To(uint32(90)), + Aggression: ptr.To(uint32(2)), RPSThreshold: ptr.To(uint32(10)), - MaxRejectionProbability: ptr.To(0.80), + MaxRejectionProbability: ptr.To(uint32(80)), SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ HTTP: &ir.HTTPSuccessCriteria{ HTTPSuccessStatus: []int32{200, 201, 300, 301}, @@ -101,22 +99,22 @@ func TestBuildAdmissionControlConfig(t *testing.T) { wantErr: false, }, { - name: "config with zero thresholds", + name: "config with min thresholds", config: &ir.AdmissionControl{ - SuccessRateThreshold: ptr.To(0.0), - Aggression: ptr.To(0.0), + SuccessRateThreshold: ptr.To(uint32(1)), + Aggression: ptr.To(uint32(1)), RPSThreshold: ptr.To(uint32(0)), - MaxRejectionProbability: ptr.To(0.0), + MaxRejectionProbability: ptr.To(uint32(0)), }, wantErr: false, }, { name: "config with max thresholds", config: &ir.AdmissionControl{ - SuccessRateThreshold: ptr.To(1.0), - Aggression: ptr.To(10.0), + SuccessRateThreshold: ptr.To(uint32(100)), + Aggression: ptr.To(uint32(10)), RPSThreshold: ptr.To(uint32(1000)), - MaxRejectionProbability: ptr.To(1.0), + MaxRejectionProbability: ptr.To(uint32(100)), }, wantErr: false, }, @@ -146,10 +144,10 @@ func TestBuildAdmissionControlConfigValues(t *testing.T) { SamplingWindow: &metav1.Duration{ Duration: 45 * time.Second, }, - SuccessRateThreshold: ptr.To(0.85), - Aggression: ptr.To(1.5), + SuccessRateThreshold: ptr.To(uint32(85)), + Aggression: ptr.To(uint32(2)), RPSThreshold: ptr.To(uint32(20)), - MaxRejectionProbability: ptr.To(0.75), + MaxRejectionProbability: ptr.To(uint32(75)), SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ HTTP: &ir.HTTPSuccessCriteria{ HTTPSuccessStatus: []int32{200, 201, 202}, @@ -166,7 +164,7 @@ func TestBuildAdmissionControlConfigValues(t *testing.T) { assert.Equal(t, int64(45), got.SamplingWindow.Seconds) assert.Equal(t, 85.0, got.SrThreshold.DefaultValue.Value) - assert.Equal(t, 1.5, got.Aggression.DefaultValue) + assert.Equal(t, 2.0, got.Aggression.DefaultValue) assert.Equal(t, uint32(20), got.RpsThreshold.DefaultValue) assert.Equal(t, 75.0, got.MaxRejectionProbability.DefaultValue.Value) require.NotNil(t, got.EvaluationCriteria) @@ -211,10 +209,10 @@ func TestBuildUpstreamAdmissionControlFilter(t *testing.T) { SamplingWindow: &metav1.Duration{ Duration: 30 * time.Second, }, - SuccessRateThreshold: ptr.To(0.95), - Aggression: ptr.To(1.0), + SuccessRateThreshold: ptr.To(uint32(95)), + Aggression: ptr.To(uint32(1)), RPSThreshold: ptr.To(uint32(5)), - MaxRejectionProbability: ptr.To(0.80), + MaxRejectionProbability: ptr.To(uint32(80)), SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ HTTP: &ir.HTTPSuccessCriteria{ HTTPSuccessStatus: []int32{200, 201}, diff --git a/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml b/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml index 42cebd5f79..a1dbb0dca2 100644 --- a/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml @@ -13,10 +13,10 @@ http: traffic: admissionControl: samplingWindow: 30s - successRateThreshold: 0.95 - aggression: 1.0 + successRateThreshold: 95 + aggression: 1 rpsThreshold: 5 - maxRejectionProbability: 0.8 + maxRejectionProbability: 80 successCriteria: http: httpSuccessStatus: @@ -37,10 +37,10 @@ http: hostname: "*" traffic: admissionControl: - successRateThreshold: 0.80 - aggression: 2.0 + successRateThreshold: 80 + aggression: 2 rpsThreshold: 10 - maxRejectionProbability: 0.95 + maxRejectionProbability: 95 successCriteria: grpc: grpcSuccessStatus: diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index a0edcb2561..0bf732dc2f 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -203,12 +203,11 @@ _Appears in:_ | 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://gateway-api.sigs.k8s.io/reference/1.4/spec/#duration)_ | false | | SamplingWindow defines the time window over which request success rates are calculated.
Defaults to 30s 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.
Envoy clamps values below 1.0 to 1.0.
Defaults to 1.0 if not specified. | +| `successRateThreshold` | _integer_ | false | | SuccessRateThreshold is the lowest request success rate, as a percentage in the
range [1, 100], at which the filter will not reject requests. Defaults to 95 if
not specified. Envoy rejects values below 1%, so values lower than 1 are not allowed. | +| `aggression` | _integer_ | false | | Aggression controls the rejection probability curve. A value of 1 means a linear
increase in rejection probability as the success rate decreases. Higher values
result in more aggressive rejection at higher success rates.
Envoy requires aggression to be greater than 0 and clamps values below 1 to 1.
Defaults to 1 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 0 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.80 (80%) if not specified. | +| `maxRejectionProbability` | _integer_ | false | | MaxRejectionProbability represents the upper limit of the rejection probability,
expressed as a percentage in the range [0, 100]. Defaults to 80 if not specified. | | `successCriteria` | _[AdmissionControlSuccessCriteria](#admissioncontrolsuccesscriteria)_ | false | | SuccessCriteria defines what constitutes a successful request for both HTTP and gRPC. | From 97fc2daae731dd6505a36199cff91e9d6e4ad816 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sat, 11 Apr 2026 08:53:05 -0700 Subject: [PATCH 17/22] test: regenerate admission-control testdata Remove stale lbPolicy field from cluster output after main merge. Signed-off-by: Adam Buran --- .../testdata/out/xds-ir/admission-control.clusters.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/xds/translator/testdata/out/xds-ir/admission-control.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/admission-control.clusters.yaml index 19d49650ea..8604a630ed 100644 --- a/internal/xds/translator/testdata/out/xds-ir/admission-control.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/admission-control.clusters.yaml @@ -10,7 +10,6 @@ resourceApiVersion: V3 serviceName: first-route-dest ignoreHealthOnHostRemoval: true - lbPolicy: LEAST_REQUEST loadBalancingPolicy: policies: - typedExtensionConfig: @@ -70,7 +69,6 @@ resourceApiVersion: V3 serviceName: second-route-dest ignoreHealthOnHostRemoval: true - lbPolicy: LEAST_REQUEST loadBalancingPolicy: policies: - typedExtensionConfig: @@ -123,7 +121,6 @@ resourceApiVersion: V3 serviceName: third-route-dest ignoreHealthOnHostRemoval: true - lbPolicy: LEAST_REQUEST loadBalancingPolicy: policies: - typedExtensionConfig: From ead9d6d6e69973d3720e597b9cb46ba00c2faac8 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sat, 11 Apr 2026 13:12:23 -0700 Subject: [PATCH 18/22] test: regenerate helm-template snapshots and extension docs Regenerates test/helm/gateway-crds-helm/*.out.yaml and site/content/en/latest/api/extension_types.md to match the current admission control API (int32 percentages, enabled field removed) and pick up envoy.filters.http.admission_control in the filter order enum. Signed-off-by: Adam Buran --- site/content/en/latest/api/extension_types.md | 2 +- test/helm/gateway-crds-helm/all.out.yaml | 39 ++++++++++--------- test/helm/gateway-crds-helm/e2e.out.yaml | 39 ++++++++++--------- .../envoy-gateway-crds.out.yaml | 39 ++++++++++--------- 4 files changed, 61 insertions(+), 58 deletions(-) diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index b8970f695d..07381e70f4 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -203,7 +203,7 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `samplingWindow` | _[Duration](https://gateway-api.sigs.k8s.io/reference/1.4/spec/#duration)_ | false | | SamplingWindow defines the time window over which request success rates are calculated.
Defaults to 30s if not specified. | +| `samplingWindow` | _[Duration](https://gateway-api.sigs.k8s.io/reference/1.5/spec/#duration)_ | false | | SamplingWindow defines the time window over which request success rates are calculated.
Defaults to 30s if not specified. | | `successRateThreshold` | _integer_ | false | | SuccessRateThreshold is the lowest request success rate, as a percentage in the
range [1, 100], at which the filter will not reject requests. Defaults to 95 if
not specified. Envoy rejects values below 1%, so values lower than 1 are not allowed. | | `aggression` | _integer_ | false | | Aggression controls the rejection probability curve. A value of 1 means a linear
increase in rejection probability as the success rate decreases. Higher values
result in more aggressive rejection at higher success rates.
Envoy requires aggression to be greater than 0 and clamps values below 1 to 1.
Defaults to 1 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 0 if not specified. | diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index b7a44b5625..108ea02c08 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -22585,25 +22585,22 @@ spec: properties: aggression: description: |- - Aggression controls the rejection probability curve. A value of 1.0 means a linear + Aggression controls the rejection probability curve. A value of 1 means a linear increase in rejection probability as the success rate decreases. Higher values result in more aggressive rejection at higher success rates. - Envoy clamps values below 1.0 to 1.0. - Defaults to 1.0 if not specified. + Envoy requires aggression to be greater than 0 and clamps values below 1 to 1. + Defaults to 1 if not specified. + format: int32 minimum: 1 - type: number - enabled: - description: |- - Enabled enables or disables the admission control filter. - Defaults to true if not specified. - type: boolean + type: integer 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.80 (80%) if not specified. - maximum: 1 + MaxRejectionProbability represents the upper limit of the rejection probability, + expressed as a percentage in the range [0, 100]. Defaults to 80 if not specified. + format: int32 + maximum: 100 minimum: 0 - type: number + type: integer rpsThreshold: description: |- RPSThreshold defines the minimum requests per second below which requests will @@ -22669,12 +22666,13 @@ spec: 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 + SuccessRateThreshold is the lowest request success rate, as a percentage in the + range [1, 100], at which the filter will not reject requests. Defaults to 95 if + not specified. Envoy rejects values below 1%, so values lower than 1 are not allowed. + format: int32 + maximum: 100 + minimum: 1 + type: integer type: object circuitBreaker: description: |- @@ -31408,6 +31406,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -31439,6 +31438,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -31468,6 +31468,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 150216bbdd..9e9dd08c2f 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -558,25 +558,22 @@ spec: properties: aggression: description: |- - Aggression controls the rejection probability curve. A value of 1.0 means a linear + Aggression controls the rejection probability curve. A value of 1 means a linear increase in rejection probability as the success rate decreases. Higher values result in more aggressive rejection at higher success rates. - Envoy clamps values below 1.0 to 1.0. - Defaults to 1.0 if not specified. + Envoy requires aggression to be greater than 0 and clamps values below 1 to 1. + Defaults to 1 if not specified. + format: int32 minimum: 1 - type: number - enabled: - description: |- - Enabled enables or disables the admission control filter. - Defaults to true if not specified. - type: boolean + type: integer 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.80 (80%) if not specified. - maximum: 1 + MaxRejectionProbability represents the upper limit of the rejection probability, + expressed as a percentage in the range [0, 100]. Defaults to 80 if not specified. + format: int32 + maximum: 100 minimum: 0 - type: number + type: integer rpsThreshold: description: |- RPSThreshold defines the minimum requests per second below which requests will @@ -642,12 +639,13 @@ spec: 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 + SuccessRateThreshold is the lowest request success rate, as a percentage in the + range [1, 100], at which the filter will not reject requests. Defaults to 95 if + not specified. Envoy rejects values below 1%, so values lower than 1 are not allowed. + format: int32 + maximum: 100 + minimum: 1 + type: integer type: object circuitBreaker: description: |- @@ -9381,6 +9379,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -9412,6 +9411,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -9441,6 +9441,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz 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 af735545a1..553e395587 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -558,25 +558,22 @@ spec: properties: aggression: description: |- - Aggression controls the rejection probability curve. A value of 1.0 means a linear + Aggression controls the rejection probability curve. A value of 1 means a linear increase in rejection probability as the success rate decreases. Higher values result in more aggressive rejection at higher success rates. - Envoy clamps values below 1.0 to 1.0. - Defaults to 1.0 if not specified. + Envoy requires aggression to be greater than 0 and clamps values below 1 to 1. + Defaults to 1 if not specified. + format: int32 minimum: 1 - type: number - enabled: - description: |- - Enabled enables or disables the admission control filter. - Defaults to true if not specified. - type: boolean + type: integer 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.80 (80%) if not specified. - maximum: 1 + MaxRejectionProbability represents the upper limit of the rejection probability, + expressed as a percentage in the range [0, 100]. Defaults to 80 if not specified. + format: int32 + maximum: 100 minimum: 0 - type: number + type: integer rpsThreshold: description: |- RPSThreshold defines the minimum requests per second below which requests will @@ -642,12 +639,13 @@ spec: 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 + SuccessRateThreshold is the lowest request success rate, as a percentage in the + range [1, 100], at which the filter will not reject requests. Defaults to 95 if + not specified. Envoy rejects values below 1%, so values lower than 1 are not allowed. + format: int32 + maximum: 100 + minimum: 1 + type: integer type: object circuitBreaker: description: |- @@ -9381,6 +9379,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -9412,6 +9411,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz @@ -9441,6 +9441,7 @@ spec: - envoy.filters.http.custom_response - envoy.filters.http.health_check - envoy.filters.http.fault + - envoy.filters.http.admission_control - envoy.filters.http.cors - envoy.filters.http.header_mutation - envoy.filters.http.ext_authz From a65fc3d1c05a4d6f0206027787f5b50dcb87da50 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Thu, 16 Apr 2026 21:09:48 -0700 Subject: [PATCH 19/22] fix(admission-control): use new() instead of ptr.To to satisfy forbidigo lint Signed-off-by: Adam Buran --- .../xds/translator/admission_control_test.go | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/internal/xds/translator/admission_control_test.go b/internal/xds/translator/admission_control_test.go index 39767ea1f2..90edcdc175 100644 --- a/internal/xds/translator/admission_control_test.go +++ b/internal/xds/translator/admission_control_test.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/ir" @@ -45,10 +44,10 @@ func TestBuildAdmissionControlConfig(t *testing.T) { SamplingWindow: &metav1.Duration{ Duration: 30 * time.Second, }, - SuccessRateThreshold: ptr.To(uint32(90)), - Aggression: ptr.To(uint32(2)), - RPSThreshold: ptr.To(uint32(10)), - MaxRejectionProbability: ptr.To(uint32(80)), + SuccessRateThreshold: new(uint32(90)), + Aggression: new(uint32(2)), + RPSThreshold: new(uint32(10)), + MaxRejectionProbability: new(uint32(80)), SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ HTTP: &ir.HTTPSuccessCriteria{ HTTPSuccessStatus: []int32{200, 201, 300, 301}, @@ -101,20 +100,20 @@ func TestBuildAdmissionControlConfig(t *testing.T) { { name: "config with min thresholds", config: &ir.AdmissionControl{ - SuccessRateThreshold: ptr.To(uint32(1)), - Aggression: ptr.To(uint32(1)), - RPSThreshold: ptr.To(uint32(0)), - MaxRejectionProbability: ptr.To(uint32(0)), + SuccessRateThreshold: new(uint32(1)), + Aggression: new(uint32(1)), + RPSThreshold: new(uint32(0)), + MaxRejectionProbability: new(uint32(0)), }, wantErr: false, }, { name: "config with max thresholds", config: &ir.AdmissionControl{ - SuccessRateThreshold: ptr.To(uint32(100)), - Aggression: ptr.To(uint32(10)), - RPSThreshold: ptr.To(uint32(1000)), - MaxRejectionProbability: ptr.To(uint32(100)), + SuccessRateThreshold: new(uint32(100)), + Aggression: new(uint32(10)), + RPSThreshold: new(uint32(1000)), + MaxRejectionProbability: new(uint32(100)), }, wantErr: false, }, @@ -144,10 +143,10 @@ func TestBuildAdmissionControlConfigValues(t *testing.T) { SamplingWindow: &metav1.Duration{ Duration: 45 * time.Second, }, - SuccessRateThreshold: ptr.To(uint32(85)), - Aggression: ptr.To(uint32(2)), - RPSThreshold: ptr.To(uint32(20)), - MaxRejectionProbability: ptr.To(uint32(75)), + SuccessRateThreshold: new(uint32(85)), + Aggression: new(uint32(2)), + RPSThreshold: new(uint32(20)), + MaxRejectionProbability: new(uint32(75)), SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ HTTP: &ir.HTTPSuccessCriteria{ HTTPSuccessStatus: []int32{200, 201, 202}, @@ -209,10 +208,10 @@ func TestBuildUpstreamAdmissionControlFilter(t *testing.T) { SamplingWindow: &metav1.Duration{ Duration: 30 * time.Second, }, - SuccessRateThreshold: ptr.To(uint32(95)), - Aggression: ptr.To(uint32(1)), - RPSThreshold: ptr.To(uint32(5)), - MaxRejectionProbability: ptr.To(uint32(80)), + SuccessRateThreshold: new(uint32(95)), + Aggression: new(uint32(1)), + RPSThreshold: new(uint32(5)), + MaxRejectionProbability: new(uint32(80)), SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ HTTP: &ir.HTTPSuccessCriteria{ HTTPSuccessStatus: []int32{200, 201}, From a98b9e2213ca2217409dad7a3352529e866aec27 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Fri, 17 Apr 2026 08:49:16 -0700 Subject: [PATCH 20/22] address review feedback: revert HCM filter ordering, hoist gRPC code map, add gatewayapi testdata - Revert the HCM HTTP filter ordering shifts in newOrderedHTTPFilter; the admission_control filter is registered as an upstream cluster filter, so it does not need a slot in the downstream HCM ordering. - Move the gRPC status-code lookup map to a package-level var so it is not rebuilt on every translation cycle. - Add backendtrafficpolicy-admission-control gatewayapi testdata covering both gateway-targeted (gRPC success criteria, all knobs set) and route-targeted (HTTP success criteria, minimal config) policies. Signed-off-by: Adam Buran Signed-off-by: Adam Buran --- ...endtrafficpolicy-admission-control.in.yaml | 108 +++++ ...ndtrafficpolicy-admission-control.out.yaml | 457 ++++++++++++++++++ internal/xds/translator/admission_control.go | 44 +- internal/xds/translator/httpfilters.go | 20 +- 4 files changed, 598 insertions(+), 31 deletions(-) create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-admission-control.in.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-admission-control.out.yaml diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-admission-control.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-admission-control.in.yaml new file mode 100644 index 0000000000..0b3678fb3f --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-admission-control.in.yaml @@ -0,0 +1,108 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-2 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +grpcRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + namespace: default + name: grpcroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-2 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + generation: 10 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + admissionControl: + samplingWindow: 60s + successRateThreshold: 90 + aggression: 2 + rpsThreshold: 10 + maxRejectionProbability: 70 + successCriteria: + grpc: + grpcSuccessStatus: + - Ok + - Cancelled + - DeadlineExceeded + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route + generation: 20 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + admissionControl: + samplingWindow: 30s + successRateThreshold: 95 + successCriteria: + http: + httpSuccessStatus: + - 200 + - 201 + - 204 diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-admission-control.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-admission-control.out.yaml new file mode 100644 index 0000000000..7cad5fccb1 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-admission-control.out.yaml @@ -0,0 +1,457 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + generation: 20 + name: policy-for-route + namespace: default + spec: + admissionControl: + samplingWindow: 30s + successCriteria: + http: + httpSuccessStatus: + - 200 + - 201 + - 204 + successRateThreshold: 95 + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + observedGeneration: 20 + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + observedGeneration: 20 + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + generation: 10 + name: policy-for-gateway + namespace: envoy-gateway + spec: + admissionControl: + aggression: 2 + maxRejectionProbability: 70 + rpsThreshold: 10 + samplingWindow: 60s + successCriteria: + grpc: + grpcSuccessStatus: + - Ok + - Cancelled + - DeadlineExceeded + successRateThreshold: 90 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + observedGeneration: 10 + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + observedGeneration: 10 + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + name: grpcroute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-2 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system + envoy-gateway/gateway-2: + proxy: + listeners: + - name: envoy-gateway/gateway-2/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-2 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-2 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + grpc: + enableGRPCStats: true + enableGRPCWeb: true + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: GRPCRoute + name: grpcroute-1 + namespace: default + name: grpcroute/default/grpcroute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: grpcroute/default/grpcroute-1/rule/0/backend/0 + protocol: GRPC + weight: 1 + hostname: '*' + isHTTP2: true + metadata: + kind: GRPCRoute + name: grpcroute-1 + namespace: default + policies: + - kind: BackendTrafficPolicy + name: policy-for-gateway + namespace: envoy-gateway + name: grpcroute/default/grpcroute-1/rule/0/match/-1/* + traffic: + admissionControl: + aggression: 2 + maxRejectionProbability: 70 + rpsThreshold: 10 + samplingWindow: 1m0s + successCriteria: + grpc: + grpcSuccessStatus: + - Ok + - Cancelled + - DeadlineExceeded + successRateThreshold: 90 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 + envoy-gateway/gateway-2: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-2-4a0e4eb9 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-2 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-2-4a0e4eb9 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-2 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-2 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-2/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + policies: + - kind: BackendTrafficPolicy + name: policy-for-route + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + admissionControl: + samplingWindow: 30s + successCriteria: + http: + httpSuccessStatus: + - 200 + - 201 + - 204 + successRateThreshold: 95 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/xds/translator/admission_control.go b/internal/xds/translator/admission_control.go index eb5ee47917..3660af728b 100644 --- a/internal/xds/translator/admission_control.go +++ b/internal/xds/translator/admission_control.go @@ -131,28 +131,30 @@ func parseDuration(s string) (time.Duration, error) { return time.ParseDuration(s) } -// grpcStatusCodeToUint32 maps a gRPC status code string name to its numeric value. +// grpcStatusCodes maps a gRPC status code string name to its numeric value. // See https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc +var grpcStatusCodes = map[string]uint32{ + "Ok": 0, + "Cancelled": 1, + "Unknown": 2, + "InvalidArgument": 3, + "DeadlineExceeded": 4, + "NotFound": 5, + "AlreadyExists": 6, + "PermissionDenied": 7, + "ResourceExhausted": 8, + "FailedPrecondition": 9, + "Aborted": 10, + "OutOfRange": 11, + "Unimplemented": 12, + "Internal": 13, + "Unavailable": 14, + "DataLoss": 15, + "Unauthenticated": 16, +} + +// grpcStatusCodeToUint32 maps a gRPC status code string name to its numeric value. func grpcStatusCodeToUint32(name string) (uint32, bool) { - codes := map[string]uint32{ - "Ok": 0, - "Cancelled": 1, - "Unknown": 2, - "InvalidArgument": 3, - "DeadlineExceeded": 4, - "NotFound": 5, - "AlreadyExists": 6, - "PermissionDenied": 7, - "ResourceExhausted": 8, - "FailedPrecondition": 9, - "Aborted": 10, - "OutOfRange": 11, - "Unimplemented": 12, - "Internal": 13, - "Unavailable": 14, - "DataLoss": 15, - "Unauthenticated": 16, - } - code, ok := codes[name] + code, ok := grpcStatusCodes[name] return code, ok } diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go index 0c22c69304..b51ccc5283 100644 --- a/internal/xds/translator/httpfilters.go +++ b/internal/xds/translator/httpfilters.go @@ -105,26 +105,26 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter { case isFilterType(filter, egv1a1.EnvoyFilterFault): order = 2 case isFilterType(filter, egv1a1.EnvoyFilterCORS): - order = 4 + order = 3 case isFilterType(filter, egv1a1.EnvoyFilterHeaderMutation): // Ensure header mutation run before ext auth which might consume the header. - order = 5 + order = 4 case isFilterType(filter, egv1a1.EnvoyFilterExtAuthz): - order = 6 + order = 5 case isFilterType(filter, egv1a1.EnvoyFilterAPIKeyAuth): - order = 7 + order = 6 case isFilterType(filter, egv1a1.EnvoyFilterBasicAuth): - order = 8 + order = 7 case isFilterType(filter, egv1a1.EnvoyFilterOAuth2): - order = 9 + order = 8 case isFilterType(filter, egv1a1.EnvoyFilterJWTAuthn): - order = 10 + order = 9 case isFilterType(filter, egv1a1.EnvoyFilterSessionPersistence): - order = 11 + order = 10 case isFilterType(filter, egv1a1.EnvoyFilterBuffer): - order = 12 + order = 11 case isFilterType(filter, egv1a1.EnvoyFilterLua): - order = 13 + mustGetFilterIndex(filter.Name) + order = 12 + mustGetFilterIndex(filter.Name) case isFilterType(filter, egv1a1.EnvoyFilterExtProc): order = 100 + mustGetFilterIndex(filter.Name) case isFilterType(filter, egv1a1.EnvoyFilterWasm): From 7b7bfdbe8e06e513c1ceea08ec9646054ec07627 Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Sat, 18 Apr 2026 16:35:44 -0700 Subject: [PATCH 21/22] Remove namespace from admission control targetRef Signed-off-by: Adam Buran --- examples/kubernetes/admission-control.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/kubernetes/admission-control.yaml b/examples/kubernetes/admission-control.yaml index 91623a5a19..bed213b278 100644 --- a/examples/kubernetes/admission-control.yaml +++ b/examples/kubernetes/admission-control.yaml @@ -13,7 +13,6 @@ spec: group: gateway.networking.k8s.io kind: HTTPRoute name: example-route - namespace: default admissionControl: samplingWindow: 30s successRateThreshold: 95 From 16fc1f467c6f11aa1eb0752873bdffe0ba07dafe Mon Sep 17 00:00:00 2001 From: Adam Buran Date: Fri, 24 Apr 2026 08:05:18 -0700 Subject: [PATCH 22/22] Address admission control review feedback Signed-off-by: Adam Buran --- api/v1alpha1/admission_control.go | 3 + ....envoyproxy.io_backendtrafficpolicies.yaml | 7 +++ ....envoyproxy.io_backendtrafficpolicies.yaml | 7 +++ internal/gatewayapi/backendtrafficpolicy.go | 19 ++---- internal/xds/translator/admission_control.go | 12 +--- .../xds/translator/admission_control_test.go | 45 -------------- .../backendtrafficpolicy_test.go | 61 +++++++++++++++++++ test/helm/gateway-crds-helm/all.out.yaml | 7 +++ test/helm/gateway-crds-helm/e2e.out.yaml | 7 +++ .../envoy-gateway-crds.out.yaml | 7 +++ 10 files changed, 106 insertions(+), 69 deletions(-) diff --git a/api/v1alpha1/admission_control.go b/api/v1alpha1/admission_control.go index 4aec5bd441..9588d476ca 100644 --- a/api/v1alpha1/admission_control.go +++ b/api/v1alpha1/admission_control.go @@ -13,6 +13,9 @@ import ( // This configuration probabilistically rejects requests based on the success rate // of previous requests in a configurable sliding time window. // All fields are optional and will use Envoy's defaults when not specified. +// +// +kubebuilder:validation:XValidation:rule="!has(self.successRateThreshold) || (self.successRateThreshold >= 1 && self.successRateThreshold <= 100)",message="successRateThreshold must be between 1 and 100" +// +kubebuilder:validation:XValidation:rule="!has(self.maxRejectionProbability) || (self.maxRejectionProbability >= 0 && self.maxRejectionProbability <= 100)",message="maxRejectionProbability must be between 0 and 100" type AdmissionControl struct { // SamplingWindow defines the time window over which request success rates are calculated. // Defaults to 30s if not specified. 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 b95b0fde39..fe8e5d466b 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 @@ -147,6 +147,13 @@ spec: minimum: 1 type: integer type: object + x-kubernetes-validations: + - message: successRateThreshold must be between 1 and 100 + rule: '!has(self.successRateThreshold) || (self.successRateThreshold + >= 1 && self.successRateThreshold <= 100)' + - message: maxRejectionProbability must be between 0 and 100 + rule: '!has(self.maxRejectionProbability) || (self.maxRejectionProbability + >= 0 && self.maxRejectionProbability <= 100)' circuitBreaker: description: |- Circuit Breaker settings for the upstream connections and requests. diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index fae04bcdfd..501b17f4cc 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -146,6 +146,13 @@ spec: minimum: 1 type: integer type: object + x-kubernetes-validations: + - message: successRateThreshold must be between 1 and 100 + rule: '!has(self.successRateThreshold) || (self.successRateThreshold + >= 1 && self.successRateThreshold <= 100)' + - message: maxRejectionProbability must be between 0 and 100 + rule: '!has(self.maxRejectionProbability) || (self.maxRejectionProbability + >= 0 && self.maxRejectionProbability <= 100)' circuitBreaker: description: |- Circuit Breaker settings for the upstream connections and requests. diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index a5a1c54b5f..3d5c865bc9 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -1709,13 +1709,18 @@ func (t *Translator) buildAdmissionControl(policy *egv1a1.BackendTrafficPolicy) } ac := &ir.AdmissionControl{ - SamplingWindow: parseSamplingWindow(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.SamplingWindow != nil { + if d, err := time.ParseDuration(string(*policy.Spec.AdmissionControl.SamplingWindow)); err == nil { + ac.SamplingWindow = &metav1.Duration{Duration: d} + } + } + if policy.Spec.AdmissionControl.SuccessCriteria != nil { ac.SuccessCriteria = &ir.AdmissionControlSuccessCriteria{} @@ -1743,18 +1748,6 @@ func (t *Translator) buildAdmissionControl(policy *egv1a1.BackendTrafficPolicy) return ac } -// parseSamplingWindow converts a gwapiv1.Duration to a metav1.Duration for the IR. -func parseSamplingWindow(d *gwapiv1.Duration) *metav1.Duration { - if d == nil { - return nil - } - duration, err := time.ParseDuration(string(*d)) - if err != nil { - return nil - } - return &metav1.Duration{Duration: duration} -} - func makeIrStatusSet(in []egv1a1.HTTPStatus) []ir.HTTPStatus { statusSet := sets.NewInt() for _, r := range in { diff --git a/internal/xds/translator/admission_control.go b/internal/xds/translator/admission_control.go index 3660af728b..e32fda1e31 100644 --- a/internal/xds/translator/admission_control.go +++ b/internal/xds/translator/admission_control.go @@ -7,8 +7,6 @@ package translator import ( "errors" - "fmt" - "time" corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" admissioncontrolv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/admission_control/v3" @@ -60,11 +58,7 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss } if admissionControl.SamplingWindow != nil { - duration, err := parseDuration(admissionControl.SamplingWindow.Duration.String()) - if err != nil { - return nil, fmt.Errorf("invalid samplingWindow: %w", err) - } - config.SamplingWindow = durationpb.New(duration) + config.SamplingWindow = durationpb.New(admissionControl.SamplingWindow.Duration) } if admissionControl.SuccessRateThreshold != nil { @@ -127,10 +121,6 @@ func buildAdmissionControlConfig(admissionControl *ir.AdmissionControl) (*admiss return config, nil } -func parseDuration(s string) (time.Duration, error) { - return time.ParseDuration(s) -} - // grpcStatusCodes maps a gRPC status code string name to its numeric value. // See https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc var grpcStatusCodes = map[string]uint32{ diff --git a/internal/xds/translator/admission_control_test.go b/internal/xds/translator/admission_control_test.go index 90edcdc175..4d99859b48 100644 --- a/internal/xds/translator/admission_control_test.go +++ b/internal/xds/translator/admission_control_test.go @@ -237,51 +237,6 @@ func TestBuildUpstreamAdmissionControlFilter(t *testing.T) { } } -func TestParseDuration(t *testing.T) { - tests := []struct { - name string - input string - want time.Duration - wantErr bool - }{ - { - name: "valid duration 60s", - input: "60s", - want: 60 * time.Second, - wantErr: false, - }, - { - name: "valid duration 30s", - input: "30s", - want: 30 * time.Second, - wantErr: false, - }, - { - name: "valid duration 1m", - input: "1m0s", - want: time.Minute, - wantErr: false, - }, - { - name: "invalid duration", - input: "invalid", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseDuration(tt.input) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - func TestGRPCStatusCodeToUint32(t *testing.T) { tests := []struct { name string diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go index ff9ea5c259..58fa6dad8e 100644 --- a/test/cel-validation/backendtrafficpolicy_test.go +++ b/test/cel-validation/backendtrafficpolicy_test.go @@ -75,6 +75,67 @@ func TestBackendTrafficPolicyTarget(t *testing.T) { }, wantErrors: []string{}, }, + { + desc: "valid admissionControl percentage bounds", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group("gateway.networking.k8s.io"), + Kind: gwapiv1.Kind("HTTPRoute"), + Name: gwapiv1.ObjectName("httpbin-route"), + }, + }, + }, + AdmissionControl: &egv1a1.AdmissionControl{ + SuccessRateThreshold: new(uint32(1)), + MaxRejectionProbability: new(uint32(100)), + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "admissionControl successRateThreshold below minimum", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group("gateway.networking.k8s.io"), + Kind: gwapiv1.Kind("HTTPRoute"), + Name: gwapiv1.ObjectName("httpbin-route"), + }, + }, + }, + AdmissionControl: &egv1a1.AdmissionControl{ + SuccessRateThreshold: new(uint32(0)), + }, + } + }, + wantErrors: []string{"successRateThreshold"}, + }, + { + desc: "admissionControl maxRejectionProbability above maximum", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group("gateway.networking.k8s.io"), + Kind: gwapiv1.Kind("HTTPRoute"), + Name: gwapiv1.ObjectName("httpbin-route"), + }, + }, + }, + AdmissionControl: &egv1a1.AdmissionControl{ + MaxRejectionProbability: new(uint32(101)), + }, + } + }, + wantErrors: []string{"maxRejectionProbability"}, + }, { desc: "no targetRef", mutate: func(btp *egv1a1.BackendTrafficPolicy) { diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 9073fa5276..4d1fd89401 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -22674,6 +22674,13 @@ spec: minimum: 1 type: integer type: object + x-kubernetes-validations: + - message: successRateThreshold must be between 1 and 100 + rule: '!has(self.successRateThreshold) || (self.successRateThreshold + >= 1 && self.successRateThreshold <= 100)' + - message: maxRejectionProbability must be between 0 and 100 + rule: '!has(self.maxRejectionProbability) || (self.maxRejectionProbability + >= 0 && self.maxRejectionProbability <= 100)' circuitBreaker: description: |- Circuit Breaker settings for the upstream connections and requests. diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 6bf8fee13f..f017045384 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -647,6 +647,13 @@ spec: minimum: 1 type: integer type: object + x-kubernetes-validations: + - message: successRateThreshold must be between 1 and 100 + rule: '!has(self.successRateThreshold) || (self.successRateThreshold + >= 1 && self.successRateThreshold <= 100)' + - message: maxRejectionProbability must be between 0 and 100 + rule: '!has(self.maxRejectionProbability) || (self.maxRejectionProbability + >= 0 && self.maxRejectionProbability <= 100)' 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 d8c812a417..a2621b1eef 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -647,6 +647,13 @@ spec: minimum: 1 type: integer type: object + x-kubernetes-validations: + - message: successRateThreshold must be between 1 and 100 + rule: '!has(self.successRateThreshold) || (self.successRateThreshold + >= 1 && self.successRateThreshold <= 100)' + - message: maxRejectionProbability must be between 0 and 100 + rule: '!has(self.maxRejectionProbability) || (self.maxRejectionProbability + >= 0 && self.maxRejectionProbability <= 100)' circuitBreaker: description: |- Circuit Breaker settings for the upstream connections and requests.