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

     1  /*
     2   *
     3   * Copyright 2022 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  package xdsclient_test
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    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/internal/pretty"
    34  	"google.golang.org/grpc/internal/testutils"
    35  	"google.golang.org/grpc/internal/testutils/xds/e2e"
    36  	"google.golang.org/grpc/internal/xds/bootstrap"
    37  	"google.golang.org/grpc/xds/internal/xdsclient"
    38  	"google.golang.org/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/envoyproxy/go-control-plane/envoy/admin/v3"
    43  	v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
    44  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    45  	v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
    46  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    47  	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    48  	v3routerpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
    49  	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    50  	v3statuspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
    51  	"github.com/envoyproxy/go-control-plane/pkg/wellknown"
    52  )
    53  
    54  func makeGenericXdsConfig(typeURL, name, version string, status v3adminpb.ClientResourceStatus, config *anypb.Any, failure *v3adminpb.UpdateFailureState) *v3statuspb.ClientConfig_GenericXdsConfig {
    55  	return &v3statuspb.ClientConfig_GenericXdsConfig{
    56  		TypeUrl:      typeURL,
    57  		Name:         name,
    58  		VersionInfo:  version,
    59  		ClientStatus: status,
    60  		XdsConfig:    config,
    61  		ErrorState:   failure,
    62  	}
    63  }
    64  
    65  func checkResourceDump(ctx context.Context, want *v3statuspb.ClientStatusResponse, pool *xdsclient.Pool) error {
    66  	var cmpOpts = cmp.Options{
    67  		protocmp.Transform(),
    68  		protocmp.IgnoreFields((*v3statuspb.ClientConfig_GenericXdsConfig)(nil), "last_updated"),
    69  		protocmp.IgnoreFields((*v3adminpb.UpdateFailureState)(nil), "last_update_attempt", "details"),
    70  	}
    71  
    72  	var lastErr error
    73  	for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) {
    74  		got := pool.DumpResources()
    75  		// Sort the client configs based on the `client_scope` field.
    76  		slices.SortFunc(got.GetConfig(), func(a, b *v3statuspb.ClientConfig) int {
    77  			return strings.Compare(a.ClientScope, b.ClientScope)
    78  		})
    79  		// Sort the resource configs based on the type_url and name fields.
    80  		for _, cc := range got.GetConfig() {
    81  			slices.SortFunc(cc.GetGenericXdsConfigs(), func(a, b *v3statuspb.ClientConfig_GenericXdsConfig) int {
    82  				if strings.Compare(a.TypeUrl, b.TypeUrl) == 0 {
    83  					return strings.Compare(a.Name, b.Name)
    84  				}
    85  				return strings.Compare(a.TypeUrl, b.TypeUrl)
    86  			})
    87  		}
    88  		diff := cmp.Diff(want, got, cmpOpts)
    89  		if diff == "" {
    90  			return nil
    91  		}
    92  		lastErr = fmt.Errorf("received unexpected resource dump, diff (-got, +want):\n%s, got: %s\n want:%s", diff, pretty.ToJSON(got), pretty.ToJSON(want))
    93  	}
    94  	return fmt.Errorf("timeout when waiting for resource dump to reach expected state: %v", lastErr)
    95  }
    96  
    97  // Tests the scenario where there are multiple xDS clients talking to the same
    98  // management server, and requesting the same set of resources. Verifies that
    99  // under all circumstances, both xDS clients receive the same configuration from
   100  // the server.
   101  func (s) TestDumpResources_ManyToOne(t *testing.T) {
   102  	// Initialize the xDS resources to be used in this test.
   103  	ldsTargets := []string{"lds.target.good:0000", "lds.target.good:1111"}
   104  	rdsTargets := []string{"route-config-0", "route-config-1"}
   105  	cdsTargets := []string{"cluster-0", "cluster-1"}
   106  	edsTargets := []string{"endpoints-0", "endpoints-1"}
   107  	listeners := make([]*v3listenerpb.Listener, len(ldsTargets))
   108  	listenerAnys := make([]*anypb.Any, len(ldsTargets))
   109  	for i := range ldsTargets {
   110  		listeners[i] = e2e.DefaultClientListener(ldsTargets[i], rdsTargets[i])
   111  		listenerAnys[i] = testutils.MarshalAny(t, listeners[i])
   112  	}
   113  	routes := make([]*v3routepb.RouteConfiguration, len(rdsTargets))
   114  	routeAnys := make([]*anypb.Any, len(rdsTargets))
   115  	for i := range rdsTargets {
   116  		routes[i] = e2e.DefaultRouteConfig(rdsTargets[i], ldsTargets[i], cdsTargets[i])
   117  		routeAnys[i] = testutils.MarshalAny(t, routes[i])
   118  	}
   119  	clusters := make([]*v3clusterpb.Cluster, len(cdsTargets))
   120  	clusterAnys := make([]*anypb.Any, len(cdsTargets))
   121  	for i := range cdsTargets {
   122  		clusters[i] = e2e.DefaultCluster(cdsTargets[i], edsTargets[i], e2e.SecurityLevelNone)
   123  		clusterAnys[i] = testutils.MarshalAny(t, clusters[i])
   124  	}
   125  	endpoints := make([]*v3endpointpb.ClusterLoadAssignment, len(edsTargets))
   126  	endpointAnys := make([]*anypb.Any, len(edsTargets))
   127  	ips := []string{"0.0.0.0", "1.1.1.1"}
   128  	ports := []uint32{123, 456}
   129  	for i := range edsTargets {
   130  		endpoints[i] = e2e.DefaultEndpoint(edsTargets[i], ips[i], ports[i:i+1])
   131  		endpointAnys[i] = testutils.MarshalAny(t, endpoints[i])
   132  	}
   133  
   134  	// Spin up an xDS management server on a local port.
   135  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   136  
   137  	nodeID := uuid.New().String()
   138  	bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
   139  	config, err := bootstrap.NewConfigFromContents(bc)
   140  	if err != nil {
   141  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err)
   142  	}
   143  	pool := xdsclient.NewPool(config)
   144  	// Create two xDS clients with the above bootstrap contents.
   145  	client1Name := t.Name() + "-1"
   146  	client1, close1, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
   147  		Name: client1Name,
   148  	})
   149  	if err != nil {
   150  		t.Fatalf("Failed to create xDS client: %v", err)
   151  	}
   152  	defer close1()
   153  	client2Name := t.Name() + "-2"
   154  	client2, close2, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
   155  		Name: client2Name,
   156  	})
   157  	if err != nil {
   158  		t.Fatalf("Failed to create xDS client: %v", err)
   159  	}
   160  	defer close2()
   161  
   162  	// Dump resources and expect empty configs.
   163  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   164  	defer cancel()
   165  	wantNode := &v3corepb.Node{
   166  		Id:                   nodeID,
   167  		UserAgentName:        "gRPC Go",
   168  		UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
   169  		ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   170  	}
   171  	wantResp := &v3statuspb.ClientStatusResponse{
   172  		Config: []*v3statuspb.ClientConfig{
   173  			{
   174  				Node:        wantNode,
   175  				ClientScope: client1Name,
   176  			},
   177  			{
   178  				Node:        wantNode,
   179  				ClientScope: client2Name,
   180  			},
   181  		},
   182  	}
   183  	if err := checkResourceDump(ctx, wantResp, pool); err != nil {
   184  		t.Fatal(err)
   185  	}
   186  
   187  	// Register watches, dump resources and expect configs in requested state.
   188  	for _, xdsC := range []xdsclient.XDSClient{client1, client2} {
   189  		for _, target := range ldsTargets {
   190  			xdsresource.WatchListener(xdsC, target, noopListenerWatcher{})
   191  		}
   192  		for _, target := range rdsTargets {
   193  			xdsresource.WatchRouteConfig(xdsC, target, noopRouteConfigWatcher{})
   194  		}
   195  		for _, target := range cdsTargets {
   196  			xdsresource.WatchCluster(xdsC, target, noopClusterWatcher{})
   197  		}
   198  		for _, target := range edsTargets {
   199  			xdsresource.WatchEndpoints(xdsC, target, noopEndpointsWatcher{})
   200  		}
   201  	}
   202  	wantConfigs := []*v3statuspb.ClientConfig_GenericXdsConfig{
   203  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   204  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   205  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   206  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   207  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   208  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   209  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   210  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   211  	}
   212  	wantResp = &v3statuspb.ClientStatusResponse{
   213  		Config: []*v3statuspb.ClientConfig{
   214  			{
   215  				Node:              wantNode,
   216  				GenericXdsConfigs: wantConfigs,
   217  				ClientScope:       client1Name,
   218  			},
   219  			{
   220  				Node:              wantNode,
   221  				GenericXdsConfigs: wantConfigs,
   222  				ClientScope:       client2Name,
   223  			},
   224  		},
   225  	}
   226  	if err := checkResourceDump(ctx, wantResp, pool); err != nil {
   227  		t.Fatal(err)
   228  	}
   229  
   230  	// Configure the resources on the management server.
   231  	if err := mgmtServer.Update(ctx, e2e.UpdateOptions{
   232  		NodeID:    nodeID,
   233  		Listeners: listeners,
   234  		Routes:    routes,
   235  		Clusters:  clusters,
   236  		Endpoints: endpoints,
   237  	}); err != nil {
   238  		t.Fatal(err)
   239  	}
   240  
   241  	// Dump resources and expect ACK configs.
   242  	wantConfigs = []*v3statuspb.ClientConfig_GenericXdsConfig{
   243  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, clusterAnys[0], nil),
   244  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, clusterAnys[1], nil),
   245  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, endpointAnys[0], nil),
   246  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, endpointAnys[1], nil),
   247  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[0], nil),
   248  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil),
   249  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, routeAnys[0], nil),
   250  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, routeAnys[1], nil),
   251  	}
   252  	wantResp = &v3statuspb.ClientStatusResponse{
   253  		Config: []*v3statuspb.ClientConfig{
   254  			{
   255  				Node:              wantNode,
   256  				GenericXdsConfigs: wantConfigs,
   257  				ClientScope:       client1Name,
   258  			},
   259  			{
   260  				Node:              wantNode,
   261  				GenericXdsConfigs: wantConfigs,
   262  				ClientScope:       client2Name,
   263  			},
   264  		},
   265  	}
   266  	if err := checkResourceDump(ctx, wantResp, pool); err != nil {
   267  		t.Fatal(err)
   268  	}
   269  
   270  	// Update the first resource of each type in the management server to a
   271  	// value which is expected to be NACK'ed by the xDS client.
   272  	listeners[0] = func() *v3listenerpb.Listener {
   273  		hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   274  			HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})},
   275  		})
   276  		return &v3listenerpb.Listener{
   277  			Name:        ldsTargets[0],
   278  			ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm},
   279  			FilterChains: []*v3listenerpb.FilterChain{{
   280  				Name: "filter-chain-name",
   281  				Filters: []*v3listenerpb.Filter{{
   282  					Name:       wellknown.HTTPConnectionManager,
   283  					ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm},
   284  				}},
   285  			}},
   286  		}
   287  	}()
   288  	routes[0].VirtualHosts = []*v3routepb.VirtualHost{{Routes: []*v3routepb.Route{{}}}}
   289  	clusters[0].ClusterDiscoveryType = &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_STATIC}
   290  	endpoints[0].Endpoints = []*v3endpointpb.LocalityLbEndpoints{{}}
   291  	if err := mgmtServer.Update(ctx, e2e.UpdateOptions{
   292  		NodeID:         nodeID,
   293  		Listeners:      listeners,
   294  		Routes:         routes,
   295  		Clusters:       clusters,
   296  		Endpoints:      endpoints,
   297  		SkipValidation: true,
   298  	}); err != nil {
   299  		t.Fatal(err)
   300  	}
   301  
   302  	wantConfigs = []*v3statuspb.ClientConfig_GenericXdsConfig{
   303  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[0], "1", v3adminpb.ClientResourceStatus_NACKED, clusterAnys[0], &v3adminpb.UpdateFailureState{VersionInfo: "2"}),
   304  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[1], "2", v3adminpb.ClientResourceStatus_ACKED, clusterAnys[1], nil),
   305  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[0], "1", v3adminpb.ClientResourceStatus_NACKED, endpointAnys[0], &v3adminpb.UpdateFailureState{VersionInfo: "2"}),
   306  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[1], "2", v3adminpb.ClientResourceStatus_ACKED, endpointAnys[1], nil),
   307  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_NACKED, listenerAnys[0], &v3adminpb.UpdateFailureState{VersionInfo: "2"}),
   308  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "2", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil),
   309  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[0], "1", v3adminpb.ClientResourceStatus_NACKED, routeAnys[0], &v3adminpb.UpdateFailureState{VersionInfo: "2"}),
   310  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[1], "2", v3adminpb.ClientResourceStatus_ACKED, routeAnys[1], nil),
   311  	}
   312  	wantResp = &v3statuspb.ClientStatusResponse{
   313  		Config: []*v3statuspb.ClientConfig{
   314  			{
   315  				Node:              wantNode,
   316  				GenericXdsConfigs: wantConfigs,
   317  				ClientScope:       client1Name,
   318  			},
   319  			{
   320  				Node:              wantNode,
   321  				GenericXdsConfigs: wantConfigs,
   322  				ClientScope:       client2Name,
   323  			},
   324  		},
   325  	}
   326  	if err := checkResourceDump(ctx, wantResp, pool); err != nil {
   327  		t.Fatal(err)
   328  	}
   329  }
   330  
   331  // Tests the scenario where there are multiple xDS client talking to different
   332  // management server, and requesting different set of resources.
   333  func (s) TestDumpResources_ManyToMany(t *testing.T) {
   334  	// Initialize the xDS resources to be used in this test:
   335  	// - The first xDS client watches old style resource names, and thereby
   336  	//   requests these resources from the top-level xDS server.
   337  	// - The second xDS client watches new style resource names with a non-empty
   338  	//   authority, and thereby requests these resources from the server
   339  	//   configuration for that authority.
   340  	authority := strings.Join(strings.Split(t.Name(), "/"), "")
   341  	ldsTargets := []string{
   342  		"lds.target.good:0000",
   343  		fmt.Sprintf("xdstp://%s/envoy.config.listener.v3.Listener/lds.targer.good:1111", authority),
   344  	}
   345  	rdsTargets := []string{
   346  		"route-config-0",
   347  		fmt.Sprintf("xdstp://%s/envoy.config.route.v3.RouteConfiguration/route-config-1", authority),
   348  	}
   349  	cdsTargets := []string{
   350  		"cluster-0",
   351  		fmt.Sprintf("xdstp://%s/envoy.config.cluster.v3.Cluster/cluster-1", authority),
   352  	}
   353  	edsTargets := []string{
   354  		"endpoints-0",
   355  		fmt.Sprintf("xdstp://%s/envoy.config.endpoint.v3.ClusterLoadAssignment/endpoints-1", authority),
   356  	}
   357  	listeners := make([]*v3listenerpb.Listener, len(ldsTargets))
   358  	listenerAnys := make([]*anypb.Any, len(ldsTargets))
   359  	for i := range ldsTargets {
   360  		listeners[i] = e2e.DefaultClientListener(ldsTargets[i], rdsTargets[i])
   361  		listenerAnys[i] = testutils.MarshalAny(t, listeners[i])
   362  	}
   363  	routes := make([]*v3routepb.RouteConfiguration, len(rdsTargets))
   364  	routeAnys := make([]*anypb.Any, len(rdsTargets))
   365  	for i := range rdsTargets {
   366  		routes[i] = e2e.DefaultRouteConfig(rdsTargets[i], ldsTargets[i], cdsTargets[i])
   367  		routeAnys[i] = testutils.MarshalAny(t, routes[i])
   368  	}
   369  	clusters := make([]*v3clusterpb.Cluster, len(cdsTargets))
   370  	clusterAnys := make([]*anypb.Any, len(cdsTargets))
   371  	for i := range cdsTargets {
   372  		clusters[i] = e2e.DefaultCluster(cdsTargets[i], edsTargets[i], e2e.SecurityLevelNone)
   373  		clusterAnys[i] = testutils.MarshalAny(t, clusters[i])
   374  	}
   375  	endpoints := make([]*v3endpointpb.ClusterLoadAssignment, len(edsTargets))
   376  	endpointAnys := make([]*anypb.Any, len(edsTargets))
   377  	ips := []string{"0.0.0.0", "1.1.1.1"}
   378  	ports := []uint32{123, 456}
   379  	for i := range edsTargets {
   380  		endpoints[i] = e2e.DefaultEndpoint(edsTargets[i], ips[i], ports[i:i+1])
   381  		endpointAnys[i] = testutils.MarshalAny(t, endpoints[i])
   382  	}
   383  
   384  	// Start two management servers.
   385  	mgmtServer1 := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   386  	mgmtServer2 := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   387  
   388  	// The first of the above management servers will be the top-level xDS
   389  	// server in the bootstrap configuration, and the second will be the xDS
   390  	// server corresponding to the test authority.
   391  	nodeID := uuid.New().String()
   392  	bc, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
   393  		Servers: []byte(fmt.Sprintf(`[{
   394  			"server_uri": %q,
   395  			"channel_creds": [{"type": "insecure"}]
   396  		}]`, mgmtServer1.Address)),
   397  		Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
   398  		Authorities: map[string]json.RawMessage{
   399  			authority: []byte(fmt.Sprintf(`{
   400  				 "xds_servers": [{
   401  					"server_uri": %q,
   402  					"channel_creds": [{"type": "insecure"}]
   403  				}]}`, mgmtServer2.Address)),
   404  		},
   405  	})
   406  	if err != nil {
   407  		t.Fatalf("Failed to create bootstrap configuration: %v", err)
   408  	}
   409  	config, err := bootstrap.NewConfigFromContents(bc)
   410  	if err != nil {
   411  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err)
   412  	}
   413  	pool := xdsclient.NewPool(config)
   414  
   415  	// Create two xDS clients with the above bootstrap contents.
   416  	client1Name := t.Name() + "-1"
   417  	client1, close1, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
   418  		Name: client1Name,
   419  	})
   420  	if err != nil {
   421  		t.Fatalf("Failed to create xDS client: %v", err)
   422  	}
   423  	defer close1()
   424  	client2Name := t.Name() + "-2"
   425  	client2, close2, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
   426  		Name: client2Name,
   427  	})
   428  	if err != nil {
   429  		t.Fatalf("Failed to create xDS client: %v", err)
   430  	}
   431  	defer close2()
   432  
   433  	// Check the resource dump before configuring resources on the management server.
   434  	// Dump resources and expect empty configs.
   435  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   436  	defer cancel()
   437  	wantNode := &v3corepb.Node{
   438  		Id:                   nodeID,
   439  		UserAgentName:        "gRPC Go",
   440  		UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
   441  		ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   442  	}
   443  	wantResp := &v3statuspb.ClientStatusResponse{
   444  		Config: []*v3statuspb.ClientConfig{
   445  			{
   446  				Node:        wantNode,
   447  				ClientScope: client1Name,
   448  			},
   449  			{
   450  				Node:        wantNode,
   451  				ClientScope: client2Name,
   452  			},
   453  		},
   454  	}
   455  	if err := checkResourceDump(ctx, wantResp, pool); err != nil {
   456  		t.Fatal(err)
   457  	}
   458  
   459  	// Register watches, the first xDS client watches old style resource names,
   460  	// while the second xDS client watches new style resource names.
   461  	xdsresource.WatchListener(client1, ldsTargets[0], noopListenerWatcher{})
   462  	xdsresource.WatchRouteConfig(client1, rdsTargets[0], noopRouteConfigWatcher{})
   463  	xdsresource.WatchCluster(client1, cdsTargets[0], noopClusterWatcher{})
   464  	xdsresource.WatchEndpoints(client1, edsTargets[0], noopEndpointsWatcher{})
   465  	xdsresource.WatchListener(client2, ldsTargets[1], noopListenerWatcher{})
   466  	xdsresource.WatchRouteConfig(client2, rdsTargets[1], noopRouteConfigWatcher{})
   467  	xdsresource.WatchCluster(client2, cdsTargets[1], noopClusterWatcher{})
   468  	xdsresource.WatchEndpoints(client2, edsTargets[1], noopEndpointsWatcher{})
   469  
   470  	// Check the resource dump. Both clients should have all resources in
   471  	// REQUESTED state.
   472  	wantConfigs1 := []*v3statuspb.ClientConfig_GenericXdsConfig{
   473  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   474  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   475  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   476  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   477  	}
   478  	wantConfigs2 := []*v3statuspb.ClientConfig_GenericXdsConfig{
   479  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   480  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   481  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   482  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   483  	}
   484  	wantResp = &v3statuspb.ClientStatusResponse{
   485  		Config: []*v3statuspb.ClientConfig{
   486  			{
   487  				Node:              wantNode,
   488  				GenericXdsConfigs: wantConfigs1,
   489  				ClientScope:       client1Name,
   490  			},
   491  			{
   492  				Node:              wantNode,
   493  				GenericXdsConfigs: wantConfigs2,
   494  				ClientScope:       client2Name,
   495  			},
   496  		},
   497  	}
   498  	if err := checkResourceDump(ctx, wantResp, pool); err != nil {
   499  		t.Fatal(err)
   500  	}
   501  
   502  	// Configure resources on the first management server.
   503  	if err := mgmtServer1.Update(ctx, e2e.UpdateOptions{
   504  		NodeID:    nodeID,
   505  		Listeners: listeners[:1],
   506  		Routes:    routes[:1],
   507  		Clusters:  clusters[:1],
   508  		Endpoints: endpoints[:1],
   509  	}); err != nil {
   510  		t.Fatal(err)
   511  	}
   512  
   513  	// Check the resource dump. One client should have resources in ACKED state,
   514  	// while the other should still have resources in REQUESTED state.
   515  	wantConfigs1 = []*v3statuspb.ClientConfig_GenericXdsConfig{
   516  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, clusterAnys[0], nil),
   517  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, endpointAnys[0], nil),
   518  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[0], nil),
   519  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, routeAnys[0], nil),
   520  	}
   521  	wantResp = &v3statuspb.ClientStatusResponse{
   522  		Config: []*v3statuspb.ClientConfig{
   523  			{
   524  				Node:              wantNode,
   525  				GenericXdsConfigs: wantConfigs1,
   526  				ClientScope:       client1Name,
   527  			},
   528  			{
   529  				Node:              wantNode,
   530  				GenericXdsConfigs: wantConfigs2,
   531  				ClientScope:       client2Name,
   532  			},
   533  		},
   534  	}
   535  	if err := checkResourceDump(ctx, wantResp, pool); err != nil {
   536  		t.Fatal(err)
   537  	}
   538  
   539  	// Configure resources on the second management server.
   540  	if err := mgmtServer2.Update(ctx, e2e.UpdateOptions{
   541  		NodeID:    nodeID,
   542  		Listeners: listeners[1:],
   543  		Routes:    routes[1:],
   544  		Clusters:  clusters[1:],
   545  		Endpoints: endpoints[1:],
   546  	}); err != nil {
   547  		t.Fatal(err)
   548  	}
   549  
   550  	// Check the resource dump. Both clients should have appropriate resources
   551  	// in REQUESTED state.
   552  	wantConfigs2 = []*v3statuspb.ClientConfig_GenericXdsConfig{
   553  		makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, clusterAnys[1], nil),
   554  		makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, endpointAnys[1], nil),
   555  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil),
   556  		makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, routeAnys[1], nil),
   557  	}
   558  	wantResp = &v3statuspb.ClientStatusResponse{
   559  		Config: []*v3statuspb.ClientConfig{
   560  			{
   561  				Node:              wantNode,
   562  				GenericXdsConfigs: wantConfigs1,
   563  				ClientScope:       client1Name,
   564  			},
   565  			{
   566  				Node:              wantNode,
   567  				GenericXdsConfigs: wantConfigs2,
   568  				ClientScope:       client2Name,
   569  			},
   570  		},
   571  	}
   572  	if err := checkResourceDump(ctx, wantResp, pool); err != nil {
   573  		t.Fatal(err)
   574  	}
   575  }