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