1// +build go1.12
2
3/*
4 *
5 * Copyright 2020 gRPC authors.
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 *     http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 */
20
21package xdsclient
22
23import (
24	"fmt"
25	"regexp"
26	"testing"
27	"time"
28
29	"github.com/google/go-cmp/cmp"
30	"github.com/google/go-cmp/cmp/cmpopts"
31	"google.golang.org/grpc/internal/testutils"
32	"google.golang.org/grpc/internal/xds/env"
33	"google.golang.org/grpc/xds/internal/httpfilter"
34	"google.golang.org/grpc/xds/internal/version"
35	"google.golang.org/protobuf/types/known/durationpb"
36
37	v2xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
38	v2routepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
39	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
40	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
41	v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
42	v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
43	anypb "github.com/golang/protobuf/ptypes/any"
44	wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
45)
46
47func (s) TestRDSGenerateRDSUpdateFromRouteConfiguration(t *testing.T) {
48	const (
49		uninterestingDomain      = "uninteresting.domain"
50		uninterestingClusterName = "uninterestingClusterName"
51		ldsTarget                = "lds.target.good:1111"
52		routeName                = "routeName"
53		clusterName              = "clusterName"
54	)
55
56	var (
57		goodRouteConfigWithFilterConfigs = func(cfgs map[string]*anypb.Any) *v3routepb.RouteConfiguration {
58			return &v3routepb.RouteConfiguration{
59				Name: routeName,
60				VirtualHosts: []*v3routepb.VirtualHost{{
61					Domains: []string{ldsTarget},
62					Routes: []*v3routepb.Route{{
63						Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
64						Action: &v3routepb.Route_Route{
65							Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName}},
66						},
67					}},
68					TypedPerFilterConfig: cfgs,
69				}},
70			}
71		}
72		goodUpdateWithFilterConfigs = func(cfgs map[string]httpfilter.FilterConfig) RouteConfigUpdate {
73			return RouteConfigUpdate{
74				VirtualHosts: []*VirtualHost{{
75					Domains: []string{ldsTarget},
76					Routes: []*Route{{
77						Prefix:           newStringP("/"),
78						WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}},
79					}},
80					HTTPFilterConfigOverride: cfgs,
81				}},
82			}
83		}
84	)
85
86	tests := []struct {
87		name       string
88		rc         *v3routepb.RouteConfiguration
89		wantUpdate RouteConfigUpdate
90		wantError  bool
91	}{
92		{
93			name: "default-route-match-field-is-nil",
94			rc: &v3routepb.RouteConfiguration{
95				VirtualHosts: []*v3routepb.VirtualHost{
96					{
97						Domains: []string{ldsTarget},
98						Routes: []*v3routepb.Route{
99							{
100								Action: &v3routepb.Route_Route{
101									Route: &v3routepb.RouteAction{
102										ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
103									},
104								},
105							},
106						},
107					},
108				},
109			},
110			wantError: true,
111		},
112		{
113			name: "default-route-match-field-is-non-nil",
114			rc: &v3routepb.RouteConfiguration{
115				VirtualHosts: []*v3routepb.VirtualHost{
116					{
117						Domains: []string{ldsTarget},
118						Routes: []*v3routepb.Route{
119							{
120								Match:  &v3routepb.RouteMatch{},
121								Action: &v3routepb.Route_Route{},
122							},
123						},
124					},
125				},
126			},
127			wantError: true,
128		},
129		{
130			name: "default-route-routeaction-field-is-nil",
131			rc: &v3routepb.RouteConfiguration{
132				VirtualHosts: []*v3routepb.VirtualHost{
133					{
134						Domains: []string{ldsTarget},
135						Routes:  []*v3routepb.Route{{}},
136					},
137				},
138			},
139			wantError: true,
140		},
141		{
142			name: "default-route-cluster-field-is-empty",
143			rc: &v3routepb.RouteConfiguration{
144				VirtualHosts: []*v3routepb.VirtualHost{
145					{
146						Domains: []string{ldsTarget},
147						Routes: []*v3routepb.Route{
148							{
149								Action: &v3routepb.Route_Route{
150									Route: &v3routepb.RouteAction{
151										ClusterSpecifier: &v3routepb.RouteAction_ClusterHeader{},
152									},
153								},
154							},
155						},
156					},
157				},
158			},
159			wantError: true,
160		},
161		{
162			// default route's match sets case-sensitive to false.
163			name: "good-route-config-but-with-casesensitive-false",
164			rc: &v3routepb.RouteConfiguration{
165				Name: routeName,
166				VirtualHosts: []*v3routepb.VirtualHost{{
167					Domains: []string{ldsTarget},
168					Routes: []*v3routepb.Route{{
169						Match: &v3routepb.RouteMatch{
170							PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
171							CaseSensitive: &wrapperspb.BoolValue{Value: false},
172						},
173						Action: &v3routepb.Route_Route{
174							Route: &v3routepb.RouteAction{
175								ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
176							}}}}}}},
177			wantUpdate: RouteConfigUpdate{
178				VirtualHosts: []*VirtualHost{
179					{
180						Domains: []string{ldsTarget},
181						Routes:  []*Route{{Prefix: newStringP("/"), CaseInsensitive: true, WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}}}},
182					},
183				},
184			},
185		},
186		{
187			name: "good-route-config-with-empty-string-route",
188			rc: &v3routepb.RouteConfiguration{
189				Name: routeName,
190				VirtualHosts: []*v3routepb.VirtualHost{
191					{
192						Domains: []string{uninterestingDomain},
193						Routes: []*v3routepb.Route{
194							{
195								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
196								Action: &v3routepb.Route_Route{
197									Route: &v3routepb.RouteAction{
198										ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
199									},
200								},
201							},
202						},
203					},
204					{
205						Domains: []string{ldsTarget},
206						Routes: []*v3routepb.Route{
207							{
208								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
209								Action: &v3routepb.Route_Route{
210									Route: &v3routepb.RouteAction{
211										ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
212									},
213								},
214							},
215						},
216					},
217				},
218			},
219			wantUpdate: RouteConfigUpdate{
220				VirtualHosts: []*VirtualHost{
221					{
222						Domains: []string{uninterestingDomain},
223						Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
224					},
225					{
226						Domains: []string{ldsTarget},
227						Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}}}},
228					},
229				},
230			},
231		},
232		{
233			// default route's match is not empty string, but "/".
234			name: "good-route-config-with-slash-string-route",
235			rc: &v3routepb.RouteConfiguration{
236				Name: routeName,
237				VirtualHosts: []*v3routepb.VirtualHost{
238					{
239						Domains: []string{ldsTarget},
240						Routes: []*v3routepb.Route{
241							{
242								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
243								Action: &v3routepb.Route_Route{
244									Route: &v3routepb.RouteAction{
245										ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
246									},
247								},
248							},
249						},
250					},
251				},
252			},
253			wantUpdate: RouteConfigUpdate{
254				VirtualHosts: []*VirtualHost{
255					{
256						Domains: []string{ldsTarget},
257						Routes:  []*Route{{Prefix: newStringP("/"), WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}}}},
258					},
259				},
260			},
261		},
262		{
263			// weights not add up to total-weight.
264			name: "route-config-with-weighted_clusters_weights_not_add_up",
265			rc: &v3routepb.RouteConfiguration{
266				Name: routeName,
267				VirtualHosts: []*v3routepb.VirtualHost{
268					{
269						Domains: []string{ldsTarget},
270						Routes: []*v3routepb.Route{
271							{
272								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
273								Action: &v3routepb.Route_Route{
274									Route: &v3routepb.RouteAction{
275										ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
276											WeightedClusters: &v3routepb.WeightedCluster{
277												Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
278													{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 2}},
279													{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 3}},
280													{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 5}},
281												},
282												TotalWeight: &wrapperspb.UInt32Value{Value: 30},
283											},
284										},
285									},
286								},
287							},
288						},
289					},
290				},
291			},
292			wantError: true,
293		},
294		{
295			name: "good-route-config-with-weighted_clusters",
296			rc: &v3routepb.RouteConfiguration{
297				Name: routeName,
298				VirtualHosts: []*v3routepb.VirtualHost{
299					{
300						Domains: []string{ldsTarget},
301						Routes: []*v3routepb.Route{
302							{
303								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
304								Action: &v3routepb.Route_Route{
305									Route: &v3routepb.RouteAction{
306										ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
307											WeightedClusters: &v3routepb.WeightedCluster{
308												Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
309													{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 2}},
310													{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 3}},
311													{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 5}},
312												},
313												TotalWeight: &wrapperspb.UInt32Value{Value: 10},
314											},
315										},
316									},
317								},
318							},
319						},
320					},
321				},
322			},
323			wantUpdate: RouteConfigUpdate{
324				VirtualHosts: []*VirtualHost{
325					{
326						Domains: []string{ldsTarget},
327						Routes: []*Route{{
328							Prefix: newStringP("/"),
329							WeightedClusters: map[string]WeightedCluster{
330								"a": {Weight: 2},
331								"b": {Weight: 3},
332								"c": {Weight: 5},
333							},
334						}},
335					},
336				},
337			},
338		},
339		{
340			name: "good-route-config-with-max-stream-duration",
341			rc: &v3routepb.RouteConfiguration{
342				Name: routeName,
343				VirtualHosts: []*v3routepb.VirtualHost{
344					{
345						Domains: []string{ldsTarget},
346						Routes: []*v3routepb.Route{
347							{
348								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
349								Action: &v3routepb.Route_Route{
350									Route: &v3routepb.RouteAction{
351										ClusterSpecifier:  &v3routepb.RouteAction_Cluster{Cluster: clusterName},
352										MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{MaxStreamDuration: durationpb.New(time.Second)},
353									},
354								},
355							},
356						},
357					},
358				},
359			},
360			wantUpdate: RouteConfigUpdate{
361				VirtualHosts: []*VirtualHost{
362					{
363						Domains: []string{ldsTarget},
364						Routes: []*Route{{
365							Prefix:            newStringP("/"),
366							WeightedClusters:  map[string]WeightedCluster{clusterName: {Weight: 1}},
367							MaxStreamDuration: newDurationP(time.Second),
368						}},
369					},
370				},
371			},
372		},
373		{
374			name: "good-route-config-with-grpc-timeout-header-max",
375			rc: &v3routepb.RouteConfiguration{
376				Name: routeName,
377				VirtualHosts: []*v3routepb.VirtualHost{
378					{
379						Domains: []string{ldsTarget},
380						Routes: []*v3routepb.Route{
381							{
382								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
383								Action: &v3routepb.Route_Route{
384									Route: &v3routepb.RouteAction{
385										ClusterSpecifier:  &v3routepb.RouteAction_Cluster{Cluster: clusterName},
386										MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{GrpcTimeoutHeaderMax: durationpb.New(time.Second)},
387									},
388								},
389							},
390						},
391					},
392				},
393			},
394			wantUpdate: RouteConfigUpdate{
395				VirtualHosts: []*VirtualHost{
396					{
397						Domains: []string{ldsTarget},
398						Routes: []*Route{{
399							Prefix:            newStringP("/"),
400							WeightedClusters:  map[string]WeightedCluster{clusterName: {Weight: 1}},
401							MaxStreamDuration: newDurationP(time.Second),
402						}},
403					},
404				},
405			},
406		},
407		{
408			name: "good-route-config-with-both-timeouts",
409			rc: &v3routepb.RouteConfiguration{
410				Name: routeName,
411				VirtualHosts: []*v3routepb.VirtualHost{
412					{
413						Domains: []string{ldsTarget},
414						Routes: []*v3routepb.Route{
415							{
416								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
417								Action: &v3routepb.Route_Route{
418									Route: &v3routepb.RouteAction{
419										ClusterSpecifier:  &v3routepb.RouteAction_Cluster{Cluster: clusterName},
420										MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{MaxStreamDuration: durationpb.New(2 * time.Second), GrpcTimeoutHeaderMax: durationpb.New(0)},
421									},
422								},
423							},
424						},
425					},
426				},
427			},
428			wantUpdate: RouteConfigUpdate{
429				VirtualHosts: []*VirtualHost{
430					{
431						Domains: []string{ldsTarget},
432						Routes: []*Route{{
433							Prefix:            newStringP("/"),
434							WeightedClusters:  map[string]WeightedCluster{clusterName: {Weight: 1}},
435							MaxStreamDuration: newDurationP(0),
436						}},
437					},
438				},
439			},
440		},
441		{
442			name:       "good-route-config-with-http-filter-config",
443			rc:         goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": customFilterConfig}),
444			wantUpdate: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
445		},
446		{
447			name:       "good-route-config-with-http-filter-config-typed-struct",
448			rc:         goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedCustomFilterTypedStructConfig}),
449			wantUpdate: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterTypedStructConfig}}),
450		},
451		{
452			name:       "good-route-config-with-optional-http-filter-config",
453			rc:         goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("custom.filter")}),
454			wantUpdate: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
455		},
456		{
457			name:      "good-route-config-with-http-err-filter-config",
458			rc:        goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": errFilterConfig}),
459			wantError: true,
460		},
461		{
462			name:      "good-route-config-with-http-optional-err-filter-config",
463			rc:        goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("err.custom.filter")}),
464			wantError: true,
465		},
466		{
467			name:      "good-route-config-with-http-unknown-filter-config",
468			rc:        goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": unknownFilterConfig}),
469			wantError: true,
470		},
471		{
472			name:       "good-route-config-with-http-optional-unknown-filter-config",
473			rc:         goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("unknown.custom.filter")}),
474			wantUpdate: goodUpdateWithFilterConfigs(nil),
475		},
476	}
477
478	for _, test := range tests {
479		t.Run(test.name, func(t *testing.T) {
480			gotUpdate, gotError := generateRDSUpdateFromRouteConfiguration(test.rc, nil, false)
481			if (gotError != nil) != test.wantError ||
482				!cmp.Equal(gotUpdate, test.wantUpdate, cmpopts.EquateEmpty(),
483					cmp.Transformer("FilterConfig", func(fc httpfilter.FilterConfig) string {
484						return fmt.Sprint(fc)
485					})) {
486				t.Errorf("generateRDSUpdateFromRouteConfiguration(%+v, %v) returned unexpected, diff (-want +got):\\n%s", test.rc, ldsTarget, cmp.Diff(test.wantUpdate, gotUpdate, cmpopts.EquateEmpty()))
487			}
488		})
489	}
490}
491
492func (s) TestUnmarshalRouteConfig(t *testing.T) {
493	const (
494		ldsTarget                = "lds.target.good:1111"
495		uninterestingDomain      = "uninteresting.domain"
496		uninterestingClusterName = "uninterestingClusterName"
497		v2RouteConfigName        = "v2RouteConfig"
498		v3RouteConfigName        = "v3RouteConfig"
499		v2ClusterName            = "v2Cluster"
500		v3ClusterName            = "v3Cluster"
501	)
502
503	var (
504		v2VirtualHost = []*v2routepb.VirtualHost{
505			{
506				Domains: []string{uninterestingDomain},
507				Routes: []*v2routepb.Route{
508					{
509						Match: &v2routepb.RouteMatch{PathSpecifier: &v2routepb.RouteMatch_Prefix{Prefix: ""}},
510						Action: &v2routepb.Route_Route{
511							Route: &v2routepb.RouteAction{
512								ClusterSpecifier: &v2routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
513							},
514						},
515					},
516				},
517			},
518			{
519				Domains: []string{ldsTarget},
520				Routes: []*v2routepb.Route{
521					{
522						Match: &v2routepb.RouteMatch{PathSpecifier: &v2routepb.RouteMatch_Prefix{Prefix: ""}},
523						Action: &v2routepb.Route_Route{
524							Route: &v2routepb.RouteAction{
525								ClusterSpecifier: &v2routepb.RouteAction_Cluster{Cluster: v2ClusterName},
526							},
527						},
528					},
529				},
530			},
531		}
532		v2RouteConfig = testutils.MarshalAny(&v2xdspb.RouteConfiguration{
533			Name:         v2RouteConfigName,
534			VirtualHosts: v2VirtualHost,
535		})
536		v3VirtualHost = []*v3routepb.VirtualHost{
537			{
538				Domains: []string{uninterestingDomain},
539				Routes: []*v3routepb.Route{
540					{
541						Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
542						Action: &v3routepb.Route_Route{
543							Route: &v3routepb.RouteAction{
544								ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
545							},
546						},
547					},
548				},
549			},
550			{
551				Domains: []string{ldsTarget},
552				Routes: []*v3routepb.Route{
553					{
554						Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
555						Action: &v3routepb.Route_Route{
556							Route: &v3routepb.RouteAction{
557								ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: v3ClusterName},
558							},
559						},
560					},
561				},
562			},
563		}
564		v3RouteConfig = testutils.MarshalAny(&v3routepb.RouteConfiguration{
565			Name:         v3RouteConfigName,
566			VirtualHosts: v3VirtualHost,
567		})
568	)
569	const testVersion = "test-version-rds"
570
571	tests := []struct {
572		name       string
573		resources  []*anypb.Any
574		wantUpdate map[string]RouteConfigUpdate
575		wantMD     UpdateMetadata
576		wantErr    bool
577	}{
578		{
579			name:      "non-routeConfig resource type",
580			resources: []*anypb.Any{{TypeUrl: version.V3HTTPConnManagerURL}},
581			wantMD: UpdateMetadata{
582				Status:  ServiceStatusNACKed,
583				Version: testVersion,
584				ErrState: &UpdateErrorMetadata{
585					Version: testVersion,
586					Err:     errPlaceHolder,
587				},
588			},
589			wantErr: true,
590		},
591		{
592			name: "badly marshaled routeconfig resource",
593			resources: []*anypb.Any{
594				{
595					TypeUrl: version.V3RouteConfigURL,
596					Value:   []byte{1, 2, 3, 4},
597				},
598			},
599			wantMD: UpdateMetadata{
600				Status:  ServiceStatusNACKed,
601				Version: testVersion,
602				ErrState: &UpdateErrorMetadata{
603					Version: testVersion,
604					Err:     errPlaceHolder,
605				},
606			},
607			wantErr: true,
608		},
609		{
610			name: "empty resource list",
611			wantMD: UpdateMetadata{
612				Status:  ServiceStatusACKed,
613				Version: testVersion,
614			},
615		},
616		{
617			name:      "v2 routeConfig resource",
618			resources: []*anypb.Any{v2RouteConfig},
619			wantUpdate: map[string]RouteConfigUpdate{
620				v2RouteConfigName: {
621					VirtualHosts: []*VirtualHost{
622						{
623							Domains: []string{uninterestingDomain},
624							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
625						},
626						{
627							Domains: []string{ldsTarget},
628							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v2ClusterName: {Weight: 1}}}},
629						},
630					},
631					Raw: v2RouteConfig,
632				},
633			},
634			wantMD: UpdateMetadata{
635				Status:  ServiceStatusACKed,
636				Version: testVersion,
637			},
638		},
639		{
640			name:      "v3 routeConfig resource",
641			resources: []*anypb.Any{v3RouteConfig},
642			wantUpdate: map[string]RouteConfigUpdate{
643				v3RouteConfigName: {
644					VirtualHosts: []*VirtualHost{
645						{
646							Domains: []string{uninterestingDomain},
647							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
648						},
649						{
650							Domains: []string{ldsTarget},
651							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v3ClusterName: {Weight: 1}}}},
652						},
653					},
654					Raw: v3RouteConfig,
655				},
656			},
657			wantMD: UpdateMetadata{
658				Status:  ServiceStatusACKed,
659				Version: testVersion,
660			},
661		},
662		{
663			name:      "multiple routeConfig resources",
664			resources: []*anypb.Any{v2RouteConfig, v3RouteConfig},
665			wantUpdate: map[string]RouteConfigUpdate{
666				v3RouteConfigName: {
667					VirtualHosts: []*VirtualHost{
668						{
669							Domains: []string{uninterestingDomain},
670							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
671						},
672						{
673							Domains: []string{ldsTarget},
674							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v3ClusterName: {Weight: 1}}}},
675						},
676					},
677					Raw: v3RouteConfig,
678				},
679				v2RouteConfigName: {
680					VirtualHosts: []*VirtualHost{
681						{
682							Domains: []string{uninterestingDomain},
683							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
684						},
685						{
686							Domains: []string{ldsTarget},
687							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v2ClusterName: {Weight: 1}}}},
688						},
689					},
690					Raw: v2RouteConfig,
691				},
692			},
693			wantMD: UpdateMetadata{
694				Status:  ServiceStatusACKed,
695				Version: testVersion,
696			},
697		},
698		{
699			// To test that unmarshal keeps processing on errors.
700			name: "good and bad routeConfig resources",
701			resources: []*anypb.Any{
702				v2RouteConfig,
703				testutils.MarshalAny(&v3routepb.RouteConfiguration{
704					Name: "bad",
705					VirtualHosts: []*v3routepb.VirtualHost{
706						{Domains: []string{ldsTarget},
707							Routes: []*v3routepb.Route{{
708								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_ConnectMatcher_{}},
709							}}}}}),
710				v3RouteConfig,
711			},
712			wantUpdate: map[string]RouteConfigUpdate{
713				v3RouteConfigName: {
714					VirtualHosts: []*VirtualHost{
715						{
716							Domains: []string{uninterestingDomain},
717							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
718						},
719						{
720							Domains: []string{ldsTarget},
721							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v3ClusterName: {Weight: 1}}}},
722						},
723					},
724					Raw: v3RouteConfig,
725				},
726				v2RouteConfigName: {
727					VirtualHosts: []*VirtualHost{
728						{
729							Domains: []string{uninterestingDomain},
730							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
731						},
732						{
733							Domains: []string{ldsTarget},
734							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v2ClusterName: {Weight: 1}}}},
735						},
736					},
737					Raw: v2RouteConfig,
738				},
739				"bad": {},
740			},
741			wantMD: UpdateMetadata{
742				Status:  ServiceStatusNACKed,
743				Version: testVersion,
744				ErrState: &UpdateErrorMetadata{
745					Version: testVersion,
746					Err:     errPlaceHolder,
747				},
748			},
749			wantErr: true,
750		},
751	}
752	for _, test := range tests {
753		t.Run(test.name, func(t *testing.T) {
754			update, md, err := UnmarshalRouteConfig(testVersion, test.resources, nil)
755			if (err != nil) != test.wantErr {
756				t.Fatalf("UnmarshalRouteConfig(), got err: %v, wantErr: %v", err, test.wantErr)
757			}
758			if diff := cmp.Diff(update, test.wantUpdate, cmpOpts); diff != "" {
759				t.Errorf("got unexpected update, diff (-got +want): %v", diff)
760			}
761			if diff := cmp.Diff(md, test.wantMD, cmpOptsIgnoreDetails); diff != "" {
762				t.Errorf("got unexpected metadata, diff (-got +want): %v", diff)
763			}
764		})
765	}
766}
767
768func (s) TestRoutesProtoToSlice(t *testing.T) {
769	var (
770		goodRouteWithFilterConfigs = func(cfgs map[string]*anypb.Any) []*v3routepb.Route {
771			// Sets per-filter config in cluster "B" and in the route.
772			return []*v3routepb.Route{{
773				Match: &v3routepb.RouteMatch{
774					PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
775					CaseSensitive: &wrapperspb.BoolValue{Value: false},
776				},
777				Action: &v3routepb.Route_Route{
778					Route: &v3routepb.RouteAction{
779						ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
780							WeightedClusters: &v3routepb.WeightedCluster{
781								Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
782									{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}, TypedPerFilterConfig: cfgs},
783									{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
784								},
785								TotalWeight: &wrapperspb.UInt32Value{Value: 100},
786							}}}},
787				TypedPerFilterConfig: cfgs,
788			}}
789		}
790		goodUpdateWithFilterConfigs = func(cfgs map[string]httpfilter.FilterConfig) []*Route {
791			// Sets per-filter config in cluster "B" and in the route.
792			return []*Route{{
793				Prefix:                   newStringP("/"),
794				CaseInsensitive:          true,
795				WeightedClusters:         map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60, HTTPFilterConfigOverride: cfgs}},
796				HTTPFilterConfigOverride: cfgs,
797			}}
798		}
799	)
800
801	tests := []struct {
802		name       string
803		routes     []*v3routepb.Route
804		wantRoutes []*Route
805		wantErr    bool
806	}{
807		{
808			name: "no path",
809			routes: []*v3routepb.Route{{
810				Match: &v3routepb.RouteMatch{},
811			}},
812			wantErr: true,
813		},
814		{
815			name: "case_sensitive is false",
816			routes: []*v3routepb.Route{{
817				Match: &v3routepb.RouteMatch{
818					PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
819					CaseSensitive: &wrapperspb.BoolValue{Value: false},
820				},
821				Action: &v3routepb.Route_Route{
822					Route: &v3routepb.RouteAction{
823						ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
824							WeightedClusters: &v3routepb.WeightedCluster{
825								Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
826									{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
827									{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
828								},
829								TotalWeight: &wrapperspb.UInt32Value{Value: 100},
830							}}}},
831			}},
832			wantRoutes: []*Route{{
833				Prefix:           newStringP("/"),
834				CaseInsensitive:  true,
835				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
836			}},
837		},
838		{
839			name: "good",
840			routes: []*v3routepb.Route{
841				{
842					Match: &v3routepb.RouteMatch{
843						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
844						Headers: []*v3routepb.HeaderMatcher{
845							{
846								Name: "th",
847								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{
848									PrefixMatch: "tv",
849								},
850								InvertMatch: true,
851							},
852						},
853						RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
854							DefaultValue: &v3typepb.FractionalPercent{
855								Numerator:   1,
856								Denominator: v3typepb.FractionalPercent_HUNDRED,
857							},
858						},
859					},
860					Action: &v3routepb.Route_Route{
861						Route: &v3routepb.RouteAction{
862							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
863								WeightedClusters: &v3routepb.WeightedCluster{
864									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
865										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
866										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
867									},
868									TotalWeight: &wrapperspb.UInt32Value{Value: 100},
869								}}}},
870				},
871			},
872			wantRoutes: []*Route{{
873				Prefix: newStringP("/a/"),
874				Headers: []*HeaderMatcher{
875					{
876						Name:        "th",
877						InvertMatch: newBoolP(true),
878						PrefixMatch: newStringP("tv"),
879					},
880				},
881				Fraction:         newUInt32P(10000),
882				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
883			}},
884			wantErr: false,
885		},
886		{
887			name: "good with regex matchers",
888			routes: []*v3routepb.Route{
889				{
890					Match: &v3routepb.RouteMatch{
891						PathSpecifier: &v3routepb.RouteMatch_SafeRegex{SafeRegex: &v3matcherpb.RegexMatcher{Regex: "/a/"}},
892						Headers: []*v3routepb.HeaderMatcher{
893							{
894								Name:                 "th",
895								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: "tv"}},
896							},
897						},
898						RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
899							DefaultValue: &v3typepb.FractionalPercent{
900								Numerator:   1,
901								Denominator: v3typepb.FractionalPercent_HUNDRED,
902							},
903						},
904					},
905					Action: &v3routepb.Route_Route{
906						Route: &v3routepb.RouteAction{
907							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
908								WeightedClusters: &v3routepb.WeightedCluster{
909									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
910										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
911										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
912									},
913									TotalWeight: &wrapperspb.UInt32Value{Value: 100},
914								}}}},
915				},
916			},
917			wantRoutes: []*Route{{
918				Regex: func() *regexp.Regexp { return regexp.MustCompile("/a/") }(),
919				Headers: []*HeaderMatcher{
920					{
921						Name:        "th",
922						InvertMatch: newBoolP(false),
923						RegexMatch:  func() *regexp.Regexp { return regexp.MustCompile("tv") }(),
924					},
925				},
926				Fraction:         newUInt32P(10000),
927				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
928			}},
929			wantErr: false,
930		},
931		{
932			name: "query is ignored",
933			routes: []*v3routepb.Route{
934				{
935					Match: &v3routepb.RouteMatch{
936						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
937					},
938					Action: &v3routepb.Route_Route{
939						Route: &v3routepb.RouteAction{
940							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
941								WeightedClusters: &v3routepb.WeightedCluster{
942									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
943										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
944										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
945									},
946									TotalWeight: &wrapperspb.UInt32Value{Value: 100},
947								}}}},
948				},
949				{
950					Name: "with_query",
951					Match: &v3routepb.RouteMatch{
952						PathSpecifier:   &v3routepb.RouteMatch_Prefix{Prefix: "/b/"},
953						QueryParameters: []*v3routepb.QueryParameterMatcher{{Name: "route_will_be_ignored"}},
954					},
955				},
956			},
957			// Only one route in the result, because the second one with query
958			// parameters is ignored.
959			wantRoutes: []*Route{{
960				Prefix:           newStringP("/a/"),
961				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
962			}},
963			wantErr: false,
964		},
965		{
966			name: "unrecognized path specifier",
967			routes: []*v3routepb.Route{
968				{
969					Match: &v3routepb.RouteMatch{
970						PathSpecifier: &v3routepb.RouteMatch_ConnectMatcher_{},
971					},
972				},
973			},
974			wantErr: true,
975		},
976		{
977			name: "bad regex in path specifier",
978			routes: []*v3routepb.Route{
979				{
980					Match: &v3routepb.RouteMatch{
981						PathSpecifier: &v3routepb.RouteMatch_SafeRegex{SafeRegex: &v3matcherpb.RegexMatcher{Regex: "??"}},
982						Headers: []*v3routepb.HeaderMatcher{
983							{
984								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "tv"},
985							},
986						},
987					},
988					Action: &v3routepb.Route_Route{
989						Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName}},
990					},
991				},
992			},
993			wantErr: true,
994		},
995		{
996			name: "bad regex in header specifier",
997			routes: []*v3routepb.Route{
998				{
999					Match: &v3routepb.RouteMatch{
1000						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1001						Headers: []*v3routepb.HeaderMatcher{
1002							{
1003								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: "??"}},
1004							},
1005						},
1006					},
1007					Action: &v3routepb.Route_Route{
1008						Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName}},
1009					},
1010				},
1011			},
1012			wantErr: true,
1013		},
1014		{
1015			name: "unrecognized header match specifier",
1016			routes: []*v3routepb.Route{
1017				{
1018					Match: &v3routepb.RouteMatch{
1019						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1020						Headers: []*v3routepb.HeaderMatcher{
1021							{
1022								Name:                 "th",
1023								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_HiddenEnvoyDeprecatedRegexMatch{},
1024							},
1025						},
1026					},
1027				},
1028			},
1029			wantErr: true,
1030		},
1031		{
1032			name: "no cluster in weighted clusters action",
1033			routes: []*v3routepb.Route{
1034				{
1035					Match: &v3routepb.RouteMatch{
1036						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1037					},
1038					Action: &v3routepb.Route_Route{
1039						Route: &v3routepb.RouteAction{
1040							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1041								WeightedClusters: &v3routepb.WeightedCluster{}}}},
1042				},
1043			},
1044			wantErr: true,
1045		},
1046		{
1047			name: "all 0-weight clusters in weighted clusters action",
1048			routes: []*v3routepb.Route{
1049				{
1050					Match: &v3routepb.RouteMatch{
1051						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1052					},
1053					Action: &v3routepb.Route_Route{
1054						Route: &v3routepb.RouteAction{
1055							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1056								WeightedClusters: &v3routepb.WeightedCluster{
1057									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1058										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 0}},
1059										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 0}},
1060									},
1061									TotalWeight: &wrapperspb.UInt32Value{Value: 0},
1062								}}}},
1063				},
1064			},
1065			wantErr: true,
1066		},
1067		{
1068			name: "totalWeight is nil in weighted clusters action",
1069			routes: []*v3routepb.Route{
1070				{
1071					Match: &v3routepb.RouteMatch{
1072						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
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: 20}},
1080										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 30}},
1081									},
1082								}}}},
1083				},
1084			},
1085			wantErr: true,
1086		},
1087		{
1088			name: "The sum of all weighted clusters is not equal totalWeight",
1089			routes: []*v3routepb.Route{
1090				{
1091					Match: &v3routepb.RouteMatch{
1092						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1093					},
1094					Action: &v3routepb.Route_Route{
1095						Route: &v3routepb.RouteAction{
1096							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1097								WeightedClusters: &v3routepb.WeightedCluster{
1098									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1099										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 50}},
1100										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 20}},
1101									},
1102									TotalWeight: &wrapperspb.UInt32Value{Value: 100},
1103								}}}},
1104				},
1105			},
1106			wantErr: true,
1107		},
1108		{
1109			name: "default totalWeight is 100 in weighted clusters action",
1110			routes: []*v3routepb.Route{
1111				{
1112					Match: &v3routepb.RouteMatch{
1113						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1114					},
1115					Action: &v3routepb.Route_Route{
1116						Route: &v3routepb.RouteAction{
1117							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1118								WeightedClusters: &v3routepb.WeightedCluster{
1119									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1120										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
1121										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
1122									},
1123								}}}},
1124				},
1125			},
1126			wantRoutes: []*Route{{
1127				Prefix:           newStringP("/a/"),
1128				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
1129			}},
1130			wantErr: false,
1131		},
1132		{
1133			name: "default totalWeight is 100 in weighted clusters action",
1134			routes: []*v3routepb.Route{
1135				{
1136					Match: &v3routepb.RouteMatch{
1137						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1138					},
1139					Action: &v3routepb.Route_Route{
1140						Route: &v3routepb.RouteAction{
1141							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1142								WeightedClusters: &v3routepb.WeightedCluster{
1143									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1144										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 30}},
1145										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 20}},
1146									},
1147									TotalWeight: &wrapperspb.UInt32Value{Value: 50},
1148								}}}},
1149				},
1150			},
1151			wantRoutes: []*Route{{
1152				Prefix:           newStringP("/a/"),
1153				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 20}, "B": {Weight: 30}},
1154			}},
1155			wantErr: false,
1156		},
1157		{
1158			name: "good-with-channel-id-hash-policy",
1159			routes: []*v3routepb.Route{
1160				{
1161					Match: &v3routepb.RouteMatch{
1162						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1163						Headers: []*v3routepb.HeaderMatcher{
1164							{
1165								Name: "th",
1166								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{
1167									PrefixMatch: "tv",
1168								},
1169								InvertMatch: true,
1170							},
1171						},
1172						RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
1173							DefaultValue: &v3typepb.FractionalPercent{
1174								Numerator:   1,
1175								Denominator: v3typepb.FractionalPercent_HUNDRED,
1176							},
1177						},
1178					},
1179					Action: &v3routepb.Route_Route{
1180						Route: &v3routepb.RouteAction{
1181							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1182								WeightedClusters: &v3routepb.WeightedCluster{
1183									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1184										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
1185										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
1186									},
1187									TotalWeight: &wrapperspb.UInt32Value{Value: 100},
1188								}},
1189							HashPolicy: []*v3routepb.RouteAction_HashPolicy{
1190								{PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "io.grpc.channel_id"}}},
1191							},
1192						}},
1193				},
1194			},
1195			wantRoutes: []*Route{{
1196				Prefix: newStringP("/a/"),
1197				Headers: []*HeaderMatcher{
1198					{
1199						Name:        "th",
1200						InvertMatch: newBoolP(true),
1201						PrefixMatch: newStringP("tv"),
1202					},
1203				},
1204				Fraction:         newUInt32P(10000),
1205				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
1206				HashPolicies: []*HashPolicy{
1207					{HashPolicyType: HashPolicyTypeChannelID},
1208				},
1209			}},
1210			wantErr: false,
1211		},
1212		// This tests that policy.Regex ends up being nil if RegexRewrite is not
1213		// set in xds response.
1214		{
1215			name: "good-with-header-hash-policy-no-regex-specified",
1216			routes: []*v3routepb.Route{
1217				{
1218					Match: &v3routepb.RouteMatch{
1219						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1220						Headers: []*v3routepb.HeaderMatcher{
1221							{
1222								Name: "th",
1223								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{
1224									PrefixMatch: "tv",
1225								},
1226								InvertMatch: true,
1227							},
1228						},
1229						RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
1230							DefaultValue: &v3typepb.FractionalPercent{
1231								Numerator:   1,
1232								Denominator: v3typepb.FractionalPercent_HUNDRED,
1233							},
1234						},
1235					},
1236					Action: &v3routepb.Route_Route{
1237						Route: &v3routepb.RouteAction{
1238							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1239								WeightedClusters: &v3routepb.WeightedCluster{
1240									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1241										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
1242										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
1243									},
1244									TotalWeight: &wrapperspb.UInt32Value{Value: 100},
1245								}},
1246							HashPolicy: []*v3routepb.RouteAction_HashPolicy{
1247								{PolicySpecifier: &v3routepb.RouteAction_HashPolicy_Header_{Header: &v3routepb.RouteAction_HashPolicy_Header{HeaderName: ":path"}}},
1248							},
1249						}},
1250				},
1251			},
1252			wantRoutes: []*Route{{
1253				Prefix: newStringP("/a/"),
1254				Headers: []*HeaderMatcher{
1255					{
1256						Name:        "th",
1257						InvertMatch: newBoolP(true),
1258						PrefixMatch: newStringP("tv"),
1259					},
1260				},
1261				Fraction:         newUInt32P(10000),
1262				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
1263				HashPolicies: []*HashPolicy{
1264					{HashPolicyType: HashPolicyTypeHeader,
1265						HeaderName: ":path"},
1266				},
1267			}},
1268			wantErr: false,
1269		},
1270		{
1271			name:       "with custom HTTP filter config",
1272			routes:     goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": customFilterConfig}),
1273			wantRoutes: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
1274		},
1275		{
1276			name:       "with custom HTTP filter config in typed struct",
1277			routes:     goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedCustomFilterTypedStructConfig}),
1278			wantRoutes: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterTypedStructConfig}}),
1279		},
1280		{
1281			name:       "with optional custom HTTP filter config",
1282			routes:     goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("custom.filter")}),
1283			wantRoutes: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
1284		},
1285		{
1286			name:    "with erroring custom HTTP filter config",
1287			routes:  goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": errFilterConfig}),
1288			wantErr: true,
1289		},
1290		{
1291			name:    "with optional erroring custom HTTP filter config",
1292			routes:  goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("err.custom.filter")}),
1293			wantErr: true,
1294		},
1295		{
1296			name:    "with unknown custom HTTP filter config",
1297			routes:  goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": unknownFilterConfig}),
1298			wantErr: true,
1299		},
1300		{
1301			name:       "with optional unknown custom HTTP filter config",
1302			routes:     goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("unknown.custom.filter")}),
1303			wantRoutes: goodUpdateWithFilterConfigs(nil),
1304		},
1305	}
1306
1307	cmpOpts := []cmp.Option{
1308		cmp.AllowUnexported(Route{}, HeaderMatcher{}, Int64Range{}, regexp.Regexp{}),
1309		cmpopts.EquateEmpty(),
1310		cmp.Transformer("FilterConfig", func(fc httpfilter.FilterConfig) string {
1311			return fmt.Sprint(fc)
1312		}),
1313	}
1314	oldRingHashSupport := env.RingHashSupport
1315	env.RingHashSupport = true
1316	defer func() { env.RingHashSupport = oldRingHashSupport }()
1317	for _, tt := range tests {
1318		t.Run(tt.name, func(t *testing.T) {
1319			got, err := routesProtoToSlice(tt.routes, nil, false)
1320			if (err != nil) != tt.wantErr {
1321				t.Fatalf("routesProtoToSlice() error = %v, wantErr %v", err, tt.wantErr)
1322			}
1323			if diff := cmp.Diff(got, tt.wantRoutes, cmpOpts...); diff != "" {
1324				t.Fatalf("routesProtoToSlice() returned unexpected diff (-got +want):\n%s", diff)
1325			}
1326		})
1327	}
1328}
1329
1330func (s) TestHashPoliciesProtoToSlice(t *testing.T) {
1331	tests := []struct {
1332		name             string
1333		hashPolicies     []*v3routepb.RouteAction_HashPolicy
1334		wantHashPolicies []*HashPolicy
1335		wantErr          bool
1336	}{
1337		// header-hash-policy tests a basic hash policy that specifies to hash a
1338		// certain header.
1339		{
1340			name: "header-hash-policy",
1341			hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1342				{
1343					PolicySpecifier: &v3routepb.RouteAction_HashPolicy_Header_{
1344						Header: &v3routepb.RouteAction_HashPolicy_Header{
1345							HeaderName: ":path",
1346							RegexRewrite: &v3matcherpb.RegexMatchAndSubstitute{
1347								Pattern:      &v3matcherpb.RegexMatcher{Regex: "/products"},
1348								Substitution: "/products",
1349							},
1350						},
1351					},
1352				},
1353			},
1354			wantHashPolicies: []*HashPolicy{
1355				{
1356					HashPolicyType:    HashPolicyTypeHeader,
1357					HeaderName:        ":path",
1358					Regex:             func() *regexp.Regexp { return regexp.MustCompile("/products") }(),
1359					RegexSubstitution: "/products",
1360				},
1361			},
1362		},
1363		// channel-id-hash-policy tests a basic hash policy that specifies to
1364		// hash a unique identifier of the channel.
1365		{
1366			name: "channel-id-hash-policy",
1367			hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1368				{PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "io.grpc.channel_id"}}},
1369			},
1370			wantHashPolicies: []*HashPolicy{
1371				{HashPolicyType: HashPolicyTypeChannelID},
1372			},
1373		},
1374		// unsupported-filter-state-key tests that an unsupported key in the
1375		// filter state hash policy are treated as a no-op.
1376		{
1377			name: "wrong-filter-state-key",
1378			hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1379				{PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "unsupported key"}}},
1380			},
1381		},
1382		// no-op-hash-policy tests that hash policies that are not supported by
1383		// grpc are treated as a no-op.
1384		{
1385			name: "no-op-hash-policy",
1386			hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1387				{PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{}},
1388			},
1389		},
1390		// header-and-channel-id-hash-policy test that a list of header and
1391		// channel id hash policies are successfully converted to an internal
1392		// struct.
1393		{
1394			name: "header-and-channel-id-hash-policy",
1395			hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1396				{
1397					PolicySpecifier: &v3routepb.RouteAction_HashPolicy_Header_{
1398						Header: &v3routepb.RouteAction_HashPolicy_Header{
1399							HeaderName: ":path",
1400							RegexRewrite: &v3matcherpb.RegexMatchAndSubstitute{
1401								Pattern:      &v3matcherpb.RegexMatcher{Regex: "/products"},
1402								Substitution: "/products",
1403							},
1404						},
1405					},
1406				},
1407				{
1408					PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "io.grpc.channel_id"}},
1409					Terminal:        true,
1410				},
1411			},
1412			wantHashPolicies: []*HashPolicy{
1413				{
1414					HashPolicyType:    HashPolicyTypeHeader,
1415					HeaderName:        ":path",
1416					Regex:             func() *regexp.Regexp { return regexp.MustCompile("/products") }(),
1417					RegexSubstitution: "/products",
1418				},
1419				{
1420					HashPolicyType: HashPolicyTypeChannelID,
1421					Terminal:       true,
1422				},
1423			},
1424		},
1425	}
1426
1427	oldRingHashSupport := env.RingHashSupport
1428	env.RingHashSupport = true
1429	defer func() { env.RingHashSupport = oldRingHashSupport }()
1430	for _, tt := range tests {
1431		t.Run(tt.name, func(t *testing.T) {
1432			got, err := hashPoliciesProtoToSlice(tt.hashPolicies, nil)
1433			if (err != nil) != tt.wantErr {
1434				t.Fatalf("hashPoliciesProtoToSlice() error = %v, wantErr %v", err, tt.wantErr)
1435			}
1436			if diff := cmp.Diff(got, tt.wantHashPolicies, cmp.AllowUnexported(regexp.Regexp{})); diff != "" {
1437				t.Fatalf("hashPoliciesProtoToSlice() returned unexpected diff (-got +want):\n%s", diff)
1438			}
1439		})
1440	}
1441}
1442
1443func newStringP(s string) *string {
1444	return &s
1445}
1446
1447func newUInt32P(i uint32) *uint32 {
1448	return &i
1449}
1450
1451func newBoolP(b bool) *bool {
1452	return &b
1453}
1454
1455func newDurationP(d time.Duration) *time.Duration {
1456	return &d
1457}
1458