google.golang.org/grpc@v1.74.2/xds/internal/clients/xdsclient/channel_test.go (about) 1 /* 2 * 3 * Copyright 2025 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 20 21 import ( 22 "context" 23 "fmt" 24 "net" 25 "strings" 26 "testing" 27 "time" 28 29 "github.com/google/go-cmp/cmp" 30 "github.com/google/go-cmp/cmp/cmpopts" 31 "github.com/google/uuid" 32 "google.golang.org/grpc/credentials/insecure" 33 "google.golang.org/grpc/xds/internal/clients" 34 "google.golang.org/grpc/xds/internal/clients/grpctransport" 35 "google.golang.org/grpc/xds/internal/clients/internal/testutils" 36 "google.golang.org/grpc/xds/internal/clients/internal/testutils/e2e" 37 "google.golang.org/grpc/xds/internal/clients/internal/testutils/fakeserver" 38 "google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource" 39 "google.golang.org/protobuf/testing/protocmp" 40 "google.golang.org/protobuf/types/known/anypb" 41 42 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 43 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 44 v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 45 v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 46 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 47 ) 48 49 // xdsChannelForTest creates an xdsChannel to the specified serverURI for 50 // testing purposes. 51 func xdsChannelForTest(t *testing.T, serverURI, nodeID string, watchExpiryTimeout time.Duration) *xdsChannel { 52 t.Helper() 53 54 // Create a grpc transport to the above management server. 55 si := clients.ServerIdentifier{ 56 ServerURI: serverURI, 57 Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}, 58 } 59 configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}} 60 tr, err := (grpctransport.NewBuilder(configs)).Build(si) 61 if err != nil { 62 t.Fatalf("Failed to create a transport for server config %v: %v", si, err) 63 } 64 65 serverCfg := ServerConfig{ 66 ServerIdentifier: si, 67 } 68 clientConfig := Config{ 69 Servers: []ServerConfig{serverCfg}, 70 Node: clients.Node{ID: nodeID}, 71 ResourceTypes: map[string]ResourceType{xdsresource.V3ListenerURL: listenerType}, 72 } 73 // Create an xdsChannel that uses everything set up above. 74 xc, err := newXDSChannel(xdsChannelOpts{ 75 transport: tr, 76 serverConfig: &serverCfg, 77 clientConfig: &clientConfig, 78 eventHandler: newTestEventHandler(), 79 watchExpiryTimeout: watchExpiryTimeout, 80 }) 81 if err != nil { 82 t.Fatalf("Failed to create xdsChannel: %v", err) 83 } 84 t.Cleanup(func() { xc.close() }) 85 return xc 86 } 87 88 // verifyUpdateAndMetadata verifies that the event handler received the expected 89 // updates and metadata. It checks that the received resource type matches the 90 // expected type, and that the received updates and metadata match the expected 91 // values. The function ignores the timestamp fields in the metadata, as those 92 // are expected to be different. 93 func verifyUpdateAndMetadata(ctx context.Context, t *testing.T, eh *testEventHandler, wantUpdates map[string]dataAndErrTuple, wantMD xdsresource.UpdateMetadata) { 94 t.Helper() 95 96 gotTyp, gotUpdates, gotMD, err := eh.waitForUpdate(ctx) 97 if err != nil { 98 t.Fatalf("Timeout when waiting for update callback to be invoked on the event handler") 99 } 100 101 if gotTyp != listenerType { 102 t.Fatalf("Got resource type %v, want %v", gotTyp, listenerType) 103 } 104 opts := cmp.Options{ 105 protocmp.Transform(), 106 cmpopts.EquateEmpty(), 107 cmpopts.EquateErrors(), 108 cmpopts.IgnoreFields(xdsresource.UpdateMetadata{}, "Timestamp"), 109 cmpopts.IgnoreFields(xdsresource.UpdateErrorMetadata{}, "Timestamp"), 110 } 111 if diff := cmp.Diff(wantUpdates, gotUpdates, opts); diff != "" { 112 t.Fatalf("Got unexpected diff in update (-want +got):\n%s\n want: %+v\n got: %+v", diff, wantUpdates, gotUpdates) 113 } 114 if diff := cmp.Diff(wantMD, gotMD, opts); diff != "" { 115 t.Fatalf("Got unexpected diff in update (-want +got):\n%s\n want: %v\n got: %v", diff, wantMD, gotMD) 116 } 117 } 118 119 // Tests different failure cases when creating a new xdsChannel. It checks that 120 // the xdsChannel creation fails when any of the required options (transport, 121 // serverConfig, bootstrapConfig, or resourceTypeGetter) are missing or nil. 122 func (s) TestChannel_New_FailureCases(t *testing.T) { 123 type fakeTransport struct { 124 clients.Transport 125 } 126 127 tests := []struct { 128 name string 129 opts xdsChannelOpts 130 wantErrStr string 131 }{ 132 { 133 name: "emptyTransport", 134 opts: xdsChannelOpts{}, 135 wantErrStr: "transport is nil", 136 }, 137 { 138 name: "emptyServerConfig", 139 opts: xdsChannelOpts{transport: &fakeTransport{}}, 140 wantErrStr: "serverConfig is nil", 141 }, 142 { 143 name: "emptyCConfig", 144 opts: xdsChannelOpts{ 145 transport: &fakeTransport{}, 146 serverConfig: &ServerConfig{}, 147 }, 148 wantErrStr: "clientConfig is nil", 149 }, 150 { 151 name: "emptyEventHandler", 152 opts: xdsChannelOpts{ 153 transport: &fakeTransport{}, 154 serverConfig: &ServerConfig{}, 155 clientConfig: &Config{}, 156 }, 157 wantErrStr: "eventHandler is nil", 158 }, 159 } 160 161 for _, test := range tests { 162 t.Run(test.name, func(t *testing.T) { 163 if _, err := newXDSChannel(test.opts); err == nil || !strings.Contains(err.Error(), test.wantErrStr) { 164 t.Fatalf("newXDSChannel() = %v, want %q", err, test.wantErrStr) 165 } 166 }) 167 } 168 } 169 170 // Tests different scenarios of the xdsChannel receiving a response from the 171 // management server. In all scenarios, the xdsChannel is expected to pass the 172 // received responses as-is to the resource parsing functionality specified by 173 // the resourceTypeGetter. 174 func (s) TestChannel_ADS_HandleResponseFromManagementServer(t *testing.T) { 175 const ( 176 listenerName1 = "listener-name-1" 177 listenerName2 = "listener-name-2" 178 routeName = "route-name" 179 clusterName = "cluster-name" 180 ) 181 var ( 182 badlyMarshaledResource = &anypb.Any{ 183 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 184 Value: []byte{1, 2, 3, 4}, 185 } 186 apiListener = &v3listenerpb.ApiListener{ 187 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 188 RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{ 189 RouteConfig: &v3routepb.RouteConfiguration{ 190 Name: routeName}, 191 }, 192 }), 193 } 194 listener1 = testutils.MarshalAny(t, &v3listenerpb.Listener{ 195 Name: listenerName1, 196 ApiListener: apiListener, 197 }) 198 listener2 = testutils.MarshalAny(t, &v3listenerpb.Listener{ 199 Name: listenerName2, 200 ApiListener: apiListener, 201 }) 202 ) 203 204 tests := []struct { 205 desc string 206 resourceNamesToRequest []string 207 managementServerResponse *v3discoverypb.DiscoveryResponse 208 wantUpdates map[string]dataAndErrTuple 209 wantMD xdsresource.UpdateMetadata 210 wantErr error 211 }{ 212 { 213 desc: "one bad resource - deserialization failure", 214 resourceNamesToRequest: []string{listenerName1}, 215 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 216 VersionInfo: "0", 217 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 218 Resources: []*anypb.Any{badlyMarshaledResource}, 219 }, 220 wantUpdates: nil, // No updates expected as the response runs into unmarshaling errors. 221 wantMD: xdsresource.UpdateMetadata{ 222 Status: xdsresource.ServiceStatusNACKed, 223 Version: "0", 224 ErrState: &xdsresource.UpdateErrorMetadata{ 225 Version: "0", 226 Err: cmpopts.AnyError, 227 }, 228 }, 229 wantErr: cmpopts.AnyError, 230 }, 231 { 232 desc: "one bad resource - validation failure", 233 resourceNamesToRequest: []string{listenerName1}, 234 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 235 VersionInfo: "0", 236 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 237 Resources: []*anypb.Any{testutils.MarshalAny(t, &v3listenerpb.Listener{ 238 Name: listenerName1, 239 ApiListener: &v3listenerpb.ApiListener{ 240 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 241 RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{}, 242 }), 243 }, 244 })}, 245 }, 246 wantUpdates: map[string]dataAndErrTuple{ 247 listenerName1: { 248 Err: cmpopts.AnyError, 249 }, 250 }, 251 wantMD: xdsresource.UpdateMetadata{ 252 Status: xdsresource.ServiceStatusNACKed, 253 Version: "0", 254 ErrState: &xdsresource.UpdateErrorMetadata{ 255 Version: "0", 256 Err: cmpopts.AnyError, 257 }, 258 }, 259 }, 260 { 261 desc: "two bad resources", 262 resourceNamesToRequest: []string{listenerName1, listenerName2}, 263 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 264 VersionInfo: "0", 265 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 266 Resources: []*anypb.Any{ 267 badlyMarshaledResource, 268 testutils.MarshalAny(t, &v3listenerpb.Listener{ 269 Name: listenerName2, 270 ApiListener: &v3listenerpb.ApiListener{ 271 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 272 RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{}, 273 }), 274 }, 275 }), 276 }, 277 }, 278 wantUpdates: map[string]dataAndErrTuple{ 279 listenerName2: { 280 Err: cmpopts.AnyError, 281 }, 282 }, 283 wantMD: xdsresource.UpdateMetadata{ 284 Status: xdsresource.ServiceStatusNACKed, 285 Version: "0", 286 ErrState: &xdsresource.UpdateErrorMetadata{ 287 Version: "0", 288 Err: cmpopts.AnyError, 289 }, 290 }, 291 }, 292 { 293 desc: "one good resource", 294 resourceNamesToRequest: []string{listenerName1}, 295 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 296 VersionInfo: "0", 297 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 298 Resources: []*anypb.Any{listener1}, 299 }, 300 wantUpdates: map[string]dataAndErrTuple{ 301 listenerName1: { 302 Resource: &listenerResourceData{Resource: listenerUpdate{ 303 RouteConfigName: routeName, 304 Raw: listener1.GetValue(), 305 }}, 306 }, 307 }, 308 wantMD: xdsresource.UpdateMetadata{ 309 Status: xdsresource.ServiceStatusACKed, 310 Version: "0", 311 }, 312 }, 313 { 314 desc: "one good and one bad - deserialization failure", 315 resourceNamesToRequest: []string{listenerName1, listenerName2}, 316 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 317 VersionInfo: "0", 318 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 319 Resources: []*anypb.Any{ 320 badlyMarshaledResource, 321 listener2, 322 }, 323 }, 324 wantUpdates: map[string]dataAndErrTuple{ 325 listenerName2: { 326 Resource: &listenerResourceData{Resource: listenerUpdate{ 327 RouteConfigName: routeName, 328 Raw: listener2.GetValue(), 329 }}, 330 }, 331 }, 332 wantMD: xdsresource.UpdateMetadata{ 333 Status: xdsresource.ServiceStatusNACKed, 334 Version: "0", 335 ErrState: &xdsresource.UpdateErrorMetadata{ 336 Version: "0", 337 Err: cmpopts.AnyError, 338 }, 339 }, 340 }, 341 { 342 desc: "one good and one bad - validation failure", 343 resourceNamesToRequest: []string{listenerName1, listenerName2}, 344 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 345 VersionInfo: "0", 346 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 347 Resources: []*anypb.Any{ 348 testutils.MarshalAny(t, &v3listenerpb.Listener{ 349 Name: listenerName1, 350 ApiListener: &v3listenerpb.ApiListener{ 351 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 352 RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{}, 353 }), 354 }, 355 }), 356 listener2, 357 }, 358 }, 359 wantUpdates: map[string]dataAndErrTuple{ 360 listenerName1: {Err: cmpopts.AnyError}, 361 listenerName2: { 362 Resource: &listenerResourceData{Resource: listenerUpdate{ 363 RouteConfigName: routeName, 364 Raw: listener2.GetValue(), 365 }}, 366 }, 367 }, 368 wantMD: xdsresource.UpdateMetadata{ 369 Status: xdsresource.ServiceStatusNACKed, 370 Version: "0", 371 ErrState: &xdsresource.UpdateErrorMetadata{ 372 Version: "0", 373 Err: cmpopts.AnyError, 374 }, 375 }, 376 }, 377 { 378 desc: "two good resources", 379 resourceNamesToRequest: []string{listenerName1, listenerName2}, 380 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 381 VersionInfo: "0", 382 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 383 Resources: []*anypb.Any{listener1, listener2}, 384 }, 385 wantUpdates: map[string]dataAndErrTuple{ 386 listenerName1: { 387 Resource: &listenerResourceData{Resource: listenerUpdate{ 388 RouteConfigName: routeName, 389 Raw: listener1.GetValue(), 390 }}, 391 }, 392 listenerName2: { 393 Resource: &listenerResourceData{Resource: listenerUpdate{ 394 RouteConfigName: routeName, 395 Raw: listener2.GetValue(), 396 }}, 397 }, 398 }, 399 wantMD: xdsresource.UpdateMetadata{ 400 Status: xdsresource.ServiceStatusACKed, 401 Version: "0", 402 }, 403 }, 404 { 405 desc: "two resources when we requested one", 406 resourceNamesToRequest: []string{listenerName1}, 407 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 408 VersionInfo: "0", 409 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 410 Resources: []*anypb.Any{listener1, listener2}, 411 }, 412 wantUpdates: map[string]dataAndErrTuple{ 413 listenerName1: { 414 Resource: &listenerResourceData{Resource: listenerUpdate{ 415 RouteConfigName: routeName, 416 Raw: listener1.GetValue(), 417 }}, 418 }, 419 listenerName2: { 420 Resource: &listenerResourceData{Resource: listenerUpdate{ 421 RouteConfigName: routeName, 422 Raw: listener2.GetValue(), 423 }}, 424 }, 425 }, 426 wantMD: xdsresource.UpdateMetadata{ 427 Status: xdsresource.ServiceStatusACKed, 428 Version: "0", 429 }, 430 }, 431 } 432 433 for _, test := range tests { 434 t.Run(test.desc, func(t *testing.T) { 435 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 436 defer cancel() 437 438 // Start a fake xDS management server and configure the response it 439 // would send to its client. 440 mgmtServer, cleanup, err := fakeserver.StartServer(nil) 441 if err != nil { 442 t.Fatalf("Failed to start fake xDS server: %v", err) 443 } 444 defer cleanup() 445 t.Logf("Started xDS management server on %s", mgmtServer.Address) 446 mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse} 447 448 // Create an xdsChannel for the test with a long watch expiry timer 449 // to ensure that watches don't expire for the duration of the test. 450 nodeID := uuid.New().String() 451 xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestTimeout) 452 defer xc.close() 453 454 // Subscribe to the resources specified in the test table. 455 for _, name := range test.resourceNamesToRequest { 456 xc.subscribe(listenerType, name) 457 } 458 459 // Wait for an update callback on the event handler and verify the 460 // contents of the update and the metadata. 461 verifyUpdateAndMetadata(ctx, t, xc.eventHandler.(*testEventHandler), test.wantUpdates, test.wantMD) 462 }) 463 } 464 } 465 466 // Tests that the xdsChannel correctly handles the expiry of a watch for a 467 // resource by ensuring that the watch expiry callback is invoked on the event 468 // handler with the expected resource type and name. 469 func (s) TestChannel_ADS_HandleResponseWatchExpiry(t *testing.T) { 470 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 471 defer cancel() 472 473 // Start an xDS management server, but do not configure any resources on it. 474 // This will result in the watch for a resource to timeout. 475 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 476 477 // Create an xdsChannel for the test with a short watch expiry timer to 478 // ensure that the test does not run very long, as it needs to wait for the 479 // watch to expire. 480 nodeID := uuid.New().String() 481 xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestShortTimeout) 482 defer xc.close() 483 484 // Subscribe to a listener resource. 485 const listenerName = "listener-name" 486 xc.subscribe(listenerType, listenerName) 487 488 // Wait for the watch expiry callback on the authority to be invoked and 489 // verify that the watch expired for the expected resource name and type. 490 eventHandler := xc.eventHandler.(*testEventHandler) 491 gotTyp, gotName, err := eventHandler.waitForResourceDoesNotExist(ctx) 492 if err != nil { 493 t.Fatal("Timeout when waiting for the watch expiry callback to be invoked on the xDS client") 494 } 495 496 if gotTyp != listenerType { 497 t.Fatalf("Got type %v, want %v", gotTyp, listenerType) 498 } 499 if gotName != listenerName { 500 t.Fatalf("Got name %v, want %v", gotName, listenerName) 501 } 502 } 503 504 // Tests that the xdsChannel correctly handles stream failures by ensuring that 505 // the stream failure callback is invoked on the event handler. 506 func (s) TestChannel_ADS_StreamFailure(t *testing.T) { 507 ctx, cancel := context.WithTimeout(context.Background(), 20000*defaultTestTimeout) 508 defer cancel() 509 510 // Start an xDS management server with a restartable listener to simulate 511 // connection failures. 512 l, err := net.Listen("tcp", "localhost:0") 513 if err != nil { 514 t.Fatalf("net.Listen() failed: %v", err) 515 } 516 lis := testutils.NewRestartableListener(l) 517 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: lis}) 518 519 // Configure a listener resource on the management server. 520 const listenerResourceName = "test-listener-resource" 521 const routeConfigurationName = "test-route-configuration-resource" 522 nodeID := uuid.New().String() 523 resources := e2e.UpdateOptions{ 524 NodeID: nodeID, 525 Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(listenerResourceName, routeConfigurationName)}, 526 SkipValidation: true, 527 } 528 if err := mgmtServer.Update(ctx, resources); err != nil { 529 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 530 } 531 532 // Create an xdsChannel for the test with a long watch expiry timer 533 // to ensure that watches don't expire for the duration of the test. 534 xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2000*defaultTestTimeout) 535 defer xc.close() 536 537 // Subscribe to the resource created above. 538 xc.subscribe(listenerType, listenerResourceName) 539 540 // Wait for an update callback on the event handler and verify the 541 // contents of the update and the metadata. 542 hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 543 RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{Rds: &v3httppb.Rds{ 544 ConfigSource: &v3corepb.ConfigSource{ 545 ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}}, 546 }, 547 RouteConfigName: routeConfigurationName, 548 }}, 549 }) 550 listenerResource, err := anypb.New(&v3listenerpb.Listener{ 551 Name: listenerResourceName, 552 ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm}, 553 }) 554 if err != nil { 555 t.Fatalf("Failed to create listener resource: %v", err) 556 } 557 558 wantUpdates := map[string]dataAndErrTuple{ 559 listenerResourceName: { 560 Resource: &listenerResourceData{ 561 Resource: listenerUpdate{ 562 RouteConfigName: routeConfigurationName, 563 Raw: listenerResource.GetValue(), 564 }, 565 }, 566 }, 567 } 568 wantMD := xdsresource.UpdateMetadata{ 569 Status: xdsresource.ServiceStatusACKed, 570 Version: "1", 571 } 572 573 eventHandler := xc.eventHandler.(*testEventHandler) 574 verifyUpdateAndMetadata(ctx, t, eventHandler, wantUpdates, wantMD) 575 576 lis.Stop() 577 if err := eventHandler.waitForStreamFailure(ctx); err != nil { 578 t.Fatalf("Timeout when waiting for the stream failure callback to be invoked on the xDS client: %v", err) 579 } 580 } 581 582 // Tests the behavior of the xdsChannel when a resource is unsubscribed. 583 // Verifies that when a previously subscribed resource is unsubscribed, a 584 // request is sent without the previously subscribed resource name. 585 func (s) TestChannel_ADS_ResourceUnsubscribe(t *testing.T) { 586 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 587 defer cancel() 588 589 // Start an xDS management server that uses a channel to inform the test 590 // about the specific LDS resource names being requested. 591 ldsResourcesCh := make(chan []string, 1) 592 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ 593 OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { 594 t.Logf("Received request for resources: %v of type %s", req.GetResourceNames(), req.GetTypeUrl()) 595 596 if req.TypeUrl != xdsresource.V3ListenerURL { 597 return fmt.Errorf("unexpected resource type URL: %q", req.TypeUrl) 598 } 599 600 // Make the most recently requested names available to the test. 601 ldsResourcesCh <- req.GetResourceNames() 602 return nil 603 }, 604 }) 605 606 // Configure two listener resources on the management server. 607 const listenerResourceName1 = "test-listener-resource-1" 608 const routeConfigurationName1 = "test-route-configuration-resource-1" 609 const listenerResourceName2 = "test-listener-resource-2" 610 const routeConfigurationName2 = "test-route-configuration-resource-2" 611 nodeID := uuid.New().String() 612 resources := e2e.UpdateOptions{ 613 NodeID: nodeID, 614 Listeners: []*v3listenerpb.Listener{ 615 e2e.DefaultClientListener(listenerResourceName1, routeConfigurationName1), 616 e2e.DefaultClientListener(listenerResourceName2, routeConfigurationName2), 617 }, 618 SkipValidation: true, 619 } 620 if err := mgmtServer.Update(ctx, resources); err != nil { 621 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 622 } 623 624 // Create an xdsChannel for the test with a long watch expiry timer 625 // to ensure that watches don't expire for the duration of the test. 626 xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestTimeout) 627 defer xc.close() 628 629 // Subscribe to the resources created above and verify that a request is 630 // sent for the same. 631 xc.subscribe(listenerType, listenerResourceName1) 632 xc.subscribe(listenerType, listenerResourceName2) 633 if err := waitForResourceNames(ctx, ldsResourcesCh, []string{listenerResourceName1, listenerResourceName2}); err != nil { 634 t.Fatal(err) 635 } 636 637 // Wait for the above resources to be ACKed. 638 if err := waitForResourceNames(ctx, ldsResourcesCh, []string{listenerResourceName1, listenerResourceName2}); err != nil { 639 t.Fatal(err) 640 } 641 642 // Unsubscribe to one of the resources created above, and ensure that the 643 // other resource is still being requested. 644 xc.unsubscribe(listenerType, listenerResourceName1) 645 if err := waitForResourceNames(ctx, ldsResourcesCh, []string{listenerResourceName2}); err != nil { 646 t.Fatal(err) 647 } 648 649 // Since the version on the management server for the above resource is not 650 // changed, we will not receive an update from it for the one resource that 651 // we are still requesting. 652 653 // Unsubscribe to the remaining resource, and ensure that no more resources 654 // are being requested. 655 xc.unsubscribe(listenerType, listenerResourceName2) 656 if err := waitForResourceNames(ctx, ldsResourcesCh, []string{}); err != nil { 657 t.Fatal(err) 658 } 659 } 660 661 // waitForResourceNames waits for the wantNames to be received on namesCh. 662 // Returns a non-nil error if the context expires before that. 663 func waitForResourceNames(ctx context.Context, namesCh chan []string, wantNames []string) error { 664 var lastRequestedNames []string 665 for ; ; <-time.After(defaultTestShortTimeout) { 666 select { 667 case <-ctx.Done(): 668 return fmt.Errorf("timeout waiting for resources %v to be requested from the management server. Last requested resources: %v", wantNames, lastRequestedNames) 669 case gotNames := <-namesCh: 670 if cmp.Equal(gotNames, wantNames, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(s1, s2 string) bool { return s1 < s2 })) { 671 return nil 672 } 673 lastRequestedNames = gotNames 674 } 675 } 676 } 677 678 // newTestEventHandler creates a new testEventHandler instance with the 679 // necessary channels for testing the xdsChannel. 680 func newTestEventHandler() *testEventHandler { 681 return &testEventHandler{ 682 typeCh: make(chan ResourceType, 1), 683 updateCh: make(chan map[string]dataAndErrTuple, 1), 684 mdCh: make(chan xdsresource.UpdateMetadata, 1), 685 nameCh: make(chan string, 1), 686 connErrCh: make(chan error, 1), 687 } 688 } 689 690 // testEventHandler is a struct that implements the xdsChannelEventhandler 691 // interface. It is used to receive events from an xdsChannel, and has multiple 692 // channels on which it makes these events available to the test. 693 type testEventHandler struct { 694 typeCh chan ResourceType // Resource type of an update or resource-does-not-exist error. 695 updateCh chan map[string]dataAndErrTuple // Resource updates. 696 mdCh chan xdsresource.UpdateMetadata // Metadata from an update. 697 nameCh chan string // Name of the non-existent resource. 698 connErrCh chan error // Connectivity error. 699 700 } 701 702 func (ta *testEventHandler) adsStreamFailure(err error) { 703 ta.connErrCh <- err 704 } 705 706 func (ta *testEventHandler) waitForStreamFailure(ctx context.Context) error { 707 select { 708 case <-ctx.Done(): 709 return ctx.Err() 710 case <-ta.connErrCh: 711 } 712 return nil 713 } 714 715 func (ta *testEventHandler) adsResourceUpdate(typ ResourceType, updates map[string]dataAndErrTuple, md xdsresource.UpdateMetadata, onDone func()) { 716 ta.typeCh <- typ 717 ta.updateCh <- updates 718 ta.mdCh <- md 719 onDone() 720 } 721 722 // waitForUpdate waits for the next resource update event from the xdsChannel. 723 // It returns the resource type, the resource updates, and the update metadata. 724 // If the context is canceled, it returns an error. 725 func (ta *testEventHandler) waitForUpdate(ctx context.Context) (ResourceType, map[string]dataAndErrTuple, xdsresource.UpdateMetadata, error) { 726 var typ ResourceType 727 var updates map[string]dataAndErrTuple 728 var md xdsresource.UpdateMetadata 729 730 select { 731 case typ = <-ta.typeCh: 732 case <-ctx.Done(): 733 return ResourceType{}, nil, xdsresource.UpdateMetadata{}, ctx.Err() 734 } 735 736 select { 737 case updates = <-ta.updateCh: 738 case <-ctx.Done(): 739 return ResourceType{}, nil, xdsresource.UpdateMetadata{}, ctx.Err() 740 } 741 742 select { 743 case md = <-ta.mdCh: 744 case <-ctx.Done(): 745 return ResourceType{}, nil, xdsresource.UpdateMetadata{}, ctx.Err() 746 } 747 return typ, updates, md, nil 748 } 749 750 func (ta *testEventHandler) adsResourceDoesNotExist(typ ResourceType, name string) { 751 ta.typeCh <- typ 752 ta.nameCh <- name 753 } 754 755 // waitForResourceDoesNotExist waits for the next resource-does-not-exist event 756 // from the xdsChannel. It returns the resource type and the resource name. If 757 // the context is canceled, it returns an error. 758 func (ta *testEventHandler) waitForResourceDoesNotExist(ctx context.Context) (ResourceType, string, error) { 759 var typ ResourceType 760 var name string 761 762 select { 763 case typ = <-ta.typeCh: 764 case <-ctx.Done(): 765 return ResourceType{}, "", ctx.Err() 766 } 767 768 select { 769 case name = <-ta.nameCh: 770 case <-ctx.Done(): 771 return ResourceType{}, "", ctx.Err() 772 } 773 return typ, name, nil 774 }