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