google.golang.org/grpc@v1.74.2/xds/internal/clients/xdsclient/test/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  	"fmt"
    24  	"net"
    25  	"strings"
    26  	"testing"
    27  
    28  	"github.com/google/uuid"
    29  	"google.golang.org/grpc/credentials/insecure"
    30  	"google.golang.org/grpc/xds/internal/clients"
    31  	"google.golang.org/grpc/xds/internal/clients/grpctransport"
    32  	"google.golang.org/grpc/xds/internal/clients/internal/testutils"
    33  	"google.golang.org/grpc/xds/internal/clients/internal/testutils/e2e"
    34  	"google.golang.org/grpc/xds/internal/clients/internal/testutils/fakeserver"
    35  	"google.golang.org/grpc/xds/internal/clients/xdsclient"
    36  	"google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource"
    37  	"google.golang.org/protobuf/types/known/anypb"
    38  
    39  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    40  	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    41  )
    42  
    43  // testLDSWatcher is a test watcher that registers two watches corresponding to
    44  // the names passed in at creation time on a valid update.
    45  type testLDSWatcher struct {
    46  	client           *xdsclient.XDSClient
    47  	name1, name2     string
    48  	lw1, lw2         *listenerWatcher
    49  	cancel1, cancel2 func()
    50  	updateCh         *testutils.Channel
    51  }
    52  
    53  func newTestLDSWatcher(client *xdsclient.XDSClient, name1, name2 string) *testLDSWatcher {
    54  	return &testLDSWatcher{
    55  		client:   client,
    56  		name1:    name1,
    57  		name2:    name2,
    58  		lw1:      newListenerWatcher(),
    59  		lw2:      newListenerWatcher(),
    60  		updateCh: testutils.NewChannelWithSize(1),
    61  	}
    62  }
    63  
    64  func (lw *testLDSWatcher) ResourceChanged(update xdsclient.ResourceData, onDone func()) {
    65  	lisData, ok := update.(*listenerResourceData)
    66  	if !ok {
    67  		lw.updateCh.Send(listenerUpdateErrTuple{resourceErr: fmt.Errorf("unexpected resource type: %T", update)})
    68  		onDone()
    69  		return
    70  	}
    71  	lw.updateCh.Send(listenerUpdateErrTuple{update: lisData.Resource})
    72  
    73  	lw.cancel1 = lw.client.WatchResource(xdsresource.V3ListenerURL, lw.name1, lw.lw1)
    74  	lw.cancel2 = lw.client.WatchResource(xdsresource.V3ListenerURL, lw.name2, lw.lw2)
    75  	onDone()
    76  }
    77  
    78  func (lw *testLDSWatcher) AmbientError(err error, onDone func()) {
    79  	// When used with a go-control-plane management server that continuously
    80  	// resends resources which are NACKed by the xDS client, using a `Replace()`
    81  	// here and in OnResourceDoesNotExist() simplifies tests which will have
    82  	// access to the most recently received error.
    83  	lw.updateCh.Replace(listenerUpdateErrTuple{ambientErr: err})
    84  	onDone()
    85  }
    86  
    87  func (lw *testLDSWatcher) ResourceError(_ error, onDone func()) {
    88  	lw.updateCh.Replace(listenerUpdateErrTuple{resourceErr: xdsresource.NewError(xdsresource.ErrorTypeResourceNotFound, "Listener not found in received response")})
    89  	onDone()
    90  }
    91  
    92  func (lw *testLDSWatcher) cancel() {
    93  	lw.cancel1()
    94  	lw.cancel2()
    95  }
    96  
    97  // TestWatchCallAnotherWatch tests the scenario where a watch is registered for
    98  // a resource, and more watches are registered from the first watch's callback.
    99  // The test verifies that this scenario does not lead to a deadlock.
   100  func (s) TestWatchCallAnotherWatch(t *testing.T) {
   101  	// Start an xDS management server and set the option to allow it to respond
   102  	// to requests which only specify a subset of the configured resources.
   103  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
   104  
   105  	nodeID := uuid.New().String()
   106  	authority := makeAuthorityName(t.Name())
   107  
   108  	resourceTypes := map[string]xdsclient.ResourceType{xdsresource.V3ListenerURL: listenerType}
   109  	si := clients.ServerIdentifier{
   110  		ServerURI:  mgmtServer.Address,
   111  		Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"},
   112  	}
   113  
   114  	configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}}
   115  	xdsClientConfig := xdsclient.Config{
   116  		Servers:          []xdsclient.ServerConfig{{ServerIdentifier: si}},
   117  		Node:             clients.Node{ID: nodeID},
   118  		TransportBuilder: grpctransport.NewBuilder(configs),
   119  		ResourceTypes:    resourceTypes,
   120  		// Xdstp style resource names used in this test use a slash removed
   121  		// version of t.Name as their authority, and the empty config
   122  		// results in the top-level xds server configuration being used for
   123  		// this authority.
   124  		Authorities: map[string]xdsclient.Authority{
   125  			authority: {XDSServers: []xdsclient.ServerConfig{}},
   126  		},
   127  	}
   128  
   129  	// Create an xDS client with the above config.
   130  	client, err := xdsclient.New(xdsClientConfig)
   131  	if err != nil {
   132  		t.Fatalf("Failed to create xDS client: %v", err)
   133  	}
   134  	defer client.Close()
   135  
   136  	// Configure the management server to return two listener resources,
   137  	// corresponding to the registered watches.
   138  	ldsNameNewStyle := makeNewStyleLDSName(authority)
   139  	resources := e2e.UpdateOptions{
   140  		NodeID: nodeID,
   141  		Listeners: []*v3listenerpb.Listener{
   142  			e2e.DefaultClientListener(ldsName, rdsName),
   143  			e2e.DefaultClientListener(ldsNameNewStyle, rdsNameNewStyle),
   144  		},
   145  		SkipValidation: true,
   146  	}
   147  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   148  	defer cancel()
   149  	if err := mgmtServer.Update(ctx, resources); err != nil {
   150  		t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err)
   151  	}
   152  
   153  	// Create a listener watcher that registers two more watches from
   154  	// the OnUpdate callback:
   155  	// - one for the same resource name as this watch, which would be
   156  	//   satisfied from xdsClient cache
   157  	// - the other for a different resource name, which would be
   158  	//   satisfied from the server
   159  	lw := newTestLDSWatcher(client, ldsName, ldsNameNewStyle)
   160  	defer lw.cancel()
   161  	ldsCancel := client.WatchResource(xdsresource.V3ListenerURL, ldsName, lw)
   162  	defer ldsCancel()
   163  
   164  	// Verify the contents of the received update for the all watchers.
   165  	// Verify the contents of the received update for the all watchers. The two
   166  	// resources returned differ only in the resource name. Therefore the
   167  	// expected update is the same for all the watchers.
   168  	wantUpdate12 := listenerUpdateErrTuple{
   169  		update: listenerUpdate{
   170  			RouteConfigName: rdsName,
   171  		},
   172  	}
   173  	// Verify the contents of the received update for the all watchers. The two
   174  	// resources returned differ only in the resource name. Therefore the
   175  	// expected update is the same for all the watchers.
   176  	wantUpdate3 := listenerUpdateErrTuple{
   177  		update: listenerUpdate{
   178  			RouteConfigName: rdsNameNewStyle,
   179  		},
   180  	}
   181  	if err := verifyListenerUpdate(ctx, lw.updateCh, wantUpdate12); err != nil {
   182  		t.Fatal(err)
   183  	}
   184  	if err := verifyListenerUpdate(ctx, lw.lw1.updateCh, wantUpdate12); err != nil {
   185  		t.Fatal(err)
   186  	}
   187  	if err := verifyListenerUpdate(ctx, lw.lw2.updateCh, wantUpdate3); err != nil {
   188  		t.Fatal(err)
   189  	}
   190  }
   191  
   192  // TestNodeProtoSentOnlyInFirstRequest verifies that a non-empty node proto gets
   193  // sent only on the first discovery request message on the ADS stream.
   194  //
   195  // It also verifies the same behavior holds after a stream restart.
   196  func (s) TestNodeProtoSentOnlyInFirstRequest(t *testing.T) {
   197  	// Create a restartable listener which can close existing connections.
   198  	l, err := net.Listen("tcp", "localhost:0")
   199  	if err != nil {
   200  		t.Fatalf("Error while listening. Err: %v", err)
   201  	}
   202  	lis := testutils.NewRestartableListener(l)
   203  
   204  	// Start a fake xDS management server with the above restartable listener.
   205  	//
   206  	// We are unable to use the go-control-plane server here, because it caches
   207  	// the node proto received in the first request message and adds it to
   208  	// subsequent requests before invoking the OnStreamRequest() callback.
   209  	// Therefore we cannot verify what is sent by the xDS client.
   210  	mgmtServer, cleanup, err := fakeserver.StartServer(lis)
   211  	if err != nil {
   212  		t.Fatalf("Failed to start fake xDS server: %v", err)
   213  	}
   214  	defer cleanup()
   215  
   216  	// Create bootstrap configuration pointing to the above management server.
   217  	nodeID := uuid.New().String()
   218  
   219  	resourceTypes := map[string]xdsclient.ResourceType{xdsresource.V3ListenerURL: listenerType}
   220  	si := clients.ServerIdentifier{
   221  		ServerURI:  mgmtServer.Address,
   222  		Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"},
   223  	}
   224  
   225  	configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}}
   226  	xdsClientConfig := xdsclient.Config{
   227  		Servers:          []xdsclient.ServerConfig{{ServerIdentifier: si}},
   228  		Node:             clients.Node{ID: nodeID},
   229  		TransportBuilder: grpctransport.NewBuilder(configs),
   230  		ResourceTypes:    resourceTypes,
   231  		// Xdstp resource names used in this test do not specify an
   232  		// authority. These will end up looking up an entry with the
   233  		// empty key in the authorities map. Having an entry with an
   234  		// empty key and empty configuration, results in these
   235  		// resources also using the top-level configuration.
   236  		Authorities: map[string]xdsclient.Authority{
   237  			"": {XDSServers: []xdsclient.ServerConfig{}},
   238  		},
   239  	}
   240  
   241  	// Create an xDS client with the above config.
   242  	client, err := xdsclient.New(xdsClientConfig)
   243  	if err != nil {
   244  		t.Fatalf("Failed to create xDS client: %v", err)
   245  	}
   246  	defer client.Close()
   247  
   248  	const (
   249  		serviceName     = "my-service-client-side-xds"
   250  		routeConfigName = "route-" + serviceName
   251  		clusterName     = "cluster-" + serviceName
   252  		serviceName2    = "my-service-client-side-xds-2"
   253  	)
   254  
   255  	// Register a watch for the Listener resource.
   256  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   257  	defer cancel()
   258  	watcher := newListenerWatcher()
   259  	ldsCancel1 := client.WatchResource(xdsresource.V3ListenerURL, serviceName, watcher)
   260  	defer ldsCancel1()
   261  
   262  	// Ensure the watch results in a discovery request with an empty node proto.
   263  	if err := readDiscoveryResponseAndCheckForNonEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil {
   264  		t.Fatal(err)
   265  	}
   266  
   267  	// Configure a listener resource on the fake xDS server.
   268  	lisAny, err := anypb.New(e2e.DefaultClientListener(serviceName, routeConfigName))
   269  	if err != nil {
   270  		t.Fatalf("Failed to marshal listener resource into an Any proto: %v", err)
   271  	}
   272  	mgmtServer.XDSResponseChan <- &fakeserver.Response{
   273  		Resp: &v3discoverypb.DiscoveryResponse{
   274  			TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   275  			VersionInfo: "1",
   276  			Resources:   []*anypb.Any{lisAny},
   277  		},
   278  	}
   279  
   280  	// The xDS client is expected to ACK the Listener resource. The discovery
   281  	// request corresponding to the ACK must contain a nil node proto.
   282  	if err := readDiscoveryResponseAndCheckForEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil {
   283  		t.Fatal(err)
   284  	}
   285  
   286  	// Register a watch for another Listener resource.
   287  	ldscancel2 := client.WatchResource(xdsresource.V3ListenerURL, serviceName2, watcher)
   288  	defer ldscancel2()
   289  
   290  	// Ensure the watch results in a discovery request with an empty node proto.
   291  	if err := readDiscoveryResponseAndCheckForEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil {
   292  		t.Fatal(err)
   293  	}
   294  
   295  	// Configure the other listener resource on the fake xDS server.
   296  	lisAny2, err := anypb.New(e2e.DefaultClientListener(serviceName2, routeConfigName))
   297  
   298  	if err != nil {
   299  		t.Fatalf("Failed to marshal route configuration resource into an Any proto: %v", err)
   300  	}
   301  
   302  	mgmtServer.XDSResponseChan <- &fakeserver.Response{
   303  		Resp: &v3discoverypb.DiscoveryResponse{
   304  			TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   305  			VersionInfo: "1",
   306  			Resources:   []*anypb.Any{lisAny2},
   307  		},
   308  	}
   309  
   310  	// Ensure the discovery request for the ACK contains an empty node proto.
   311  	if err := readDiscoveryResponseAndCheckForEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil {
   312  		t.Fatal(err)
   313  	}
   314  
   315  	// Stop the management server and expect the error callback to be invoked.
   316  	lis.Stop()
   317  	select {
   318  	case <-ctx.Done():
   319  		t.Fatal("Timeout when waiting for the connection error to be propagated to the watcher")
   320  	case <-watcher.ambientErrCh.C:
   321  	}
   322  	// Restart the management server.
   323  	lis.Restart()
   324  
   325  	// The xDS client is expected to re-request previously requested resources.
   326  	// Here, we expect 1 DiscoveryRequest messages with both the listener resources.
   327  	// The message should contain a non-nil node proto (since its the first
   328  	// request after restart).
   329  	//
   330  	// And since we don't push any responses on the response channel of the fake
   331  	// server, we do not expect any ACKs here.
   332  	if err := readDiscoveryResponseAndCheckForNonEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil {
   333  		t.Fatal(err)
   334  	}
   335  }
   336  
   337  // readDiscoveryResponseAndCheckForEmptyNodeProto reads a discovery request
   338  // message out of the provided reqCh. It returns an error if it fails to read a
   339  // message before the context deadline expires, or if the read message contains
   340  // a non-empty node proto.
   341  func readDiscoveryResponseAndCheckForEmptyNodeProto(ctx context.Context, reqCh *testutils.Channel) error {
   342  	v, err := reqCh.Receive(ctx)
   343  	if err != nil {
   344  		return fmt.Errorf("Timeout when waiting for a DiscoveryRequest message")
   345  	}
   346  	req := v.(*fakeserver.Request).Req.(*v3discoverypb.DiscoveryRequest)
   347  	if node := req.GetNode(); node != nil {
   348  		return fmt.Errorf("Node proto received in DiscoveryRequest message is %v, want empty node proto", node)
   349  	}
   350  	return nil
   351  }
   352  
   353  // readDiscoveryResponseAndCheckForNonEmptyNodeProto reads a discovery request
   354  // message out of the provided reqCh. It returns an error if it fails to read a
   355  // message before the context deadline expires, or if the read message contains
   356  // an empty node proto.
   357  func readDiscoveryResponseAndCheckForNonEmptyNodeProto(ctx context.Context, reqCh *testutils.Channel) error {
   358  	v, err := reqCh.Receive(ctx)
   359  	if err != nil {
   360  		return fmt.Errorf("Timeout when waiting for a DiscoveryRequest message")
   361  	}
   362  	req := v.(*fakeserver.Request).Req.(*v3discoverypb.DiscoveryRequest)
   363  	if node := req.GetNode(); node == nil {
   364  		return fmt.Errorf("Empty node proto received in DiscoveryRequest message, want non-empty node proto")
   365  	}
   366  	return nil
   367  }
   368  
   369  // Tests that the errors returned by the xDS client when watching a resource
   370  // contain the node ID that was used to create the client. This test covers two
   371  // scenarios:
   372  //
   373  //  1. When a watch is registered for an already registered resource type, but
   374  //     a new watch is registered with a type url which is not present in
   375  //     provided resource types.
   376  //  2. When a watch is registered for a resource name whose authority is not
   377  //     found in the xDS client config.
   378  func (s) TestWatchErrorsContainNodeID(t *testing.T) {
   379  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   380  
   381  	// Create bootstrap configuration pointing to the above management server.
   382  	nodeID := uuid.New().String()
   383  
   384  	resourceTypes := map[string]xdsclient.ResourceType{xdsresource.V3ListenerURL: listenerType}
   385  	si := clients.ServerIdentifier{
   386  		ServerURI:  mgmtServer.Address,
   387  		Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"},
   388  	}
   389  
   390  	configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}}
   391  	xdsClientConfig := xdsclient.Config{
   392  		Servers:          []xdsclient.ServerConfig{{ServerIdentifier: si}},
   393  		Node:             clients.Node{ID: nodeID},
   394  		TransportBuilder: grpctransport.NewBuilder(configs),
   395  		ResourceTypes:    resourceTypes,
   396  		// Xdstp resource names used in this test do not specify an
   397  		// authority. These will end up looking up an entry with the
   398  		// empty key in the authorities map. Having an entry with an
   399  		// empty key and empty configuration, results in these
   400  		// resources also using the top-level configuration.
   401  		Authorities: map[string]xdsclient.Authority{
   402  			"": {XDSServers: []xdsclient.ServerConfig{}},
   403  		},
   404  	}
   405  
   406  	// Create an xDS client with the above config.
   407  	client, err := xdsclient.New(xdsClientConfig)
   408  	if err != nil {
   409  		t.Fatalf("Failed to create xDS client: %v", err)
   410  	}
   411  	defer client.Close()
   412  
   413  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   414  	defer cancel()
   415  
   416  	t.Run("Right_Wrong_ResourceType_Implementations", func(t *testing.T) {
   417  		const listenerName = "listener-name"
   418  		watcher := newListenerWatcher()
   419  		client.WatchResource(xdsresource.V3ListenerURL, listenerName, watcher)
   420  
   421  		sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
   422  		defer sCancel()
   423  		select {
   424  		case <-sCtx.Done():
   425  		case <-watcher.updateCh.C:
   426  			t.Fatal("Unexpected resource update")
   427  		case <-watcher.resourceErrCh.C:
   428  			t.Fatal("Unexpected resource error")
   429  		}
   430  
   431  		client.WatchResource("non-existent-type-url", listenerName, watcher)
   432  		select {
   433  		case <-ctx.Done():
   434  			t.Fatal("Timeout when waiting for error callback to be invoked")
   435  		case u, ok := <-watcher.resourceErrCh.C:
   436  			if !ok {
   437  				t.Fatalf("got no update, wanted listener resource error from the management server")
   438  			}
   439  			gotErr := u.(listenerUpdateErrTuple).resourceErr
   440  			if !strings.Contains(gotErr.Error(), nodeID) {
   441  				t.Fatalf("Unexpected error: %v, want error with node ID: %q", err, nodeID)
   442  			}
   443  		}
   444  	})
   445  
   446  	t.Run("Missing_Authority", func(t *testing.T) {
   447  		const listenerName = "xdstp://nonexistant-authority/envoy.config.listener.v3.Listener/listener-name"
   448  		watcher := newListenerWatcher()
   449  		client.WatchResource(xdsresource.V3ListenerURL, listenerName, watcher)
   450  
   451  		select {
   452  		case <-ctx.Done():
   453  			t.Fatal("Timeout when waiting for error callback to be invoked")
   454  		case u, ok := <-watcher.resourceErrCh.C:
   455  			if !ok {
   456  				t.Fatalf("got no update, wanted listener resource error from the management server")
   457  			}
   458  			gotErr := u.(listenerUpdateErrTuple).resourceErr
   459  			if !strings.Contains(gotErr.Error(), nodeID) {
   460  				t.Fatalf("Unexpected error: %v, want error with node ID: %q", err, nodeID)
   461  			}
   462  		}
   463  	})
   464  }
   465  
   466  // erroringTransportBuilder is a transport builder which always returns a nil
   467  // transport along with an error.
   468  type erroringTransportBuilder struct{}
   469  
   470  func (*erroringTransportBuilder) Build(_ clients.ServerIdentifier) (clients.Transport, error) {
   471  	return nil, fmt.Errorf("failed to create transport")
   472  }
   473  
   474  // Tests that the errors returned by the xDS client when watching a resource
   475  // contain the node ID when channel creation to the management server fails.
   476  func (s) TestWatchErrorsContainNodeID_ChannelCreationFailure(t *testing.T) {
   477  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   478  
   479  	// Create bootstrap configuration pointing to the above management server.
   480  	nodeID := uuid.New().String()
   481  
   482  	resourceTypes := map[string]xdsclient.ResourceType{xdsresource.V3ListenerURL: listenerType}
   483  	si := clients.ServerIdentifier{
   484  		ServerURI:  mgmtServer.Address,
   485  		Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"},
   486  	}
   487  
   488  	xdsClientConfig := xdsclient.Config{
   489  		Servers:          []xdsclient.ServerConfig{{ServerIdentifier: si}},
   490  		Node:             clients.Node{ID: nodeID},
   491  		TransportBuilder: &erroringTransportBuilder{},
   492  		ResourceTypes:    resourceTypes,
   493  		// Xdstp resource names used in this test do not specify an
   494  		// authority. These will end up looking up an entry with the
   495  		// empty key in the authorities map. Having an entry with an
   496  		// empty key and empty configuration, results in these
   497  		// resources also using the top-level configuration.
   498  		Authorities: map[string]xdsclient.Authority{
   499  			"": {XDSServers: []xdsclient.ServerConfig{}},
   500  		},
   501  	}
   502  
   503  	// Create an xDS client with the above config.
   504  	client, err := xdsclient.New(xdsClientConfig)
   505  	if err != nil {
   506  		t.Fatalf("Failed to create xDS client: %v", err)
   507  	}
   508  	defer client.Close()
   509  
   510  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   511  	defer cancel()
   512  
   513  	const listenerName = "listener-name"
   514  	watcher := newListenerWatcher()
   515  	client.WatchResource(xdsresource.V3ListenerURL, listenerName, watcher)
   516  
   517  	select {
   518  	case <-ctx.Done():
   519  		t.Fatal("Timeout when waiting for error callback to be invoked")
   520  	case u, ok := <-watcher.resourceErrCh.C:
   521  		if !ok {
   522  			t.Fatalf("got no update, wanted listener resource error from the management server")
   523  		}
   524  		gotErr := u.(listenerUpdateErrTuple).resourceErr
   525  		if !strings.Contains(gotErr.Error(), nodeID) {
   526  			t.Fatalf("Unexpected error: %v, want error with node ID: %q", err, nodeID)
   527  		}
   528  	}
   529  }