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