google.golang.org/grpc@v1.72.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 "google.golang.org/grpc/internal/testutils" 39 "google.golang.org/grpc/internal/testutils/xds/e2e" 40 "google.golang.org/grpc/internal/xds/bootstrap" 41 "google.golang.org/grpc/metadata" 42 "google.golang.org/grpc/resolver" 43 "google.golang.org/grpc/serviceconfig" 44 "google.golang.org/grpc/xds/internal/balancer/clustermanager" 45 "google.golang.org/grpc/xds/internal/balancer/ringhash" 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 returned by the management server is NACKed 292 // by the xDS client, which then returns an update containing an error to the 293 // resolver. Verifies that the update is propagated to the ClientConn by the 294 // resolver. It also tests the cases where the resolver gets a good update 295 // subsequently, and another error after the good update. The test also verifies 296 // that these are propagated to the ClientConn. 297 func (s) TestResolverBadServiceUpdate(t *testing.T) { 298 // Spin up an xDS management server for the test. 299 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 300 defer cancel() 301 nodeID := uuid.New().String() 302 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 303 304 // Configure a listener resource that is expected to be NACKed because it 305 // does not contain the `RouteSpecifier` field in the HTTPConnectionManager. 306 hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 307 HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})}, 308 }) 309 lis := &v3listenerpb.Listener{ 310 Name: defaultTestServiceName, 311 ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm}, 312 FilterChains: []*v3listenerpb.FilterChain{{ 313 Name: "filter-chain-name", 314 Filters: []*v3listenerpb.Filter{{ 315 Name: wellknown.HTTPConnectionManager, 316 ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm}, 317 }}, 318 }}, 319 } 320 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, []*v3listenerpb.Listener{lis}, nil) 321 322 // Build the resolver and expect an error update from it. 323 stateCh, errCh, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 324 wantErr := "no RouteSpecifier" 325 if err := waitForErrorFromResolver(ctx, errCh, wantErr, nodeID); err != nil { 326 t.Fatal(err) 327 } 328 329 // Configure good listener and route configuration resources on the 330 // management server. 331 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 332 routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(defaultTestRouteConfigName, defaultTestServiceName, defaultTestClusterName)} 333 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 334 335 // Expect a good update from the resolver. 336 verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 337 338 // Configure another bad resource on the management server and expect an 339 // error update from the resolver. 340 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, []*v3listenerpb.Listener{lis}, nil) 341 if err := waitForErrorFromResolver(ctx, errCh, wantErr, nodeID); err != nil { 342 t.Fatal(err) 343 } 344 } 345 346 // TestResolverGoodServiceUpdate tests the case where the resource returned by 347 // the management server is ACKed by the xDS client, which then returns a good 348 // service update to the resolver. The test verifies that the service config 349 // returned by the resolver matches expectations, and that the config selector 350 // returned by the resolver picks clusters based on the route configuration 351 // received from the management server. 352 func (s) TestResolverGoodServiceUpdate(t *testing.T) { 353 for _, tt := range []struct { 354 name string 355 routeConfig *v3routepb.RouteConfiguration 356 wantServiceConfig string 357 wantClusters map[string]bool 358 }{ 359 { 360 name: "single cluster", 361 routeConfig: e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{ 362 RouteConfigName: defaultTestRouteConfigName, 363 ListenerName: defaultTestServiceName, 364 ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeCluster, 365 ClusterName: defaultTestClusterName, 366 }), 367 wantServiceConfig: wantDefaultServiceConfig, 368 wantClusters: map[string]bool{fmt.Sprintf("cluster:%s", defaultTestClusterName): true}, 369 }, 370 { 371 name: "two clusters", 372 routeConfig: e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{ 373 RouteConfigName: defaultTestRouteConfigName, 374 ListenerName: defaultTestServiceName, 375 ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeWeightedCluster, 376 WeightedClusters: map[string]int{"cluster_1": 75, "cluster_2": 25}, 377 }), 378 // This update contains the cluster from the previous update as well 379 // as this update, as the previous config selector still references 380 // the old cluster when the new one is pushed. 381 wantServiceConfig: `{ 382 "loadBalancingConfig": [{ 383 "xds_cluster_manager_experimental": { 384 "children": { 385 "cluster:cluster_1": { 386 "childPolicy": [{ 387 "cds_experimental": { 388 "cluster": "cluster_1" 389 } 390 }] 391 }, 392 "cluster:cluster_2": { 393 "childPolicy": [{ 394 "cds_experimental": { 395 "cluster": "cluster_2" 396 } 397 }] 398 } 399 } 400 } 401 }]}`, 402 wantClusters: map[string]bool{"cluster:cluster_1": true, "cluster:cluster_2": true}, 403 }, 404 } { 405 t.Run(tt.name, func(t *testing.T) { 406 // Spin up an xDS management server for the test. 407 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 408 defer cancel() 409 nodeID := uuid.New().String() 410 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 411 412 // Configure the management server with a good listener resource and a 413 // route configuration resource, as specified by the test case. 414 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 415 routes := []*v3routepb.RouteConfiguration{tt.routeConfig} 416 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 417 418 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 419 420 // Read the update pushed by the resolver to the ClientConn. 421 cs := verifyUpdateFromResolver(ctx, t, stateCh, tt.wantServiceConfig) 422 423 pickedClusters := make(map[string]bool) 424 // Odds of picking 75% cluster 100 times in a row: 1 in 3E-13. And 425 // with the random number generator stubbed out, we can rely on this 426 // to be 100% reproducible. 427 for i := 0; i < 100; i++ { 428 res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 429 if err != nil { 430 t.Fatalf("cs.SelectConfig(): %v", err) 431 } 432 cluster := clustermanager.GetPickedClusterForTesting(res.Context) 433 pickedClusters[cluster] = true 434 res.OnCommitted() 435 } 436 if !cmp.Equal(pickedClusters, tt.wantClusters) { 437 t.Errorf("Picked clusters: %v; want: %v", pickedClusters, tt.wantClusters) 438 } 439 }) 440 } 441 } 442 443 // Tests a case where a resolver receives a RouteConfig update with a HashPolicy 444 // specifying to generate a hash. The configSelector generated should 445 // successfully generate a Hash. 446 func (s) TestResolverRequestHash(t *testing.T) { 447 // Spin up an xDS management server for the test. 448 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 449 defer cancel() 450 nodeID := uuid.New().String() 451 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 452 453 // Configure the management server with a good listener resource and a 454 // route configuration resource that specifies a hash policy. 455 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 456 routes := []*v3routepb.RouteConfiguration{{ 457 Name: defaultTestRouteConfigName, 458 VirtualHosts: []*v3routepb.VirtualHost{{ 459 Domains: []string{defaultTestServiceName}, 460 Routes: []*v3routepb.Route{{ 461 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}}, 462 Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ 463 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{WeightedClusters: &v3routepb.WeightedCluster{ 464 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 465 { 466 Name: defaultTestClusterName, 467 Weight: &wrapperspb.UInt32Value{Value: 100}, 468 }, 469 }, 470 }}, 471 HashPolicy: []*v3routepb.RouteAction_HashPolicy{{ 472 PolicySpecifier: &v3routepb.RouteAction_HashPolicy_Header_{ 473 Header: &v3routepb.RouteAction_HashPolicy_Header{ 474 HeaderName: ":path", 475 }, 476 }, 477 Terminal: true, 478 }}, 479 }}, 480 }}, 481 }}, 482 }} 483 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 484 485 // Build the resolver and read the config selector out of it. 486 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 487 cs := verifyUpdateFromResolver(ctx, t, stateCh, "") 488 489 // Selecting a config when there was a hash policy specified in the route 490 // that will be selected should put a request hash in the config's context. 491 res, err := cs.SelectConfig(iresolver.RPCInfo{ 492 Context: metadata.NewOutgoingContext(ctx, metadata.Pairs(":path", "/products")), 493 Method: "/service/method", 494 }) 495 if err != nil { 496 t.Fatalf("cs.SelectConfig(): %v", err) 497 } 498 wantHash := xxhash.Sum64String("/products") 499 gotHash, ok := ringhash.XDSRequestHash(res.Context) 500 if !ok { 501 t.Fatalf("Got no request hash, want: %v", wantHash) 502 } 503 if gotHash != wantHash { 504 t.Fatalf("Got request hash: %v, want: %v", gotHash, wantHash) 505 } 506 } 507 508 // Tests the case where resources are removed from the management server, 509 // causing it to send an empty update to the xDS client, which returns a 510 // resource-not-found error to the xDS resolver. The test verifies that an 511 // ongoing RPC is handled to completion when this happens. 512 func (s) TestResolverRemovedWithRPCs(t *testing.T) { 513 // Spin up an xDS management server for the test. 514 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 515 defer cancel() 516 nodeID := uuid.New().String() 517 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 518 519 // Configure resources on the management server. 520 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 521 routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(defaultTestRouteConfigName, defaultTestServiceName, defaultTestClusterName)} 522 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 523 524 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 525 526 // Read the update pushed by the resolver to the ClientConn. 527 cs := verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 528 529 res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 530 if err != nil { 531 t.Fatalf("cs.SelectConfig(): %v", err) 532 } 533 534 // Delete the resources on the management server. This should result in a 535 // resource-not-found error from the xDS client. 536 if err := mgmtServer.Update(ctx, e2e.UpdateOptions{NodeID: nodeID}); err != nil { 537 t.Fatal(err) 538 } 539 540 // The RPC started earlier is still in progress. So, the xDS resolver will 541 // not produce an empty service config at this point. Instead it will retain 542 // the cluster to which the RPC is ongoing in the service config, but will 543 // return an erroring config selector which will fail new RPCs. 544 cs = verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 545 _, err = cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 546 if err := verifyResolverError(err, codes.Unavailable, "no valid clusters", nodeID); err != nil { 547 t.Fatal(err) 548 } 549 550 // "Finish the RPC"; this could cause a panic if the resolver doesn't 551 // handle it correctly. 552 res.OnCommitted() 553 554 // Now that the RPC is committed, the xDS resolver is expected to send an 555 // update with an empty service config. 556 var state resolver.State 557 select { 558 case <-ctx.Done(): 559 t.Fatalf("Timeout waiting for an update from the resolver: %v", ctx.Err()) 560 case state = <-stateCh: 561 if err := state.ServiceConfig.Err; err != nil { 562 t.Fatalf("Received error in service config: %v", state.ServiceConfig.Err) 563 } 564 wantSCParsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)("{}") 565 if !internal.EqualServiceConfigForTesting(state.ServiceConfig.Config, wantSCParsed.Config) { 566 t.Fatalf("Got service config:\n%s \nWant service config:\n%s", cmp.Diff(nil, state.ServiceConfig.Config), cmp.Diff(nil, wantSCParsed.Config)) 567 } 568 } 569 570 // Workaround for https://github.com/envoyproxy/go-control-plane/issues/431. 571 // 572 // The xDS client can miss route configurations due to a race condition 573 // between resource removal and re-addition. To avoid this, continuously 574 // push new versions of the resources to the server, ensuring the client 575 // eventually receives the configuration. 576 // 577 // TODO(https://github.com/grpc/grpc-go/issues/7807): Remove this workaround 578 // once the issue is fixed. 579 waitForStateUpdate: 580 for { 581 sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) 582 defer sCancel() 583 584 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 585 586 select { 587 case state = <-stateCh: 588 if err := state.ServiceConfig.Err; err != nil { 589 t.Fatalf("Received error in service config: %v", state.ServiceConfig.Err) 590 } 591 wantSCParsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(wantDefaultServiceConfig) 592 if !internal.EqualServiceConfigForTesting(state.ServiceConfig.Config, wantSCParsed.Config) { 593 t.Fatalf("Got service config:\n%s \nWant service config:\n%s", cmp.Diff(nil, state.ServiceConfig.Config), cmp.Diff(nil, wantSCParsed.Config)) 594 } 595 break waitForStateUpdate 596 case <-sCtx.Done(): 597 } 598 } 599 cs = iresolver.GetConfigSelector(state) 600 if cs == nil { 601 t.Fatal("Received nil config selector in update from resolver") 602 } 603 604 res, err = cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 605 if err != nil { 606 t.Fatalf("cs.SelectConfig(): %v", err) 607 } 608 res.OnCommitted() 609 } 610 611 // Tests the case where resources returned by the management server are removed. 612 // The test verifies that the resolver pushes the expected config selector and 613 // service config in this case. 614 func (s) TestResolverRemovedResource(t *testing.T) { 615 // Spin up an xDS management server for the test. 616 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 617 defer cancel() 618 nodeID := uuid.New().String() 619 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 620 621 // Configure resources on the management server. 622 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 623 routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(defaultTestRouteConfigName, defaultTestServiceName, defaultTestClusterName)} 624 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 625 626 stateCh, errCh, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 627 628 // Read the update pushed by the resolver to the ClientConn. 629 cs := verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 630 631 // "Make an RPC" by invoking the config selector. 632 res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 633 if err != nil { 634 t.Fatalf("cs.SelectConfig(): %v", err) 635 } 636 637 // "Finish the RPC"; this could cause a panic if the resolver doesn't 638 // handle it correctly. 639 res.OnCommitted() 640 641 // Delete the resources on the management server, resulting in a 642 // resource-not-found error from the xDS client. 643 if err := mgmtServer.Update(ctx, e2e.UpdateOptions{NodeID: nodeID}); err != nil { 644 t.Fatal(err) 645 } 646 647 // The channel should receive the existing service config with the original 648 // cluster but with an erroring config selector. 649 cs = verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 650 651 // "Make another RPC" by invoking the config selector. 652 _, err = cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 653 if err := verifyResolverError(err, codes.Unavailable, "no valid clusters", nodeID); err != nil { 654 t.Fatal(err) 655 } 656 657 // In the meantime, an empty ServiceConfig update should have been sent. 658 var state resolver.State 659 select { 660 case <-ctx.Done(): 661 t.Fatalf("Timeout waiting for an update from the resolver: %v", ctx.Err()) 662 case state = <-stateCh: 663 if err := state.ServiceConfig.Err; err != nil { 664 t.Fatalf("Received error in service config: %v", state.ServiceConfig.Err) 665 } 666 wantSCParsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)("{}") 667 if !internal.EqualServiceConfigForTesting(state.ServiceConfig.Config, wantSCParsed.Config) { 668 t.Fatalf("Got service config:\n%s \nWant service config:\n%s", cmp.Diff(nil, state.ServiceConfig.Config), cmp.Diff(nil, wantSCParsed.Config)) 669 } 670 } 671 672 // The xDS resolver is expected to report an error to the channel. 673 select { 674 case <-ctx.Done(): 675 t.Fatalf("Timeout waiting for an error from the resolver: %v", ctx.Err()) 676 case err := <-errCh: 677 if err := verifyResolverError(err, codes.Unavailable, "no valid clusters", nodeID); err != nil { 678 t.Fatal(err) 679 } 680 } 681 } 682 683 // Tests the case where the resolver receives max stream duration as part of the 684 // listener and route configuration resources. The test verifies that the RPC 685 // timeout returned by the config selector matches expectations. A non-nil max 686 // stream duration (this includes an explicit zero value) in a matching route 687 // overrides the value specified in the listener resource. 688 func (s) TestResolverMaxStreamDuration(t *testing.T) { 689 // Spin up an xDS management server for the test. 690 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 691 defer cancel() 692 nodeID := uuid.New().String() 693 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 694 695 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 696 697 // Configure the management server with a listener resource that specifies a 698 // max stream duration as part of its HTTP connection manager. Also 699 // configure a route configuration resource, which has multiple routes with 700 // different values of max stream duration. 701 hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 702 RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{Rds: &v3httppb.Rds{ 703 ConfigSource: &v3corepb.ConfigSource{ 704 ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}}, 705 }, 706 RouteConfigName: defaultTestRouteConfigName, 707 }}, 708 HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter}, 709 CommonHttpProtocolOptions: &v3corepb.HttpProtocolOptions{ 710 MaxStreamDuration: durationpb.New(1 * time.Second), 711 }, 712 }) 713 listeners := []*v3listenerpb.Listener{{ 714 Name: defaultTestServiceName, 715 ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm}, 716 FilterChains: []*v3listenerpb.FilterChain{{ 717 Name: "filter-chain-name", 718 Filters: []*v3listenerpb.Filter{{ 719 Name: wellknown.HTTPConnectionManager, 720 ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm}, 721 }}, 722 }}, 723 }} 724 routes := []*v3routepb.RouteConfiguration{{ 725 Name: defaultTestRouteConfigName, 726 VirtualHosts: []*v3routepb.VirtualHost{{ 727 Domains: []string{defaultTestServiceName}, 728 Routes: []*v3routepb.Route{ 729 { 730 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/foo"}}, 731 Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ 732 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{WeightedClusters: &v3routepb.WeightedCluster{ 733 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 734 { 735 Name: "A", 736 Weight: &wrapperspb.UInt32Value{Value: 100}, 737 }, 738 }}, 739 }, 740 MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{ 741 MaxStreamDuration: durationpb.New(5 * time.Second), 742 }, 743 }}, 744 }, 745 { 746 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/bar"}}, 747 Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ 748 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{WeightedClusters: &v3routepb.WeightedCluster{ 749 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 750 { 751 Name: "B", 752 Weight: &wrapperspb.UInt32Value{Value: 100}, 753 }, 754 }}, 755 }, 756 MaxStreamDuration: &v3routepb.RouteAction_MaxStreamDuration{ 757 MaxStreamDuration: durationpb.New(0 * time.Second), 758 }, 759 }}, 760 }, 761 { 762 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}}, 763 Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ 764 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{WeightedClusters: &v3routepb.WeightedCluster{ 765 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 766 { 767 Name: "C", 768 Weight: &wrapperspb.UInt32Value{Value: 100}, 769 }, 770 }}, 771 }, 772 }}, 773 }, 774 }, 775 }}, 776 }} 777 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 778 779 // Read the update pushed by the resolver to the ClientConn. 780 cs := verifyUpdateFromResolver(ctx, t, stateCh, "") 781 782 testCases := []struct { 783 name string 784 method string 785 want *time.Duration 786 }{{ 787 name: "RDS setting", 788 method: "/foo/method", 789 want: newDurationP(5 * time.Second), 790 }, { 791 name: "explicit zero in RDS; ignore LDS", 792 method: "/bar/method", 793 want: nil, 794 }, { 795 name: "no config in RDS; fallback to LDS", 796 method: "/baz/method", 797 want: newDurationP(time.Second), 798 }} 799 800 for _, tc := range testCases { 801 t.Run(tc.name, func(t *testing.T) { 802 req := iresolver.RPCInfo{ 803 Method: tc.method, 804 Context: ctx, 805 } 806 res, err := cs.SelectConfig(req) 807 if err != nil { 808 t.Errorf("cs.SelectConfig(%v): %v", req, err) 809 return 810 } 811 res.OnCommitted() 812 got := res.MethodConfig.Timeout 813 if !cmp.Equal(got, tc.want) { 814 t.Errorf("For method %q: res.MethodConfig.Timeout = %v; want %v", tc.method, got, tc.want) 815 } 816 }) 817 } 818 } 819 820 // Tests that clusters remain in service config if RPCs are in flight. 821 func (s) TestResolverDelayedOnCommitted(t *testing.T) { 822 // Spin up an xDS management server for the test. 823 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 824 defer cancel() 825 nodeID := uuid.New().String() 826 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 827 828 // Configure resources on the management server. 829 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 830 routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(defaultTestRouteConfigName, defaultTestServiceName, defaultTestClusterName)} 831 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 832 833 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 834 835 // Read the update pushed by the resolver to the ClientConn. 836 cs := verifyUpdateFromResolver(ctx, t, stateCh, wantDefaultServiceConfig) 837 838 // Make an RPC, but do not commit it yet. 839 resOld, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 840 if err != nil { 841 t.Fatalf("cs.SelectConfig(): %v", err) 842 } 843 wantClusterName := fmt.Sprintf("cluster:%s", defaultTestClusterName) 844 if cluster := clustermanager.GetPickedClusterForTesting(resOld.Context); cluster != wantClusterName { 845 t.Fatalf("Picked cluster is %q, want %q", cluster, wantClusterName) 846 } 847 848 // Delay resOld.OnCommitted(). As long as there are pending RPCs to removed 849 // clusters, they still appear in the service config. 850 851 // Update the route configuration resource on the management server to 852 // return a new cluster. 853 newClusterName := "new-" + defaultTestClusterName 854 routes = []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(defaultTestRouteConfigName, defaultTestServiceName, newClusterName)} 855 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 856 857 // Read the update pushed by the resolver to the ClientConn and ensure the 858 // old cluster is present in the service config. Also ensure that the newly 859 // returned config selector does not hold a reference to the old cluster. 860 wantSC := fmt.Sprintf(` 861 { 862 "loadBalancingConfig": [ 863 { 864 "xds_cluster_manager_experimental": { 865 "children": { 866 "cluster:%s": { 867 "childPolicy": [ 868 { 869 "cds_experimental": { 870 "cluster": "%s" 871 } 872 } 873 ] 874 }, 875 "cluster:%s": { 876 "childPolicy": [ 877 { 878 "cds_experimental": { 879 "cluster": "%s" 880 } 881 } 882 ] 883 } 884 } 885 } 886 } 887 ] 888 }`, defaultTestClusterName, defaultTestClusterName, newClusterName, newClusterName) 889 cs = verifyUpdateFromResolver(ctx, t, stateCh, wantSC) 890 891 resNew, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 892 if err != nil { 893 t.Fatalf("cs.SelectConfig(): %v", err) 894 } 895 wantClusterName = fmt.Sprintf("cluster:%s", newClusterName) 896 if cluster := clustermanager.GetPickedClusterForTesting(resNew.Context); cluster != wantClusterName { 897 t.Fatalf("Picked cluster is %q, want %q", cluster, wantClusterName) 898 } 899 900 // Invoke OnCommitted on the old RPC; should lead to a service config update 901 // that deletes the old cluster, as the old cluster no longer has any 902 // pending RPCs. 903 resOld.OnCommitted() 904 905 wantSC = fmt.Sprintf(` 906 { 907 "loadBalancingConfig": [ 908 { 909 "xds_cluster_manager_experimental": { 910 "children": { 911 "cluster:%s": { 912 "childPolicy": [ 913 { 914 "cds_experimental": { 915 "cluster": "%s" 916 } 917 } 918 ] 919 } 920 } 921 } 922 } 923 ] 924 }`, newClusterName, newClusterName) 925 verifyUpdateFromResolver(ctx, t, stateCh, wantSC) 926 } 927 928 // Tests the case where two LDS updates with the same RDS name to watch are 929 // received without an RDS in between. Those LDS updates shouldn't trigger a 930 // service config update. 931 func (s) TestResolverMultipleLDSUpdates(t *testing.T) { 932 // Spin up an xDS management server for the test. 933 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 934 defer cancel() 935 nodeID := uuid.New().String() 936 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 937 938 // Configure the management server with a listener resource, but no route 939 // configuration resource. 940 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 941 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, nil) 942 943 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 944 945 // Ensure there is no update from the resolver. 946 verifyNoUpdateFromResolver(ctx, t, stateCh) 947 948 // Configure the management server with a listener resource that points to 949 // the same route configuration resource but has different values for max 950 // stream duration field. There is still no route configuration resource on 951 // the management server. 952 hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 953 RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{Rds: &v3httppb.Rds{ 954 ConfigSource: &v3corepb.ConfigSource{ 955 ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}}, 956 }, 957 RouteConfigName: defaultTestRouteConfigName, 958 }}, 959 HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter}, 960 CommonHttpProtocolOptions: &v3corepb.HttpProtocolOptions{ 961 MaxStreamDuration: durationpb.New(1 * time.Second), 962 }, 963 }) 964 listeners = []*v3listenerpb.Listener{{ 965 Name: defaultTestServiceName, 966 ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm}, 967 FilterChains: []*v3listenerpb.FilterChain{{ 968 Name: "filter-chain-name", 969 Filters: []*v3listenerpb.Filter{{ 970 Name: wellknown.HTTPConnectionManager, 971 ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm}, 972 }}, 973 }}, 974 }} 975 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, nil) 976 977 // Ensure that there is no update from the resolver. 978 verifyNoUpdateFromResolver(ctx, t, stateCh) 979 } 980 981 // TestResolverWRR tests the case where the route configuration returned by the 982 // management server contains a set of weighted clusters. The test performs a 983 // bunch of RPCs using the cluster specifier returned by the resolver, and 984 // verifies the cluster distribution. 985 func (s) TestResolverWRR(t *testing.T) { 986 origNewWRR := rinternal.NewWRR 987 rinternal.NewWRR = testutils.NewTestWRR 988 defer func() { rinternal.NewWRR = origNewWRR }() 989 990 // Spin up an xDS management server for the test. 991 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 992 defer cancel() 993 nodeID := uuid.New().String() 994 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 995 996 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 997 998 // Configure resources on the management server. 999 listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)} 1000 routes := []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{ 1001 RouteConfigName: defaultTestRouteConfigName, 1002 ListenerName: defaultTestServiceName, 1003 ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeWeightedCluster, 1004 WeightedClusters: map[string]int{"A": 75, "B": 25}, 1005 })} 1006 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, listeners, routes) 1007 1008 // Read the update pushed by the resolver to the ClientConn. 1009 cs := verifyUpdateFromResolver(ctx, t, stateCh, "") 1010 1011 // Make RPCs to verify WRR behavior in the cluster specifier. 1012 picks := map[string]int{} 1013 for i := 0; i < 100; i++ { 1014 res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"}) 1015 if err != nil { 1016 t.Fatalf("cs.SelectConfig(): %v", err) 1017 } 1018 picks[clustermanager.GetPickedClusterForTesting(res.Context)]++ 1019 res.OnCommitted() 1020 } 1021 want := map[string]int{"cluster:A": 75, "cluster:B": 25} 1022 if !cmp.Equal(picks, want) { 1023 t.Errorf("Picked clusters: %v; want: %v", picks, want) 1024 } 1025 } 1026 1027 const filterCfgPathFieldName = "path" 1028 const filterCfgErrorFieldName = "new_stream_error" 1029 1030 type filterCfg struct { 1031 httpfilter.FilterConfig 1032 path string 1033 newStreamErr error 1034 } 1035 1036 type filterBuilder struct { 1037 paths []string 1038 typeURL string 1039 } 1040 1041 func (fb *filterBuilder) TypeURLs() []string { return []string{fb.typeURL} } 1042 1043 func filterConfigFromProto(cfg proto.Message) (httpfilter.FilterConfig, error) { 1044 ts, ok := cfg.(*v3xdsxdstypepb.TypedStruct) 1045 if !ok { 1046 return nil, fmt.Errorf("unsupported filter config type: %T, want %T", cfg, &v3xdsxdstypepb.TypedStruct{}) 1047 } 1048 1049 if ts.GetValue() == nil { 1050 return filterCfg{}, nil 1051 } 1052 ret := filterCfg{} 1053 if v := ts.GetValue().GetFields()[filterCfgPathFieldName]; v != nil { 1054 ret.path = v.GetStringValue() 1055 } 1056 if v := ts.GetValue().GetFields()[filterCfgErrorFieldName]; v != nil { 1057 if v.GetStringValue() == "" { 1058 ret.newStreamErr = nil 1059 } else { 1060 ret.newStreamErr = fmt.Errorf("%s", v.GetStringValue()) 1061 } 1062 } 1063 return ret, nil 1064 } 1065 1066 func (*filterBuilder) ParseFilterConfig(cfg proto.Message) (httpfilter.FilterConfig, error) { 1067 return filterConfigFromProto(cfg) 1068 } 1069 1070 func (*filterBuilder) ParseFilterConfigOverride(override proto.Message) (httpfilter.FilterConfig, error) { 1071 return filterConfigFromProto(override) 1072 } 1073 1074 func (*filterBuilder) IsTerminal() bool { return false } 1075 1076 var _ httpfilter.ClientInterceptorBuilder = &filterBuilder{} 1077 1078 func (fb *filterBuilder) BuildClientInterceptor(config, override httpfilter.FilterConfig) (iresolver.ClientInterceptor, error) { 1079 if config == nil { 1080 panic("unexpected missing config") 1081 } 1082 1083 fi := &filterInterceptor{ 1084 parent: fb, 1085 pathCh: make(chan string, 10), 1086 } 1087 1088 fb.paths = append(fb.paths, "build:"+config.(filterCfg).path) 1089 err := config.(filterCfg).newStreamErr 1090 if override != nil { 1091 fb.paths = append(fb.paths, "override:"+override.(filterCfg).path) 1092 err = override.(filterCfg).newStreamErr 1093 } 1094 1095 fi.cfgPath = config.(filterCfg).path 1096 fi.err = err 1097 return fi, nil 1098 } 1099 1100 type filterInterceptor struct { 1101 parent *filterBuilder 1102 pathCh chan string 1103 cfgPath string 1104 err error 1105 } 1106 1107 func (fi *filterInterceptor) NewStream(ctx context.Context, ri iresolver.RPCInfo, done func(), newStream func(ctx context.Context, done func()) (iresolver.ClientStream, error)) (iresolver.ClientStream, error) { 1108 fi.parent.paths = append(fi.parent.paths, "newstream:"+fi.cfgPath) 1109 if fi.err != nil { 1110 return nil, fi.err 1111 } 1112 d := func() { 1113 fi.parent.paths = append(fi.parent.paths, "done:"+fi.cfgPath) 1114 done() 1115 } 1116 cs, err := newStream(ctx, d) 1117 if err != nil { 1118 return nil, err 1119 } 1120 return &clientStream{ClientStream: cs}, nil 1121 } 1122 1123 type clientStream struct { 1124 iresolver.ClientStream 1125 } 1126 1127 func (s) TestConfigSelector_FailureCases(t *testing.T) { 1128 const methodName = "1" 1129 1130 tests := []struct { 1131 name string 1132 listener *v3listenerpb.Listener 1133 wantErr string 1134 }{ 1135 { 1136 name: "route type RouteActionUnsupported invalid for client", 1137 listener: &v3listenerpb.Listener{ 1138 Name: defaultTestServiceName, 1139 ApiListener: &v3listenerpb.ApiListener{ 1140 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 1141 RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{ 1142 RouteConfig: &v3routepb.RouteConfiguration{ 1143 Name: defaultTestRouteConfigName, 1144 VirtualHosts: []*v3routepb.VirtualHost{{ 1145 Domains: []string{defaultTestServiceName}, 1146 Routes: []*v3routepb.Route{{ 1147 Match: &v3routepb.RouteMatch{ 1148 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: methodName}, 1149 }, 1150 Action: &v3routepb.Route_FilterAction{}, 1151 }}, 1152 }}, 1153 }}, 1154 HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter}, 1155 }), 1156 }, 1157 }, 1158 wantErr: "matched route does not have a supported route action type", 1159 }, 1160 { 1161 name: "route type RouteActionNonForwardingAction invalid for client", 1162 listener: &v3listenerpb.Listener{ 1163 Name: defaultTestServiceName, 1164 ApiListener: &v3listenerpb.ApiListener{ 1165 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 1166 RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{ 1167 RouteConfig: &v3routepb.RouteConfiguration{ 1168 Name: defaultTestRouteConfigName, 1169 VirtualHosts: []*v3routepb.VirtualHost{{ 1170 Domains: []string{defaultTestServiceName}, 1171 Routes: []*v3routepb.Route{{ 1172 Match: &v3routepb.RouteMatch{ 1173 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: methodName}, 1174 }, 1175 Action: &v3routepb.Route_NonForwardingAction{}, 1176 }}, 1177 }}, 1178 }}, 1179 HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter}, 1180 }), 1181 }, 1182 }, 1183 wantErr: "matched route does not have a supported route action type", 1184 }, 1185 } 1186 1187 for _, test := range tests { 1188 t.Run(test.name, func(t *testing.T) { 1189 // Spin up an xDS management server. 1190 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 1191 defer cancel() 1192 nodeID := uuid.New().String() 1193 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 1194 1195 // Build an xDS resolver. 1196 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 1197 1198 // Update the management server with a listener resource that 1199 // contains inline route configuration. 1200 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, []*v3listenerpb.Listener{test.listener}, nil) 1201 1202 // Ensure that the resolver pushes a state update to the channel. 1203 cs := verifyUpdateFromResolver(ctx, t, stateCh, "") 1204 1205 // Ensure that it returns the expected error. 1206 _, err := cs.SelectConfig(iresolver.RPCInfo{Method: methodName, Context: ctx}) 1207 if err := verifyResolverError(err, codes.Unavailable, test.wantErr, nodeID); err != nil { 1208 t.Fatal(err) 1209 } 1210 }) 1211 } 1212 } 1213 1214 func newHTTPFilter(t *testing.T, name, typeURL, path, err string) *v3httppb.HttpFilter { 1215 return &v3httppb.HttpFilter{ 1216 Name: name, 1217 ConfigType: &v3httppb.HttpFilter_TypedConfig{ 1218 TypedConfig: testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1219 TypeUrl: typeURL, 1220 Value: &structpb.Struct{ 1221 Fields: map[string]*structpb.Value{ 1222 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: path}}, 1223 filterCfgErrorFieldName: {Kind: &structpb.Value_StringValue{StringValue: err}}, 1224 }, 1225 }, 1226 }), 1227 }, 1228 } 1229 } 1230 1231 func (s) TestXDSResolverHTTPFilters(t *testing.T) { 1232 const methodName1 = "1" 1233 const methodName2 = "2" 1234 testFilterName := t.Name() 1235 1236 testCases := []struct { 1237 name string 1238 listener *v3listenerpb.Listener 1239 rpcRes map[string][][]string 1240 wantStreamErr string 1241 }{ 1242 { 1243 name: "NewStream error - ensure earlier interceptor Done is still called", 1244 listener: &v3listenerpb.Listener{ 1245 Name: defaultTestServiceName, 1246 ApiListener: &v3listenerpb.ApiListener{ 1247 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 1248 RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{ 1249 RouteConfig: &v3routepb.RouteConfiguration{ 1250 Name: defaultTestRouteConfigName, 1251 VirtualHosts: []*v3routepb.VirtualHost{{ 1252 Domains: []string{defaultTestServiceName}, 1253 Routes: []*v3routepb.Route{{ 1254 Match: &v3routepb.RouteMatch{ 1255 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: methodName1}, 1256 }, 1257 Action: &v3routepb.Route_Route{ 1258 Route: &v3routepb.RouteAction{ 1259 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{ 1260 WeightedClusters: &v3routepb.WeightedCluster{ 1261 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 1262 {Name: "A", Weight: wrapperspb.UInt32(1)}, 1263 {Name: "B", Weight: wrapperspb.UInt32(1)}, 1264 }, 1265 }, 1266 }, 1267 }, 1268 }, 1269 }}, 1270 }}, 1271 }}, 1272 HttpFilters: []*v3httppb.HttpFilter{ 1273 newHTTPFilter(t, "foo", testFilterName, "foo1", ""), 1274 newHTTPFilter(t, "bar", testFilterName, "bar1", "bar newstream err"), 1275 e2e.RouterHTTPFilter, 1276 }, 1277 }), 1278 }, 1279 }, 1280 rpcRes: map[string][][]string{ 1281 methodName1: { 1282 {"build:foo1", "build:bar1", "newstream:foo1", "newstream:bar1", "done:foo1"}, // err in bar1 NewStream() 1283 }, 1284 }, 1285 wantStreamErr: "bar newstream err", 1286 }, 1287 { 1288 name: "all overrides", 1289 listener: &v3listenerpb.Listener{ 1290 Name: defaultTestServiceName, 1291 ApiListener: &v3listenerpb.ApiListener{ 1292 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 1293 RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{ 1294 RouteConfig: &v3routepb.RouteConfiguration{ 1295 Name: defaultTestRouteConfigName, 1296 VirtualHosts: []*v3routepb.VirtualHost{{ 1297 Domains: []string{defaultTestServiceName}, 1298 Routes: []*v3routepb.Route{ 1299 { 1300 Match: &v3routepb.RouteMatch{ 1301 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: methodName1}, 1302 }, 1303 Action: &v3routepb.Route_Route{ 1304 Route: &v3routepb.RouteAction{ 1305 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{ 1306 WeightedClusters: &v3routepb.WeightedCluster{ 1307 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 1308 {Name: "A", Weight: wrapperspb.UInt32(1)}, 1309 {Name: "B", Weight: wrapperspb.UInt32(1)}, 1310 }, 1311 }, 1312 }, 1313 }, 1314 }, 1315 }, 1316 { 1317 Match: &v3routepb.RouteMatch{ 1318 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: methodName2}, 1319 }, 1320 Action: &v3routepb.Route_Route{ 1321 Route: &v3routepb.RouteAction{ 1322 ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{ 1323 WeightedClusters: &v3routepb.WeightedCluster{ 1324 Clusters: []*v3routepb.WeightedCluster_ClusterWeight{ 1325 {Name: "A", Weight: wrapperspb.UInt32(1)}, 1326 { 1327 Name: "B", 1328 Weight: wrapperspb.UInt32(1), 1329 TypedPerFilterConfig: map[string]*anypb.Any{ 1330 "foo": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1331 TypeUrl: testFilterName, 1332 Value: &structpb.Struct{ 1333 Fields: map[string]*structpb.Value{ 1334 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "foo4"}}, 1335 }, 1336 }, 1337 }), 1338 "bar": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1339 TypeUrl: testFilterName, 1340 Value: &structpb.Struct{ 1341 Fields: map[string]*structpb.Value{ 1342 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "bar4"}}, 1343 }, 1344 }, 1345 }), 1346 }, 1347 }, 1348 }, 1349 }, 1350 }, 1351 }, 1352 }, 1353 TypedPerFilterConfig: map[string]*anypb.Any{ 1354 "foo": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1355 TypeUrl: testFilterName, 1356 Value: &structpb.Struct{ 1357 Fields: map[string]*structpb.Value{ 1358 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "foo3"}}, 1359 filterCfgErrorFieldName: {Kind: &structpb.Value_StringValue{StringValue: ""}}, 1360 }, 1361 }, 1362 }), 1363 "bar": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1364 TypeUrl: testFilterName, 1365 Value: &structpb.Struct{ 1366 Fields: map[string]*structpb.Value{ 1367 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "bar3"}}, 1368 }, 1369 }, 1370 }), 1371 }, 1372 }, 1373 }, 1374 TypedPerFilterConfig: map[string]*anypb.Any{ 1375 "foo": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1376 TypeUrl: testFilterName, 1377 Value: &structpb.Struct{ 1378 Fields: map[string]*structpb.Value{ 1379 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "foo2"}}, 1380 filterCfgErrorFieldName: {Kind: &structpb.Value_StringValue{StringValue: ""}}, 1381 }, 1382 }, 1383 }), 1384 "bar": testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 1385 TypeUrl: testFilterName, 1386 Value: &structpb.Struct{ 1387 Fields: map[string]*structpb.Value{ 1388 filterCfgPathFieldName: {Kind: &structpb.Value_StringValue{StringValue: "bar2"}}, 1389 }, 1390 }, 1391 }), 1392 }, 1393 }}, 1394 }}, 1395 HttpFilters: []*v3httppb.HttpFilter{ 1396 newHTTPFilter(t, "foo", testFilterName, "foo1", "this is overridden to nil"), 1397 newHTTPFilter(t, "bar", testFilterName, "bar1", ""), 1398 e2e.RouterHTTPFilter, 1399 }, 1400 }), 1401 }, 1402 }, 1403 rpcRes: map[string][][]string{ 1404 methodName1: { 1405 {"build:foo1", "override:foo2", "build:bar1", "override:bar2", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1406 {"build:foo1", "override:foo2", "build:bar1", "override:bar2", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1407 }, 1408 methodName2: { 1409 {"build:foo1", "override:foo3", "build:bar1", "override:bar3", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1410 {"build:foo1", "override:foo4", "build:bar1", "override:bar4", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1411 {"build:foo1", "override:foo3", "build:bar1", "override:bar3", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1412 {"build:foo1", "override:foo4", "build:bar1", "override:bar4", "newstream:foo1", "newstream:bar1", "done:bar1", "done:foo1"}, 1413 }, 1414 }, 1415 }, 1416 } 1417 1418 for _, tc := range testCases { 1419 t.Run(tc.name, func(t *testing.T) { 1420 origNewWRR := rinternal.NewWRR 1421 rinternal.NewWRR = testutils.NewTestWRR 1422 defer func() { rinternal.NewWRR = origNewWRR }() 1423 1424 // Register a custom httpFilter builder for the test. 1425 fb := &filterBuilder{typeURL: testFilterName} 1426 httpfilter.Register(fb) 1427 1428 // Spin up an xDS management server. 1429 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 1430 defer cancel() 1431 nodeID := uuid.New().String() 1432 mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID) 1433 1434 // Build an xDS resolver. 1435 stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc) 1436 1437 // Update the management server with a listener resource that 1438 // contains an inline route configuration. 1439 configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, []*v3listenerpb.Listener{tc.listener}, nil) 1440 1441 // Ensure that the resolver pushes a state update to the channel. 1442 cs := verifyUpdateFromResolver(ctx, t, stateCh, "") 1443 1444 for method, wants := range tc.rpcRes { 1445 // Order of wants is non-deterministic. 1446 remainingWant := make([][]string, len(wants)) 1447 copy(remainingWant, wants) 1448 for n := range wants { 1449 res, err := cs.SelectConfig(iresolver.RPCInfo{Method: method, Context: ctx}) 1450 if err != nil { 1451 t.Fatalf("Unexpected error from cs.SelectConfig(_): %v", err) 1452 } 1453 1454 var doneFunc func() 1455 _, err = res.Interceptor.NewStream(ctx, iresolver.RPCInfo{}, func() {}, func(ctx context.Context, done func()) (iresolver.ClientStream, error) { 1456 doneFunc = done 1457 return nil, nil 1458 }) 1459 if tc.wantStreamErr != "" { 1460 if err == nil || !strings.Contains(err.Error(), tc.wantStreamErr) { 1461 t.Errorf("NewStream(...) = _, %v; want _, Contains(%v)", err, tc.wantStreamErr) 1462 } 1463 if err == nil { 1464 res.OnCommitted() 1465 doneFunc() 1466 } 1467 continue 1468 } 1469 if err != nil { 1470 t.Fatalf("unexpected error from Interceptor.NewStream: %v", err) 1471 1472 } 1473 res.OnCommitted() 1474 doneFunc() 1475 1476 gotPaths := fb.paths 1477 fb.paths = []string{} 1478 1479 // Confirm the desired path is found in remainingWant, and remove it. 1480 pass := false 1481 for i := range remainingWant { 1482 if cmp.Equal(gotPaths, remainingWant[i]) { 1483 remainingWant[i] = remainingWant[len(remainingWant)-1] 1484 remainingWant = remainingWant[:len(remainingWant)-1] 1485 pass = true 1486 break 1487 } 1488 } 1489 if !pass { 1490 t.Errorf("%q:%v - path:\n%v\nwant one of:\n%v", method, n, gotPaths, remainingWant) 1491 } 1492 } 1493 } 1494 }) 1495 } 1496 } 1497 1498 func newDurationP(d time.Duration) *time.Duration { 1499 return &d 1500 }