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 }