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