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