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