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  }