diff --git a/.github/workflows/cherrypick.yaml b/.github/workflows/cherrypick.yaml index 4b35693e9cc6..073cddc7979d 100644 --- a/.github/workflows/cherrypick.yaml +++ b/.github/workflows/cherrypick.yaml @@ -6,23 +6,23 @@ on: types: ["closed"] jobs: - cherry_pick_release_v0_5: + cherry_pick_release_v0_6: runs-on: ubuntu-22.04 - name: Cherry pick into release-v0.5 - if: ${{ contains(github.event.pull_request.labels.*.name, 'cherrypick/release-v0.5') && github.event.pull_request.merged == true }} + name: Cherry pick into release-v0.6 + if: ${{ contains(github.event.pull_request.labels.*.name, 'cherrypick/release-v0.6') && github.event.pull_request.merged == true }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Cherry pick into release/v0.5 + - name: Cherry pick into release/v0.6 uses: carloscastrojumo/github-cherry-pick-action@v1.0.9 with: - branch: release/v0.5 - title: "[release/v0.5] {old_title}" - body: "Cherry picking #{old_pull_request_id} onto release/v0.5" + branch: release/v0.6 + title: "[release/v0.6] {old_title}" + body: "Cherry picking #{old_pull_request_id} onto release/v0.6" labels: | - cherrypick/release-v0.5 + cherrypick/release-v0.6 # put release manager here reviewers: | arkodg diff --git a/api/v1alpha1/envoypatchpolicy_types.go b/api/v1alpha1/envoypatchpolicy_types.go index 30c7f8652c07..1ac40697d668 100644 --- a/api/v1alpha1/envoypatchpolicy_types.go +++ b/api/v1alpha1/envoypatchpolicy_types.go @@ -49,7 +49,8 @@ type EnvoyPatchPolicySpec struct { JSONPatches []EnvoyJSONPatchConfig `json:"jsonPatches,omitempty"` // TargetRef is the name of the Gateway API resource this policy // is being attached to. - // Currently only attaching to Gateway is supported + // By default attaching to Gateway is supported and + // when mergeGateways is enabled it should attach to GatewayClass. // This Policy and the TargetRef MUST be in the same namespace // for this Policy to have effect and be applied to the Gateway // TargetRef diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index acf34417c761..e11da79d8504 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -134,7 +134,7 @@ type EnvoyProxyKubernetesProvider struct { // +optional // +kubebuilder:validation:XValidation:message="minReplicas must be greater than 0",rule="!has(self.minReplicas) || self.minReplicas > 0" // +kubebuilder:validation:XValidation:message="maxReplicas must be greater than 0",rule="!has(self.maxReplicas) || self.maxReplicas > 0" - // +kubebuilder:validation:XValidation:message="maxReplicas cannot be less than or equal to minReplicas",rule="!has(self.minReplicas) || self.maxReplicas > self.minReplicas" + // +kubebuilder:validation:XValidation:message="maxReplicas cannot be less than minReplicas",rule="!has(self.minReplicas) || self.maxReplicas >= self.minReplicas" EnvoyHpa *KubernetesHorizontalPodAutoscalerSpec `json:"envoyHpa,omitempty"` } diff --git a/api/v1alpha1/ratelimit_types.go b/api/v1alpha1/ratelimit_types.go index e013579efdf0..6e183b7ccd91 100644 --- a/api/v1alpha1/ratelimit_types.go +++ b/api/v1alpha1/ratelimit_types.go @@ -9,7 +9,7 @@ package v1alpha1 // +union type RateLimitSpec struct { // Type decides the scope for the RateLimits. - // Valid RateLimitType values are "Global". + // Valid RateLimitType values are "Global" or "Local". // // +unionDiscriminator Type RateLimitType `json:"type"` @@ -17,27 +17,50 @@ type RateLimitSpec struct { // // +optional Global *GlobalRateLimit `json:"global,omitempty"` + + // Local defines local rate limit configuration. + // + // +optional + Local *LocalRateLimit `json:"local,omitempty"` } // RateLimitType specifies the types of RateLimiting. -// +kubebuilder:validation:Enum=Global +// +kubebuilder:validation:Enum=Global;Local type RateLimitType string const ( - // GlobalRateLimitType allows the rate limits to be applied across all Envoy proxy instances. + // GlobalRateLimitType allows the rate limits to be applied across all Envoy + // proxy instances. GlobalRateLimitType RateLimitType = "Global" + + // LocalRateLimitType allows the rate limits to be applied on a per Envoy + // proxy instance basis. + LocalRateLimitType RateLimitType = "Local" ) // GlobalRateLimit defines global rate limit configuration. type GlobalRateLimit struct { - // Rules are a list of RateLimit selectors and limits. - // Each rule and its associated limit is applied - // in a mutually exclusive way i.e. if multiple - // rules get selected, each of their associated - // limits get applied, so a single traffic request - // might increase the rate limit counters for multiple - // rules if selected. + // Rules are a list of RateLimit selectors and limits. Each rule and its + // associated limit is applied in a mutually exclusive way. If a request + // matches multiple rules, each of their associated limits get applied, so a + // single request might increase the rate limit counters for multiple rules + // if selected. The rate limit service will return a logical OR of the individual + // rate limit decisions of all matching rules. For example, if a request + // matches two rules, one rate limited and one not, the final decision will be + // to rate limit the request. + // + // +kubebuilder:validation:MaxItems=16 + Rules []RateLimitRule `json:"rules"` +} + +// LocalRateLimit defines local rate limit configuration. +type LocalRateLimit struct { + // Rules are a list of RateLimit selectors and limits. If a request matches + // multiple rules, the strictest limit is applied. For example, if a request + // matches two rules, one with 10rps and one with 20rps, the final limit will + // be based on the rule with 10rps. // + // +optional // +kubebuilder:validation:MaxItems=16 Rules []RateLimitRule `json:"rules"` } @@ -49,8 +72,14 @@ type RateLimitRule struct { // specific clients using attributes from the traffic flow. // All individual select conditions must hold True for this rule // and its limit to be applied. - // If this field is empty, it is equivalent to True, and - // the limit is applied. + // + // If no client selectors are specified, the rule applies to all traffic of + // the targeted Route. + // + // If the policy targets a Gateway, the rule applies to each Route of the Gateway. + // Please note that each Route has its own rate limit counters. For example, + // if a Gateway has two Routes, and the policy has a rule with limit 10rps, + // each Route will have its own 10rps limit. // // +optional // +kubebuilder:validation:MaxItems=8 @@ -70,6 +99,7 @@ type RateLimitRule struct { type RateLimitSelectCondition struct { // Headers is a list of request headers to match. Multiple header values are ANDed together, // meaning, a request MUST match all the specified headers. + // At least one of headers or sourceCIDR condition must be specified. // // +listType=map // +listMapKey=name @@ -78,6 +108,7 @@ type RateLimitSelectCondition struct { Headers []HeaderMatch `json:"headers,omitempty"` // SourceCIDR is the client IP Address range to match on. + // At least one of headers or sourceCIDR condition must be specified. // // +optional SourceCIDR *SourceMatch `json:"sourceCIDR,omitempty"` @@ -91,6 +122,7 @@ const ( SourceMatchExact SourceMatchType = "Exact" // SourceMatchDistinct Each IP Address within the specified Source IP CIDR is treated as a distinct client selector // and uses a separate rate limit bucket/counter. + // Note: This is only supported for Global Rate Limits. SourceMatchDistinct SourceMatchType = "Distinct" ) @@ -148,6 +180,7 @@ const ( // HeaderMatchDistinct matches any and all possible unique values encountered in the // specified HTTP Header. Note that each unique value will receive its own rate limit // bucket. + // Note: This is only supported for Global Rate Limits. HeaderMatchDistinct HeaderMatchType = "Distinct" ) @@ -162,3 +195,18 @@ type RateLimitValue struct { // // +kubebuilder:validation:Enum=Second;Minute;Hour;Day type RateLimitUnit string + +// RateLimitUnit constants. +const ( + // RateLimitUnitSecond specifies the rate limit interval to be 1 second. + RateLimitUnitSecond RateLimitUnit = "Second" + + // RateLimitUnitMinute specifies the rate limit interval to be 1 minute. + RateLimitUnitMinute RateLimitUnit = "Minute" + + // RateLimitUnitHour specifies the rate limit interval to be 1 hour. + RateLimitUnitHour RateLimitUnit = "Hour" + + // RateLimitUnitDay specifies the rate limit interval to be 1 day. + RateLimitUnitDay RateLimitUnit = "Day" +) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 93b6aa7711bc..87bc434fbb2e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1776,6 +1776,28 @@ func (in *LoadBalancer) DeepCopy() *LoadBalancer { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalRateLimit) DeepCopyInto(out *LocalRateLimit) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]RateLimitRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalRateLimit. +func (in *LocalRateLimit) DeepCopy() *LocalRateLimit { + if in == nil { + return nil + } + out := new(LocalRateLimit) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDC) DeepCopyInto(out *OIDC) { *out = *in @@ -2260,6 +2282,11 @@ func (in *RateLimitSpec) DeepCopyInto(out *RateLimitSpec) { *out = new(GlobalRateLimit) (*in).DeepCopyInto(*out) } + if in.Local != nil { + in, out := &in.Local, &out.Local + *out = new(LocalRateLimit) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitSpec. 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 421f651e2cc0..9790d76a1d2d 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -211,22 +211,32 @@ spec: rules: description: Rules are a list of RateLimit selectors and limits. Each rule and its associated limit is applied in a mutually - exclusive way i.e. if multiple rules get selected, each - of their associated limits get applied, so a single traffic - request might increase the rate limit counters for multiple - rules if selected. + exclusive way. If a request matches multiple rules, each + of their associated limits get applied, so a single request + might increase the rate limit counters for multiple rules + if selected. The rate limit service will return a logical + OR of the individual rate limit decisions of all matching + rules. For example, if a request matches two rules, one + rate limited and one not, the final decision will be to + rate limit the request. items: description: RateLimitRule defines the semantics for matching attributes from the incoming requests, and setting limits for them. properties: clientSelectors: - description: ClientSelectors holds the list of select + description: "ClientSelectors holds the list of select conditions to select specific clients using attributes from the traffic flow. All individual select conditions must hold True for this rule and its limit to be applied. - If this field is empty, it is equivalent to True, - and the limit is applied. + \n If no client selectors are specified, the rule + applies to all traffic of the targeted Route. \n If + the policy targets a Gateway, the rule applies to + each Route of the Gateway. Please note that each Route + has its own rate limit counters. For example, if a + Gateway has two Routes, and the policy has a rule + with limit 10rps, each Route will have its own 10rps + limit." items: description: RateLimitSelectCondition specifies the attributes within the traffic flow that can be used @@ -238,7 +248,8 @@ spec: description: Headers is a list of request headers to match. Multiple header values are ANDed together, meaning, a request MUST match all the specified - headers. + headers. At least one of headers or sourceCIDR + condition must be specified. items: description: HeaderMatch defines the match attributes within the HTTP Headers of the request. @@ -276,7 +287,8 @@ spec: x-kubernetes-list-type: map sourceCIDR: description: SourceCIDR is the client IP Address - range to match on. + range to match on. At least one of headers or + sourceCIDR condition must be specified. properties: type: default: Exact @@ -331,11 +343,143 @@ spec: required: - rules type: object + local: + description: Local defines local rate limit configuration. + properties: + rules: + description: Rules are a list of RateLimit selectors and limits. + If a request matches multiple rules, the strictest limit + is applied. For example, if a request matches two rules, + one with 10rps and one with 20rps, the final limit will + be based on the rule with 10rps. + items: + description: RateLimitRule defines the semantics for matching + attributes from the incoming requests, and setting limits + for them. + properties: + clientSelectors: + description: "ClientSelectors holds the list of select + conditions to select specific clients using attributes + from the traffic flow. All individual select conditions + must hold True for this rule and its limit to be applied. + \n If no client selectors are specified, the rule + applies to all traffic of the targeted Route. \n If + the policy targets a Gateway, the rule applies to + each Route of the Gateway. Please note that each Route + has its own rate limit counters. For example, if a + Gateway has two Routes, and the policy has a rule + with limit 10rps, each Route will have its own 10rps + limit." + items: + description: RateLimitSelectCondition specifies the + attributes within the traffic flow that can be used + to select a subset of clients to be ratelimited. + All the individual conditions must hold True for + the overall condition to hold True. + properties: + headers: + description: Headers is a list of request headers + to match. Multiple header values are ANDed together, + meaning, a request MUST match all the specified + headers. At least one of headers or sourceCIDR + condition must be specified. + items: + description: HeaderMatch defines the match attributes + within the HTTP Headers of the request. + properties: + name: + description: Name of the HTTP header. + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: Type specifies how to match + against the value of the header. + enum: + - Exact + - RegularExpression + - Distinct + type: string + value: + description: Value within the HTTP header. + Due to the case-insensitivity of header + names, "foo" and "Foo" are considered + equivalent. Do not set this field when + Type="Distinct", implying matching on + any/all unique values within the header. + maxLength: 1024 + type: string + required: + - name + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + sourceCIDR: + description: SourceCIDR is the client IP Address + range to match on. At least one of headers or + sourceCIDR condition must be specified. + properties: + type: + default: Exact + type: string + value: + description: Value is the IP CIDR that represents + the range of Source IP Addresses of the + client. These could also be the intermediate + addresses through which the request has + flown through and is part of the `X-Forwarded-For` + header. For example, `192.168.0.1/32`, `192.168.0.0/24`, + `001:db8::/64`. + maxLength: 256 + minLength: 1 + type: string + required: + - value + type: object + type: object + maxItems: 8 + type: array + limit: + description: Limit holds the rate limit values. This + limit is applied for traffic flows when the selectors + compute to True, causing the request to be counted + towards the limit. The limit is enforced and the request + is ratelimited, i.e. a response with 429 HTTP status + code is sent back to the client when the selected + requests have reached the limit. + properties: + requests: + type: integer + unit: + description: RateLimitUnit specifies the intervals + for setting rate limits. Valid RateLimitUnit values + are "Second", "Minute", "Hour", and "Day". + enum: + - Second + - Minute + - Hour + - Day + type: string + required: + - requests + - unit + type: object + required: + - limit + type: object + maxItems: 16 + type: array + type: object type: description: Type decides the scope for the RateLimits. Valid - RateLimitType values are "Global". + RateLimitType values are "Global" or "Local". enum: - Global + - Local type: string required: - type diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml index e5b61e5d87ca..339564310e5d 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml @@ -102,9 +102,11 @@ spec: type: integer targetRef: description: TargetRef is the name of the Gateway API resource this - policy is being attached to. Currently only attaching to Gateway - is supported This Policy and the TargetRef MUST be in the same namespace - for this Policy to have effect and be applied to the Gateway TargetRef + policy is being attached to. By default attaching to Gateway is + supported and when mergeGateways is enabled it should attach to + GatewayClass. This Policy and the TargetRef MUST be in the same + namespace for this Policy to have effect and be applied to the Gateway + TargetRef properties: group: description: Group is the group of the target resource. diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index ed653daf7b22..11c48953e588 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -6272,8 +6272,8 @@ spec: rule: '!has(self.minReplicas) || self.minReplicas > 0' - message: maxReplicas must be greater than 0 rule: '!has(self.maxReplicas) || self.maxReplicas > 0' - - message: maxReplicas cannot be less than or equal to minReplicas - rule: '!has(self.minReplicas) || self.maxReplicas > self.minReplicas' + - message: maxReplicas cannot be less than minReplicas + rule: '!has(self.minReplicas) || self.maxReplicas >= self.minReplicas' envoyService: description: EnvoyService defines the desired state of the Envoy service resource. If unspecified, default settings diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index f9af6fc0d68e..95c1157019ff 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -6,7 +6,9 @@ package gatewayapi import ( + "errors" "fmt" + "math" "net" "sort" "strings" @@ -242,6 +244,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen rl *ir.RateLimit lb *ir.LoadBalancer pp *ir.ProxyProtocol + cb *ir.CircuitBreaker ) // Build IR @@ -254,6 +257,10 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen if policy.Spec.ProxyProtocol != nil { pp = t.buildProxyProtocol(policy) } + if policy.Spec.CircuitBreaker != nil { + cb = t.buildCircuitBreaker(policy) + } + // Apply IR to all relevant routes prefix := irRoutePrefix(route) for _, ir := range xdsIR { @@ -264,6 +271,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen r.RateLimit = rl r.LoadBalancer = lb r.ProxyProtocol = pp + r.CircuitBreaker = cb } } } @@ -276,6 +284,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back rl *ir.RateLimit lb *ir.LoadBalancer pp *ir.ProxyProtocol + cb *ir.CircuitBreaker ) // Build IR @@ -288,6 +297,9 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back if policy.Spec.ProxyProtocol != nil { pp = t.buildProxyProtocol(policy) } + if policy.Spec.CircuitBreaker != nil { + cb = t.buildCircuitBreaker(policy) + } // Apply IR to all the routes within the specific Gateway // If the feature is already set, then skip it, since it must be have // set by a policy attaching to the route @@ -307,14 +319,151 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back if r.ProxyProtocol == nil { r.ProxyProtocol = pp } + if r.CircuitBreaker == nil { + r.CircuitBreaker = cb + } } } } func (t *Translator) buildRateLimit(policy *egv1a1.BackendTrafficPolicy) *ir.RateLimit { + switch policy.Spec.RateLimit.Type { + case egv1a1.GlobalRateLimitType: + return t.buildGlobalRateLimit(policy) + case egv1a1.LocalRateLimitType: + return t.buildLocalRateLimit(policy) + } + + status.SetBackendTrafficPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + "Invalid rateLimit type", + ) + return nil +} + +func (t *Translator) buildLocalRateLimit(policy *egv1a1.BackendTrafficPolicy) *ir.RateLimit { + if policy.Spec.RateLimit.Local == nil { + message := "Local configuration empty for rateLimit." + status.SetBackendTrafficPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + message, + ) + return nil + } + + local := policy.Spec.RateLimit.Local + + // Envoy local rateLimit requires a default limit to be set for a route. + // EG uses the first rule without clientSelectors as the default route-level + // limit. If no such rule is found, EG uses a default limit of uint32 max. + var defaultLimit *ir.RateLimitValue + for _, rule := range local.Rules { + if rule.ClientSelectors == nil || len(rule.ClientSelectors) == 0 { + if defaultLimit != nil { + message := "Local rateLimit can not have more than one rule without clientSelectors." + status.SetBackendTrafficPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + message, + ) + return nil + } + defaultLimit = &ir.RateLimitValue{ + Requests: rule.Limit.Requests, + Unit: ir.RateLimitUnit(rule.Limit.Unit), + } + } + } + // If no rule without clientSelectors is found, use uint32 max as the default + // limit, which effectively make the default limit unlimited. + if defaultLimit == nil { + defaultLimit = &ir.RateLimitValue{ + Requests: math.MaxUint32, + Unit: ir.RateLimitUnit(egv1a1.RateLimitUnitSecond), + } + } + + // Validate that the rule limit unit is a multiple of the default limit unit. + // This is required by Envoy local rateLimit implementation. + // see https://github.com/envoyproxy/envoy/blob/6d9a6e995f472526de2b75233abca69aa00021ed/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc#L49 + defaultLimitUnit := ratelimitUnitToDuration(egv1a1.RateLimitUnit(defaultLimit.Unit)) + for _, rule := range local.Rules { + ruleLimitUint := ratelimitUnitToDuration(rule.Limit.Unit) + if defaultLimitUnit == 0 || ruleLimitUint%defaultLimitUnit != 0 { + message := "Local rateLimit rule limit unit must be a multiple of the default limit unit." + status.SetBackendTrafficPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + message, + ) + return nil + } + } + + var err error + var irRule *ir.RateLimitRule + var irRules = make([]*ir.RateLimitRule, 0) + for _, rule := range local.Rules { + // We don't process the rule without clientSelectors here because it's + // previously used as the default route-level limit. + if len(rule.ClientSelectors) == 0 { + continue + } + + irRule, err = buildRateLimitRule(rule) + if err != nil { + status.SetBackendTrafficPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + status.Error2ConditionMsg(err), + ) + return nil + } + if irRule.CIDRMatch != nil && irRule.CIDRMatch.Distinct { + message := "Local rateLimit does not support distinct CIDRMatch." + status.SetBackendTrafficPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + message, + ) + return nil + } + for _, match := range irRule.HeaderMatches { + if match.Distinct { + message := "Local rateLimit does not support distinct HeaderMatch." + status.SetBackendTrafficPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + message, + ) + return nil + } + } + irRules = append(irRules, irRule) + } + + rateLimit := &ir.RateLimit{ + Local: &ir.LocalRateLimit{ + Default: *defaultLimit, + Rules: irRules, + }, + } + return rateLimit +} + +func (t *Translator) buildGlobalRateLimit(policy *egv1a1.BackendTrafficPolicy) *ir.RateLimit { if policy.Spec.RateLimit.Global == nil { - message := "Global configuration empty for rateLimit" + message := "Global configuration empty for rateLimit." status.SetBackendTrafficPolicyCondition(policy, gwv1a2.PolicyConditionAccepted, metav1.ConditionFalse, @@ -323,8 +472,9 @@ func (t *Translator) buildRateLimit(policy *egv1a1.BackendTrafficPolicy) *ir.Rat ) return nil } + if !t.GlobalRateLimitEnabled { - message := "Enable Ratelimit in the EnvoyGateway config to configure global rateLimit" + message := "Enable Ratelimit in the EnvoyGateway config to configure global rateLimit." status.SetBackendTrafficPolicyCondition(policy, gwv1a2.PolicyConditionAccepted, metav1.ConditionFalse, @@ -333,92 +483,100 @@ func (t *Translator) buildRateLimit(policy *egv1a1.BackendTrafficPolicy) *ir.Rat ) return nil } + + global := policy.Spec.RateLimit.Global rateLimit := &ir.RateLimit{ Global: &ir.GlobalRateLimit{ - Rules: make([]*ir.RateLimitRule, len(policy.Spec.RateLimit.Global.Rules)), + Rules: make([]*ir.RateLimitRule, len(global.Rules)), }, } - rules := rateLimit.Global.Rules - for i, rule := range policy.Spec.RateLimit.Global.Rules { - rules[i] = &ir.RateLimitRule{ - Limit: &ir.RateLimitValue{ - Requests: rule.Limit.Requests, - Unit: ir.RateLimitUnit(rule.Limit.Unit), - }, - HeaderMatches: make([]*ir.StringMatch, 0), + irRules := rateLimit.Global.Rules + var err error + for i, rule := range global.Rules { + irRules[i], err = buildRateLimitRule(rule) + if err != nil { + status.SetBackendTrafficPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + status.Error2ConditionMsg(err), + ) + return nil + } + } + + return rateLimit +} + +func buildRateLimitRule(rule egv1a1.RateLimitRule) (*ir.RateLimitRule, error) { + irRule := &ir.RateLimitRule{ + Limit: ir.RateLimitValue{ + Requests: rule.Limit.Requests, + Unit: ir.RateLimitUnit(rule.Limit.Unit), + }, + HeaderMatches: make([]*ir.StringMatch, 0), + } + + for _, match := range rule.ClientSelectors { + if len(match.Headers) == 0 && match.SourceCIDR == nil { + return nil, errors.New( + "unable to translate rateLimit. At least one of the" + + " header or sourceCIDR must be specified") } - for _, match := range rule.ClientSelectors { - for _, header := range match.Headers { - switch { - case header.Type == nil && header.Value != nil: - fallthrough - case *header.Type == egv1a1.HeaderMatchExact && header.Value != nil: - m := &ir.StringMatch{ - Name: header.Name, - Exact: header.Value, - } - rules[i].HeaderMatches = append(rules[i].HeaderMatches, m) - case *header.Type == egv1a1.HeaderMatchRegularExpression && header.Value != nil: - m := &ir.StringMatch{ - Name: header.Name, - SafeRegex: header.Value, - } - rules[i].HeaderMatches = append(rules[i].HeaderMatches, m) - case *header.Type == egv1a1.HeaderMatchDistinct && header.Value == nil: - m := &ir.StringMatch{ - Name: header.Name, - Distinct: true, - } - rules[i].HeaderMatches = append(rules[i].HeaderMatches, m) - default: - // set negative status condition. - message := "Unable to translate rateLimit. Either the header.Type is not valid or the header is missing a value" - status.SetBackendTrafficPolicyCondition(policy, - gwv1a2.PolicyConditionAccepted, - metav1.ConditionFalse, - gwv1a2.PolicyReasonInvalid, - message, - ) - - return nil + for _, header := range match.Headers { + switch { + case header.Type == nil && header.Value != nil: + fallthrough + case *header.Type == egv1a1.HeaderMatchExact && header.Value != nil: + m := &ir.StringMatch{ + Name: header.Name, + Exact: header.Value, } + irRule.HeaderMatches = append(irRule.HeaderMatches, m) + case *header.Type == egv1a1.HeaderMatchRegularExpression && header.Value != nil: + m := &ir.StringMatch{ + Name: header.Name, + SafeRegex: header.Value, + } + irRule.HeaderMatches = append(irRule.HeaderMatches, m) + case *header.Type == egv1a1.HeaderMatchDistinct && header.Value == nil: + m := &ir.StringMatch{ + Name: header.Name, + Distinct: true, + } + irRule.HeaderMatches = append(irRule.HeaderMatches, m) + default: + return nil, errors.New( + "unable to translate rateLimit. Either the header." + + "Type is not valid or the header is missing a value") } + } - if match.SourceCIDR != nil { - // distinct means that each IP Address within the specified Source IP CIDR is treated as a - // distinct client selector and uses a separate rate limit bucket/counter. - distinct := false - sourceCIDR := match.SourceCIDR.Value - if match.SourceCIDR.Type != nil && *match.SourceCIDR.Type == egv1a1.SourceMatchDistinct { - distinct = true - } + if match.SourceCIDR != nil { + // distinct means that each IP Address within the specified Source IP CIDR is treated as a + // distinct client selector and uses a separate rate limit bucket/counter. + distinct := false + sourceCIDR := match.SourceCIDR.Value + if match.SourceCIDR.Type != nil && *match.SourceCIDR.Type == egv1a1.SourceMatchDistinct { + distinct = true + } - ip, ipn, err := net.ParseCIDR(sourceCIDR) - if err != nil { - message := "Unable to translate rateLimit" - status.SetBackendTrafficPolicyCondition(policy, - gwv1a2.PolicyConditionAccepted, - metav1.ConditionFalse, - gwv1a2.PolicyReasonInvalid, - message, - ) - - return nil - } + ip, ipn, err := net.ParseCIDR(sourceCIDR) + if err != nil { + return nil, errors.New("unable to translate rateLimit") + } - mask, _ := ipn.Mask.Size() - rules[i].CIDRMatch = &ir.CIDRMatch{ - CIDR: ipn.String(), - IPv6: ip.To4() == nil, - MaskLen: mask, - Distinct: distinct, - } + mask, _ := ipn.Mask.Size() + irRule.CIDRMatch = &ir.CIDRMatch{ + CIDR: ipn.String(), + IPv6: ip.To4() == nil, + MaskLen: mask, + Distinct: distinct, } } } - - return rateLimit + return irRule, nil } func (t *Translator) buildLoadBalancer(policy *egv1a1.BackendTrafficPolicy) *ir.LoadBalancer { @@ -482,3 +640,74 @@ func (t *Translator) buildProxyProtocol(policy *egv1a1.BackendTrafficPolicy) *ir return pp } + +func ratelimitUnitToDuration(unit egv1a1.RateLimitUnit) int64 { + var seconds int64 + + switch unit { + case egv1a1.RateLimitUnitSecond: + seconds = 1 + case egv1a1.RateLimitUnitMinute: + seconds = 60 + case egv1a1.RateLimitUnitHour: + seconds = 60 * 60 + case egv1a1.RateLimitUnitDay: + seconds = 60 * 60 * 24 + } + return seconds +} + +func (t *Translator) buildCircuitBreaker(policy *egv1a1.BackendTrafficPolicy) *ir.CircuitBreaker { + var cb *ir.CircuitBreaker + pcb := policy.Spec.CircuitBreaker + + if pcb != nil { + cb = &ir.CircuitBreaker{} + + if pcb.MaxConnections != nil { + if ui32, ok := int64ToUint32(*pcb.MaxConnections); ok { + cb.MaxConnections = &ui32 + } else { + setCircuitBreakerPolicyErrorCondition(policy, fmt.Sprintf("invalid MaxConnections value %d", *pcb.MaxConnections)) + return nil + } + } + + if pcb.MaxParallelRequests != nil { + if ui32, ok := int64ToUint32(*pcb.MaxParallelRequests); ok { + cb.MaxParallelRequests = &ui32 + } else { + setCircuitBreakerPolicyErrorCondition(policy, fmt.Sprintf("invalid MaxParallelRequests value %d", *pcb.MaxParallelRequests)) + return nil + } + } + + if pcb.MaxPendingRequests != nil { + if ui32, ok := int64ToUint32(*pcb.MaxPendingRequests); ok { + cb.MaxPendingRequests = &ui32 + } else { + setCircuitBreakerPolicyErrorCondition(policy, fmt.Sprintf("invalid MaxPendingRequests value %d", *pcb.MaxPendingRequests)) + return nil + } + } + } + + return cb +} + +func setCircuitBreakerPolicyErrorCondition(policy *egv1a1.BackendTrafficPolicy, errMsg string) { + message := fmt.Sprintf("Unable to translate Circuit Breaker: %s", errMsg) + status.SetBackendTrafficPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + message, + ) +} + +func int64ToUint32(in int64) (uint32, bool) { + if in >= 0 && in <= math.MaxUint32 { + return uint32(in), true + } + return 0, false +} diff --git a/internal/gatewayapi/backendtrafficpolicy_test.go b/internal/gatewayapi/backendtrafficpolicy_test.go new file mode 100644 index 000000000000..df943a2032b1 --- /dev/null +++ b/internal/gatewayapi/backendtrafficpolicy_test.go @@ -0,0 +1,52 @@ +// 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 gatewayapi + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInt64ToUint32(t *testing.T) { + type testCase struct { + Name string + In int64 + Out uint32 + Success bool + } + + testCases := []testCase{ + { + Name: "valid", + In: 1024, + Out: 1024, + Success: true, + }, + { + Name: "invalid-underflow", + In: -1, + Out: 0, + Success: false, + }, + { + Name: "invalid-overflow", + In: math.MaxUint32 + 1, + Out: 0, + Success: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + out, success := int64ToUint32(tc.In) + require.Equal(t, tc.Out, out) + require.Equal(t, tc.Success, success) + }) + } +} diff --git a/internal/gatewayapi/envoypatchpolicy.go b/internal/gatewayapi/envoypatchpolicy.go index 6e790ff25db2..3f65c7131815 100644 --- a/internal/gatewayapi/envoypatchpolicy.go +++ b/internal/gatewayapi/envoypatchpolicy.go @@ -28,6 +28,7 @@ func (t *Translator) ProcessEnvoyPatchPolicies(envoyPatchPolicies []*egv1a1.Envo for _, policy := range envoyPatchPolicies { policy := policy.DeepCopy() targetNs := policy.Spec.TargetRef.Namespace + targetKind := KindGateway // If empty, default to namespace of policy if targetNs == nil { @@ -37,12 +38,17 @@ func (t *Translator) ProcessEnvoyPatchPolicies(envoyPatchPolicies []*egv1a1.Envo // Get the IR // It must exist since the gateways have already been processed irKey := irStringKey(string(*targetNs), string(policy.Spec.TargetRef.Name)) + if t.MergeGateways { + irKey = string(t.GatewayClassName) + targetKind = KindGatewayClass + } + gwXdsIR, ok := xdsIR[irKey] if !ok { // This status condition will not get updated in the resource because // the IR is missing, but it has been kept here in case we publish // the status from this layer instead of the xds layer. - message := fmt.Sprintf("Gateway:%s not found.", policy.Spec.TargetRef.Name) + message := fmt.Sprintf("%s:%s not found.", targetKind, policy.Spec.TargetRef.Name) status.SetEnvoyPatchPolicyCondition(policy, gwv1a2.PolicyConditionAccepted, @@ -61,11 +67,9 @@ func (t *Translator) ProcessEnvoyPatchPolicies(envoyPatchPolicies []*egv1a1.Envo // Append the IR gwXdsIR.EnvoyPatchPolicies = append(gwXdsIR.EnvoyPatchPolicies, &policyIR) - - // Ensure policy can only target a Gateway - if policy.Spec.TargetRef.Group != gwv1b1.GroupName || policy.Spec.TargetRef.Kind != KindGateway { + if policy.Spec.TargetRef.Group != gwv1b1.GroupName || string(policy.Spec.TargetRef.Kind) != targetKind { message := fmt.Sprintf("TargetRef.Group:%s TargetRef.Kind:%s, only TargetRef.Group:%s and TargetRef.Kind:%s is supported.", - policy.Spec.TargetRef.Group, policy.Spec.TargetRef.Kind, gwv1b1.GroupName, KindGateway) + policy.Spec.TargetRef.Group, policy.Spec.TargetRef.Kind, gwv1b1.GroupName, targetKind) status.SetEnvoyPatchPolicyCondition(policy, gwv1a2.PolicyConditionAccepted, @@ -78,8 +82,8 @@ func (t *Translator) ProcessEnvoyPatchPolicies(envoyPatchPolicies []*egv1a1.Envo // Ensure Policy and target Gateway are in the same namespace if policy.Namespace != string(*targetNs) { - message := fmt.Sprintf("Namespace:%s TargetRef.Namespace:%s, EnvoyPatchPolicy can only target a Gateway in the same namespace.", - policy.Namespace, *targetNs) + message := fmt.Sprintf("Namespace:%s TargetRef.Namespace:%s, EnvoyPatchPolicy can only target a %s in the same namespace.", + policy.Namespace, *targetNs, targetKind) status.SetEnvoyPatchPolicyCondition(policy, gwv1a2.PolicyConditionAccepted, diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers-error.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers-error.in.yaml new file mode 100644 index 000000000000..d762170ad927 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers-error.in.yaml @@ -0,0 +1,44 @@ +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 +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 +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + circuitBreaker: + maxConnections: -1 diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers-error.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers-error.out.yaml new file mode 100755 index 000000000000..4e3acbd5931e --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers-error.out.yaml @@ -0,0 +1,141 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + circuitBreaker: + maxConnections: -1 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: 'Unable to translate Circuit Breaker: invalid MaxConnections value + -1' + reason: Invalid + status: "False" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + 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 +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + creationTimestamp: null + 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 +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: true + name: envoy-gateway/gateway-1/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: grpcroute/default/grpcroute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: GRPC + weight: 1 + hostname: '*' + name: grpcroute/default/grpcroute-1/rule/0/match/-1/* diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers.in.yaml new file mode 100644 index 000000000000..babae4b36509 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers.in.yaml @@ -0,0 +1,95 @@ +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 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + circuitBreaker: + maxConnections: 2048 + maxPendingRequests: 1 + maxParallelRequests: 4294967295 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: default + circuitBreaker: + maxConnections: 42 + maxPendingRequests: 42 + maxParallelRequests: 42 diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers.out.yaml new file mode 100755 index 000000000000..cea46c84bc30 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-circuitbreakers.out.yaml @@ -0,0 +1,297 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-route + namespace: default + spec: + circuitBreaker: + maxConnections: 42 + maxParallelRequests: 42 + maxPendingRequests: 42 + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: default + status: + conditions: + - lastTransitionTime: null + message: BackendTrafficPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + circuitBreaker: + maxConnections: 2048 + maxParallelRequests: 4294967295 + maxPendingRequests: 1 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: BackendTrafficPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + 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: + creationTimestamp: null + 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: + creationTimestamp: null + 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: + creationTimestamp: null + 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: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 + envoy-gateway/gateway-2: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-2/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-2 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-2 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: true + name: envoy-gateway/gateway-1/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + circuitBreaker: + maxConnections: 2048 + maxParallelRequests: 4294967295 + maxPendingRequests: 1 + destination: + name: grpcroute/default/grpcroute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: GRPC + weight: 1 + hostname: '*' + name: grpcroute/default/grpcroute-1/rule/0/match/-1/* + envoy-gateway/gateway-2: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-2/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + circuitBreaker: + maxConnections: 42 + maxParallelRequests: 42 + maxPendingRequests: 42 + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-default-route-level-limit.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-default-route-level-limit.in.yaml new file mode 100644 index 000000000000..c1136403c360 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-default-route-level-limit.in.yaml @@ -0,0 +1,71 @@ +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 +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-1 + 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 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + rateLimit: + type: Local + local: + rules: # No Rule without selector is specified, so int32 max is used as default. + - clientSelectors: + - headers: + - name: x-user-id + value: one + - name: x-org-id + value: foo + limit: + requests: 10 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: two + - name: x-org-id + value: bar + sourceCIDR: + value: 192.168.0.0/16 + limit: + requests: 10 + unit: Minute diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-default-route-level-limit.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-default-route-level-limit.out.yaml new file mode 100755 index 000000000000..97ee75b2266b --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-default-route-level-limit.out.yaml @@ -0,0 +1,202 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + rateLimit: + local: + rules: + - clientSelectors: + - headers: + - name: x-user-id + value: one + - name: x-org-id + value: foo + limit: + requests: 10 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: two + - name: x-org-id + value: bar + sourceCIDR: + value: 192.168.0.0/16 + limit: + requests: 10 + unit: Minute + type: Local + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: BackendTrafficPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + 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 +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + 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-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + rateLimit: + local: + default: + requests: 4294967295 + unit: Second + rules: + - headerMatches: + - distinct: false + exact: one + name: x-user-id + - distinct: false + exact: foo + name: x-org-id + limit: + requests: 10 + unit: Hour + - cidrMatch: + cidr: 192.168.0.0/16 + distinct: false + ipv6: false + maskLen: 16 + headerMatches: + - distinct: false + exact: two + name: x-user-id + - distinct: false + exact: bar + name: x-org-id + limit: + requests: 10 + unit: Minute diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-limit-unit.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-limit-unit.in.yaml new file mode 100644 index 000000000000..c1d34e704800 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-limit-unit.in.yaml @@ -0,0 +1,74 @@ +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 +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-1 + 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 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + rateLimit: + type: Local + local: + rules: + - limit: + requests: 1000 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: one + - name: x-org-id + value: foo + limit: + requests: 10 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: two + - name: x-org-id + value: bar + sourceCIDR: + value: 192.168.0.0/16 + limit: + requests: 10 + unit: Minute # Local rateLimit rule limit unit is not a multiple of the default limit unit. diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-limit-unit.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-limit-unit.out.yaml new file mode 100755 index 000000000000..6190b2759a9e --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-limit-unit.out.yaml @@ -0,0 +1,175 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + rateLimit: + local: + rules: + - limit: + requests: 1000 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: one + - name: x-org-id + value: foo + limit: + requests: 10 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: two + - name: x-org-id + value: bar + sourceCIDR: + value: 192.168.0.0/16 + limit: + requests: 10 + unit: Minute + type: Local + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: Local rateLimit rule limit unit must be a multiple of the default limit + unit. + reason: Invalid + status: "False" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + 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 +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + 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-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-match-type.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-match-type.in.yaml new file mode 100644 index 000000000000..e6a595b157a8 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-match-type.in.yaml @@ -0,0 +1,71 @@ +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 +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-1 + 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 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + rateLimit: + type: Local + local: + rules: + - clientSelectors: + - headers: + - name: x-user-id + value: one + - name: x-org-id + type: Distinct + limit: + requests: 10 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: two + - name: x-org-id + value: bar + sourceCIDR: + value: 192.168.0.0/16 + limit: + requests: 10 + unit: Minute diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-match-type.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-match-type.out.yaml new file mode 100755 index 000000000000..21d867ab73ee --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-match-type.out.yaml @@ -0,0 +1,171 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + rateLimit: + local: + rules: + - clientSelectors: + - headers: + - name: x-user-id + value: one + - name: x-org-id + type: Distinct + limit: + requests: 10 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: two + - name: x-org-id + value: bar + sourceCIDR: + value: 192.168.0.0/16 + limit: + requests: 10 + unit: Minute + type: Local + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: Local rateLimit does not support distinct HeaderMatch. + reason: Invalid + status: "False" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + 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 +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + 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-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-multiple-route-level-limits.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-multiple-route-level-limits.in.yaml new file mode 100644 index 000000000000..9a2f3b942468 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-multiple-route-level-limits.in.yaml @@ -0,0 +1,77 @@ +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 +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-1 + 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 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + rateLimit: + type: Local + local: + rules: + - limit: # There are two Rule without selector, so this is invalid. + requests: 10 + unit: Minute + - limit: + requests: 20 + unit: Minute + - clientSelectors: + - headers: + - name: x-user-id + value: one + - name: x-org-id + value: foo + limit: + requests: 10 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: two + - name: x-org-id + value: bar + sourceCIDR: + value: 192.168.0.0/16 + limit: + requests: 10 + unit: Minute diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-multiple-route-level-limits.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-multiple-route-level-limits.out.yaml new file mode 100755 index 000000000000..ac2dbc8e74b3 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-multiple-route-level-limits.out.yaml @@ -0,0 +1,177 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + rateLimit: + local: + rules: + - limit: + requests: 10 + unit: Minute + - limit: + requests: 20 + unit: Minute + - clientSelectors: + - headers: + - name: x-user-id + value: one + - name: x-org-id + value: foo + limit: + requests: 10 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: two + - name: x-org-id + value: bar + sourceCIDR: + value: 192.168.0.0/16 + limit: + requests: 10 + unit: Minute + type: Local + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: Local rateLimit can not have more than one rule without clientSelectors. + reason: Invalid + status: "False" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + 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 +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + 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-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit.in.yaml new file mode 100644 index 000000000000..7f9d36ab4840 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit.in.yaml @@ -0,0 +1,74 @@ +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 +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-1 + 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 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + rateLimit: + type: Local + local: + rules: + - limit: # The Rule without selector is used as default + requests: 10 + unit: Minute + - clientSelectors: + - headers: + - name: x-user-id + value: one + - name: x-org-id + value: foo + limit: + requests: 10 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: two + - name: x-org-id + value: bar + sourceCIDR: + value: 192.168.0.0/16 + limit: + requests: 10 + unit: Minute diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit.out.yaml new file mode 100755 index 000000000000..c7834e5eedbc --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit.out.yaml @@ -0,0 +1,205 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + rateLimit: + local: + rules: + - limit: + requests: 10 + unit: Minute + - clientSelectors: + - headers: + - name: x-user-id + value: one + - name: x-org-id + value: foo + limit: + requests: 10 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: two + - name: x-org-id + value: bar + sourceCIDR: + value: 192.168.0.0/16 + limit: + requests: 10 + unit: Minute + type: Local + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: BackendTrafficPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + 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 +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + 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-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + rateLimit: + local: + default: + requests: 10 + unit: Minute + rules: + - headerMatches: + - distinct: false + exact: one + name: x-user-id + - distinct: false + exact: foo + name: x-org-id + limit: + requests: 10 + unit: Hour + - cidrMatch: + cidr: 192.168.0.0/16 + distinct: false + ipv6: false + maskLen: 16 + headerMatches: + - distinct: false + exact: two + name: x-user-id + - distinct: false + exact: bar + name: x-org-id + limit: + requests: 10 + unit: Minute diff --git a/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-kind-merge-gateways.in.yaml b/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-kind-merge-gateways.in.yaml new file mode 100644 index 000000000000..573018430f23 --- /dev/null +++ b/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-kind-merge-gateways.in.yaml @@ -0,0 +1,43 @@ +envoyproxy: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + mergeGateways: true +envoyPatchPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyPatchPolicy + metadata: + namespace: envoy-gateway + name: edit-conn-buffer-bytes + spec: + type: "JSONPatch" + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + jsonPatches: + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + name: "envoy-gateway-gateway-1-http" + operation: + op: replace + path: "/per_connection_buffer_limit_bytes" + value: "1024" +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: Same diff --git a/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-kind-merge-gateways.out.yaml b/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-kind-merge-gateways.out.yaml new file mode 100755 index 000000000000..1cff7cb42661 --- /dev/null +++ b/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-kind-merge-gateways.out.yaml @@ -0,0 +1,91 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + 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 +infraIR: + envoy-gateway-class: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + creationTimestamp: null + name: test + namespace: envoy-gateway-system + spec: + logging: {} + mergeGateways: true + status: {} + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: envoy-gateway/gateway-1/http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gatewayclass: envoy-gateway-class + name: envoy-gateway-class +xdsIR: + envoy-gateway-class: + accessLog: + text: + - path: /dev/stdout + envoyPatchPolicies: + - name: edit-conn-buffer-bytes + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: TargetRef.Group:gateway.networking.k8s.io TargetRef.Kind:Gateway, + only TargetRef.Group:gateway.networking.k8s.io and TargetRef.Kind:GatewayClass + is supported. + reason: Invalid + status: "False" + type: Accepted + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 diff --git a/internal/gatewayapi/testdata/envoypatchpolicy-valid-merge-gateways.in.yaml b/internal/gatewayapi/testdata/envoypatchpolicy-valid-merge-gateways.in.yaml new file mode 100644 index 000000000000..d3604de13637 --- /dev/null +++ b/internal/gatewayapi/testdata/envoypatchpolicy-valid-merge-gateways.in.yaml @@ -0,0 +1,64 @@ +envoyproxy: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + mergeGateways: true +envoyPatchPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyPatchPolicy + metadata: + namespace: envoy-gateway + name: edit-conn-buffer-bytes + spec: + type: "JSONPatch" + targetRef: + group: gateway.networking.k8s.io + kind: GatewayClass + name: envoy-gateway-class + namespace: envoy-gateway + jsonPatches: + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + name: "envoy-gateway-gateway-1-http" + operation: + op: replace + path: "/per_connection_buffer_limit_bytes" + value: "1024" +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyPatchPolicy + metadata: + namespace: envoy-gateway + name: edit-ignore-global-limit + spec: + type: "JSONPatch" + targetRef: + group: gateway.networking.k8s.io + kind: GatewayClass + name: envoy-gateway-class + namespace: envoy-gateway + # Higher priority + priority: -1 + jsonPatches: + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + name: "envoy-gateway-gateway-1-http" + operation: + op: replace + path: "/ignore_global_conn_limit" + value: "true" +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: Same diff --git a/internal/gatewayapi/testdata/envoypatchpolicy-valid-merge-gateways.out.yaml b/internal/gatewayapi/testdata/envoypatchpolicy-valid-merge-gateways.out.yaml new file mode 100755 index 000000000000..6f63d1546221 --- /dev/null +++ b/internal/gatewayapi/testdata/envoypatchpolicy-valid-merge-gateways.out.yaml @@ -0,0 +1,112 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + 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 +infraIR: + envoy-gateway-class: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + creationTimestamp: null + name: test + namespace: envoy-gateway-system + spec: + logging: {} + mergeGateways: true + status: {} + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: envoy-gateway/gateway-1/http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gatewayclass: envoy-gateway-class + name: envoy-gateway-class +xdsIR: + envoy-gateway-class: + accessLog: + text: + - path: /dev/stdout + envoyPatchPolicies: + - jsonPatches: + - name: envoy-gateway-gateway-1-http + operation: + op: replace + path: /ignore_global_conn_limit + value: "true" + type: type.googleapis.com/envoy.config.listener.v3.Listener + name: edit-ignore-global-limit + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: EnvoyPatchPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - jsonPatches: + - name: envoy-gateway-gateway-1-http + operation: + op: replace + path: /per_connection_buffer_limit_bytes + value: "1024" + type: type.googleapis.com/envoy.config.listener.v3.Listener + name: edit-conn-buffer-bytes + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: EnvoyPatchPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 09e7e11a53ee..ea45bc117449 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -306,6 +306,8 @@ type HTTPRoute struct { BasicAuth *BasicAuth `json:"basicAuth,omitempty" yaml:"basicAuth,omitempty"` // ExtensionRefs holds unstructured resources that were introduced by an extension and used on the HTTPRoute as extensionRef filters ExtensionRefs []*UnstructuredRef `json:"extensionRefs,omitempty" yaml:"extensionRefs,omitempty"` + // Circuit Breaker Settings + CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty" yaml:"circuitBreaker,omitempty"` } // UnstructuredRef holds unstructured data for an arbitrary k8s resource introduced by an extension @@ -895,11 +897,27 @@ func (h UDPListener) Validate() error { type RateLimit struct { // Global rate limit settings. Global *GlobalRateLimit `json:"global,omitempty" yaml:"global,omitempty"` + + // Local rate limit settings. + Local *LocalRateLimit `json:"local,omitempty" yaml:"local,omitempty"` } // GlobalRateLimit holds the global rate limiting configuration. // +k8s:deepcopy-gen=true type GlobalRateLimit struct { + // TODO zhaohuabing: add default values for Global rate limiting. + + // Rules for rate limiting. + Rules []*RateLimitRule `json:"rules,omitempty" yaml:"rules,omitempty"` +} + +// LocalRateLimit holds the local rate limiting configuration. +// +k8s:deepcopy-gen=true +type LocalRateLimit struct { + // Default rate limiting values. + // If a request does not match any of the rules, the default values are used. + Default RateLimitValue `json:"default,omitempty" yaml:"default,omitempty"` + // Rules for rate limiting. Rules []*RateLimitRule `json:"rules,omitempty" yaml:"rules,omitempty"` } @@ -912,7 +930,7 @@ type RateLimitRule struct { // CIDRMatch define the match conditions on the source IP's CIDR for this route. CIDRMatch *CIDRMatch `json:"cidrMatch,omitempty" yaml:"cidrMatch,omitempty"` // Limit holds the rate limit values. - Limit *RateLimitValue `json:"limit,omitempty" yaml:"limit,omitempty"` + Limit RateLimitValue `json:"limit,omitempty" yaml:"limit,omitempty"` } type CIDRMatch struct { @@ -924,6 +942,7 @@ type CIDRMatch struct { Distinct bool `json:"distinct" yaml:"distinct"` } +// TODO zhaohuabing: remove this function func (r *RateLimitRule) IsMatchSet() bool { return len(r.HeaderMatches) != 0 || r.CIDRMatch != nil } @@ -1129,3 +1148,16 @@ type SlowStart struct { // Window defines the duration of the warm up period for newly added host. Window *metav1.Duration `json:"window" yaml:"window"` } + +// Backend CircuitBreaker settings for the DEFAULT routing priority +// +k8s:deepcopy-gen=true +type CircuitBreaker struct { + // The maximum number of connections that Envoy will establish. + MaxConnections *uint32 `json:"maxConnections,omitempty" yaml:"maxConnections,omitempty"` + + // The maximum number of pending requests that Envoy will queue. + MaxPendingRequests *uint32 `json:"maxPendingRequests,omitempty" yaml:"maxPendingRequests,omitempty"` + + // The maximum number of parallel requests that Envoy will make. + MaxParallelRequests *uint32 `json:"maxParallelRequests,omitempty" yaml:"maxParallelRequests,omitempty"` +} diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 3b56aa25d0d8..88cc71bf30bf 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -143,6 +143,36 @@ func (in *CORS) DeepCopy() *CORS { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CircuitBreaker) DeepCopyInto(out *CircuitBreaker) { + *out = *in + if in.MaxConnections != nil { + in, out := &in.MaxConnections, &out.MaxConnections + *out = new(uint32) + **out = **in + } + if in.MaxPendingRequests != nil { + in, out := &in.MaxPendingRequests, &out.MaxPendingRequests + *out = new(uint32) + **out = **in + } + if in.MaxParallelRequests != nil { + in, out := &in.MaxParallelRequests, &out.MaxParallelRequests + *out = new(uint32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CircuitBreaker. +func (in *CircuitBreaker) DeepCopy() *CircuitBreaker { + if in == nil { + return nil + } + out := new(CircuitBreaker) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConsistentHash) DeepCopyInto(out *ConsistentHash) { *out = *in @@ -517,6 +547,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) { } } } + if in.CircuitBreaker != nil { + in, out := &in.CircuitBreaker, &out.CircuitBreaker + *out = new(CircuitBreaker) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRoute. @@ -724,6 +759,33 @@ func (in *LoadBalancer) DeepCopy() *LoadBalancer { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalRateLimit) DeepCopyInto(out *LocalRateLimit) { + *out = *in + out.Default = in.Default + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]*RateLimitRule, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(RateLimitRule) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalRateLimit. +func (in *LocalRateLimit) DeepCopy() *LocalRateLimit { + if in == nil { + return nil + } + out := new(LocalRateLimit) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Metrics) DeepCopyInto(out *Metrics) { *out = *in @@ -908,6 +970,11 @@ func (in *RateLimit) DeepCopyInto(out *RateLimit) { *out = new(GlobalRateLimit) (*in).DeepCopyInto(*out) } + if in.Local != nil { + in, out := &in.Local, &out.Local + *out = new(LocalRateLimit) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimit. @@ -939,11 +1006,7 @@ func (in *RateLimitRule) DeepCopyInto(out *RateLimitRule) { *out = new(CIDRMatch) **out = **in } - if in.Limit != nil { - in, out := &in.Limit, &out.Limit - *out = new(RateLimitValue) - **out = **in - } + out.Limit = in.Limit } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitRule. diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go index d7aa3d21afff..91acdc04a662 100644 --- a/internal/xds/translator/cluster.go +++ b/internal/xds/translator/cluster.go @@ -31,12 +31,13 @@ const ( ) type xdsClusterArgs struct { - name string - settings []*ir.DestinationSetting - tSocket *corev3.TransportSocket - endpointType EndpointType - loadBalancer *ir.LoadBalancer - proxyProtocol *ir.ProxyProtocol + name string + settings []*ir.DestinationSetting + tSocket *corev3.TransportSocket + endpointType EndpointType + loadBalancer *ir.LoadBalancer + proxyProtocol *ir.ProxyProtocol + circuitBreaker *ir.CircuitBreaker } type EndpointType int @@ -130,9 +131,43 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster { cluster.LbPolicy = clusterv3.Cluster_MAGLEV } + if args.circuitBreaker != nil { + cluster.CircuitBreakers = buildXdsClusterCircuitBreaker(args.circuitBreaker) + } + return cluster } +func buildXdsClusterCircuitBreaker(circuitBreaker *ir.CircuitBreaker) *clusterv3.CircuitBreakers { + cbt := &clusterv3.CircuitBreakers_Thresholds{ + Priority: corev3.RoutingPriority_DEFAULT, + } + + if circuitBreaker.MaxConnections != nil { + cbt.MaxConnections = &wrapperspb.UInt32Value{ + Value: *circuitBreaker.MaxConnections, + } + } + + if circuitBreaker.MaxPendingRequests != nil { + cbt.MaxPendingRequests = &wrapperspb.UInt32Value{ + Value: *circuitBreaker.MaxPendingRequests, + } + } + + if circuitBreaker.MaxParallelRequests != nil { + cbt.MaxRequests = &wrapperspb.UInt32Value{ + Value: *circuitBreaker.MaxParallelRequests, + } + } + + ecb := &clusterv3.CircuitBreakers{ + Thresholds: []*clusterv3.CircuitBreakers_Thresholds{cbt}, + } + + return ecb +} + func buildXdsClusterLoadAssignment(clusterName string, destSettings []*ir.DestinationSetting) *endpointv3.ClusterLoadAssignment { localities := make([]*endpointv3.LocalityLbEndpoints, 0, len(destSettings)) for i, ds := range destSettings { diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go index f16bb5c09ac4..8bea1b0e99a5 100644 --- a/internal/xds/translator/httpfilters.go +++ b/internal/xds/translator/httpfilters.go @@ -99,6 +99,8 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter { order = 4 case filter.Name == wellknown.HTTPRateLimit: order = 5 + case filter.Name == localRateLimitFilter: + order = 6 case filter.Name == wellknown.Router: order = 100 } diff --git a/internal/xds/translator/local_ratelimit.go b/internal/xds/translator/local_ratelimit.go new file mode 100644 index 000000000000..f54071d35361 --- /dev/null +++ b/internal/xds/translator/local_ratelimit.go @@ -0,0 +1,307 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "errors" + "fmt" + + configv3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + rlv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3" + localrlv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/local_ratelimit/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" + "github.com/golang/protobuf/ptypes/duration" + "github.com/golang/protobuf/ptypes/wrappers" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/wrapperspb" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/xds/types" +) + +const ( + localRateLimitFilter = "envoy.filters.http.local_ratelimit" + localRateLimitFilterStatPrefix = "http_local_rate_limiter" + descriptorMaskedRemoteAddress = "masked_remote_address" +) + +func init() { + registerHTTPFilter(&localRateLimit{}) +} + +type localRateLimit struct { +} + +var _ httpFilter = &localRateLimit{} + +// patchHCM builds and appends the local rate limit filter to the HTTP Connection Manager +// if applicable, and it does not already exist. +func (*localRateLimit) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { + if mgr == nil { + return errors.New("hcm is nil") + } + if irListener == nil { + return errors.New("ir listener is nil") + } + if !listenerContainsLocalRateLimit(irListener) { + return nil + } + + // Return early if filter already exists. + for _, httpFilter := range mgr.HttpFilters { + if httpFilter.Name == localRateLimitFilter { + return nil + } + } + + localRl := &localrlv3.LocalRateLimit{ + StatPrefix: localRateLimitFilterStatPrefix, + } + + localRlAny, err := anypb.New(localRl) + if err != nil { + return err + } + + // The local rate limit filter at the HTTP connection manager level is an + // empty filter. The real configuration is done at the route level. + // See https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/local_rate_limit_filter + filter := &hcmv3.HttpFilter{ + Name: localRateLimitFilter, + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: localRlAny, + }, + } + + mgr.HttpFilters = append(mgr.HttpFilters, filter) + return nil +} + +func listenerContainsLocalRateLimit(irListener *ir.HTTPListener) bool { + if irListener == nil { + return false + } + + for _, route := range irListener.Routes { + if routeContainsLocalRateLimit(route) { + return true + } + } + + return false +} + +func routeContainsLocalRateLimit(irRoute *ir.HTTPRoute) bool { + if irRoute == nil || irRoute.RateLimit == nil || irRoute.RateLimit.Local == nil { + return false + } + + return true +} + +func (*localRateLimit) patchResources(*types.ResourceVersionTable, + []*ir.HTTPRoute) error { + return nil +} + +func (*localRateLimit) patchRouteConfig(*routev3.RouteConfiguration, *ir.HTTPListener) error { + return nil +} + +func (*localRateLimit) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error { + routeAction := route.GetRoute() + + // Return early if no rate limit config exists. + if irRoute.RateLimit == nil || irRoute.RateLimit.Local == nil || routeAction == nil { + return nil + } + + if routeAction.RateLimits != nil { + // This should not happen since this is the only place where the rate limit + // config is added in a route. + return fmt.Errorf( + "route already contains rate limit config: %s", + route.Name) + } + + local := irRoute.RateLimit.Local + + rateLimits, descriptors, err := buildRouteLocalRateLimits(local) + if err != nil { + return err + } + routeAction.RateLimits = rateLimits + + filterCfg := route.GetTypedPerFilterConfig() + if _, ok := filterCfg[localRateLimitFilter]; ok { + // This should not happen since this is the only place where the filter + // config is added in a route. + return fmt.Errorf( + "route already contains local rate limit filter config: %s", + route.Name) + } + + localRl := &localrlv3.LocalRateLimit{ + StatPrefix: localRateLimitFilterStatPrefix, + TokenBucket: &typev3.TokenBucket{ + MaxTokens: uint32(local.Default.Requests), + TokensPerFill: &wrapperspb.UInt32Value{ + Value: uint32(local.Default.Requests), + }, + FillInterval: ratelimitUnitToDuration(local.Default.Unit), + }, + FilterEnabled: &configv3.RuntimeFractionalPercent{ + DefaultValue: &typev3.FractionalPercent{ + Numerator: 100, + Denominator: typev3.FractionalPercent_HUNDRED, + }, + }, + FilterEnforced: &configv3.RuntimeFractionalPercent{ + DefaultValue: &typev3.FractionalPercent{ + Numerator: 100, + Denominator: typev3.FractionalPercent_HUNDRED, + }, + }, + Descriptors: descriptors, + // By setting AlwaysConsumeDefaultTokenBucket to false, the descriptors + // won't consume the default token bucket. This means that a request only + // counts towards the default token bucket if it does not match any of the + // descriptors. + AlwaysConsumeDefaultTokenBucket: &wrappers.BoolValue{ + Value: false, + }, + } + + localRlAny, err := anypb.New(localRl) + if err != nil { + return err + } + + if filterCfg == nil { + route.TypedPerFilterConfig = make(map[string]*anypb.Any) + } + + route.TypedPerFilterConfig[localRateLimitFilter] = localRlAny + return nil +} + +func buildRouteLocalRateLimits(local *ir.LocalRateLimit) ( + []*routev3.RateLimit, []*rlv3.LocalRateLimitDescriptor, error) { + var rateLimits []*routev3.RateLimit + var descriptors []*rlv3.LocalRateLimitDescriptor + + // Rules are ORed + for rIdx, rule := range local.Rules { + var rlActions []*routev3.RateLimit_Action + var descriptorEntries []*rlv3.RateLimitDescriptor_Entry + + // HeaderMatches + for mIdx, match := range rule.HeaderMatches { + if match.Distinct { + // This is a sanity check. This should never happen because Gateway + // API translator should have already validated this. + if rule.CIDRMatch.Distinct { + return nil, nil, errors.New("local rateLimit does not support distinct HeaderMatch") + } + } + + // Setup HeaderValueMatch actions + descriptorKey := getRouteRuleDescriptor(rIdx, mIdx) + descriptorVal := getRouteRuleDescriptor(rIdx, mIdx) + headerMatcher := &routev3.HeaderMatcher{ + Name: match.Name, + HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{ + StringMatch: buildXdsStringMatcher(match), + }, + } + action := &routev3.RateLimit_Action{ + ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{ + HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{ + DescriptorKey: descriptorKey, + DescriptorValue: descriptorVal, + ExpectMatch: &wrapperspb.BoolValue{ + Value: true, + }, + Headers: []*routev3.HeaderMatcher{headerMatcher}, + }, + }, + } + entry := &rlv3.RateLimitDescriptor_Entry{ + Key: descriptorKey, + Value: descriptorVal, + } + rlActions = append(rlActions, action) + descriptorEntries = append(descriptorEntries, entry) + } + + // Source IP CIDRMatch + if rule.CIDRMatch != nil { + // This is a sanity check. This should never happen because Gateway + // API translator should have already validated this. + if rule.CIDRMatch.Distinct { + return nil, nil, errors.New("local rateLimit does not support distinct CIDRMatch") + } + + // Setup MaskedRemoteAddress action + mra := &routev3.RateLimit_Action_MaskedRemoteAddress{} + maskLen := &wrapperspb.UInt32Value{Value: uint32(rule.CIDRMatch.MaskLen)} + if rule.CIDRMatch.IPv6 { + mra.V6PrefixMaskLen = maskLen + } else { + mra.V4PrefixMaskLen = maskLen + } + action := &routev3.RateLimit_Action{ + ActionSpecifier: &routev3.RateLimit_Action_MaskedRemoteAddress_{ + MaskedRemoteAddress: mra, + }, + } + entry := &rlv3.RateLimitDescriptor_Entry{ + Key: descriptorMaskedRemoteAddress, + Value: rule.CIDRMatch.CIDR, + } + descriptorEntries = append(descriptorEntries, entry) + rlActions = append(rlActions, action) + } + + rateLimit := &routev3.RateLimit{Actions: rlActions} + rateLimits = append(rateLimits, rateLimit) + + descriptor := &rlv3.LocalRateLimitDescriptor{ + Entries: descriptorEntries, + TokenBucket: &typev3.TokenBucket{ + MaxTokens: uint32(rule.Limit.Requests), + TokensPerFill: &wrapperspb.UInt32Value{ + Value: uint32(rule.Limit.Requests), + }, + FillInterval: ratelimitUnitToDuration(rule.Limit.Unit), + }, + } + descriptors = append(descriptors, descriptor) + } + + return rateLimits, descriptors, nil +} + +func ratelimitUnitToDuration(unit ir.RateLimitUnit) *duration.Duration { + var seconds int64 + + switch egv1a1.RateLimitUnit(unit) { + case egv1a1.RateLimitUnitSecond: + seconds = 1 + case egv1a1.RateLimitUnitMinute: + seconds = 60 + case egv1a1.RateLimitUnitHour: + seconds = 60 * 60 + case egv1a1.RateLimitUnitDay: + seconds = 60 * 60 * 24 + } + return &duration.Duration{ + Seconds: seconds, + } +} diff --git a/internal/xds/translator/testdata/in/xds-ir/circuit-breaker.yaml b/internal/xds/translator/testdata/in/xds-ir/circuit-breaker.yaml new file mode 100644 index 000000000000..bd61285403d3 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/circuit-breaker.yaml @@ -0,0 +1,19 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + routes: + - name: "first-route" + hostname: "*" + circuitBreaker: + maxConnections: 1 + maxPendingRequests: 1 + maxParallelRequests: 1 + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 diff --git a/internal/xds/translator/testdata/in/xds-ir/local-ratelimit.yaml b/internal/xds/translator/testdata/in/xds-ir/local-ratelimit.yaml new file mode 100644 index 000000000000..ee136965b95d --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/local-ratelimit.yaml @@ -0,0 +1,81 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + routes: + - name: "first-route-ratelimit-single-rule" + hostname: "*" + rateLimit: + local: + default: + requests: 10 + unit: Minute + rules: + - headerMatches: + - name: x-user-id + exact: one + - name: x-org-id + exact: foo + limit: + requests: 10 + unit: Hour + pathMatch: + exact: "foo/bar" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + - name: "second-route-ratelimit-multiple-rules" + hostname: "*" + rateLimit: + local: + default: + requests: 10 + unit: Minute + rules: + - headerMatches: + - name: x-user-id + exact: one + - name: x-org-id + exact: foo + limit: + requests: 10 + unit: Hour + - cidrMatch: + cidr: 192.168.0.0/16 + maskLen: 16 + headerMatches: + - name: x-user-id + exact: two + - name: x-org-id + exact: bar + limit: + requests: 10 + unit: Minute + pathMatch: + exact: "example" + destination: + name: "second-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + - name: "third-route-ratelimit-no-rule" + hostname: "*" + rateLimit: + local: + default: + requests: 10 + unit: Minute + pathMatch: + exact: "test" + destination: + name: "third-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 diff --git a/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.clusters.yaml new file mode 100644 index 000000000000..59a43e91a452 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.clusters.yaml @@ -0,0 +1,19 @@ +- circuitBreakers: + thresholds: + - maxConnections: 1 + maxPendingRequests: 1 + maxRequests: 1 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + lbPolicy: LEAST_REQUEST + name: first-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.endpoints.yaml new file mode 100644 index 000000000000..3b3f2d09076e --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.endpoints.yaml @@ -0,0 +1,12 @@ +- 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 diff --git a/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.listeners.yaml new file mode 100644 index 000000000000..73ee1b42ef6f --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.listeners.yaml @@ -0,0 +1,33 @@ +- address: + socketAddress: + address: 0.0.0.0 + 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 + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + upgradeConfigs: + - upgradeType: websocket + useRemoteAddress: true + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.routes.yaml new file mode 100644 index 000000000000..2734c7cc42a0 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/circuit-breaker.routes.yaml @@ -0,0 +1,12 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + prefix: / + name: first-route + route: + cluster: first-route-dest diff --git a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.clusters.yaml new file mode 100755 index 000000000000..869321c6504a --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.clusters.yaml @@ -0,0 +1,42 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + lbPolicy: LEAST_REQUEST + name: first-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: second-route-dest + lbPolicy: LEAST_REQUEST + name: second-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: third-route-dest + lbPolicy: LEAST_REQUEST + name: third-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.endpoints.yaml new file mode 100755 index 000000000000..475b89a087c3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.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/local-ratelimit.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.listeners.yaml new file mode 100755 index 000000000000..d9d4a248f2df --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.listeners.yaml @@ -0,0 +1,37 @@ +- address: + socketAddress: + address: 0.0.0.0 + 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.local_ratelimit + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit + statPrefix: http_local_rate_limiter + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + upgradeConfigs: + - upgradeType: websocket + useRemoteAddress: true + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.routes.yaml new file mode 100755 index 000000000000..3f28acd5a153 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.routes.yaml @@ -0,0 +1,153 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + path: foo/bar + name: first-route-ratelimit-single-rule + route: + cluster: first-route-dest + rateLimits: + - actions: + - headerValueMatch: + descriptorKey: rule-0-match-0 + descriptorValue: rule-0-match-0 + expectMatch: true + headers: + - name: x-user-id + stringMatch: + exact: one + - headerValueMatch: + descriptorKey: rule-0-match-1 + descriptorValue: rule-0-match-1 + expectMatch: true + headers: + - name: x-org-id + stringMatch: + exact: foo + typedPerFilterConfig: + envoy.filters.http.local_ratelimit: + '@type': type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit + alwaysConsumeDefaultTokenBucket: false + descriptors: + - entries: + - key: rule-0-match-0 + value: rule-0-match-0 + - key: rule-0-match-1 + value: rule-0-match-1 + tokenBucket: + fillInterval: 3600s + maxTokens: 10 + tokensPerFill: 10 + filterEnabled: + defaultValue: + numerator: 100 + filterEnforced: + defaultValue: + numerator: 100 + statPrefix: http_local_rate_limiter + tokenBucket: + fillInterval: 60s + maxTokens: 10 + tokensPerFill: 10 + - match: + path: example + name: second-route-ratelimit-multiple-rules + route: + cluster: second-route-dest + rateLimits: + - actions: + - headerValueMatch: + descriptorKey: rule-0-match-0 + descriptorValue: rule-0-match-0 + expectMatch: true + headers: + - name: x-user-id + stringMatch: + exact: one + - headerValueMatch: + descriptorKey: rule-0-match-1 + descriptorValue: rule-0-match-1 + expectMatch: true + headers: + - name: x-org-id + stringMatch: + exact: foo + - actions: + - headerValueMatch: + descriptorKey: rule-1-match-0 + descriptorValue: rule-1-match-0 + expectMatch: true + headers: + - name: x-user-id + stringMatch: + exact: two + - headerValueMatch: + descriptorKey: rule-1-match-1 + descriptorValue: rule-1-match-1 + expectMatch: true + headers: + - name: x-org-id + stringMatch: + exact: bar + - maskedRemoteAddress: + v4PrefixMaskLen: 16 + typedPerFilterConfig: + envoy.filters.http.local_ratelimit: + '@type': type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit + alwaysConsumeDefaultTokenBucket: false + descriptors: + - entries: + - key: rule-0-match-0 + value: rule-0-match-0 + - key: rule-0-match-1 + value: rule-0-match-1 + tokenBucket: + fillInterval: 3600s + maxTokens: 10 + tokensPerFill: 10 + - entries: + - key: rule-1-match-0 + value: rule-1-match-0 + - key: rule-1-match-1 + value: rule-1-match-1 + - key: masked_remote_address + value: 192.168.0.0/16 + tokenBucket: + fillInterval: 60s + maxTokens: 10 + tokensPerFill: 10 + filterEnabled: + defaultValue: + numerator: 100 + filterEnforced: + defaultValue: + numerator: 100 + statPrefix: http_local_rate_limiter + tokenBucket: + fillInterval: 60s + maxTokens: 10 + tokensPerFill: 10 + - match: + path: test + name: third-route-ratelimit-no-rule + route: + cluster: third-route-dest + typedPerFilterConfig: + envoy.filters.http.local_ratelimit: + '@type': type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit + alwaysConsumeDefaultTokenBucket: false + filterEnabled: + defaultValue: + numerator: 100 + filterEnforced: + defaultValue: + numerator: 100 + statPrefix: http_local_rate_limiter + tokenBucket: + fillInterval: 60s + maxTokens: 10 + tokensPerFill: 10 diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index c9f510f32361..f73f3a0b7f3b 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -498,12 +498,13 @@ func processXdsCluster(tCtx *types.ResourceVersionTable, httpRoute *ir.HTTPRoute } if err := addXdsCluster(tCtx, &xdsClusterArgs{ - name: httpRoute.Destination.Name, - settings: httpRoute.Destination.Settings, - tSocket: nil, - endpointType: endpointType, - loadBalancer: httpRoute.LoadBalancer, - proxyProtocol: httpRoute.ProxyProtocol, + name: httpRoute.Destination.Name, + settings: httpRoute.Destination.Settings, + tSocket: nil, + endpointType: endpointType, + loadBalancer: httpRoute.LoadBalancer, + proxyProtocol: httpRoute.ProxyProtocol, + circuitBreaker: httpRoute.CircuitBreaker, }); err != nil && !errors.Is(err, ErrXdsClusterExists) { return err } diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index 6b27b901e41c..80e6ca91432e 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -215,6 +215,12 @@ func TestTranslateXds(t *testing.T) { { name: "basic-auth", }, + { + name: "local-ratelimit", + }, + { + name: "circuit-breaker", + }, } for _, tc := range testCases { diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index dd7e990cdfd3..42ee35e29213 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -621,7 +621,7 @@ _Appears in:_ | --- | --- | | `type` _[EnvoyPatchType](#envoypatchtype)_ | Type decides the type of patch. Valid EnvoyPatchType values are "JSONPatch". | | `jsonPatches` _[EnvoyJSONPatchConfig](#envoyjsonpatchconfig) array_ | JSONPatch defines the JSONPatch configuration. | -| `targetRef` _[PolicyTargetReference](#policytargetreference)_ | TargetRef is the name of the Gateway API resource this policy is being attached to. Currently only attaching to Gateway is supported This Policy and the TargetRef MUST be in the same namespace for this Policy to have effect and be applied to the Gateway TargetRef | +| `targetRef` _[PolicyTargetReference](#policytargetreference)_ | TargetRef is the name of the Gateway API resource this policy is being attached to. By default attaching to Gateway is supported and when mergeGateways is enabled it should attach to GatewayClass. This Policy and the TargetRef MUST be in the same namespace for this Policy to have effect and be applied to the Gateway TargetRef | | `priority` _integer_ | Priority of the EnvoyPatchPolicy. If multiple EnvoyPatchPolicies are applied to the same TargetRef, they will be applied in the ascending order of the priority i.e. int32.min has the highest priority and int32.max has the lowest priority. Defaults to 0. | @@ -880,7 +880,7 @@ _Appears in:_ | Field | Description | | --- | --- | -| `rules` _[RateLimitRule](#ratelimitrule) array_ | Rules are a list of RateLimit selectors and limits. Each rule and its associated limit is applied in a mutually exclusive way i.e. if multiple rules get selected, each of their associated limits get applied, so a single traffic request might increase the rate limit counters for multiple rules if selected. | +| `rules` _[RateLimitRule](#ratelimitrule) array_ | Rules are a list of RateLimit selectors and limits. Each rule and its associated limit is applied in a mutually exclusive way. If a request matches multiple rules, each of their associated limits get applied, so a single request might increase the rate limit counters for multiple rules if selected. The rate limit service will return a logical OR of the individual rate limit decisions of all matching rules. For example, if a request matches two rules, one rate limited and one not, the final decision will be to rate limit the request. | #### GroupVersionKind @@ -1177,6 +1177,20 @@ _Appears in:_ +#### LocalRateLimit + + + +LocalRateLimit defines local rate limit configuration. + +_Appears in:_ +- [RateLimitSpec](#ratelimitspec) + +| Field | Description | +| --- | --- | +| `rules` _[RateLimitRule](#ratelimitrule) array_ | Rules are a list of RateLimit selectors and limits. If a request matches multiple rules, the strictest limit is applied. For example, if a request matches two rules, one with 10rps and one with 20rps, the final limit will be based on the rule with 10rps. | + + #### LogLevel _Underlying type:_ `string` @@ -1575,10 +1589,13 @@ RateLimitRule defines the semantics for matching attributes from the incoming re _Appears in:_ - [GlobalRateLimit](#globalratelimit) +- [LocalRateLimit](#localratelimit) | Field | Description | | --- | --- | -| `clientSelectors` _[RateLimitSelectCondition](#ratelimitselectcondition) array_ | ClientSelectors holds the list of select conditions to select specific clients using attributes from the traffic flow. All individual select conditions must hold True for this rule and its limit to be applied. If this field is empty, it is equivalent to True, and the limit is applied. | +| `clientSelectors` _[RateLimitSelectCondition](#ratelimitselectcondition) array_ | ClientSelectors holds the list of select conditions to select specific clients using attributes from the traffic flow. All individual select conditions must hold True for this rule and its limit to be applied. + If no client selectors are specified, the rule applies to all traffic of the targeted Route. + If the policy targets a Gateway, the rule applies to each Route of the Gateway. Please note that each Route has its own rate limit counters. For example, if a Gateway has two Routes, and the policy has a rule with limit 10rps, each Route will have its own 10rps limit. | | `limit` _[RateLimitValue](#ratelimitvalue)_ | Limit holds the rate limit values. This limit is applied for traffic flows when the selectors compute to True, causing the request to be counted towards the limit. The limit is enforced and the request is ratelimited, i.e. a response with 429 HTTP status code is sent back to the client when the selected requests have reached the limit. | @@ -1593,8 +1610,8 @@ _Appears in:_ | Field | Description | | --- | --- | -| `headers` _[HeaderMatch](#headermatch) array_ | Headers is a list of request headers to match. Multiple header values are ANDed together, meaning, a request MUST match all the specified headers. | -| `sourceCIDR` _[SourceMatch](#sourcematch)_ | SourceCIDR is the client IP Address range to match on. | +| `headers` _[HeaderMatch](#headermatch) array_ | Headers is a list of request headers to match. Multiple header values are ANDed together, meaning, a request MUST match all the specified headers. At least one of headers or sourceCIDR condition must be specified. | +| `sourceCIDR` _[SourceMatch](#sourcematch)_ | SourceCIDR is the client IP Address range to match on. At least one of headers or sourceCIDR condition must be specified. | #### RateLimitSpec @@ -1608,8 +1625,9 @@ _Appears in:_ | Field | Description | | --- | --- | -| `type` _[RateLimitType](#ratelimittype)_ | Type decides the scope for the RateLimits. Valid RateLimitType values are "Global". | +| `type` _[RateLimitType](#ratelimittype)_ | Type decides the scope for the RateLimits. Valid RateLimitType values are "Global" or "Local". | | `global` _[GlobalRateLimit](#globalratelimit)_ | Global defines global rate limit configuration. | +| `local` _[LocalRateLimit](#localratelimit)_ | Local defines local rate limit configuration. | #### RateLimitType diff --git a/site/content/en/latest/design/envoy-patch-policy.md b/site/content/en/latest/design/envoy-patch-policy.md index 47422b0baa32..94d9389fc616 100644 --- a/site/content/en/latest/design/envoy-patch-policy.md +++ b/site/content/en/latest/design/envoy-patch-policy.md @@ -146,7 +146,7 @@ output xDS is created. semantics. ## Design Decisions -* This API will only support a single `targetRef` and can bind to only a `Gateway` resource. This simplifies reasoning of how +* This API will only support a single `targetRef` and can bind to only a `Gateway` or `GatewayClass` resource. This simplifies reasoning of how patches will work. * This API will always be an experimental API and cannot be graduated into a stable API because Envoy Gateway cannot garuntee * that the naming scheme for the generated resources names will not change across releases @@ -162,7 +162,7 @@ patches will work. -[Direct Policy Attachment]: https://gateway-api.sigs.k8s.io/references/policy-attachment/#direct-policy-attachment +[Direct Policy Attachment]: https://gateway-api.sigs.k8s.io/references/policy-attachment/#direct-policy-attachment [RFC 6902]: https://datatracker.ietf.org/doc/html/rfc6902 [Gateway API]: https://gateway-api.sigs.k8s.io/ [Kubernetes]: https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/ diff --git a/site/content/en/latest/user/cors.md b/site/content/en/latest/user/cors.md index d8867ccb8d2b..92ad735d0602 100644 --- a/site/content/en/latest/user/cors.md +++ b/site/content/en/latest/user/cors.md @@ -31,8 +31,8 @@ spec: name: backend cors: allowOrigins: - - type: Exact - value: "www.foo.com" + - type: RegularExpression + value: .*\.foo\.com allowMethods: - GET - POST diff --git a/site/content/en/latest/user/envoy-patch-policy.md b/site/content/en/latest/user/envoy-patch-policy.md index 2980baccbb22..dc403d57bc04 100644 --- a/site/content/en/latest/user/envoy-patch-policy.md +++ b/site/content/en/latest/user/envoy-patch-policy.md @@ -10,7 +10,7 @@ is unstable and the outcome may change across versions. Use at your own risk. ## Introduction The [EnvoyPatchPolicy][] API allows user to modify the output [xDS][] -configuration generated by Envoy Gateway intended for EnvoyProxy, +configuration generated by Envoy Gateway intended for EnvoyProxy, using [JSON Patch][] semantics. ## Motivation @@ -30,7 +30,7 @@ Before proceeding, you should be able to query the example backend using HTTP. * By default [EnvoyPatchPolicy][] is disabled. Lets enable it in the [EnvoyGateway][] startup configuration * The default installation of Envoy Gateway installs a default [EnvoyGateway][] configuration and attaches it -using a `ConfigMap`. In the next step, we will update this resource to enable EnvoyPatchPolicy. +using a `ConfigMap`. In the next step, we will update this resource to enable EnvoyPatchPolicy. ```shell @@ -104,6 +104,45 @@ spec: EOF ``` +When mergeGateways is enabled, there will be one Envoy deployment for all Gateways in the cluster. +Then the EnvoyPatchPolicy should target a specific GatewayClass. + +```shell +cat <// + name: default/eg/http + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/local_reply_config" + value: + mappers: + - filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + runtime_key: key_b + status_code: 406 + body: + inline_string: "could not find what you are looking for" +EOF +``` + * Lets edit the HTTPRoute resource from the Quickstart to only match on paths with value `/get` ``` diff --git a/test/cel-validation/envoyproxy_test.go b/test/cel-validation/envoyproxy_test.go index 40347665ea85..9f029534b120 100644 --- a/test/cel-validation/envoyproxy_test.go +++ b/test/cel-validation/envoyproxy_test.go @@ -481,7 +481,24 @@ func TestEnvoyProxyProvider(t *testing.T) { }, } }, - wantErrors: []string{"maxReplicas cannot be less than or equal to minReplicas"}, + wantErrors: []string{"maxReplicas cannot be less than minReplicas"}, + }, + { + desc: "ProxyHpa-maxReplicas-equals-to-minReplicas", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + Provider: &egv1a1.EnvoyProxyProvider{ + Type: egv1a1.ProviderTypeKubernetes, + Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{ + EnvoyHpa: &egv1a1.KubernetesHorizontalPodAutoscalerSpec{ + MinReplicas: ptr.To[int32](2), + MaxReplicas: ptr.To[int32](2), + }, + }, + }, + } + }, + wantErrors: []string{}, }, { desc: "ProxyHpa-valid", diff --git a/test/e2e/testdata/circuitbreaker.yaml b/test/e2e/testdata/circuitbreaker.yaml new file mode 100644 index 000000000000..12557e7ef1a4 --- /dev/null +++ b/test/e2e/testdata/circuitbreaker.yaml @@ -0,0 +1,32 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: circuitbreaker-example + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-circuitbreaker + namespace: gateway-conformance-infra + circuitBreaker: + maxConnections: 0 + maxRequests: 0 + maxPendingRequests: 0 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-circuitbreaker + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /circuitbreaker + backendRefs: + - name: infra-backend-v1 + port: 8080 diff --git a/test/e2e/testdata/local-ratelimit.yaml b/test/e2e/testdata/local-ratelimit.yaml new file mode 100644 index 000000000000..e0e5513a03ac --- /dev/null +++ b/test/e2e/testdata/local-ratelimit.yaml @@ -0,0 +1,95 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: ratelimit-specific-user + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-ratelimit-specific-user + namespace: gateway-conformance-infra + rateLimit: + type: Local + local: + rules: + - limit: + requests: 10 + unit: Hour + - clientSelectors: + - headers: + - name: x-user-id + value: john + limit: + requests: 3 + unit: Hour +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: ratelimit-all-traffic + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-ratelimit-all-traffic + namespace: gateway-conformance-infra + rateLimit: + type: Local + local: + rules: + - limit: + requests: 3 + unit: Hour +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-ratelimit-specific-user + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - backendRefs: + - name: infra-backend-v1 + port: 8080 + matches: + - path: + type: Exact + value: /ratelimit-specific-user +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-ratelimit-all-traffic + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - backendRefs: + - name: infra-backend-v1 + port: 8080 + matches: + - path: + type: Exact + value: /ratelimit-all-traffic +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-no-ratelimit + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - backendRefs: + - name: infra-backend-v1 + port: 8080 + matches: + - path: + type: Exact + value: /no-ratelimit diff --git a/test/e2e/tests/basic-auth.go b/test/e2e/tests/basic-auth.go index a5a584d226be..cecac9302161 100644 --- a/test/e2e/tests/basic-auth.go +++ b/test/e2e/tests/basic-auth.go @@ -181,5 +181,5 @@ func SecurityPolicyMustBeAccepted( t.Logf("SecurityPolicy not yet accepted: %v", securityPolicy) return false, nil }) - require.NoErrorf(t, waitErr, "error waiting for HTTPRoute to have parents matching expectations") + require.NoErrorf(t, waitErr, "error waiting for SecurityPolicy to be accepted") } diff --git a/test/e2e/tests/circuitbreaker.go b/test/e2e/tests/circuitbreaker.go new file mode 100644 index 000000000000..0bce74ba68f1 --- /dev/null +++ b/test/e2e/tests/circuitbreaker.go @@ -0,0 +1,59 @@ +// 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. + +//go:build e2e +// +build e2e + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, CircuitBreakerTest) +} + +var CircuitBreakerTest = suite.ConformanceTest{ + ShortName: "CircuitBreaker", + Description: "Deny All Requests", + Manifests: []string{"testdata/circuitbreaker.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("Deny All Requests", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-with-circuitbreaker", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + // expect overflow since the policy applies a "closed" circuit breaker + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/circuitbreaker", + }, + Response: http.Response{ + StatusCode: 503, + Headers: map[string]string{ + "x-envoy-overloaded": "true", + }, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + }, +} diff --git a/test/e2e/tests/local-ratelimit.go b/test/e2e/tests/local-ratelimit.go new file mode 100644 index 000000000000..28e3caa7034c --- /dev/null +++ b/test/e2e/tests/local-ratelimit.go @@ -0,0 +1,226 @@ +// 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. + +//go:build e2e +// +build e2e + +package tests + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + gwv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" +) + +func init() { + ConformanceTests = append(ConformanceTests, LocalRateLimitSpecificUserTest) + ConformanceTests = append(ConformanceTests, LocalRateLimitAllTrafficTest) + ConformanceTests = append(ConformanceTests, LocalRateLimitNoLimitRouteTest) +} + +var LocalRateLimitSpecificUserTest = suite.ConformanceTest{ + ShortName: "LocalRateLimitSpecificUser", + Description: "Limit a specific user", + Manifests: []string{"testdata/local-ratelimit.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("limit a specific user", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-ratelimit-specific-user", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + backendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-specific-user", Namespace: ns}) + + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/ratelimit-specific-user", + Headers: map[string]string{ + "x-user-id": "john", + }, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + + expectLimitResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/ratelimit-specific-user", + Headers: map[string]string{ + "x-user-id": "john", + }, + }, + Response: http.Response{ + StatusCode: 429, + }, + Namespace: ns, + } + expectLimitReq := http.MakeRequest(t, &expectLimitResp, gwAddr, "HTTP", "http") + + // should just send exactly 4 requests, and expect 429 + + // keep sending requests till get 200 first, that will cost one 200 + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) + + // fire the rest request + if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("fail to get expected response at first three request: %v", err) + } + + // this request should be limited because the user is john and the limit is 3 + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil { + t.Errorf("fail to get expected response at last fourth request: %v", err) + } + + // test another user + expectOkResp = http.ExpectedResponse{ + Request: http.Request{ + Path: "/ratelimit-specific-user", + Headers: map[string]string{ + "x-user-id": "mike", + }, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + expectOkReq = http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + // the requests should not be limited because the user is mike + if err := GotExactExpectedResponse(t, 4, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("fail to get expected response at first three request: %v", err) + } + }) + }, +} + +var LocalRateLimitAllTrafficTest = suite.ConformanceTest{ + ShortName: "LocalRateLimitAllTraffic", + Description: "Limit all traffic on a route", + Manifests: []string{"testdata/local-ratelimit.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("limit all traffic on a route", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-ratelimit-all-traffic", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + backendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-all-traffic", Namespace: ns}) + + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/ratelimit-all-traffic", + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + + expectLimitResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/ratelimit-all-traffic", + }, + Response: http.Response{ + StatusCode: 429, + }, + Namespace: ns, + } + expectLimitReq := http.MakeRequest(t, &expectLimitResp, gwAddr, "HTTP", "http") + + // should just send exactly 4 requests, and expect 429 + + // keep sending requests till get 200 first, that will cost one 200 + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) + + // fire the rest request + if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("fail to get expected response at first three request: %v", err) + } + + // this request should be limited because the limit is 3 + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil { + t.Errorf("fail to get expected response at last fourth request: %v", err) + } + }) + }, +} + +var LocalRateLimitNoLimitRouteTest = suite.ConformanceTest{ + ShortName: "LocalRateLimitNoLimitRoute", + Description: "No rate limit on this route", + Manifests: []string{"testdata/local-ratelimit.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("no rate limit on this route", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-no-ratelimit", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/no-ratelimit", + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + + // should just send exactly 4 requests, and expect 429 + + // keep sending requests till get 200 first, that will cost one 200 + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) + + // the requests should not be limited because there is no rate limit on this route + if err := GotExactExpectedResponse(t, 3, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("fail to get expected response at last fourth request: %v", err) + } + }) + }, +} + +// backendTrafficPolicyMustBeAccepted waits for the specified BackendTrafficPolicy to be accepted. +func backendTrafficPolicyMustBeAccepted( + t *testing.T, + client client.Client, + policyName types.NamespacedName) { + t.Helper() + + waitErr := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 60*time.Second, true, func(ctx context.Context) (bool, error) { + policy := &egv1a1.BackendTrafficPolicy{} + err := client.Get(ctx, policyName, policy) + if err != nil { + return false, fmt.Errorf("error fetching BackendTrafficPolicy: %w", err) + } + + for _, condition := range policy.Status.Conditions { + if condition.Type == string(gwv1a2.PolicyConditionAccepted) && condition.Status == metav1.ConditionTrue { + return true, nil + } + } + t.Logf("BackendTrafficPolicy not yet accepted: %v", policy) + return false, nil + }) + require.NoErrorf(t, waitErr, "error waiting for BackendTrafficPolicy to be accepted") +} diff --git a/test/e2e/tests/ratelimit.go b/test/e2e/tests/ratelimit.go index 62937315512b..eb48f955f3c5 100644 --- a/test/e2e/tests/ratelimit.go +++ b/test/e2e/tests/ratelimit.go @@ -64,10 +64,10 @@ var RateLimitTest = suite.ConformanceTest{ http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) // fire the rest request - if err := GotExactNExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { t.Errorf("fail to get expected response at first three request: %v", err) } - if err := GotExactNExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil { + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil { t.Errorf("fail to get expected response at last fourth request: %v", err) } }) @@ -165,15 +165,15 @@ var RateLimitBasedJwtClaimsTest = suite.ConformanceTest{ http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, JwtOkResp) // fire the rest request - if err := GotExactNExpectedResponse(t, 2, suite.RoundTripper, JwtReq, JwtOkResp); err != nil { + if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, JwtReq, JwtOkResp); err != nil { t.Errorf("failed to get expected response at third request: %v", err) } - if err := GotExactNExpectedResponse(t, 1, suite.RoundTripper, JwtReq, expectLimitResp); err != nil { + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, JwtReq, expectLimitResp); err != nil { t.Errorf("failed to get expected response at the fourth request: %v", err) } // Carrying different jwt claims will not be limited - if err := GotExactNExpectedResponse(t, 4, suite.RoundTripper, difJwtReq, expectOkResp); err != nil { + if err := GotExactExpectedResponse(t, 4, suite.RoundTripper, difJwtReq, expectOkResp); err != nil { t.Errorf("failed to get expected response for the request with a different jwt: %v", err) } @@ -188,7 +188,7 @@ var RateLimitBasedJwtClaimsTest = suite.ConformanceTest{ Namespace: ns, } noTokenReq := http.MakeRequest(t, &noTokenResp, gwAddr, "HTTP", "http") - if err := GotExactNExpectedResponse(t, 1, suite.RoundTripper, noTokenReq, noTokenResp); err != nil { + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, noTokenReq, noTokenResp); err != nil { t.Errorf("failed to get expected response: %v", err) } @@ -196,7 +196,7 @@ var RateLimitBasedJwtClaimsTest = suite.ConformanceTest{ }, } -func GotExactNExpectedResponse(t *testing.T, n int, r roundtripper.RoundTripper, req roundtripper.Request, resp http.ExpectedResponse) error { +func GotExactExpectedResponse(t *testing.T, n int, r roundtripper.RoundTripper, req roundtripper.Request, resp http.ExpectedResponse) error { for i := 0; i < n; i++ { cReq, cRes, err := r.CaptureRoundTrip(req) if err != nil {