1
17
18 package xdsresource
19
20 import (
21 "errors"
22 "fmt"
23 "math"
24 "regexp"
25 "testing"
26 "time"
27
28 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
29 "github.com/google/go-cmp/cmp"
30 "github.com/google/go-cmp/cmp/cmpopts"
31 "google.golang.org/grpc/codes"
32 "google.golang.org/grpc/internal/pretty"
33 "google.golang.org/grpc/internal/testutils"
34 "google.golang.org/grpc/internal/xds/matcher"
35 "google.golang.org/grpc/xds/internal/clusterspecifier"
36 "google.golang.org/grpc/xds/internal/httpfilter"
37 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version"
38 "google.golang.org/protobuf/proto"
39 "google.golang.org/protobuf/types/known/anypb"
40 "google.golang.org/protobuf/types/known/durationpb"
41 "google.golang.org/protobuf/types/known/wrapperspb"
42
43 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
44 rpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
45 v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
46 v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
47 v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
48 v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
49 )
50
51 func (s) TestRDSGenerateRDSUpdateFromRouteConfiguration(t *testing.T) {
52 const (
53 uninterestingDomain = "uninteresting.domain"
54 uninterestingClusterName = "uninterestingClusterName"
55 ldsTarget = "lds.target.good:1111"
56 routeName = "routeName"
57 clusterName = "clusterName"
58 )
59
60 var (
61 goodRouteConfigWithFilterConfigs = func(cfgs map[string]*anypb.Any) *v3routepb.RouteConfiguration {
62 return &v3routepb.RouteConfiguration{
63 Name: routeName,
64 VirtualHosts: []*v3routepb.VirtualHost{{
65 Domains: []string{ldsTarget},
66 Routes: []*v3routepb.Route{{
67 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
68 Action: &v3routepb.Route_Route{
69 Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName}},
70 },
71 }},
72 TypedPerFilterConfig: cfgs,
73 }},
74 }
75 }
76 goodRouteConfigWithClusterSpecifierPlugins = func(csps []*v3routepb.ClusterSpecifierPlugin, cspReferences []string) *v3routepb.RouteConfiguration {
77 var rs []*v3routepb.Route
78
79 for i, cspReference := range cspReferences {
80 rs = append(rs, &v3routepb.Route{
81 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: fmt.Sprint(i + 1)}},
82 Action: &v3routepb.Route_Route{
83 Route: &v3routepb.RouteAction{
84 ClusterSpecifier: &v3routepb.RouteAction_ClusterSpecifierPlugin{ClusterSpecifierPlugin: cspReference},
85 },
86 },
87 })
88 }
89
90 rc := &v3routepb.RouteConfiguration{
91 Name: routeName,
92 VirtualHosts: []*v3routepb.VirtualHost{{
93 Domains: []string{ldsTarget},
94 Routes: rs,
95 }},
96 ClusterSpecifierPlugins: csps,
97 }
98
99 return rc
100 }
101 goodRouteConfigWithClusterSpecifierPluginsAndNormalRoute = func(csps []*v3routepb.ClusterSpecifierPlugin, cspReferences []string) *v3routepb.RouteConfiguration {
102 rs := goodRouteConfigWithClusterSpecifierPlugins(csps, cspReferences)
103 rs.VirtualHosts[0].Routes = append(rs.VirtualHosts[0].Routes, &v3routepb.Route{
104 Match: &v3routepb.RouteMatch{
105 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
106 CaseSensitive: &wrapperspb.BoolValue{Value: false},
107 },
108 Action: &v3routepb.Route_Route{
109 Route: &v3routepb.RouteAction{
110 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
111 }}})
112 return rs
113 }
114 goodRouteConfigWithUnsupportedClusterSpecifier = &v3routepb.RouteConfiguration{
115 Name: routeName,
116 VirtualHosts: []*v3routepb.VirtualHost{{
117 Domains: []string{ldsTarget},
118 Routes: []*v3routepb.Route{
119 {
120 Match: &v3routepb.RouteMatch{
121 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
122 CaseSensitive: &wrapperspb.BoolValue{Value: false},
123 },
124 Action: &v3routepb.Route_Route{
125 Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName}},
126 }},
127 {
128 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "|"}},
129 Action: &v3routepb.Route_Route{
130 Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_ClusterHeader{}},
131 }},
132 },
133 },
134 },
135 }
136
137 goodUpdateWithFilterConfigs = func(cfgs map[string]httpfilter.FilterConfig) RouteConfigUpdate {
138 return RouteConfigUpdate{
139 VirtualHosts: []*VirtualHost{{
140 Domains: []string{ldsTarget},
141 Routes: []*Route{{
142 Prefix: newStringP("/"),
143 WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}},
144 ActionType: RouteActionRoute,
145 }},
146 HTTPFilterConfigOverride: cfgs,
147 }},
148 }
149 }
150 goodUpdateWithNormalRoute = RouteConfigUpdate{
151 VirtualHosts: []*VirtualHost{
152 {
153 Domains: []string{ldsTarget},
154 Routes: []*Route{{Prefix: newStringP("/"),
155 CaseInsensitive: true,
156 WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}},
157 ActionType: RouteActionRoute}},
158 },
159 },
160 }
161 goodUpdateWithClusterSpecifierPluginA = RouteConfigUpdate{
162 VirtualHosts: []*VirtualHost{{
163 Domains: []string{ldsTarget},
164 Routes: []*Route{{
165 Prefix: newStringP("1"),
166 ActionType: RouteActionRoute,
167 ClusterSpecifierPlugin: "cspA",
168 }},
169 }},
170 ClusterSpecifierPlugins: map[string]clusterspecifier.BalancerConfig{
171 "cspA": nil,
172 },
173 }
174 clusterSpecifierPlugin = func(name string, config *anypb.Any, isOptional bool) *v3routepb.ClusterSpecifierPlugin {
175 return &v3routepb.ClusterSpecifierPlugin{
176 Extension: &v3corepb.TypedExtensionConfig{
177 Name: name,
178 TypedConfig: config,
179 },
180 IsOptional: isOptional,
181 }
182 }
183 goodRouteConfigWithRetryPolicy = func(vhrp *v3routepb.RetryPolicy, rrp *v3routepb.RetryPolicy) *v3routepb.RouteConfiguration {
184 return &v3routepb.RouteConfiguration{
185 Name: routeName,
186 VirtualHosts: []*v3routepb.VirtualHost{{
187 Domains: []string{ldsTarget},
188 Routes: []*v3routepb.Route{{
189 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
190 Action: &v3routepb.Route_Route{
191 Route: &v3routepb.RouteAction{
192 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
193 RetryPolicy: rrp,
194 },
195 },
196 }},
197 RetryPolicy: vhrp,
198 }},
199 }
200 }
201 goodUpdateWithRetryPolicy = func(vhrc *RetryConfig, rrc *RetryConfig) RouteConfigUpdate {
202 return RouteConfigUpdate{
203 VirtualHosts: []*VirtualHost{{
204 Domains: []string{ldsTarget},
205 Routes: []*Route{{
206 Prefix: newStringP("/"),
207 WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}},
208 ActionType: RouteActionRoute,
209 RetryConfig: rrc,
210 }},
211 RetryConfig: vhrc,
212 }},
213 }
214 }
215 defaultRetryBackoff = RetryBackoff{BaseInterval: 25 * time.Millisecond, MaxInterval: 250 * time.Millisecond}
216 )
217
218 tests := []struct {
219 name string
220 rc *v3routepb.RouteConfiguration
221 wantUpdate RouteConfigUpdate
222 wantError bool
223 }{
224 {
225 name: "default-route-match-field-is-nil",
226 rc: &v3routepb.RouteConfiguration{
227 VirtualHosts: []*v3routepb.VirtualHost{
228 {
229 Domains: []string{ldsTarget},
230 Routes: []*v3routepb.Route{
231 {
232 Action: &v3routepb.Route_Route{
233 Route: &v3routepb.RouteAction{
234 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
235 },
236 },
237 },
238 },
239 },
240 },
241 },
242 wantError: true,
243 },
244 {
245 name: "default-route-match-field-is-non-nil",
246 rc: &v3routepb.RouteConfiguration{
247 VirtualHosts: []*v3routepb.VirtualHost{
248 {
249 Domains: []string{ldsTarget},
250 Routes: []*v3routepb.Route{
251 {
252 Match: &v3routepb.RouteMatch{},
253 Action: &v3routepb.Route_Route{},
254 },
255 },
256 },
257 },
258 },
259 wantError: true,
260 },
261 {
262 name: "default-route-routeaction-field-is-nil",
263 rc: &v3routepb.RouteConfiguration{
264 VirtualHosts: []*v3routepb.VirtualHost{
265 {
266 Domains: []string{ldsTarget},
267 Routes: []*v3routepb.Route{{}},
268 },
269 },
270 },
271 wantError: true,
272 },
273 {
274 name: "default-route-cluster-field-is-empty",
275 rc: &v3routepb.RouteConfiguration{
276 VirtualHosts: []*v3routepb.VirtualHost{
277 {
278 Domains: []string{ldsTarget},
279 Routes: []*v3routepb.Route{
280 {
281 Action: &v3routepb.Route_Route{
282 Route: &v3routepb.RouteAction{
283 ClusterSpecifier: &v3routepb.RouteAction_ClusterHeader{},
284 },
285 },
286 },
287 },
288 },
289 },
290 },
291 wantError: true,
292 },
293 {
294
295 name: "good-route-config-but-with-casesensitive-false",
296 rc: &v3routepb.RouteConfiguration{
297 Name: routeName,
298 VirtualHosts: []*v3routepb.VirtualHost{{
299 Domains: []string{ldsTarget},
300 Routes: []*v3routepb.Route{{
301 Match: &v3routepb.RouteMatch{
302 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
303 CaseSensitive: &wrapperspb.BoolValue{Value: false},
304 },
305 Action: &v3routepb.Route_Route{
306 Route: &v3routepb.RouteAction{
307 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
308 }}}}}}},
309 wantUpdate: RouteConfigUpdate{
310 VirtualHosts: []*VirtualHost{
311 {
312 Domains: []string{ldsTarget},
313 Routes: []*Route{{Prefix: newStringP("/"),
314 CaseInsensitive: true,
315 WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}},
316 ActionType: RouteActionRoute}},
317 },
318 },
319 },
320 },
321 {
322 name: "good-route-config-with-empty-string-route",
323 rc: &v3routepb.RouteConfiguration{
324 Name: routeName,
325 VirtualHosts: []*v3routepb.VirtualHost{
326 {
327 Domains: []string{uninterestingDomain},
328 Routes: []*v3routepb.Route{
329 {
330 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
331 Action: &v3routepb.Route_Route{
332 Route: &v3routepb.RouteAction{
333 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
334 },
335 },
336 },
337 },
338 },
339 {
340 Domains: []string{ldsTarget},
341 Routes: []*v3routepb.Route{
342 {
343 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
344 Action: &v3routepb.Route_Route{
345 Route: &v3routepb.RouteAction{
346 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
347 },
348 },
349 },
350 },
351 },
352 },
353 },
354 wantUpdate: RouteConfigUpdate{
355 VirtualHosts: []*VirtualHost{
356 {
357 Domains: []string{uninterestingDomain},
358 Routes: []*Route{{Prefix: newStringP(""),
359 WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}},
360 ActionType: RouteActionRoute}},
361 },
362 {
363 Domains: []string{ldsTarget},
364 Routes: []*Route{{Prefix: newStringP(""),
365 WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}},
366 ActionType: RouteActionRoute}},
367 },
368 },
369 },
370 },
371 {
372
373 name: "good-route-config-with-slash-string-route",
374 rc: &v3routepb.RouteConfiguration{
375 Name: routeName,
376 VirtualHosts: []*v3routepb.VirtualHost{
377 {
378 Domains: []string{ldsTarget},
379 Routes: []*v3routepb.Route{
380 {
381 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
382 Action: &v3routepb.Route_Route{
383 Route: &v3routepb.RouteAction{
384 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
385 },
386 },
387 },
388 },
389 },
390 },
391 },
392 wantUpdate: RouteConfigUpdate{
393 VirtualHosts: []*VirtualHost{
394 {
395 Domains: []string{ldsTarget},
396 Routes: []*Route{{Prefix: newStringP("/"),
397 WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}},
398 ActionType: RouteActionRoute}},
399 },
400 },
401 },
402 },
403 {
404 name: "good-route-config-with-weighted_clusters",
405 rc: &v3routepb.RouteConfiguration{
406 Name: routeName,
407 VirtualHosts: []*v3routepb.VirtualHost{
408 {
409 Domains: []string{ldsTarget},
410 Routes: []*v3routepb.Route{
411 {
412 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
413 Action: &v3routepb.Route_Route{
414 Route: &v3routepb.RouteAction{
415 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
416 WeightedClusters: &v3routepb.WeightedCluster{
417 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
418 {Name: "a", Weight: &wrapperspb.UInt32Value{Value: 2}},
419 {Name: "b", Weight: &wrapperspb.UInt32Value{Value: 3}},
420 {Name: "c", Weight: &wrapperspb.UInt32Value{Value: 5}},
421 },
422 },
423 },
424 },
425 },
426 },
427 },
428 },
429 },
430 },
431 wantUpdate: RouteConfigUpdate{
432 VirtualHosts: []*VirtualHost{
433 {
434 Domains: []string{ldsTarget},
435 Routes: []*Route{{
436 Prefix: newStringP("/"),
437 WeightedClusters: map[string]WeightedCluster{
438 "a": {Weight: 2},
439 "b": {Weight: 3},
440 "c": {Weight: 5},
441 },
442 ActionType: RouteActionRoute,
443 }},
444 },
445 },
446 },
447 },
448 {
449 name: "good-route-config-with-max-stream-duration",
450 rc: &v3routepb.RouteConfiguration{
451 Name: routeName,
452 VirtualHosts: []*v3routepb.VirtualHost{
453 {
454 Domains: []string{ldsTarget},
455 Routes: []*v3routepb.Route{
456 {
457 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
458 Action: &v3routepb.Route_Route{
459 Route: &v3routepb.RouteAction{
460 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
461 MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{MaxStreamDuration: durationpb.New(time.Second)},
462 },
463 },
464 },
465 },
466 },
467 },
468 },
469 wantUpdate: RouteConfigUpdate{
470 VirtualHosts: []*VirtualHost{
471 {
472 Domains: []string{ldsTarget},
473 Routes: []*Route{{
474 Prefix: newStringP("/"),
475 WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}},
476 MaxStreamDuration: newDurationP(time.Second),
477 ActionType: RouteActionRoute,
478 }},
479 },
480 },
481 },
482 },
483 {
484 name: "good-route-config-with-grpc-timeout-header-max",
485 rc: &v3routepb.RouteConfiguration{
486 Name: routeName,
487 VirtualHosts: []*v3routepb.VirtualHost{
488 {
489 Domains: []string{ldsTarget},
490 Routes: []*v3routepb.Route{
491 {
492 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
493 Action: &v3routepb.Route_Route{
494 Route: &v3routepb.RouteAction{
495 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
496 MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{GrpcTimeoutHeaderMax: durationpb.New(time.Second)},
497 },
498 },
499 },
500 },
501 },
502 },
503 },
504 wantUpdate: RouteConfigUpdate{
505 VirtualHosts: []*VirtualHost{
506 {
507 Domains: []string{ldsTarget},
508 Routes: []*Route{{
509 Prefix: newStringP("/"),
510 WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}},
511 MaxStreamDuration: newDurationP(time.Second),
512 ActionType: RouteActionRoute,
513 }},
514 },
515 },
516 },
517 },
518 {
519 name: "good-route-config-with-both-timeouts",
520 rc: &v3routepb.RouteConfiguration{
521 Name: routeName,
522 VirtualHosts: []*v3routepb.VirtualHost{
523 {
524 Domains: []string{ldsTarget},
525 Routes: []*v3routepb.Route{
526 {
527 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
528 Action: &v3routepb.Route_Route{
529 Route: &v3routepb.RouteAction{
530 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
531 MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{MaxStreamDuration: durationpb.New(2 * time.Second), GrpcTimeoutHeaderMax: durationpb.New(0)},
532 },
533 },
534 },
535 },
536 },
537 },
538 },
539 wantUpdate: RouteConfigUpdate{
540 VirtualHosts: []*VirtualHost{
541 {
542 Domains: []string{ldsTarget},
543 Routes: []*Route{{
544 Prefix: newStringP("/"),
545 WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}},
546 MaxStreamDuration: newDurationP(0),
547 ActionType: RouteActionRoute,
548 }},
549 },
550 },
551 },
552 },
553 {
554 name: "good-route-config-with-http-filter-config",
555 rc: goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": customFilterConfig}),
556 wantUpdate: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
557 },
558 {
559 name: "good-route-config-with-http-filter-config-in-old-typed-struct",
560 rc: goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": testutils.MarshalAny(t, customFilterOldTypedStructConfig)}),
561 wantUpdate: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterOldTypedStructConfig}}),
562 },
563 {
564 name: "good-route-config-with-http-filter-config-in-new-typed-struct",
565 rc: goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": testutils.MarshalAny(t, customFilterNewTypedStructConfig)}),
566 wantUpdate: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterNewTypedStructConfig}}),
567 },
568 {
569 name: "good-route-config-with-optional-http-filter-config",
570 rc: goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter(t, "custom.filter")}),
571 wantUpdate: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
572 },
573 {
574 name: "good-route-config-with-http-err-filter-config",
575 rc: goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": errFilterConfig}),
576 wantError: true,
577 },
578 {
579 name: "good-route-config-with-http-optional-err-filter-config",
580 rc: goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter(t, "err.custom.filter")}),
581 wantError: true,
582 },
583 {
584 name: "good-route-config-with-http-unknown-filter-config",
585 rc: goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": unknownFilterConfig}),
586 wantError: true,
587 },
588 {
589 name: "good-route-config-with-http-optional-unknown-filter-config",
590 rc: goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter(t, "unknown.custom.filter")}),
591 wantUpdate: goodUpdateWithFilterConfigs(nil),
592 },
593 {
594 name: "good-route-config-with-bad-rbac-http-filter-configuration",
595 rc: goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"rbac": testutils.MarshalAny(t, &v3rbacpb.RBACPerRoute{Rbac: &v3rbacpb.RBAC{
596 Rules: &rpb.RBAC{
597 Action: rpb.RBAC_ALLOW,
598 Policies: map[string]*rpb.Policy{
599 "certain-destination-ip": {
600 Permissions: []*rpb.Permission{
601 {Rule: &rpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "not a correct address", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
602 },
603 Principals: []*rpb.Principal{
604 {Identifier: &rpb.Principal_Any{Any: true}},
605 },
606 },
607 },
608 },
609 }})}),
610 wantError: true,
611 },
612 {
613 name: "good-route-config-with-retry-policy",
614 rc: goodRouteConfigWithRetryPolicy(
615 &v3routepb.RetryPolicy{RetryOn: "cancelled"},
616 &v3routepb.RetryPolicy{RetryOn: "deadline-exceeded,unsupported", NumRetries: &wrapperspb.UInt32Value{Value: 2}}),
617 wantUpdate: goodUpdateWithRetryPolicy(
618 &RetryConfig{RetryOn: map[codes.Code]bool{codes.Canceled: true}, NumRetries: 1, RetryBackoff: defaultRetryBackoff},
619 &RetryConfig{RetryOn: map[codes.Code]bool{codes.DeadlineExceeded: true}, NumRetries: 2, RetryBackoff: defaultRetryBackoff}),
620 },
621 {
622 name: "good-route-config-with-retry-backoff",
623 rc: goodRouteConfigWithRetryPolicy(
624 &v3routepb.RetryPolicy{RetryOn: "internal", RetryBackOff: &v3routepb.RetryPolicy_RetryBackOff{BaseInterval: durationpb.New(10 * time.Millisecond), MaxInterval: durationpb.New(10 * time.Millisecond)}},
625 &v3routepb.RetryPolicy{RetryOn: "resource-exhausted", RetryBackOff: &v3routepb.RetryPolicy_RetryBackOff{BaseInterval: durationpb.New(10 * time.Millisecond)}}),
626 wantUpdate: goodUpdateWithRetryPolicy(
627 &RetryConfig{RetryOn: map[codes.Code]bool{codes.Internal: true}, NumRetries: 1, RetryBackoff: RetryBackoff{BaseInterval: 10 * time.Millisecond, MaxInterval: 10 * time.Millisecond}},
628 &RetryConfig{RetryOn: map[codes.Code]bool{codes.ResourceExhausted: true}, NumRetries: 1, RetryBackoff: RetryBackoff{BaseInterval: 10 * time.Millisecond, MaxInterval: 100 * time.Millisecond}}),
629 },
630 {
631 name: "bad-retry-policy-0-retries",
632 rc: goodRouteConfigWithRetryPolicy(&v3routepb.RetryPolicy{RetryOn: "cancelled", NumRetries: &wrapperspb.UInt32Value{Value: 0}}, nil),
633 wantUpdate: RouteConfigUpdate{},
634 wantError: true,
635 },
636 {
637 name: "bad-retry-policy-0-base-interval",
638 rc: goodRouteConfigWithRetryPolicy(&v3routepb.RetryPolicy{RetryOn: "cancelled", RetryBackOff: &v3routepb.RetryPolicy_RetryBackOff{BaseInterval: durationpb.New(0)}}, nil),
639 wantUpdate: RouteConfigUpdate{},
640 wantError: true,
641 },
642 {
643 name: "bad-retry-policy-negative-max-interval",
644 rc: goodRouteConfigWithRetryPolicy(&v3routepb.RetryPolicy{RetryOn: "cancelled", RetryBackOff: &v3routepb.RetryPolicy_RetryBackOff{MaxInterval: durationpb.New(-time.Second)}}, nil),
645 wantUpdate: RouteConfigUpdate{},
646 wantError: true,
647 },
648 {
649 name: "bad-retry-policy-negative-max-interval-no-known-retry-on",
650 rc: goodRouteConfigWithRetryPolicy(&v3routepb.RetryPolicy{RetryOn: "something", RetryBackOff: &v3routepb.RetryPolicy_RetryBackOff{MaxInterval: durationpb.New(-time.Second)}}, nil),
651 wantUpdate: RouteConfigUpdate{},
652 wantError: true,
653 },
654 {
655 name: "cluster-specifier-declared-which-not-registered",
656 rc: goodRouteConfigWithClusterSpecifierPlugins([]*v3routepb.ClusterSpecifierPlugin{
657 clusterSpecifierPlugin("cspA", configOfClusterSpecifierDoesntExist, false),
658 }, []string{"cspA"}),
659 wantError: true,
660 },
661 {
662 name: "error-in-cluster-specifier-plugin-conversion-method",
663 rc: goodRouteConfigWithClusterSpecifierPlugins([]*v3routepb.ClusterSpecifierPlugin{
664 clusterSpecifierPlugin("cspA", errorClusterSpecifierConfig, false),
665 }, []string{"cspA"}),
666 wantError: true,
667 },
668 {
669 name: "route-action-that-references-undeclared-cluster-specifier-plugin",
670 rc: goodRouteConfigWithClusterSpecifierPlugins([]*v3routepb.ClusterSpecifierPlugin{
671 clusterSpecifierPlugin("cspA", mockClusterSpecifierConfig, false),
672 }, []string{"cspA", "cspB"}),
673 wantError: true,
674 },
675 {
676 name: "emitted-cluster-specifier-plugins",
677 rc: goodRouteConfigWithClusterSpecifierPlugins([]*v3routepb.ClusterSpecifierPlugin{
678 clusterSpecifierPlugin("cspA", mockClusterSpecifierConfig, false),
679 }, []string{"cspA"}),
680 wantUpdate: goodUpdateWithClusterSpecifierPluginA,
681 },
682 {
683 name: "deleted-cluster-specifier-plugins-not-referenced",
684 rc: goodRouteConfigWithClusterSpecifierPlugins([]*v3routepb.ClusterSpecifierPlugin{
685 clusterSpecifierPlugin("cspA", mockClusterSpecifierConfig, false),
686 clusterSpecifierPlugin("cspB", mockClusterSpecifierConfig, false),
687 }, []string{"cspA"}),
688 wantUpdate: goodUpdateWithClusterSpecifierPluginA,
689 },
690
691
692
693
694 {
695 name: "cluster-specifier-plugin-not-found-and-optional-route-should-ignore",
696 rc: goodRouteConfigWithClusterSpecifierPluginsAndNormalRoute([]*v3routepb.ClusterSpecifierPlugin{
697 clusterSpecifierPlugin("cspA", configOfClusterSpecifierDoesntExist, true),
698 }, []string{"cspA"}),
699 wantUpdate: goodUpdateWithNormalRoute,
700 },
701
702
703
704
705 {
706 name: "unsupported-cluster-specifier-route-should-ignore",
707 rc: goodRouteConfigWithUnsupportedClusterSpecifier,
708 wantUpdate: goodUpdateWithNormalRoute,
709 },
710 }
711 for _, test := range tests {
712 t.Run(test.name, func(t *testing.T) {
713 gotUpdate, gotError := generateRDSUpdateFromRouteConfiguration(test.rc)
714 if (gotError != nil) != test.wantError ||
715 !cmp.Equal(gotUpdate, test.wantUpdate, cmpopts.EquateEmpty(),
716 cmp.Transformer("FilterConfig", func(fc httpfilter.FilterConfig) string {
717 return fmt.Sprint(fc)
718 })) {
719 t.Errorf("generateRDSUpdateFromRouteConfiguration(%+v, %v) returned unexpected, diff (-want +got):\\n%s", test.rc, ldsTarget, cmp.Diff(test.wantUpdate, gotUpdate, cmpopts.EquateEmpty()))
720 }
721 })
722 }
723 }
724
725 var configOfClusterSpecifierDoesntExist = &anypb.Any{
726 TypeUrl: "does.not.exist",
727 Value: []byte{1, 2, 3},
728 }
729
730 var mockClusterSpecifierConfig = &anypb.Any{
731 TypeUrl: "mock.cluster.specifier.plugin",
732 Value: []byte{1, 2, 3},
733 }
734
735 var errorClusterSpecifierConfig = &anypb.Any{
736 TypeUrl: "error.cluster.specifier.plugin",
737 Value: []byte{1, 2, 3},
738 }
739
740 func init() {
741 clusterspecifier.Register(mockClusterSpecifierPlugin{})
742 clusterspecifier.Register(errorClusterSpecifierPlugin{})
743 }
744
745 type mockClusterSpecifierPlugin struct {
746 }
747
748 func (mockClusterSpecifierPlugin) TypeURLs() []string {
749 return []string{"mock.cluster.specifier.plugin"}
750 }
751
752 func (mockClusterSpecifierPlugin) ParseClusterSpecifierConfig(proto.Message) (clusterspecifier.BalancerConfig, error) {
753 return []map[string]any{}, nil
754 }
755
756 type errorClusterSpecifierPlugin struct{}
757
758 func (errorClusterSpecifierPlugin) TypeURLs() []string {
759 return []string{"error.cluster.specifier.plugin"}
760 }
761
762 func (errorClusterSpecifierPlugin) ParseClusterSpecifierConfig(proto.Message) (clusterspecifier.BalancerConfig, error) {
763 return nil, errors.New("error from cluster specifier conversion function")
764 }
765
766 func (s) TestUnmarshalRouteConfig(t *testing.T) {
767 const (
768 ldsTarget = "lds.target.good:1111"
769 uninterestingDomain = "uninteresting.domain"
770 uninterestingClusterName = "uninterestingClusterName"
771 v3RouteConfigName = "v3RouteConfig"
772 v3ClusterName = "v3Cluster"
773 )
774
775 var (
776 v3VirtualHost = []*v3routepb.VirtualHost{
777 {
778 Domains: []string{uninterestingDomain},
779 Routes: []*v3routepb.Route{
780 {
781 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
782 Action: &v3routepb.Route_Route{
783 Route: &v3routepb.RouteAction{
784 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
785 },
786 },
787 },
788 },
789 },
790 {
791 Domains: []string{ldsTarget},
792 Routes: []*v3routepb.Route{
793 {
794 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
795 Action: &v3routepb.Route_Route{
796 Route: &v3routepb.RouteAction{
797 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: v3ClusterName},
798 },
799 },
800 },
801 },
802 },
803 }
804 v3RouteConfig = testutils.MarshalAny(t, &v3routepb.RouteConfiguration{
805 Name: v3RouteConfigName,
806 VirtualHosts: v3VirtualHost,
807 })
808 )
809
810 tests := []struct {
811 name string
812 resource *anypb.Any
813 wantName string
814 wantUpdate RouteConfigUpdate
815 wantErr bool
816 }{
817 {
818 name: "non-routeConfig resource type",
819 resource: &anypb.Any{TypeUrl: version.V3HTTPConnManagerURL},
820 wantErr: true,
821 },
822 {
823 name: "badly marshaled routeconfig resource",
824 resource: &anypb.Any{
825 TypeUrl: version.V3RouteConfigURL,
826 Value: []byte{1, 2, 3, 4},
827 },
828 wantErr: true,
829 },
830 {
831 name: "v3 routeConfig resource",
832 resource: v3RouteConfig,
833 wantName: v3RouteConfigName,
834 wantUpdate: RouteConfigUpdate{
835 VirtualHosts: []*VirtualHost{
836 {
837 Domains: []string{uninterestingDomain},
838 Routes: []*Route{{Prefix: newStringP(""),
839 WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}},
840 ActionType: RouteActionRoute}},
841 },
842 {
843 Domains: []string{ldsTarget},
844 Routes: []*Route{{Prefix: newStringP(""),
845 WeightedClusters: map[string]WeightedCluster{v3ClusterName: {Weight: 1}},
846 ActionType: RouteActionRoute}},
847 },
848 },
849 Raw: v3RouteConfig,
850 },
851 },
852 {
853 name: "v3 routeConfig resource wrapped",
854 resource: testutils.MarshalAny(t, &v3discoverypb.Resource{Resource: v3RouteConfig}),
855 wantName: v3RouteConfigName,
856 wantUpdate: RouteConfigUpdate{
857 VirtualHosts: []*VirtualHost{
858 {
859 Domains: []string{uninterestingDomain},
860 Routes: []*Route{{Prefix: newStringP(""),
861 WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}},
862 ActionType: RouteActionRoute}},
863 },
864 {
865 Domains: []string{ldsTarget},
866 Routes: []*Route{{Prefix: newStringP(""),
867 WeightedClusters: map[string]WeightedCluster{v3ClusterName: {Weight: 1}},
868 ActionType: RouteActionRoute}},
869 },
870 },
871 Raw: v3RouteConfig,
872 },
873 },
874 }
875 for _, test := range tests {
876 t.Run(test.name, func(t *testing.T) {
877 name, update, err := unmarshalRouteConfigResource(test.resource)
878 if (err != nil) != test.wantErr {
879 t.Errorf("unmarshalRouteConfigResource(%s), got err: %v, wantErr: %v", pretty.ToJSON(test.resource), err, test.wantErr)
880 }
881 if name != test.wantName {
882 t.Errorf("unmarshalRouteConfigResource(%s), got name: %s, want: %s", pretty.ToJSON(test.resource), name, test.wantName)
883 }
884 if diff := cmp.Diff(update, test.wantUpdate, cmpOpts); diff != "" {
885 t.Errorf("unmarshalRouteConfigResource(%s), got unexpected update, diff (-got +want): %v", pretty.ToJSON(test.resource), diff)
886 }
887 })
888 }
889 }
890
891 func (s) TestRoutesProtoToSlice(t *testing.T) {
892 sm, _ := matcher.StringMatcherFromProto(&v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "tv"}})
893 var (
894 goodRouteWithFilterConfigs = func(cfgs map[string]*anypb.Any) []*v3routepb.Route {
895
896 return []*v3routepb.Route{{
897 Match: &v3routepb.RouteMatch{
898 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
899 CaseSensitive: &wrapperspb.BoolValue{Value: false},
900 },
901 Action: &v3routepb.Route_Route{
902 Route: &v3routepb.RouteAction{
903 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
904 WeightedClusters: &v3routepb.WeightedCluster{
905 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
906 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}, TypedPerFilterConfig: cfgs},
907 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
908 },
909 }}}},
910 TypedPerFilterConfig: cfgs,
911 }}
912 }
913 goodUpdateWithFilterConfigs = func(cfgs map[string]httpfilter.FilterConfig) []*Route {
914
915 return []*Route{{
916 Prefix: newStringP("/"),
917 CaseInsensitive: true,
918 WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60, HTTPFilterConfigOverride: cfgs}},
919 HTTPFilterConfigOverride: cfgs,
920 ActionType: RouteActionRoute,
921 }}
922 }
923 )
924
925 tests := []struct {
926 name string
927 routes []*v3routepb.Route
928 wantRoutes []*Route
929 wantErr bool
930 }{
931 {
932 name: "no path",
933 routes: []*v3routepb.Route{{
934 Match: &v3routepb.RouteMatch{},
935 }},
936 wantErr: true,
937 },
938 {
939 name: "case_sensitive is false",
940 routes: []*v3routepb.Route{{
941 Match: &v3routepb.RouteMatch{
942 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
943 CaseSensitive: &wrapperspb.BoolValue{Value: false},
944 },
945 Action: &v3routepb.Route_Route{
946 Route: &v3routepb.RouteAction{
947 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
948 WeightedClusters: &v3routepb.WeightedCluster{
949 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
950 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
951 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
952 },
953 }}}},
954 }},
955 wantRoutes: []*Route{{
956 Prefix: newStringP("/"),
957 CaseInsensitive: true,
958 WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
959 ActionType: RouteActionRoute,
960 }},
961 },
962 {
963 name: "good",
964 routes: []*v3routepb.Route{
965 {
966 Match: &v3routepb.RouteMatch{
967 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
968 Headers: []*v3routepb.HeaderMatcher{
969 {
970 Name: "th",
971 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{
972 PrefixMatch: "tv",
973 },
974 InvertMatch: true,
975 },
976 },
977 RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
978 DefaultValue: &v3typepb.FractionalPercent{
979 Numerator: 1,
980 Denominator: v3typepb.FractionalPercent_HUNDRED,
981 },
982 },
983 },
984 Action: &v3routepb.Route_Route{
985 Route: &v3routepb.RouteAction{
986 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
987 WeightedClusters: &v3routepb.WeightedCluster{
988 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
989 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
990 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
991 },
992 }}}},
993 },
994 },
995 wantRoutes: []*Route{{
996 Prefix: newStringP("/a/"),
997 Headers: []*HeaderMatcher{
998 {
999 Name: "th",
1000 InvertMatch: newBoolP(true),
1001 PrefixMatch: newStringP("tv"),
1002 },
1003 },
1004 Fraction: newUInt32P(10000),
1005 WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
1006 ActionType: RouteActionRoute,
1007 }},
1008 wantErr: false,
1009 },
1010 {
1011 name: "good with regex matchers",
1012 routes: []*v3routepb.Route{
1013 {
1014 Match: &v3routepb.RouteMatch{
1015 PathSpecifier: &v3routepb.RouteMatch_SafeRegex{SafeRegex: &v3matcherpb.RegexMatcher{Regex: "/a/"}},
1016 Headers: []*v3routepb.HeaderMatcher{
1017 {
1018 Name: "th",
1019 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: "tv"}},
1020 },
1021 },
1022 RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
1023 DefaultValue: &v3typepb.FractionalPercent{
1024 Numerator: 1,
1025 Denominator: v3typepb.FractionalPercent_HUNDRED,
1026 },
1027 },
1028 },
1029 Action: &v3routepb.Route_Route{
1030 Route: &v3routepb.RouteAction{
1031 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1032 WeightedClusters: &v3routepb.WeightedCluster{
1033 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1034 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
1035 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
1036 },
1037 }}}},
1038 },
1039 },
1040 wantRoutes: []*Route{{
1041 Regex: func() *regexp.Regexp { return regexp.MustCompile("/a/") }(),
1042 Headers: []*HeaderMatcher{
1043 {
1044 Name: "th",
1045 InvertMatch: newBoolP(false),
1046 RegexMatch: func() *regexp.Regexp { return regexp.MustCompile("tv") }(),
1047 },
1048 },
1049 Fraction: newUInt32P(10000),
1050 WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
1051 ActionType: RouteActionRoute,
1052 }},
1053 wantErr: false,
1054 },
1055 {
1056 name: "good with string matcher",
1057 routes: []*v3routepb.Route{
1058 {
1059 Match: &v3routepb.RouteMatch{
1060 PathSpecifier: &v3routepb.RouteMatch_SafeRegex{SafeRegex: &v3matcherpb.RegexMatcher{Regex: "/a/"}},
1061 Headers: []*v3routepb.HeaderMatcher{
1062 {
1063 Name: "th",
1064 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_StringMatch{StringMatch: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "tv"}}},
1065 },
1066 },
1067 RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
1068 DefaultValue: &v3typepb.FractionalPercent{
1069 Numerator: 1,
1070 Denominator: v3typepb.FractionalPercent_HUNDRED,
1071 },
1072 },
1073 },
1074 Action: &v3routepb.Route_Route{
1075 Route: &v3routepb.RouteAction{
1076 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1077 WeightedClusters: &v3routepb.WeightedCluster{
1078 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1079 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
1080 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
1081 },
1082 }}}},
1083 },
1084 },
1085 wantRoutes: []*Route{{
1086 Regex: func() *regexp.Regexp { return regexp.MustCompile("/a/") }(),
1087 Headers: []*HeaderMatcher{
1088 {
1089 Name: "th",
1090 InvertMatch: newBoolP(false),
1091 StringMatch: &sm,
1092 },
1093 },
1094 Fraction: newUInt32P(10000),
1095 WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
1096 ActionType: RouteActionRoute,
1097 }},
1098 wantErr: false,
1099 },
1100 {
1101 name: "query is ignored",
1102 routes: []*v3routepb.Route{
1103 {
1104 Match: &v3routepb.RouteMatch{
1105 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1106 },
1107 Action: &v3routepb.Route_Route{
1108 Route: &v3routepb.RouteAction{
1109 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1110 WeightedClusters: &v3routepb.WeightedCluster{
1111 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1112 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
1113 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
1114 },
1115 }}}},
1116 },
1117 {
1118 Name: "with_query",
1119 Match: &v3routepb.RouteMatch{
1120 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/b/"},
1121 QueryParameters: []*v3routepb.QueryParameterMatcher{{Name: "route_will_be_ignored"}},
1122 },
1123 },
1124 },
1125
1126
1127 wantRoutes: []*Route{{
1128 Prefix: newStringP("/a/"),
1129 WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
1130 ActionType: RouteActionRoute,
1131 }},
1132 wantErr: false,
1133 },
1134 {
1135 name: "unrecognized path specifier",
1136 routes: []*v3routepb.Route{
1137 {
1138 Match: &v3routepb.RouteMatch{
1139 PathSpecifier: &v3routepb.RouteMatch_ConnectMatcher_{},
1140 },
1141 },
1142 },
1143 wantErr: true,
1144 },
1145 {
1146 name: "bad regex in path specifier",
1147 routes: []*v3routepb.Route{
1148 {
1149 Match: &v3routepb.RouteMatch{
1150 PathSpecifier: &v3routepb.RouteMatch_SafeRegex{SafeRegex: &v3matcherpb.RegexMatcher{Regex: "??"}},
1151 Headers: []*v3routepb.HeaderMatcher{
1152 {
1153 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "tv"},
1154 },
1155 },
1156 },
1157 Action: &v3routepb.Route_Route{
1158 Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName}},
1159 },
1160 },
1161 },
1162 wantErr: true,
1163 },
1164 {
1165 name: "bad regex in header specifier",
1166 routes: []*v3routepb.Route{
1167 {
1168 Match: &v3routepb.RouteMatch{
1169 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1170 Headers: []*v3routepb.HeaderMatcher{
1171 {
1172 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: "??"}},
1173 },
1174 },
1175 },
1176 Action: &v3routepb.Route_Route{
1177 Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName}},
1178 },
1179 },
1180 },
1181 wantErr: true,
1182 },
1183 {
1184 name: "unrecognized header match specifier",
1185 routes: []*v3routepb.Route{
1186 {
1187 Match: &v3routepb.RouteMatch{
1188 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1189 Headers: []*v3routepb.HeaderMatcher{
1190 {
1191 Name: "th",
1192 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_StringMatch{},
1193 },
1194 },
1195 },
1196 },
1197 },
1198 wantErr: true,
1199 },
1200 {
1201 name: "no cluster in weighted clusters action",
1202 routes: []*v3routepb.Route{
1203 {
1204 Match: &v3routepb.RouteMatch{
1205 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1206 },
1207 Action: &v3routepb.Route_Route{
1208 Route: &v3routepb.RouteAction{
1209 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1210 WeightedClusters: &v3routepb.WeightedCluster{}}}},
1211 },
1212 },
1213 wantErr: true,
1214 },
1215 {
1216 name: "all 0-weight clusters in weighted clusters action",
1217 routes: []*v3routepb.Route{
1218 {
1219 Match: &v3routepb.RouteMatch{
1220 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1221 },
1222 Action: &v3routepb.Route_Route{
1223 Route: &v3routepb.RouteAction{
1224 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1225 WeightedClusters: &v3routepb.WeightedCluster{
1226 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1227 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 0}},
1228 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 0}},
1229 },
1230 }}}},
1231 },
1232 },
1233 wantErr: true,
1234 },
1235 {
1236 name: "The sum of all weighted clusters is more than uint32",
1237 routes: []*v3routepb.Route{
1238 {
1239 Match: &v3routepb.RouteMatch{
1240 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1241 },
1242 Action: &v3routepb.Route_Route{
1243 Route: &v3routepb.RouteAction{
1244 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1245 WeightedClusters: &v3routepb.WeightedCluster{
1246 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1247 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: math.MaxUint32}},
1248 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: math.MaxUint32}},
1249 },
1250 }}}},
1251 },
1252 },
1253 wantErr: true,
1254 },
1255 {
1256 name: "unsupported cluster specifier",
1257 routes: []*v3routepb.Route{
1258 {
1259 Match: &v3routepb.RouteMatch{
1260 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1261 },
1262 Action: &v3routepb.Route_Route{
1263 Route: &v3routepb.RouteAction{
1264 ClusterSpecifier: &v3routepb.RouteAction_ClusterSpecifierPlugin{}}},
1265 },
1266 },
1267 wantErr: true,
1268 },
1269 {
1270 name: "default totalWeight is 100 in weighted clusters action",
1271 routes: []*v3routepb.Route{
1272 {
1273 Match: &v3routepb.RouteMatch{
1274 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1275 },
1276 Action: &v3routepb.Route_Route{
1277 Route: &v3routepb.RouteAction{
1278 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1279 WeightedClusters: &v3routepb.WeightedCluster{
1280 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1281 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
1282 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
1283 },
1284 }}}},
1285 },
1286 },
1287 wantRoutes: []*Route{{
1288 Prefix: newStringP("/a/"),
1289 WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
1290 ActionType: RouteActionRoute,
1291 }},
1292 wantErr: false,
1293 },
1294 {
1295 name: "default totalWeight is 100 in weighted clusters action",
1296 routes: []*v3routepb.Route{
1297 {
1298 Match: &v3routepb.RouteMatch{
1299 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1300 },
1301 Action: &v3routepb.Route_Route{
1302 Route: &v3routepb.RouteAction{
1303 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1304 WeightedClusters: &v3routepb.WeightedCluster{
1305 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1306 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 30}},
1307 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 20}},
1308 },
1309 }}}},
1310 },
1311 },
1312 wantRoutes: []*Route{{
1313 Prefix: newStringP("/a/"),
1314 WeightedClusters: map[string]WeightedCluster{"A": {Weight: 20}, "B": {Weight: 30}},
1315 ActionType: RouteActionRoute,
1316 }},
1317 wantErr: false,
1318 },
1319 {
1320 name: "good-with-channel-id-hash-policy",
1321 routes: []*v3routepb.Route{
1322 {
1323 Match: &v3routepb.RouteMatch{
1324 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1325 Headers: []*v3routepb.HeaderMatcher{
1326 {
1327 Name: "th",
1328 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{
1329 PrefixMatch: "tv",
1330 },
1331 InvertMatch: true,
1332 },
1333 },
1334 RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
1335 DefaultValue: &v3typepb.FractionalPercent{
1336 Numerator: 1,
1337 Denominator: v3typepb.FractionalPercent_HUNDRED,
1338 },
1339 },
1340 },
1341 Action: &v3routepb.Route_Route{
1342 Route: &v3routepb.RouteAction{
1343 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1344 WeightedClusters: &v3routepb.WeightedCluster{
1345 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1346 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
1347 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
1348 },
1349 }},
1350 HashPolicy: []*v3routepb.RouteAction_HashPolicy{
1351 {PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "io.grpc.channel_id"}}},
1352 },
1353 }},
1354 },
1355 },
1356 wantRoutes: []*Route{{
1357 Prefix: newStringP("/a/"),
1358 Headers: []*HeaderMatcher{
1359 {
1360 Name: "th",
1361 InvertMatch: newBoolP(true),
1362 PrefixMatch: newStringP("tv"),
1363 },
1364 },
1365 Fraction: newUInt32P(10000),
1366 WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
1367 HashPolicies: []*HashPolicy{
1368 {HashPolicyType: HashPolicyTypeChannelID},
1369 },
1370 ActionType: RouteActionRoute,
1371 }},
1372 wantErr: false,
1373 },
1374
1375
1376 {
1377 name: "good-with-header-hash-policy-no-regex-specified",
1378 routes: []*v3routepb.Route{
1379 {
1380 Match: &v3routepb.RouteMatch{
1381 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1382 Headers: []*v3routepb.HeaderMatcher{
1383 {
1384 Name: "th",
1385 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{
1386 PrefixMatch: "tv",
1387 },
1388 InvertMatch: true,
1389 },
1390 },
1391 RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
1392 DefaultValue: &v3typepb.FractionalPercent{
1393 Numerator: 1,
1394 Denominator: v3typepb.FractionalPercent_HUNDRED,
1395 },
1396 },
1397 },
1398 Action: &v3routepb.Route_Route{
1399 Route: &v3routepb.RouteAction{
1400 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1401 WeightedClusters: &v3routepb.WeightedCluster{
1402 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1403 {Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
1404 {Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
1405 },
1406 }},
1407 HashPolicy: []*v3routepb.RouteAction_HashPolicy{
1408 {PolicySpecifier: &v3routepb.RouteAction_HashPolicy_Header_{Header: &v3routepb.RouteAction_HashPolicy_Header{HeaderName: ":path"}}},
1409 },
1410 }},
1411 },
1412 },
1413 wantRoutes: []*Route{{
1414 Prefix: newStringP("/a/"),
1415 Headers: []*HeaderMatcher{
1416 {
1417 Name: "th",
1418 InvertMatch: newBoolP(true),
1419 PrefixMatch: newStringP("tv"),
1420 },
1421 },
1422 Fraction: newUInt32P(10000),
1423 WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
1424 HashPolicies: []*HashPolicy{
1425 {HashPolicyType: HashPolicyTypeHeader,
1426 HeaderName: ":path"},
1427 },
1428 ActionType: RouteActionRoute,
1429 }},
1430 wantErr: false,
1431 },
1432 {
1433 name: "with custom HTTP filter config",
1434 routes: goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": customFilterConfig}),
1435 wantRoutes: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
1436 },
1437 {
1438 name: "with custom HTTP filter config in typed struct",
1439 routes: goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": testutils.MarshalAny(t, customFilterOldTypedStructConfig)}),
1440 wantRoutes: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterOldTypedStructConfig}}),
1441 },
1442 {
1443 name: "with optional custom HTTP filter config",
1444 routes: goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter(t, "custom.filter")}),
1445 wantRoutes: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
1446 },
1447 {
1448 name: "with erroring custom HTTP filter config",
1449 routes: goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": errFilterConfig}),
1450 wantErr: true,
1451 },
1452 {
1453 name: "with optional erroring custom HTTP filter config",
1454 routes: goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter(t, "err.custom.filter")}),
1455 wantErr: true,
1456 },
1457 {
1458 name: "with unknown custom HTTP filter config",
1459 routes: goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": unknownFilterConfig}),
1460 wantErr: true,
1461 },
1462 {
1463 name: "with optional unknown custom HTTP filter config",
1464 routes: goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter(t, "unknown.custom.filter")}),
1465 wantRoutes: goodUpdateWithFilterConfigs(nil),
1466 },
1467 }
1468
1469 cmpOpts := []cmp.Option{
1470 cmp.AllowUnexported(Route{}, HeaderMatcher{}, Int64Range{}, regexp.Regexp{}),
1471 cmpopts.EquateEmpty(),
1472 cmp.Transformer("FilterConfig", func(fc httpfilter.FilterConfig) string {
1473 return fmt.Sprint(fc)
1474 }),
1475 }
1476 for _, tt := range tests {
1477 t.Run(tt.name, func(t *testing.T) {
1478 got, _, err := routesProtoToSlice(tt.routes, nil)
1479 if (err != nil) != tt.wantErr {
1480 t.Fatalf("routesProtoToSlice() error = %v, wantErr %v", err, tt.wantErr)
1481 }
1482 if diff := cmp.Diff(got, tt.wantRoutes, cmpOpts...); diff != "" {
1483 t.Fatalf("routesProtoToSlice() returned unexpected diff (-got +want):\n%s", diff)
1484 }
1485 })
1486 }
1487 }
1488
1489 func (s) TestHashPoliciesProtoToSlice(t *testing.T) {
1490 tests := []struct {
1491 name string
1492 hashPolicies []*v3routepb.RouteAction_HashPolicy
1493 wantHashPolicies []*HashPolicy
1494 wantErr bool
1495 }{
1496
1497
1498 {
1499 name: "header-hash-policy",
1500 hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1501 {
1502 PolicySpecifier: &v3routepb.RouteAction_HashPolicy_Header_{
1503 Header: &v3routepb.RouteAction_HashPolicy_Header{
1504 HeaderName: ":path",
1505 RegexRewrite: &v3matcherpb.RegexMatchAndSubstitute{
1506 Pattern: &v3matcherpb.RegexMatcher{Regex: "/products"},
1507 Substitution: "/products",
1508 },
1509 },
1510 },
1511 },
1512 },
1513 wantHashPolicies: []*HashPolicy{
1514 {
1515 HashPolicyType: HashPolicyTypeHeader,
1516 HeaderName: ":path",
1517 Regex: func() *regexp.Regexp { return regexp.MustCompile("/products") }(),
1518 RegexSubstitution: "/products",
1519 },
1520 },
1521 },
1522
1523
1524 {
1525 name: "channel-id-hash-policy",
1526 hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1527 {PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "io.grpc.channel_id"}}},
1528 },
1529 wantHashPolicies: []*HashPolicy{
1530 {HashPolicyType: HashPolicyTypeChannelID},
1531 },
1532 },
1533
1534
1535 {
1536 name: "wrong-filter-state-key",
1537 hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1538 {PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "unsupported key"}}},
1539 },
1540 },
1541
1542
1543 {
1544 name: "no-op-hash-policy",
1545 hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1546 {PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{}},
1547 },
1548 },
1549
1550
1551
1552 {
1553 name: "header-and-channel-id-hash-policy",
1554 hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1555 {
1556 PolicySpecifier: &v3routepb.RouteAction_HashPolicy_Header_{
1557 Header: &v3routepb.RouteAction_HashPolicy_Header{
1558 HeaderName: ":path",
1559 RegexRewrite: &v3matcherpb.RegexMatchAndSubstitute{
1560 Pattern: &v3matcherpb.RegexMatcher{Regex: "/products"},
1561 Substitution: "/products",
1562 },
1563 },
1564 },
1565 },
1566 {
1567 PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "io.grpc.channel_id"}},
1568 Terminal: true,
1569 },
1570 },
1571 wantHashPolicies: []*HashPolicy{
1572 {
1573 HashPolicyType: HashPolicyTypeHeader,
1574 HeaderName: ":path",
1575 Regex: func() *regexp.Regexp { return regexp.MustCompile("/products") }(),
1576 RegexSubstitution: "/products",
1577 },
1578 {
1579 HashPolicyType: HashPolicyTypeChannelID,
1580 Terminal: true,
1581 },
1582 },
1583 },
1584 }
1585
1586 for _, tt := range tests {
1587 t.Run(tt.name, func(t *testing.T) {
1588 got, err := hashPoliciesProtoToSlice(tt.hashPolicies)
1589 if (err != nil) != tt.wantErr {
1590 t.Fatalf("hashPoliciesProtoToSlice() error = %v, wantErr %v", err, tt.wantErr)
1591 }
1592 if diff := cmp.Diff(got, tt.wantHashPolicies, cmp.AllowUnexported(regexp.Regexp{})); diff != "" {
1593 t.Fatalf("hashPoliciesProtoToSlice() returned unexpected diff (-got +want):\n%s", diff)
1594 }
1595 })
1596 }
1597 }
1598
1599 func newStringP(s string) *string {
1600 return &s
1601 }
1602
1603 func newUInt32P(i uint32) *uint32 {
1604 return &i
1605 }
1606
1607 func newBoolP(b bool) *bool {
1608 return &b
1609 }
1610
1611 func newDurationP(d time.Duration) *time.Duration {
1612 return &d
1613 }
1614
View as plain text