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