google.golang.org/grpc@v1.62.1/xds/internal/xdsclient/transport/transport_ack_nack_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  package transport_test
    19  
    20  import (
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"testing"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	"github.com/google/go-cmp/cmp/cmpopts"
    28  	"github.com/google/uuid"
    29  	"google.golang.org/grpc/codes"
    30  	"google.golang.org/grpc/internal/testutils"
    31  	"google.golang.org/grpc/internal/testutils/xds/e2e"
    32  	xdstestutils "google.golang.org/grpc/xds/internal/testutils"
    33  	"google.golang.org/grpc/xds/internal/xdsclient/transport"
    34  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version"
    35  	"google.golang.org/protobuf/proto"
    36  	"google.golang.org/protobuf/testing/protocmp"
    37  	"google.golang.org/protobuf/types/known/anypb"
    38  	"google.golang.org/protobuf/types/known/wrapperspb"
    39  
    40  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    41  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    42  	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    43  	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    44  	statuspb "google.golang.org/genproto/googleapis/rpc/status"
    45  )
    46  
    47  var (
    48  	errWantNack = errors.New("unsupported field 'use_original_dst' is present and set to true")
    49  
    50  	// A simple update handler for listener resources which validates only the
    51  	// `use_original_dst` field.
    52  	dataModelValidator = func(update transport.ResourceUpdate) error {
    53  		for _, r := range update.Resources {
    54  			inner := &v3discoverypb.Resource{}
    55  			if err := proto.Unmarshal(r.GetValue(), inner); err != nil {
    56  				return fmt.Errorf("failed to unmarshal DiscoveryResponse: %v", err)
    57  			}
    58  			lis := &v3listenerpb.Listener{}
    59  			if err := proto.Unmarshal(r.GetValue(), lis); err != nil {
    60  				return fmt.Errorf("failed to unmarshal DiscoveryResponse: %v", err)
    61  			}
    62  			if useOrigDst := lis.GetUseOriginalDst(); useOrigDst != nil && useOrigDst.GetValue() {
    63  				return errWantNack
    64  			}
    65  		}
    66  		return nil
    67  	}
    68  )
    69  
    70  // TestSimpleAckAndNack tests simple ACK and NACK scenarios.
    71  //  1. When the data model layer likes a received response, the test verifies
    72  //     that an ACK is sent matching the version and nonce from the response.
    73  //  2. When a subsequent response is disliked by the data model layer, the test
    74  //     verifies that a NACK is sent matching the previously ACKed version and
    75  //     current nonce from the response.
    76  //  3. When a subsequent response is liked by the data model layer, the test
    77  //     verifies that an ACK is sent matching the version and nonce from the
    78  //     current response.
    79  func (s) TestSimpleAckAndNack(t *testing.T) {
    80  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
    81  	defer cancel()
    82  
    83  	// Create an xDS management server listening on a local port. Configure the
    84  	// request and response handlers to push on channels which are inspected by
    85  	// the test goroutine to verify ack version and nonce.
    86  	streamRequestCh := make(chan *v3discoverypb.DiscoveryRequest, 1)
    87  	streamResponseCh := make(chan *v3discoverypb.DiscoveryResponse, 1)
    88  	mgmtServer, err := e2e.StartManagementServer(e2e.ManagementServerOptions{
    89  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
    90  			select {
    91  			case streamRequestCh <- req:
    92  			case <-ctx.Done():
    93  			}
    94  			return nil
    95  		},
    96  		OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) {
    97  			select {
    98  			case streamResponseCh <- resp:
    99  			case <-ctx.Done():
   100  			}
   101  		},
   102  	})
   103  	if err != nil {
   104  		t.Fatalf("Failed to start xDS management server: %v", err)
   105  	}
   106  	defer mgmtServer.Stop()
   107  	t.Logf("Started xDS management server on %s", mgmtServer.Address)
   108  
   109  	// Configure the management server with appropriate resources.
   110  	apiListener := &v3listenerpb.ApiListener{
   111  		ApiListener: func() *anypb.Any {
   112  			return testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   113  				RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
   114  					Rds: &v3httppb.Rds{
   115  						ConfigSource: &v3corepb.ConfigSource{
   116  							ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
   117  						},
   118  						RouteConfigName: "route-configuration-name",
   119  					},
   120  				},
   121  			})
   122  		}(),
   123  	}
   124  	const resourceName = "resource name 1"
   125  	listenerResource := &v3listenerpb.Listener{
   126  		Name:        resourceName,
   127  		ApiListener: apiListener,
   128  	}
   129  	nodeID := uuid.New().String()
   130  	mgmtServer.Update(ctx, e2e.UpdateOptions{
   131  		NodeID:         nodeID,
   132  		Listeners:      []*v3listenerpb.Listener{listenerResource},
   133  		SkipValidation: true,
   134  	})
   135  
   136  	// Create a new transport.
   137  	tr, err := transport.New(transport.Options{
   138  		ServerCfg:      *xdstestutils.ServerConfigForAddress(t, mgmtServer.Address),
   139  		OnRecvHandler:  dataModelValidator,
   140  		OnErrorHandler: func(err error) {},
   141  		OnSendHandler:  func(*transport.ResourceSendInfo) {},
   142  		NodeProto:      &v3corepb.Node{Id: nodeID},
   143  	})
   144  	if err != nil {
   145  		t.Fatalf("Failed to create xDS transport: %v", err)
   146  	}
   147  	defer tr.Close()
   148  
   149  	// Send a discovery request through the transport.
   150  	tr.SendRequest(version.V3ListenerURL, []string{resourceName})
   151  
   152  	// Verify that the initial discovery request matches expectation.
   153  	var gotReq *v3discoverypb.DiscoveryRequest
   154  	select {
   155  	case gotReq = <-streamRequestCh:
   156  	case <-ctx.Done():
   157  		t.Fatalf("Timeout waiting for discovery request on the stream")
   158  	}
   159  	wantReq := &v3discoverypb.DiscoveryRequest{
   160  		VersionInfo:   "",
   161  		Node:          &v3corepb.Node{Id: nodeID},
   162  		ResourceNames: []string{resourceName},
   163  		TypeUrl:       "type.googleapis.com/envoy.config.listener.v3.Listener",
   164  		ResponseNonce: "",
   165  	}
   166  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" {
   167  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   168  	}
   169  
   170  	// Capture the version and nonce from the response.
   171  	var gotResp *v3discoverypb.DiscoveryResponse
   172  	select {
   173  	case gotResp = <-streamResponseCh:
   174  	case <-ctx.Done():
   175  		t.Fatalf("Timeout waiting for discovery response on the stream")
   176  	}
   177  
   178  	// Verify that the ACK contains the appropriate version and nonce.
   179  	wantReq.VersionInfo = gotResp.GetVersionInfo()
   180  	wantReq.ResponseNonce = gotResp.GetNonce()
   181  	select {
   182  	case gotReq = <-streamRequestCh:
   183  	case <-ctx.Done():
   184  		t.Fatalf("Timeout waiting for the discovery request ACK on the stream")
   185  	}
   186  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" {
   187  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   188  	}
   189  
   190  	// Update the management server's copy of the resource to include a field
   191  	// which will cause the resource to be NACKed.
   192  	badListener := proto.Clone(listenerResource).(*v3listenerpb.Listener)
   193  	badListener.UseOriginalDst = &wrapperspb.BoolValue{Value: true}
   194  	mgmtServer.Update(ctx, e2e.UpdateOptions{
   195  		NodeID:         nodeID,
   196  		Listeners:      []*v3listenerpb.Listener{badListener},
   197  		SkipValidation: true,
   198  	})
   199  
   200  	select {
   201  	case gotResp = <-streamResponseCh:
   202  	case <-ctx.Done():
   203  		t.Fatalf("Timeout waiting for discovery response on the stream")
   204  	}
   205  
   206  	// Verify that the NACK contains the appropriate version, nonce and error.
   207  	// We expect the version to not change as this is a NACK.
   208  	wantReq.ResponseNonce = gotResp.GetNonce()
   209  	wantReq.ErrorDetail = &statuspb.Status{
   210  		Code:    int32(codes.InvalidArgument),
   211  		Message: errWantNack.Error(),
   212  	}
   213  	select {
   214  	case gotReq = <-streamRequestCh:
   215  	case <-ctx.Done():
   216  		t.Fatalf("Timeout waiting for the discovery request ACK on the stream")
   217  	}
   218  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" {
   219  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   220  	}
   221  
   222  	// Update the management server to send a good resource again.
   223  	mgmtServer.Update(ctx, e2e.UpdateOptions{
   224  		NodeID:         nodeID,
   225  		Listeners:      []*v3listenerpb.Listener{listenerResource},
   226  		SkipValidation: true,
   227  	})
   228  
   229  	// The envoy-go-control-plane management server keeps resending the same
   230  	// resource as long as we keep NACK'ing it. So, we will see the bad resource
   231  	// sent to us a few times here, before receiving the good resource.
   232  	for {
   233  		select {
   234  		case gotResp = <-streamResponseCh:
   235  		case <-ctx.Done():
   236  			t.Fatalf("Timeout waiting for discovery response on the stream")
   237  		}
   238  
   239  		// Verify that the ACK contains the appropriate version and nonce.
   240  		wantReq.VersionInfo = gotResp.GetVersionInfo()
   241  		wantReq.ResponseNonce = gotResp.GetNonce()
   242  		wantReq.ErrorDetail = nil
   243  		select {
   244  		case gotReq = <-streamRequestCh:
   245  		case <-ctx.Done():
   246  			t.Fatalf("Timeout waiting for the discovery request ACK on the stream")
   247  		}
   248  		diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort))
   249  		if diff == "" {
   250  			break
   251  		}
   252  		t.Logf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   253  	}
   254  }
   255  
   256  // TestInvalidFirstResponse tests the case where the first response is invalid.
   257  // The test verifies that the NACK contains an empty version string.
   258  func (s) TestInvalidFirstResponse(t *testing.T) {
   259  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   260  	defer cancel()
   261  
   262  	// Create an xDS management server listening on a local port. Configure the
   263  	// request and response handlers to push on channels which are inspected by
   264  	// the test goroutine to verify ack version and nonce.
   265  	streamRequestCh := make(chan *v3discoverypb.DiscoveryRequest, 1)
   266  	streamResponseCh := make(chan *v3discoverypb.DiscoveryResponse, 1)
   267  	mgmtServer, err := e2e.StartManagementServer(e2e.ManagementServerOptions{
   268  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
   269  			select {
   270  			case streamRequestCh <- req:
   271  			case <-ctx.Done():
   272  			}
   273  			return nil
   274  		},
   275  		OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) {
   276  			select {
   277  			case streamResponseCh <- resp:
   278  			case <-ctx.Done():
   279  			}
   280  		},
   281  	})
   282  	if err != nil {
   283  		t.Fatalf("Failed to start xDS management server: %v", err)
   284  	}
   285  	defer mgmtServer.Stop()
   286  	t.Logf("Started xDS management server on %s", mgmtServer.Address)
   287  
   288  	// Configure the management server with appropriate resources.
   289  	apiListener := &v3listenerpb.ApiListener{
   290  		ApiListener: func() *anypb.Any {
   291  			return testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   292  				RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
   293  					Rds: &v3httppb.Rds{
   294  						ConfigSource: &v3corepb.ConfigSource{
   295  							ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
   296  						},
   297  						RouteConfigName: "route-configuration-name",
   298  					},
   299  				},
   300  			})
   301  		}(),
   302  	}
   303  	const resourceName = "resource name 1"
   304  	listenerResource := &v3listenerpb.Listener{
   305  		Name:           resourceName,
   306  		ApiListener:    apiListener,
   307  		UseOriginalDst: &wrapperspb.BoolValue{Value: true}, // This will cause the resource to be NACKed.
   308  	}
   309  	nodeID := uuid.New().String()
   310  	mgmtServer.Update(ctx, e2e.UpdateOptions{
   311  		NodeID:         nodeID,
   312  		Listeners:      []*v3listenerpb.Listener{listenerResource},
   313  		SkipValidation: true,
   314  	})
   315  
   316  	// Create a new transport.
   317  	tr, err := transport.New(transport.Options{
   318  		ServerCfg:      *xdstestutils.ServerConfigForAddress(t, mgmtServer.Address),
   319  		NodeProto:      &v3corepb.Node{Id: nodeID},
   320  		OnRecvHandler:  dataModelValidator,
   321  		OnErrorHandler: func(err error) {},
   322  		OnSendHandler:  func(*transport.ResourceSendInfo) {},
   323  	})
   324  	if err != nil {
   325  		t.Fatalf("Failed to create xDS transport: %v", err)
   326  	}
   327  	defer tr.Close()
   328  
   329  	// Send a discovery request through the transport.
   330  	tr.SendRequest(version.V3ListenerURL, []string{resourceName})
   331  
   332  	// Verify that the initial discovery request matches expectation.
   333  	var gotReq *v3discoverypb.DiscoveryRequest
   334  	select {
   335  	case gotReq = <-streamRequestCh:
   336  	case <-ctx.Done():
   337  		t.Fatalf("Timeout waiting for discovery request on the stream")
   338  	}
   339  	wantReq := &v3discoverypb.DiscoveryRequest{
   340  		Node:          &v3corepb.Node{Id: nodeID},
   341  		ResourceNames: []string{resourceName},
   342  		TypeUrl:       "type.googleapis.com/envoy.config.listener.v3.Listener",
   343  	}
   344  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" {
   345  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   346  	}
   347  
   348  	var gotResp *v3discoverypb.DiscoveryResponse
   349  	select {
   350  	case gotResp = <-streamResponseCh:
   351  	case <-ctx.Done():
   352  		t.Fatalf("Timeout waiting for discovery response on the stream")
   353  	}
   354  
   355  	// NACK should contain the appropriate error, nonce, but empty version.
   356  	wantReq.VersionInfo = ""
   357  	wantReq.ResponseNonce = gotResp.GetNonce()
   358  	wantReq.ErrorDetail = &statuspb.Status{
   359  		Code:    int32(codes.InvalidArgument),
   360  		Message: errWantNack.Error(),
   361  	}
   362  	select {
   363  	case gotReq = <-streamRequestCh:
   364  	case <-ctx.Done():
   365  		t.Fatalf("Timeout waiting for the discovery request ACK on the stream")
   366  	}
   367  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" {
   368  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   369  	}
   370  }
   371  
   372  // TestResourceIsNotRequestedAnymore tests the scenario where the xDS client is
   373  // no longer interested in a resource. The following sequence of events are
   374  // tested:
   375  //  1. A resource is requested and a good response is received. The test verifies
   376  //     that an ACK is sent for this resource.
   377  //  2. The previously requested resource is no longer requested. The test
   378  //     verifies that a request with no resource names is sent out.
   379  //  3. The same resource is requested again. The test verifies that the request
   380  //     is sent with the previously ACKed version.
   381  func (s) TestResourceIsNotRequestedAnymore(t *testing.T) {
   382  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   383  	defer cancel()
   384  
   385  	// Create an xDS management server listening on a local port. Configure the
   386  	// request and response handlers to push on channels which are inspected by
   387  	// the test goroutine to verify ack version and nonce.
   388  	streamRequestCh := make(chan *v3discoverypb.DiscoveryRequest, 1)
   389  	streamResponseCh := make(chan *v3discoverypb.DiscoveryResponse, 1)
   390  	mgmtServer, err := e2e.StartManagementServer(e2e.ManagementServerOptions{
   391  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
   392  			select {
   393  			case streamRequestCh <- req:
   394  			case <-ctx.Done():
   395  			}
   396  			return nil
   397  		},
   398  		OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) {
   399  			select {
   400  			case streamResponseCh <- resp:
   401  			case <-ctx.Done():
   402  			}
   403  		},
   404  	})
   405  	if err != nil {
   406  		t.Fatalf("Failed to start xDS management server: %v", err)
   407  	}
   408  	defer mgmtServer.Stop()
   409  	t.Logf("Started xDS management server on %s", mgmtServer.Address)
   410  
   411  	// Configure the management server with appropriate resources.
   412  	apiListener := &v3listenerpb.ApiListener{
   413  		ApiListener: func() *anypb.Any {
   414  			return testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   415  				RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
   416  					Rds: &v3httppb.Rds{
   417  						ConfigSource: &v3corepb.ConfigSource{
   418  							ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
   419  						},
   420  						RouteConfigName: "route-configuration-name",
   421  					},
   422  				},
   423  			})
   424  		}(),
   425  	}
   426  	const resourceName = "resource name 1"
   427  	listenerResource := &v3listenerpb.Listener{
   428  		Name:        resourceName,
   429  		ApiListener: apiListener,
   430  	}
   431  	nodeID := uuid.New().String()
   432  	mgmtServer.Update(ctx, e2e.UpdateOptions{
   433  		NodeID:         nodeID,
   434  		Listeners:      []*v3listenerpb.Listener{listenerResource},
   435  		SkipValidation: true,
   436  	})
   437  
   438  	// Create a new transport.
   439  	tr, err := transport.New(transport.Options{
   440  		ServerCfg:      *xdstestutils.ServerConfigForAddress(t, mgmtServer.Address),
   441  		NodeProto:      &v3corepb.Node{Id: nodeID},
   442  		OnRecvHandler:  dataModelValidator,
   443  		OnErrorHandler: func(err error) {},
   444  		OnSendHandler:  func(*transport.ResourceSendInfo) {},
   445  	})
   446  	if err != nil {
   447  		t.Fatalf("Failed to create xDS transport: %v", err)
   448  	}
   449  	defer tr.Close()
   450  
   451  	// Send a discovery request through the transport.
   452  	tr.SendRequest(version.V3ListenerURL, []string{resourceName})
   453  
   454  	// Verify that the initial discovery request matches expectation.
   455  	var gotReq *v3discoverypb.DiscoveryRequest
   456  	select {
   457  	case gotReq = <-streamRequestCh:
   458  	case <-ctx.Done():
   459  		t.Fatalf("Timeout waiting for discovery request on the stream")
   460  	}
   461  	wantReq := &v3discoverypb.DiscoveryRequest{
   462  		VersionInfo:   "",
   463  		Node:          &v3corepb.Node{Id: nodeID},
   464  		ResourceNames: []string{resourceName},
   465  		TypeUrl:       "type.googleapis.com/envoy.config.listener.v3.Listener",
   466  		ResponseNonce: "",
   467  	}
   468  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" {
   469  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   470  	}
   471  
   472  	// Capture the version and nonce from the response.
   473  	var gotResp *v3discoverypb.DiscoveryResponse
   474  	select {
   475  	case gotResp = <-streamResponseCh:
   476  	case <-ctx.Done():
   477  		t.Fatalf("Timeout waiting for discovery response on the stream")
   478  	}
   479  
   480  	// Verify that the ACK contains the appropriate version and nonce.
   481  	wantReq.VersionInfo = gotResp.GetVersionInfo()
   482  	wantReq.ResponseNonce = gotResp.GetNonce()
   483  	select {
   484  	case gotReq = <-streamRequestCh:
   485  	case <-ctx.Done():
   486  		t.Fatalf("Timeout waiting for the discovery request ACK on the stream")
   487  	}
   488  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" {
   489  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   490  	}
   491  
   492  	// Send a discovery request with no resource names.
   493  	tr.SendRequest(version.V3ListenerURL, []string{})
   494  
   495  	// Verify that the discovery request matches expectation.
   496  	select {
   497  	case gotReq = <-streamRequestCh:
   498  	case <-ctx.Done():
   499  		t.Fatalf("Timeout waiting for discovery request on the stream")
   500  	}
   501  	wantReq.ResourceNames = nil
   502  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" {
   503  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   504  	}
   505  
   506  	// Send a discovery request for the same resource requested earlier.
   507  	tr.SendRequest(version.V3ListenerURL, []string{resourceName})
   508  
   509  	// Verify that the discovery request contains the version from the
   510  	// previously received response.
   511  	select {
   512  	case gotReq = <-streamRequestCh:
   513  	case <-ctx.Done():
   514  		t.Fatalf("Timeout waiting for discovery request on the stream")
   515  	}
   516  	wantReq.ResourceNames = []string{resourceName}
   517  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" {
   518  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   519  	}
   520  }