diff --git a/api/v1alpha1/ratelimit_types.go b/api/v1alpha1/ratelimit_types.go index 72382d699f1..9e9a97dec17 100644 --- a/api/v1alpha1/ratelimit_types.go +++ b/api/v1alpha1/ratelimit_types.go @@ -62,6 +62,7 @@ type LocalRateLimit struct { // // +optional // +kubebuilder:validation:MaxItems=16 + // +kubebuilder:validation:XValidation:rule="self.all(foo, !has(foo.responseHitsAddend))", message="responseHitsAddend is not supported for Local Rate Limits" Rules []RateLimitRule `json:"rules"` } @@ -91,6 +92,64 @@ type RateLimitRule struct { // 429 HTTP status code is sent back to the client when // the selected requests have reached the limit. Limit RateLimitValue `json:"limit"` + // RequestHitsAddend specifies the number to reduce the rate limit counters + // on the request path. If the addend is not specified, the default behavior + // is to reduce the rate limit counters by 1. + // + // When Envoy receives a request that matches the rule, it tries to reduce the + // rate limit counters by the specified number. If the counter doesn't have + // enough capacity, the request is rate limited. + // + // +optional + // +notImplementedHide + RequestHitsAddend *RateLimitHitsAddend `json:"requestHitsAddend,omitempty"` + // ResponseHitsAddend specifies the number to reduce the rate limit counters + // after the response is sent back to the client or the request stream is closed. + // + // The addend is used to reduce the rate limit counters for the matching requests. + // Since the reduction happens after the request stream is complete, the rate limit + // won't be enforced for the current request, but for the subsequent matching requests. + // + // This is optional and if not specified, the rate limit counters are not reduced + // on the response path. + // + // Currently, this is only supported for HTTP Global Rate Limits. + // + // +optional + // +notImplementedHide + ResponseHitsAddend *RateLimitHitsAddend `json:"responseHitsAddend,omitempty"` +} + +// RateLimitHitsAddend specifies where the Envoy retrieves the number to reduce the rate limit counters. +// +// By default, Envoy looks up the addend from the `envoy.ratelimit.hits_addend` filter metadata. +// If there's no such metadata or the number stored in the metadata is invalid, it will use the default +// usage number of 1. +// +// This default behavior can be overridden by specifying exactly one of the fields in this RateLimitUsage. +// If either of the fields is not specified, Envoy will use the default behavior described above. +// +// See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto.html#config-route-v3-ratelimit-hitsaddend +// for more information. +// +// +kubebuilder:validation:XValidation:rule="!(has(self.number) && has(self.format))",message="only one of number or format can be specified" +type RateLimitHitsAddend struct { + // Number specifies the fixed usage number to reduce the rate limit counters. + // + // +optional + // +notImplementedHide + Number *uint64 `json:"number,omitempty"` + // Format specifies the format of the usage number. See the Envoy documentation for the supported format which + // is the same as the access log format: + // https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format + // + // For example `%DYNAMIC_METADATA(com.test.my_filter:test_key)%"` will retrieve the usage number from the + // `com.test.my_filter` filter metadata namespace with the key `test_key`. + // Another example is `%BYTES_RECEIVED%` which will retrieve the usage number from the bytes received by the client. + // + // +optional + // +notImplementedHide + Format *string `json:"format,omitempty"` } // RateLimitSelectCondition specifies the attributes within the traffic flow that can diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index dbc28e6aca2..011f7104ce6 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -4570,6 +4570,31 @@ func (in *RateLimitDatabaseBackend) DeepCopy() *RateLimitDatabaseBackend { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RateLimitHitsAddend) DeepCopyInto(out *RateLimitHitsAddend) { + *out = *in + if in.Number != nil { + in, out := &in.Number, &out.Number + *out = new(uint64) + **out = **in + } + if in.Format != nil { + in, out := &in.Format, &out.Format + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitHitsAddend. +func (in *RateLimitHitsAddend) DeepCopy() *RateLimitHitsAddend { + if in == nil { + return nil + } + out := new(RateLimitHitsAddend) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RateLimitMetrics) DeepCopyInto(out *RateLimitMetrics) { *out = *in @@ -4636,6 +4661,16 @@ func (in *RateLimitRule) DeepCopyInto(out *RateLimitRule) { } } out.Limit = in.Limit + if in.RequestHitsAddend != nil { + in, out := &in.RequestHitsAddend, &out.RequestHitsAddend + *out = new(RateLimitHitsAddend) + (*in).DeepCopyInto(*out) + } + if in.ResponseHitsAddend != nil { + in, out := &in.ResponseHitsAddend, &out.ResponseHitsAddend + *out = new(RateLimitHitsAddend) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitRule. diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index f9fb0f329dd..757623ef616 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -777,6 +777,68 @@ spec: - requests - unit type: object + requestHitsAddend: + description: |- + RequestHitsAddend specifies the number to reduce the rate limit counters + on the request path. If the addend is not specified, the default behavior + is to reduce the rate limit counters by 1. + + When Envoy receives a request that matches the rule, it tries to reduce the + rate limit counters by the specified number. If the counter doesn't have + enough capacity, the request is rate limited. + properties: + format: + description: |- + Format specifies the format of the usage number. See the Envoy documentation for the supported format which + is the same as the access log format: + https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format + + For example `%DYNAMIC_METADATA(com.test.my_filter:test_key)%"` will retrieve the usage number from the + `com.test.my_filter` filter metadata namespace with the key `test_key`. + Another example is `%BYTES_RECEIVED%` which will retrieve the usage number from the bytes received by the client. + type: string + number: + description: Number specifies the fixed usage number + to reduce the rate limit counters. + format: int64 + type: integer + type: object + x-kubernetes-validations: + - message: only one of number or format can be specified + rule: '!(has(self.number) && has(self.format))' + responseHitsAddend: + description: |- + ResponseHitsAddend specifies the number to reduce the rate limit counters + after the response is sent back to the client or the request stream is closed. + + The addend is used to reduce the rate limit counters for the matching requests. + Since the reduction happens after the request stream is complete, the rate limit + won't be enforced for the current request, but for the subsequent matching requests. + + This is optional and if not specified, the rate limit counters are not reduced + on the response path. + + Currently, this is only supported for HTTP Global Rate Limits. + properties: + format: + description: |- + Format specifies the format of the usage number. See the Envoy documentation for the supported format which + is the same as the access log format: + https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format + + For example `%DYNAMIC_METADATA(com.test.my_filter:test_key)%"` will retrieve the usage number from the + `com.test.my_filter` filter metadata namespace with the key `test_key`. + Another example is `%BYTES_RECEIVED%` which will retrieve the usage number from the bytes received by the client. + type: string + number: + description: Number specifies the fixed usage number + to reduce the rate limit counters. + format: int64 + type: integer + type: object + x-kubernetes-validations: + - message: only one of number or format can be specified + rule: '!(has(self.number) && has(self.format))' required: - limit type: object @@ -912,11 +974,77 @@ spec: - requests - unit type: object + requestHitsAddend: + description: |- + RequestHitsAddend specifies the number to reduce the rate limit counters + on the request path. If the addend is not specified, the default behavior + is to reduce the rate limit counters by 1. + + When Envoy receives a request that matches the rule, it tries to reduce the + rate limit counters by the specified number. If the counter doesn't have + enough capacity, the request is rate limited. + properties: + format: + description: |- + Format specifies the format of the usage number. See the Envoy documentation for the supported format which + is the same as the access log format: + https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format + + For example `%DYNAMIC_METADATA(com.test.my_filter:test_key)%"` will retrieve the usage number from the + `com.test.my_filter` filter metadata namespace with the key `test_key`. + Another example is `%BYTES_RECEIVED%` which will retrieve the usage number from the bytes received by the client. + type: string + number: + description: Number specifies the fixed usage number + to reduce the rate limit counters. + format: int64 + type: integer + type: object + x-kubernetes-validations: + - message: only one of number or format can be specified + rule: '!(has(self.number) && has(self.format))' + responseHitsAddend: + description: |- + ResponseHitsAddend specifies the number to reduce the rate limit counters + after the response is sent back to the client or the request stream is closed. + + The addend is used to reduce the rate limit counters for the matching requests. + Since the reduction happens after the request stream is complete, the rate limit + won't be enforced for the current request, but for the subsequent matching requests. + + This is optional and if not specified, the rate limit counters are not reduced + on the response path. + + Currently, this is only supported for HTTP Global Rate Limits. + properties: + format: + description: |- + Format specifies the format of the usage number. See the Envoy documentation for the supported format which + is the same as the access log format: + https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format + + For example `%DYNAMIC_METADATA(com.test.my_filter:test_key)%"` will retrieve the usage number from the + `com.test.my_filter` filter metadata namespace with the key `test_key`. + Another example is `%BYTES_RECEIVED%` which will retrieve the usage number from the bytes received by the client. + type: string + number: + description: Number specifies the fixed usage number + to reduce the rate limit counters. + format: int64 + type: integer + type: object + x-kubernetes-validations: + - message: only one of number or format can be specified + rule: '!(has(self.number) && has(self.format))' required: - limit type: object maxItems: 16 type: array + x-kubernetes-validations: + - message: responseHitsAddend is not supported for Local Rate + Limits + rule: self.all(foo, !has(foo.responseHitsAddend)) type: object type: description: |- diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 5119d756646..dae16269797 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -3352,6 +3352,32 @@ _Appears in:_ | `Redis` | RedisBackendType uses a redis database for the rate limit service.
| +#### RateLimitHitsAddend + + + +RateLimitHitsAddend specifies where the Envoy retrieves the number to reduce the rate limit counters. + + +By default, Envoy looks up the addend from the `envoy.ratelimit.hits_addend` filter metadata. +If there's no such metadata or the number stored in the metadata is invalid, it will use the default +usage number of 1. + + +This default behavior can be overridden by specifying exactly one of the fields in this RateLimitUsage. +If either of the fields is not specified, Envoy will use the default behavior described above. + + +See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto.html#config-route-v3-ratelimit-hitsaddend +for more information. + +_Appears in:_ +- [RateLimitRule](#ratelimitrule) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | + + #### RateLimitMetrics diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index 5119d756646..dae16269797 100644 --- a/site/content/zh/latest/api/extension_types.md +++ b/site/content/zh/latest/api/extension_types.md @@ -3352,6 +3352,32 @@ _Appears in:_ | `Redis` | RedisBackendType uses a redis database for the rate limit service.
| +#### RateLimitHitsAddend + + + +RateLimitHitsAddend specifies where the Envoy retrieves the number to reduce the rate limit counters. + + +By default, Envoy looks up the addend from the `envoy.ratelimit.hits_addend` filter metadata. +If there's no such metadata or the number stored in the metadata is invalid, it will use the default +usage number of 1. + + +This default behavior can be overridden by specifying exactly one of the fields in this RateLimitUsage. +If either of the fields is not specified, Envoy will use the default behavior described above. + + +See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto.html#config-route-v3-ratelimit-hitsaddend +for more information. + +_Appears in:_ +- [RateLimitRule](#ratelimitrule) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | + + #### RateLimitMetrics diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go index d5e6a1b2d1f..7b54c4cca69 100644 --- a/test/cel-validation/backendtrafficpolicy_test.go +++ b/test/cel-validation/backendtrafficpolicy_test.go @@ -1502,6 +1502,126 @@ func TestBackendTrafficPolicyTarget(t *testing.T) { "only ConfigMap is supported for ValueRe", }, }, + { + desc: "valid Global rate limit rules with request and response hit addends", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + rules := []egv1a1.RateLimitRule{ + { + Limit: egv1a1.RateLimitValue{Requests: 10, Unit: "Minute"}, + // Default values for RequestHitsAddend and ResponseHitsAddend. + ResponseHitsAddend: &egv1a1.RateLimitHitsAddend{}, + RequestHitsAddend: &egv1a1.RateLimitHitsAddend{}, + }, + { + Limit: egv1a1.RateLimitValue{Requests: 10, Unit: "Minute"}, + // Only ResponseHitsAddend is set. + RequestHitsAddend: &egv1a1.RateLimitHitsAddend{}, + }, + { + Limit: egv1a1.RateLimitValue{Requests: 10, Unit: "Minute"}, + // Only RequestHitsAddend is set. + ResponseHitsAddend: &egv1a1.RateLimitHitsAddend{}, + }, + { + Limit: egv1a1.RateLimitValue{Requests: 10, Unit: "Minute"}, + // Both RequestHitsAddend and ResponseHitsAddend are set with values. + RequestHitsAddend: &egv1a1.RateLimitHitsAddend{Number: ptr.To[uint64](200)}, + ResponseHitsAddend: &egv1a1.RateLimitHitsAddend{Number: ptr.To[uint64](200)}, + }, + { + Limit: egv1a1.RateLimitValue{Requests: 10, Unit: "Minute"}, + // Both RequestHitsAddend and ResponseHitsAddend are set with formats. + RequestHitsAddend: &egv1a1.RateLimitHitsAddend{Format: ptr.To[string]("%DYNAMIC_METADATA(com.test.my_filter:test_key)%")}, + ResponseHitsAddend: &egv1a1.RateLimitHitsAddend{Format: ptr.To[string]("%DYNAMIC_METADATA(com.test.my_filter:test_key)%")}, + }, + } + + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + RateLimit: &egv1a1.RateLimitSpec{ + Type: egv1a1.GlobalRateLimitType, + Global: &egv1a1.GlobalRateLimit{ + Rules: rules, + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "invalid Global rate limit rules with request and response hit addends specifying both number and format fields", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + RateLimit: &egv1a1.RateLimitSpec{ + Type: egv1a1.GlobalRateLimitType, + Global: &egv1a1.GlobalRateLimit{ + Rules: []egv1a1.RateLimitRule{ + { + Limit: egv1a1.RateLimitValue{Requests: 10, Unit: "Minute"}, + RequestHitsAddend: &egv1a1.RateLimitHitsAddend{ + Format: ptr.To[string]("foo"), + Number: ptr.To[uint64](200), + }, + ResponseHitsAddend: &egv1a1.RateLimitHitsAddend{ + Format: ptr.To[string]("bar"), + Number: ptr.To[uint64](200), + }, + }, + }, + }, + }, + } + }, + wantErrors: []string{ + `only one of number or format can be specified, spec.rateLimit.global.rules[0].responseHitsAddend`, + }, + }, + { + desc: "invalid count of local rate limit rules specifying ResponseHitsAddend", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + RateLimit: &egv1a1.RateLimitSpec{ + Type: egv1a1.GlobalRateLimitType, + Local: &egv1a1.LocalRateLimit{ + Rules: []egv1a1.RateLimitRule{ + { + Limit: egv1a1.RateLimitValue{Requests: 10, Unit: "Minute"}, + // This is not supported for LocalRateLimit. + ResponseHitsAddend: &egv1a1.RateLimitHitsAddend{}, + }, + }, + }, + }, + } + }, + wantErrors: []string{`responseHitsAddend is not supported for Local Rate Limits`}, + }, } for _, tc := range cases {