diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 4f578a51b1..9a1ea23ea0 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -24,7 +24,8 @@ var ( ErrHTTPRouteMatchEmpty = errors.New("either PathMatch, HeaderMatches or QueryParamMatches fields must be specified") ErrRouteDestinationHostInvalid = errors.New("field Address must be a valid IP address") ErrRouteDestinationPortInvalid = errors.New("field Port specified is invalid") - ErrStringMatchConditionInvalid = errors.New("only one of the Exact, Prefix or SafeRegex fields must be specified") + ErrStringMatchConditionInvalid = errors.New("only one of the Exact, Prefix, SafeRegex or Distinct fields must be set") + ErrStringMatchNameIsEmpty = errors.New("field Name must be specified") ErrDirectResponseStatusInvalid = errors.New("only HTTP status codes 100 - 599 are supported for DirectResponse") ErrRedirectUnsupportedStatus = errors.New("only HTTP status codes 301 and 302 are supported for redirect filters") ErrRedirectUnsupportedScheme = errors.New("only http and https are supported for the scheme in redirect filters") @@ -211,6 +212,9 @@ type HTTPRoute struct { Destinations []*RouteDestination // Rewrite to be changed for this route. URLRewrite *URLRewrite + // RateLimit defines the more specific match conditions as well as limits for ratelimiting + // the requests on this route. + RateLimit *RateLimit } // Validate the fields within the HTTPRoute structure @@ -460,7 +464,7 @@ func (r HTTPPathModifier) Validate() error { } // StringMatch holds the various match conditions. -// Only one of Exact, Prefix or SafeRegex can be set. +// Only one of Exact, Prefix, SafeRegex or Distinct can be set. // +k8s:deepcopy-gen=true type StringMatch struct { // Name of the field to match on. @@ -473,6 +477,9 @@ type StringMatch struct { Suffix *string // SafeRegex match condition. SafeRegex *string + // Distinct match condition. + // Used to match any and all possible unique values encountered within the Name field. + Distinct bool } // Validate the fields within the StringMatch structure @@ -491,6 +498,12 @@ func (s StringMatch) Validate() error { if s.SafeRegex != nil { matchCount++ } + if s.Distinct { + if s.Name == "" { + errs = multierror.Append(errs, ErrStringMatchNameIsEmpty) + } + matchCount++ + } if matchCount != 1 { errs = multierror.Append(errs, ErrStringMatchConditionInvalid) @@ -591,3 +604,44 @@ func (h UDPListener) Validate() error { } return errs } + +// RateLimit holds the rate limiting configuration. +// +k8s:deepcopy-gen=true +type RateLimit struct { + // Global rate limit settings. + Global *GlobalRateLimit +} + +// GlobalRateLimit holds the global rate limiting configuration. +// +k8s:deepcopy-gen=true +type GlobalRateLimit struct { + // Rules for rate limiting. + Rules []*RateLimitRule +} + +// RateLimitRule holds the match and limit configuration for ratelimiting. +// +k8s:deepcopy-gen=true +type RateLimitRule struct { + // HeaderMatches define the match conditions on the request headers for this route. + HeaderMatches []*StringMatch + // Limit holds the rate limit values. + Limit *RateLimitValue +} + +type RateLimitUnit string + +const ( + Second RateLimitUnit = "second" + Minute RateLimitUnit = "minute" + Hour RateLimitUnit = "hour" + Day RateLimitUnit = "day" +) + +// RateLimitValue holds the +// +k8s:deepcopy-gen=true +type RateLimitValue struct { + // Requests are the number of requests that need to be rate limited. + Requests uint32 + // Unit of rate limiting. + Unit RateLimitUnit +} diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index e948d8ff84..50796775cb 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -49,6 +49,32 @@ func (in *DirectResponse) DeepCopy() *DirectResponse { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GlobalRateLimit) DeepCopyInto(out *GlobalRateLimit) { + *out = *in + 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 GlobalRateLimit. +func (in *GlobalRateLimit) DeepCopy() *GlobalRateLimit { + if in == nil { + return nil + } + out := new(GlobalRateLimit) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPListener) DeepCopyInto(out *HTTPListener) { *out = *in @@ -187,6 +213,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) { *out = new(URLRewrite) (*in).DeepCopyInto(*out) } + if in.RateLimit != nil { + in, out := &in.RateLimit, &out.RateLimit + *out = new(RateLimit) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRoute. @@ -308,6 +339,72 @@ func (in *ProxyListener) DeepCopy() *ProxyListener { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RateLimit) DeepCopyInto(out *RateLimit) { + *out = *in + if in.Global != nil { + in, out := &in.Global, &out.Global + *out = new(GlobalRateLimit) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimit. +func (in *RateLimit) DeepCopy() *RateLimit { + if in == nil { + return nil + } + out := new(RateLimit) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RateLimitRule) DeepCopyInto(out *RateLimitRule) { + *out = *in + if in.HeaderMatches != nil { + in, out := &in.HeaderMatches, &out.HeaderMatches + *out = make([]*StringMatch, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(StringMatch) + (*in).DeepCopyInto(*out) + } + } + } + if in.Limit != nil { + in, out := &in.Limit, &out.Limit + *out = new(RateLimitValue) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitRule. +func (in *RateLimitRule) DeepCopy() *RateLimitRule { + if in == nil { + return nil + } + out := new(RateLimitRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RateLimitValue) DeepCopyInto(out *RateLimitValue) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitValue. +func (in *RateLimitValue) DeepCopy() *RateLimitValue { + if in == nil { + return nil + } + out := new(RateLimitValue) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Redirect) DeepCopyInto(out *Redirect) { *out = *in diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index ce3d35e7b0..09e5a88314 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -92,6 +92,11 @@ func addXdsHTTPFilterChain(xdsListener *listener.Listener, irListener *ir.HTTPLi }}, } + // TODO: Make this a generic interface for all API Gateway features. + if err := patchHCMWithRateLimit(mgr, irListener); err != nil { + return err + } + mgrAny, err := anypb.New(mgr) if err != nil { return err diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go new file mode 100644 index 0000000000..6bd9c8c6ad --- /dev/null +++ b/internal/xds/translator/ratelimit.go @@ -0,0 +1,231 @@ +// 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 ( + "strconv" + "time" + + cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" + ratelimit "github.com/envoyproxy/go-control-plane/envoy/config/ratelimit/v3" + route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + ratelimitfilter "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ratelimit/v3" + hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + wkt "github.com/envoyproxy/go-control-plane/pkg/wellknown" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/wrapperspb" + + "github.com/envoyproxy/gateway/internal/ir" +) + +// patchHCMWithRateLimit builds and appends the Rate Limit Filter to the HTTP connection manager +// if applicable and it does not already exist. +func patchHCMWithRateLimit(mgr *hcm.HttpConnectionManager, irListener *ir.HTTPListener) error { + // Return early if rate limits dont exist + if !isRateLimitPresent(irListener) { + return nil + } + + // Return early if filter already exists. + for _, httpFilter := range mgr.HttpFilters { + if httpFilter.Name == wkt.HTTPRateLimit { + return nil + } + } + + rateLimitFilter := buildRateLimitFilter(irListener) + // Make sure the router filter is the terminal filter in the chain + mgr.HttpFilters = append([]*hcm.HttpFilter{rateLimitFilter}, mgr.HttpFilters...) + return nil +} + +// isRateLimitPresent returns true if rate limit config exists for the listener. +func isRateLimitPresent(irListener *ir.HTTPListener) bool { + // Return true if rate limit config exists. + for _, route := range irListener.Routes { + if route.RateLimit != nil && route.RateLimit.Global != nil { + return true + } + } + return false +} + +func buildRateLimitFilter(irListener *ir.HTTPListener) *hcm.HttpFilter { + rateLimitFilterProto := &ratelimitfilter.RateLimit{ + Domain: getRateLimitDomain(irListener), + RateLimitService: &ratelimit.RateLimitServiceConfig{ + GrpcService: &core.GrpcService{ + TargetSpecifier: &core.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ + ClusterName: getRateLimitServiceClusterName(), + }, + }, + }, + TransportApiVersion: core.ApiVersion_V3, + }, + } + + any, err := anypb.New(rateLimitFilterProto) + if err != nil { + return nil + } + + rateLimitFilter := &hcm.HttpFilter{ + Name: wkt.HTTPRateLimit, + ConfigType: &hcm.HttpFilter_TypedConfig{ + TypedConfig: any, + }, + } + return rateLimitFilter +} + +// patchRouteWithRateLimit builds rate limit actions and appends to the route. +func patchRouteWithRateLimit(xdsRouteAction *route.RouteAction, irRoute *ir.HTTPRoute) error { //nolint:unparam + // Return early if no rate limit config exists. + if irRoute.RateLimit == nil || irRoute.RateLimit.Global == nil { + return nil + } + + rateLimits := buildRouteRateLimits(irRoute.Name, irRoute.RateLimit.Global) + xdsRouteAction.RateLimits = rateLimits + return nil +} + +func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) []*route.RateLimit { + rateLimits := []*route.RateLimit{} + // Rules are ORed + for rIdx, rule := range global.Rules { + rlActions := []*route.RateLimit_Action{} + // Matches are ANDed + for mIdx, match := range rule.HeaderMatches { + // Case for distinct match + if match.Distinct { + // Setup RequestHeader actions + descriptorKey := getRateLimitDescriptorKey(descriptorPrefix, rIdx, mIdx) + action := &route.RateLimit_Action{ + ActionSpecifier: &route.RateLimit_Action_RequestHeaders_{ + RequestHeaders: &route.RateLimit_Action_RequestHeaders{ + HeaderName: match.Name, + DescriptorKey: descriptorKey, + }, + }, + } + rlActions = append(rlActions, action) + } else { + // Setup HeaderValueMatch actions + descriptorKey := getRateLimitDescriptorKey(descriptorPrefix, rIdx, mIdx) + descriptorVal := getRateLimitDescriptorValue(descriptorPrefix, rIdx, mIdx) + headerMatcher := &route.HeaderMatcher{ + Name: match.Name, + HeaderMatchSpecifier: &route.HeaderMatcher_StringMatch{ + StringMatch: buildXdsStringMatcher(match), + }, + } + action := &route.RateLimit_Action{ + ActionSpecifier: &route.RateLimit_Action_HeaderValueMatch_{ + HeaderValueMatch: &route.RateLimit_Action_HeaderValueMatch{ + DescriptorKey: descriptorKey, + DescriptorValue: descriptorVal, + ExpectMatch: &wrapperspb.BoolValue{ + Value: true, + }, + Headers: []*route.HeaderMatcher{headerMatcher}, + }, + }, + } + rlActions = append(rlActions, action) + } + } + + // Case when header match is not set and the rate limit is applied + // to all traffic. + if len(rule.HeaderMatches) == 0 { + // Setup GenericKey action + action := &route.RateLimit_Action{ + ActionSpecifier: &route.RateLimit_Action_GenericKey_{ + GenericKey: &route.RateLimit_Action_GenericKey{ + DescriptorKey: getRateLimitDescriptorKey(descriptorPrefix, rIdx, -1), + DescriptorValue: getRateLimitDescriptorValue(descriptorPrefix, rIdx, -1), + }, + }, + } + rlActions = append(rlActions, action) + } + + rateLimit := &route.RateLimit{Actions: rlActions} + rateLimits = append(rateLimits, rateLimit) + } + + return rateLimits +} + +func buildRateLimitServiceCluster(irListener *ir.HTTPListener) *cluster.Cluster { + // Return early if rate limits dont exist. + if !isRateLimitPresent(irListener) { + return nil + } + + clusterName := getRateLimitServiceClusterName() + host, port := getRateLimitServiceGrpcHostPort() + rateLimitServerCluster := &cluster.Cluster{ + Name: clusterName, + ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STRICT_DNS}, + ConnectTimeout: durationpb.New(10 * time.Second), + LbPolicy: cluster.Cluster_RANDOM, + LoadAssignment: &endpoint.ClusterLoadAssignment{ + ClusterName: clusterName, + Endpoints: []*endpoint.LocalityLbEndpoints{ + { + LbEndpoints: []*endpoint.LbEndpoint{ + { + HostIdentifier: &endpoint.LbEndpoint_Endpoint{ + Endpoint: &endpoint.Endpoint{ + Address: &core.Address{ + Address: &core.Address_SocketAddress{ + SocketAddress: &core.SocketAddress{ + Address: host, + PortSpecifier: &core.SocketAddress_PortValue{PortValue: uint32(port)}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Http2ProtocolOptions: &core.Http2ProtocolOptions{}, + DnsRefreshRate: durationpb.New(30 * time.Second), + RespectDnsTtl: true, + DnsLookupFamily: cluster.Cluster_V4_ONLY, + } + return rateLimitServerCluster +} + +func getRateLimitDescriptorKey(prefix string, ruleIndex, matchIndex int) string { + return prefix + "-key-rule-" + strconv.Itoa(ruleIndex) + "-match-" + strconv.Itoa(matchIndex) +} + +func getRateLimitDescriptorValue(prefix string, ruleIndex, matchIndex int) string { + return prefix + "-value-rule-" + strconv.Itoa(ruleIndex) + "-match-" + strconv.Itoa(matchIndex) +} + +func getRateLimitServiceClusterName() string { + return "ratelimit_cluster" +} + +func getRateLimitDomain(irListener *ir.HTTPListener) string { + // Use IR listener name as domain + return irListener.Name +} + +func getRateLimitServiceGrpcHostPort() (string, int) { + return "TODO", 0 +} diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index f57daeda4b..6d12da56d8 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -49,6 +49,11 @@ func buildXdsRoute(httpRoute *ir.HTTPRoute) *route.Route { } } + // TODO: convert this into a generic interface for API Gateway features + if err := patchRouteWithRateLimit(ret.GetRoute(), httpRoute); err != nil { + return nil + } + return ret } diff --git a/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml b/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml new file mode 100644 index 0000000000..4bdfb033cd --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml @@ -0,0 +1,49 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + routes: + - name: "first-route" + rateLimit: + global: + rules: + - headerMatches: + - name: "x-user-id" + exact: "one" + limit: + requests: 5 + unit: second + pathMatch: + exact: "foo/bar" + destinations: + - host: "1.2.3.4" + port: 50000 + - name: "second-route" + rateLimit: + global: + rules: + - headerMatches: + - name: "x-user-id" + distinct: true + limit: + requests: 5 + unit: second + pathMatch: + exact: "example" + destinations: + - host: "1.2.3.4" + port: 50000 + - name: "third-route" + rateLimit: + global: + rules: + - limit: + requests: 5 + unit: second + pathMatch: + exact: "test" + destinations: + - host: "1.2.3.4" + port: 50000 diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit.clusters.yaml new file mode 100644 index 0000000000..cb605d770d --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit.clusters.yaml @@ -0,0 +1,71 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: first-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: first-route + outlierDetection: {} + type: STATIC +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: second-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: second-route + outlierDetection: {} + type: STATIC +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: third-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: third-route + outlierDetection: {} + type: STATIC +- connectTimeout: 10s + dnsLookupFamily: V4_ONLY + dnsRefreshRate: 30s + http2ProtocolOptions: {} + lbPolicy: RANDOM + loadAssignment: + clusterName: ratelimit_cluster + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: TODO + portValue: 0 + name: ratelimit_cluster + respectDnsTtl: true + type: STRICT_DNS diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit.listeners.yaml new file mode 100644 index 0000000000..f7cadff3d8 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit.listeners.yaml @@ -0,0 +1,49 @@ +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + 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 + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + httpFilters: + - name: envoy.filters.http.ratelimit + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit + domain: first-listener + rateLimitService: + grpcService: + envoyGrpc: + clusterName: ratelimit_cluster + transportApiVersion: V3 + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + rds: + configSource: + apiConfigSource: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + name: first-listener diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml new file mode 100644 index 0000000000..0bdd770aa0 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml @@ -0,0 +1,38 @@ +- name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener + routes: + - match: + path: foo/bar + route: + cluster: first-route + rateLimits: + - actions: + - headerValueMatch: + descriptorKey: first-route-key-rule-0-match-0 + descriptorValue: first-route-value-rule-0-match-0 + expectMatch: true + headers: + - name: x-user-id + stringMatch: + exact: one + - match: + path: example + route: + cluster: second-route + rateLimits: + - actions: + - requestHeaders: + descriptorKey: second-route-key-rule-0-match-0 + headerName: x-user-id + - match: + path: test + route: + cluster: third-route + rateLimits: + - actions: + - genericKey: + descriptorKey: third-route-key-rule-0-match--1 + descriptorValue: third-route-value-rule-0-match--1 diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index a3bb2795df..0a1500b02c 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -8,6 +8,7 @@ package translator import ( "errors" + cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" @@ -108,6 +109,18 @@ func processHTTPListenerXdsTranslation(tCtx *types.ResourceVersionTable, httpLis } xdsRouteCfg.VirtualHosts = append(xdsRouteCfg.VirtualHosts, vHost) + + // TODO: Make this into a generic interface for API Gateway features + // Check if a ratelimit cluster exists, if not, add it, if its needed. + // This is current O(n) right now, but it also leverages an existing + // object without allocating new memory. Consider improving it in the future. + if rlCluster := findXdsCluster(tCtx, getRateLimitServiceClusterName()); rlCluster == nil { + rlCluster := buildRateLimitServiceCluster(httpListener) + // Add cluster + if rlCluster != nil { + tCtx.AddXdsResource(resource.ClusterType, rlCluster) + } + } } return nil } @@ -147,6 +160,7 @@ func processUDPListenerXdsTranslation(tCtx *types.ResourceVersionTable, udpListe tCtx.AddXdsResource(resource.ListenerType, xdsListener) } return nil + } // findXdsListener finds a xds listener with the same address, port and protocol, and returns nil if there is no match. @@ -168,6 +182,22 @@ func findXdsListener(tCtx *types.ResourceVersionTable, address string, port uint return nil } +// findXdsCluster finds a xds cluster with the same name, and returns nil if there is no match. +func findXdsCluster(tCtx *types.ResourceVersionTable, name string) *cluster.Cluster { + if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resource.ClusterType] == nil { + return nil + } + + for _, r := range tCtx.XdsResources[resource.ClusterType] { + cluster := r.(*cluster.Cluster) + if cluster.Name == name { + return cluster + } + } + + return nil +} + // findXdsRouteConfig finds an xds route with the name and returns nil if there is no match. func findXdsRouteConfig(tCtx *types.ResourceVersionTable, name string) *route.RouteConfiguration { if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resource.RouteType] == nil { diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index 3d8df22453..51bd02b828 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -101,6 +101,9 @@ func TestTranslate(t *testing.T) { { name: "http-route-rewrite-url-host", }, + { + name: "ratelimit", + }, } for _, tc := range testCases {