google.golang.org/grpc@v1.74.2/xds/internal/resolver/xds_resolver_test.go (about) 1 /* 2 * 3 * Copyright 2019 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 19 package resolver_test 20 21 import ( 22 "context" 23 "encoding/json" 24 "fmt" 25 "strings" 26 "sync" 27 "testing" 28 "time" 29 30 xxhash "github.com/cespare/xxhash/v2" 31 "github.com/envoyproxy/go-control-plane/pkg/wellknown" 32 "github.com/google/go-cmp/cmp" 33 "github.com/google/uuid" 34 "google.golang.org/grpc/codes" 35 estats "google.golang.org/grpc/experimental/stats" 36 "google.golang.org/grpc/internal" 37 iresolver "google.golang.org/grpc/internal/resolver" 38 iringhash "google.golang.org/grpc/internal/ringhash" 39 "google.golang.org/grpc/internal/testutils" 40 "google.golang.org/grpc/internal/testutils/xds/e2e" 41 "google.golang.org/grpc/internal/xds/bootstrap" 42 "google.golang.org/grpc/metadata" 43 "google.golang.org/grpc/resolver" 44 "google.golang.org/grpc/serviceconfig" 45 "google.golang.org/grpc/xds/internal/balancer/clustermanager" 46 "google.golang.org/grpc/xds/internal/httpfilter" 47 rinternal "google.golang.org/grpc/xds/internal/resolver/internal" 48 "google.golang.org/grpc/xds/internal/xdsclient" 49 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version" 50 "google.golang.org/protobuf/proto" 51 "google.golang.org/protobuf/types/known/anypb" 52 "google.golang.org/protobuf/types/known/durationpb" 53 "google.golang.org/protobuf/types/known/structpb" 54 "google.golang.org/protobuf/types/known/wrapperspb" 55 56 v3xdsxdstypepb "github.com/cncf/xds/go/xds/type/v3" 57 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 58 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 59 v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 60 v3routerpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" 61 v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 62 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 63 64 _ "google.golang.org/grpc/xds/internal/balancer/cdsbalancer" // Register the cds LB policy 65 _ "google.golang.org/grpc/xds/internal/httpfilter/router" // Register the router filter 66 ) 67 68 // Tests the case where xDS client creation is expected to fail because the 69 // bootstrap configuration for the xDS client pool is not specified. The test 70 // verifies that xDS resolver build fails as well. 71 func (s) TestResolverBuilder_ClientCreationFails_NoBootstrap(t *testing.T) { 72 // Build an xDS resolver specifying nil for bootstrap configuration for the 73 // xDS client pool. 74 pool := xdsclient.NewPool(nil) 75 var xdsResolver resolver.Builder 76 if newResolver := internal.NewXDSResolverWithPoolForTesting; newResolver != nil { 77 var err error 78 xdsResolver, err = newResolver.(func(*xdsclient.Pool) (resolver.Builder, error))(pool) 79 if err != nil { 80 t.Fatalf("Failed to create xDS resolver for testing: %v", err) 81 } 82 } 83 84 target := resolver.Target{URL: *testutils.MustParseURL("xds:///target")} 85 if _, err := xdsResolver.Build(target, nil, resolver.BuildOptions{}); err == nil { 86 t.Fatalf("xds Resolver Build(%v) succeeded when expected to fail, because there is no bootstrap configuration for the xDS client pool", pool) 87 } 88 } 89 90 // Tests the case where the specified dial target contains an authority that is 91 // not specified in the bootstrap file. Verifies that the resolver.Build method 92 // fails with the expected error string. 93 func (s) TestResolverBuilder_AuthorityNotDefinedInBootstrap(t *testing.T) { 94 contents := e2e.DefaultBootstrapContents(t, "node-id", "dummy-management-server") 95 96 // Create an xDS resolver with the above bootstrap configuration. 97 if internal.NewXDSResolverWithConfigForTesting == nil { 98 t.Fatalf("internal.NewXDSResolverWithConfigForTesting is nil") 99 } 100 xdsResolver, err := internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(contents) 101 if err != nil { 102 t.Fatalf("Failed to create xDS resolver for testing: %v", err) 103 } 104 105 target := resolver.Target{URL: *testutils.MustParseURL("xds://non-existing-authority/target")} 106 const wantErr = `authority "non-existing-authority" specified in dial target "xds://non-existing-authority/target" is not found in the bootstrap file` 107 r, err := xdsResolver.Build(target, &testutils.ResolverClientConn{Logger: t}, resolver.BuildOptions{}) 108 if r != nil { 109 r.Close() 110 } 111 if err == nil { 112 t.Fatalf("xds Resolver Build(%v) succeeded for target with authority not specified in bootstrap", target) 113 } 114 if !strings.Contains(err.Error(), wantErr) { 115 t.Fatalf("xds Resolver Build(%v) returned err: %v, wantErr: %v", target, err, wantErr) 116 } 117 } 118 119 // Test builds an xDS resolver and verifies that the resource name specified in 120 // the discovery request matches expectations. 121 func (s) TestResolverResourceName(t *testing.T) { 122 tests := []struct { 123 name string 124 listenerResourceNameTemplate string 125 extraAuthority string 126 dialTarget string 127 wantResourceNames []string 128 }{ 129 { 130 name: "default %s old style", 131 listenerResourceNameTemplate: "%s", 132 dialTarget: "xds:///target", 133 wantResourceNames: []string{"target"}, 134 }, 135 { 136 name: "old style no percent encoding", 137 listenerResourceNameTemplate: "/path/to/%s", 138 dialTarget: "xds:///target", 139 wantResourceNames: []string{"/path/to/target"}, 140 }, 141 { 142 name: "new style with %s", 143 listenerResourceNameTemplate: "xdstp://authority.com/%s", 144 dialTarget: "xds:///0.0.0.0:8080", 145 wantResourceNames: []string{"xdstp://authority.com/0.0.0.0:8080"}, 146 }, 147 { 148 name: "new style percent encoding", 149 listenerResourceNameTemplate: "xdstp://authority.com/%s", 150 dialTarget: "xds:///[::1]:8080", 151 wantResourceNames: []string{"xdstp://authority.com/%5B::1%5D:8080"}, 152 }, 153 { 154 name: "new style different authority", 155 listenerResourceNameTemplate: "xdstp://authority.com/%s", 156 extraAuthority: "test-authority", 157 dialTarget: "xds://test-authority/target", 158 wantResourceNames: []string{"xdstp://test-authority/envoy.config.listener.v3.Listener/target"}, 159 }, 160 } 161 for _, tt := range tests { 162 t.Run(tt.name, func(t *testing.T) { 163 // Spin up an xDS management server for the test. 164 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 165 defer cancel() 166 nodeID := uuid.New().String() 167 mgmtServer, lisCh, _, _ := setupManagementServerForTest(t, nodeID) 168 169 // Create a bootstrap configuration with test options. 170 opts := bootstrap.ConfigOptionsForTesting{ 171 Servers: []byte(fmt.Sprintf(`[{ 172 "server_uri": %q, 173 "channel_creds": [{"type": "insecure"}] 174 }]`, mgmtServer.Address)), 175 ClientDefaultListenerResourceNameTemplate: tt.listenerResourceNameTemplate, 176 Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)), 177 } 178 if tt.extraAuthority != "" { 179 // In this test, we really don't care about having multiple 180 // management servers. All we need to verify is whether the 181 // resource name matches expectation. 182 opts.Authorities = map[string]json.RawMessage{ 183 tt.extraAuthority: []byte(fmt.Sprintf(`{ 184 "server_uri": %q, 185 "channel_creds": [{"type": "insecure"}] 186 }`, mgmtServer.Address)), 187 } 188 } 189 contents, err := bootstrap.NewContentsForTesting(opts) 190 if err != nil { 191 t.Fatalf("Failed to create bootstrap configuration: %v", err) 192 } 193 194 buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL(tt.dialTarget)}, contents) 195 waitForResourceNames(ctx, t, lisCh, tt.wantResourceNames) 196 }) 197 } 198 } 199 200 // Tests the case where a service update from the underlying xDS client is 201 // received after the resolver is closed, and verifies that the update is not 202 // propagated to the ClientConn. 203 func (s) TestResolverWatchCallbackAfterClose(t *testing.T) { 204 // Setup the management server that synchronizes with the test goroutine 205 // using two channels. The management server signals the test goroutine when 206 // it receives a discovery request for a route configuration resource. And 207 // the test goroutine signals the management server when the resolver is 208 // closed. 209 routeConfigResourceNamesCh := make(chan []string, 1) 210 waitForResolverCloseCh := make(chan struct{}) 211 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ 212 OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { 213 if req.GetTypeUrl() == version.V3RouteConfigURL { 214 select { 215 case <-routeConfigResourceNamesCh: 216 default: 217 } 218 select { 219 case routeConfigResourceNamesCh <- req.GetResourceNames(): 220 default: 221 } 222 <-waitForResolverCloseCh 223 } 224 return nil 225 }, 226 }) 227 228 // Create a bootstrap configuration specifying the above management server. 229 nodeID := uuid.New().String() 230 contents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 231 232 // Configure resources on the management server. 233 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 234 routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(defaultTestRouteConfigName, defaultTestServiceName, defaultTestClusterName)} 235 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 236 defer cancel() 237 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 238 239 // Wait for a discovery request for a route configuration resource. 240 stateCh, _, r := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, contents) 241 waitForResourceNames(ctx, t, routeConfigResourceNamesCh, []string{defaultTestRouteConfigName}) 242 243 // Close the resolver and unblock the management server. 244 r.Close() 245 close(waitForResolverCloseCh) 246 247 // Verify that the update from the management server is not propagated to 248 // the ClientConn. The xDS resolver, once closed, is expected to drop 249 // updates from the xDS client. 250 verifyNoUpdateFromResolver(ctx, t, stateCh) 251 } 252 253 // Tests that the xDS resolver's Close method closes the xDS client. 254 func (s) TestResolverCloseClosesXDSClient(t *testing.T) { 255 // Override xDS client creation to use bootstrap configuration pointing to a 256 // dummy management server. Also close a channel when the returned xDS 257 // client is closed. 258 origNewClient := rinternal.NewXDSClient 259 closeCh := make(chan struct{}) 260 rinternal.NewXDSClient = func(string, estats.MetricsRecorder) (xdsclient.XDSClient, func(), error) { 261 bc := e2e.DefaultBootstrapContents(t, uuid.New().String(), "dummy-management-server-address") 262 config, err := bootstrap.NewConfigFromContents(bc) 263 if err != nil { 264 t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) 265 } 266 pool := xdsclient.NewPool(config) 267 if err != nil { 268 t.Fatalf("Failed to create an xDS client pool: %v", err) 269 } 270 c, cancel, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ 271 Name: t.Name(), 272 WatchExpiryTimeout: defaultTestTimeout, 273 }) 274 return c, sync.OnceFunc(func() { 275 close(closeCh) 276 cancel() 277 }), err 278 } 279 defer func() { rinternal.NewXDSClient = origNewClient }() 280 281 _, _, r := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///my-service-client-side-xds")}, nil) 282 r.Close() 283 284 select { 285 case <-closeCh: 286 case <-time.After(defaultTestTimeout): 287 t.Fatal("Timeout when waiting for xDS client to be closed") 288 } 289 } 290 291 // Tests the case where a resource, not present in cache, returned by the 292 // management server is NACKed by the xDS client, which then returns an update 293 // containing a resource error to the resolver. It tests the case where the 294 // resolver gets an error update without any previous good update. The test 295 // also verifies that these are propagated to the ClientConn. 296 func (s) TestResolverBadServiceUpdate_NACKedWithoutCache(t *testing.T) { 297 // Spin up an xDS management server for the test. 298 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 299 defer cancel() 300 nodeID := uuid.New().String() 301 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 302 303 // Configure a listener resource that is expected to be NACKed because it 304 // does not contain the `RouteSpecifier` field in the HTTPConnectionManager. 305 hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 306 HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})}, 307 }) 308 lis := &v3listenerpb.Listener{ 309 Name: defaultTestServiceName, 310 ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm}, 311 FilterChains: []*v3listenerpb.FilterChain{{ 312 Name: "filter-chain-name", 313 Filters: []*v3listenerpb.Filter{{ 314 Name: wellknown.HTTPConnectionManager, 315 ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm}, 316 }}, 317 }}, 318 } 319 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, []*v3listenerpb.Listener{lis}, nil) 320 321 // Build the resolver and expect an error update from it. Since the 322 // resource is not cached, it should be received as resource error. 323 _, errCh, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 324 if err := waitForErrorFromResolver(ctx, errCh, "no RouteSpecifier", nodeID); err != nil { 325 t.Fatal(err) 326 } 327 } 328 329 // Tests the case where a resource, present in cache, returned by the 330 // management server is NACKed by the xDS client, which then returns 331 // an update containing an ambient error to the resolver. Verifies that the 332 // update is propagated to the ClientConn by the resolver. It tests the 333 // case where the resolver gets a good update first, and an error 334 // after the good update. The test also verifies that these are propagated to 335 // the ClientConn and that RPC succeeds as expected after receiving good update 336 // as well as ambient error. 337 func (s) TestResolverBadServiceUpdate_NACKedWithCache(t *testing.T) { 338 // Spin up an xDS management server for the test. 339 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 340 defer cancel() 341 nodeID := uuid.New().String() 342 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 343 344 stateCh, errCh, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 345 346 // Configure good listener and route configuration resources on the 347 // management server. 348 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 349 routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(defaultTestRouteConfigName, defaultTestServiceName, defaultTestClusterName)} 350 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 351 352 // Expect a good update from the resolver. 353 cs := verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 354 355 // "Make an RPC" by invoking the config selector. 356 _, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 357 if err != nil { 358 t.Fatalf("cs.SelectConfig(): %v", err) 359 } 360 361 // Configure a listener resource that is expected to be NACKed because it 362 // does not contain the `RouteSpecifier` field in the HTTPConnectionManager. 363 hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 364 HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})}, 365 }) 366 lis := &v3listenerpb.Listener{ 367 Name: defaultTestServiceName, 368 ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm}, 369 FilterChains: []*v3listenerpb.FilterChain{{ 370 Name: "filter-chain-name", 371 Filters: []*v3listenerpb.Filter{{ 372 Name: wellknown.HTTPConnectionManager, 373 ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm}, 374 }}, 375 }}, 376 } 377 378 // Expect an error update from the resolver. Since the resource is cached, 379 // it should be received as an ambient error. 380 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, []*v3listenerpb.Listener{lis}, nil) 381 if err := waitForErrorFromResolver(ctx, errCh, "no RouteSpecifier", nodeID); err != nil { 382 t.Fatal(err) 383 } 384 385 // "Make an RPC" by invoking the config selector which should succeed by 386 // continuing to use the previously cached resource. 387 _, err = cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 388 if err != nil { 389 t.Fatalf("cs.SelectConfig(): %v", err) 390 } 391 } 392 393 // TestResolverGoodServiceUpdate tests the case where the resource returned by 394 // the management server is ACKed by the xDS client, which then returns a good 395 // service update to the resolver. The test verifies that the service config 396 // returned by the resolver matches expectations, and that the config selector 397 // returned by the resolver picks clusters based on the route configuration 398 // received from the management server. 399 func (s) TestResolverGoodServiceUpdate(t *testing.T) { 400 for _, tt := range []struct { 401 name string 402 routeConfig *v3routepb.RouteConfiguration 403 wantServiceConfig string 404 wantClusters map[string]bool 405 }{ 406 { 407 name: "single cluster", 408 routeConfig: e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{ 409 RouteConfigName: defaultTestRouteConfigName, 410 ListenerName: defaultTestServiceName, 411 ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeCluster, 412 ClusterName: defaultTestClusterName, 413 }), 414 wantServiceConfig: wantDefaultServiceConfig, 415 wantClusters: map[string]bool{fmt.Sprintf("cluster:%s", defaultTestClusterName): true}, 416 }, 417 { 418 name: "two clusters", 419 routeConfig: e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{ 420 RouteConfigName: defaultTestRouteConfigName, 421 ListenerName: defaultTestServiceName, 422 ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeWeightedCluster, 423 WeightedClusters: map[string]int{"cluster_1": 75, "cluster_2": 25}, 424 }), 425 // This update contains the cluster from the previous update as well 426 // as this update, as the previous config selector still references 427 // the old cluster when the new one is pushed. 428 wantServiceConfig: `{ 429 "loadBalancingConfig": [{ 430 "xds_cluster_manager_experimental": { 431 "children": { 432 "cluster:cluster_1": { 433 "childPolicy": [{ 434 "cds_experimental": { 435 "cluster": "cluster_1" 436 } 437 }] 438 }, 439 "cluster:cluster_2": { 440 "childPolicy": [{ 441 "cds_experimental": { 442 "cluster": "cluster_2" 443 } 444 }] 445 } 446 } 447 } 448 }]}`, 449 wantClusters: map[string]bool{"cluster:cluster_1": true, "cluster:cluster_2": true}, 450 }, 451 } { 452 t.Run(tt.name, func(t *testing.T) { 453 // Spin up an xDS management server for the test. 454 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 455 defer cancel() 456 nodeID := uuid.New().String() 457 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 458 459 // Configure the management server with a good listener resource and a 460 // route configuration resource, as specified by the test case. 461 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 462 routes := []*v3routepb.RouteConfiguration{tt.routeConfig} 463 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 464 465 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 466 467 // Read the update pushed by the resolver to the ClientConn. 468 cs := verifyUpdateFromResolver(ctx, t, stateCh, tt.wantServiceConfig) 469 470 pickedClusters := make(map[string]bool) 471 // Odds of picking 75% cluster 100 times in a row: 1 in 3E-13. And 472 // with the random number generator stubbed out, we can rely on this 473 // to be 100% reproducible. 474 for i := 0; i < 100; i++ { 475 res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 476 if err != nil { 477 t.Fatalf("cs.SelectConfig(): %v", err) 478 } 479 cluster := clustermanager.GetPickedClusterForTesting(res.Context) 480 pickedClusters[cluster] = true 481 res.OnCommitted() 482 } 483 if !cmp.Equal(pickedClusters, tt.wantClusters) { 484 t.Errorf("Picked clusters: %v; want: %v", pickedClusters, tt.wantClusters) 485 } 486 }) 487 } 488 } 489 490 // Tests a case where a resolver receives a RouteConfig update with a HashPolicy 491 // specifying to generate a hash. The configSelector generated should 492 // successfully generate a Hash. 493 func (s) TestResolverRequestHash(t *testing.T) { 494 // Spin up an xDS management server for the test. 495 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 496 defer cancel() 497 nodeID := uuid.New().String() 498 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 499 500 // Configure the management server with a good listener resource and a 501 // route configuration resource that specifies a hash policy. 502 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 503 routes := []*v3routepb.RouteConfiguration{{ 504 Name: defaultTestRouteConfigName, 505 VirtualHosts: []*v3routepb.VirtualHost{{ 506 Domains: []string{defaultTestServiceName}, 507 Routes: []*v3routepb.Route{{ 508 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}}, 509 Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ 510 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{WeightedClusters: &v3routepb.WeightedCluster{ 511 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 512 { 513 Name: defaultTestClusterName, 514 Weight: &wrapperspb.UInt32Value{Value: 100}, 515 }, 516 }, 517 }}, 518 HashPolicy: []*v3routepb.RouteAction_HashPolicy{{ 519 PolicySpecifier: &v3routepb.RouteAction_HashPolicy_Header_{ 520 Header: &v3routepb.RouteAction_HashPolicy_Header{ 521 HeaderName: ":path", 522 }, 523 }, 524 Terminal: true, 525 }}, 526 }}, 527 }}, 528 }}, 529 }} 530 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 531 532 // Build the resolver and read the config selector out of it. 533 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 534 cs := verifyUpdateFromResolver(ctx, t, stateCh, "") 535 536 // Selecting a config when there was a hash policy specified in the route 537 // that will be selected should put a request hash in the config's context. 538 res, err := cs.SelectConfig(iresolver.RPCInfo{ 539 Context: metadata.NewOutgoingContext(ctx, metadata.Pairs(":path", "/products")), 540 Method: "/service/method", 541 }) 542 if err != nil { 543 t.Fatalf("cs.SelectConfig(): %v", err) 544 } 545 wantHash := xxhash.Sum64String("/products") 546 gotHash, ok := iringhash.XDSRequestHash(res.Context) 547 if !ok { 548 t.Fatalf("Got no request hash, want: %v", wantHash) 549 } 550 if gotHash != wantHash { 551 t.Fatalf("Got request hash: %v, want: %v", gotHash, wantHash) 552 } 553 } 554 555 // Tests the case where resources are removed from the management server, 556 // causing it to send an empty update to the xDS client, which returns a 557 // resource-not-found error to the xDS resolver. The test verifies that an 558 // ongoing RPC is handled to completion when this happens. 559 func (s) TestResolverRemovedWithRPCs(t *testing.T) { 560 // Spin up an xDS management server for the test. 561 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 562 defer cancel() 563 nodeID := uuid.New().String() 564 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 565 566 // Configure resources on the management server. 567 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 568 routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(defaultTestRouteConfigName, defaultTestServiceName, defaultTestClusterName)} 569 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 570 571 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 572 573 // Read the update pushed by the resolver to the ClientConn. 574 cs := verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 575 576 res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 577 if err != nil { 578 t.Fatalf("cs.SelectConfig(): %v", err) 579 } 580 581 // Delete the resources on the management server. This should result in a 582 // resource-not-found error from the xDS client. 583 if err := mgmtServer.Update(ctx, e2e.UpdateOptions{NodeID: nodeID}); err != nil { 584 t.Fatal(err) 585 } 586 587 // The RPC started earlier is still in progress. So, the xDS resolver will 588 // not produce an empty service config at this point. Instead it will retain 589 // the cluster to which the RPC is ongoing in the service config, but will 590 // return an erroring config selector which will fail new RPCs. 591 cs = verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 592 _, err = cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 593 if err := verifyResolverError(err, codes.Unavailable, "has been removed", nodeID); err != nil { 594 t.Fatal(err) 595 } 596 597 // "Finish the RPC"; this could cause a panic if the resolver doesn't 598 // handle it correctly. 599 res.OnCommitted() 600 601 // Now that the RPC is committed, the xDS resolver is expected to send an 602 // update with an empty service config. 603 var state resolver.State 604 select { 605 case <-ctx.Done(): 606 t.Fatalf("Timeout waiting for an update from the resolver: %v", ctx.Err()) 607 case state = <-stateCh: 608 if err := state.ServiceConfig.Err; err != nil { 609 t.Fatalf("Received error in service config: %v", state.ServiceConfig.Err) 610 } 611 wantSCParsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)("{}") 612 if !internal.EqualServiceConfigForTesting(state.ServiceConfig.Config, wantSCParsed.Config) { 613 t.Fatalf("Got service config:\n%s \nWant service config:\n%s", cmp.Diff(nil, state.ServiceConfig.Config), cmp.Diff(nil, wantSCParsed.Config)) 614 } 615 } 616 617 // Workaround for https://github.com/envoyproxy/go-control-plane/issues/431. 618 // 619 // The xDS client can miss route configurations due to a race condition 620 // between resource removal and re-addition. To avoid this, continuously 621 // push new versions of the resources to the server, ensuring the client 622 // eventually receives the configuration. 623 // 624 // TODO(https://github.com/grpc/grpc-go/issues/7807): Remove this workaround 625 // once the issue is fixed. 626 waitForStateUpdate: 627 for { 628 sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) 629 defer sCancel() 630 631 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 632 633 select { 634 case state = <-stateCh: 635 if err := state.ServiceConfig.Err; err != nil { 636 t.Fatalf("Received error in service config: %v", state.ServiceConfig.Err) 637 } 638 wantSCParsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(wantDefaultServiceConfig) 639 if !internal.EqualServiceConfigForTesting(state.ServiceConfig.Config, wantSCParsed.Config) { 640 t.Fatalf("Got service config:\n%s \nWant service config:\n%s", cmp.Diff(nil, state.ServiceConfig.Config), cmp.Diff(nil, wantSCParsed.Config)) 641 } 642 break waitForStateUpdate 643 case <-sCtx.Done(): 644 } 645 } 646 cs = iresolver.GetConfigSelector(state) 647 if cs == nil { 648 t.Fatal("Received nil config selector in update from resolver") 649 } 650 651 res, err = cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 652 if err != nil { 653 t.Fatalf("cs.SelectConfig(): %v", err) 654 } 655 res.OnCommitted() 656 } 657 658 // Tests the case where resources returned by the management server are removed. 659 // The test verifies that the resolver pushes the expected config selector and 660 // service config in this case. 661 func (s) TestResolverRemovedResource(t *testing.T) { 662 // Spin up an xDS management server for the test. 663 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 664 defer cancel() 665 nodeID := uuid.New().String() 666 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 667 668 // Configure resources on the management server. 669 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 670 routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(defaultTestRouteConfigName, defaultTestServiceName, defaultTestClusterName)} 671 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 672 673 stateCh, errCh, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 674 675 // Read the update pushed by the resolver to the ClientConn. 676 cs := verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 677 678 // "Make an RPC" by invoking the config selector. 679 res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 680 if err != nil { 681 t.Fatalf("cs.SelectConfig(): %v", err) 682 } 683 684 // "Finish the RPC"; this could cause a panic if the resolver doesn't 685 // handle it correctly. 686 res.OnCommitted() 687 688 // Delete the resources on the management server, resulting in a 689 // resource-not-found error from the xDS client. 690 if err := mgmtServer.Update(ctx, e2e.UpdateOptions{NodeID: nodeID}); err != nil { 691 t.Fatal(err) 692 } 693 694 // The channel should receive the existing service config with the original 695 // cluster but with an erroring config selector. 696 cs = verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 697 698 // "Make another RPC" by invoking the config selector. 699 _, err = cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 700 if err := verifyResolverError(err, codes.Unavailable, "has been removed", nodeID); err != nil { 701 t.Fatal(err) 702 } 703 704 // In the meantime, an empty ServiceConfig update should have been sent. 705 var state resolver.State 706 select { 707 case <-ctx.Done(): 708 t.Fatalf("Timeout waiting for an update from the resolver: %v", ctx.Err()) 709 case state = <-stateCh: 710 if err := state.ServiceConfig.Err; err != nil { 711 t.Fatalf("Received error in service config: %v", state.ServiceConfig.Err) 712 } 713 wantSCParsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)("{}") 714 if !internal.EqualServiceConfigForTesting(state.ServiceConfig.Config, wantSCParsed.Config) { 715 t.Fatalf("Got service config:\n%s \nWant service config:\n%s", cmp.Diff(nil, state.ServiceConfig.Config), cmp.Diff(nil, wantSCParsed.Config)) 716 } 717 } 718 719 // The xDS resolver is expected to report an error to the channel. 720 select { 721 case <-ctx.Done(): 722 t.Fatalf("Timeout waiting for an error from the resolver: %v", ctx.Err()) 723 case err := <-errCh: 724 if err := verifyResolverError(err, codes.Unavailable, "has been removed", nodeID); err != nil { 725 t.Fatal(err) 726 } 727 } 728 } 729 730 // Tests the case where the resolver receives max stream duration as part of the 731 // listener and route configuration resources. The test verifies that the RPC 732 // timeout returned by the config selector matches expectations. A non-nil max 733 // stream duration (this includes an explicit zero value) in a matching route 734 // overrides the value specified in the listener resource. 735 func (s) TestResolverMaxStreamDuration(t *testing.T) { 736 // Spin up an xDS management server for the test. 737 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 738 defer cancel() 739 nodeID := uuid.New().String() 740 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 741 742 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 743 744 // Configure the management server with a listener resource that specifies a 745 // max stream duration as part of its HTTP connection manager. Also 746 // configure a route configuration resource, which has multiple routes with 747 // different values of max stream duration. 748 hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 749 RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{Rds: &v3httppb.Rds{ 750 ConfigSource: &v3corepb.ConfigSource{ 751 ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}}, 752 }, 753 RouteConfigName: defaultTestRouteConfigName, 754 }}, 755 HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter}, 756 CommonHttpProtocolOptions: &v3corepb.HttpProtocolOptions{ 757 MaxStreamDuration: durationpb.New(1 * time.Second), 758 }, 759 }) 760 listeners := []*v3listenerpb.Listener{{ 761 Name: defaultTestServiceName, 762 ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm}, 763 FilterChains: []*v3listenerpb.FilterChain{{ 764 Name: "filter-chain-name", 765 Filters: []*v3listenerpb.Filter{{ 766 Name: wellknown.HTTPConnectionManager, 767 ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm}, 768 }}, 769 }}, 770 }} 771 routes := []*v3routepb.RouteConfiguration{{ 772 Name: defaultTestRouteConfigName, 773 VirtualHosts: []*v3routepb.VirtualHost{{ 774 Domains: []string{defaultTestServiceName}, 775 Routes: []*v3routepb.Route{ 776 { 777 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/foo"}}, 778 Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ 779 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{WeightedClusters: &v3routepb.WeightedCluster{ 780 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 781 { 782 Name: "A", 783 Weight: &wrapperspb.UInt32Value{Value: 100}, 784 }, 785 }}, 786 }, 787 MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{ 788 MaxStreamDuration: durationpb.New(5 * time.Second), 789 }, 790 }}, 791 }, 792 { 793 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/bar"}}, 794 Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ 795 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{WeightedClusters: &v3routepb.WeightedCluster{ 796 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 797 { 798 Name: "B", 799 Weight: &wrapperspb.UInt32Value{Value: 100}, 800 }, 801 }}, 802 }, 803 MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{ 804 MaxStreamDuration: durationpb.New(0 * time.Second), 805 }, 806 }}, 807 }, 808 { 809 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}}, 810 Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ 811 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{WeightedClusters: &v3routepb.WeightedCluster{ 812 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 813 { 814 Name: "C", 815 Weight: &wrapperspb.UInt32Value{Value: 100}, 816 }, 817 }}, 818 }, 819 }}, 820 }, 821 }, 822 }}, 823 }} 824 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 825 826 // Read the update pushed by the resolver to the ClientConn. 827 cs := verifyUpdateFromResolver(ctx, t, stateCh, "") 828 829 testCases := []struct { 830 name string 831 method string 832 want *time.Duration 833 }{{ 834 name: "RDS setting", 835 method: "/foo/method", 836 want: newDurationP(5 * time.Second), 837 }, { 838 name: "explicit zero in RDS; ignore LDS", 839 method: "/bar/method", 840 want: nil, 841 }, { 842 name: "no config in RDS; fallback to LDS", 843 method: "/baz/method", 844 want: newDurationP(time.Second), 845 }} 846 847 for _, tc := range testCases { 848 t.Run(tc.name, func(t *testing.T) { 849 req := iresolver.RPCInfo{ 850 Method: tc.method, 851 Context: ctx, 852 } 853 res, err := cs.SelectConfig(req) 854 if err != nil { 855 t.Errorf("cs.SelectConfig(%v): %v", req, err) 856 return 857 } 858 res.OnCommitted() 859 got := res.MethodConfig.Timeout 860 if !cmp.Equal(got, tc.want) { 861 t.Errorf("For method %q: res.MethodConfig.Timeout = %v; want %v", tc.method, got, tc.want) 862 } 863 }) 864 } 865 } 866 867 // Tests that clusters remain in service config if RPCs are in flight. 868 func (s) TestResolverDelayedOnCommitted(t *testing.T) { 869 // Spin up an xDS management server for the test. 870 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 871 defer cancel() 872 nodeID := uuid.New().String() 873 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 874 875 // Configure resources on the management server. 876 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 877 routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(defaultTestRouteConfigName, defaultTestServiceName, defaultTestClusterName)} 878 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 879 880 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 881 882 // Read the update pushed by the resolver to the ClientConn. 883 cs := verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 884 885 // Make an RPC, but do not commit it yet. 886 resOld, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 887 if err != nil { 888 t.Fatalf("cs.SelectConfig(): %v", err) 889 } 890 wantClusterName := fmt.Sprintf("cluster:%s", defaultTestClusterName) 891 if cluster := clustermanager.GetPickedClusterForTesting(resOld.Context); cluster != wantClusterName { 892 t.Fatalf("Picked cluster is %q, want %q", cluster, wantClusterName) 893 } 894 895 // Delay resOld.OnCommitted(). As long as there are pending RPCs to removed 896 // clusters, they still appear in the service config. 897 898 // Update the route configuration resource on the management server to 899 // return a new cluster. 900 newClusterName := "new-" + defaultTestClusterName 901 routes = []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(defaultTestRouteConfigName, defaultTestServiceName, newClusterName)} 902 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 903 904 // Read the update pushed by the resolver to the ClientConn and ensure the 905 // old cluster is present in the service config. Also ensure that the newly 906 // returned config selector does not hold a reference to the old cluster. 907 wantSC := fmt.Sprintf(` 908 { 909 "loadBalancingConfig": [ 910 { 911 "xds_cluster_manager_experimental": { 912 "children": { 913 "cluster:%s": { 914 "childPolicy": [ 915 { 916 "cds_experimental": { 917 "cluster": "%s" 918 } 919 } 920 ] 921 }, 922 "cluster:%s": { 923 "childPolicy": [ 924 { 925 "cds_experimental": { 926 "cluster": "%s" 927 } 928 } 929 ] 930 } 931 } 932 } 933 } 934 ] 935 }`, defaultTestClusterName, defaultTestClusterName, newClusterName, newClusterName) 936 cs = verifyUpdateFromResolver(ctx, t, stateCh, wantSC) 937 938 resNew, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 939 if err != nil { 940 t.Fatalf("cs.SelectConfig(): %v", err) 941 } 942 wantClusterName = fmt.Sprintf("cluster:%s", newClusterName) 943 if cluster := clustermanager.GetPickedClusterForTesting(resNew.Context); cluster != wantClusterName { 944 t.Fatalf("Picked cluster is %q, want %q", cluster, wantClusterName) 945 } 946 947 // Invoke OnCommitted on the old RPC; should lead to a service config update 948 // that deletes the old cluster, as the old cluster no longer has any 949 // pending RPCs. 950 resOld.OnCommitted() 951 952 wantSC = fmt.Sprintf(` 953 { 954 "loadBalancingConfig": [ 955 { 956 "xds_cluster_manager_experimental": { 957 "children": { 958 "cluster:%s": { 959 "childPolicy": [ 960 { 961 "cds_experimental": { 962 "cluster": "%s" 963 } 964 } 965 ] 966 } 967 } 968 } 969 } 970 ] 971 }`, newClusterName, newClusterName) 972 verifyUpdateFromResolver(ctx, t, stateCh, wantSC) 973 } 974 975 // Tests the case where two LDS updates with the same RDS name to watch are 976 // received without an RDS in between. Those LDS updates shouldn't trigger a 977 // service config update. 978 func (s) TestResolverMultipleLDSUpdates(t *testing.T) { 979 // Spin up an xDS management server for the test. 980 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 981 defer cancel() 982 nodeID := uuid.New().String() 983 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 984 985 // Configure the management server with a listener resource, but no route 986 // configuration resource. 987 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 988 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, nil) 989 990 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 991 992 // Ensure there is no update from the resolver. 993 verifyNoUpdateFromResolver(ctx, t, stateCh) 994 995 // Configure the management server with a listener resource that points to 996 // the same route configuration resource but has different values for max 997 // stream duration field. There is still no route configuration resource on 998 // the management server. 999 hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 1000 RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{Rds: &v3httppb.Rds{ 1001 ConfigSource: &v3corepb.ConfigSource{ 1002 ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}}, 1003 }, 1004 RouteConfigName: defaultTestRouteConfigName, 1005 }}, 1006 HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter}, 1007 CommonHttpProtocolOptions: &v3corepb.HttpProtocolOptions{ 1008 MaxStreamDuration: durationpb.New(1 * time.Second), 1009 }, 1010 }) 1011 listeners = []*v3listenerpb.Listener{{ 1012 Name: defaultTestServiceName, 1013 ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm}, 1014 FilterChains: []*v3listenerpb.FilterChain{{ 1015 Name: "filter-chain-name", 1016 Filters: []*v3listenerpb.Filter{{ 1017 Name: wellknown.HTTPConnectionManager, 1018 ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm}, 1019 }}, 1020 }}, 1021 }} 1022 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, nil) 1023 1024 // Ensure that there is no update from the resolver. 1025 verifyNoUpdateFromResolver(ctx, t, stateCh) 1026 } 1027 1028 // TestResolverWRR tests the case where the route configuration returned by the 1029 // management server contains a set of weighted clusters. The test performs a 1030 // bunch of RPCs using the cluster specifier returned by the resolver, and 1031 // verifies the cluster distribution. 1032 func (s) TestResolverWRR(t *testing.T) { 1033 origNewWRR := rinternal.NewWRR 1034 rinternal.NewWRR = testutils.NewTestWRR 1035 defer func() { rinternal.NewWRR = origNewWRR }() 1036 1037 // Spin up an xDS management server for the test. 1038 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 1039 defer cancel() 1040 nodeID := uuid.New().String() 1041 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 1042 1043 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 1044 1045 // Configure resources on the management server. 1046 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 1047 routes := []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{ 1048 RouteConfigName: defaultTestRouteConfigName, 1049 ListenerName: defaultTestServiceName, 1050 ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeWeightedCluster, 1051 WeightedClusters: map[string]int{"A": 75, "B": 25}, 1052 })} 1053 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 1054 1055 // Read the update pushed by the resolver to the ClientConn. 1056 cs := verifyUpdateFromResolver(ctx, t, stateCh, "") 1057 1058 // Make RPCs to verify WRR behavior in the cluster specifier. 1059 picks := map[string]int{} 1060 for i := 0; i < 100; i++ { 1061 res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 1062 if err != nil { 1063 t.Fatalf("cs.SelectConfig(): %v", err) 1064 } 1065 picks[clustermanager.GetPickedClusterForTesting(res.Context)]++ 1066 res.OnCommitted() 1067 } 1068 want := map[string]int{"cluster:A": 75, "cluster:B": 25} 1069 if !cmp.Equal(picks, want) { 1070 t.Errorf("Picked clusters: %v; want: %v", picks, want) 1071 } 1072 } 1073 1074 const filterCfgPathFieldName = "path" 1075 const filterCfgErrorFieldName = "new_stream_error" 1076 1077 type filterCfg struct { 1078 httpfilter.FilterConfig 1079 path string 1080 newStreamErr error 1081 } 1082 1083 type filterBuilder struct { 1084 paths []string 1085 typeURL string 1086 } 1087 1088 func (fb *filterBuilder) TypeURLs() []string { return []string{fb.typeURL} } 1089 1090 func filterConfigFromProto(cfg proto.Message) (httpfilter.FilterConfig, error) { 1091 ts, ok := cfg.(*v3xdsxdstypepb.TypedStruct) 1092 if !ok { 1093 return nil, fmt.Errorf("unsupported filter config type: %T, want %T", cfg, &v3xdsxdstypepb.TypedStruct{}) 1094 } 1095 1096 if ts.GetValue() == nil { 1097 return filterCfg{}, nil 1098 } 1099 ret := filterCfg{} 1100 if v := ts.GetValue().GetFields()[filterCfgPathFieldName]; v != nil { 1101 ret.path = v.GetStringValue() 1102 } 1103 if v := ts.GetValue().GetFields()[filterCfgErrorFieldName]; v != nil { 1104 if v.GetStringValue() == "" { 1105 ret.newStreamErr = nil 1106 } else { 1107 ret.newStreamErr = fmt.Errorf("%s", v.GetStringValue()) 1108 } 1109 } 1110 return ret, nil 1111 } 1112 1113 func (*filterBuilder) ParseFilterConfig(cfg proto.Message) (httpfilter.FilterConfig, error) { 1114 return filterConfigFromProto(cfg) 1115 } 1116 1117 func (*filterBuilder) ParseFilterConfigOverride(override proto.Message) (httpfilter.FilterConfig, error) { 1118 return filterConfigFromProto(override) 1119 } 1120 1121 func (*filterBuilder) IsTerminal() bool { return false } 1122 1123 var _ httpfilter.ClientInterceptorBuilder = &filterBuilder{} 1124 1125 func (fb *filterBuilder) BuildClientInterceptor(config, override httpfilter.FilterConfig) (iresolver.ClientInterceptor, error) { 1126 if config == nil { 1127 panic("unexpected missing config") 1128 } 1129 1130 fi := &filterInterceptor{ 1131 parent: fb, 1132 pathCh: make(chan string, 10), 1133 } 1134 1135 fb.paths = append(fb.paths, "build:"+config.(filterCfg).path) 1136 err := config.(filterCfg).newStreamErr 1137 if override != nil { 1138 fb.paths = append(fb.paths, "override:"+override.(filterCfg).path) 1139 err = override.(filterCfg).newStreamErr 1140 } 1141 1142 fi.cfgPath = config.(filterCfg).path 1143 fi.err = err 1144 return fi, nil 1145 } 1146 1147 type filterInterceptor struct { 1148 parent *filterBuilder 1149 pathCh chan string 1150 cfgPath string 1151 err error 1152 } 1153 1154 func (fi *filterInterceptor) NewStream(ctx context.Context, _ iresolver.RPCInfo, done func(), newStream func(ctx context.Context, done func()) (iresolver.ClientStream, error)) (iresolver.ClientStream, error) { 1155 fi.parent.paths = append(fi.parent.paths, "newstream:"+fi.cfgPath) 1156 if fi.err != nil { 1157 return nil, fi.err 1158 } 1159 d := func() { 1160 fi.parent.paths = append(fi.parent.paths, "done:"+fi.cfgPath) 1161 done() 1162 } 1163 cs, err := newStream(ctx, d) 1164 if err != nil { 1165 return nil, err 1166 } 1167 return &clientStream{ClientStream: cs}, nil 1168 } 1169 1170 type clientStream struct { 1171 iresolver.ClientStream 1172 } 1173 1174 func (s) TestConfigSelector_FailureCases(t *testing.T) { 1175 const methodName = "1" 1176 1177 tests := []struct { 1178 name string 1179 listener *v3listenerpb.Listener 1180 wantErr string 1181 }{ 1182 { 1183 name: "route type RouteActionUnsupported invalid for client", 1184 listener: &v3listenerpb.Listener{ 1185 Name: defaultTestServiceName, 1186 ApiListener: &v3listenerpb.ApiListener{ 1187 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 1188 RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{ 1189 RouteConfig: &v3routepb.RouteConfiguration{ 1190 Name: defaultTestRouteConfigName, 1191 VirtualHosts: []*v3routepb.VirtualHost{{ 1192 Domains: []string{defaultTestServiceName}, 1193 Routes: []*v3routepb.Route{{ 1194 Match: &v3routepb.RouteMatch{ 1195 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: methodName}, 1196 }, 1197 Action: &v3routepb.Route_FilterAction{}, 1198 }}, 1199 }}, 1200 }}, 1201 HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter}, 1202 }), 1203 }, 1204 }, 1205 wantErr: "matched route does not have a supported route action type", 1206 }, 1207 { 1208 name: "route type RouteActionNonForwardingAction invalid for client", 1209 listener: &v3listenerpb.Listener{ 1210 Name: defaultTestServiceName, 1211 ApiListener: &v3listenerpb.ApiListener{ 1212 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 1213 RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{ 1214 RouteConfig: &v3routepb.RouteConfiguration{ 1215 Name: defaultTestRouteConfigName, 1216 VirtualHosts: []*v3routepb.VirtualHost{{ 1217 Domains: []string{defaultTestServiceName}, 1218 Routes: []*v3routepb.Route{{ 1219 Match: &v3routepb.RouteMatch{ 1220 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: methodName}, 1221 }, 1222 Action: &v3routepb.Route_NonForwardingAction{}, 1223 }}, 1224 }}, 1225 }}, 1226 HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter}, 1227 }), 1228 }, 1229 }, 1230 wantErr: "matched route does not have a supported route action type", 1231 }, 1232 } 1233 1234 for _, test := range tests { 1235 t.Run(test.name, func(t *testing.T) { 1236 // Spin up an xDS management server. 1237 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 1238 defer cancel() 1239 nodeID := uuid.New().String() 1240 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 1241 1242 // Build an xDS resolver. 1243 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 1244 1245 // Update the management server with a listener resource that 1246 // contains inline route configuration. 1247 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, []*v3listenerpb.Listener{test.listener}, nil) 1248 1249 // Ensure that the resolver pushes a state update to the channel. 1250 cs := verifyUpdateFromResolver(ctx, t, stateCh, "") 1251 1252 // Ensure that it returns the expected error. 1253 _, err := cs.SelectConfig(iresolver.RPCInfo{Method: methodName, Context: ctx}) 1254 if err := verifyResolverError(err, codes.Unavailable, test.wantErr, nodeID); err != nil { 1255 t.Fatal(err) 1256 } 1257 }) 1258 } 1259 } 1260 1261 func newHTTPFilter(t *testing.T, name, typeURL, path, err string) *v3httppb.HttpFilter { 1262 return &v3httppb.HttpFilter{ 1263 Name: name, 1264 ConfigType: &v3httppb.HttpFilter_TypedConfig{ 1265 TypedConfig: testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1266 TypeUrl: typeURL, 1267 Value: &structpb.Struct{ 1268 Fields: map[string]*structpb.Value{ 1269 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: path}}, 1270 filterCfgErrorFieldName: {Kind: &structpb.Value_StringValue{StringValue: err}}, 1271 }, 1272 }, 1273 }), 1274 }, 1275 } 1276 } 1277 1278 func (s) TestXDSResolverHTTPFilters(t *testing.T) { 1279 const methodName1 = "1" 1280 const methodName2 = "2" 1281 testFilterName := t.Name() 1282 1283 testCases := []struct { 1284 name string 1285 listener *v3listenerpb.Listener 1286 rpcRes map[string][][]string 1287 wantStreamErr string 1288 }{ 1289 { 1290 name: "NewStream error - ensure earlier interceptor Done is still called", 1291 listener: &v3listenerpb.Listener{ 1292 Name: defaultTestServiceName, 1293 ApiListener: &v3listenerpb.ApiListener{ 1294 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 1295 RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{ 1296 RouteConfig: &v3routepb.RouteConfiguration{ 1297 Name: defaultTestRouteConfigName, 1298 VirtualHosts: []*v3routepb.VirtualHost{{ 1299 Domains: []string{defaultTestServiceName}, 1300 Routes: []*v3routepb.Route{{ 1301 Match: &v3routepb.RouteMatch{ 1302 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: methodName1}, 1303 }, 1304 Action: &v3routepb.Route_Route{ 1305 Route: &v3routepb.RouteAction{ 1306 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{ 1307 WeightedClusters: &v3routepb.WeightedCluster{ 1308 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 1309 {Name: "A", Weight: wrapperspb.UInt32(1)}, 1310 {Name: "B", Weight: wrapperspb.UInt32(1)}, 1311 }, 1312 }, 1313 }, 1314 }, 1315 }, 1316 }}, 1317 }}, 1318 }}, 1319 HttpFilters: []*v3httppb.HttpFilter{ 1320 newHTTPFilter(t, "foo", testFilterName, "foo1", ""), 1321 newHTTPFilter(t, "bar", testFilterName, "bar1", "bar newstream err"), 1322 e2e.RouterHTTPFilter, 1323 }, 1324 }), 1325 }, 1326 }, 1327 rpcRes: map[string][][]string{ 1328 methodName1: { 1329 {"build:foo1", "build:bar1", "newstream:foo1", "newstream:bar1", "done:foo1"}, // err in bar1 NewStream() 1330 }, 1331 }, 1332 wantStreamErr: "bar newstream err", 1333 }, 1334 { 1335 name: "all overrides", 1336 listener: &v3listenerpb.Listener{ 1337 Name: defaultTestServiceName, 1338 ApiListener: &v3listenerpb.ApiListener{ 1339 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 1340 RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{ 1341 RouteConfig: &v3routepb.RouteConfiguration{ 1342 Name: defaultTestRouteConfigName, 1343 VirtualHosts: []*v3routepb.VirtualHost{{ 1344 Domains: []string{defaultTestServiceName}, 1345 Routes: []*v3routepb.Route{ 1346 { 1347 Match: &v3routepb.RouteMatch{ 1348 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: methodName1}, 1349 }, 1350 Action: &v3routepb.Route_Route{ 1351 Route: &v3routepb.RouteAction{ 1352 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{ 1353 WeightedClusters: &v3routepb.WeightedCluster{ 1354 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 1355 {Name: "A", Weight: wrapperspb.UInt32(1)}, 1356 {Name: "B", Weight: wrapperspb.UInt32(1)}, 1357 }, 1358 }, 1359 }, 1360 }, 1361 }, 1362 }, 1363 { 1364 Match: &v3routepb.RouteMatch{ 1365 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: methodName2}, 1366 }, 1367 Action: &v3routepb.Route_Route{ 1368 Route: &v3routepb.RouteAction{ 1369 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{ 1370 WeightedClusters: &v3routepb.WeightedCluster{ 1371 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 1372 {Name: "A", Weight: wrapperspb.UInt32(1)}, 1373 { 1374 Name: "B", 1375 Weight: wrapperspb.UInt32(1), 1376 TypedPerFilterConfig: map[string]*anypb.Any{ 1377 "foo": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1378 TypeUrl: testFilterName, 1379 Value: &structpb.Struct{ 1380 Fields: map[string]*structpb.Value{ 1381 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "foo4"}}, 1382 }, 1383 }, 1384 }), 1385 "bar": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1386 TypeUrl: testFilterName, 1387 Value: &structpb.Struct{ 1388 Fields: map[string]*structpb.Value{ 1389 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "bar4"}}, 1390 }, 1391 }, 1392 }), 1393 }, 1394 }, 1395 }, 1396 }, 1397 }, 1398 }, 1399 }, 1400 TypedPerFilterConfig: map[string]*anypb.Any{ 1401 "foo": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1402 TypeUrl: testFilterName, 1403 Value: &structpb.Struct{ 1404 Fields: map[string]*structpb.Value{ 1405 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "foo3"}}, 1406 filterCfgErrorFieldName: {Kind: &structpb.Value_StringValue{StringValue: ""}}, 1407 }, 1408 }, 1409 }), 1410 "bar": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1411 TypeUrl: testFilterName, 1412 Value: &structpb.Struct{ 1413 Fields: map[string]*structpb.Value{ 1414 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "bar3"}}, 1415 }, 1416 }, 1417 }), 1418 }, 1419 }, 1420 }, 1421 TypedPerFilterConfig: map[string]*anypb.Any{ 1422 "foo": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1423 TypeUrl: testFilterName, 1424 Value: &structpb.Struct{ 1425 Fields: map[string]*structpb.Value{ 1426 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "foo2"}}, 1427 filterCfgErrorFieldName: {Kind: &structpb.Value_StringValue{StringValue: ""}}, 1428 }, 1429 }, 1430 }), 1431 "bar": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1432 TypeUrl: testFilterName, 1433 Value: &structpb.Struct{ 1434 Fields: map[string]*structpb.Value{ 1435 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "bar2"}}, 1436 }, 1437 }, 1438 }), 1439 }, 1440 }}, 1441 }}, 1442 HttpFilters: []*v3httppb.HttpFilter{ 1443 newHTTPFilter(t, "foo", testFilterName, "foo1", "this is overridden to nil"), 1444 newHTTPFilter(t, "bar", testFilterName, "bar1", ""), 1445 e2e.RouterHTTPFilter, 1446 }, 1447 }), 1448 }, 1449 }, 1450 rpcRes: map[string][][]string{ 1451 methodName1: { 1452 {"build:foo1", "override:foo2", "build:bar1", "override:bar2", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1453 {"build:foo1", "override:foo2", "build:bar1", "override:bar2", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1454 }, 1455 methodName2: { 1456 {"build:foo1", "override:foo3", "build:bar1", "override:bar3", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1457 {"build:foo1", "override:foo4", "build:bar1", "override:bar4", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1458 {"build:foo1", "override:foo3", "build:bar1", "override:bar3", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1459 {"build:foo1", "override:foo4", "build:bar1", "override:bar4", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1460 }, 1461 }, 1462 }, 1463 } 1464 1465 for _, tc := range testCases { 1466 t.Run(tc.name, func(t *testing.T) { 1467 origNewWRR := rinternal.NewWRR 1468 rinternal.NewWRR = testutils.NewTestWRR 1469 defer func() { rinternal.NewWRR = origNewWRR }() 1470 1471 // Register a custom httpFilter builder for the test. 1472 fb := &filterBuilder{typeURL: testFilterName} 1473 httpfilter.Register(fb) 1474 1475 // Spin up an xDS management server. 1476 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 1477 defer cancel() 1478 nodeID := uuid.New().String() 1479 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 1480 1481 // Build an xDS resolver. 1482 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 1483 1484 // Update the management server with a listener resource that 1485 // contains an inline route configuration. 1486 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, []*v3listenerpb.Listener{tc.listener}, nil) 1487 1488 // Ensure that the resolver pushes a state update to the channel. 1489 cs := verifyUpdateFromResolver(ctx, t, stateCh, "") 1490 1491 for method, wants := range tc.rpcRes { 1492 // Order of wants is non-deterministic. 1493 remainingWant := make([][]string, len(wants)) 1494 copy(remainingWant, wants) 1495 for n := range wants { 1496 res, err := cs.SelectConfig(iresolver.RPCInfo{Method: method, Context: ctx}) 1497 if err != nil { 1498 t.Fatalf("Unexpected error from cs.SelectConfig(_): %v", err) 1499 } 1500 1501 var doneFunc func() 1502 _, err = res.Interceptor.NewStream(ctx, iresolver.RPCInfo{}, func() {}, func(_ context.Context, done func()) (iresolver.ClientStream, error) { 1503 doneFunc = done 1504 return nil, nil 1505 }) 1506 if tc.wantStreamErr != "" { 1507 if err == nil || !strings.Contains(err.Error(), tc.wantStreamErr) { 1508 t.Errorf("NewStream(...) = _, %v; want _, Contains(%v)", err, tc.wantStreamErr) 1509 } 1510 if err == nil { 1511 res.OnCommitted() 1512 doneFunc() 1513 } 1514 continue 1515 } 1516 if err != nil { 1517 t.Fatalf("unexpected error from Interceptor.NewStream: %v", err) 1518 1519 } 1520 res.OnCommitted() 1521 doneFunc() 1522 1523 gotPaths := fb.paths 1524 fb.paths = []string{} 1525 1526 // Confirm the desired path is found in remainingWant, and remove it. 1527 pass := false 1528 for i := range remainingWant { 1529 if cmp.Equal(gotPaths, remainingWant[i]) { 1530 remainingWant[i] = remainingWant[len(remainingWant)-1] 1531 remainingWant = remainingWant[:len(remainingWant)-1] 1532 pass = true 1533 break 1534 } 1535 } 1536 if !pass { 1537 t.Errorf("%q:%v - path:\n%v\nwant one of:\n%v", method, n, gotPaths, remainingWant) 1538 } 1539 } 1540 } 1541 }) 1542 } 1543 } 1544 1545 func newDurationP(d time.Duration) *time.Duration { 1546 return &d 1547 }