google.golang.org/grpc@v1.72.2/xds/internal/xdsclient/tests/misc_watchers_test.go (about) 1 /* 2 * 3 * Copyright 2022 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 xdsclient_test 20 21 import ( 22 "context" 23 "encoding/json" 24 "fmt" 25 "strings" 26 "testing" 27 28 "github.com/google/uuid" 29 "google.golang.org/grpc" 30 "google.golang.org/grpc/internal/testutils" 31 "google.golang.org/grpc/internal/testutils/xds/e2e" 32 "google.golang.org/grpc/internal/testutils/xds/fakeserver" 33 "google.golang.org/grpc/internal/xds/bootstrap" 34 "google.golang.org/grpc/xds/internal" 35 xdstestutils "google.golang.org/grpc/xds/internal/testutils" 36 "google.golang.org/grpc/xds/internal/xdsclient" 37 xdsclientinternal "google.golang.org/grpc/xds/internal/xdsclient/internal" 38 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource" 39 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version" 40 "google.golang.org/protobuf/types/known/anypb" 41 42 v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 43 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 44 ) 45 46 var ( 47 // Resource type implementations retrieved from the resource type map in the 48 // internal package, which is initialized when the individual resource types 49 // are created. 50 listenerResourceType = internal.ResourceTypeMapForTesting[version.V3ListenerURL].(xdsresource.Type) 51 routeConfigResourceType = internal.ResourceTypeMapForTesting[version.V3RouteConfigURL].(xdsresource.Type) 52 ) 53 54 // This route configuration watcher registers two watches corresponding to the 55 // names passed in at creation time on a valid update. 56 type testRouteConfigWatcher struct { 57 client xdsclient.XDSClient 58 name1, name2 string 59 rcw1, rcw2 *routeConfigWatcher 60 cancel1, cancel2 func() 61 updateCh *testutils.Channel 62 } 63 64 func newTestRouteConfigWatcher(client xdsclient.XDSClient, name1, name2 string) *testRouteConfigWatcher { 65 return &testRouteConfigWatcher{ 66 client: client, 67 name1: name1, 68 name2: name2, 69 rcw1: newRouteConfigWatcher(), 70 rcw2: newRouteConfigWatcher(), 71 updateCh: testutils.NewChannel(), 72 } 73 } 74 75 func (rw *testRouteConfigWatcher) OnUpdate(update *xdsresource.RouteConfigResourceData, onDone xdsresource.OnDoneFunc) { 76 rw.updateCh.Send(routeConfigUpdateErrTuple{update: update.Resource}) 77 78 rw.cancel1 = xdsresource.WatchRouteConfig(rw.client, rw.name1, rw.rcw1) 79 rw.cancel2 = xdsresource.WatchRouteConfig(rw.client, rw.name2, rw.rcw2) 80 onDone() 81 } 82 83 func (rw *testRouteConfigWatcher) OnError(err error, onDone xdsresource.OnDoneFunc) { 84 // When used with a go-control-plane management server that continuously 85 // resends resources which are NACKed by the xDS client, using a `Replace()` 86 // here and in OnResourceDoesNotExist() simplifies tests which will have 87 // access to the most recently received error. 88 rw.updateCh.Replace(routeConfigUpdateErrTuple{err: err}) 89 onDone() 90 } 91 92 func (rw *testRouteConfigWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) { 93 rw.updateCh.Replace(routeConfigUpdateErrTuple{err: xdsresource.NewError(xdsresource.ErrorTypeResourceNotFound, "RouteConfiguration not found in received response")}) 94 onDone() 95 } 96 97 func (rw *testRouteConfigWatcher) cancel() { 98 rw.cancel1() 99 rw.cancel2() 100 } 101 102 // TestWatchCallAnotherWatch tests the scenario where a watch is registered for 103 // a resource, and more watches are registered from the first watch's callback. 104 // The test verifies that this scenario does not lead to a deadlock. 105 func (s) TestWatchCallAnotherWatch(t *testing.T) { 106 // Start an xDS management server and set the option to allow it to respond 107 // to requests which only specify a subset of the configured resources. 108 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) 109 110 nodeID := uuid.New().String() 111 authority := makeAuthorityName(t.Name()) 112 bc, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{ 113 Servers: []byte(fmt.Sprintf(`[{ 114 "server_uri": %q, 115 "channel_creds": [{"type": "insecure"}] 116 }]`, mgmtServer.Address)), 117 Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)), 118 Authorities: map[string]json.RawMessage{ 119 // Xdstp style resource names used in this test use a slash removed 120 // version of t.Name as their authority, and the empty config 121 // results in the top-level xds server configuration being used for 122 // this authority. 123 authority: []byte(`{}`), 124 }, 125 }) 126 if err != nil { 127 t.Fatalf("Failed to create bootstrap configuration: %v", err) 128 } 129 130 // Create an xDS client with the above bootstrap contents. 131 config, err := bootstrap.NewConfigFromContents(bc) 132 if err != nil { 133 t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) 134 } 135 pool := xdsclient.NewPool(config) 136 client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ 137 Name: t.Name(), 138 }) 139 if err != nil { 140 t.Fatalf("Failed to create xDS client: %v", err) 141 } 142 defer close() 143 144 // Configure the management server to respond with route config resources. 145 ldsNameNewStyle := makeNewStyleLDSName(authority) 146 rdsNameNewStyle := makeNewStyleRDSName(authority) 147 resources := e2e.UpdateOptions{ 148 NodeID: nodeID, 149 Routes: []*v3routepb.RouteConfiguration{ 150 e2e.DefaultRouteConfig(rdsName, ldsName, cdsName), 151 e2e.DefaultRouteConfig(rdsNameNewStyle, ldsNameNewStyle, cdsName), 152 }, 153 SkipValidation: true, 154 } 155 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 156 defer cancel() 157 if err := mgmtServer.Update(ctx, resources); err != nil { 158 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 159 } 160 161 // Create a route configuration watcher that registers two more watches from 162 // the OnUpdate callback: 163 // - one for the same resource name as this watch, which would be 164 // satisfied from xdsClient cache 165 // - the other for a different resource name, which would be 166 // satisfied from the server 167 rw := newTestRouteConfigWatcher(client, rdsName, rdsNameNewStyle) 168 defer rw.cancel() 169 rdsCancel := xdsresource.WatchRouteConfig(client, rdsName, rw) 170 defer rdsCancel() 171 172 // Verify the contents of the received update for the all watchers. 173 wantUpdate12 := routeConfigUpdateErrTuple{ 174 update: xdsresource.RouteConfigUpdate{ 175 VirtualHosts: []*xdsresource.VirtualHost{ 176 { 177 Domains: []string{ldsName}, 178 Routes: []*xdsresource.Route{ 179 { 180 Prefix: newStringP("/"), 181 ActionType: xdsresource.RouteActionRoute, 182 WeightedClusters: map[string]xdsresource.WeightedCluster{cdsName: {Weight: 100}}, 183 }, 184 }, 185 }, 186 }, 187 }, 188 } 189 wantUpdate3 := routeConfigUpdateErrTuple{ 190 update: xdsresource.RouteConfigUpdate{ 191 VirtualHosts: []*xdsresource.VirtualHost{ 192 { 193 Domains: []string{ldsNameNewStyle}, 194 Routes: []*xdsresource.Route{ 195 { 196 Prefix: newStringP("/"), 197 ActionType: xdsresource.RouteActionRoute, 198 WeightedClusters: map[string]xdsresource.WeightedCluster{cdsName: {Weight: 100}}, 199 }, 200 }, 201 }, 202 }, 203 }, 204 } 205 if err := verifyRouteConfigUpdate(ctx, rw.updateCh, wantUpdate12); err != nil { 206 t.Fatal(err) 207 } 208 if err := verifyRouteConfigUpdate(ctx, rw.rcw1.updateCh, wantUpdate12); err != nil { 209 t.Fatal(err) 210 } 211 if err := verifyRouteConfigUpdate(ctx, rw.rcw2.updateCh, wantUpdate3); err != nil { 212 t.Fatal(err) 213 } 214 } 215 216 // TestNodeProtoSentOnlyInFirstRequest verifies that a non-empty node proto gets 217 // sent only on the first discovery request message on the ADS stream. 218 // 219 // It also verifies the same behavior holds after a stream restart. 220 func (s) TestNodeProtoSentOnlyInFirstRequest(t *testing.T) { 221 // Create a restartable listener which can close existing connections. 222 l, err := testutils.LocalTCPListener() 223 if err != nil { 224 t.Fatalf("testutils.LocalTCPListener() failed: %v", err) 225 } 226 lis := testutils.NewRestartableListener(l) 227 228 // Start a fake xDS management server with the above restartable listener. 229 // 230 // We are unable to use the go-control-plane server here, because it caches 231 // the node proto received in the first request message and adds it to 232 // subsequent requests before invoking the OnStreamRequest() callback. 233 // Therefore we cannot verify what is sent by the xDS client. 234 mgmtServer, cleanup, err := fakeserver.StartServer(lis) 235 if err != nil { 236 t.Fatalf("Failed to start fake xDS server: %v", err) 237 } 238 defer cleanup() 239 240 // Create a bootstrap file in a temporary directory. 241 nodeID := uuid.New().String() 242 bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 243 244 // Create an xDS client with the above bootstrap contents. 245 config, err := bootstrap.NewConfigFromContents(bc) 246 if err != nil { 247 t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) 248 } 249 pool := xdsclient.NewPool(config) 250 client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ 251 Name: t.Name(), 252 }) 253 if err != nil { 254 t.Fatalf("Failed to create xDS client: %v", err) 255 } 256 defer close() 257 258 const ( 259 serviceName = "my-service-client-side-xds" 260 routeConfigName = "route-" + serviceName 261 clusterName = "cluster-" + serviceName 262 ) 263 264 // Register a watch for the Listener resource. 265 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 266 defer cancel() 267 watcher := xdstestutils.NewTestResourceWatcher() 268 client.WatchResource(listenerResourceType, serviceName, watcher) 269 270 // Ensure the watch results in a discovery request with an empty node proto. 271 if err := readDiscoveryResponseAndCheckForNonEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil { 272 t.Fatal(err) 273 } 274 275 // Configure a listener resource on the fake xDS server. 276 lisAny, err := anypb.New(e2e.DefaultClientListener(serviceName, routeConfigName)) 277 if err != nil { 278 t.Fatalf("Failed to marshal listener resource into an Any proto: %v", err) 279 } 280 mgmtServer.XDSResponseChan <- &fakeserver.Response{ 281 Resp: &v3discoverypb.DiscoveryResponse{ 282 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 283 VersionInfo: "1", 284 Resources: []*anypb.Any{lisAny}, 285 }, 286 } 287 288 // The xDS client is expected to ACK the Listener resource. The discovery 289 // request corresponding to the ACK must contain a nil node proto. 290 if err := readDiscoveryResponseAndCheckForEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil { 291 t.Fatal(err) 292 } 293 294 // Register a watch for a RouteConfiguration resource. 295 client.WatchResource(routeConfigResourceType, routeConfigName, watcher) 296 297 // Ensure the watch results in a discovery request with an empty node proto. 298 if err := readDiscoveryResponseAndCheckForEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil { 299 t.Fatal(err) 300 } 301 302 // Configure the route configuration resource on the fake xDS server. 303 rcAny, err := anypb.New(e2e.DefaultRouteConfig(routeConfigName, serviceName, clusterName)) 304 if err != nil { 305 t.Fatalf("Failed to marshal route configuration resource into an Any proto: %v", err) 306 } 307 mgmtServer.XDSResponseChan <- &fakeserver.Response{ 308 Resp: &v3discoverypb.DiscoveryResponse{ 309 TypeUrl: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", 310 VersionInfo: "1", 311 Resources: []*anypb.Any{rcAny}, 312 }, 313 } 314 315 // Ensure the discovery request for the ACK contains an empty node proto. 316 if err := readDiscoveryResponseAndCheckForEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil { 317 t.Fatal(err) 318 } 319 320 // Stop the management server and expect the error callback to be invoked. 321 lis.Stop() 322 select { 323 case <-ctx.Done(): 324 t.Fatal("Timeout when waiting for the connection error to be propagated to the watcher") 325 case <-watcher.ErrorCh: 326 } 327 328 // Restart the management server. 329 lis.Restart() 330 331 // The xDS client is expected to re-request previously requested resources. 332 // Hence, we expect two DiscoveryRequest messages (one for the Listener and 333 // one for the RouteConfiguration resource). The first message should contain 334 // a non-nil node proto and the second should contain a nil-proto. 335 // 336 // And since we don't push any responses on the response channel of the fake 337 // server, we do not expect any ACKs here. 338 if err := readDiscoveryResponseAndCheckForNonEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil { 339 t.Fatal(err) 340 } 341 if err := readDiscoveryResponseAndCheckForEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil { 342 t.Fatal(err) 343 } 344 } 345 346 // readDiscoveryResponseAndCheckForEmptyNodeProto reads a discovery request 347 // message out of the provided reqCh. It returns an error if it fails to read a 348 // message before the context deadline expires, or if the read message contains 349 // a non-empty node proto. 350 func readDiscoveryResponseAndCheckForEmptyNodeProto(ctx context.Context, reqCh *testutils.Channel) error { 351 v, err := reqCh.Receive(ctx) 352 if err != nil { 353 return fmt.Errorf("Timeout when waiting for a DiscoveryRequest message") 354 } 355 req := v.(*fakeserver.Request).Req.(*v3discoverypb.DiscoveryRequest) 356 if node := req.GetNode(); node != nil { 357 return fmt.Errorf("Node proto received in DiscoveryRequest message is %v, want empty node proto", node) 358 } 359 return nil 360 } 361 362 // readDiscoveryResponseAndCheckForNonEmptyNodeProto reads a discovery request 363 // message out of the provided reqCh. It returns an error if it fails to read a 364 // message before the context deadline expires, or if the read message contains 365 // an empty node proto. 366 func readDiscoveryResponseAndCheckForNonEmptyNodeProto(ctx context.Context, reqCh *testutils.Channel) error { 367 v, err := reqCh.Receive(ctx) 368 if err != nil { 369 return fmt.Errorf("Timeout when waiting for a DiscoveryRequest message") 370 } 371 req := v.(*fakeserver.Request).Req.(*v3discoverypb.DiscoveryRequest) 372 if node := req.GetNode(); node == nil { 373 return fmt.Errorf("Empty node proto received in DiscoveryRequest message, want non-empty node proto") 374 } 375 return nil 376 } 377 378 type testRouteConfigResourceType struct{} 379 380 func (testRouteConfigResourceType) TypeURL() string { return version.V3RouteConfigURL } 381 func (testRouteConfigResourceType) TypeName() string { return "RouteConfigResource" } 382 func (testRouteConfigResourceType) AllResourcesRequiredInSotW() bool { return false } 383 func (testRouteConfigResourceType) Decode(*xdsresource.DecodeOptions, *anypb.Any) (*xdsresource.DecodeResult, error) { 384 return nil, nil 385 } 386 387 // Tests that the errors returned by the xDS client when watching a resource 388 // contain the node ID that was used to create the client. This test covers two 389 // scenarios: 390 // 391 // 1. When a watch is registered for an already registered resource type, but 392 // this time with a different implementation, 393 // 2. When a watch is registered for a resource name whose authority is not 394 // found in the bootstrap configuration. 395 func (s) TestWatchErrorsContainNodeID(t *testing.T) { 396 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 397 398 // Create bootstrap configuration pointing to the above management server. 399 nodeID := uuid.New().String() 400 bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 401 402 // Create an xDS client with the above bootstrap contents. 403 config, err := bootstrap.NewConfigFromContents(bc) 404 if err != nil { 405 t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) 406 } 407 pool := xdsclient.NewPool(config) 408 client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ 409 Name: t.Name(), 410 }) 411 if err != nil { 412 t.Fatalf("Failed to create xDS client: %v", err) 413 } 414 defer close() 415 416 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 417 defer cancel() 418 419 t.Run("Multiple_ResourceType_Implementations", func(t *testing.T) { 420 const routeConfigName = "route-config-name" 421 watcher := xdstestutils.NewTestResourceWatcher() 422 client.WatchResource(routeConfigResourceType, routeConfigName, watcher) 423 424 sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) 425 defer sCancel() 426 select { 427 case <-sCtx.Done(): 428 case <-watcher.UpdateCh: 429 t.Fatal("Unexpected resource update") 430 case <-watcher.ErrorCh: 431 t.Fatal("Unexpected resource error") 432 case <-watcher.ResourceDoesNotExistCh: 433 t.Fatal("Unexpected resource does not exist") 434 } 435 436 client.WatchResource(testRouteConfigResourceType{}, routeConfigName, watcher) 437 select { 438 case <-ctx.Done(): 439 t.Fatal("Timeout when waiting for error callback to be invoked") 440 case err := <-watcher.ErrorCh: 441 if err == nil || !strings.Contains(err.Error(), nodeID) { 442 t.Fatalf("Unexpected error: %v, want error with node ID: %q", err, nodeID) 443 } 444 } 445 }) 446 447 t.Run("Missing_Authority", func(t *testing.T) { 448 const routeConfigName = "xdstp://nonexistant-authority/envoy.config.route.v3.RouteConfiguration/route-config-name" 449 watcher := xdstestutils.NewTestResourceWatcher() 450 client.WatchResource(routeConfigResourceType, routeConfigName, watcher) 451 452 select { 453 case <-ctx.Done(): 454 t.Fatal("Timeout when waiting for error callback to be invoked") 455 case err := <-watcher.ErrorCh: 456 if err == nil || !strings.Contains(err.Error(), nodeID) { 457 t.Fatalf("Unexpected error: %v, want error with node ID: %q", err, nodeID) 458 } 459 } 460 }) 461 } 462 463 // Tests that the errors returned by the xDS client when watching a resource 464 // contain the node ID when channel creation to the management server fails. 465 func (s) TestWatchErrorsContainNodeID_ChannelCreationFailure(t *testing.T) { 466 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 467 468 // Create bootstrap configuration pointing to the above management server. 469 nodeID := uuid.New().String() 470 bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 471 472 // Create an xDS client with the above bootstrap contents. 473 config, err := bootstrap.NewConfigFromContents(bc) 474 if err != nil { 475 t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) 476 } 477 pool := xdsclient.NewPool(config) 478 client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ 479 Name: t.Name(), 480 }) 481 if err != nil { 482 t.Fatalf("Failed to create xDS client: %v", err) 483 } 484 defer close() 485 486 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 487 defer cancel() 488 489 // Override the xDS channel dialer with one that always fails. 490 origDialer := xdsclientinternal.GRPCNewClient 491 xdsclientinternal.GRPCNewClient = func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { 492 return nil, fmt.Errorf("failed to create channel") 493 } 494 defer func() { xdsclientinternal.GRPCNewClient = origDialer }() 495 496 const routeConfigName = "route-config-name" 497 watcher := xdstestutils.NewTestResourceWatcher() 498 client.WatchResource(routeConfigResourceType, routeConfigName, watcher) 499 500 select { 501 case <-ctx.Done(): 502 t.Fatal("Timeout when waiting for error callback to be invoked") 503 case err := <-watcher.ErrorCh: 504 if err == nil || !strings.Contains(err.Error(), nodeID) { 505 t.Fatalf("Unexpected error: %v, want error with node ID: %q", err, nodeID) 506 } 507 } 508 }