From 70e8b66fe548cb124ee2f73333b61b37f603513b Mon Sep 17 00:00:00 2001 From: Takeshi Yoneda Date: Fri, 10 Jan 2025 05:27:42 +0900 Subject: [PATCH] api: adds cost specifier to RateLimitRule (#4957) * api: usage based rate limit API support Signed-off-by: Takeshi Yoneda --- api/v1alpha1/ratelimit_types.go | 86 +++++++ api/v1alpha1/zz_generated.deepcopy.go | 70 ++++++ ....envoyproxy.io_backendtrafficpolicies.yaml | 215 ++++++++++++++++++ site/content/en/latest/api/extension_types.md | 58 +++++ site/content/zh/latest/api/extension_types.md | 58 +++++ .../backendtrafficpolicy_test.go | 131 +++++++++++ 6 files changed, 618 insertions(+) diff --git a/api/v1alpha1/ratelimit_types.go b/api/v1alpha1/ratelimit_types.go index 72382d699f1..de195ffd760 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.cost) || !has(foo.cost.response))", message="response cost is not supported for Local Rate Limits" Rules []RateLimitRule `json:"rules"` } @@ -91,6 +92,91 @@ 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"` + // Cost specifies the cost of requests and responses for the rule. + // + // This is optional and if not specified, the default behavior is to reduce the rate limit counters by 1 on + // the request path and do not reduce the rate limit counters on the response path. + // + // +optional + // +notImplementedHide + Cost *RateLimitCost `json:"cost,omitempty"` +} + +type RateLimitCost struct { + // Request specifies the number to reduce the rate limit counters + // on the request path. If this 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 + Request *RateLimitCostSpecifier `json:"request,omitempty"` + // Response 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 cost 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 + Response *RateLimitCostSpecifier `json:"response,omitempty"` +} + +// RateLimitCostSpecifier specifies where the Envoy retrieves the number to reduce the rate limit counters. +// +// +kubebuilder:validation:XValidation:rule="!(has(self.number) && has(self.metadata))",message="only one of number or metadata can be specified" +type RateLimitCostSpecifier struct { + // From specifies where to get the rate limit cost. Currently, only "Number" and "Metadata" are supported. + // + // +kubebuilder:validation:Required + From RateLimitCostFrom `json:"from"` + // Number specifies the fixed usage number to reduce the rate limit counters. + // Using zero can be used to only check the rate limit counters without reducing them. + // + // +optional + // +notImplementedHide + Number *uint64 `json:"number,omitempty"` + // Metadata specifies the per-request metadata to retrieve the usage number from. + // + // +optional + // +notImplementedHide + Metadata *RateLimitCostMetadata `json:"metadata,omitempty"` +} + +// RateLimitCostFrom specifies the source of the rate limit cost. +// Valid RateLimitCostType values are "Number" and "Metadata". +// +// +kubebuilder:validation:Enum=Number;Metadata +type RateLimitCostFrom string + +const ( + // RateLimitCostFromNumber specifies the rate limit cost to be a fixed number. + RateLimitCostFromNumber RateLimitCostFrom = "Number" + // RateLimitCostFromMetadata specifies the rate limit cost to be retrieved from the per-request dynamic metadata. + RateLimitCostFromMetadata RateLimitCostFrom = "Metadata" + // TODO: add headers, etc. Anything that can be represented in "Format" can be added here. + // https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format +) + +// RateLimitCostMetadata specifies the filter metadata to retrieve the usage number from. +type RateLimitCostMetadata struct { + // Namespace is the namespace of the dynamic metadata. + // + // +kubebuilder:validation:Required + Namespace string `json:"namespace"` + // Key is the key to retrieve the usage number from the filter metadata. + // + // +kubebuilder:validation:Required + Key string `json:"key"` } // 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 a01ae7856b7..d3b252dede0 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -4597,6 +4597,71 @@ func (in *RateLimit) DeepCopy() *RateLimit { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RateLimitCost) DeepCopyInto(out *RateLimitCost) { + *out = *in + if in.Request != nil { + in, out := &in.Request, &out.Request + *out = new(RateLimitCostSpecifier) + (*in).DeepCopyInto(*out) + } + if in.Response != nil { + in, out := &in.Response, &out.Response + *out = new(RateLimitCostSpecifier) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitCost. +func (in *RateLimitCost) DeepCopy() *RateLimitCost { + if in == nil { + return nil + } + out := new(RateLimitCost) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RateLimitCostMetadata) DeepCopyInto(out *RateLimitCostMetadata) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitCostMetadata. +func (in *RateLimitCostMetadata) DeepCopy() *RateLimitCostMetadata { + if in == nil { + return nil + } + out := new(RateLimitCostMetadata) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RateLimitCostSpecifier) DeepCopyInto(out *RateLimitCostSpecifier) { + *out = *in + if in.Number != nil { + in, out := &in.Number, &out.Number + *out = new(uint64) + **out = **in + } + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = new(RateLimitCostMetadata) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitCostSpecifier. +func (in *RateLimitCostSpecifier) DeepCopy() *RateLimitCostSpecifier { + if in == nil { + return nil + } + out := new(RateLimitCostSpecifier) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RateLimitDatabaseBackend) DeepCopyInto(out *RateLimitDatabaseBackend) { *out = *in @@ -4683,6 +4748,11 @@ func (in *RateLimitRule) DeepCopyInto(out *RateLimitRule) { } } out.Limit = in.Limit + if in.Cost != nil { + in, out := &in.Cost, &out.Cost + *out = new(RateLimitCost) + (*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..38c5d9a3cce 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -752,6 +752,112 @@ spec: type: object maxItems: 8 type: array + cost: + description: |- + Cost specifies the cost of requests and responses for the rule. + + This is optional and if not specified, the default behavior is to reduce the rate limit counters by 1 on + the request path and do not reduce the rate limit counters on the response path. + properties: + request: + description: |- + Request specifies the number to reduce the rate limit counters + on the request path. If this 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: + from: + description: From specifies where to get the + rate limit cost. Currently, only "Number" + and "Metadata" are supported. + enum: + - Number + - Metadata + type: string + metadata: + description: Metadata specifies the per-request + metadata to retrieve the usage number from. + properties: + key: + description: Key is the key to retrieve + the usage number from the filter metadata. + type: string + namespace: + description: Namespace is the namespace + of the dynamic metadata. + type: string + required: + - key + - namespace + type: object + number: + description: |- + Number specifies the fixed usage number to reduce the rate limit counters. + Using zero can be used to only check the rate limit counters without reducing them. + format: int64 + type: integer + required: + - from + type: object + x-kubernetes-validations: + - message: only one of number or metadata can be + specified + rule: '!(has(self.number) && has(self.metadata))' + response: + description: |- + Response 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 cost 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: + from: + description: From specifies where to get the + rate limit cost. Currently, only "Number" + and "Metadata" are supported. + enum: + - Number + - Metadata + type: string + metadata: + description: Metadata specifies the per-request + metadata to retrieve the usage number from. + properties: + key: + description: Key is the key to retrieve + the usage number from the filter metadata. + type: string + namespace: + description: Namespace is the namespace + of the dynamic metadata. + type: string + required: + - key + - namespace + type: object + number: + description: |- + Number specifies the fixed usage number to reduce the rate limit counters. + Using zero can be used to only check the rate limit counters without reducing them. + format: int64 + type: integer + required: + - from + type: object + x-kubernetes-validations: + - message: only one of number or metadata can be + specified + rule: '!(has(self.number) && has(self.metadata))' + type: object limit: description: |- Limit holds the rate limit values. @@ -887,6 +993,112 @@ spec: type: object maxItems: 8 type: array + cost: + description: |- + Cost specifies the cost of requests and responses for the rule. + + This is optional and if not specified, the default behavior is to reduce the rate limit counters by 1 on + the request path and do not reduce the rate limit counters on the response path. + properties: + request: + description: |- + Request specifies the number to reduce the rate limit counters + on the request path. If this 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: + from: + description: From specifies where to get the + rate limit cost. Currently, only "Number" + and "Metadata" are supported. + enum: + - Number + - Metadata + type: string + metadata: + description: Metadata specifies the per-request + metadata to retrieve the usage number from. + properties: + key: + description: Key is the key to retrieve + the usage number from the filter metadata. + type: string + namespace: + description: Namespace is the namespace + of the dynamic metadata. + type: string + required: + - key + - namespace + type: object + number: + description: |- + Number specifies the fixed usage number to reduce the rate limit counters. + Using zero can be used to only check the rate limit counters without reducing them. + format: int64 + type: integer + required: + - from + type: object + x-kubernetes-validations: + - message: only one of number or metadata can be + specified + rule: '!(has(self.number) && has(self.metadata))' + response: + description: |- + Response 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 cost 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: + from: + description: From specifies where to get the + rate limit cost. Currently, only "Number" + and "Metadata" are supported. + enum: + - Number + - Metadata + type: string + metadata: + description: Metadata specifies the per-request + metadata to retrieve the usage number from. + properties: + key: + description: Key is the key to retrieve + the usage number from the filter metadata. + type: string + namespace: + description: Namespace is the namespace + of the dynamic metadata. + type: string + required: + - key + - namespace + type: object + number: + description: |- + Number specifies the fixed usage number to reduce the rate limit counters. + Using zero can be used to only check the rate limit counters without reducing them. + format: int64 + type: integer + required: + - from + type: object + x-kubernetes-validations: + - message: only one of number or metadata can be + specified + rule: '!(has(self.number) && has(self.metadata))' + type: object limit: description: |- Limit holds the rate limit values. @@ -917,6 +1129,9 @@ spec: type: object maxItems: 16 type: array + x-kubernetes-validations: + - message: response cost is not supported for Local Rate Limits + rule: self.all(foo, !has(foo.cost) || !has(foo.cost.response)) 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 4e3e89da5db..b27e7c8d12d 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -3377,6 +3377,64 @@ _Appears in:_ | `telemetry` | _[RateLimitTelemetry](#ratelimittelemetry)_ | false | Telemetry defines telemetry configuration for RateLimit. | +#### RateLimitCost + + + + + +_Appears in:_ +- [RateLimitRule](#ratelimitrule) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | + + +#### RateLimitCostFrom + +_Underlying type:_ _string_ + +RateLimitCostFrom specifies the source of the rate limit cost. +Valid RateLimitCostType values are "Number" and "Metadata". + +_Appears in:_ +- [RateLimitCostSpecifier](#ratelimitcostspecifier) + +| Value | Description | +| ----- | ----------- | +| `Number` | RateLimitCostFromNumber specifies the rate limit cost to be a fixed number.
| +| `Metadata` | RateLimitCostFromMetadata specifies the rate limit cost to be retrieved from the per-request dynamic metadata.
| + + +#### RateLimitCostMetadata + + + +RateLimitCostMetadata specifies the filter metadata to retrieve the usage number from. + +_Appears in:_ +- [RateLimitCostSpecifier](#ratelimitcostspecifier) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `namespace` | _string_ | true | Namespace is the namespace of the dynamic metadata. | +| `key` | _string_ | true | Key is the key to retrieve the usage number from the filter metadata. | + + +#### RateLimitCostSpecifier + + + +RateLimitCostSpecifier specifies where the Envoy retrieves the number to reduce the rate limit counters. + +_Appears in:_ +- [RateLimitCost](#ratelimitcost) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `from` | _[RateLimitCostFrom](#ratelimitcostfrom)_ | true | From specifies where to get the rate limit cost. Currently, only "Number" and "Metadata" are supported. | + + #### RateLimitDatabaseBackend diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index 4e3e89da5db..b27e7c8d12d 100644 --- a/site/content/zh/latest/api/extension_types.md +++ b/site/content/zh/latest/api/extension_types.md @@ -3377,6 +3377,64 @@ _Appears in:_ | `telemetry` | _[RateLimitTelemetry](#ratelimittelemetry)_ | false | Telemetry defines telemetry configuration for RateLimit. | +#### RateLimitCost + + + + + +_Appears in:_ +- [RateLimitRule](#ratelimitrule) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | + + +#### RateLimitCostFrom + +_Underlying type:_ _string_ + +RateLimitCostFrom specifies the source of the rate limit cost. +Valid RateLimitCostType values are "Number" and "Metadata". + +_Appears in:_ +- [RateLimitCostSpecifier](#ratelimitcostspecifier) + +| Value | Description | +| ----- | ----------- | +| `Number` | RateLimitCostFromNumber specifies the rate limit cost to be a fixed number.
| +| `Metadata` | RateLimitCostFromMetadata specifies the rate limit cost to be retrieved from the per-request dynamic metadata.
| + + +#### RateLimitCostMetadata + + + +RateLimitCostMetadata specifies the filter metadata to retrieve the usage number from. + +_Appears in:_ +- [RateLimitCostSpecifier](#ratelimitcostspecifier) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `namespace` | _string_ | true | Namespace is the namespace of the dynamic metadata. | +| `key` | _string_ | true | Key is the key to retrieve the usage number from the filter metadata. | + + +#### RateLimitCostSpecifier + + + +RateLimitCostSpecifier specifies where the Envoy retrieves the number to reduce the rate limit counters. + +_Appears in:_ +- [RateLimitCost](#ratelimitcost) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `from` | _[RateLimitCostFrom](#ratelimitcostfrom)_ | true | From specifies where to get the rate limit cost. Currently, only "Number" and "Metadata" are supported. | + + #### RateLimitDatabaseBackend diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go index d5e6a1b2d1f..d2d0c082b2f 100644 --- a/test/cel-validation/backendtrafficpolicy_test.go +++ b/test/cel-validation/backendtrafficpolicy_test.go @@ -1502,6 +1502,137 @@ 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"}, + Cost: &egv1a1.RateLimitCost{ + Request: &egv1a1.RateLimitCostSpecifier{From: egv1a1.RateLimitCostFromNumber, Number: ptr.To[uint64](200)}, + }, + }, + { + Limit: egv1a1.RateLimitValue{Requests: 10, Unit: "Minute"}, + Cost: &egv1a1.RateLimitCost{ + Response: &egv1a1.RateLimitCostSpecifier{From: egv1a1.RateLimitCostFromNumber, Number: ptr.To[uint64](200)}, + }, + }, + { + Limit: egv1a1.RateLimitValue{Requests: 10, Unit: "Minute"}, + Cost: &egv1a1.RateLimitCost{ + Request: &egv1a1.RateLimitCostSpecifier{From: egv1a1.RateLimitCostFromNumber, Number: ptr.To[uint64](200)}, + Response: &egv1a1.RateLimitCostSpecifier{From: egv1a1.RateLimitCostFromNumber, Number: ptr.To[uint64](200)}, + }, + }, + { + Limit: egv1a1.RateLimitValue{Requests: 10, Unit: "Minute"}, + Cost: &egv1a1.RateLimitCost{ + Request: &egv1a1.RateLimitCostSpecifier{ + From: egv1a1.RateLimitCostFromMetadata, + Metadata: &egv1a1.RateLimitCostMetadata{ + Namespace: "com.test.my_filter", + Key: "on_request_key", + }, + }, + Response: &egv1a1.RateLimitCostSpecifier{ + From: egv1a1.RateLimitCostFromMetadata, + Metadata: &egv1a1.RateLimitCostMetadata{ + Namespace: "com.test.my_filter", + Key: "on_response_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 cost specifying both number and metadata 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"}, + Cost: &egv1a1.RateLimitCost{ + Request: &egv1a1.RateLimitCostSpecifier{ + From: egv1a1.RateLimitCostFromNumber, + Metadata: &egv1a1.RateLimitCostMetadata{}, + Number: ptr.To[uint64](200), + }, + }, + }, + }, + }, + }, + } + }, + wantErrors: []string{ + `spec.rateLimit.global.rules[0].cost.request: Invalid value: "object": only one of number or metadata can be specified`, + }, + }, + { + desc: "invalid count of local rate limit rules specifying costPerResponse", + 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"}, + Cost: &egv1a1.RateLimitCost{ + // This is not supported for LocalRateLimit. + Response: &egv1a1.RateLimitCostSpecifier{From: egv1a1.RateLimitCostFromNumber, Number: ptr.To[uint64](200)}, + }, + }, + }, + }, + }, + } + }, + wantErrors: []string{`response cost is not supported for Local Rate Limits`}, + }, } for _, tc := range cases {