diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index 45272825845f..e89d062bf56e 100644 --- a/api/v1alpha1/backendtrafficpolicy_types.go +++ b/api/v1alpha1/backendtrafficpolicy_types.go @@ -45,6 +45,10 @@ type BackendTrafficPolicySpec struct { // This Policy and the TargetRef MUST be in the same namespace // for this Policy to have effect and be applied to the Gateway. TargetRef gwapiv1a2.PolicyTargetReferenceWithSectionName `json:"targetRef"` + + // RateLimit allows the user to limit the number of incoming requests + // to a predefined value based on attributes within the traffic flow. + RateLimit *RateLimitFilterSpec `json:"rateLimit,omitempty"` } // BackendTrafficPolicyStatus defines the state of BackendTrafficPolicy diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c9b8600c45f7..4bff569fa8ba 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -160,6 +160,11 @@ func (in *BackendTrafficPolicyList) DeepCopyObject() runtime.Object { func (in *BackendTrafficPolicySpec) DeepCopyInto(out *BackendTrafficPolicySpec) { *out = *in in.TargetRef.DeepCopyInto(&out.TargetRef) + if in.RateLimit != nil { + in, out := &in.RateLimit, &out.RateLimit + *out = new(RateLimitFilterSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendTrafficPolicySpec. 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 fa4d0b93bd90..98fa2a635480 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -44,6 +44,146 @@ spec: spec: description: spec defines the desired state of BackendTrafficPolicy. properties: + rateLimit: + description: RateLimit allows the user to limit the number of incoming + requests to a predefined value based on attributes within the traffic + flow. + properties: + global: + description: Global defines global rate limit configuration. + properties: + 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. + 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. + If this field is empty, it is equivalent to True, + and the limit is applied. + 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. + 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. + 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 + required: + - rules + type: object + type: + description: Type decides the scope for the RateLimits. Valid + RateLimitType values are "Global". + enum: + - Global + type: string + required: + - type + type: object targetRef: description: targetRef is the name of the resource this policy is being attached to. This Policy and the TargetRef MUST be in the diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index 3d42c103e7c5..cca1b80d1b67 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -7,7 +7,9 @@ package gatewayapi import ( "fmt" + "net" "sort" + "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -15,6 +17,7 @@ import ( gwv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" "github.com/envoyproxy/gateway/internal/status" "github.com/envoyproxy/gateway/internal/utils/ptr" ) @@ -35,7 +38,7 @@ type policyGatewayTargetContext struct { attached bool } -func ProcessBackendTrafficPolicies(backendTrafficPolicies []*egv1a1.BackendTrafficPolicy, +func (t *Translator) ProcessBackendTrafficPolicies(backendTrafficPolicies []*egv1a1.BackendTrafficPolicy, gateways []*GatewayContext, routes []RouteContext, xdsIR XdsIRMap) []*egv1a1.BackendTrafficPolicy { @@ -82,39 +85,29 @@ func ProcessBackendTrafficPolicies(backendTrafficPolicies []*egv1a1.BackendTraff continue } - translateBackendTrafficPolicy(policy, xdsIR) + t.translateBackendTrafficPolicyForRoute(policy, route, xdsIR) - // Set Accepted=True - status.SetBackendTrafficPolicyCondition(policy, - gwv1a2.PolicyConditionAccepted, - metav1.ConditionTrue, - gwv1a2.PolicyReasonAccepted, - "BackendTrafficPolicy has been accepted.", - ) + message := "BackendTrafficPolicy has been accepted." + status.SetBackendTrafficPolicyAcceptedIfUnset(&policy.Status, message) } } - // Process the policies targeting Gateways with a section name + // Process the policies targeting Gateways for _, policy := range backendTrafficPolicies { if policy.Spec.TargetRef.Kind == KindGateway { policy := policy.DeepCopy() res = append(res, policy) // Negative statuses have already been assigned so its safe to skip - gatewayKey := resolveBTPolicyGatewayTargetRef(policy, gatewayMap) - if gatewayKey == nil { + gateway := resolveBTPolicyGatewayTargetRef(policy, gatewayMap) + if gateway == nil { continue } - translateBackendTrafficPolicy(policy, xdsIR) + t.translateBackendTrafficPolicyForGateway(policy, gateway, xdsIR) - // Set Accepted=True - status.SetBackendTrafficPolicyCondition(policy, - gwv1a2.PolicyConditionAccepted, - metav1.ConditionTrue, - gwv1a2.PolicyReasonAccepted, - "BackendTrafficPolicy has been accepted.", - ) + message := "BackendTrafficPolicy has been accepted." + status.SetBackendTrafficPolicyAcceptedIfUnset(&policy.Status, message) } } @@ -243,6 +236,159 @@ func resolveBTPolicyRouteTargetRef(policy *egv1a1.BackendTrafficPolicy, routes m return route.RouteContext } -func translateBackendTrafficPolicy(policy *egv1a1.BackendTrafficPolicy, xdsIR XdsIRMap) { - // TODO + +func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.BackendTrafficPolicy, route RouteContext, xdsIR XdsIRMap) { + // Build IR + var rl *ir.RateLimit + if policy.Spec.RateLimit != nil { + rl = t.buildRateLimit(policy) + } + + // Apply IR to all relevant routes + prefix := irRoutePrefix(route) + for _, ir := range xdsIR { + for _, http := range ir.HTTP { + for _, r := range http.Routes { + // Apply if there is a match + if strings.HasPrefix(r.Name, prefix) { + r.RateLimit = rl + } + } + } + + } +} + +func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.BackendTrafficPolicy, gateway *GatewayContext, xdsIR XdsIRMap) { + // Build IR + var rl *ir.RateLimit + if policy.Spec.RateLimit != nil { + rl = t.buildRateLimit(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 + irKey := t.getIRKey(gateway.Gateway) + // Should exist since we've validated this + ir := xdsIR[irKey] + + for _, http := range ir.HTTP { + for _, r := range http.Routes { + // Apply if not already set + if r.RateLimit == nil { + r.RateLimit = rl + } + } + } + +} + +func (t *Translator) buildRateLimit(policy *egv1a1.BackendTrafficPolicy) *ir.RateLimit { + if policy.Spec.RateLimit.Global == nil { + message := "Global configuration empty for rateLimit" + status.SetBackendTrafficPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + message, + ) + return nil + } + if !t.GlobalRateLimitEnabled { + message := "Enable Ratelimit in the EnvoyGateway config to configure global rateLimit" + status.SetBackendTrafficPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + message, + ) + return nil + } + rateLimit := &ir.RateLimit{ + Global: &ir.GlobalRateLimit{ + Rules: make([]*ir.RateLimitRule, len(policy.Spec.RateLimit.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), + } + 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 + } + } + + 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 + } + + mask, _ := ipn.Mask.Size() + rules[i].CIDRMatch = &ir.CIDRMatch{ + CIDR: ipn.String(), + IPv6: ip.To4() == nil, + MaskLen: mask, + Distinct: distinct, + } + } + } + } + + return rateLimit } diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 7a79002b29b6..f9a6732dbae7 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -422,12 +422,16 @@ func irUDPListenerName(listener *ListenerContext, udpRoute *UDPRouteContext) str return fmt.Sprintf("%s/%s/%s/%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name, udpRoute.Name) } +func irRoutePrefix(route RouteContext) string { + return fmt.Sprintf("%s/%s/%s", strings.ToLower(string(GetRouteType(route))), route.GetNamespace(), route.GetName()) +} + func irRouteName(route RouteContext, ruleIdx, matchIdx int) string { - return fmt.Sprintf("%s/%s/%s/rule/%d/match/%d", strings.ToLower(string(GetRouteType(route))), route.GetNamespace(), route.GetName(), ruleIdx, matchIdx) + return fmt.Sprintf("%s/rule/%d/match/%d", irRoutePrefix(route), ruleIdx, matchIdx) } func irRouteDestinationName(route RouteContext, ruleIdx int) string { - return fmt.Sprintf("%s/%s/%s/rule/%d", strings.ToLower(string(GetRouteType(route))), route.GetNamespace(), route.GetName(), ruleIdx) + return fmt.Sprintf("%s/rule/%d", irRoutePrefix(route), ruleIdx) } func irTLSConfigs(tlsSecrets []*v1.Secret) []*ir.TLSListenerConfig { diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index ae8279a8d4c7..1d3c1199612c 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -181,7 +181,7 @@ func (t *Translator) Translate(resources *Resources) *TranslateResult { for _, u := range udpRoutes { routes = append(routes, u) } - backendTrafficPolicies := ProcessBackendTrafficPolicies(resources.BackendTrafficPolicies, gateways, routes, xdsIR) + backendTrafficPolicies := t.ProcessBackendTrafficPolicies(resources.BackendTrafficPolicies, gateways, routes, xdsIR) // Sort xdsIR based on the Gateway API spec sortXdsIRMap(xdsIR) diff --git a/internal/status/backendtrafficpolicy.go b/internal/status/backendtrafficpolicy.go index 22b8ad2f5f53..0cfc93473557 100644 --- a/internal/status/backendtrafficpolicy.go +++ b/internal/status/backendtrafficpolicy.go @@ -18,3 +18,15 @@ func SetBackendTrafficPolicyCondition(c *egv1a1.BackendTrafficPolicy, conditionT cond := newCondition(string(conditionType), status, string(reason), message, time.Now(), c.Generation) c.Status.Conditions = MergeConditions(c.Status.Conditions, cond) } + +func SetBackendTrafficPolicyAcceptedIfUnset(s *egv1a1.BackendTrafficPolicyStatus, message string) { + // Return early if Accepted condition is already set + for _, c := range s.Conditions { + if c.Type == string(gwv1a2.PolicyConditionAccepted) { + return + } + } + + cond := newCondition(string(gwv1a2.PolicyConditionAccepted), metav1.ConditionTrue, string(gwv1a2.PolicyReasonAccepted), message, time.Now(), 0) + s.Conditions = MergeConditions(s.Conditions, cond) +} diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 1f3545304b6e..dfdde5ca3f83 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -114,6 +114,7 @@ _Appears in:_ | Field | Description | | --- | --- | | `targetRef` _[PolicyTargetReferenceWithSectionName](#policytargetreferencewithsectionname)_ | targetRef is the name of the resource this policy is being attached to. This Policy and the TargetRef MUST be in the same namespace for this Policy to have effect and be applied to the Gateway. | +| `rateLimit` _[RateLimitFilterSpec](#ratelimitfilterspec)_ | RateLimit allows the user to limit the number of incoming requests to a predefined value based on attributes within the traffic flow. | @@ -742,22 +743,6 @@ HeaderMatch defines the match attributes within the HTTP Headers of the request. _Appears in:_ - [RateLimitSelectCondition](#ratelimitselectcondition) -| Field | Description | -| --- | --- | -| `type` _[HeaderMatchType](#headermatchtype)_ | Type specifies how to match against the value of the header. | -| `name` _string_ | Name of the HTTP header. | -| `value` _string_ | 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. | - - -#### HeaderMatchType - -_Underlying type:_ `string` - -HeaderMatchType specifies the semantics of how HTTP header values should be compared. Valid HeaderMatchType values are "Exact", "RegularExpression", and "Distinct". - -_Appears in:_ -- [HeaderMatch](#headermatch) - #### InfrastructureProviderType @@ -1302,6 +1287,7 @@ RateLimitFilter allows the user to limit the number of incoming requests to a pr RateLimitFilterSpec defines the desired state of RateLimitFilter. _Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) - [RateLimitFilter](#ratelimitfilter) | Field | Description | @@ -1466,21 +1452,6 @@ _Appears in:_ _Appears in:_ - [RateLimitSelectCondition](#ratelimitselectcondition) -| Field | Description | -| --- | --- | -| `type` _[SourceMatchType](#sourcematchtype)_ | | -| `value` _string_ | 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`. | - - -#### SourceMatchType - -_Underlying type:_ `string` - - - -_Appears in:_ -- [SourceMatch](#sourcematch) - #### TCPKeepalive