google.golang.org/grpc@v1.62.1/xds/internal/xdsclient/tests/resource_update_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  	"fmt"
    24  	"strings"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/google/go-cmp/cmp/cmpopts"
    30  	"github.com/google/uuid"
    31  	"google.golang.org/grpc/internal/testutils"
    32  	"google.golang.org/grpc/internal/testutils/xds/e2e"
    33  	"google.golang.org/grpc/internal/testutils/xds/fakeserver"
    34  	"google.golang.org/grpc/xds/internal"
    35  	xdstestutils "google.golang.org/grpc/xds/internal/testutils"
    36  	"google.golang.org/grpc/xds/internal/xdsclient"
    37  	"google.golang.org/grpc/xds/internal/xdsclient/bootstrap"
    38  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
    39  	"google.golang.org/protobuf/proto"
    40  	"google.golang.org/protobuf/testing/protocmp"
    41  	"google.golang.org/protobuf/types/known/anypb"
    42  	"google.golang.org/protobuf/types/known/wrapperspb"
    43  
    44  	v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
    45  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    46  	v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
    47  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    48  	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    49  	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    50  	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    51  
    52  	_ "google.golang.org/grpc/xds/internal/httpfilter/router" // Register the router filter.
    53  )
    54  
    55  // startFakeManagementServer starts a fake xDS management server and returns a
    56  // cleanup function to close the fake server.
    57  func startFakeManagementServer(t *testing.T) (*fakeserver.Server, func()) {
    58  	t.Helper()
    59  	fs, sCleanup, err := fakeserver.StartServer(nil)
    60  	if err != nil {
    61  		t.Fatalf("Failed to start fake xDS server: %v", err)
    62  	}
    63  	return fs, sCleanup
    64  }
    65  
    66  func compareUpdateMetadata(ctx context.Context, dumpFunc func() map[string]xdsresource.UpdateWithMD, want map[string]xdsresource.UpdateWithMD) error {
    67  	var lastErr error
    68  	for ; ctx.Err() == nil; <-time.After(100 * time.Millisecond) {
    69  		cmpOpts := cmp.Options{
    70  			cmpopts.EquateEmpty(),
    71  			cmp.Comparer(func(a, b time.Time) bool { return true }),
    72  			cmpopts.EquateErrors(),
    73  			protocmp.Transform(),
    74  		}
    75  		gotUpdateMetadata := dumpFunc()
    76  		diff := cmp.Diff(want, gotUpdateMetadata, cmpOpts)
    77  		if diff == "" {
    78  			return nil
    79  		}
    80  		lastErr = fmt.Errorf("unexpected diff in metadata, diff (-want +got):\n%s\n want: %+v\n got: %+v", diff, want, gotUpdateMetadata)
    81  	}
    82  	return fmt.Errorf("timeout when waiting for expected update metadata: %v", lastErr)
    83  }
    84  
    85  // TestHandleListenerResponseFromManagementServer covers different scenarios
    86  // involving receipt of an LDS response from the management server. The test
    87  // verifies that the internal state of the xDS client (parsed resource and
    88  // metadata) matches expectations.
    89  func (s) TestHandleListenerResponseFromManagementServer(t *testing.T) {
    90  	const (
    91  		resourceName1 = "resource-name-1"
    92  		resourceName2 = "resource-name-2"
    93  	)
    94  	var (
    95  		emptyRouterFilter = e2e.RouterHTTPFilter
    96  		apiListener       = &v3listenerpb.ApiListener{
    97  			ApiListener: func() *anypb.Any {
    98  				return testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
    99  					RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
   100  						Rds: &v3httppb.Rds{
   101  							ConfigSource: &v3corepb.ConfigSource{
   102  								ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
   103  							},
   104  							RouteConfigName: "route-configuration-name",
   105  						},
   106  					},
   107  					HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
   108  				})
   109  			}(),
   110  		}
   111  		resource1 = &v3listenerpb.Listener{
   112  			Name:        resourceName1,
   113  			ApiListener: apiListener,
   114  		}
   115  		resource2 = &v3listenerpb.Listener{
   116  			Name:        resourceName2,
   117  			ApiListener: apiListener,
   118  		}
   119  	)
   120  
   121  	tests := []struct {
   122  		desc                     string
   123  		resourceName             string
   124  		managementServerResponse *v3discoverypb.DiscoveryResponse
   125  		wantUpdate               xdsresource.ListenerUpdate
   126  		wantErr                  string
   127  		wantUpdateMetadata       map[string]xdsresource.UpdateWithMD
   128  	}{
   129  		{
   130  			desc:         "badly-marshaled-response",
   131  			resourceName: resourceName1,
   132  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   133  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   134  				VersionInfo: "1",
   135  				Resources: []*anypb.Any{{
   136  					TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener",
   137  					Value:   []byte{1, 2, 3, 4},
   138  				}},
   139  			},
   140  			wantErr: "Listener not found in received response",
   141  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   142  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   143  			},
   144  		},
   145  		{
   146  			desc:         "empty-response",
   147  			resourceName: resourceName1,
   148  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   149  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   150  				VersionInfo: "1",
   151  			},
   152  			wantErr: "Listener not found in received response",
   153  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   154  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   155  			},
   156  		},
   157  		{
   158  			desc:         "unexpected-type-in-response",
   159  			resourceName: resourceName1,
   160  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   161  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   162  				VersionInfo: "1",
   163  				Resources:   []*anypb.Any{testutils.MarshalAny(t, &v3routepb.RouteConfiguration{})},
   164  			},
   165  			wantErr: "Listener not found in received response",
   166  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   167  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   168  			},
   169  		},
   170  		{
   171  			desc:         "one-bad-resource",
   172  			resourceName: resourceName1,
   173  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   174  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   175  				VersionInfo: "1",
   176  				Resources: []*anypb.Any{testutils.MarshalAny(t, &v3listenerpb.Listener{
   177  					Name: resourceName1,
   178  					ApiListener: &v3listenerpb.ApiListener{
   179  						ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{}),
   180  					}}),
   181  				},
   182  			},
   183  			wantErr: "no RouteSpecifier",
   184  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   185  				"resource-name-1": {MD: xdsresource.UpdateMetadata{
   186  					Status: xdsresource.ServiceStatusNACKed,
   187  					ErrState: &xdsresource.UpdateErrorMetadata{
   188  						Version: "1",
   189  						Err:     cmpopts.AnyError,
   190  					},
   191  				}},
   192  			},
   193  		},
   194  		{
   195  			desc:         "one-good-resource",
   196  			resourceName: resourceName1,
   197  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   198  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   199  				VersionInfo: "1",
   200  				Resources:   []*anypb.Any{testutils.MarshalAny(t, resource1)},
   201  			},
   202  			wantUpdate: xdsresource.ListenerUpdate{
   203  				RouteConfigName: "route-configuration-name",
   204  				HTTPFilters:     []xdsresource.HTTPFilter{{Name: "router"}},
   205  			},
   206  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   207  				"resource-name-1": {
   208  					MD:  xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"},
   209  					Raw: testutils.MarshalAny(t, resource1),
   210  				},
   211  			},
   212  		},
   213  		{
   214  			desc:         "two-resources-when-we-requested-one",
   215  			resourceName: resourceName1,
   216  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   217  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   218  				VersionInfo: "1",
   219  				Resources:   []*anypb.Any{testutils.MarshalAny(t, resource1), testutils.MarshalAny(t, resource2)},
   220  			},
   221  			wantUpdate: xdsresource.ListenerUpdate{
   222  				RouteConfigName: "route-configuration-name",
   223  				HTTPFilters:     []xdsresource.HTTPFilter{{Name: "router"}},
   224  			},
   225  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   226  				"resource-name-1": {
   227  					MD:  xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"},
   228  					Raw: testutils.MarshalAny(t, resource1),
   229  				},
   230  			},
   231  		},
   232  	}
   233  
   234  	for _, test := range tests {
   235  		t.Run(test.desc, func(t *testing.T) {
   236  			// Create a fake xDS management server listening on a local port,
   237  			// and set it up with the response to send.
   238  			mgmtServer, cleanup := startFakeManagementServer(t)
   239  			defer cleanup()
   240  			t.Logf("Started xDS management server on %s", mgmtServer.Address)
   241  
   242  			// Create an xDS client talking to the above management server.
   243  			nodeID := uuid.New().String()
   244  			client, close, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{
   245  				XDSServer: xdstestutils.ServerConfigForAddress(t, mgmtServer.Address),
   246  				NodeProto: &v3corepb.Node{Id: nodeID},
   247  			}, defaultTestWatchExpiryTimeout, time.Duration(0))
   248  			if err != nil {
   249  				t.Fatalf("failed to create xds client: %v", err)
   250  			}
   251  			defer close()
   252  			t.Logf("Created xDS client to %s", mgmtServer.Address)
   253  
   254  			// Register a watch, and push the results on to a channel.
   255  			lw := newListenerWatcher()
   256  			cancel := xdsresource.WatchListener(client, test.resourceName, lw)
   257  			defer cancel()
   258  			t.Logf("Registered a watch for Listener %q", test.resourceName)
   259  
   260  			// Wait for the discovery request to be sent out.
   261  			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   262  			defer cancel()
   263  			val, err := mgmtServer.XDSRequestChan.Receive(ctx)
   264  			if err != nil {
   265  				t.Fatalf("Timeout when waiting for discovery request at the management server: %v", ctx)
   266  			}
   267  			wantReq := &fakeserver.Request{Req: &v3discoverypb.DiscoveryRequest{
   268  				Node:          &v3corepb.Node{Id: nodeID},
   269  				ResourceNames: []string{test.resourceName},
   270  				TypeUrl:       "type.googleapis.com/envoy.config.listener.v3.Listener",
   271  			}}
   272  			gotReq := val.(*fakeserver.Request)
   273  			if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   274  				t.Fatalf("Discovery request received at management server is %+v, want %+v", gotReq, wantReq)
   275  			}
   276  			t.Logf("Discovery request received at management server")
   277  
   278  			// Configure the fake management server with a response.
   279  			mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse}
   280  
   281  			// Wait for an update from the xDS client and compare with expected
   282  			// update.
   283  			val, err = lw.updateCh.Receive(ctx)
   284  			if err != nil {
   285  				t.Fatalf("Timeout when waiting for watch callback to invoked after response from management server: %v", err)
   286  			}
   287  			gotUpdate := val.(listenerUpdateErrTuple).update
   288  			gotErr := val.(listenerUpdateErrTuple).err
   289  			if (gotErr != nil) != (test.wantErr != "") {
   290  				t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr)
   291  			}
   292  			if gotErr != nil && !strings.Contains(gotErr.Error(), test.wantErr) {
   293  				t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr)
   294  			}
   295  			cmpOpts := []cmp.Option{
   296  				cmpopts.EquateEmpty(),
   297  				cmpopts.IgnoreFields(xdsresource.HTTPFilter{}, "Filter", "Config"),
   298  				cmpopts.IgnoreFields(xdsresource.ListenerUpdate{}, "Raw"),
   299  			}
   300  			if diff := cmp.Diff(test.wantUpdate, gotUpdate, cmpOpts...); diff != "" {
   301  				t.Fatalf("Unexpected diff in metadata, diff (-want +got):\n%s", diff)
   302  			}
   303  			if err := compareUpdateMetadata(ctx, func() map[string]xdsresource.UpdateWithMD {
   304  				dump := client.DumpResources()
   305  				return dump["type.googleapis.com/envoy.config.listener.v3.Listener"]
   306  			}, test.wantUpdateMetadata); err != nil {
   307  				t.Fatal(err)
   308  			}
   309  		})
   310  	}
   311  }
   312  
   313  // TestHandleRouteConfigResponseFromManagementServer covers different scenarios
   314  // involving receipt of an RDS response from the management server. The test
   315  // verifies that the internal state of the xDS client (parsed resource and
   316  // metadata) matches expectations.
   317  func (s) TestHandleRouteConfigResponseFromManagementServer(t *testing.T) {
   318  	const (
   319  		resourceName1 = "resource-name-1"
   320  		resourceName2 = "resource-name-2"
   321  	)
   322  	var (
   323  		virtualHosts = []*v3routepb.VirtualHost{
   324  			{
   325  				Domains: []string{"lds-target-name"},
   326  				Routes: []*v3routepb.Route{
   327  					{
   328  						Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
   329  						Action: &v3routepb.Route_Route{
   330  							Route: &v3routepb.RouteAction{
   331  								ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: "cluster-name"},
   332  							},
   333  						},
   334  					},
   335  				},
   336  			},
   337  		}
   338  		resource1 = &v3routepb.RouteConfiguration{
   339  			Name:         resourceName1,
   340  			VirtualHosts: virtualHosts,
   341  		}
   342  		resource2 = &v3routepb.RouteConfiguration{
   343  			Name:         resourceName2,
   344  			VirtualHosts: virtualHosts,
   345  		}
   346  	)
   347  
   348  	tests := []struct {
   349  		desc                     string
   350  		resourceName             string
   351  		managementServerResponse *v3discoverypb.DiscoveryResponse
   352  		wantUpdate               xdsresource.RouteConfigUpdate
   353  		wantErr                  string
   354  		wantUpdateMetadata       map[string]xdsresource.UpdateWithMD
   355  	}{
   356  		// The first three tests involve scenarios where the response fails
   357  		// protobuf deserialization (because it contains an invalid data or type
   358  		// in the anypb.Any) or the requested resource is not present in the
   359  		// response.  In either case, no resource update makes its way to the
   360  		// top-level xDS client. An RDS response without a requested resource
   361  		// does not mean that the resource does not exist in the server. It
   362  		// could be part of a future update.  Therefore, the only failure mode
   363  		// for this resource is for the watch to timeout.
   364  		{
   365  			desc:         "badly-marshaled-response",
   366  			resourceName: resourceName1,
   367  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   368  				TypeUrl:     "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
   369  				VersionInfo: "1",
   370  				Resources: []*anypb.Any{{
   371  					TypeUrl: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
   372  					Value:   []byte{1, 2, 3, 4},
   373  				}},
   374  			},
   375  			wantErr: "RouteConfiguration not found in received response",
   376  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   377  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   378  			},
   379  		},
   380  		{
   381  			desc:         "empty-response",
   382  			resourceName: resourceName1,
   383  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   384  				TypeUrl:     "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
   385  				VersionInfo: "1",
   386  			},
   387  			wantErr: "RouteConfiguration not found in received response",
   388  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   389  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   390  			},
   391  		},
   392  		{
   393  			desc:         "unexpected-type-in-response",
   394  			resourceName: resourceName1,
   395  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   396  				TypeUrl:     "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
   397  				VersionInfo: "1",
   398  				Resources:   []*anypb.Any{testutils.MarshalAny(t, &v3clusterpb.Cluster{})},
   399  			},
   400  			wantErr: "RouteConfiguration not found in received response",
   401  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   402  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   403  			},
   404  		},
   405  		{
   406  			desc:         "one-bad-resource",
   407  			resourceName: resourceName1,
   408  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   409  				TypeUrl:     "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
   410  				VersionInfo: "1",
   411  				Resources: []*anypb.Any{testutils.MarshalAny(t, &v3routepb.RouteConfiguration{
   412  					Name: resourceName1,
   413  					VirtualHosts: []*v3routepb.VirtualHost{{
   414  						Domains: []string{"lds-resource-name"},
   415  						Routes: []*v3routepb.Route{{
   416  							Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
   417  							Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{
   418  								ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: "cluster-resource-name"},
   419  							}}}},
   420  						RetryPolicy: &v3routepb.RetryPolicy{
   421  							NumRetries: &wrapperspb.UInt32Value{Value: 0},
   422  						},
   423  					}},
   424  				})},
   425  			},
   426  			wantErr: "received route is invalid: retry_policy.num_retries = 0; must be >= 1",
   427  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   428  				"resource-name-1": {MD: xdsresource.UpdateMetadata{
   429  					Status: xdsresource.ServiceStatusNACKed,
   430  					ErrState: &xdsresource.UpdateErrorMetadata{
   431  						Version: "1",
   432  						Err:     cmpopts.AnyError,
   433  					},
   434  				}},
   435  			},
   436  		},
   437  		{
   438  			desc:         "one-good-resource",
   439  			resourceName: resourceName1,
   440  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   441  				TypeUrl:     "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
   442  				VersionInfo: "1",
   443  				Resources:   []*anypb.Any{testutils.MarshalAny(t, resource1)},
   444  			},
   445  			wantUpdate: xdsresource.RouteConfigUpdate{
   446  				VirtualHosts: []*xdsresource.VirtualHost{
   447  					{
   448  						Domains: []string{"lds-target-name"},
   449  						Routes: []*xdsresource.Route{{Prefix: newStringP(""),
   450  							WeightedClusters: map[string]xdsresource.WeightedCluster{"cluster-name": {Weight: 1}},
   451  							ActionType:       xdsresource.RouteActionRoute}},
   452  					},
   453  				},
   454  			},
   455  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   456  				"resource-name-1": {
   457  					MD:  xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"},
   458  					Raw: testutils.MarshalAny(t, resource1),
   459  				},
   460  			},
   461  		},
   462  		{
   463  			desc:         "two-resources-when-we-requested-one",
   464  			resourceName: resourceName1,
   465  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   466  				TypeUrl:     "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
   467  				VersionInfo: "1",
   468  				Resources:   []*anypb.Any{testutils.MarshalAny(t, resource1), testutils.MarshalAny(t, resource2)},
   469  			},
   470  			wantUpdate: xdsresource.RouteConfigUpdate{
   471  				VirtualHosts: []*xdsresource.VirtualHost{
   472  					{
   473  						Domains: []string{"lds-target-name"},
   474  						Routes: []*xdsresource.Route{{Prefix: newStringP(""),
   475  							WeightedClusters: map[string]xdsresource.WeightedCluster{"cluster-name": {Weight: 1}},
   476  							ActionType:       xdsresource.RouteActionRoute}},
   477  					},
   478  				},
   479  			},
   480  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   481  				"resource-name-1": {
   482  					MD:  xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"},
   483  					Raw: testutils.MarshalAny(t, resource1),
   484  				},
   485  			},
   486  		},
   487  	}
   488  	for _, test := range tests {
   489  		t.Run(test.desc, func(t *testing.T) {
   490  			// Create a fake xDS management server listening on a local port,
   491  			// and set it up with the response to send.
   492  			mgmtServer, cleanup := startFakeManagementServer(t)
   493  			defer cleanup()
   494  			t.Logf("Started xDS management server on %s", mgmtServer.Address)
   495  
   496  			// Create an xDS client talking to the above management server.
   497  			nodeID := uuid.New().String()
   498  			client, close, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{
   499  				XDSServer: xdstestutils.ServerConfigForAddress(t, mgmtServer.Address),
   500  				NodeProto: &v3corepb.Node{Id: nodeID},
   501  			}, defaultTestWatchExpiryTimeout, time.Duration(0))
   502  			if err != nil {
   503  				t.Fatalf("failed to create xds client: %v", err)
   504  			}
   505  			defer close()
   506  			t.Logf("Created xDS client to %s", mgmtServer.Address)
   507  
   508  			// Register a watch, and push the results on to a channel.
   509  			rw := newRouteConfigWatcher()
   510  			cancel := xdsresource.WatchRouteConfig(client, test.resourceName, rw)
   511  			defer cancel()
   512  			t.Logf("Registered a watch for Route Configuration %q", test.resourceName)
   513  
   514  			// Wait for the discovery request to be sent out.
   515  			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   516  			defer cancel()
   517  			val, err := mgmtServer.XDSRequestChan.Receive(ctx)
   518  			if err != nil {
   519  				t.Fatalf("Timeout when waiting for discovery request at the management server: %v", ctx)
   520  			}
   521  			wantReq := &fakeserver.Request{Req: &v3discoverypb.DiscoveryRequest{
   522  				Node:          &v3corepb.Node{Id: nodeID},
   523  				ResourceNames: []string{test.resourceName},
   524  				TypeUrl:       "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
   525  			}}
   526  			gotReq := val.(*fakeserver.Request)
   527  			if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   528  				t.Fatalf("Discovery request received at management server is %+v, want %+v", gotReq, wantReq)
   529  			}
   530  			t.Logf("Discovery request received at management server")
   531  
   532  			// Configure the fake management server with a response.
   533  			mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse}
   534  
   535  			// Wait for an update from the xDS client and compare with expected
   536  			// update.
   537  			val, err = rw.updateCh.Receive(ctx)
   538  			if err != nil {
   539  				t.Fatalf("Timeout when waiting for watch callback to invoked after response from management server: %v", err)
   540  			}
   541  			gotUpdate := val.(routeConfigUpdateErrTuple).update
   542  			gotErr := val.(routeConfigUpdateErrTuple).err
   543  			if (gotErr != nil) != (test.wantErr != "") {
   544  				t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr)
   545  			}
   546  			if gotErr != nil && !strings.Contains(gotErr.Error(), test.wantErr) {
   547  				t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr)
   548  			}
   549  			cmpOpts := []cmp.Option{
   550  				cmpopts.EquateEmpty(),
   551  				cmpopts.IgnoreFields(xdsresource.RouteConfigUpdate{}, "Raw"),
   552  			}
   553  			if diff := cmp.Diff(test.wantUpdate, gotUpdate, cmpOpts...); diff != "" {
   554  				t.Fatalf("Unexpected diff in metadata, diff (-want +got):\n%s", diff)
   555  			}
   556  			if err := compareUpdateMetadata(ctx, func() map[string]xdsresource.UpdateWithMD {
   557  				dump := client.DumpResources()
   558  				return dump["type.googleapis.com/envoy.config.route.v3.RouteConfiguration"]
   559  			}, test.wantUpdateMetadata); err != nil {
   560  				t.Fatal(err)
   561  			}
   562  		})
   563  	}
   564  }
   565  
   566  // TestHandleClusterResponseFromManagementServer covers different scenarios
   567  // involving receipt of a CDS response from the management server. The test
   568  // verifies that the internal state of the xDS client (parsed resource and
   569  // metadata) matches expectations.
   570  func (s) TestHandleClusterResponseFromManagementServer(t *testing.T) {
   571  	const (
   572  		resourceName1 = "resource-name-1"
   573  		resourceName2 = "resource-name-2"
   574  	)
   575  	resource1 := e2e.ClusterResourceWithOptions(e2e.ClusterOptions{
   576  		ClusterName: resourceName1,
   577  		ServiceName: "eds-service-name",
   578  		EnableLRS:   true,
   579  	})
   580  	resource2 := proto.Clone(resource1).(*v3clusterpb.Cluster)
   581  	resource2.Name = resourceName2
   582  
   583  	tests := []struct {
   584  		desc                     string
   585  		resourceName             string
   586  		managementServerResponse *v3discoverypb.DiscoveryResponse
   587  		wantUpdate               xdsresource.ClusterUpdate
   588  		wantErr                  string
   589  		wantUpdateMetadata       map[string]xdsresource.UpdateWithMD
   590  	}{
   591  		{
   592  			desc:         "badly-marshaled-response",
   593  			resourceName: resourceName1,
   594  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   595  				TypeUrl:     "type.googleapis.com/envoy.config.cluster.v3.Cluster",
   596  				VersionInfo: "1",
   597  				Resources: []*anypb.Any{{
   598  					TypeUrl: "type.googleapis.com/envoy.config.cluster.v3.Cluster",
   599  					Value:   []byte{1, 2, 3, 4},
   600  				}},
   601  			},
   602  			wantErr: "Cluster not found in received response",
   603  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   604  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   605  			},
   606  		},
   607  		{
   608  			desc:         "empty-response",
   609  			resourceName: resourceName1,
   610  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   611  				TypeUrl:     "type.googleapis.com/envoy.config.cluster.v3.Cluster",
   612  				VersionInfo: "1",
   613  			},
   614  			wantErr: "Cluster not found in received response",
   615  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   616  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   617  			},
   618  		},
   619  		{
   620  			desc:         "unexpected-type-in-response",
   621  			resourceName: resourceName1,
   622  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   623  				TypeUrl:     "type.googleapis.com/envoy.config.cluster.v3.Cluster",
   624  				VersionInfo: "1",
   625  				Resources:   []*anypb.Any{testutils.MarshalAny(t, &v3endpointpb.ClusterLoadAssignment{})},
   626  			},
   627  			wantErr: "Cluster not found in received response",
   628  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   629  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   630  			},
   631  		},
   632  		{
   633  			desc:         "one-bad-resource",
   634  			resourceName: resourceName1,
   635  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   636  				TypeUrl:     "type.googleapis.com/envoy.config.cluster.v3.Cluster",
   637  				VersionInfo: "1",
   638  				Resources: []*anypb.Any{testutils.MarshalAny(t, &v3clusterpb.Cluster{
   639  					Name:                 resourceName1,
   640  					ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
   641  					EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
   642  						EdsConfig: &v3corepb.ConfigSource{
   643  							ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
   644  								Ads: &v3corepb.AggregatedConfigSource{},
   645  							},
   646  						},
   647  						ServiceName: "eds-service-name",
   648  					},
   649  					LbPolicy: v3clusterpb.Cluster_MAGLEV,
   650  				})},
   651  			},
   652  			wantErr: "unexpected lbPolicy MAGLEV",
   653  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   654  				"resource-name-1": {MD: xdsresource.UpdateMetadata{
   655  					Status: xdsresource.ServiceStatusNACKed,
   656  					ErrState: &xdsresource.UpdateErrorMetadata{
   657  						Version: "1",
   658  						Err:     cmpopts.AnyError,
   659  					},
   660  				}},
   661  			},
   662  		},
   663  		{
   664  			desc:         "one-good-resource",
   665  			resourceName: resourceName1,
   666  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   667  				TypeUrl:     "type.googleapis.com/envoy.config.cluster.v3.Cluster",
   668  				VersionInfo: "1",
   669  				Resources:   []*anypb.Any{testutils.MarshalAny(t, resource1)},
   670  			},
   671  			wantUpdate: xdsresource.ClusterUpdate{
   672  				ClusterName:     "resource-name-1",
   673  				EDSServiceName:  "eds-service-name",
   674  				LRSServerConfig: xdsresource.ClusterLRSServerSelf,
   675  			},
   676  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   677  				"resource-name-1": {
   678  					MD:  xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"},
   679  					Raw: testutils.MarshalAny(t, resource1),
   680  				},
   681  			},
   682  		},
   683  		{
   684  			desc:         "two-resources-when-we-requested-one",
   685  			resourceName: resourceName1,
   686  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   687  				TypeUrl:     "type.googleapis.com/envoy.config.cluster.v3.Cluster",
   688  				VersionInfo: "1",
   689  				Resources:   []*anypb.Any{testutils.MarshalAny(t, resource1), testutils.MarshalAny(t, resource2)},
   690  			},
   691  			wantUpdate: xdsresource.ClusterUpdate{
   692  				ClusterName:     "resource-name-1",
   693  				EDSServiceName:  "eds-service-name",
   694  				LRSServerConfig: xdsresource.ClusterLRSServerSelf,
   695  			},
   696  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   697  				"resource-name-1": {
   698  					MD:  xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"},
   699  					Raw: testutils.MarshalAny(t, resource1),
   700  				},
   701  			},
   702  		},
   703  	}
   704  
   705  	for _, test := range tests {
   706  		t.Run(test.desc, func(t *testing.T) {
   707  			// Create a fake xDS management server listening on a local port,
   708  			// and set it up with the response to send.
   709  			mgmtServer, cleanup := startFakeManagementServer(t)
   710  			defer cleanup()
   711  			t.Logf("Started xDS management server on %s", mgmtServer.Address)
   712  
   713  			// Create an xDS client talking to the above management server.
   714  			nodeID := uuid.New().String()
   715  			client, close, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{
   716  				XDSServer: xdstestutils.ServerConfigForAddress(t, mgmtServer.Address),
   717  				NodeProto: &v3corepb.Node{Id: nodeID},
   718  			}, defaultTestWatchExpiryTimeout, time.Duration(0))
   719  			if err != nil {
   720  				t.Fatalf("failed to create xds client: %v", err)
   721  			}
   722  			defer close()
   723  			t.Logf("Created xDS client to %s", mgmtServer.Address)
   724  
   725  			// Register a watch, and push the results on to a channel.
   726  			cw := newClusterWatcher()
   727  			cancel := xdsresource.WatchCluster(client, test.resourceName, cw)
   728  			defer cancel()
   729  			t.Logf("Registered a watch for Cluster %q", test.resourceName)
   730  
   731  			// Wait for the discovery request to be sent out.
   732  			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   733  			defer cancel()
   734  			val, err := mgmtServer.XDSRequestChan.Receive(ctx)
   735  			if err != nil {
   736  				t.Fatalf("Timeout when waiting for discovery request at the management server: %v", ctx)
   737  			}
   738  			wantReq := &fakeserver.Request{Req: &v3discoverypb.DiscoveryRequest{
   739  				Node:          &v3corepb.Node{Id: nodeID},
   740  				ResourceNames: []string{test.resourceName},
   741  				TypeUrl:       "type.googleapis.com/envoy.config.cluster.v3.Cluster",
   742  			}}
   743  			gotReq := val.(*fakeserver.Request)
   744  			if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   745  				t.Fatalf("Discovery request received at management server is %+v, want %+v", gotReq, wantReq)
   746  			}
   747  			t.Logf("Discovery request received at management server")
   748  
   749  			// Configure the fake management server with a response.
   750  			mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse}
   751  
   752  			// Wait for an update from the xDS client and compare with expected
   753  			// update.
   754  			val, err = cw.updateCh.Receive(ctx)
   755  			if err != nil {
   756  				t.Fatalf("Timeout when waiting for watch callback to invoked after response from management server: %v", err)
   757  			}
   758  			gotUpdate := val.(clusterUpdateErrTuple).update
   759  			gotErr := val.(clusterUpdateErrTuple).err
   760  			if (gotErr != nil) != (test.wantErr != "") {
   761  				t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr)
   762  			}
   763  			if gotErr != nil && !strings.Contains(gotErr.Error(), test.wantErr) {
   764  				t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr)
   765  			}
   766  			cmpOpts := []cmp.Option{
   767  				cmpopts.EquateEmpty(),
   768  				cmpopts.IgnoreFields(xdsresource.ClusterUpdate{}, "Raw", "LBPolicy"),
   769  			}
   770  			if diff := cmp.Diff(test.wantUpdate, gotUpdate, cmpOpts...); diff != "" {
   771  				t.Fatalf("Unexpected diff in metadata, diff (-want +got):\n%s", diff)
   772  			}
   773  			if err := compareUpdateMetadata(ctx, func() map[string]xdsresource.UpdateWithMD {
   774  				dump := client.DumpResources()
   775  				return dump["type.googleapis.com/envoy.config.cluster.v3.Cluster"]
   776  			}, test.wantUpdateMetadata); err != nil {
   777  				t.Fatal(err)
   778  			}
   779  		})
   780  	}
   781  }
   782  
   783  // TestHandleEndpointsResponseFromManagementServer covers different scenarios
   784  // involving receipt of a CDS response from the management server. The test
   785  // verifies that the internal state of the xDS client (parsed resource and
   786  // metadata) matches expectations.
   787  func (s) TestHandleEndpointsResponseFromManagementServer(t *testing.T) {
   788  	const (
   789  		resourceName1 = "resource-name-1"
   790  		resourceName2 = "resource-name-2"
   791  	)
   792  	resource1 := &v3endpointpb.ClusterLoadAssignment{
   793  		ClusterName: resourceName1,
   794  		Endpoints: []*v3endpointpb.LocalityLbEndpoints{
   795  			{
   796  				Locality: &v3corepb.Locality{SubZone: "locality-1"},
   797  				LbEndpoints: []*v3endpointpb.LbEndpoint{
   798  					{
   799  						HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{
   800  							Endpoint: &v3endpointpb.Endpoint{
   801  								Address: &v3corepb.Address{
   802  									Address: &v3corepb.Address_SocketAddress{
   803  										SocketAddress: &v3corepb.SocketAddress{
   804  											Protocol: v3corepb.SocketAddress_TCP,
   805  											Address:  "addr1",
   806  											PortSpecifier: &v3corepb.SocketAddress_PortValue{
   807  												PortValue: uint32(314),
   808  											},
   809  										},
   810  									},
   811  								},
   812  							},
   813  						},
   814  					},
   815  				},
   816  				LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 1},
   817  				Priority:            1,
   818  			},
   819  			{
   820  				Locality: &v3corepb.Locality{SubZone: "locality-2"},
   821  				LbEndpoints: []*v3endpointpb.LbEndpoint{
   822  					{
   823  						HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{
   824  							Endpoint: &v3endpointpb.Endpoint{
   825  								Address: &v3corepb.Address{
   826  									Address: &v3corepb.Address_SocketAddress{
   827  										SocketAddress: &v3corepb.SocketAddress{
   828  											Protocol: v3corepb.SocketAddress_TCP,
   829  											Address:  "addr2",
   830  											PortSpecifier: &v3corepb.SocketAddress_PortValue{
   831  												PortValue: uint32(159),
   832  											},
   833  										},
   834  									},
   835  								},
   836  							},
   837  						},
   838  					},
   839  				},
   840  				LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 1},
   841  				Priority:            0,
   842  			},
   843  		},
   844  	}
   845  	resource2 := proto.Clone(resource1).(*v3endpointpb.ClusterLoadAssignment)
   846  	resource2.ClusterName = resourceName2
   847  
   848  	tests := []struct {
   849  		desc                     string
   850  		resourceName             string
   851  		managementServerResponse *v3discoverypb.DiscoveryResponse
   852  		wantUpdate               xdsresource.EndpointsUpdate
   853  		wantErr                  string
   854  		wantUpdateMetadata       map[string]xdsresource.UpdateWithMD
   855  	}{
   856  		// The first three tests involve scenarios where the response fails
   857  		// protobuf deserialization (because it contains an invalid data or type
   858  		// in the anypb.Any) or the requested resource is not present in the
   859  		// response.  In either case, no resource update makes its way to the
   860  		// top-level xDS client. An EDS response without a requested resource
   861  		// does not mean that the resource does not exist in the server. It
   862  		// could be part of a future update.  Therefore, the only failure mode
   863  		// for this resource is for the watch to timeout.
   864  		{
   865  			desc:         "badly-marshaled-response",
   866  			resourceName: resourceName1,
   867  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   868  				TypeUrl:     "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
   869  				VersionInfo: "1",
   870  				Resources: []*anypb.Any{{
   871  					TypeUrl: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
   872  					Value:   []byte{1, 2, 3, 4},
   873  				}},
   874  			},
   875  			wantErr: "Endpoints not found in received response",
   876  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   877  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   878  			},
   879  		},
   880  		{
   881  			desc:         "empty-response",
   882  			resourceName: resourceName1,
   883  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   884  				TypeUrl:     "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
   885  				VersionInfo: "1",
   886  			},
   887  			wantErr: "Endpoints not found in received response",
   888  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   889  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   890  			},
   891  		},
   892  		{
   893  			desc:         "unexpected-type-in-response",
   894  			resourceName: resourceName1,
   895  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   896  				TypeUrl:     "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
   897  				VersionInfo: "1",
   898  				Resources:   []*anypb.Any{testutils.MarshalAny(t, &v3listenerpb.Listener{})},
   899  			},
   900  			wantErr: "Endpoints not found in received response",
   901  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   902  				"resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}},
   903  			},
   904  		},
   905  		{
   906  			desc:         "one-bad-resource",
   907  			resourceName: resourceName1,
   908  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   909  				TypeUrl:     "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
   910  				VersionInfo: "1",
   911  				Resources: []*anypb.Any{testutils.MarshalAny(t, &v3endpointpb.ClusterLoadAssignment{
   912  					ClusterName: resourceName1,
   913  					Endpoints: []*v3endpointpb.LocalityLbEndpoints{
   914  						{
   915  							Locality: &v3corepb.Locality{SubZone: "locality-1"},
   916  							LbEndpoints: []*v3endpointpb.LbEndpoint{
   917  								{
   918  									HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{
   919  										Endpoint: &v3endpointpb.Endpoint{
   920  											Address: &v3corepb.Address{
   921  												Address: &v3corepb.Address_SocketAddress{
   922  													SocketAddress: &v3corepb.SocketAddress{
   923  														Protocol: v3corepb.SocketAddress_TCP,
   924  														Address:  "addr1",
   925  														PortSpecifier: &v3corepb.SocketAddress_PortValue{
   926  															PortValue: uint32(314),
   927  														},
   928  													},
   929  												},
   930  											},
   931  										},
   932  									},
   933  									LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 0},
   934  								},
   935  							},
   936  							LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 1},
   937  							Priority:            1,
   938  						},
   939  					},
   940  				}),
   941  				},
   942  			},
   943  			wantErr: "EDS response contains an endpoint with zero weight",
   944  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   945  				"resource-name-1": {MD: xdsresource.UpdateMetadata{
   946  					Status: xdsresource.ServiceStatusNACKed,
   947  					ErrState: &xdsresource.UpdateErrorMetadata{
   948  						Version: "1",
   949  						Err:     cmpopts.AnyError,
   950  					},
   951  				}},
   952  			},
   953  		},
   954  		{
   955  			desc:         "one-good-resource",
   956  			resourceName: resourceName1,
   957  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   958  				TypeUrl:     "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
   959  				VersionInfo: "1",
   960  				Resources:   []*anypb.Any{testutils.MarshalAny(t, resource1)},
   961  			},
   962  			wantUpdate: xdsresource.EndpointsUpdate{
   963  				Localities: []xdsresource.Locality{
   964  					{
   965  						Endpoints: []xdsresource.Endpoint{{Address: "addr1:314", Weight: 1}},
   966  						ID:        internal.LocalityID{SubZone: "locality-1"},
   967  						Priority:  1,
   968  						Weight:    1,
   969  					},
   970  					{
   971  						Endpoints: []xdsresource.Endpoint{{Address: "addr2:159", Weight: 1}},
   972  						ID:        internal.LocalityID{SubZone: "locality-2"},
   973  						Priority:  0,
   974  						Weight:    1,
   975  					},
   976  				},
   977  			},
   978  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
   979  				"resource-name-1": {
   980  					MD:  xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"},
   981  					Raw: testutils.MarshalAny(t, resource1),
   982  				},
   983  			},
   984  		},
   985  		{
   986  			desc:         "two-resources-when-we-requested-one",
   987  			resourceName: resourceName1,
   988  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   989  				TypeUrl:     "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
   990  				VersionInfo: "1",
   991  				Resources:   []*anypb.Any{testutils.MarshalAny(t, resource1), testutils.MarshalAny(t, resource2)},
   992  			},
   993  			wantUpdate: xdsresource.EndpointsUpdate{
   994  				Localities: []xdsresource.Locality{
   995  					{
   996  						Endpoints: []xdsresource.Endpoint{{Address: "addr1:314", Weight: 1}},
   997  						ID:        internal.LocalityID{SubZone: "locality-1"},
   998  						Priority:  1,
   999  						Weight:    1,
  1000  					},
  1001  					{
  1002  						Endpoints: []xdsresource.Endpoint{{Address: "addr2:159", Weight: 1}},
  1003  						ID:        internal.LocalityID{SubZone: "locality-2"},
  1004  						Priority:  0,
  1005  						Weight:    1,
  1006  					},
  1007  				},
  1008  			},
  1009  			wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{
  1010  				"resource-name-1": {
  1011  					MD:  xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"},
  1012  					Raw: testutils.MarshalAny(t, resource1),
  1013  				},
  1014  			},
  1015  		},
  1016  	}
  1017  
  1018  	for _, test := range tests {
  1019  		t.Run(test.desc, func(t *testing.T) {
  1020  			// Create a fake xDS management server listening on a local port,
  1021  			// and set it up with the response to send.
  1022  			mgmtServer, cleanup := startFakeManagementServer(t)
  1023  			defer cleanup()
  1024  			t.Logf("Started xDS management server on %s", mgmtServer.Address)
  1025  
  1026  			// Create an xDS client talking to the above management server.
  1027  			nodeID := uuid.New().String()
  1028  			client, close, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{
  1029  				XDSServer: xdstestutils.ServerConfigForAddress(t, mgmtServer.Address),
  1030  				NodeProto: &v3corepb.Node{Id: nodeID},
  1031  			}, defaultTestWatchExpiryTimeout, time.Duration(0))
  1032  			if err != nil {
  1033  				t.Fatalf("failed to create xds client: %v", err)
  1034  			}
  1035  			defer close()
  1036  			t.Logf("Created xDS client to %s", mgmtServer.Address)
  1037  
  1038  			// Register a watch, and push the results on to a channel.
  1039  			ew := newEndpointsWatcher()
  1040  			cancel := xdsresource.WatchEndpoints(client, test.resourceName, ew)
  1041  			defer cancel()
  1042  			t.Logf("Registered a watch for Endpoint %q", test.resourceName)
  1043  
  1044  			// Wait for the discovery request to be sent out.
  1045  			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
  1046  			defer cancel()
  1047  			val, err := mgmtServer.XDSRequestChan.Receive(ctx)
  1048  			if err != nil {
  1049  				t.Fatalf("Timeout when waiting for discovery request at the management server: %v", ctx)
  1050  			}
  1051  			wantReq := &fakeserver.Request{Req: &v3discoverypb.DiscoveryRequest{
  1052  				Node:          &v3corepb.Node{Id: nodeID},
  1053  				ResourceNames: []string{test.resourceName},
  1054  				TypeUrl:       "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
  1055  			}}
  1056  			gotReq := val.(*fakeserver.Request)
  1057  			if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
  1058  				t.Fatalf("Discovery request received at management server is %+v, want %+v", gotReq, wantReq)
  1059  			}
  1060  			t.Logf("Discovery request received at management server")
  1061  
  1062  			// Configure the fake management server with a response.
  1063  			mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse}
  1064  
  1065  			// Wait for an update from the xDS client and compare with expected
  1066  			// update.
  1067  			val, err = ew.updateCh.Receive(ctx)
  1068  			if err != nil {
  1069  				t.Fatalf("Timeout when waiting for watch callback to invoked after response from management server: %v", err)
  1070  			}
  1071  			gotUpdate := val.(endpointsUpdateErrTuple).update
  1072  			gotErr := val.(endpointsUpdateErrTuple).err
  1073  			if (gotErr != nil) != (test.wantErr != "") {
  1074  				t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr)
  1075  			}
  1076  			if gotErr != nil && !strings.Contains(gotErr.Error(), test.wantErr) {
  1077  				t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr)
  1078  			}
  1079  			cmpOpts := []cmp.Option{
  1080  				cmpopts.EquateEmpty(),
  1081  				cmpopts.IgnoreFields(xdsresource.EndpointsUpdate{}, "Raw"),
  1082  			}
  1083  			if diff := cmp.Diff(test.wantUpdate, gotUpdate, cmpOpts...); diff != "" {
  1084  				t.Fatalf("Unexpected diff in metadata, diff (-want +got):\n%s", diff)
  1085  			}
  1086  			if err := compareUpdateMetadata(ctx, func() map[string]xdsresource.UpdateWithMD {
  1087  				dump := client.DumpResources()
  1088  				return dump["type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"]
  1089  			}, test.wantUpdateMetadata); err != nil {
  1090  				t.Fatal(err)
  1091  			}
  1092  		})
  1093  	}
  1094  }