github.com/hxx258456/ccgo@v0.0.5-0.20230213014102-48b35f46f66f/grpc/xds/csds/csds_test.go (about)

     1  /*
     2   *
     3   * Copyright 2021 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 csds
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"sort"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/golang/protobuf/proto"
    30  	"github.com/google/go-cmp/cmp"
    31  	"github.com/google/uuid"
    32  	grpc "github.com/hxx258456/ccgo/grpc"
    33  	"github.com/hxx258456/ccgo/grpc/internal/testutils"
    34  	"github.com/hxx258456/ccgo/grpc/internal/xds"
    35  	_ "github.com/hxx258456/ccgo/grpc/xds/internal/httpfilter/router"
    36  	"github.com/hxx258456/ccgo/grpc/xds/internal/testutils/e2e"
    37  	"github.com/hxx258456/ccgo/grpc/xds/internal/xdsclient"
    38  	"github.com/hxx258456/ccgo/grpc/xds/internal/xdsclient/xdsresource"
    39  	"google.golang.org/protobuf/testing/protocmp"
    40  	"google.golang.org/protobuf/types/known/anypb"
    41  
    42  	v3adminpb "github.com/hxx258456/ccgo/go-control-plane/envoy/admin/v3"
    43  	v2corepb "github.com/hxx258456/ccgo/go-control-plane/envoy/api/v2/core"
    44  	v3clusterpb "github.com/hxx258456/ccgo/go-control-plane/envoy/config/cluster/v3"
    45  	v3corepb "github.com/hxx258456/ccgo/go-control-plane/envoy/config/core/v3"
    46  	v3endpointpb "github.com/hxx258456/ccgo/go-control-plane/envoy/config/endpoint/v3"
    47  	v3listenerpb "github.com/hxx258456/ccgo/go-control-plane/envoy/config/listener/v3"
    48  	v3routepb "github.com/hxx258456/ccgo/go-control-plane/envoy/config/route/v3"
    49  	v3statuspb "github.com/hxx258456/ccgo/go-control-plane/envoy/service/status/v3"
    50  	v3statuspbgrpc "github.com/hxx258456/ccgo/go-control-plane/envoy/service/status/v3"
    51  )
    52  
    53  const (
    54  	defaultTestTimeout = 10 * time.Second
    55  )
    56  
    57  var cmpOpts = cmp.Options{
    58  	cmp.Transformer("sort", func(in []*v3statuspb.ClientConfig_GenericXdsConfig) []*v3statuspb.ClientConfig_GenericXdsConfig {
    59  		out := append([]*v3statuspb.ClientConfig_GenericXdsConfig(nil), in...)
    60  		sort.Slice(out, func(i, j int) bool {
    61  			a, b := out[i], out[j]
    62  			if a == nil {
    63  				return true
    64  			}
    65  			if b == nil {
    66  				return false
    67  			}
    68  			if strings.Compare(a.TypeUrl, b.TypeUrl) == 0 {
    69  				return strings.Compare(a.Name, b.Name) < 0
    70  			}
    71  			return strings.Compare(a.TypeUrl, b.TypeUrl) < 0
    72  		})
    73  		return out
    74  	}),
    75  	protocmp.Transform(),
    76  }
    77  
    78  // filterFields clears unimportant fields in the proto messages.
    79  //
    80  // protocmp.IgnoreFields() doesn't work on nil messages (it panics).
    81  func filterFields(ms []*v3statuspb.ClientConfig_GenericXdsConfig) []*v3statuspb.ClientConfig_GenericXdsConfig {
    82  	out := append([]*v3statuspb.ClientConfig_GenericXdsConfig{}, ms...)
    83  	for _, m := range out {
    84  		if m == nil {
    85  			continue
    86  		}
    87  		m.LastUpdated = nil
    88  		if m.ErrorState != nil {
    89  			m.ErrorState.Details = "blahblah"
    90  			m.ErrorState.LastUpdateAttempt = nil
    91  		}
    92  	}
    93  	return out
    94  }
    95  
    96  var (
    97  	ldsTargets   = []string{"lds.target.good:0000", "lds.target.good:1111"}
    98  	listeners    = make([]*v3listenerpb.Listener, len(ldsTargets))
    99  	listenerAnys = make([]*anypb.Any, len(ldsTargets))
   100  
   101  	rdsTargets = []string{"route-config-0", "route-config-1"}
   102  	routes     = make([]*v3routepb.RouteConfiguration, len(rdsTargets))
   103  	routeAnys  = make([]*anypb.Any, len(rdsTargets))
   104  
   105  	cdsTargets  = []string{"cluster-0", "cluster-1"}
   106  	clusters    = make([]*v3clusterpb.Cluster, len(cdsTargets))
   107  	clusterAnys = make([]*anypb.Any, len(cdsTargets))
   108  
   109  	edsTargets   = []string{"endpoints-0", "endpoints-1"}
   110  	endpoints    = make([]*v3endpointpb.ClusterLoadAssignment, len(edsTargets))
   111  	endpointAnys = make([]*anypb.Any, len(edsTargets))
   112  	ips          = []string{"0.0.0.0", "1.1.1.1"}
   113  	ports        = []uint32{123, 456}
   114  )
   115  
   116  func init() {
   117  	for i := range ldsTargets {
   118  		listeners[i] = e2e.DefaultClientListener(ldsTargets[i], rdsTargets[i])
   119  		listenerAnys[i] = testutils.MarshalAny(listeners[i])
   120  	}
   121  	for i := range rdsTargets {
   122  		routes[i] = e2e.DefaultRouteConfig(rdsTargets[i], ldsTargets[i], cdsTargets[i])
   123  		routeAnys[i] = testutils.MarshalAny(routes[i])
   124  	}
   125  	for i := range cdsTargets {
   126  		clusters[i] = e2e.DefaultCluster(cdsTargets[i], edsTargets[i], e2e.SecurityLevelNone)
   127  		clusterAnys[i] = testutils.MarshalAny(clusters[i])
   128  	}
   129  	for i := range edsTargets {
   130  		endpoints[i] = e2e.DefaultEndpoint(edsTargets[i], ips[i], ports[i:i+1])
   131  		endpointAnys[i] = testutils.MarshalAny(endpoints[i])
   132  	}
   133  }
   134  
   135  func TestCSDS(t *testing.T) {
   136  	const retryCount = 10
   137  
   138  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   139  	defer cancel()
   140  	xdsC, mgmServer, nodeID, stream, cleanup := commonSetup(ctx, t)
   141  	defer cleanup()
   142  
   143  	for _, target := range ldsTargets {
   144  		xdsC.WatchListener(target, func(xdsresource.ListenerUpdate, error) {})
   145  	}
   146  	for _, target := range rdsTargets {
   147  		xdsC.WatchRouteConfig(target, func(xdsresource.RouteConfigUpdate, error) {})
   148  	}
   149  	for _, target := range cdsTargets {
   150  		xdsC.WatchCluster(target, func(xdsresource.ClusterUpdate, error) {})
   151  	}
   152  	for _, target := range edsTargets {
   153  		xdsC.WatchEndpoints(target, func(xdsresource.EndpointsUpdate, error) {})
   154  	}
   155  
   156  	for i := 0; i < retryCount; i++ {
   157  		err := checkForRequested(stream)
   158  		if err == nil {
   159  			break
   160  		}
   161  		if i == retryCount-1 {
   162  			t.Fatalf("%v", err)
   163  		}
   164  		time.Sleep(time.Millisecond * 100)
   165  	}
   166  
   167  	if err := mgmServer.Update(ctx, e2e.UpdateOptions{
   168  		NodeID:    nodeID,
   169  		Listeners: listeners,
   170  		Routes:    routes,
   171  		Clusters:  clusters,
   172  		Endpoints: endpoints,
   173  	}); err != nil {
   174  		t.Fatal(err)
   175  	}
   176  	for i := 0; i < retryCount; i++ {
   177  		err := checkForACKed(stream)
   178  		if err == nil {
   179  			break
   180  		}
   181  		if i == retryCount-1 {
   182  			t.Fatalf("%v", err)
   183  		}
   184  		time.Sleep(time.Millisecond * 100)
   185  	}
   186  
   187  	const nackResourceIdx = 0
   188  	var (
   189  		nackListeners = append([]*v3listenerpb.Listener{}, listeners...)
   190  		nackRoutes    = append([]*v3routepb.RouteConfiguration{}, routes...)
   191  		nackClusters  = append([]*v3clusterpb.Cluster{}, clusters...)
   192  		nackEndpoints = append([]*v3endpointpb.ClusterLoadAssignment{}, endpoints...)
   193  	)
   194  	nackListeners[0] = &v3listenerpb.Listener{Name: ldsTargets[nackResourceIdx], ApiListener: &v3listenerpb.ApiListener{}} // 0 will be nacked. 1 will stay the same.
   195  	nackRoutes[0] = &v3routepb.RouteConfiguration{
   196  		Name: rdsTargets[nackResourceIdx], VirtualHosts: []*v3routepb.VirtualHost{{Routes: []*v3routepb.Route{{}}}},
   197  	}
   198  	nackClusters[0] = &v3clusterpb.Cluster{
   199  		Name: cdsTargets[nackResourceIdx], ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_STATIC},
   200  	}
   201  	nackEndpoints[0] = &v3endpointpb.ClusterLoadAssignment{
   202  		ClusterName: edsTargets[nackResourceIdx], Endpoints: []*v3endpointpb.LocalityLbEndpoints{{}},
   203  	}
   204  	if err := mgmServer.Update(ctx, e2e.UpdateOptions{
   205  		NodeID:         nodeID,
   206  		Listeners:      nackListeners,
   207  		Routes:         nackRoutes,
   208  		Clusters:       nackClusters,
   209  		Endpoints:      nackEndpoints,
   210  		SkipValidation: true,
   211  	}); err != nil {
   212  		t.Fatal(err)
   213  	}
   214  	for i := 0; i < retryCount; i++ {
   215  		err := checkForNACKed(nackResourceIdx, stream)
   216  		if err == nil {
   217  			break
   218  		}
   219  		if i == retryCount-1 {
   220  			t.Fatalf("%v", err)
   221  		}
   222  		time.Sleep(time.Millisecond * 100)
   223  	}
   224  }
   225  
   226  func commonSetup(ctx context.Context, t *testing.T) (xdsclient.XDSClient, *e2e.ManagementServer, string, v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient, func()) {
   227  	t.Helper()
   228  
   229  	// Spin up a xDS management server on a local port.
   230  	nodeID := uuid.New().String()
   231  	fs, err := e2e.StartManagementServer()
   232  	if err != nil {
   233  		t.Fatal(err)
   234  	}
   235  
   236  	// Create a bootstrap file in a temporary directory.
   237  	bootstrapCleanup, err := xds.SetupBootstrapFile(xds.BootstrapOptions{
   238  		Version:   xds.TransportV3,
   239  		NodeID:    nodeID,
   240  		ServerURI: fs.Address,
   241  	})
   242  	if err != nil {
   243  		t.Fatal(err)
   244  	}
   245  	// Create xds_client.
   246  	xdsC, err := xdsclient.New()
   247  	if err != nil {
   248  		t.Fatalf("failed to create xds client: %v", err)
   249  	}
   250  	oldNewXDSClient := newXDSClient
   251  	newXDSClient = func() xdsclient.XDSClient { return xdsC }
   252  
   253  	// Initialize an gRPC server and register CSDS on it.
   254  	server := grpc.NewServer()
   255  	csdss, err := NewClientStatusDiscoveryServer()
   256  	if err != nil {
   257  		t.Fatal(err)
   258  	}
   259  	v3statuspbgrpc.RegisterClientStatusDiscoveryServiceServer(server, csdss)
   260  	// Create a local listener and pass it to Serve().
   261  	lis, err := testutils.LocalTCPListener()
   262  	if err != nil {
   263  		t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
   264  	}
   265  	go func() {
   266  		if err := server.Serve(lis); err != nil {
   267  			t.Errorf("Serve() failed: %v", err)
   268  		}
   269  	}()
   270  
   271  	// Create CSDS client.
   272  	conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure())
   273  	if err != nil {
   274  		t.Fatalf("cannot connect to server: %v", err)
   275  	}
   276  	c := v3statuspbgrpc.NewClientStatusDiscoveryServiceClient(conn)
   277  	stream, err := c.StreamClientStatus(ctx, grpc.WaitForReady(true))
   278  	if err != nil {
   279  		t.Fatalf("cannot get ServerReflectionInfo: %v", err)
   280  	}
   281  
   282  	return xdsC, fs, nodeID, stream, func() {
   283  		fs.Stop()
   284  		conn.Close()
   285  		server.Stop()
   286  		csdss.Close()
   287  		newXDSClient = oldNewXDSClient
   288  		xdsC.Close()
   289  		bootstrapCleanup()
   290  	}
   291  }
   292  
   293  func checkForRequested(stream v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient) error {
   294  	if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil {
   295  		return fmt.Errorf("failed to send request: %v", err)
   296  	}
   297  	r, err := stream.Recv()
   298  	if err != nil {
   299  		// io.EOF is not ok.
   300  		return fmt.Errorf("failed to recv response: %v", err)
   301  	}
   302  
   303  	if n := len(r.Config); n != 1 {
   304  		return fmt.Errorf("got %d configs, want 1: %v", n, proto.MarshalTextString(r))
   305  	}
   306  
   307  	var want []*v3statuspb.ClientConfig_GenericXdsConfig
   308  	// Status is Requested, but version and xds config are all unset.
   309  	for i := range ldsTargets {
   310  		want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{
   311  			TypeUrl: listenerTypeURL, Name: ldsTargets[i], ClientStatus: v3adminpb.ClientResourceStatus_REQUESTED,
   312  		})
   313  	}
   314  	for i := range rdsTargets {
   315  		want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{
   316  			TypeUrl: routeConfigTypeURL, Name: rdsTargets[i], ClientStatus: v3adminpb.ClientResourceStatus_REQUESTED,
   317  		})
   318  	}
   319  	for i := range cdsTargets {
   320  		want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{
   321  			TypeUrl: clusterTypeURL, Name: cdsTargets[i], ClientStatus: v3adminpb.ClientResourceStatus_REQUESTED,
   322  		})
   323  	}
   324  	for i := range edsTargets {
   325  		want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{
   326  			TypeUrl: endpointsTypeURL, Name: edsTargets[i], ClientStatus: v3adminpb.ClientResourceStatus_REQUESTED,
   327  		})
   328  	}
   329  	if diff := cmp.Diff(filterFields(r.Config[0].GenericXdsConfigs), want, cmpOpts); diff != "" {
   330  		return fmt.Errorf(diff)
   331  	}
   332  	return nil
   333  }
   334  
   335  func checkForACKed(stream v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient) error {
   336  	const wantVersion = "1"
   337  
   338  	if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil {
   339  		return fmt.Errorf("failed to send: %v", err)
   340  	}
   341  	r, err := stream.Recv()
   342  	if err != nil {
   343  		// io.EOF is not ok.
   344  		return fmt.Errorf("failed to recv response: %v", err)
   345  	}
   346  
   347  	if n := len(r.Config); n != 1 {
   348  		return fmt.Errorf("got %d configs, want 1: %v", n, proto.MarshalTextString(r))
   349  	}
   350  
   351  	var want []*v3statuspb.ClientConfig_GenericXdsConfig
   352  	// Status is Acked, config is filled with the prebuilt Anys.
   353  	for i := range ldsTargets {
   354  		want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{
   355  			TypeUrl:      listenerTypeURL,
   356  			Name:         ldsTargets[i],
   357  			VersionInfo:  wantVersion,
   358  			XdsConfig:    listenerAnys[i],
   359  			ClientStatus: v3adminpb.ClientResourceStatus_ACKED,
   360  		})
   361  	}
   362  	for i := range rdsTargets {
   363  		want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{
   364  			TypeUrl:      routeConfigTypeURL,
   365  			Name:         rdsTargets[i],
   366  			VersionInfo:  wantVersion,
   367  			XdsConfig:    routeAnys[i],
   368  			ClientStatus: v3adminpb.ClientResourceStatus_ACKED,
   369  		})
   370  	}
   371  	for i := range cdsTargets {
   372  		want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{
   373  			TypeUrl:      clusterTypeURL,
   374  			Name:         cdsTargets[i],
   375  			VersionInfo:  wantVersion,
   376  			XdsConfig:    clusterAnys[i],
   377  			ClientStatus: v3adminpb.ClientResourceStatus_ACKED,
   378  		})
   379  	}
   380  	for i := range edsTargets {
   381  		want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{
   382  			TypeUrl:      endpointsTypeURL,
   383  			Name:         edsTargets[i],
   384  			VersionInfo:  wantVersion,
   385  			XdsConfig:    endpointAnys[i],
   386  			ClientStatus: v3adminpb.ClientResourceStatus_ACKED,
   387  		})
   388  	}
   389  	if diff := cmp.Diff(filterFields(r.Config[0].GenericXdsConfigs), want, cmpOpts); diff != "" {
   390  		return fmt.Errorf(diff)
   391  	}
   392  	return nil
   393  }
   394  
   395  func checkForNACKed(nackResourceIdx int, stream v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient) error {
   396  	const (
   397  		ackVersion  = "1"
   398  		nackVersion = "2"
   399  	)
   400  	if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil {
   401  		return fmt.Errorf("failed to send: %v", err)
   402  	}
   403  	r, err := stream.Recv()
   404  	if err != nil {
   405  		// io.EOF is not ok.
   406  		return fmt.Errorf("failed to recv response: %v", err)
   407  	}
   408  
   409  	if n := len(r.Config); n != 1 {
   410  		return fmt.Errorf("got %d configs, want 1: %v", n, proto.MarshalTextString(r))
   411  	}
   412  
   413  	var want []*v3statuspb.ClientConfig_GenericXdsConfig
   414  	// Resources with the nackIdx are NACKed.
   415  	for i := range ldsTargets {
   416  		config := &v3statuspb.ClientConfig_GenericXdsConfig{
   417  			TypeUrl:      listenerTypeURL,
   418  			Name:         ldsTargets[i],
   419  			VersionInfo:  nackVersion,
   420  			XdsConfig:    listenerAnys[i],
   421  			ClientStatus: v3adminpb.ClientResourceStatus_ACKED,
   422  		}
   423  		if i == nackResourceIdx {
   424  			config.VersionInfo = ackVersion
   425  			config.ClientStatus = v3adminpb.ClientResourceStatus_NACKED
   426  			config.ErrorState = &v3adminpb.UpdateFailureState{
   427  				Details:     "blahblah",
   428  				VersionInfo: nackVersion,
   429  			}
   430  		}
   431  		want = append(want, config)
   432  	}
   433  	for i := range rdsTargets {
   434  		config := &v3statuspb.ClientConfig_GenericXdsConfig{
   435  			TypeUrl:      routeConfigTypeURL,
   436  			Name:         rdsTargets[i],
   437  			VersionInfo:  nackVersion,
   438  			XdsConfig:    routeAnys[i],
   439  			ClientStatus: v3adminpb.ClientResourceStatus_ACKED,
   440  		}
   441  		if i == nackResourceIdx {
   442  			config.VersionInfo = ackVersion
   443  			config.ClientStatus = v3adminpb.ClientResourceStatus_NACKED
   444  			config.ErrorState = &v3adminpb.UpdateFailureState{
   445  				Details:     "blahblah",
   446  				VersionInfo: nackVersion,
   447  			}
   448  		}
   449  		want = append(want, config)
   450  	}
   451  	for i := range cdsTargets {
   452  		config := &v3statuspb.ClientConfig_GenericXdsConfig{
   453  			TypeUrl:      clusterTypeURL,
   454  			Name:         cdsTargets[i],
   455  			VersionInfo:  nackVersion,
   456  			XdsConfig:    clusterAnys[i],
   457  			ClientStatus: v3adminpb.ClientResourceStatus_ACKED,
   458  		}
   459  		if i == nackResourceIdx {
   460  			config.VersionInfo = ackVersion
   461  			config.ClientStatus = v3adminpb.ClientResourceStatus_NACKED
   462  			config.ErrorState = &v3adminpb.UpdateFailureState{
   463  				Details:     "blahblah",
   464  				VersionInfo: nackVersion,
   465  			}
   466  		}
   467  		want = append(want, config)
   468  	}
   469  	for i := range edsTargets {
   470  		config := &v3statuspb.ClientConfig_GenericXdsConfig{
   471  			TypeUrl:      endpointsTypeURL,
   472  			Name:         edsTargets[i],
   473  			VersionInfo:  nackVersion,
   474  			XdsConfig:    endpointAnys[i],
   475  			ClientStatus: v3adminpb.ClientResourceStatus_ACKED,
   476  		}
   477  		if i == nackResourceIdx {
   478  			config.VersionInfo = ackVersion
   479  			config.ClientStatus = v3adminpb.ClientResourceStatus_NACKED
   480  			config.ErrorState = &v3adminpb.UpdateFailureState{
   481  				Details:     "blahblah",
   482  				VersionInfo: nackVersion,
   483  			}
   484  		}
   485  		want = append(want, config)
   486  	}
   487  	if diff := cmp.Diff(filterFields(r.Config[0].GenericXdsConfigs), want, cmpOpts); diff != "" {
   488  		return fmt.Errorf(diff)
   489  	}
   490  	return nil
   491  }
   492  
   493  func TestCSDSNoXDSClient(t *testing.T) {
   494  	oldNewXDSClient := newXDSClient
   495  	newXDSClient = func() xdsclient.XDSClient { return nil }
   496  	defer func() { newXDSClient = oldNewXDSClient }()
   497  
   498  	// Initialize an gRPC server and register CSDS on it.
   499  	server := grpc.NewServer()
   500  	csdss, err := NewClientStatusDiscoveryServer()
   501  	if err != nil {
   502  		t.Fatal(err)
   503  	}
   504  	defer csdss.Close()
   505  	v3statuspbgrpc.RegisterClientStatusDiscoveryServiceServer(server, csdss)
   506  	// Create a local listener and pass it to Serve().
   507  	lis, err := testutils.LocalTCPListener()
   508  	if err != nil {
   509  		t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
   510  	}
   511  	go func() {
   512  		if err := server.Serve(lis); err != nil {
   513  			t.Errorf("Serve() failed: %v", err)
   514  		}
   515  	}()
   516  	defer server.Stop()
   517  
   518  	// Create CSDS client.
   519  	conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure())
   520  	if err != nil {
   521  		t.Fatalf("cannot connect to server: %v", err)
   522  	}
   523  	defer conn.Close()
   524  	c := v3statuspbgrpc.NewClientStatusDiscoveryServiceClient(conn)
   525  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   526  	defer cancel()
   527  	stream, err := c.StreamClientStatus(ctx, grpc.WaitForReady(true))
   528  	if err != nil {
   529  		t.Fatalf("cannot get ServerReflectionInfo: %v", err)
   530  	}
   531  
   532  	if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil {
   533  		t.Fatalf("failed to send: %v", err)
   534  	}
   535  	r, err := stream.Recv()
   536  	if err != nil {
   537  		// io.EOF is not ok.
   538  		t.Fatalf("failed to recv response: %v", err)
   539  	}
   540  	if n := len(r.Config); n != 0 {
   541  		t.Fatalf("got %d configs, want 0: %v", n, proto.MarshalTextString(r))
   542  	}
   543  }
   544  
   545  func Test_nodeProtoToV3(t *testing.T) {
   546  	const (
   547  		testID      = "test-id"
   548  		testCluster = "test-cluster"
   549  		testZone    = "test-zone"
   550  	)
   551  	tests := []struct {
   552  		name string
   553  		n    proto.Message
   554  		want *v3corepb.Node
   555  	}{
   556  		{
   557  			name: "v3",
   558  			n: &v3corepb.Node{
   559  				Id:       testID,
   560  				Cluster:  testCluster,
   561  				Locality: &v3corepb.Locality{Zone: testZone},
   562  			},
   563  			want: &v3corepb.Node{
   564  				Id:       testID,
   565  				Cluster:  testCluster,
   566  				Locality: &v3corepb.Locality{Zone: testZone},
   567  			},
   568  		},
   569  		{
   570  			name: "v2",
   571  			n: &v2corepb.Node{
   572  				Id:       testID,
   573  				Cluster:  testCluster,
   574  				Locality: &v2corepb.Locality{Zone: testZone},
   575  			},
   576  			want: &v3corepb.Node{
   577  				Id:       testID,
   578  				Cluster:  testCluster,
   579  				Locality: &v3corepb.Locality{Zone: testZone},
   580  			},
   581  		},
   582  		{
   583  			name: "not node",
   584  			n:    &v2corepb.Locality{Zone: testZone},
   585  			want: nil, // Input is not a node, should return nil.
   586  		},
   587  	}
   588  	for _, tt := range tests {
   589  		t.Run(tt.name, func(t *testing.T) {
   590  			got := nodeProtoToV3(tt.n)
   591  			if diff := cmp.Diff(got, tt.want, protocmp.Transform()); diff != "" {
   592  				t.Errorf("nodeProtoToV3() got unexpected result, diff (-got, +want): %v", diff)
   593  			}
   594  		})
   595  	}
   596  }