google.golang.org/grpc@v1.72.2/xds/csds/csds_e2e_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_test
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"io"
    25  	"slices"
    26  	"strings"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/google/go-cmp/cmp"
    31  	"github.com/google/uuid"
    32  	"google.golang.org/grpc"
    33  	"google.golang.org/grpc/credentials/insecure"
    34  	"google.golang.org/grpc/internal/grpctest"
    35  	"google.golang.org/grpc/internal/pretty"
    36  	"google.golang.org/grpc/internal/testutils"
    37  	"google.golang.org/grpc/internal/testutils/xds/e2e"
    38  	"google.golang.org/grpc/internal/xds/bootstrap"
    39  	"google.golang.org/grpc/xds/csds"
    40  	"google.golang.org/grpc/xds/internal/xdsclient"
    41  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
    42  	"google.golang.org/protobuf/encoding/prototext"
    43  	"google.golang.org/protobuf/testing/protocmp"
    44  	"google.golang.org/protobuf/types/known/anypb"
    45  
    46  	v3adminpb "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
    47  	v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
    48  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    49  	v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
    50  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    51  	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    52  	v3statuspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
    53  	v3statuspbgrpc "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
    54  
    55  	_ "google.golang.org/grpc/xds/internal/httpfilter/router" // Register the router filter
    56  )
    57  
    58  const defaultTestTimeout = 5 * time.Second
    59  
    60  type s struct {
    61  	grpctest.Tester
    62  }
    63  
    64  func Test(t *testing.T) {
    65  	grpctest.RunSubTests(t, s{})
    66  }
    67  
    68  // The following watcher implementations are no-ops since we don't really care
    69  // about the callback received by these watchers in the test. We only care
    70  // whether CSDS reports the expected state.
    71  
    72  type nopListenerWatcher struct{}
    73  
    74  func (nopListenerWatcher) OnUpdate(_ *xdsresource.ListenerResourceData, onDone xdsresource.OnDoneFunc) {
    75  	onDone()
    76  }
    77  func (nopListenerWatcher) OnError(_ error, onDone xdsresource.OnDoneFunc) {
    78  	onDone()
    79  }
    80  func (nopListenerWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) {
    81  	onDone()
    82  }
    83  
    84  type nopRouteConfigWatcher struct{}
    85  
    86  func (nopRouteConfigWatcher) OnUpdate(_ *xdsresource.RouteConfigResourceData, onDone xdsresource.OnDoneFunc) {
    87  	onDone()
    88  }
    89  func (nopRouteConfigWatcher) OnError(_ error, onDone xdsresource.OnDoneFunc) {
    90  	onDone()
    91  }
    92  func (nopRouteConfigWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) {
    93  	onDone()
    94  }
    95  
    96  type nopClusterWatcher struct{}
    97  
    98  func (nopClusterWatcher) OnUpdate(_ *xdsresource.ClusterResourceData, onDone xdsresource.OnDoneFunc) {
    99  	onDone()
   100  }
   101  func (nopClusterWatcher) OnError(_ error, onDone xdsresource.OnDoneFunc) {
   102  	onDone()
   103  }
   104  func (nopClusterWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) {
   105  	onDone()
   106  }
   107  
   108  type nopEndpointsWatcher struct{}
   109  
   110  func (nopEndpointsWatcher) OnUpdate(_ *xdsresource.EndpointsResourceData, onDone xdsresource.OnDoneFunc) {
   111  	onDone()
   112  }
   113  func (nopEndpointsWatcher) OnError(_ error, onDone xdsresource.OnDoneFunc) {
   114  	onDone()
   115  }
   116  func (nopEndpointsWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) {
   117  	onDone()
   118  }
   119  
   120  // This watcher writes the onDone callback on to a channel for the test to
   121  // invoke it when it wants to unblock the next read on the ADS stream in the xDS
   122  // client. This is particularly useful when a resource is NACKed, because the
   123  // go-control-plane management server continuously resends the same resource in
   124  // this case, and applying flow control from these watchers ensures that xDS
   125  // client does not spend all of its time receiving and NACKing updates from the
   126  // management server. This was indeed the case on arm64 (before we had support
   127  // for ADS stream level flow control), and was causing CSDS to not receive any
   128  // updates from the xDS client.
   129  type blockingListenerWatcher struct {
   130  	testCtxDone <-chan struct{}             // Closed when the test is done.
   131  	onDoneCh    chan xdsresource.OnDoneFunc // Channel to write the onDone callback to.
   132  }
   133  
   134  func newBlockingListenerWatcher(testCtxDone <-chan struct{}) *blockingListenerWatcher {
   135  	return &blockingListenerWatcher{
   136  		testCtxDone: testCtxDone,
   137  		onDoneCh:    make(chan xdsresource.OnDoneFunc, 1),
   138  	}
   139  }
   140  
   141  func (w *blockingListenerWatcher) OnUpdate(_ *xdsresource.ListenerResourceData, onDone xdsresource.OnDoneFunc) {
   142  	writeOnDone(w.testCtxDone, w.onDoneCh, onDone)
   143  }
   144  func (w *blockingListenerWatcher) OnError(_ error, onDone xdsresource.OnDoneFunc) {
   145  	writeOnDone(w.testCtxDone, w.onDoneCh, onDone)
   146  }
   147  func (w *blockingListenerWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) {
   148  	writeOnDone(w.testCtxDone, w.onDoneCh, onDone)
   149  }
   150  
   151  // writeOnDone attempts to write the onDone callback on the onDone channel. It
   152  // returns when it can successfully write to the channel or when the test is
   153  // done, which is signalled by testCtxDone being closed.
   154  func writeOnDone(testCtxDone <-chan struct{}, onDoneCh chan xdsresource.OnDoneFunc, onDone xdsresource.OnDoneFunc) {
   155  	select {
   156  	case <-testCtxDone:
   157  	case onDoneCh <- onDone:
   158  	}
   159  }
   160  
   161  // Creates a gRPC server and starts serving a CSDS service implementation on it.
   162  // Returns the address of the newly created gRPC server.
   163  //
   164  // Registers cleanup functions on t to stop the gRPC server and the CSDS
   165  // implementation.
   166  func startCSDSServer(t *testing.T) string {
   167  	t.Helper()
   168  
   169  	server := grpc.NewServer()
   170  	t.Cleanup(server.Stop)
   171  
   172  	csdss, err := csds.NewClientStatusDiscoveryServer()
   173  	if err != nil {
   174  		t.Fatalf("Failed to create CSDS service implementation: %v", err)
   175  	}
   176  	v3statuspbgrpc.RegisterClientStatusDiscoveryServiceServer(server, csdss)
   177  	t.Cleanup(csdss.Close)
   178  
   179  	// Create a local listener and pass it to Serve().
   180  	lis, err := testutils.LocalTCPListener()
   181  	if err != nil {
   182  		t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
   183  	}
   184  	go func() {
   185  		if err := server.Serve(lis); err != nil {
   186  			t.Errorf("Serve() failed: %v", err)
   187  		}
   188  	}()
   189  	return lis.Addr().String()
   190  }
   191  
   192  func startCSDSClientStream(ctx context.Context, t *testing.T, serverAddr string) v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient {
   193  	conn, err := grpc.NewClient(serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
   194  	if err != nil {
   195  		t.Fatalf("Failed to dial CSDS server %q: %v", serverAddr, err)
   196  	}
   197  
   198  	client := v3statuspbgrpc.NewClientStatusDiscoveryServiceClient(conn)
   199  	stream, err := client.StreamClientStatus(ctx, grpc.WaitForReady(true))
   200  	if err != nil {
   201  		t.Fatalf("Failed to create a stream for CSDS: %v", err)
   202  	}
   203  	t.Cleanup(func() { conn.Close() })
   204  	return stream
   205  }
   206  
   207  // Tests CSDS functionality. The test performs the following:
   208  //   - Spins up a management server and creates two xDS clients talking to it.
   209  //   - Registers a set of watches on the xDS clients, and verifies that the CSDS
   210  //     response reports resources in REQUESTED state.
   211  //   - Configures resources on the management server corresponding to the ones
   212  //     being watched by the clients, and verifies that the CSDS response reports
   213  //     resources in ACKED state.
   214  //
   215  // For the above operations, the test also verifies that the client_scope field
   216  // in the CSDS response is populated appropriately.
   217  func (s) TestCSDS(t *testing.T) {
   218  	// Spin up a xDS management server on a local port.
   219  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   220  
   221  	// Create a bootstrap contents pointing to the above management server.
   222  	nodeID := uuid.New().String()
   223  	bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
   224  	config, err := bootstrap.NewConfigFromContents(bootstrapContents)
   225  	if err != nil {
   226  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
   227  	}
   228  	// We use the default xDS client pool here because the CSDS service reports
   229  	// on the state of the default xDS client which is implicitly managed
   230  	// within the xdsclient.DefaultPool.
   231  	xdsclient.DefaultPool.SetFallbackBootstrapConfig(config)
   232  	defer func() { xdsclient.DefaultPool.UnsetBootstrapConfigForTesting() }()
   233  	// Create two xDS clients, with different names. These should end up
   234  	// creating two different xDS clients.
   235  	const xdsClient1Name = "xds-csds-client-1"
   236  	xdsClient1, xdsClose1, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{
   237  		Name: xdsClient1Name,
   238  	})
   239  	if err != nil {
   240  		t.Fatalf("Failed to create xDS client: %v", err)
   241  	}
   242  	defer xdsClose1()
   243  	const xdsClient2Name = "xds-csds-client-2"
   244  	xdsClient2, xdsClose2, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{
   245  		Name: xdsClient2Name,
   246  	})
   247  	if err != nil {
   248  		t.Fatalf("Failed to create xDS client: %v", err)
   249  	}
   250  	defer xdsClose2()
   251  
   252  	// Start a CSDS server and create a client stream to it.
   253  	addr := startCSDSServer(t)
   254  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   255  	defer cancel()
   256  	stream := startCSDSClientStream(ctx, t, addr)
   257  
   258  	// Verify that the xDS client reports an empty config.
   259  	wantNode := &v3corepb.Node{
   260  		Id:                   nodeID,
   261  		UserAgentName:        "gRPC Go",
   262  		UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
   263  		ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   264  	}
   265  	wantResp := &v3statuspb.ClientStatusResponse{
   266  		Config: []*v3statuspb.ClientConfig{
   267  			{
   268  				Node:        wantNode,
   269  				ClientScope: xdsClient1Name,
   270  			},
   271  			{
   272  				Node:        wantNode,
   273  				ClientScope: xdsClient2Name,
   274  			},
   275  		},
   276  	}
   277  	if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
   278  		t.Fatal(err)
   279  	}
   280  
   281  	// Initialize the xDS resources to be used in this test.
   282  	ldsTargets := []string{"lds.target.good:0000", "lds.target.good:1111"}
   283  	rdsTargets := []string{"route-config-0", "route-config-1"}
   284  	cdsTargets := []string{"cluster-0", "cluster-1"}
   285  	edsTargets := []string{"endpoints-0", "endpoints-1"}
   286  	listeners := make([]*v3listenerpb.Listener, len(ldsTargets))
   287  	listenerAnys := make([]*anypb.Any, len(ldsTargets))
   288  	for i := range ldsTargets {
   289  		listeners[i] = e2e.DefaultClientListener(ldsTargets[i], rdsTargets[i])
   290  		listenerAnys[i] = testutils.MarshalAny(t, listeners[i])
   291  	}
   292  	routes := make([]*v3routepb.RouteConfiguration, len(rdsTargets))
   293  	routeAnys := make([]*anypb.Any, len(rdsTargets))
   294  	for i := range rdsTargets {
   295  		routes[i] = e2e.DefaultRouteConfig(rdsTargets[i], ldsTargets[i], cdsTargets[i])
   296  		routeAnys[i] = testutils.MarshalAny(t, routes[i])
   297  	}
   298  	clusters := make([]*v3clusterpb.Cluster, len(cdsTargets))
   299  	clusterAnys := make([]*anypb.Any, len(cdsTargets))
   300  	for i := range cdsTargets {
   301  		clusters[i] = e2e.DefaultCluster(cdsTargets[i], edsTargets[i], e2e.SecurityLevelNone)
   302  		clusterAnys[i] = testutils.MarshalAny(t, clusters[i])
   303  	}
   304  	endpoints := make([]*v3endpointpb.ClusterLoadAssignment, len(edsTargets))
   305  	endpointAnys := make([]*anypb.Any, len(edsTargets))
   306  	ips := []string{"0.0.0.0", "1.1.1.1"}
   307  	ports := []uint32{123, 456}
   308  	for i := range edsTargets {
   309  		endpoints[i] = e2e.DefaultEndpoint(edsTargets[i], ips[i], ports[i:i+1])
   310  		endpointAnys[i] = testutils.MarshalAny(t, endpoints[i])
   311  	}
   312  
   313  	// Register watches on the xDS clients for two resources of each type.
   314  	for _, xdsC := range []xdsclient.XDSClient{xdsClient1, xdsClient2} {
   315  		for _, target := range ldsTargets {
   316  			xdsresource.WatchListener(xdsC, target, nopListenerWatcher{})
   317  		}
   318  		for _, target := range rdsTargets {
   319  			xdsresource.WatchRouteConfig(xdsC, target, nopRouteConfigWatcher{})
   320  		}
   321  		for _, target := range cdsTargets {
   322  			xdsresource.WatchCluster(xdsC, target, nopClusterWatcher{})
   323  		}
   324  		for _, target := range edsTargets {
   325  			xdsresource.WatchEndpoints(xdsC, target, nopEndpointsWatcher{})
   326  		}
   327  	}
   328  
   329  	// Verify that the xDS client reports the resources as being in "Requested"
   330  	// state, and in version "0".
   331  	wantConfigs := []*v3statuspb.ClientConfig_GenericXdsConfig{
   332  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   333  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   334  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   335  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   336  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   337  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   338  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   339  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   340  	}
   341  	wantResp = &v3statuspb.ClientStatusResponse{
   342  		Config: []*v3statuspb.ClientConfig{
   343  			{
   344  				Node:              wantNode,
   345  				GenericXdsConfigs: wantConfigs,
   346  				ClientScope:       xdsClient1Name,
   347  			},
   348  			{
   349  				Node:              wantNode,
   350  				GenericXdsConfigs: wantConfigs,
   351  				ClientScope:       xdsClient2Name,
   352  			},
   353  		},
   354  	}
   355  	if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
   356  		t.Fatal(err)
   357  	}
   358  
   359  	// Configure the management server with two resources of each type,
   360  	// corresponding to the watches registered above.
   361  	if err := mgmtServer.Update(ctx, e2e.UpdateOptions{
   362  		NodeID:    nodeID,
   363  		Listeners: listeners,
   364  		Routes:    routes,
   365  		Clusters:  clusters,
   366  		Endpoints: endpoints,
   367  	}); err != nil {
   368  		t.Fatal(err)
   369  	}
   370  
   371  	// Verify that the xDS client reports the resources as being in "ACKed"
   372  	// state, and in version "1".
   373  	wantConfigs = []*v3statuspb.ClientConfig_GenericXdsConfig{
   374  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, clusterAnys[0], nil),
   375  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, clusterAnys[1], nil),
   376  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, endpointAnys[0], nil),
   377  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, endpointAnys[1], nil),
   378  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[0], nil),
   379  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil),
   380  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, routeAnys[0], nil),
   381  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, routeAnys[1], nil),
   382  	}
   383  	wantResp = &v3statuspb.ClientStatusResponse{
   384  		Config: []*v3statuspb.ClientConfig{
   385  			{
   386  				Node:              wantNode,
   387  				GenericXdsConfigs: wantConfigs,
   388  				ClientScope:       xdsClient1Name,
   389  			},
   390  			{
   391  				Node:              wantNode,
   392  				GenericXdsConfigs: wantConfigs,
   393  				ClientScope:       xdsClient2Name,
   394  			},
   395  		},
   396  	}
   397  	if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
   398  		t.Fatal(err)
   399  	}
   400  }
   401  
   402  // Tests CSDS functionality. The test performs the following:
   403  //   - Spins up a management server and creates two xDS clients talking to it.
   404  //   - Registers one watch on each xDS client, and verifies that the CSDS
   405  //     response reports resources in REQUESTED state.
   406  //   - Configures two resources on the management server and verifies that the
   407  //     CSDS response reports the resources as being in ACKED state.
   408  //   - Updates one of two resources on the management server such that it is
   409  //     expected to be NACKed by the client. Verifies that the CSDS response
   410  //     contains one resource in ACKED state and one in NACKED state.
   411  //
   412  // For the above operations, the test also verifies that the client_scope field
   413  // in the CSDS response is populated appropriately.
   414  //
   415  // This test does a bunch of similar things to the previous test, but has
   416  // reduced complexity because of having to deal with a single resource type.
   417  // This makes it possible to test the NACKing a resource (which results in
   418  // continuous resending of the resource by the go-control-plane management
   419  // server), in an easier and less flaky way.
   420  func (s) TestCSDS_NACK(t *testing.T) {
   421  	// Spin up a xDS management server on a local port.
   422  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
   423  
   424  	// Create a bootstrap contents pointing to the above management server.
   425  	nodeID := uuid.New().String()
   426  	bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
   427  	config, err := bootstrap.NewConfigFromContents(bootstrapContents)
   428  	if err != nil {
   429  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
   430  	}
   431  	// We use the default xDS client pool here because the CSDS service reports
   432  	// on the state of the default xDS client which is implicitly managed
   433  	// within the xdsclient.DefaultPool.
   434  	xdsclient.DefaultPool.SetFallbackBootstrapConfig(config)
   435  	defer func() { xdsclient.DefaultPool.UnsetBootstrapConfigForTesting() }()
   436  	// Create two xDS clients, with different names. These should end up
   437  	// creating two different xDS clients.
   438  	const xdsClient1Name = "xds-csds-client-1"
   439  	xdsClient1, xdsClose1, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{
   440  		Name: xdsClient1Name,
   441  	})
   442  	if err != nil {
   443  		t.Fatalf("Failed to create xDS client: %v", err)
   444  	}
   445  	defer xdsClose1()
   446  	const xdsClient2Name = "xds-csds-client-2"
   447  	xdsClient2, xdsClose2, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{
   448  		Name: xdsClient2Name,
   449  	})
   450  	if err != nil {
   451  		t.Fatalf("Failed to create xDS client: %v", err)
   452  	}
   453  	defer xdsClose2()
   454  
   455  	// Start a CSDS server and create a client stream to it.
   456  	addr := startCSDSServer(t)
   457  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   458  	defer cancel()
   459  	stream := startCSDSClientStream(ctx, t, addr)
   460  
   461  	// Verify that the xDS client reports an empty config.
   462  	wantNode := &v3corepb.Node{
   463  		Id:                   nodeID,
   464  		UserAgentName:        "gRPC Go",
   465  		UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
   466  		ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   467  	}
   468  	wantResp := &v3statuspb.ClientStatusResponse{
   469  		Config: []*v3statuspb.ClientConfig{
   470  			{
   471  				Node:        wantNode,
   472  				ClientScope: xdsClient1Name,
   473  			},
   474  			{
   475  				Node:        wantNode,
   476  				ClientScope: xdsClient2Name,
   477  			},
   478  		},
   479  	}
   480  	if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
   481  		t.Fatal(err)
   482  	}
   483  
   484  	// Initialize the xDS resources to be used in this test.
   485  	const ldsTarget0, ldsTarget1 = "lds.target.good:0000", "lds.target.good:1111"
   486  	listener0 := e2e.DefaultClientListener(ldsTarget0, "rds-name")
   487  	listener1 := e2e.DefaultClientListener(ldsTarget1, "rds-name")
   488  	listenerAny0 := testutils.MarshalAny(t, listener0)
   489  	listenerAny1 := testutils.MarshalAny(t, listener1)
   490  
   491  	// Register the watchers, one for each xDS client.
   492  	watcher1 := nopListenerWatcher{}
   493  	watcher2 := newBlockingListenerWatcher(ctx.Done())
   494  	xdsresource.WatchListener(xdsClient1, ldsTarget0, watcher1)
   495  	xdsresource.WatchListener(xdsClient2, ldsTarget1, watcher2)
   496  
   497  	// Verify that the xDS client reports the resources as being in "Requested"
   498  	// state, and in version "0".
   499  	wantResp = &v3statuspb.ClientStatusResponse{
   500  		Config: []*v3statuspb.ClientConfig{
   501  			{
   502  				Node: wantNode,
   503  				GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
   504  					makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget0, "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   505  				},
   506  				ClientScope: xdsClient1Name,
   507  			},
   508  			{
   509  				Node: wantNode,
   510  				GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
   511  					makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget1, "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   512  				},
   513  				ClientScope: xdsClient2Name,
   514  			},
   515  		},
   516  	}
   517  	if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
   518  		t.Fatal(err)
   519  	}
   520  
   521  	// Configure the management server with two listener resources corresponding
   522  	// to the watches registered above.
   523  	if err := mgmtServer.Update(ctx, e2e.UpdateOptions{
   524  		NodeID:         nodeID,
   525  		Listeners:      []*v3listenerpb.Listener{listener0, listener1},
   526  		SkipValidation: true,
   527  	}); err != nil {
   528  		t.Fatal(err)
   529  	}
   530  
   531  	// Verify that the xDS client reports the resources as being in "ACKed"
   532  	// state, and in version "1".
   533  	wantResp = &v3statuspb.ClientStatusResponse{
   534  		Config: []*v3statuspb.ClientConfig{
   535  			{
   536  				Node: wantNode,
   537  				GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
   538  					makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget0, "1", v3adminpb.ClientResourceStatus_ACKED, listenerAny0, nil),
   539  				},
   540  				ClientScope: xdsClient1Name,
   541  			},
   542  			{
   543  				Node: wantNode,
   544  				GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
   545  					makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget1, "1", v3adminpb.ClientResourceStatus_ACKED, listenerAny1, nil),
   546  				},
   547  				ClientScope: xdsClient2Name,
   548  			},
   549  		},
   550  	}
   551  	if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
   552  		t.Fatal(err)
   553  	}
   554  
   555  	// Unblock reads on the ADS stream by calling the onDone callback sent to
   556  	// the watcher.
   557  	select {
   558  	case <-ctx.Done():
   559  		t.Fatal("Timed out waiting for watch callback")
   560  	case onDone := <-watcher2.onDoneCh:
   561  		onDone()
   562  	}
   563  
   564  	// Update the second resource with an empty ApiListener field which is
   565  	// expected to be NACK'ed by the xDS client.
   566  	listener1.ApiListener = nil
   567  	if err := mgmtServer.Update(ctx, e2e.UpdateOptions{
   568  		NodeID:         nodeID,
   569  		Listeners:      []*v3listenerpb.Listener{listener0, listener1},
   570  		SkipValidation: true,
   571  	}); err != nil {
   572  		t.Fatal(err)
   573  	}
   574  
   575  	// Wait for the update to reach the watchers.
   576  	select {
   577  	case <-ctx.Done():
   578  		t.Fatal("Timed out waiting for watch callback")
   579  	case onDone := <-watcher2.onDoneCh:
   580  		onDone()
   581  	}
   582  
   583  	// Verify that the xDS client reports the first listener resource as being
   584  	// ACKed and the second listener resource as being NACKed.  The version for
   585  	// the ACKed resource would be "2", while that for the NACKed resource would
   586  	// be "1". In the NACKed resource, the version which is NACKed is stored in
   587  	// the ErrorState field.
   588  	wantResp = &v3statuspb.ClientStatusResponse{
   589  		Config: []*v3statuspb.ClientConfig{
   590  			{
   591  				Node: wantNode,
   592  				GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
   593  					makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget0, "2", v3adminpb.ClientResourceStatus_ACKED, listenerAny0, nil),
   594  				},
   595  				ClientScope: xdsClient1Name,
   596  			},
   597  			{
   598  				Node: wantNode,
   599  				GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
   600  					makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget1, "1", v3adminpb.ClientResourceStatus_NACKED, listenerAny1, &v3adminpb.UpdateFailureState{VersionInfo: "2"}),
   601  				},
   602  				ClientScope: xdsClient2Name,
   603  			},
   604  		},
   605  	}
   606  	if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
   607  		t.Fatal(err)
   608  	}
   609  }
   610  
   611  func makeGenericXdsConfig(typeURL, name, version string, status v3adminpb.ClientResourceStatus, config *anypb.Any, failure *v3adminpb.UpdateFailureState) *v3statuspb.ClientConfig_GenericXdsConfig {
   612  	return &v3statuspb.ClientConfig_GenericXdsConfig{
   613  		TypeUrl:      typeURL,
   614  		Name:         name,
   615  		VersionInfo:  version,
   616  		ClientStatus: status,
   617  		XdsConfig:    config,
   618  		ErrorState:   failure,
   619  	}
   620  }
   621  
   622  // Repeatedly sends CSDS requests and receives CSDS responses on the provided
   623  // stream and verifies that the response matches `want`. Returns an error if
   624  // sending or receiving on the stream fails, or if the context expires before a
   625  // response matching `want` is received.
   626  //
   627  // Expects client configs in `want` to be sorted on `client_scope` and the
   628  // resource dump to be sorted on type_url and resource name.
   629  func checkClientStatusResponse(ctx context.Context, stream v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient, want *v3statuspb.ClientStatusResponse) error {
   630  	var cmpOpts = cmp.Options{
   631  		protocmp.Transform(),
   632  		protocmp.IgnoreFields((*v3statuspb.ClientConfig_GenericXdsConfig)(nil), "last_updated"),
   633  		protocmp.IgnoreFields((*v3adminpb.UpdateFailureState)(nil), "last_update_attempt", "details"),
   634  	}
   635  
   636  	var lastErr error
   637  	for ; ctx.Err() == nil; <-time.After(100 * time.Millisecond) {
   638  		if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil {
   639  			if err != io.EOF {
   640  				return fmt.Errorf("failed to send ClientStatusRequest: %v", err)
   641  			}
   642  			// If the stream has closed, we call Recv() until it returns a non-nil
   643  			// error to get the actual error on the stream.
   644  			for {
   645  				if _, err := stream.Recv(); err != nil {
   646  					return fmt.Errorf("failed to recv ClientStatusResponse: %v", err)
   647  				}
   648  			}
   649  		}
   650  		got, err := stream.Recv()
   651  		if err != nil {
   652  			return fmt.Errorf("failed to recv ClientStatusResponse: %v", err)
   653  		}
   654  		// Sort the client configs based on the `client_scope` field.
   655  		slices.SortFunc(got.GetConfig(), func(a, b *v3statuspb.ClientConfig) int {
   656  			return strings.Compare(a.ClientScope, b.ClientScope)
   657  		})
   658  		// Sort the resource configs based on the type_url and name fields.
   659  		for _, cc := range got.GetConfig() {
   660  			slices.SortFunc(cc.GetGenericXdsConfigs(), func(a, b *v3statuspb.ClientConfig_GenericXdsConfig) int {
   661  				if strings.Compare(a.TypeUrl, b.TypeUrl) == 0 {
   662  					return strings.Compare(a.Name, b.Name)
   663  				}
   664  				return strings.Compare(a.TypeUrl, b.TypeUrl)
   665  			})
   666  		}
   667  		diff := cmp.Diff(want, got, cmpOpts)
   668  		if diff == "" {
   669  			return nil
   670  		}
   671  		lastErr = fmt.Errorf("received unexpected resource dump, diff (-got, +want):\n%s, got: %s\n want:%s", diff, pretty.ToJSON(got), pretty.ToJSON(want))
   672  	}
   673  	return fmt.Errorf("timeout when waiting for resource dump to reach expected state: ctxErr: %v, otherErr: %v", ctx.Err(), lastErr)
   674  }
   675  
   676  func (s) TestCSDSNoXDSClient(t *testing.T) {
   677  	// Create a bootstrap file in a temporary directory. Since we pass an empty
   678  	// bootstrap configuration, it will fail xDS client creation because the
   679  	// `server_uri` field is unset.
   680  	testutils.CreateBootstrapFileForTesting(t, []byte(``))
   681  
   682  	// Start a CSDS server and create a client stream to it.
   683  	addr := startCSDSServer(t)
   684  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   685  	defer cancel()
   686  	stream := startCSDSClientStream(ctx, t, addr)
   687  
   688  	if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil {
   689  		t.Fatalf("Failed to send ClientStatusRequest: %v", err)
   690  	}
   691  	r, err := stream.Recv()
   692  	if err != nil {
   693  		// io.EOF is not ok.
   694  		t.Fatalf("Failed to recv ClientStatusResponse: %v", err)
   695  	}
   696  	if n := len(r.Config); n != 0 {
   697  		t.Fatalf("got %d configs, want 0: %v", n, prototext.Format(r))
   698  	}
   699  }