diff --git a/api/v1alpha1/admission_control.go b/api/v1alpha1/admission_control.go new file mode 100644 index 0000000000..8528b81123 --- /dev/null +++ b/api/v1alpha1/admission_control.go @@ -0,0 +1,126 @@ +// 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 ( + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// AdmissionControl configures health-based load shedding for upstream backends. +// +// Envoy tracks recent upstream responses over a sliding sampling window. When the +// observed success rate drops below the configured threshold, Envoy +// probabilistically rejects new requests before forwarding them upstream. This can +// reduce pressure on degraded backends and give them time to recover. +// +// All fields are optional. When omitted, Envoy's admission control defaults are used. +// +// +kubebuilder:validation:XValidation:rule="!has(self.minSuccessRate) || (self.minSuccessRate >= 1 && self.minSuccessRate <= 100)",message="minSuccessRate must be between 1 and 100" +// +kubebuilder:validation:XValidation:rule="!has(self.maxRejectionPercent) || (self.maxRejectionPercent >= 0 && self.maxRejectionPercent <= 100)",message="maxRejectionPercent must be between 0 and 100" +// +kubebuilder:validation:XValidation:rule="!has(self.samplingWindow) || duration(self.samplingWindow) >= duration('1s')",message="samplingWindow must be at least 1s" +type AdmissionControl struct { + // SamplingWindow defines the time window over which request success rates are calculated. + // Must be at least 1s; Envoy truncates the window to whole seconds and uses it as the + // denominator in RPS calculations, so sub-second values would produce a zero denominator. + // Defaults to 30s if not specified. + // + // +optional + SamplingWindow *gwapiv1.Duration `json:"samplingWindow,omitempty"` + + // MinSuccessRate 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=1 + // +kubebuilder:validation:Maximum=100 + MinSuccessRate *uint32 `json:"minSuccessRate,omitempty"` + + // RejectionAggression controls how steeply the rejection probability rises + // as the observed success rate falls below MinSuccessRate. A value of 1 + // produces a linear curve; higher values reject more aggressively for a + // given drop in success rate. Must be greater than 0; values below 1 are + // clamped to 1. Defaults to 1. + // + // +optional + // +kubebuilder:validation:Minimum=1 + RejectionAggression *uint32 `json:"rejectionAggression,omitempty"` + + // MinRequestRate defines the minimum requests per second below which requests will + // pass through the filter without rejection. Defaults to 0 if not specified. + // + // +optional + // +kubebuilder:validation:Minimum=0 + MinRequestRate *uint32 `json:"minRequestRate,omitempty"` + + // MaxRejectionPercent 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 + // +kubebuilder:validation:Maximum=100 + MaxRejectionPercent *uint32 `json:"maxRejectionPercent,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 { + // StatusCodes defines HTTP status codes that are considered successful. + // + // +optional + StatusCodes []HTTPStatus `json:"statusCodes,omitempty"` +} + +// 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;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 = "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. +type GRPCSuccessCriteria struct { + // StatusCodes 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 + StatusCodes []GRPCSuccessCode `json:"statusCodes,omitempty"` +} diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index 9392bebd2c..9468331583 100644 --- a/api/v1alpha1/backendtrafficpolicy_types.go +++ b/api/v1alpha1/backendtrafficpolicy_types.go @@ -45,6 +45,7 @@ type BackendTrafficPolicy struct { // +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute', 'UDPRoute', 'TCPRoute', 'TLSRoute']) : true ", message="this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute/TLSRoute" // +kubebuilder:validation:XValidation:rule="!has(self.compression) || !has(self.compressor)", message="either compression or compressor can be set, not both" // +kubebuilder:validation:XValidation:rule="!has(self.requestBuffer) || !has(self.httpUpgrade) || self.httpUpgrade.size() == 0", message="requestBuffer cannot be used together with httpUpgrade" +// +kubebuilder:validation:XValidation:rule="!has(self.admissionControl) || ((!has(self.targetRef) || self.targetRef.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute']) && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute'])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute'])))", message="admissionControl can only be used with HTTPRoute, GRPCRoute, or Gateway targets" type BackendTrafficPolicySpec struct { PolicyTargetReferences `json:",inline"` ClusterSettings `json:",inline"` @@ -74,6 +75,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/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 68bbe20fb4..2e6ba5df99 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -204,6 +204,76 @@ 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.SamplingWindow != nil { + in, out := &in.SamplingWindow, &out.SamplingWindow + *out = new(v1.Duration) + **out = **in + } + if in.MinSuccessRate != nil { + in, out := &in.MinSuccessRate, &out.MinSuccessRate + *out = new(uint32) + **out = **in + } + if in.RejectionAggression != nil { + in, out := &in.RejectionAggression, &out.RejectionAggression + *out = new(uint32) + **out = **in + } + if in.MinRequestRate != nil { + in, out := &in.MinRequestRate, &out.MinRequestRate + *out = new(uint32) + **out = **in + } + if in.MaxRejectionPercent != nil { + in, out := &in.MaxRejectionPercent, &out.MaxRejectionPercent + *out = new(uint32) + **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 @@ -743,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) @@ -3791,6 +3866,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.StatusCodes != nil { + in, out := &in.StatusCodes, &out.StatusCodes + *out = make([]GRPCSuccessCode, 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 @@ -4527,6 +4622,26 @@ 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 *HTTPSuccessCriteria) DeepCopyInto(out *HTTPSuccessCriteria) { + *out = *in + if in.StatusCodes != nil { + in, out := &in.StatusCodes, &out.StatusCodes + *out = make([]HTTPStatus, 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 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 1fab99d1e5..7bd3a27a60 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,115 @@ 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: + maxRejectionPercent: + description: |- + MaxRejectionPercent 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: integer + minRequestRate: + description: |- + MinRequestRate 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 + minSuccessRate: + description: |- + MinSuccessRate 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 + rejectionAggression: + description: |- + RejectionAggression controls how steeply the rejection probability rises + as the observed success rate falls below MinSuccessRate. A value of 1 + produces a linear curve; higher values reject more aggressively for a + given drop in success rate. Must be greater than 0; values below 1 are + clamped to 1. Defaults to 1. + format: int32 + minimum: 1 + type: integer + samplingWindow: + description: |- + SamplingWindow defines the time window over which request success rates are calculated. + Must be at least 1s; Envoy truncates the window to whole seconds and uses it as the + denominator in RPS calculations, so sub-second values would produce a zero denominator. + 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: + statusCodes: + description: |- + StatusCodes 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: + statusCodes: + description: StatusCodes 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 + type: object + x-kubernetes-validations: + - message: minSuccessRate must be between 1 and 100 + rule: '!has(self.minSuccessRate) || (self.minSuccessRate >= 1 && + self.minSuccessRate <= 100)' + - message: maxRejectionPercent must be between 0 and 100 + rule: '!has(self.maxRejectionPercent) || (self.maxRejectionPercent + >= 0 && self.maxRejectionPercent <= 100)' + - message: samplingWindow must be at least 1s + rule: '!has(self.samplingWindow) || duration(self.samplingWindow) + >= duration(''1s'')' bandwidthLimit: description: |- BandwidthLimit allows the user to limit the bandwidth of traffic @@ -3067,6 +3176,13 @@ spec: - message: requestBuffer cannot be used together with httpUpgrade rule: '!has(self.requestBuffer) || !has(self.httpUpgrade) || self.httpUpgrade.size() == 0' + - message: admissionControl can only be used with HTTPRoute, GRPCRoute, + or Gateway targets + rule: '!has(self.admissionControl) || ((!has(self.targetRef) || self.targetRef.kind + in [''Gateway'', ''HTTPRoute'', ''GRPCRoute'']) && (!has(self.targetRefs) + || self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', + ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, + sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && 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 4acddfd95f..1b829d7c82 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,115 @@ 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: + maxRejectionPercent: + description: |- + MaxRejectionPercent 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: integer + minRequestRate: + description: |- + MinRequestRate 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 + minSuccessRate: + description: |- + MinSuccessRate 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 + rejectionAggression: + description: |- + RejectionAggression controls how steeply the rejection probability rises + as the observed success rate falls below MinSuccessRate. A value of 1 + produces a linear curve; higher values reject more aggressively for a + given drop in success rate. Must be greater than 0; values below 1 are + clamped to 1. Defaults to 1. + format: int32 + minimum: 1 + type: integer + samplingWindow: + description: |- + SamplingWindow defines the time window over which request success rates are calculated. + Must be at least 1s; Envoy truncates the window to whole seconds and uses it as the + denominator in RPS calculations, so sub-second values would produce a zero denominator. + 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: + statusCodes: + description: |- + StatusCodes 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: + statusCodes: + description: StatusCodes 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 + type: object + x-kubernetes-validations: + - message: minSuccessRate must be between 1 and 100 + rule: '!has(self.minSuccessRate) || (self.minSuccessRate >= 1 && + self.minSuccessRate <= 100)' + - message: maxRejectionPercent must be between 0 and 100 + rule: '!has(self.maxRejectionPercent) || (self.maxRejectionPercent + >= 0 && self.maxRejectionPercent <= 100)' + - message: samplingWindow must be at least 1s + rule: '!has(self.samplingWindow) || duration(self.samplingWindow) + >= duration(''1s'')' bandwidthLimit: description: |- BandwidthLimit allows the user to limit the bandwidth of traffic @@ -3066,6 +3175,13 @@ spec: - message: requestBuffer cannot be used together with httpUpgrade rule: '!has(self.requestBuffer) || !has(self.httpUpgrade) || self.httpUpgrade.size() == 0' + - message: admissionControl can only be used with HTTPRoute, GRPCRoute, + or Gateway targets + rule: '!has(self.admissionControl) || ((!has(self.targetRef) || self.targetRef.kind + in [''Gateway'', ''HTTPRoute'', ''GRPCRoute'']) && (!has(self.targetRefs) + || self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', + ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, + sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && diff --git a/examples/kubernetes/admission-control.yaml b/examples/kubernetes/admission-control.yaml new file mode 100644 index 0000000000..39a7366e71 --- /dev/null +++ b/examples/kubernetes/admission-control.yaml @@ -0,0 +1,62 @@ +# 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 + admissionControl: + samplingWindow: 30s + minSuccessRate: 95 + rejectionAggression: 1 + minRequestRate: 5 + maxRejectionPercent: 80 + successCriteria: + http: + statusCodes: + - 200 + - 201 + - 204 + grpc: + statusCodes: + - Ok +--- +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 5467936e92..23f7f4210c 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -1037,6 +1037,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 @@ -1075,6 +1076,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) @@ -1133,6 +1137,7 @@ func (t *Translator) buildTrafficFeatures(policy *egv1a1.BackendTrafficPolicy) ( HealthCheck: hc, CircuitBreaker: cb, FaultInjection: fi, + AdmissionControl: ac, TCPKeepalive: ka, Retry: rt, BackendConnection: bc, @@ -1780,6 +1785,51 @@ 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{ + MinSuccessRate: policy.Spec.AdmissionControl.MinSuccessRate, + RejectionAggression: policy.Spec.AdmissionControl.RejectionAggression, + MinRequestRate: policy.Spec.AdmissionControl.MinRequestRate, + MaxRejectionPercent: policy.Spec.AdmissionControl.MaxRejectionPercent, + } + + 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{} + + if policy.Spec.AdmissionControl.SuccessCriteria.HTTP != nil { + httpStatuses := make([]int32, len(policy.Spec.AdmissionControl.SuccessCriteria.HTTP.StatusCodes)) + for i, s := range policy.Spec.AdmissionControl.SuccessCriteria.HTTP.StatusCodes { + httpStatuses[i] = int32(s) + } + ac.SuccessCriteria.HTTP = &ir.HTTPSuccessCriteria{ + StatusCodes: httpStatuses, + } + } + + if policy.Spec.AdmissionControl.SuccessCriteria.GRPC != nil { + grpcStatuses := make([]string, len(policy.Spec.AdmissionControl.SuccessCriteria.GRPC.StatusCodes)) + for i, s := range policy.Spec.AdmissionControl.SuccessCriteria.GRPC.StatusCodes { + grpcStatuses[i] = string(s) + } + ac.SuccessCriteria.GRPC = &ir.GRPCSuccessCriteria{ + StatusCodes: grpcStatuses, + } + } + } + + return ac +} + func makeIrStatusSet(in []egv1a1.HTTPStatus) []ir.HTTPStatus { statusSet := sets.NewInt() for _, r := range in { 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..e251aeb668 --- /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 + minSuccessRate: 90 + rejectionAggression: 2 + minRequestRate: 10 + maxRejectionPercent: 70 + successCriteria: + grpc: + statusCodes: + - 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 + minSuccessRate: 95 + successCriteria: + http: + statusCodes: + - 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..3a0d2080ce --- /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: + minSuccessRate: 95 + samplingWindow: 30s + successCriteria: + http: + statusCodes: + - 200 + - 201 + - 204 + 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: + maxRejectionPercent: 70 + minRequestRate: 10 + minSuccessRate: 90 + rejectionAggression: 2 + samplingWindow: 60s + successCriteria: + grpc: + statusCodes: + - Ok + - Cancelled + - DeadlineExceeded + 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: + maxRejectionPercent: 70 + minRequestRate: 10 + minSuccessRate: 90 + rejectionAggression: 2 + samplingWindow: 1m0s + successCriteria: + grpc: + statusCodes: + - Ok + - Cancelled + - DeadlineExceeded + 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: + minSuccessRate: 95 + samplingWindow: 30s + successCriteria: + http: + statusCodes: + - 200 + - 201 + - 204 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index b24e7f0dc3..9a1c7e91b3 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -1044,6 +1044,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 @@ -1701,6 +1703,53 @@ 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 { + // SamplingWindow defines the time window over which request success rates are calculated. + SamplingWindow *metav1.Duration `json:"samplingWindow,omitempty" yaml:"samplingWindow,omitempty"` + // MinSuccessRate is the lowest request success rate, as a percentage in the + // range [1, 100], at which the filter will not reject requests. + MinSuccessRate *uint32 `json:"minSuccessRate,omitempty" yaml:"minSuccessRate,omitempty"` + // RejectionAggression controls the rejection probability curve. + RejectionAggression *uint32 `json:"rejectionAggression,omitempty" yaml:"rejectionAggression,omitempty"` + // MinRequestRate defines the minimum requests per second below which requests will + // pass through the filter without rejection. + MinRequestRate *uint32 `json:"minRequestRate,omitempty" yaml:"minRequestRate,omitempty"` + // MaxRejectionPercent represents the upper limit of the rejection probability, + // as a percentage in the range [0, 100]. + MaxRejectionPercent *uint32 `json:"maxRejectionPercent,omitempty" yaml:"maxRejectionPercent,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 { + // StatusCodes defines HTTP status codes that are considered successful. + StatusCodes []int32 `json:"statusCodes,omitempty" yaml:"statusCodes,omitempty"` +} + +// GRPCSuccessCriteria defines success criteria for gRPC requests. +// +// +k8s:deepcopy-gen=true +type GRPCSuccessCriteria struct { + // StatusCodes defines gRPC status codes that are considered successful (string enum names). + StatusCodes []string `json:"statusCodes,omitempty" yaml:"statusCodes,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 3bc828e1b4..56d5300387 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -299,6 +299,76 @@ 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.SamplingWindow != nil { + in, out := &in.SamplingWindow, &out.SamplingWindow + *out = new(metav1.Duration) + **out = **in + } + if in.MinSuccessRate != nil { + in, out := &in.MinSuccessRate, &out.MinSuccessRate + *out = new(uint32) + **out = **in + } + if in.RejectionAggression != nil { + in, out := &in.RejectionAggression, &out.RejectionAggression + *out = new(uint32) + **out = **in + } + if in.MinRequestRate != nil { + in, out := &in.MinRequestRate, &out.MinRequestRate + *out = new(uint32) + **out = **in + } + if in.MaxRejectionPercent != nil { + in, out := &in.MaxRejectionPercent, &out.MaxRejectionPercent + *out = new(uint32) + **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 @@ -1813,6 +1883,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.StatusCodes != nil { + in, out := &in.StatusCodes, &out.StatusCodes + *out = make([]string, 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 @@ -2468,6 +2558,26 @@ 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 *HTTPSuccessCriteria) DeepCopyInto(out *HTTPSuccessCriteria) { + *out = *in + if in.StatusCodes != nil { + in, out := &in.StatusCodes, &out.StatusCodes + *out = make([]int32, 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 @@ -4944,6 +5054,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..2bc6d3511c --- /dev/null +++ b/internal/xds/translator/admission_control.go @@ -0,0 +1,149 @@ +// 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" + + 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" + 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/durationpb" + "google.golang.org/protobuf/types/known/wrapperspb" + + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/utils/proto" +) + +// 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 nil, err + } + + configAny, err := proto.ToAnyWithValidation(config) + if err != nil { + return nil, err + } + + return &hcmv3.HttpFilter{ + Name: "envoy.filters.http.admission_control", + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: configAny, + }, + }, 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") + } + + // The filter is enabled whenever the policy is configured. + config := &admissioncontrolv3.AdmissionControl{ + Enabled: &corev3.RuntimeFeatureFlag{ + DefaultValue: &wrapperspb.BoolValue{Value: true}, + }, + } + + if admissionControl.SamplingWindow != nil { + config.SamplingWindow = durationpb.New(admissionControl.SamplingWindow.Duration) + } + + if admissionControl.MinSuccessRate != nil { + config.SrThreshold = &corev3.RuntimePercent{ + DefaultValue: &typev3.Percent{Value: float64(*admissionControl.MinSuccessRate)}, + } + } + + if admissionControl.RejectionAggression != nil { + config.Aggression = &corev3.RuntimeDouble{ + DefaultValue: float64(*admissionControl.RejectionAggression), + } + } + + if admissionControl.MinRequestRate != nil { + config.RpsThreshold = &corev3.RuntimeUInt32{ + DefaultValue: *admissionControl.MinRequestRate, + } + } + + if admissionControl.MaxRejectionPercent != nil { + config.MaxRejectionProbability = &corev3.RuntimePercent{ + DefaultValue: &typev3.Percent{Value: float64(*admissionControl.MaxRejectionPercent)}, + } + } + + successCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria{} + + // Set success criteria (part of EvaluationCriteria oneof) + if admissionControl.SuccessCriteria != nil { + // HTTP success criteria: each individual status code becomes a single-element range [code, code+1) + if admissionControl.SuccessCriteria.HTTP != nil && len(admissionControl.SuccessCriteria.HTTP.StatusCodes) > 0 { + httpCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria_HttpCriteria{} + for _, code := range admissionControl.SuccessCriteria.HTTP.StatusCodes { + httpCriteria.HttpSuccessStatus = append(httpCriteria.HttpSuccessStatus, &typev3.Int32Range{ + Start: code, + End: code + 1, + }) + } + successCriteria.HttpCriteria = httpCriteria + } + + if admissionControl.SuccessCriteria.GRPC != nil && len(admissionControl.SuccessCriteria.GRPC.StatusCodes) > 0 { + grpcCriteria := &admissioncontrolv3.AdmissionControl_SuccessCriteria_GrpcCriteria{} + for _, status := range admissionControl.SuccessCriteria.GRPC.StatusCodes { + if code, ok := grpcStatusCodeToUint32(status); ok { + grpcCriteria.GrpcSuccessStatus = append(grpcCriteria.GrpcSuccessStatus, code) + } + } + successCriteria.GrpcCriteria = grpcCriteria + } + + } + + // Always set EvaluationCriteria (required field) + config.EvaluationCriteria = &admissioncontrolv3.AdmissionControl_SuccessCriteria_{ + SuccessCriteria: successCriteria, + } + + return config, nil +} + +// 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) { + code, ok := grpcStatusCodes[name] + return code, ok +} diff --git a/internal/xds/translator/admission_control_test.go b/internal/xds/translator/admission_control_test.go new file mode 100644 index 0000000000..b77022e078 --- /dev/null +++ b/internal/xds/translator/admission_control_test.go @@ -0,0 +1,263 @@ +// 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" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/envoyproxy/gateway/internal/ir" +) + +func TestBuildAdmissionControlConfig(t *testing.T) { + tests := []struct { + name string + config *ir.AdmissionControl + wantErr bool + }{ + { + name: "valid admission control config", + config: &ir.AdmissionControl{}, + wantErr: false, + }, + { + name: "nil config", + 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, + }, + MinSuccessRate: new(uint32(90)), + RejectionAggression: new(uint32(2)), + MinRequestRate: new(uint32(10)), + MaxRejectionPercent: new(uint32(80)), + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ + HTTP: &ir.HTTPSuccessCriteria{ + StatusCodes: []int32{200, 201, 300, 301}, + }, + GRPC: &ir.GRPCSuccessCriteria{ + StatusCodes: []string{"Ok", "Cancelled"}, + }, + }, + }, + wantErr: false, + }, + { + name: "config with only HTTP success criteria", + config: &ir.AdmissionControl{ + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ + HTTP: &ir.HTTPSuccessCriteria{ + StatusCodes: []int32{200, 201, 202}, + }, + }, + }, + wantErr: false, + }, + { + name: "config with only gRPC success criteria", + config: &ir.AdmissionControl{ + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ + GRPC: &ir.GRPCSuccessCriteria{ + StatusCodes: []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 min thresholds", + config: &ir.AdmissionControl{ + MinSuccessRate: new(uint32(1)), + RejectionAggression: new(uint32(1)), + MinRequestRate: new(uint32(0)), + MaxRejectionPercent: new(uint32(0)), + }, + wantErr: false, + }, + { + name: "config with max thresholds", + config: &ir.AdmissionControl{ + MinSuccessRate: new(uint32(100)), + RejectionAggression: new(uint32(10)), + MinRequestRate: new(uint32(1000)), + MaxRejectionPercent: new(uint32(100)), + }, + wantErr: false, + }, + } + + 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) + require.NotNil(t, got) + + require.NotNil(t, got.Enabled) + require.NotNil(t, got.Enabled.DefaultValue) + assert.True(t, got.Enabled.DefaultValue.Value) + + require.NotNil(t, got.EvaluationCriteria) + }) + } +} + +func TestBuildAdmissionControlConfigValues(t *testing.T) { + config := &ir.AdmissionControl{ + SamplingWindow: &metav1.Duration{ + Duration: 45 * time.Second, + }, + MinSuccessRate: new(uint32(85)), + RejectionAggression: new(uint32(2)), + MinRequestRate: new(uint32(20)), + MaxRejectionPercent: new(uint32(75)), + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ + HTTP: &ir.HTTPSuccessCriteria{ + StatusCodes: []int32{200, 201, 202}, + }, + GRPC: &ir.GRPCSuccessCriteria{ + StatusCodes: []string{"Ok", "Cancelled", "Unknown"}, + }, + }, + } + + got, err := buildAdmissionControlConfig(config) + require.NoError(t, err) + require.NotNil(t, got) + + assert.Equal(t, int64(45), got.SamplingWindow.Seconds) + assert.Equal(t, 85.0, got.SrThreshold.DefaultValue.Value) + 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) +} + +func TestBuildAdmissionControlConfigDefaults(t *testing.T) { + config := &ir.AdmissionControl{} + + got, err := buildAdmissionControlConfig(config) + require.NoError(t, err) + require.NotNil(t, got) + + 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) + + require.NotNil(t, got.Enabled) + require.NotNil(t, got.EvaluationCriteria) +} + +func TestBuildUpstreamAdmissionControlFilter(t *testing.T) { + tests := []struct { + name string + ac *ir.AdmissionControl + wantErr bool + }{ + { + name: "nil config", + ac: nil, + wantErr: true, + }, + { + name: "empty config", + ac: &ir.AdmissionControl{}, + wantErr: false, + }, + { + name: "full config", + ac: &ir.AdmissionControl{ + SamplingWindow: &metav1.Duration{ + Duration: 30 * time.Second, + }, + MinSuccessRate: new(uint32(95)), + RejectionAggression: new(uint32(1)), + MinRequestRate: new(uint32(5)), + MaxRejectionPercent: new(uint32(80)), + SuccessCriteria: &ir.AdmissionControlSuccessCriteria{ + HTTP: &ir.HTTPSuccessCriteria{ + StatusCodes: []int32{200, 201}, + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filter, err := buildUpstreamAdmissionControlFilter(tt.ac) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, filter) + assert.Equal(t, "envoy.filters.http.admission_control", filter.Name) + assert.NotNil(t, filter.ConfigType) + }) + } +} + +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 1659739985..40249b03bb 100644 --- a/internal/xds/translator/cluster.go +++ b/internal/xds/translator/cluster.go @@ -76,6 +76,7 @@ type xdsClusterArgs struct { metrics *ir.Metrics backendConnection *ir.BackendConnection dns *ir.DNS + admissionControl *ir.AdmissionControl useClientProtocol bool ipFamily *egv1a1.IPFamily metadata *ir.ResourceMetadata @@ -1025,7 +1026,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 || forceHTTP1UpstreamProtocol @@ -1143,8 +1145,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) @@ -1163,6 +1164,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() @@ -1171,7 +1180,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/testdata/in/xds-ir/admission-control.yaml b/internal/xds/translator/testdata/in/xds-ir/admission-control.yaml new file mode 100644 index 0000000000..52a29b3c9e --- /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 + minSuccessRate: 95 + rejectionAggression: 1 + minRequestRate: 5 + maxRejectionPercent: 80 + successCriteria: + http: + statusCodes: + - 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: + minSuccessRate: 80 + rejectionAggression: 2 + minRequestRate: 10 + maxRejectionPercent: 95 + successCriteria: + grpc: + statusCodes: + - "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.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/admission-control.clusters.yaml new file mode 100644 index 0000000000..8604a630ed --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/admission-control.clusters.yaml @@ -0,0 +1,149 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + ignoreHealthOnHostRemoval: true + 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 + 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 + 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/admission-control.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml new file mode 100644 index 0000000000..67715e26e3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/admission-control.routes.yaml @@ -0,0 +1,28 @@ +- 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 + - match: + path: example + name: second-route + route: + cluster: second-route-dest + upgradeConfigs: + - upgradeType: websocket + - match: + path: test + name: third-route + route: + cluster: third-route-dest + upgradeConfigs: + - upgradeType: websocket diff --git a/internal/xds/translator/utils.go b/internal/xds/translator/utils.go index 84ec9dd4dd..6e3a570e03 100644 --- a/internal/xds/translator/utils.go +++ b/internal/xds/translator/utils.go @@ -219,6 +219,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 diff --git a/release-notes/current.yaml b/release-notes/current.yaml index 6104bee389..8936c633fd 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -51,6 +51,7 @@ new features: | Added support for path override in ExtAuth HTTP service. Added support for bandwidth limit. Added support for defining Envoy Proxy image, pullPolicy, and pullSecrets via the helm chart. Note that to merge these helm-configured values with EnvoyProxy resources, the EnvoyProxy must include `mergeType: StrategicMerge` or `mergeType: JSONMerge`. + Added support for Envoy Admission Control to BackendTrafficPolicy, enabling client-side load shedding based on historical upstream success rates using Envoy's admission control filter. bug fixes: | Fixed local rate limit rules with identical sourceCIDR client selectors producing conflicting descriptors. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index c0be272b08..330ad8bf41 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -189,6 +189,47 @@ _Appears in:_ | `GRPC` | ActiveHealthCheckerTypeGRPC defines the GRPC type of health checking.
| +#### AdmissionControl + + + +AdmissionControl configures health-based load shedding for upstream backends. + +Envoy tracks recent upstream responses over a sliding sampling window. When the +observed success rate drops below the configured threshold, Envoy +probabilistically rejects new requests before forwarding them upstream. This can +reduce pressure on degraded backends and give them time to recover. + +All fields are optional. When omitted, Envoy's admission control defaults are used. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `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.
Must be at least 1s; Envoy truncates the window to whole seconds and uses it as the
denominator in RPS calculations, so sub-second values would produce a zero denominator.
Defaults to 30s if not specified. | +| `minSuccessRate` | _integer_ | false | | MinSuccessRate 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. | +| `rejectionAggression` | _integer_ | false | | RejectionAggression controls how steeply the rejection probability rises
as the observed success rate falls below MinSuccessRate. A value of 1
produces a linear curve; higher values reject more aggressively for a
given drop in success rate. Must be greater than 0; values below 1 are
clamped to 1. Defaults to 1. | +| `minRequestRate` | _integer_ | false | | MinRequestRate defines the minimum requests per second below which requests will
pass through the filter without rejection. Defaults to 0 if not specified. | +| `maxRejectionPercent` | _integer_ | false | | MaxRejectionPercent 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. | + + +#### 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_ @@ -544,6 +585,7 @@ _Appears in:_ | `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. | | `bandwidthLimit` | _[BandwidthLimitSpec](#bandwidthlimitspec)_ | false | | BandwidthLimit allows the user to limit the bandwidth of traffic
sent to and received from the backend. | | `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. | @@ -2594,6 +2636,51 @@ _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` | | +| `InvalidArgument` | | +| `DeadlineExceeded` | | +| `NotFound` | | +| `AlreadyExists` | | +| `PermissionDenied` | | +| `ResourceExhausted` | | +| `FailedPrecondition` | | +| `Aborted` | | +| `OutOfRange` | | +| `Unimplemented` | | +| `Internal` | | +| `Unavailable` | | +| `DataLoss` | | +| `Unauthenticated` | | + + +#### GRPCSuccessCriteria + + + +GRPCSuccessCriteria defines success criteria for gRPC requests. + +_Appears in:_ +- [AdmissionControlSuccessCriteria](#admissioncontrolsuccesscriteria) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `statusCodes` | _[GRPCSuccessCode](#grpcsuccesscode) array_ | false | | StatusCodes 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 @@ -3089,10 +3176,25 @@ HTTPStatus defines the http status code. _Appears in:_ - [HTTPActiveHealthChecker](#httpactivehealthchecker) +- [HTTPSuccessCriteria](#httpsuccesscriteria) - [RetryOn](#retryon) +#### HTTPSuccessCriteria + + + +HTTPSuccessCriteria defines success criteria for HTTP requests. + +_Appears in:_ +- [AdmissionControlSuccessCriteria](#admissioncontrolsuccesscriteria) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `statusCodes` | _[HTTPStatus](#httpstatus) array_ | false | | StatusCodes defines HTTP status codes that are considered successful. | + + #### HTTPTimeout diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go index 9b874c83b6..2d400f5705 100644 --- a/test/cel-validation/backendtrafficpolicy_test.go +++ b/test/cel-validation/backendtrafficpolicy_test.go @@ -75,6 +75,168 @@ 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{ + MinSuccessRate: new(uint32(1)), + MaxRejectionPercent: new(uint32(100)), + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "admissionControl minSuccessRate 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{ + MinSuccessRate: new(uint32(0)), + }, + } + }, + wantErrors: []string{"minSuccessRate"}, + }, + { + desc: "admissionControl maxRejectionPercent 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{ + MaxRejectionPercent: new(uint32(101)), + }, + } + }, + wantErrors: []string{"maxRejectionPercent"}, + }, + { + desc: "admissionControl rejected on TCPRoute target", + 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("TCPRoute"), + Name: gwapiv1.ObjectName("tcp-route"), + }, + }, + }, + AdmissionControl: &egv1a1.AdmissionControl{ + MinSuccessRate: new(uint32(50)), + }, + } + }, + wantErrors: []string{"admissionControl can only be used with HTTPRoute, GRPCRoute, or Gateway targets"}, + }, + { + desc: "admissionControl rejected on UDPRoute target via targetRefs", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRefs: []gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + { + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group("gateway.networking.k8s.io"), + Kind: gwapiv1.Kind("UDPRoute"), + Name: gwapiv1.ObjectName("udp-route"), + }, + }, + }, + }, + AdmissionControl: &egv1a1.AdmissionControl{ + MinSuccessRate: new(uint32(50)), + }, + } + }, + wantErrors: []string{"admissionControl can only be used with HTTPRoute, GRPCRoute, or Gateway targets"}, + }, + { + desc: "admissionControl rejected on TLSRoute target via targetSelectors", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Kind: gwapiv1.Kind("TLSRoute"), + MatchLabels: map[string]string{"app": "foo"}, + }, + }, + }, + AdmissionControl: &egv1a1.AdmissionControl{ + MinSuccessRate: new(uint32(50)), + }, + } + }, + wantErrors: []string{"admissionControl can only be used with HTTPRoute, GRPCRoute, or Gateway targets"}, + }, + { + desc: "admissionControl allowed on Gateway target", + 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("Gateway"), + Name: gwapiv1.ObjectName("eg"), + }, + }, + }, + AdmissionControl: &egv1a1.AdmissionControl{ + MinSuccessRate: new(uint32(50)), + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "admissionControl allowed on GRPCRoute target", + 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("GRPCRoute"), + Name: gwapiv1.ObjectName("grpc-route"), + }, + }, + }, + AdmissionControl: &egv1a1.AdmissionControl{ + MinSuccessRate: new(uint32(50)), + }, + } + }, + wantErrors: []string{}, + }, { 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 ff7e70b6ee..f49e164f63 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -22577,6 +22577,115 @@ 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: + maxRejectionPercent: + description: |- + MaxRejectionPercent 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: integer + minRequestRate: + description: |- + MinRequestRate 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 + minSuccessRate: + description: |- + MinSuccessRate 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 + rejectionAggression: + description: |- + RejectionAggression controls how steeply the rejection probability rises + as the observed success rate falls below MinSuccessRate. A value of 1 + produces a linear curve; higher values reject more aggressively for a + given drop in success rate. Must be greater than 0; values below 1 are + clamped to 1. Defaults to 1. + format: int32 + minimum: 1 + type: integer + samplingWindow: + description: |- + SamplingWindow defines the time window over which request success rates are calculated. + Must be at least 1s; Envoy truncates the window to whole seconds and uses it as the + denominator in RPS calculations, so sub-second values would produce a zero denominator. + 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: + statusCodes: + description: |- + StatusCodes 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: + statusCodes: + description: StatusCodes 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 + type: object + x-kubernetes-validations: + - message: minSuccessRate must be between 1 and 100 + rule: '!has(self.minSuccessRate) || (self.minSuccessRate >= 1 && + self.minSuccessRate <= 100)' + - message: maxRejectionPercent must be between 0 and 100 + rule: '!has(self.maxRejectionPercent) || (self.maxRejectionPercent + >= 0 && self.maxRejectionPercent <= 100)' + - message: samplingWindow must be at least 1s + rule: '!has(self.samplingWindow) || duration(self.samplingWindow) + >= duration(''1s'')' bandwidthLimit: description: |- BandwidthLimit allows the user to limit the bandwidth of traffic @@ -25594,6 +25703,13 @@ spec: - message: requestBuffer cannot be used together with httpUpgrade rule: '!has(self.requestBuffer) || !has(self.httpUpgrade) || self.httpUpgrade.size() == 0' + - message: admissionControl can only be used with HTTPRoute, GRPCRoute, + or Gateway targets + rule: '!has(self.admissionControl) || ((!has(self.targetRef) || self.targetRef.kind + in [''Gateway'', ''HTTPRoute'', ''GRPCRoute'']) && (!has(self.targetRefs) + || self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', + ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, + sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 4f4d9eec65..360c331a21 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -550,6 +550,115 @@ 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: + maxRejectionPercent: + description: |- + MaxRejectionPercent 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: integer + minRequestRate: + description: |- + MinRequestRate 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 + minSuccessRate: + description: |- + MinSuccessRate 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 + rejectionAggression: + description: |- + RejectionAggression controls how steeply the rejection probability rises + as the observed success rate falls below MinSuccessRate. A value of 1 + produces a linear curve; higher values reject more aggressively for a + given drop in success rate. Must be greater than 0; values below 1 are + clamped to 1. Defaults to 1. + format: int32 + minimum: 1 + type: integer + samplingWindow: + description: |- + SamplingWindow defines the time window over which request success rates are calculated. + Must be at least 1s; Envoy truncates the window to whole seconds and uses it as the + denominator in RPS calculations, so sub-second values would produce a zero denominator. + 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: + statusCodes: + description: |- + StatusCodes 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: + statusCodes: + description: StatusCodes 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 + type: object + x-kubernetes-validations: + - message: minSuccessRate must be between 1 and 100 + rule: '!has(self.minSuccessRate) || (self.minSuccessRate >= 1 && + self.minSuccessRate <= 100)' + - message: maxRejectionPercent must be between 0 and 100 + rule: '!has(self.maxRejectionPercent) || (self.maxRejectionPercent + >= 0 && self.maxRejectionPercent <= 100)' + - message: samplingWindow must be at least 1s + rule: '!has(self.samplingWindow) || duration(self.samplingWindow) + >= duration(''1s'')' bandwidthLimit: description: |- BandwidthLimit allows the user to limit the bandwidth of traffic @@ -3567,6 +3676,13 @@ spec: - message: requestBuffer cannot be used together with httpUpgrade rule: '!has(self.requestBuffer) || !has(self.httpUpgrade) || self.httpUpgrade.size() == 0' + - message: admissionControl can only be used with HTTPRoute, GRPCRoute, + or Gateway targets + rule: '!has(self.admissionControl) || ((!has(self.targetRef) || self.targetRef.kind + in [''Gateway'', ''HTTPRoute'', ''GRPCRoute'']) && (!has(self.targetRefs) + || self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', + ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, + sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && 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 e591ca487d..edadb77748 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,115 @@ 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: + maxRejectionPercent: + description: |- + MaxRejectionPercent 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: integer + minRequestRate: + description: |- + MinRequestRate 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 + minSuccessRate: + description: |- + MinSuccessRate 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 + rejectionAggression: + description: |- + RejectionAggression controls how steeply the rejection probability rises + as the observed success rate falls below MinSuccessRate. A value of 1 + produces a linear curve; higher values reject more aggressively for a + given drop in success rate. Must be greater than 0; values below 1 are + clamped to 1. Defaults to 1. + format: int32 + minimum: 1 + type: integer + samplingWindow: + description: |- + SamplingWindow defines the time window over which request success rates are calculated. + Must be at least 1s; Envoy truncates the window to whole seconds and uses it as the + denominator in RPS calculations, so sub-second values would produce a zero denominator. + 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: + statusCodes: + description: |- + StatusCodes 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: + statusCodes: + description: StatusCodes 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 + type: object + x-kubernetes-validations: + - message: minSuccessRate must be between 1 and 100 + rule: '!has(self.minSuccessRate) || (self.minSuccessRate >= 1 && + self.minSuccessRate <= 100)' + - message: maxRejectionPercent must be between 0 and 100 + rule: '!has(self.maxRejectionPercent) || (self.maxRejectionPercent + >= 0 && self.maxRejectionPercent <= 100)' + - message: samplingWindow must be at least 1s + rule: '!has(self.samplingWindow) || duration(self.samplingWindow) + >= duration(''1s'')' bandwidthLimit: description: |- BandwidthLimit allows the user to limit the bandwidth of traffic @@ -3567,6 +3676,13 @@ spec: - message: requestBuffer cannot be used together with httpUpgrade rule: '!has(self.requestBuffer) || !has(self.httpUpgrade) || self.httpUpgrade.size() == 0' + - message: admissionControl can only be used with HTTPRoute, GRPCRoute, + or Gateway targets + rule: '!has(self.admissionControl) || ((!has(self.targetRef) || self.targetRef.kind + in [''Gateway'', ''HTTPRoute'', ''GRPCRoute'']) && (!has(self.targetRefs) + || self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', + ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, + sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) &&