gitee.com/ks-custle/core-gm@v0.0.0-20230922171213-b83bdd97b62c/grpc/xds/internal/xdsclient/xdsresource/unmarshal_rds_test.go (about)

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