google.golang.org/grpc@v1.74.2/xds/internal/xdsclient/tests/ads_stream_ack_nack_test.go (about)

     1  /*
     2   *
     3   * Copyright 2024 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  	"strings"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/google/uuid"
    30  	"google.golang.org/grpc"
    31  	"google.golang.org/grpc/internal/testutils"
    32  	"google.golang.org/grpc/internal/testutils/xds/e2e"
    33  	"google.golang.org/grpc/internal/xds/bootstrap"
    34  	"google.golang.org/grpc/xds/internal/xdsclient"
    35  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
    36  	"google.golang.org/protobuf/proto"
    37  	"google.golang.org/protobuf/testing/protocmp"
    38  
    39  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    40  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    41  	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    42  )
    43  
    44  // Creates an xDS client with the given bootstrap contents.
    45  func createXDSClient(t *testing.T, bootstrapContents []byte) xdsclient.XDSClient {
    46  	t.Helper()
    47  
    48  	config, err := bootstrap.NewConfigFromContents(bootstrapContents)
    49  	if err != nil {
    50  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
    51  	}
    52  	pool := xdsclient.NewPool(config)
    53  	client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
    54  		Name: t.Name(),
    55  	})
    56  	if err != nil {
    57  		t.Fatalf("Failed to create xDS client: %v", err)
    58  	}
    59  	t.Cleanup(close)
    60  	return client
    61  }
    62  
    63  // Tests simple ACK and NACK scenarios on the ADS stream:
    64  //  1. When a good response is received, i.e. once that is expected to be ACKed,
    65  //     the test verifies that an ACK is sent matching the version and nonce from
    66  //     the response.
    67  //  2. When a subsequent bad response is received, i.e. once is expected to be
    68  //     NACKed, the test verifies that a NACK is sent matching the previously
    69  //     ACKed version and current nonce from the response.
    70  //  3. When a subsequent good response is received, the test verifies that an
    71  //     ACK is sent matching the version and nonce from the current response.
    72  func (s) TestADS_ACK_NACK_Simple(t *testing.T) {
    73  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
    74  	defer cancel()
    75  
    76  	// Create an xDS management server listening on a local port. Configure the
    77  	// request and response handlers to push on channels that are inspected by
    78  	// the test goroutine to verify ACK version and nonce.
    79  	streamRequestCh := testutils.NewChannel()
    80  	streamResponseCh := testutils.NewChannel()
    81  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
    82  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
    83  			streamRequestCh.SendContext(ctx, req)
    84  			return nil
    85  		},
    86  		OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) {
    87  			streamResponseCh.SendContext(ctx, resp)
    88  		},
    89  	})
    90  
    91  	// Create a listener resource on the management server.
    92  	const listenerName = "listener"
    93  	const routeConfigName = "route-config"
    94  	nodeID := uuid.New().String()
    95  	listenerResource := e2e.DefaultClientListener(listenerName, routeConfigName)
    96  	resources := e2e.UpdateOptions{
    97  		NodeID:         nodeID,
    98  		Listeners:      []*v3listenerpb.Listener{listenerResource},
    99  		SkipValidation: true,
   100  	}
   101  	if err := mgmtServer.Update(ctx, resources); err != nil {
   102  		t.Fatal(err)
   103  	}
   104  
   105  	// Create an xDS client with bootstrap pointing to the above server.
   106  	bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
   107  	client := createXDSClient(t, bc)
   108  
   109  	// Register a watch for a listener resource.
   110  	lw := newListenerWatcher()
   111  	ldsCancel := xdsresource.WatchListener(client, listenerName, lw)
   112  	defer ldsCancel()
   113  
   114  	// Verify that the initial discovery request matches expectation.
   115  	r, err := streamRequestCh.Receive(ctx)
   116  	if err != nil {
   117  		t.Fatal("Timeout when waiting for the initial discovery request")
   118  	}
   119  	gotReq := r.(*v3discoverypb.DiscoveryRequest)
   120  	wantReq := &v3discoverypb.DiscoveryRequest{
   121  		VersionInfo: "",
   122  		Node: &v3corepb.Node{
   123  			Id:                   nodeID,
   124  			UserAgentName:        "gRPC Go",
   125  			UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
   126  			ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   127  		},
   128  		ResourceNames: []string{listenerName},
   129  		TypeUrl:       "type.googleapis.com/envoy.config.listener.v3.Listener",
   130  		ResponseNonce: "",
   131  	}
   132  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   133  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   134  	}
   135  
   136  	// Capture the version and nonce from the response.
   137  	r, err = streamResponseCh.Receive(ctx)
   138  	if err != nil {
   139  		t.Fatal("Timeout when waiting for a discovery response from the server")
   140  	}
   141  	gotResp := r.(*v3discoverypb.DiscoveryResponse)
   142  
   143  	// Verify that the ACK contains the appropriate version and nonce.
   144  	r, err = streamRequestCh.Receive(ctx)
   145  	if err != nil {
   146  		t.Fatal("Timeout when waiting for ACK")
   147  	}
   148  	gotReq = r.(*v3discoverypb.DiscoveryRequest)
   149  	wantReq.VersionInfo = gotResp.GetVersionInfo()
   150  	wantReq.ResponseNonce = gotResp.GetNonce()
   151  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   152  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   153  	}
   154  
   155  	// Verify the update received by the watcher.
   156  	wantUpdate := listenerUpdateErrTuple{
   157  		update: xdsresource.ListenerUpdate{
   158  			RouteConfigName: routeConfigName,
   159  			HTTPFilters:     []xdsresource.HTTPFilter{{Name: "router"}},
   160  		},
   161  	}
   162  	if err := verifyListenerUpdate(ctx, lw.updateCh, wantUpdate); err != nil {
   163  		t.Fatal(err)
   164  	}
   165  
   166  	// Update the management server with a listener resource that contains an
   167  	// empty HTTP connection manager within the apiListener, which will cause
   168  	// the resource to be NACKed.
   169  	badListener := proto.Clone(listenerResource).(*v3listenerpb.Listener)
   170  	badListener.ApiListener.ApiListener = nil
   171  	mgmtServer.Update(ctx, e2e.UpdateOptions{
   172  		NodeID:         nodeID,
   173  		Listeners:      []*v3listenerpb.Listener{badListener},
   174  		SkipValidation: true,
   175  	})
   176  
   177  	r, err = streamResponseCh.Receive(ctx)
   178  	if err != nil {
   179  		t.Fatal("Timeout when waiting for a discovery response from the server")
   180  	}
   181  	gotResp = r.(*v3discoverypb.DiscoveryResponse)
   182  
   183  	wantNackErr := xdsresource.NewError(xdsresource.ErrorTypeNACKed, "unexpected http connection manager resource type")
   184  	if err := verifyListenerUpdate(ctx, lw.updateCh, listenerUpdateErrTuple{err: wantNackErr}); err != nil {
   185  		t.Fatal(err)
   186  	}
   187  
   188  	// Verify that the NACK contains the appropriate version, nonce and error.
   189  	// We expect the version to not change as this is a NACK.
   190  	r, err = streamRequestCh.Receive(ctx)
   191  	if err != nil {
   192  		t.Fatal("Timeout when waiting for NACK")
   193  	}
   194  	gotReq = r.(*v3discoverypb.DiscoveryRequest)
   195  	if gotNonce, wantNonce := gotReq.GetResponseNonce(), gotResp.GetNonce(); gotNonce != wantNonce {
   196  		t.Errorf("Unexpected nonce in discovery request, got: %v, want: %v", gotNonce, wantNonce)
   197  	}
   198  	if gotErr := gotReq.GetErrorDetail(); gotErr == nil || !strings.Contains(gotErr.GetMessage(), wantNackErr.Error()) {
   199  		t.Fatalf("Unexpected error in discovery request, got: %v, want: %v", gotErr.GetMessage(), wantNackErr)
   200  	}
   201  
   202  	// Update the management server to send a good resource again.
   203  	mgmtServer.Update(ctx, e2e.UpdateOptions{
   204  		NodeID:         nodeID,
   205  		Listeners:      []*v3listenerpb.Listener{listenerResource},
   206  		SkipValidation: true,
   207  	})
   208  
   209  	// The envoy-go-control-plane management server keeps resending the same
   210  	// resource as long as we keep NACK'ing it. So, we will see the bad resource
   211  	// sent to us a few times here, before receiving the good resource.
   212  	var lastErr error
   213  	for {
   214  		if ctx.Err() != nil {
   215  			t.Fatalf("Timeout when waiting for an ACK from the xDS client. Last seen error: %v", lastErr)
   216  		}
   217  
   218  		r, err = streamResponseCh.Receive(ctx)
   219  		if err != nil {
   220  			t.Fatal("Timeout when waiting for a discovery response from the server")
   221  		}
   222  		gotResp = r.(*v3discoverypb.DiscoveryResponse)
   223  
   224  		// Verify that the ACK contains the appropriate version and nonce.
   225  		r, err = streamRequestCh.Receive(ctx)
   226  		if err != nil {
   227  			t.Fatal("Timeout when waiting for ACK")
   228  		}
   229  		gotReq = r.(*v3discoverypb.DiscoveryRequest)
   230  		wantReq.VersionInfo = gotResp.GetVersionInfo()
   231  		wantReq.ResponseNonce = gotResp.GetNonce()
   232  		wantReq.ErrorDetail = nil
   233  		diff := cmp.Diff(gotReq, wantReq, protocmp.Transform())
   234  		if diff == "" {
   235  			lastErr = nil
   236  			break
   237  		}
   238  		lastErr = fmt.Errorf("unexpected diff in discovery request, diff (-got, +want):\n%s", diff)
   239  	}
   240  
   241  	// Verify the update received by the watcher.
   242  	for ; ctx.Err() == nil; <-time.After(100 * time.Millisecond) {
   243  		if err := verifyListenerUpdate(ctx, lw.updateCh, wantUpdate); err != nil {
   244  			lastErr = err
   245  			continue
   246  		}
   247  		break
   248  	}
   249  	if ctx.Err() != nil {
   250  		t.Fatalf("Timeout when waiting for listener update. Last seen error: %v", lastErr)
   251  	}
   252  }
   253  
   254  // Tests the case where the first response is invalid. The test verifies that
   255  // the NACK contains an empty version string.
   256  func (s) TestADS_NACK_InvalidFirstResponse(t *testing.T) {
   257  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   258  	defer cancel()
   259  
   260  	// Create an xDS management server listening on a local port. Configure the
   261  	// request and response handlers to push on channels that are inspected by
   262  	// the test goroutine to verify ACK version and nonce.
   263  	streamRequestCh := testutils.NewChannel()
   264  	streamResponseCh := testutils.NewChannel()
   265  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
   266  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
   267  			streamRequestCh.SendContext(ctx, req)
   268  			return nil
   269  		},
   270  		OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) {
   271  			streamResponseCh.SendContext(ctx, resp)
   272  		},
   273  	})
   274  
   275  	// Create a listener resource on the management server that is expected to
   276  	// be NACKed by the xDS client.
   277  	const listenerName = "listener"
   278  	const routeConfigName = "route-config"
   279  	nodeID := uuid.New().String()
   280  	listenerResource := e2e.DefaultClientListener(listenerName, routeConfigName)
   281  	listenerResource.ApiListener.ApiListener = nil
   282  	resources := e2e.UpdateOptions{
   283  		NodeID:         nodeID,
   284  		Listeners:      []*v3listenerpb.Listener{listenerResource},
   285  		SkipValidation: true,
   286  	}
   287  	if err := mgmtServer.Update(ctx, resources); err != nil {
   288  		t.Fatal(err)
   289  	}
   290  
   291  	// Create an xDS client with bootstrap pointing to the above server.
   292  	bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
   293  	client := createXDSClient(t, bc)
   294  
   295  	// Register a watch for a listener resource.
   296  	lw := newListenerWatcher()
   297  	ldsCancel := xdsresource.WatchListener(client, listenerName, lw)
   298  	defer ldsCancel()
   299  
   300  	// Verify that the initial discovery request matches expectation.
   301  	r, err := streamRequestCh.Receive(ctx)
   302  	if err != nil {
   303  		t.Fatal("Timeout when waiting for the initial discovery request")
   304  	}
   305  	gotReq := r.(*v3discoverypb.DiscoveryRequest)
   306  	wantReq := &v3discoverypb.DiscoveryRequest{
   307  		VersionInfo: "",
   308  		Node: &v3corepb.Node{
   309  			Id:                   nodeID,
   310  			UserAgentName:        "gRPC Go",
   311  			UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
   312  			ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   313  		},
   314  		ResourceNames: []string{listenerName},
   315  		TypeUrl:       "type.googleapis.com/envoy.config.listener.v3.Listener",
   316  		ResponseNonce: "",
   317  	}
   318  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   319  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   320  	}
   321  
   322  	// Capture the version and nonce from the response.
   323  	r, err = streamResponseCh.Receive(ctx)
   324  	if err != nil {
   325  		t.Fatal("Timeout when waiting for the discovery response from client")
   326  	}
   327  	gotResp := r.(*v3discoverypb.DiscoveryResponse)
   328  
   329  	// Verify that the error is propagated to the watcher.
   330  	var wantNackErr = xdsresource.NewError(xdsresource.ErrorTypeNACKed, "unexpected http connection manager resource type")
   331  	if err := verifyListenerUpdate(ctx, lw.updateCh, listenerUpdateErrTuple{err: wantNackErr}); err != nil {
   332  		t.Fatal(err)
   333  	}
   334  
   335  	// NACK should contain the appropriate error, nonce, but empty version.
   336  	r, err = streamRequestCh.Receive(ctx)
   337  	if err != nil {
   338  		t.Fatal("Timeout when waiting for ACK")
   339  	}
   340  	gotReq = r.(*v3discoverypb.DiscoveryRequest)
   341  	if gotVersion, wantVersion := gotReq.GetVersionInfo(), ""; gotVersion != wantVersion {
   342  		t.Errorf("Unexpected version in discovery request, got: %v, want: %v", gotVersion, wantVersion)
   343  	}
   344  	if gotNonce, wantNonce := gotReq.GetResponseNonce(), gotResp.GetNonce(); gotNonce != wantNonce {
   345  		t.Errorf("Unexpected nonce in discovery request, got: %v, want: %v", gotNonce, wantNonce)
   346  	}
   347  	if gotErr := gotReq.GetErrorDetail(); gotErr == nil || !strings.Contains(gotErr.GetMessage(), wantNackErr.Error()) {
   348  		t.Fatalf("Unexpected error in discovery request, got: %v, want: %v", gotErr.GetMessage(), wantNackErr)
   349  	}
   350  }
   351  
   352  // Tests the scenario where the xDS client is no longer interested in a
   353  // resource. The following sequence of events are tested:
   354  //  1. A resource is requested and a good response is received. The test verifies
   355  //     that an ACK is sent for this resource.
   356  //  2. The previously requested resource is no longer requested. The test
   357  //     verifies that the connection to the management server is closed.
   358  //  3. The same resource is requested again. The test verifies that a new
   359  //     request is sent with an empty version string, which corresponds to the
   360  //     first request on a new connection.
   361  func (s) TestADS_ACK_NACK_ResourceIsNotRequestedAnymore(t *testing.T) {
   362  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   363  	defer cancel()
   364  
   365  	// Create an xDS management server listening on a local port. Configure the
   366  	// request and response handlers to push on channels that are inspected by
   367  	// the test goroutine to verify ACK version and nonce.
   368  	streamRequestCh := testutils.NewChannel()
   369  	streamResponseCh := testutils.NewChannel()
   370  	streamCloseCh := testutils.NewChannel()
   371  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
   372  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
   373  			streamRequestCh.SendContext(ctx, req)
   374  			return nil
   375  		},
   376  		OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) {
   377  			streamResponseCh.SendContext(ctx, resp)
   378  		},
   379  		OnStreamClosed: func(int64, *v3corepb.Node) {
   380  			streamCloseCh.SendContext(ctx, struct{}{})
   381  		},
   382  	})
   383  
   384  	// Create a listener resource on the management server.
   385  	const listenerName = "listener"
   386  	const routeConfigName = "route-config"
   387  	nodeID := uuid.New().String()
   388  	listenerResource := e2e.DefaultClientListener(listenerName, routeConfigName)
   389  	resources := e2e.UpdateOptions{
   390  		NodeID:         nodeID,
   391  		Listeners:      []*v3listenerpb.Listener{listenerResource},
   392  		SkipValidation: true,
   393  	}
   394  	if err := mgmtServer.Update(ctx, resources); err != nil {
   395  		t.Fatal(err)
   396  	}
   397  
   398  	// Create an xDS client with bootstrap pointing to the above server.
   399  	bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
   400  	config, err := bootstrap.NewConfigFromContents(bc)
   401  	if err != nil {
   402  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err)
   403  	}
   404  	pool := xdsclient.NewPool(config)
   405  	client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
   406  		Name: t.Name(),
   407  	})
   408  	if err != nil {
   409  		t.Fatalf("Failed to create xDS client: %v", err)
   410  	}
   411  	defer close()
   412  
   413  	// Register a watch for a listener resource.
   414  	lw := newListenerWatcher()
   415  	ldsCancel := xdsresource.WatchListener(client, listenerName, lw)
   416  	defer ldsCancel()
   417  
   418  	// Verify that the initial discovery request matches expectation.
   419  	r, err := streamRequestCh.Receive(ctx)
   420  	if err != nil {
   421  		t.Fatal("Timeout when waiting for the initial discovery request")
   422  	}
   423  	gotReq := r.(*v3discoverypb.DiscoveryRequest)
   424  	wantReq := &v3discoverypb.DiscoveryRequest{
   425  		VersionInfo: "",
   426  		Node: &v3corepb.Node{
   427  			Id:                   nodeID,
   428  			UserAgentName:        "gRPC Go",
   429  			UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
   430  			ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   431  		},
   432  		ResourceNames: []string{listenerName},
   433  		TypeUrl:       "type.googleapis.com/envoy.config.listener.v3.Listener",
   434  		ResponseNonce: "",
   435  	}
   436  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   437  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   438  	}
   439  
   440  	// Capture the version and nonce from the response.
   441  	r, err = streamResponseCh.Receive(ctx)
   442  	if err != nil {
   443  		t.Fatal("Timeout when waiting for the discovery response from client")
   444  	}
   445  	gotResp := r.(*v3discoverypb.DiscoveryResponse)
   446  
   447  	// Verify that the ACK contains the appropriate version and nonce.
   448  	r, err = streamRequestCh.Receive(ctx)
   449  	if err != nil {
   450  		t.Fatal("Timeout when waiting for ACK")
   451  	}
   452  	gotReq = r.(*v3discoverypb.DiscoveryRequest)
   453  	wantACKReq := proto.Clone(wantReq).(*v3discoverypb.DiscoveryRequest)
   454  	wantACKReq.VersionInfo = gotResp.GetVersionInfo()
   455  	wantACKReq.ResponseNonce = gotResp.GetNonce()
   456  	if diff := cmp.Diff(gotReq, wantACKReq, protocmp.Transform()); diff != "" {
   457  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   458  	}
   459  
   460  	// Verify the update received by the watcher.
   461  	wantUpdate := listenerUpdateErrTuple{
   462  		update: xdsresource.ListenerUpdate{
   463  			RouteConfigName: routeConfigName,
   464  			HTTPFilters:     []xdsresource.HTTPFilter{{Name: "router"}},
   465  		},
   466  	}
   467  	if err := verifyListenerUpdate(ctx, lw.updateCh, wantUpdate); err != nil {
   468  		t.Fatal(err)
   469  	}
   470  
   471  	// Cancel the watch on the listener resource. This should result in the
   472  	// existing connection to be management server getting closed.
   473  	ldsCancel()
   474  	if _, err := streamCloseCh.Receive(ctx); err != nil {
   475  		t.Fatalf("Timeout when expecting existing connection to be closed: %v", err)
   476  	}
   477  
   478  	// There is a race between two events when the last watch on an xdsChannel
   479  	// is canceled:
   480  	// - an empty discovery request being sent out
   481  	// - the ADS stream being closed
   482  	// To handle this race, we drain the request channel here so that if an
   483  	// empty discovery request was received, it is pulled out of the request
   484  	// channel and thereby guaranteeing a clean slate for the next watch
   485  	// registered below.
   486  	streamRequestCh.Drain()
   487  
   488  	// Register a watch for the same listener resource.
   489  	lw = newListenerWatcher()
   490  	ldsCancel = xdsresource.WatchListener(client, listenerName, lw)
   491  	defer ldsCancel()
   492  
   493  	// Verify that the discovery request is identical to the first one sent out
   494  	// to the management server.
   495  	r, err = streamRequestCh.Receive(ctx)
   496  	if err != nil {
   497  		t.Fatal("Timeout when waiting for discovery request")
   498  	}
   499  	gotReq = r.(*v3discoverypb.DiscoveryRequest)
   500  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   501  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   502  	}
   503  
   504  	// Verify the update received by the watcher.
   505  	if err := verifyListenerUpdate(ctx, lw.updateCh, wantUpdate); err != nil {
   506  		t.Fatal(err)
   507  	}
   508  }