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 client
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		disableFI  bool // disable fault injection
92	}{
93		{
94			name: "default-route-match-field-is-nil",
95			rc: &v3routepb.RouteConfiguration{
96				VirtualHosts: []*v3routepb.VirtualHost{
97					{
98						Domains: []string{ldsTarget},
99						Routes: []*v3routepb.Route{
100							{
101								Action: &v3routepb.Route_Route{
102									Route: &v3routepb.RouteAction{
103										ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
104									},
105								},
106							},
107						},
108					},
109				},
110			},
111			wantError: true,
112		},
113		{
114			name: "default-route-match-field-is-non-nil",
115			rc: &v3routepb.RouteConfiguration{
116				VirtualHosts: []*v3routepb.VirtualHost{
117					{
118						Domains: []string{ldsTarget},
119						Routes: []*v3routepb.Route{
120							{
121								Match:  &v3routepb.RouteMatch{},
122								Action: &v3routepb.Route_Route{},
123							},
124						},
125					},
126				},
127			},
128			wantError: true,
129		},
130		{
131			name: "default-route-routeaction-field-is-nil",
132			rc: &v3routepb.RouteConfiguration{
133				VirtualHosts: []*v3routepb.VirtualHost{
134					{
135						Domains: []string{ldsTarget},
136						Routes:  []*v3routepb.Route{{}},
137					},
138				},
139			},
140			wantError: true,
141		},
142		{
143			name: "default-route-cluster-field-is-empty",
144			rc: &v3routepb.RouteConfiguration{
145				VirtualHosts: []*v3routepb.VirtualHost{
146					{
147						Domains: []string{ldsTarget},
148						Routes: []*v3routepb.Route{
149							{
150								Action: &v3routepb.Route_Route{
151									Route: &v3routepb.RouteAction{
152										ClusterSpecifier: &v3routepb.RouteAction_ClusterHeader{},
153									},
154								},
155							},
156						},
157					},
158				},
159			},
160			wantError: true,
161		},
162		{
163			// default route's match sets case-sensitive to false.
164			name: "good-route-config-but-with-casesensitive-false",
165			rc: &v3routepb.RouteConfiguration{
166				Name: routeName,
167				VirtualHosts: []*v3routepb.VirtualHost{{
168					Domains: []string{ldsTarget},
169					Routes: []*v3routepb.Route{{
170						Match: &v3routepb.RouteMatch{
171							PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
172							CaseSensitive: &wrapperspb.BoolValue{Value: false},
173						},
174						Action: &v3routepb.Route_Route{
175							Route: &v3routepb.RouteAction{
176								ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
177							}}}}}}},
178			wantUpdate: RouteConfigUpdate{
179				VirtualHosts: []*VirtualHost{
180					{
181						Domains: []string{ldsTarget},
182						Routes:  []*Route{{Prefix: newStringP("/"), CaseInsensitive: true, WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}}}},
183					},
184				},
185			},
186		},
187		{
188			name: "good-route-config-with-empty-string-route",
189			rc: &v3routepb.RouteConfiguration{
190				Name: routeName,
191				VirtualHosts: []*v3routepb.VirtualHost{
192					{
193						Domains: []string{uninterestingDomain},
194						Routes: []*v3routepb.Route{
195							{
196								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
197								Action: &v3routepb.Route_Route{
198									Route: &v3routepb.RouteAction{
199										ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
200									},
201								},
202							},
203						},
204					},
205					{
206						Domains: []string{ldsTarget},
207						Routes: []*v3routepb.Route{
208							{
209								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
210								Action: &v3routepb.Route_Route{
211									Route: &v3routepb.RouteAction{
212										ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
213									},
214								},
215							},
216						},
217					},
218				},
219			},
220			wantUpdate: RouteConfigUpdate{
221				VirtualHosts: []*VirtualHost{
222					{
223						Domains: []string{uninterestingDomain},
224						Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
225					},
226					{
227						Domains: []string{ldsTarget},
228						Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}}}},
229					},
230				},
231			},
232		},
233		{
234			// default route's match is not empty string, but "/".
235			name: "good-route-config-with-slash-string-route",
236			rc: &v3routepb.RouteConfiguration{
237				Name: routeName,
238				VirtualHosts: []*v3routepb.VirtualHost{
239					{
240						Domains: []string{ldsTarget},
241						Routes: []*v3routepb.Route{
242							{
243								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
244								Action: &v3routepb.Route_Route{
245									Route: &v3routepb.RouteAction{
246										ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
247									},
248								},
249							},
250						},
251					},
252				},
253			},
254			wantUpdate: RouteConfigUpdate{
255				VirtualHosts: []*VirtualHost{
256					{
257						Domains: []string{ldsTarget},
258						Routes:  []*Route{{Prefix: newStringP("/"), WeightedClusters: map[string]WeightedCluster{clusterName: {Weight: 1}}}},
259					},
260				},
261			},
262		},
263		{
264			// weights not add up to total-weight.
265			name: "route-config-with-weighted_clusters_weights_not_add_up",
266			rc: &v3routepb.RouteConfiguration{
267				Name: routeName,
268				VirtualHosts: []*v3routepb.VirtualHost{
269					{
270						Domains: []string{ldsTarget},
271						Routes: []*v3routepb.Route{
272							{
273								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
274								Action: &v3routepb.Route_Route{
275									Route: &v3routepb.RouteAction{
276										ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
277											WeightedClusters: &v3routepb.WeightedCluster{
278												Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
279													{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 2}},
280													{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 3}},
281													{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 5}},
282												},
283												TotalWeight: &wrapperspb.UInt32Value{Value: 30},
284											},
285										},
286									},
287								},
288							},
289						},
290					},
291				},
292			},
293			wantError: true,
294		},
295		{
296			name: "good-route-config-with-weighted_clusters",
297			rc: &v3routepb.RouteConfiguration{
298				Name: routeName,
299				VirtualHosts: []*v3routepb.VirtualHost{
300					{
301						Domains: []string{ldsTarget},
302						Routes: []*v3routepb.Route{
303							{
304								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
305								Action: &v3routepb.Route_Route{
306									Route: &v3routepb.RouteAction{
307										ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
308											WeightedClusters: &v3routepb.WeightedCluster{
309												Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
310													{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 2}},
311													{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 3}},
312													{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 5}},
313												},
314												TotalWeight: &wrapperspb.UInt32Value{Value: 10},
315											},
316										},
317									},
318								},
319							},
320						},
321					},
322				},
323			},
324			wantUpdate: RouteConfigUpdate{
325				VirtualHosts: []*VirtualHost{
326					{
327						Domains: []string{ldsTarget},
328						Routes: []*Route{{
329							Prefix: newStringP("/"),
330							WeightedClusters: map[string]WeightedCluster{
331								"a": {Weight: 2},
332								"b": {Weight: 3},
333								"c": {Weight: 5},
334							},
335						}},
336					},
337				},
338			},
339		},
340		{
341			name: "good-route-config-with-max-stream-duration",
342			rc: &v3routepb.RouteConfiguration{
343				Name: routeName,
344				VirtualHosts: []*v3routepb.VirtualHost{
345					{
346						Domains: []string{ldsTarget},
347						Routes: []*v3routepb.Route{
348							{
349								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
350								Action: &v3routepb.Route_Route{
351									Route: &v3routepb.RouteAction{
352										ClusterSpecifier:  &v3routepb.RouteAction_Cluster{Cluster: clusterName},
353										MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{MaxStreamDuration: durationpb.New(time.Second)},
354									},
355								},
356							},
357						},
358					},
359				},
360			},
361			wantUpdate: RouteConfigUpdate{
362				VirtualHosts: []*VirtualHost{
363					{
364						Domains: []string{ldsTarget},
365						Routes: []*Route{{
366							Prefix:            newStringP("/"),
367							WeightedClusters:  map[string]WeightedCluster{clusterName: {Weight: 1}},
368							MaxStreamDuration: newDurationP(time.Second),
369						}},
370					},
371				},
372			},
373		},
374		{
375			name: "good-route-config-with-grpc-timeout-header-max",
376			rc: &v3routepb.RouteConfiguration{
377				Name: routeName,
378				VirtualHosts: []*v3routepb.VirtualHost{
379					{
380						Domains: []string{ldsTarget},
381						Routes: []*v3routepb.Route{
382							{
383								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
384								Action: &v3routepb.Route_Route{
385									Route: &v3routepb.RouteAction{
386										ClusterSpecifier:  &v3routepb.RouteAction_Cluster{Cluster: clusterName},
387										MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{GrpcTimeoutHeaderMax: durationpb.New(time.Second)},
388									},
389								},
390							},
391						},
392					},
393				},
394			},
395			wantUpdate: RouteConfigUpdate{
396				VirtualHosts: []*VirtualHost{
397					{
398						Domains: []string{ldsTarget},
399						Routes: []*Route{{
400							Prefix:            newStringP("/"),
401							WeightedClusters:  map[string]WeightedCluster{clusterName: {Weight: 1}},
402							MaxStreamDuration: newDurationP(time.Second),
403						}},
404					},
405				},
406			},
407		},
408		{
409			name: "good-route-config-with-both-timeouts",
410			rc: &v3routepb.RouteConfiguration{
411				Name: routeName,
412				VirtualHosts: []*v3routepb.VirtualHost{
413					{
414						Domains: []string{ldsTarget},
415						Routes: []*v3routepb.Route{
416							{
417								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
418								Action: &v3routepb.Route_Route{
419									Route: &v3routepb.RouteAction{
420										ClusterSpecifier:  &v3routepb.RouteAction_Cluster{Cluster: clusterName},
421										MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{MaxStreamDuration: durationpb.New(2 * time.Second), GrpcTimeoutHeaderMax: durationpb.New(0)},
422									},
423								},
424							},
425						},
426					},
427				},
428			},
429			wantUpdate: RouteConfigUpdate{
430				VirtualHosts: []*VirtualHost{
431					{
432						Domains: []string{ldsTarget},
433						Routes: []*Route{{
434							Prefix:            newStringP("/"),
435							WeightedClusters:  map[string]WeightedCluster{clusterName: {Weight: 1}},
436							MaxStreamDuration: newDurationP(0),
437						}},
438					},
439				},
440			},
441		},
442		{
443			name:       "good-route-config-with-http-filter-config",
444			rc:         goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": customFilterConfig}),
445			wantUpdate: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
446		},
447		{
448			name:       "good-route-config-with-http-filter-config-typed-struct",
449			rc:         goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedCustomFilterTypedStructConfig}),
450			wantUpdate: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterTypedStructConfig}}),
451		},
452		{
453			name:       "good-route-config-with-optional-http-filter-config",
454			rc:         goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("custom.filter")}),
455			wantUpdate: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
456		},
457		{
458			name:      "good-route-config-with-http-err-filter-config",
459			rc:        goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": errFilterConfig}),
460			wantError: true,
461		},
462		{
463			name:      "good-route-config-with-http-optional-err-filter-config",
464			rc:        goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("err.custom.filter")}),
465			wantError: true,
466		},
467		{
468			name:      "good-route-config-with-http-unknown-filter-config",
469			rc:        goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": unknownFilterConfig}),
470			wantError: true,
471		},
472		{
473			name:       "good-route-config-with-http-optional-unknown-filter-config",
474			rc:         goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("unknown.custom.filter")}),
475			wantUpdate: goodUpdateWithFilterConfigs(nil),
476		},
477		{
478			name:       "good-route-config-with-http-err-filter-config-fi-disabled",
479			disableFI:  true,
480			rc:         goodRouteConfigWithFilterConfigs(map[string]*anypb.Any{"foo": errFilterConfig}),
481			wantUpdate: goodUpdateWithFilterConfigs(nil),
482		},
483	}
484
485	for _, test := range tests {
486		t.Run(test.name, func(t *testing.T) {
487			oldFI := env.FaultInjectionSupport
488			env.FaultInjectionSupport = !test.disableFI
489
490			gotUpdate, gotError := generateRDSUpdateFromRouteConfiguration(test.rc, nil, false)
491			if (gotError != nil) != test.wantError ||
492				!cmp.Equal(gotUpdate, test.wantUpdate, cmpopts.EquateEmpty(),
493					cmp.Transformer("FilterConfig", func(fc httpfilter.FilterConfig) string {
494						return fmt.Sprint(fc)
495					})) {
496				t.Errorf("generateRDSUpdateFromRouteConfiguration(%+v, %v) returned unexpected, diff (-want +got):\\n%s", test.rc, ldsTarget, cmp.Diff(test.wantUpdate, gotUpdate, cmpopts.EquateEmpty()))
497
498				env.FaultInjectionSupport = oldFI
499			}
500		})
501	}
502}
503
504func (s) TestUnmarshalRouteConfig(t *testing.T) {
505	const (
506		ldsTarget                = "lds.target.good:1111"
507		uninterestingDomain      = "uninteresting.domain"
508		uninterestingClusterName = "uninterestingClusterName"
509		v2RouteConfigName        = "v2RouteConfig"
510		v3RouteConfigName        = "v3RouteConfig"
511		v2ClusterName            = "v2Cluster"
512		v3ClusterName            = "v3Cluster"
513	)
514
515	var (
516		v2VirtualHost = []*v2routepb.VirtualHost{
517			{
518				Domains: []string{uninterestingDomain},
519				Routes: []*v2routepb.Route{
520					{
521						Match: &v2routepb.RouteMatch{PathSpecifier: &v2routepb.RouteMatch_Prefix{Prefix: ""}},
522						Action: &v2routepb.Route_Route{
523							Route: &v2routepb.RouteAction{
524								ClusterSpecifier: &v2routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
525							},
526						},
527					},
528				},
529			},
530			{
531				Domains: []string{ldsTarget},
532				Routes: []*v2routepb.Route{
533					{
534						Match: &v2routepb.RouteMatch{PathSpecifier: &v2routepb.RouteMatch_Prefix{Prefix: ""}},
535						Action: &v2routepb.Route_Route{
536							Route: &v2routepb.RouteAction{
537								ClusterSpecifier: &v2routepb.RouteAction_Cluster{Cluster: v2ClusterName},
538							},
539						},
540					},
541				},
542			},
543		}
544		v2RouteConfig = testutils.MarshalAny(&v2xdspb.RouteConfiguration{
545			Name:         v2RouteConfigName,
546			VirtualHosts: v2VirtualHost,
547		})
548		v3VirtualHost = []*v3routepb.VirtualHost{
549			{
550				Domains: []string{uninterestingDomain},
551				Routes: []*v3routepb.Route{
552					{
553						Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
554						Action: &v3routepb.Route_Route{
555							Route: &v3routepb.RouteAction{
556								ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
557							},
558						},
559					},
560				},
561			},
562			{
563				Domains: []string{ldsTarget},
564				Routes: []*v3routepb.Route{
565					{
566						Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
567						Action: &v3routepb.Route_Route{
568							Route: &v3routepb.RouteAction{
569								ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: v3ClusterName},
570							},
571						},
572					},
573				},
574			},
575		}
576		v3RouteConfig = testutils.MarshalAny(&v3routepb.RouteConfiguration{
577			Name:         v3RouteConfigName,
578			VirtualHosts: v3VirtualHost,
579		})
580	)
581	const testVersion = "test-version-rds"
582
583	tests := []struct {
584		name       string
585		resources  []*anypb.Any
586		wantUpdate map[string]RouteConfigUpdate
587		wantMD     UpdateMetadata
588		wantErr    bool
589	}{
590		{
591			name:      "non-routeConfig resource type",
592			resources: []*anypb.Any{{TypeUrl: version.V3HTTPConnManagerURL}},
593			wantMD: UpdateMetadata{
594				Status:  ServiceStatusNACKed,
595				Version: testVersion,
596				ErrState: &UpdateErrorMetadata{
597					Version: testVersion,
598					Err:     errPlaceHolder,
599				},
600			},
601			wantErr: true,
602		},
603		{
604			name: "badly marshaled routeconfig resource",
605			resources: []*anypb.Any{
606				{
607					TypeUrl: version.V3RouteConfigURL,
608					Value:   []byte{1, 2, 3, 4},
609				},
610			},
611			wantMD: UpdateMetadata{
612				Status:  ServiceStatusNACKed,
613				Version: testVersion,
614				ErrState: &UpdateErrorMetadata{
615					Version: testVersion,
616					Err:     errPlaceHolder,
617				},
618			},
619			wantErr: true,
620		},
621		{
622			name: "empty resource list",
623			wantMD: UpdateMetadata{
624				Status:  ServiceStatusACKed,
625				Version: testVersion,
626			},
627		},
628		{
629			name:      "v2 routeConfig resource",
630			resources: []*anypb.Any{v2RouteConfig},
631			wantUpdate: map[string]RouteConfigUpdate{
632				v2RouteConfigName: {
633					VirtualHosts: []*VirtualHost{
634						{
635							Domains: []string{uninterestingDomain},
636							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
637						},
638						{
639							Domains: []string{ldsTarget},
640							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v2ClusterName: {Weight: 1}}}},
641						},
642					},
643					Raw: v2RouteConfig,
644				},
645			},
646			wantMD: UpdateMetadata{
647				Status:  ServiceStatusACKed,
648				Version: testVersion,
649			},
650		},
651		{
652			name:      "v3 routeConfig resource",
653			resources: []*anypb.Any{v3RouteConfig},
654			wantUpdate: map[string]RouteConfigUpdate{
655				v3RouteConfigName: {
656					VirtualHosts: []*VirtualHost{
657						{
658							Domains: []string{uninterestingDomain},
659							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
660						},
661						{
662							Domains: []string{ldsTarget},
663							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v3ClusterName: {Weight: 1}}}},
664						},
665					},
666					Raw: v3RouteConfig,
667				},
668			},
669			wantMD: UpdateMetadata{
670				Status:  ServiceStatusACKed,
671				Version: testVersion,
672			},
673		},
674		{
675			name:      "multiple routeConfig resources",
676			resources: []*anypb.Any{v2RouteConfig, v3RouteConfig},
677			wantUpdate: map[string]RouteConfigUpdate{
678				v3RouteConfigName: {
679					VirtualHosts: []*VirtualHost{
680						{
681							Domains: []string{uninterestingDomain},
682							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
683						},
684						{
685							Domains: []string{ldsTarget},
686							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v3ClusterName: {Weight: 1}}}},
687						},
688					},
689					Raw: v3RouteConfig,
690				},
691				v2RouteConfigName: {
692					VirtualHosts: []*VirtualHost{
693						{
694							Domains: []string{uninterestingDomain},
695							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
696						},
697						{
698							Domains: []string{ldsTarget},
699							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v2ClusterName: {Weight: 1}}}},
700						},
701					},
702					Raw: v2RouteConfig,
703				},
704			},
705			wantMD: UpdateMetadata{
706				Status:  ServiceStatusACKed,
707				Version: testVersion,
708			},
709		},
710		{
711			// To test that unmarshal keeps processing on errors.
712			name: "good and bad routeConfig resources",
713			resources: []*anypb.Any{
714				v2RouteConfig,
715				testutils.MarshalAny(&v3routepb.RouteConfiguration{
716					Name: "bad",
717					VirtualHosts: []*v3routepb.VirtualHost{
718						{Domains: []string{ldsTarget},
719							Routes: []*v3routepb.Route{{
720								Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_ConnectMatcher_{}},
721							}}}}}),
722				v3RouteConfig,
723			},
724			wantUpdate: map[string]RouteConfigUpdate{
725				v3RouteConfigName: {
726					VirtualHosts: []*VirtualHost{
727						{
728							Domains: []string{uninterestingDomain},
729							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
730						},
731						{
732							Domains: []string{ldsTarget},
733							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v3ClusterName: {Weight: 1}}}},
734						},
735					},
736					Raw: v3RouteConfig,
737				},
738				v2RouteConfigName: {
739					VirtualHosts: []*VirtualHost{
740						{
741							Domains: []string{uninterestingDomain},
742							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{uninterestingClusterName: {Weight: 1}}}},
743						},
744						{
745							Domains: []string{ldsTarget},
746							Routes:  []*Route{{Prefix: newStringP(""), WeightedClusters: map[string]WeightedCluster{v2ClusterName: {Weight: 1}}}},
747						},
748					},
749					Raw: v2RouteConfig,
750				},
751				"bad": {},
752			},
753			wantMD: UpdateMetadata{
754				Status:  ServiceStatusNACKed,
755				Version: testVersion,
756				ErrState: &UpdateErrorMetadata{
757					Version: testVersion,
758					Err:     errPlaceHolder,
759				},
760			},
761			wantErr: true,
762		},
763	}
764	for _, test := range tests {
765		t.Run(test.name, func(t *testing.T) {
766			update, md, err := UnmarshalRouteConfig(testVersion, test.resources, nil)
767			if (err != nil) != test.wantErr {
768				t.Fatalf("UnmarshalRouteConfig(), got err: %v, wantErr: %v", err, test.wantErr)
769			}
770			if diff := cmp.Diff(update, test.wantUpdate, cmpOpts); diff != "" {
771				t.Errorf("got unexpected update, diff (-got +want): %v", diff)
772			}
773			if diff := cmp.Diff(md, test.wantMD, cmpOptsIgnoreDetails); diff != "" {
774				t.Errorf("got unexpected metadata, diff (-got +want): %v", diff)
775			}
776		})
777	}
778}
779
780func (s) TestRoutesProtoToSlice(t *testing.T) {
781	var (
782		goodRouteWithFilterConfigs = func(cfgs map[string]*anypb.Any) []*v3routepb.Route {
783			// Sets per-filter config in cluster "B" and in the route.
784			return []*v3routepb.Route{{
785				Match: &v3routepb.RouteMatch{
786					PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
787					CaseSensitive: &wrapperspb.BoolValue{Value: false},
788				},
789				Action: &v3routepb.Route_Route{
790					Route: &v3routepb.RouteAction{
791						ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
792							WeightedClusters: &v3routepb.WeightedCluster{
793								Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
794									{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}, TypedPerFilterConfig: cfgs},
795									{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
796								},
797								TotalWeight: &wrapperspb.UInt32Value{Value: 100},
798							}}}},
799				TypedPerFilterConfig: cfgs,
800			}}
801		}
802		goodUpdateWithFilterConfigs = func(cfgs map[string]httpfilter.FilterConfig) []*Route {
803			// Sets per-filter config in cluster "B" and in the route.
804			return []*Route{{
805				Prefix:                   newStringP("/"),
806				CaseInsensitive:          true,
807				WeightedClusters:         map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60, HTTPFilterConfigOverride: cfgs}},
808				HTTPFilterConfigOverride: cfgs,
809			}}
810		}
811	)
812
813	tests := []struct {
814		name       string
815		routes     []*v3routepb.Route
816		wantRoutes []*Route
817		wantErr    bool
818		disableFI  bool // disable fault injection
819	}{
820		{
821			name: "no path",
822			routes: []*v3routepb.Route{{
823				Match: &v3routepb.RouteMatch{},
824			}},
825			wantErr: true,
826		},
827		{
828			name: "case_sensitive is false",
829			routes: []*v3routepb.Route{{
830				Match: &v3routepb.RouteMatch{
831					PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
832					CaseSensitive: &wrapperspb.BoolValue{Value: false},
833				},
834				Action: &v3routepb.Route_Route{
835					Route: &v3routepb.RouteAction{
836						ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
837							WeightedClusters: &v3routepb.WeightedCluster{
838								Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
839									{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
840									{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
841								},
842								TotalWeight: &wrapperspb.UInt32Value{Value: 100},
843							}}}},
844			}},
845			wantRoutes: []*Route{{
846				Prefix:           newStringP("/"),
847				CaseInsensitive:  true,
848				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
849			}},
850		},
851		{
852			name: "good",
853			routes: []*v3routepb.Route{
854				{
855					Match: &v3routepb.RouteMatch{
856						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
857						Headers: []*v3routepb.HeaderMatcher{
858							{
859								Name: "th",
860								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{
861									PrefixMatch: "tv",
862								},
863								InvertMatch: true,
864							},
865						},
866						RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
867							DefaultValue: &v3typepb.FractionalPercent{
868								Numerator:   1,
869								Denominator: v3typepb.FractionalPercent_HUNDRED,
870							},
871						},
872					},
873					Action: &v3routepb.Route_Route{
874						Route: &v3routepb.RouteAction{
875							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
876								WeightedClusters: &v3routepb.WeightedCluster{
877									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
878										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
879										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
880									},
881									TotalWeight: &wrapperspb.UInt32Value{Value: 100},
882								}}}},
883				},
884			},
885			wantRoutes: []*Route{{
886				Prefix: newStringP("/a/"),
887				Headers: []*HeaderMatcher{
888					{
889						Name:        "th",
890						InvertMatch: newBoolP(true),
891						PrefixMatch: newStringP("tv"),
892					},
893				},
894				Fraction:         newUInt32P(10000),
895				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
896			}},
897			wantErr: false,
898		},
899		{
900			name: "good with regex matchers",
901			routes: []*v3routepb.Route{
902				{
903					Match: &v3routepb.RouteMatch{
904						PathSpecifier: &v3routepb.RouteMatch_SafeRegex{SafeRegex: &v3matcherpb.RegexMatcher{Regex: "/a/"}},
905						Headers: []*v3routepb.HeaderMatcher{
906							{
907								Name:                 "th",
908								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: "tv"}},
909							},
910						},
911						RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
912							DefaultValue: &v3typepb.FractionalPercent{
913								Numerator:   1,
914								Denominator: v3typepb.FractionalPercent_HUNDRED,
915							},
916						},
917					},
918					Action: &v3routepb.Route_Route{
919						Route: &v3routepb.RouteAction{
920							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
921								WeightedClusters: &v3routepb.WeightedCluster{
922									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
923										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
924										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
925									},
926									TotalWeight: &wrapperspb.UInt32Value{Value: 100},
927								}}}},
928				},
929			},
930			wantRoutes: []*Route{{
931				Regex: func() *regexp.Regexp { return regexp.MustCompile("/a/") }(),
932				Headers: []*HeaderMatcher{
933					{
934						Name:        "th",
935						InvertMatch: newBoolP(false),
936						RegexMatch:  func() *regexp.Regexp { return regexp.MustCompile("tv") }(),
937					},
938				},
939				Fraction:         newUInt32P(10000),
940				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
941			}},
942			wantErr: false,
943		},
944		{
945			name: "query is ignored",
946			routes: []*v3routepb.Route{
947				{
948					Match: &v3routepb.RouteMatch{
949						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
950					},
951					Action: &v3routepb.Route_Route{
952						Route: &v3routepb.RouteAction{
953							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
954								WeightedClusters: &v3routepb.WeightedCluster{
955									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
956										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
957										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
958									},
959									TotalWeight: &wrapperspb.UInt32Value{Value: 100},
960								}}}},
961				},
962				{
963					Name: "with_query",
964					Match: &v3routepb.RouteMatch{
965						PathSpecifier:   &v3routepb.RouteMatch_Prefix{Prefix: "/b/"},
966						QueryParameters: []*v3routepb.QueryParameterMatcher{{Name: "route_will_be_ignored"}},
967					},
968				},
969			},
970			// Only one route in the result, because the second one with query
971			// parameters is ignored.
972			wantRoutes: []*Route{{
973				Prefix:           newStringP("/a/"),
974				WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
975			}},
976			wantErr: false,
977		},
978		{
979			name: "unrecognized path specifier",
980			routes: []*v3routepb.Route{
981				{
982					Match: &v3routepb.RouteMatch{
983						PathSpecifier: &v3routepb.RouteMatch_ConnectMatcher_{},
984					},
985				},
986			},
987			wantErr: true,
988		},
989		{
990			name: "bad regex in path specifier",
991			routes: []*v3routepb.Route{
992				{
993					Match: &v3routepb.RouteMatch{
994						PathSpecifier: &v3routepb.RouteMatch_SafeRegex{SafeRegex: &v3matcherpb.RegexMatcher{Regex: "??"}},
995						Headers: []*v3routepb.HeaderMatcher{
996							{
997								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "tv"},
998							},
999						},
1000					},
1001					Action: &v3routepb.Route_Route{
1002						Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName}},
1003					},
1004				},
1005			},
1006			wantErr: true,
1007		},
1008		{
1009			name: "bad regex in header specifier",
1010			routes: []*v3routepb.Route{
1011				{
1012					Match: &v3routepb.RouteMatch{
1013						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1014						Headers: []*v3routepb.HeaderMatcher{
1015							{
1016								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: "??"}},
1017							},
1018						},
1019					},
1020					Action: &v3routepb.Route_Route{
1021						Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName}},
1022					},
1023				},
1024			},
1025			wantErr: true,
1026		},
1027		{
1028			name: "unrecognized header match specifier",
1029			routes: []*v3routepb.Route{
1030				{
1031					Match: &v3routepb.RouteMatch{
1032						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1033						Headers: []*v3routepb.HeaderMatcher{
1034							{
1035								Name:                 "th",
1036								HeaderMatchSpecifier: &v3routepb.HeaderMatcher_HiddenEnvoyDeprecatedRegexMatch{},
1037							},
1038						},
1039					},
1040				},
1041			},
1042			wantErr: true,
1043		},
1044		{
1045			name: "no cluster in weighted clusters action",
1046			routes: []*v3routepb.Route{
1047				{
1048					Match: &v3routepb.RouteMatch{
1049						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1050					},
1051					Action: &v3routepb.Route_Route{
1052						Route: &v3routepb.RouteAction{
1053							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1054								WeightedClusters: &v3routepb.WeightedCluster{}}}},
1055				},
1056			},
1057			wantErr: true,
1058		},
1059		{
1060			name: "all 0-weight clusters in weighted clusters action",
1061			routes: []*v3routepb.Route{
1062				{
1063					Match: &v3routepb.RouteMatch{
1064						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1065					},
1066					Action: &v3routepb.Route_Route{
1067						Route: &v3routepb.RouteAction{
1068							ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1069								WeightedClusters: &v3routepb.WeightedCluster{
1070									Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1071										{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 0}},
1072										{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 0}},
1073									},
1074									TotalWeight: &wrapperspb.UInt32Value{Value: 0},
1075								}}}},
1076				},
1077			},
1078			wantErr: true,
1079		},
1080		{
1081			name:       "with custom HTTP filter config",
1082			routes:     goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": customFilterConfig}),
1083			wantRoutes: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
1084		},
1085		{
1086			name:       "with custom HTTP filter config in typed struct",
1087			routes:     goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedCustomFilterTypedStructConfig}),
1088			wantRoutes: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterTypedStructConfig}}),
1089		},
1090		{
1091			name:       "with optional custom HTTP filter config",
1092			routes:     goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("custom.filter")}),
1093			wantRoutes: goodUpdateWithFilterConfigs(map[string]httpfilter.FilterConfig{"foo": filterConfig{Override: customFilterConfig}}),
1094		},
1095		{
1096			name:       "with custom HTTP filter config, FI disabled",
1097			disableFI:  true,
1098			routes:     goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": customFilterConfig}),
1099			wantRoutes: goodUpdateWithFilterConfigs(nil),
1100		},
1101		{
1102			name:    "with erroring custom HTTP filter config",
1103			routes:  goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": errFilterConfig}),
1104			wantErr: true,
1105		},
1106		{
1107			name:    "with optional erroring custom HTTP filter config",
1108			routes:  goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("err.custom.filter")}),
1109			wantErr: true,
1110		},
1111		{
1112			name:       "with erroring custom HTTP filter config, FI disabled",
1113			disableFI:  true,
1114			routes:     goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": errFilterConfig}),
1115			wantRoutes: goodUpdateWithFilterConfigs(nil),
1116		},
1117		{
1118			name:    "with unknown custom HTTP filter config",
1119			routes:  goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": unknownFilterConfig}),
1120			wantErr: true,
1121		},
1122		{
1123			name:       "with optional unknown custom HTTP filter config",
1124			routes:     goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": wrappedOptionalFilter("unknown.custom.filter")}),
1125			wantRoutes: goodUpdateWithFilterConfigs(nil),
1126		},
1127	}
1128
1129	cmpOpts := []cmp.Option{
1130		cmp.AllowUnexported(Route{}, HeaderMatcher{}, Int64Range{}, regexp.Regexp{}),
1131		cmpopts.EquateEmpty(),
1132		cmp.Transformer("FilterConfig", func(fc httpfilter.FilterConfig) string {
1133			return fmt.Sprint(fc)
1134		}),
1135	}
1136
1137	for _, tt := range tests {
1138		t.Run(tt.name, func(t *testing.T) {
1139			oldFI := env.FaultInjectionSupport
1140			env.FaultInjectionSupport = !tt.disableFI
1141			defer func() { env.FaultInjectionSupport = oldFI }()
1142
1143			got, err := routesProtoToSlice(tt.routes, nil, false)
1144			if (err != nil) != tt.wantErr {
1145				t.Fatalf("routesProtoToSlice() error = %v, wantErr %v", err, tt.wantErr)
1146			}
1147			if diff := cmp.Diff(got, tt.wantRoutes, cmpOpts...); diff != "" {
1148				t.Fatalf("routesProtoToSlice() returned unexpected diff (-got +want):\n%s", diff)
1149			}
1150		})
1151	}
1152}
1153
1154func newStringP(s string) *string {
1155	return &s
1156}
1157
1158func newUInt32P(i uint32) *uint32 {
1159	return &i
1160}
1161
1162func newBoolP(b bool) *bool {
1163	return &b
1164}
1165
1166func newDurationP(d time.Duration) *time.Duration {
1167	return &d
1168}
1169