From 48f0ca9eac05f46381f2240f51dc2de5fb05de5a Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Fri, 11 Nov 2022 13:58:54 -0800 Subject: [PATCH 01/10] Global RateLimit Xds translation * Enhance `XdsIR` with `RateLimit` to hold rate limiting config. * Translate IR field into route level rate limit actions * Add `BuildRateLimitServiceConfig` which translates the XdsIR into configuration for the envoy rate limit service. Relates to https://github.com/envoyproxy/gateway/issues/670 Signed-off-by: Arko Dasgupta --- internal/ir/xds.go | 44 +++++ internal/ir/zz_generated.deepcopy.go | 97 ++++++++++ internal/xds/translator/listener.go | 5 + internal/xds/translator/ratelimit.go | 243 ++++++++++++++++++++++++++ internal/xds/translator/route.go | 5 + internal/xds/translator/translator.go | 31 ++++ 6 files changed, 425 insertions(+) create mode 100644 internal/xds/translator/ratelimit.go diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 4f578a51b17..ddd000d0647 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -211,6 +211,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 @@ -591,3 +594,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 e948d8ff843..50796775cbd 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 ce3d35e7b04..09e5a883149 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 00000000000..1f6388327a9 --- /dev/null +++ b/internal/xds/translator/ratelimit.go @@ -0,0 +1,243 @@ +// 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" + matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/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" +) + +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 +} + +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 +} + +func PatchRouteWithRateLimit(xdsRouteAction *route.RouteAction, irRoute *ir.HTTPRoute) error { + // 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 { + if match.Exact == nil && match.Prefix == nil && match.SafeRegex == nil { + // 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 + descriptorVal := getRateLimitDescriptorValue(descriptorPrefix, rIdx, mIdx) + headerMatcher := buildHeaderMatcher(match) + action := &route.RateLimit_Action{ + ActionSpecifier: &route.RateLimit_Action_HeaderValueMatch_{ + HeaderValueMatch: &route.RateLimit_Action_HeaderValueMatch{ + DescriptorValue: descriptorVal, + ExpectMatch: &wrapperspb.BoolValue{ + Value: true, + }, + Headers: []*route.HeaderMatcher{headerMatcher}, + }, + }, + } + rlActions = append(rlActions, action) + } + } + + rateLimit := &route.RateLimit{Actions: rlActions} + rateLimits = append(rateLimits, rateLimit) + } + + return rateLimits +} + +func buildHeaderMatcher(match *ir.StringMatch) *route.HeaderMatcher { + var stringMatcher *matcher.StringMatcher + + if match.Exact != nil { + stringMatcher = &matcher.StringMatcher{ + MatchPattern: &matcher.StringMatcher_Exact{ + Exact: *match.Exact, + }, + } + } + if match.Prefix != nil { + stringMatcher = &matcher.StringMatcher{ + MatchPattern: &matcher.StringMatcher_Prefix{ + Prefix: *match.Prefix, + }, + } + } + if match.SafeRegex != nil { + stringMatcher = &matcher.StringMatcher{ + MatchPattern: &matcher.StringMatcher_SafeRegex{ + SafeRegex: &matcher.RegexMatcher{ + Regex: *match.SafeRegex, + EngineType: &matcher.RegexMatcher_GoogleRe2{ + GoogleRe2: &matcher.RegexMatcher_GoogleRE2{}, + }, + }, + }, + } + } + + return &route.HeaderMatcher{ + Name: match.Name, + HeaderMatchSpecifier: &route.HeaderMatcher_StringMatch{ + StringMatch: stringMatcher, + }, + } +} + +func buildRateLimitServiceCluster(irListener *ir.HTTPListener) (*cluster.Cluster, error) { + // Return early if rate limits dont exist. + if !isRateLimitPresent(irListener) { + return nil, 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, nil +} + +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 f57daeda4be..16854038d65 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, err + } + return ret } diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index a3bb2795df4..4a33d646226 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,19 @@ 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. + if rlCluster := findXdsCluster(tCtx, getRateLimitServiceClusterName()); rlCluster == nil { + rlCluster, err := buildRateLimitServiceCluster(httpListener) + if err != nil { + return multierror.Append(err, errors.New("error building ratelimit cluster")) + } + // Add cluster + if rlCluster != nil { + tCtx.AddXdsResource(resource.ClusterType, rlCluster) + } + } } return nil } @@ -147,6 +161,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 +183,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 { From a0d6988b58dd201dcec28b2fa180592fe032c21f Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Wed, 16 Nov 2022 19:47:17 -0800 Subject: [PATCH 02/10] tests Signed-off-by: Arko Dasgupta --- .../testdata/in/xds-ir/ratelimit.yaml | 23 +++++++++ .../out/xds-ir/ratelimit.clusters.yaml | 35 +++++++++++++ .../out/xds-ir/ratelimit.listeners.yaml | 49 +++++++++++++++++++ .../testdata/out/xds-ir/ratelimit.routes.yaml | 19 +++++++ internal/xds/translator/translator_test.go | 3 ++ 5 files changed, 129 insertions(+) create mode 100644 internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/ratelimit.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/ratelimit.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml 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 00000000000..7d917d3e872 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml @@ -0,0 +1,23 @@ +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: + name: "test" + exact: "foo/bar" + 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 00000000000..97777b1b3c3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit.clusters.yaml @@ -0,0 +1,35 @@ +- 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 +- 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 00000000000..f7cadff3d8a --- /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 00000000000..1a58e137740 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml @@ -0,0 +1,19 @@ +- name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener + routes: + - match: + path: foo/bar + route: + cluster: first-route + rateLimits: + - actions: + - headerValueMatch: + descriptorValue: first-route-value-rule-0-match-0 + expectMatch: true + headers: + - name: x-user-id + stringMatch: + exact: one diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index 3d8df224534..51bd02b8289 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 { From 8a030e6076177a8d305796caef1641a564694916 Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Wed, 16 Nov 2022 20:17:45 -0800 Subject: [PATCH 03/10] handle empty header name and value case Signed-off-by: Arko Dasgupta --- internal/xds/translator/ratelimit.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go index 1f6388327a9..d99e62fe92c 100644 --- a/internal/xds/translator/ratelimit.go +++ b/internal/xds/translator/ratelimit.go @@ -101,6 +101,7 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ rlActions := []*route.RateLimit_Action{} // Matches are ANDed for mIdx, match := range rule.HeaderMatches { + // Case when header value is not set if match.Exact == nil && match.Prefix == nil && match.SafeRegex == nil { // Setup RequestHeader actions descriptorKey := getRateLimitDescriptorKey(descriptorPrefix, rIdx, mIdx) @@ -132,6 +133,19 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ } } + // Case when header match is not set and the rate limit is applied + // to all traffic. + if len(rule.HeaderMatches) == 0 { + action := &route.RateLimit_Action{ + ActionSpecifier: &route.RateLimit_Action_GenericKey_{ + GenericKey: &route.RateLimit_Action_GenericKey{ + DescriptorValue: getRateLimitDescriptorValue(descriptorPrefix, rIdx, -1), + }, + }, + } + rlActions = append(rlActions, action) + } + rateLimit := &route.RateLimit{Actions: rlActions} rateLimits = append(rateLimits, rateLimit) } From 37069234e47ae1ada15f91a9e542d43e592b852e Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Thu, 17 Nov 2022 11:57:52 -0800 Subject: [PATCH 04/10] more tests Signed-off-by: Arko Dasgupta --- internal/xds/translator/ratelimit.go | 1 + .../testdata/in/xds-ir/ratelimit.yaml | 27 +++++++++++++- .../out/xds-ir/ratelimit.clusters.yaml | 36 +++++++++++++++++++ .../testdata/out/xds-ir/ratelimit.routes.yaml | 18 ++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go index d99e62fe92c..2577bfde552 100644 --- a/internal/xds/translator/ratelimit.go +++ b/internal/xds/translator/ratelimit.go @@ -139,6 +139,7 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ action := &route.RateLimit_Action{ ActionSpecifier: &route.RateLimit_Action_GenericKey_{ GenericKey: &route.RateLimit_Action_GenericKey{ + DescriptorKey: getRateLimitDescriptorKey(descriptorPrefix, rIdx, -1), DescriptorValue: getRateLimitDescriptorValue(descriptorPrefix, rIdx, -1), }, }, diff --git a/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml b/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml index 7d917d3e872..afafd146673 100644 --- a/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml @@ -16,8 +16,33 @@ http: requests: 5 unit: second pathMatch: - name: "test" exact: "foo/bar" destinations: + - host: "1.2.3.4" + port: 50000 + - name: "second-route" + rateLimit: + global: + rules: + - headerMatches: + - name: "x-user-id" + 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 index 97777b1b3c3..cb605d770dc 100644 --- a/internal/xds/translator/testdata/out/xds-ir/ratelimit.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit.clusters.yaml @@ -16,6 +16,42 @@ 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 diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml index 1a58e137740..d9b2c18aa21 100644 --- a/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml @@ -17,3 +17,21 @@ - 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 From 64df594944a5c6182f46d8e7b33ef33db32f2c05 Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Thu, 17 Nov 2022 15:56:07 -0800 Subject: [PATCH 05/10] rm dup string matcher Signed-off-by: Arko Dasgupta --- internal/xds/translator/ratelimit.go | 46 ++++------------------------ 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go index 2577bfde552..9dc2a5000a8 100644 --- a/internal/xds/translator/ratelimit.go +++ b/internal/xds/translator/ratelimit.go @@ -16,7 +16,6 @@ import ( 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" - matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" wkt "github.com/envoyproxy/go-control-plane/pkg/wellknown" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/durationpb" @@ -117,7 +116,12 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ } else { // Setup HeaderValueMatch actions descriptorVal := getRateLimitDescriptorValue(descriptorPrefix, rIdx, mIdx) - headerMatcher := buildHeaderMatcher(match) + 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{ @@ -154,44 +158,6 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ return rateLimits } -func buildHeaderMatcher(match *ir.StringMatch) *route.HeaderMatcher { - var stringMatcher *matcher.StringMatcher - - if match.Exact != nil { - stringMatcher = &matcher.StringMatcher{ - MatchPattern: &matcher.StringMatcher_Exact{ - Exact: *match.Exact, - }, - } - } - if match.Prefix != nil { - stringMatcher = &matcher.StringMatcher{ - MatchPattern: &matcher.StringMatcher_Prefix{ - Prefix: *match.Prefix, - }, - } - } - if match.SafeRegex != nil { - stringMatcher = &matcher.StringMatcher{ - MatchPattern: &matcher.StringMatcher_SafeRegex{ - SafeRegex: &matcher.RegexMatcher{ - Regex: *match.SafeRegex, - EngineType: &matcher.RegexMatcher_GoogleRe2{ - GoogleRe2: &matcher.RegexMatcher_GoogleRE2{}, - }, - }, - }, - } - } - - return &route.HeaderMatcher{ - Name: match.Name, - HeaderMatchSpecifier: &route.HeaderMatcher_StringMatch{ - StringMatch: stringMatcher, - }, - } -} - func buildRateLimitServiceCluster(irListener *ir.HTTPListener) (*cluster.Cluster, error) { // Return early if rate limits dont exist. if !isRateLimitPresent(irListener) { From b91d18f1a0fd76e4d0d77b7545dd8921097662d7 Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Tue, 3 Jan 2023 17:22:08 -0800 Subject: [PATCH 06/10] lint Signed-off-by: Arko Dasgupta --- internal/xds/translator/ratelimit.go | 6 +++--- internal/xds/translator/route.go | 2 +- internal/xds/translator/translator.go | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go index 9dc2a5000a8..9b733fa9a7d 100644 --- a/internal/xds/translator/ratelimit.go +++ b/internal/xds/translator/ratelimit.go @@ -158,10 +158,10 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ return rateLimits } -func buildRateLimitServiceCluster(irListener *ir.HTTPListener) (*cluster.Cluster, error) { +func buildRateLimitServiceCluster(irListener *ir.HTTPListener) *cluster.Cluster { // Return early if rate limits dont exist. if !isRateLimitPresent(irListener) { - return nil, nil + return nil } clusterName := getRateLimitServiceClusterName() @@ -199,7 +199,7 @@ func buildRateLimitServiceCluster(irListener *ir.HTTPListener) (*cluster.Cluster RespectDnsTtl: true, DnsLookupFamily: cluster.Cluster_V4_ONLY, } - return rateLimitServerCluster, nil + return rateLimitServerCluster } func getRateLimitDescriptorKey(prefix string, ruleIndex, matchIndex int) string { diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index 16854038d65..18364c00c3f 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -51,7 +51,7 @@ 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, err + return nil } return ret diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index 4a33d646226..038cb3385e2 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -113,10 +113,14 @@ func processHTTPListenerXdsTranslation(tCtx *types.ResourceVersionTable, httpLis // TODO: Make this into a generic interface for API Gateway features // Check if a ratelimit cluster exists, if not, add it, if its needed. if rlCluster := findXdsCluster(tCtx, getRateLimitServiceClusterName()); rlCluster == nil { +<<<<<<< HEAD rlCluster, err := buildRateLimitServiceCluster(httpListener) if err != nil { return multierror.Append(err, errors.New("error building ratelimit cluster")) } +======= + rlCluster := buildRateLimitServiceCluster(httpListener) +>>>>>>> 80d769b... lint // Add cluster if rlCluster != nil { tCtx.AddXdsResource(resource.ClusterType, rlCluster) From d2da9b4e7b85ce6eae0d6f04e9a9956643fa0c5a Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Wed, 4 Jan 2023 15:31:18 -0800 Subject: [PATCH 07/10] minor enhancement Signed-off-by: Arko Dasgupta --- internal/ir/xds.go | 12 +++++++++++- internal/xds/translator/ratelimit.go | 11 ++++++++--- internal/xds/translator/route.go | 2 +- .../xds/translator/testdata/in/xds-ir/ratelimit.yaml | 1 + 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/internal/ir/xds.go b/internal/ir/xds.go index ddd000d0647..e0708c0eea3 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") @@ -476,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 @@ -494,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) diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go index 9b733fa9a7d..c1104d2ffaf 100644 --- a/internal/xds/translator/ratelimit.go +++ b/internal/xds/translator/ratelimit.go @@ -24,6 +24,8 @@ import ( "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) { @@ -43,6 +45,7 @@ func patchHCMWithRateLimit(mgr *hcm.HttpConnectionManager, irListener *ir.HTTPLi 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 { @@ -82,7 +85,8 @@ func buildRateLimitFilter(irListener *ir.HTTPListener) *hcm.HttpFilter { return rateLimitFilter } -func PatchRouteWithRateLimit(xdsRouteAction *route.RouteAction, irRoute *ir.HTTPRoute) error { +// 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 @@ -100,8 +104,8 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ rlActions := []*route.RateLimit_Action{} // Matches are ANDed for mIdx, match := range rule.HeaderMatches { - // Case when header value is not set - if match.Exact == nil && match.Prefix == nil && match.SafeRegex == nil { + // Case for distinct match + if match.Distinct { // Setup RequestHeader actions descriptorKey := getRateLimitDescriptorKey(descriptorPrefix, rIdx, mIdx) action := &route.RateLimit_Action{ @@ -140,6 +144,7 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ // 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{ diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index 18364c00c3f..6d12da56d87 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -50,7 +50,7 @@ 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 { + if err := patchRouteWithRateLimit(ret.GetRoute(), httpRoute); err != nil { return nil } diff --git a/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml b/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml index afafd146673..4bdfb033cd0 100644 --- a/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/ratelimit.yaml @@ -26,6 +26,7 @@ http: rules: - headerMatches: - name: "x-user-id" + distinct: true limit: requests: 5 unit: second From bdab0cac6be55e97d9925fc4550db710b4ac080e Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Thu, 5 Jan 2023 10:44:16 -0800 Subject: [PATCH 08/10] address comments Signed-off-by: Arko Dasgupta --- internal/xds/translator/ratelimit.go | 2 ++ .../xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml | 1 + internal/xds/translator/translator.go | 2 ++ 3 files changed, 5 insertions(+) diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go index c1104d2ffaf..6bd9c8c6ad8 100644 --- a/internal/xds/translator/ratelimit.go +++ b/internal/xds/translator/ratelimit.go @@ -119,6 +119,7 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ rlActions = append(rlActions, action) } else { // Setup HeaderValueMatch actions + descriptorKey := getRateLimitDescriptorKey(descriptorPrefix, rIdx, mIdx) descriptorVal := getRateLimitDescriptorValue(descriptorPrefix, rIdx, mIdx) headerMatcher := &route.HeaderMatcher{ Name: match.Name, @@ -129,6 +130,7 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ action := &route.RateLimit_Action{ ActionSpecifier: &route.RateLimit_Action_HeaderValueMatch_{ HeaderValueMatch: &route.RateLimit_Action_HeaderValueMatch{ + DescriptorKey: descriptorKey, DescriptorValue: descriptorVal, ExpectMatch: &wrapperspb.BoolValue{ Value: true, diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml index d9b2c18aa21..0bdd770aa05 100644 --- a/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit.routes.yaml @@ -11,6 +11,7 @@ rateLimits: - actions: - headerValueMatch: + descriptorKey: first-route-key-rule-0-match-0 descriptorValue: first-route-value-rule-0-match-0 expectMatch: true headers: diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index 038cb3385e2..5335d0f5325 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -112,6 +112,8 @@ func processHTTPListenerXdsTranslation(tCtx *types.ResourceVersionTable, httpLis // 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 { <<<<<<< HEAD rlCluster, err := buildRateLimitServiceCluster(httpListener) From 5c6622ab19868d46ad8116b9eb437242f3b4f7ea Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Thu, 5 Jan 2023 15:09:21 -0800 Subject: [PATCH 09/10] fix godoc for StringMatch Signed-off-by: Arko Dasgupta --- internal/ir/xds.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ir/xds.go b/internal/ir/xds.go index e0708c0eea3..9a1ea23ea0a 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -464,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. From 8a097f39104ae4470cafbccd39eb33e6111cc888 Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Fri, 6 Jan 2023 15:53:35 -0800 Subject: [PATCH 10/10] fix merge conflicts Signed-off-by: Arko Dasgupta --- internal/xds/translator/translator.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index 5335d0f5325..0a1500b02ca 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -115,14 +115,7 @@ func processHTTPListenerXdsTranslation(tCtx *types.ResourceVersionTable, httpLis // 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 { -<<<<<<< HEAD - rlCluster, err := buildRateLimitServiceCluster(httpListener) - if err != nil { - return multierror.Append(err, errors.New("error building ratelimit cluster")) - } -======= rlCluster := buildRateLimitServiceCluster(httpListener) ->>>>>>> 80d769b... lint // Add cluster if rlCluster != nil { tCtx.AddXdsResource(resource.ClusterType, rlCluster)