...

Source file src/google.golang.org/grpc/xds/internal/xdsclient/xdsresource/unmarshal_rds_test.go

Documentation: google.golang.org/grpc/xds/internal/xdsclient/xdsresource

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

View as plain text