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

     1  /*
     2   *
     3   * Copyright 2024 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  package xdsclient
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/envoyproxy/go-control-plane/pkg/wellknown"
    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/internal/xds/bootstrap"
    35  	xdsinternal "google.golang.org/grpc/xds/internal"
    36  	"google.golang.org/grpc/xds/internal/httpfilter"
    37  	"google.golang.org/grpc/xds/internal/httpfilter/router"
    38  	"google.golang.org/grpc/xds/internal/xdsclient/transport"
    39  	"google.golang.org/grpc/xds/internal/xdsclient/transport/ads"
    40  	"google.golang.org/grpc/xds/internal/xdsclient/transport/grpctransport"
    41  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
    42  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version"
    43  	"google.golang.org/protobuf/testing/protocmp"
    44  	"google.golang.org/protobuf/types/known/anypb"
    45  	"google.golang.org/protobuf/types/known/durationpb"
    46  
    47  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/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  	v3routerpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
    51  	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    52  	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    53  )
    54  
    55  // Lookup the listener resource type from the resource type map. This is used to
    56  // parse listener resources used in this test.
    57  var listenerType = xdsinternal.ResourceTypeMapForTesting[version.V3ListenerURL].(xdsresource.Type)
    58  
    59  // xdsChannelForTest creates an xdsChannel to the specified serverURI for
    60  // testing purposes.
    61  func xdsChannelForTest(t *testing.T, serverURI, nodeID string, watchExpiryTimeout time.Duration) *xdsChannel {
    62  	t.Helper()
    63  
    64  	// Create server configuration for the above management server.
    65  	serverCfg, err := bootstrap.ServerConfigForTesting(bootstrap.ServerConfigTestingOptions{URI: serverURI})
    66  	if err != nil {
    67  		t.Fatalf("Failed to create server config for testing: %v", err)
    68  	}
    69  
    70  	// Create a grpc transport to the above management server.
    71  	tr, err := (&grpctransport.Builder{}).Build(transport.BuildOptions{ServerConfig: serverCfg})
    72  	if err != nil {
    73  		t.Fatalf("Failed to create a transport for server config %s: %v", serverCfg, err)
    74  	}
    75  
    76  	// Create bootstrap configuration with the top-level xds servers
    77  	// field containing the server configuration for the above
    78  	// management server.
    79  	contents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
    80  		Servers: []byte(fmt.Sprintf(`[{
    81  			"server_uri": %q,
    82  			"channel_creds": [{"type": "insecure"}]
    83  		}]`, serverURI)),
    84  		Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
    85  	})
    86  	if err != nil {
    87  		t.Fatalf("Failed to create bootstrap contents: %v", err)
    88  	}
    89  	bootstrapCfg, err := bootstrap.NewConfigFromContents(contents)
    90  	if err != nil {
    91  		t.Fatalf("Failed to create bootstrap configuration: %v", err)
    92  	}
    93  
    94  	// Create an xdsChannel that uses everything set up above.
    95  	xc, err := newXDSChannel(xdsChannelOpts{
    96  		transport:       tr,
    97  		serverConfig:    serverCfg,
    98  		bootstrapConfig: bootstrapCfg,
    99  		resourceTypeGetter: func(typeURL string) xdsresource.Type {
   100  			if typeURL != "type.googleapis.com/envoy.config.listener.v3.Listener" {
   101  				return nil
   102  			}
   103  			return listenerType
   104  		},
   105  		eventHandler:       newTestEventHandler(),
   106  		watchExpiryTimeout: watchExpiryTimeout,
   107  	})
   108  	if err != nil {
   109  		t.Fatalf("Failed to create xdsChannel: %v", err)
   110  	}
   111  	t.Cleanup(func() { xc.close() })
   112  	return xc
   113  }
   114  
   115  // verifyUpdateAndMetadata verifies that the event handler received the expected
   116  // updates and metadata.  It checks that the received resource type matches the
   117  // expected type, and that the received updates and metadata match the expected
   118  // values. The function ignores the timestamp fields in the metadata, as those
   119  // are expected to be different.
   120  func verifyUpdateAndMetadata(ctx context.Context, t *testing.T, eh *testEventHandler, wantUpdates map[string]ads.DataAndErrTuple, wantMD xdsresource.UpdateMetadata) {
   121  	t.Helper()
   122  
   123  	gotTyp, gotUpdates, gotMD, err := eh.waitForUpdate(ctx)
   124  	if err != nil {
   125  		t.Fatalf("Timeout when waiting for update callback to be invoked on the event handler")
   126  	}
   127  
   128  	if gotTyp != listenerType {
   129  		t.Fatalf("Got resource type %v, want %v", gotTyp, listenerType)
   130  	}
   131  	opts := cmp.Options{
   132  		protocmp.Transform(),
   133  		cmpopts.EquateEmpty(),
   134  		cmpopts.EquateErrors(),
   135  		cmpopts.IgnoreFields(xdsresource.UpdateMetadata{}, "Timestamp"),
   136  		cmpopts.IgnoreFields(xdsresource.UpdateErrorMetadata{}, "Timestamp"),
   137  	}
   138  	if diff := cmp.Diff(wantUpdates, gotUpdates, opts); diff != "" {
   139  		t.Fatalf("Got unexpected diff in update (-want +got):\n%s\n want: %+v\n got: %+v", diff, wantUpdates, gotUpdates)
   140  	}
   141  	if diff := cmp.Diff(wantMD, gotMD, opts); diff != "" {
   142  		t.Fatalf("Got unexpected diff in update (-want +got):\n%s\n want: %v\n got: %v", diff, wantMD, gotMD)
   143  	}
   144  }
   145  
   146  // Tests different failure cases when creating a new xdsChannel. It checks that
   147  // the xdsChannel creation fails when any of the required options (transport,
   148  // serverConfig, bootstrapConfig, or resourceTypeGetter) are missing or nil.
   149  func (s) TestChannel_New_FailureCases(t *testing.T) {
   150  	type fakeTransport struct {
   151  		transport.Transport
   152  	}
   153  
   154  	tests := []struct {
   155  		name       string
   156  		opts       xdsChannelOpts
   157  		wantErrStr string
   158  	}{
   159  		{
   160  			name:       "emptyTransport",
   161  			opts:       xdsChannelOpts{},
   162  			wantErrStr: "transport is nil",
   163  		},
   164  		{
   165  			name:       "emptyServerConfig",
   166  			opts:       xdsChannelOpts{transport: &fakeTransport{}},
   167  			wantErrStr: "serverConfig is nil",
   168  		},
   169  		{
   170  			name: "emptyBootstrapConfig",
   171  			opts: xdsChannelOpts{
   172  				transport:    &fakeTransport{},
   173  				serverConfig: &bootstrap.ServerConfig{},
   174  			},
   175  			wantErrStr: "bootstrapConfig is nil",
   176  		},
   177  		{
   178  			name: "emptyResourceTypeGetter",
   179  			opts: xdsChannelOpts{
   180  				transport:       &fakeTransport{},
   181  				serverConfig:    &bootstrap.ServerConfig{},
   182  				bootstrapConfig: &bootstrap.Config{},
   183  			},
   184  			wantErrStr: "resourceTypeGetter is nil",
   185  		},
   186  		{
   187  			name: "emptyEventHandler",
   188  			opts: xdsChannelOpts{
   189  				transport:          &fakeTransport{},
   190  				serverConfig:       &bootstrap.ServerConfig{},
   191  				bootstrapConfig:    &bootstrap.Config{},
   192  				resourceTypeGetter: func(string) xdsresource.Type { return nil },
   193  			},
   194  			wantErrStr: "eventHandler is nil",
   195  		},
   196  	}
   197  
   198  	for _, test := range tests {
   199  		t.Run(test.name, func(t *testing.T) {
   200  			if _, err := newXDSChannel(test.opts); err == nil || !strings.Contains(err.Error(), test.wantErrStr) {
   201  				t.Fatalf("newXDSChannel() = %v, want %q", err, test.wantErrStr)
   202  			}
   203  		})
   204  	}
   205  }
   206  
   207  // Tests different scenarios of the xdsChannel receiving a response from the
   208  // management server. In all scenarios, the xdsChannel is expected to pass the
   209  // received responses as-is to the resource parsing functionality specified by
   210  // the resourceTypeGetter.
   211  func (s) TestChannel_ADS_HandleResponseFromManagementServer(t *testing.T) {
   212  	const (
   213  		listenerName1 = "listener-name-1"
   214  		listenerName2 = "listener-name-2"
   215  		routeName     = "route-name"
   216  		clusterName   = "cluster-name"
   217  	)
   218  	var (
   219  		badlyMarshaledResource = &anypb.Any{
   220  			TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener",
   221  			Value:   []byte{1, 2, 3, 4},
   222  		}
   223  		apiListener = &v3listenerpb.ApiListener{
   224  			ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   225  				RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
   226  					RouteConfig: &v3routepb.RouteConfiguration{
   227  						Name: routeName,
   228  						VirtualHosts: []*v3routepb.VirtualHost{{
   229  							Domains: []string{"*"},
   230  							Routes: []*v3routepb.Route{{
   231  								Match: &v3routepb.RouteMatch{
   232  									PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
   233  								},
   234  								Action: &v3routepb.Route_Route{
   235  									Route: &v3routepb.RouteAction{
   236  										ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
   237  									}}}}}}},
   238  				},
   239  				HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter},
   240  				CommonHttpProtocolOptions: &v3corepb.HttpProtocolOptions{
   241  					MaxStreamDuration: durationpb.New(time.Second),
   242  				},
   243  			}),
   244  		}
   245  		listener1 = testutils.MarshalAny(t, &v3listenerpb.Listener{
   246  			Name:        listenerName1,
   247  			ApiListener: apiListener,
   248  		})
   249  		listener2 = testutils.MarshalAny(t, &v3listenerpb.Listener{
   250  			Name:        listenerName2,
   251  			ApiListener: apiListener,
   252  		})
   253  	)
   254  
   255  	tests := []struct {
   256  		desc                     string
   257  		resourceNamesToRequest   []string
   258  		managementServerResponse *v3discoverypb.DiscoveryResponse
   259  		wantUpdates              map[string]ads.DataAndErrTuple
   260  		wantMD                   xdsresource.UpdateMetadata
   261  		wantErr                  error
   262  	}{
   263  		{
   264  			desc:                   "one bad resource - deserialization failure",
   265  			resourceNamesToRequest: []string{listenerName1},
   266  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   267  				VersionInfo: "0",
   268  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   269  				Resources:   []*anypb.Any{badlyMarshaledResource},
   270  			},
   271  			wantUpdates: nil, // No updates expected as the response runs into unmarshaling errors.
   272  			wantMD: xdsresource.UpdateMetadata{
   273  				Status:  xdsresource.ServiceStatusNACKed,
   274  				Version: "0",
   275  				ErrState: &xdsresource.UpdateErrorMetadata{
   276  					Version: "0",
   277  					Err:     cmpopts.AnyError,
   278  				},
   279  			},
   280  			wantErr: cmpopts.AnyError,
   281  		},
   282  		{
   283  			desc:                   "one bad resource - validation failure",
   284  			resourceNamesToRequest: []string{listenerName1},
   285  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   286  				VersionInfo: "0",
   287  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   288  				Resources: []*anypb.Any{testutils.MarshalAny(t, &v3listenerpb.Listener{
   289  					Name: listenerName1,
   290  					ApiListener: &v3listenerpb.ApiListener{
   291  						ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   292  							RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{},
   293  						}),
   294  					},
   295  				})},
   296  			},
   297  			wantUpdates: map[string]ads.DataAndErrTuple{
   298  				listenerName1: {
   299  					Err: cmpopts.AnyError,
   300  				},
   301  			},
   302  			wantMD: xdsresource.UpdateMetadata{
   303  				Status:  xdsresource.ServiceStatusNACKed,
   304  				Version: "0",
   305  				ErrState: &xdsresource.UpdateErrorMetadata{
   306  					Version: "0",
   307  					Err:     cmpopts.AnyError,
   308  				},
   309  			},
   310  		},
   311  		{
   312  			desc:                   "two bad resources",
   313  			resourceNamesToRequest: []string{listenerName1, listenerName2},
   314  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   315  				VersionInfo: "0",
   316  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   317  				Resources: []*anypb.Any{
   318  					badlyMarshaledResource,
   319  					testutils.MarshalAny(t, &v3listenerpb.Listener{
   320  						Name: listenerName2,
   321  						ApiListener: &v3listenerpb.ApiListener{
   322  							ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   323  								RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{},
   324  							}),
   325  						},
   326  					}),
   327  				},
   328  			},
   329  			wantUpdates: map[string]ads.DataAndErrTuple{
   330  				listenerName2: {
   331  					Err: cmpopts.AnyError,
   332  				},
   333  			},
   334  			wantMD: xdsresource.UpdateMetadata{
   335  				Status:  xdsresource.ServiceStatusNACKed,
   336  				Version: "0",
   337  				ErrState: &xdsresource.UpdateErrorMetadata{
   338  					Version: "0",
   339  					Err:     cmpopts.AnyError,
   340  				},
   341  			},
   342  		},
   343  		{
   344  			desc:                   "one good resource",
   345  			resourceNamesToRequest: []string{listenerName1},
   346  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   347  				VersionInfo: "0",
   348  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   349  				Resources:   []*anypb.Any{listener1},
   350  			},
   351  			wantUpdates: map[string]ads.DataAndErrTuple{
   352  				listenerName1: {
   353  					Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{
   354  						InlineRouteConfig: &xdsresource.RouteConfigUpdate{
   355  							VirtualHosts: []*xdsresource.VirtualHost{{
   356  								Domains: []string{"*"},
   357  								Routes: []*xdsresource.Route{{
   358  									Prefix:           newStringP("/"),
   359  									WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}},
   360  									ActionType:       xdsresource.RouteActionRoute},
   361  								},
   362  							}}},
   363  						MaxStreamDuration: time.Second,
   364  						Raw:               listener1,
   365  						HTTPFilters:       makeRouterFilterList(t),
   366  					}},
   367  				},
   368  			},
   369  			wantMD: xdsresource.UpdateMetadata{
   370  				Status:  xdsresource.ServiceStatusACKed,
   371  				Version: "0",
   372  			},
   373  		},
   374  		{
   375  			desc:                   "one good and one bad - deserialization failure",
   376  			resourceNamesToRequest: []string{listenerName1, listenerName2},
   377  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   378  				VersionInfo: "0",
   379  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   380  				Resources: []*anypb.Any{
   381  					badlyMarshaledResource,
   382  					listener2,
   383  				},
   384  			},
   385  			wantUpdates: map[string]ads.DataAndErrTuple{
   386  				listenerName2: {
   387  					Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{
   388  						InlineRouteConfig: &xdsresource.RouteConfigUpdate{
   389  							VirtualHosts: []*xdsresource.VirtualHost{{
   390  								Domains: []string{"*"},
   391  								Routes: []*xdsresource.Route{{
   392  									Prefix:           newStringP("/"),
   393  									WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}},
   394  									ActionType:       xdsresource.RouteActionRoute},
   395  								},
   396  							}}},
   397  						MaxStreamDuration: time.Second,
   398  						Raw:               listener2,
   399  						HTTPFilters:       makeRouterFilterList(t),
   400  					}},
   401  				},
   402  			},
   403  			wantMD: xdsresource.UpdateMetadata{
   404  				Status:  xdsresource.ServiceStatusNACKed,
   405  				Version: "0",
   406  				ErrState: &xdsresource.UpdateErrorMetadata{
   407  					Version: "0",
   408  					Err:     cmpopts.AnyError,
   409  				},
   410  			},
   411  		},
   412  		{
   413  			desc:                   "one good and one bad - validation failure",
   414  			resourceNamesToRequest: []string{listenerName1, listenerName2},
   415  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   416  				VersionInfo: "0",
   417  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   418  				Resources: []*anypb.Any{
   419  					testutils.MarshalAny(t, &v3listenerpb.Listener{
   420  						Name: listenerName1,
   421  						ApiListener: &v3listenerpb.ApiListener{
   422  							ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   423  								RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{},
   424  							}),
   425  						},
   426  					}),
   427  					listener2,
   428  				},
   429  			},
   430  			wantUpdates: map[string]ads.DataAndErrTuple{
   431  				listenerName1: {Err: cmpopts.AnyError},
   432  				listenerName2: {
   433  					Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{
   434  						InlineRouteConfig: &xdsresource.RouteConfigUpdate{
   435  							VirtualHosts: []*xdsresource.VirtualHost{{
   436  								Domains: []string{"*"},
   437  								Routes: []*xdsresource.Route{{
   438  									Prefix:           newStringP("/"),
   439  									WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}},
   440  									ActionType:       xdsresource.RouteActionRoute},
   441  								},
   442  							}}},
   443  						MaxStreamDuration: time.Second,
   444  						Raw:               listener2,
   445  						HTTPFilters:       makeRouterFilterList(t),
   446  					}},
   447  				},
   448  			},
   449  			wantMD: xdsresource.UpdateMetadata{
   450  				Status:  xdsresource.ServiceStatusNACKed,
   451  				Version: "0",
   452  				ErrState: &xdsresource.UpdateErrorMetadata{
   453  					Version: "0",
   454  					Err:     cmpopts.AnyError,
   455  				},
   456  			},
   457  		},
   458  		{
   459  			desc:                   "two good resources",
   460  			resourceNamesToRequest: []string{listenerName1, listenerName2},
   461  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   462  				VersionInfo: "0",
   463  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   464  				Resources:   []*anypb.Any{listener1, listener2},
   465  			},
   466  			wantUpdates: map[string]ads.DataAndErrTuple{
   467  				listenerName1: {
   468  					Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{
   469  						InlineRouteConfig: &xdsresource.RouteConfigUpdate{
   470  							VirtualHosts: []*xdsresource.VirtualHost{{
   471  								Domains: []string{"*"},
   472  								Routes: []*xdsresource.Route{{
   473  									Prefix:           newStringP("/"),
   474  									WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}},
   475  									ActionType:       xdsresource.RouteActionRoute},
   476  								},
   477  							}}},
   478  						MaxStreamDuration: time.Second,
   479  						Raw:               listener1,
   480  						HTTPFilters:       makeRouterFilterList(t),
   481  					}},
   482  				},
   483  				listenerName2: {
   484  					Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{
   485  						InlineRouteConfig: &xdsresource.RouteConfigUpdate{
   486  							VirtualHosts: []*xdsresource.VirtualHost{{
   487  								Domains: []string{"*"},
   488  								Routes: []*xdsresource.Route{{
   489  									Prefix:           newStringP("/"),
   490  									WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}},
   491  									ActionType:       xdsresource.RouteActionRoute},
   492  								},
   493  							}}},
   494  						MaxStreamDuration: time.Second,
   495  						Raw:               listener2,
   496  						HTTPFilters:       makeRouterFilterList(t),
   497  					}},
   498  				},
   499  			},
   500  			wantMD: xdsresource.UpdateMetadata{
   501  				Status:  xdsresource.ServiceStatusACKed,
   502  				Version: "0",
   503  			},
   504  		},
   505  		{
   506  			desc:                   "two resources when we requested one",
   507  			resourceNamesToRequest: []string{listenerName1},
   508  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   509  				VersionInfo: "0",
   510  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   511  				Resources:   []*anypb.Any{listener1, listener2},
   512  			},
   513  			wantUpdates: map[string]ads.DataAndErrTuple{
   514  				listenerName1: {
   515  					Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{
   516  						InlineRouteConfig: &xdsresource.RouteConfigUpdate{
   517  							VirtualHosts: []*xdsresource.VirtualHost{{
   518  								Domains: []string{"*"},
   519  								Routes: []*xdsresource.Route{{
   520  									Prefix:           newStringP("/"),
   521  									WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}},
   522  									ActionType:       xdsresource.RouteActionRoute},
   523  								},
   524  							}}},
   525  						MaxStreamDuration: time.Second,
   526  						Raw:               listener1,
   527  						HTTPFilters:       makeRouterFilterList(t),
   528  					}},
   529  				},
   530  				listenerName2: {
   531  					Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{
   532  						InlineRouteConfig: &xdsresource.RouteConfigUpdate{
   533  							VirtualHosts: []*xdsresource.VirtualHost{{
   534  								Domains: []string{"*"},
   535  								Routes: []*xdsresource.Route{{
   536  									Prefix:           newStringP("/"),
   537  									WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}},
   538  									ActionType:       xdsresource.RouteActionRoute},
   539  								},
   540  							}}},
   541  						MaxStreamDuration: time.Second,
   542  						Raw:               listener2,
   543  						HTTPFilters:       makeRouterFilterList(t),
   544  					}},
   545  				},
   546  			},
   547  			wantMD: xdsresource.UpdateMetadata{
   548  				Status:  xdsresource.ServiceStatusACKed,
   549  				Version: "0",
   550  			},
   551  		},
   552  	}
   553  
   554  	for _, test := range tests {
   555  		t.Run(test.desc, func(t *testing.T) {
   556  			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   557  			defer cancel()
   558  
   559  			// Start a fake xDS management server and configure the response it
   560  			// would send to its client.
   561  			mgmtServer, cleanup, err := fakeserver.StartServer(nil)
   562  			if err != nil {
   563  				t.Fatalf("Failed to start fake xDS server: %v", err)
   564  			}
   565  			defer cleanup()
   566  			t.Logf("Started xDS management server on %s", mgmtServer.Address)
   567  			mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse}
   568  
   569  			// Create an xdsChannel for the test with a long watch expiry timer
   570  			// to ensure that watches don't expire for the duration of the test.
   571  			nodeID := uuid.New().String()
   572  			xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestTimeout)
   573  			defer xc.close()
   574  
   575  			// Subscribe to the resources specified in the test table.
   576  			for _, name := range test.resourceNamesToRequest {
   577  				xc.subscribe(listenerType, name)
   578  			}
   579  
   580  			// Wait for an update callback on the event handler and verify the
   581  			// contents of the update and the metadata.
   582  			verifyUpdateAndMetadata(ctx, t, xc.eventHandler.(*testEventHandler), test.wantUpdates, test.wantMD)
   583  		})
   584  	}
   585  }
   586  
   587  // Tests that the xdsChannel correctly handles the expiry of a watch for a
   588  // resource by ensuring that the watch expiry callback is invoked on the event
   589  // handler with the expected resource type and name.
   590  func (s) TestChannel_ADS_HandleResponseWatchExpiry(t *testing.T) {
   591  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   592  	defer cancel()
   593  
   594  	// Start an xDS management server, but do not configure any resources on it.
   595  	// This will result in the watch for a resource to timeout.
   596  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   597  
   598  	// Create an xdsChannel for the test with a short watch expiry timer to
   599  	// ensure that the test does not run very long, as it needs to wait for the
   600  	// watch to expire.
   601  	nodeID := uuid.New().String()
   602  	xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestShortTimeout)
   603  	defer xc.close()
   604  
   605  	// Subscribe to a listener resource.
   606  	const listenerName = "listener-name"
   607  	xc.subscribe(listenerType, listenerName)
   608  
   609  	// Wait for the watch expiry callback on the authority to be invoked and
   610  	// verify that the watch expired for the expected resource name and type.
   611  	eventHandler := xc.eventHandler.(*testEventHandler)
   612  	gotTyp, gotName, err := eventHandler.waitForResourceDoesNotExist(ctx)
   613  	if err != nil {
   614  		t.Fatal("Timeout when waiting for the watch expiry callback to be invoked on the xDS client")
   615  	}
   616  
   617  	if gotTyp != listenerType {
   618  		t.Fatalf("Got type %v, want %v", gotTyp, listenerType)
   619  	}
   620  	if gotName != listenerName {
   621  		t.Fatalf("Got name %v, want %v", gotName, listenerName)
   622  	}
   623  }
   624  
   625  // Tests that the xdsChannel correctly handles stream failures by ensuring that
   626  // the stream failure callback is invoked on the event handler.
   627  func (s) TestChannel_ADS_StreamFailure(t *testing.T) {
   628  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   629  	defer cancel()
   630  
   631  	// Start an xDS management server with a restartable listener to simulate
   632  	// connection failures.
   633  	l, err := testutils.LocalTCPListener()
   634  	if err != nil {
   635  		t.Fatalf("net.Listen() failed: %v", err)
   636  	}
   637  	lis := testutils.NewRestartableListener(l)
   638  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: lis})
   639  
   640  	// Configure a listener resource on the management server.
   641  	const listenerResourceName = "test-listener-resource"
   642  	const routeConfigurationName = "test-route-configuration-resource"
   643  	nodeID := uuid.New().String()
   644  	resources := e2e.UpdateOptions{
   645  		NodeID:         nodeID,
   646  		Listeners:      []*v3listenerpb.Listener{e2e.DefaultClientListener(listenerResourceName, routeConfigurationName)},
   647  		SkipValidation: true,
   648  	}
   649  	if err := mgmtServer.Update(ctx, resources); err != nil {
   650  		t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err)
   651  	}
   652  
   653  	// Create an xdsChannel for the test with a long watch expiry timer
   654  	// to ensure that watches don't expire for the duration of the test.
   655  	xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestTimeout)
   656  	defer xc.close()
   657  
   658  	// Subscribe to the resource created above.
   659  	xc.subscribe(listenerType, listenerResourceName)
   660  
   661  	// Wait for an update callback on the event handler and verify the
   662  	// contents of the update and the metadata.
   663  	hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   664  		RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{Rds: &v3httppb.Rds{
   665  			ConfigSource: &v3corepb.ConfigSource{
   666  				ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
   667  			},
   668  			RouteConfigName: routeConfigurationName,
   669  		}},
   670  		HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})},
   671  	})
   672  	listenerResource, err := anypb.New(&v3listenerpb.Listener{
   673  		Name:        listenerResourceName,
   674  		ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm},
   675  		FilterChains: []*v3listenerpb.FilterChain{{
   676  			Name: "filter-chain-name",
   677  			Filters: []*v3listenerpb.Filter{{
   678  				Name:       wellknown.HTTPConnectionManager,
   679  				ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm},
   680  			}},
   681  		}},
   682  	})
   683  	if err != nil {
   684  		t.Fatalf("Failed to create listener resource: %v", err)
   685  	}
   686  
   687  	wantUpdates := map[string]ads.DataAndErrTuple{
   688  		listenerResourceName: {
   689  			Resource: &xdsresource.ListenerResourceData{
   690  				Resource: xdsresource.ListenerUpdate{
   691  					RouteConfigName: routeConfigurationName,
   692  					HTTPFilters:     makeRouterFilterList(t),
   693  					Raw:             listenerResource,
   694  				},
   695  			},
   696  		},
   697  	}
   698  	wantMD := xdsresource.UpdateMetadata{
   699  		Status:  xdsresource.ServiceStatusACKed,
   700  		Version: "1",
   701  	}
   702  
   703  	eventHandler := xc.eventHandler.(*testEventHandler)
   704  	verifyUpdateAndMetadata(ctx, t, eventHandler, wantUpdates, wantMD)
   705  
   706  	lis.Stop()
   707  	if err := eventHandler.waitForStreamFailure(ctx); err != nil {
   708  		t.Fatalf("Timeout when waiting for the stream failure callback to be invoked on the xDS client: %v", err)
   709  	}
   710  }
   711  
   712  // Tests the behavior of the xdsChannel when a resource is unsubscribed.
   713  // Verifies that when a previously subscribed resource is unsubscribed, a
   714  // request is sent without the previously subscribed resource name.
   715  func (s) TestChannel_ADS_ResourceUnsubscribe(t *testing.T) {
   716  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   717  	defer cancel()
   718  
   719  	// Start an xDS management server that uses a channel to inform the test
   720  	// about the specific LDS resource names being requested.
   721  	ldsResourcesCh := make(chan []string, 1)
   722  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
   723  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
   724  			t.Logf("Received request for resources: %v of type %s", req.GetResourceNames(), req.GetTypeUrl())
   725  
   726  			if req.TypeUrl != version.V3ListenerURL {
   727  				return fmt.Errorf("unexpected resource type URL: %q", req.TypeUrl)
   728  			}
   729  
   730  			// Make the most recently requested names available to the test.
   731  			ldsResourcesCh <- req.GetResourceNames()
   732  			return nil
   733  		},
   734  	})
   735  
   736  	// Configure two listener resources on the management server.
   737  	const listenerResourceName1 = "test-listener-resource-1"
   738  	const routeConfigurationName1 = "test-route-configuration-resource-1"
   739  	const listenerResourceName2 = "test-listener-resource-2"
   740  	const routeConfigurationName2 = "test-route-configuration-resource-2"
   741  	nodeID := uuid.New().String()
   742  	resources := e2e.UpdateOptions{
   743  		NodeID: nodeID,
   744  		Listeners: []*v3listenerpb.Listener{
   745  			e2e.DefaultClientListener(listenerResourceName1, routeConfigurationName1),
   746  			e2e.DefaultClientListener(listenerResourceName2, routeConfigurationName2),
   747  		},
   748  		SkipValidation: true,
   749  	}
   750  	if err := mgmtServer.Update(ctx, resources); err != nil {
   751  		t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err)
   752  	}
   753  
   754  	// Create an xdsChannel for the test with a long watch expiry timer
   755  	// to ensure that watches don't expire for the duration of the test.
   756  	xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestTimeout)
   757  	defer xc.close()
   758  
   759  	// Subscribe to the resources created above and verify that a request is
   760  	// sent for the same.
   761  	xc.subscribe(listenerType, listenerResourceName1)
   762  	xc.subscribe(listenerType, listenerResourceName2)
   763  	if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerResourceName1, listenerResourceName2}); err != nil {
   764  		t.Fatal(err)
   765  	}
   766  
   767  	// Wait for the above resources to be ACKed.
   768  	if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerResourceName1, listenerResourceName2}); err != nil {
   769  		t.Fatal(err)
   770  	}
   771  
   772  	// Unsubscribe to one of the resources created above, and ensure that the
   773  	// other resource is still being requested.
   774  	xc.unsubscribe(listenerType, listenerResourceName1)
   775  	if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerResourceName2}); err != nil {
   776  		t.Fatal(err)
   777  	}
   778  
   779  	// Since the version on the management server for the above resource is not
   780  	// changed, we will not receive an update from it for the one resource that
   781  	// we are still requesting.
   782  
   783  	// Unsubscribe to the remaining resource, and ensure that no more resources
   784  	// are being requested.
   785  	xc.unsubscribe(listenerType, listenerResourceName2)
   786  	if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{}); err != nil {
   787  		t.Fatal(err)
   788  	}
   789  }
   790  
   791  // Tests the load reporting functionality of the xdsChannel.  It creates an
   792  // xdsChannel, starts load reporting, and verifies that an LRS streaming RPC is
   793  // created. It then makes another call to the load reporting API and ensures
   794  // that a new LRS stream is not created. Finally, it cancels the load reporting
   795  // calls and ensures that the stream is closed when the last call is canceled.
   796  //
   797  // Note that this test does not actually report any load. That is already tested
   798  // by an e2e style test in the xdsclient package.
   799  func (s) TestChannel_LRS_ReportLoad(t *testing.T) {
   800  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   801  	defer cancel()
   802  
   803  	// Create a management server that serves LRS.
   804  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{SupportLoadReportingService: true})
   805  
   806  	// Create an xdsChannel for the test. Node id and watch expiry timer don't
   807  	// matter for LRS.
   808  	xc := xdsChannelForTest(t, mgmtServer.Address, "", defaultTestTimeout)
   809  	defer xc.close()
   810  
   811  	// Start load reporting and verify that an LRS streaming RPC is created.
   812  	_, stopLRS1 := xc.reportLoad()
   813  	lrsServer := mgmtServer.LRSServer
   814  	if _, err := lrsServer.LRSStreamOpenChan.Receive(ctx); err != nil {
   815  		t.Fatalf("Timeout when waiting for an LRS streaming RPC to be created: %v", err)
   816  	}
   817  
   818  	// Make another call to the load reporting API, and ensure that a new LRS
   819  	// stream is not created.
   820  	_, stopLRS2 := xc.reportLoad()
   821  	sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout)
   822  	defer sCancel()
   823  	if _, err := lrsServer.LRSStreamOpenChan.Receive(sCtx); err != context.DeadlineExceeded {
   824  		t.Fatal("New LRS streaming RPC created when expected to use an existing one")
   825  	}
   826  
   827  	// Cancel the first load reporting call, and ensure that the stream does not
   828  	// close (because we have another call open).
   829  	stopLRS1()
   830  	sCtx, sCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout)
   831  	defer sCancel()
   832  	if _, err := lrsServer.LRSStreamCloseChan.Receive(sCtx); err != context.DeadlineExceeded {
   833  		t.Fatal("LRS stream closed when expected to stay open")
   834  	}
   835  
   836  	// Cancel the second load reporting call, and ensure the stream is closed.
   837  	stopLRS2()
   838  	if _, err := lrsServer.LRSStreamCloseChan.Receive(ctx); err != nil {
   839  		t.Fatal("Timeout waiting for LRS stream to close")
   840  	}
   841  }
   842  
   843  // waitForResourceNames waits for the wantNames to be received on namesCh.
   844  // Returns a non-nil error if the context expires before that.
   845  func waitForResourceNames(ctx context.Context, t *testing.T, namesCh chan []string, wantNames []string) error {
   846  	t.Helper()
   847  
   848  	var lastRequestedNames []string
   849  	for ; ; <-time.After(defaultTestShortTimeout) {
   850  		select {
   851  		case <-ctx.Done():
   852  			return fmt.Errorf("timeout waiting for resources %v to be requested from the management server. Last requested resources: %v", wantNames, lastRequestedNames)
   853  		case gotNames := <-namesCh:
   854  			if cmp.Equal(gotNames, wantNames, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(s1, s2 string) bool { return s1 < s2 })) {
   855  				return nil
   856  			}
   857  			lastRequestedNames = gotNames
   858  		}
   859  	}
   860  }
   861  
   862  // newTestEventHandler creates a new testEventHandler instance with the
   863  // necessary channels for testing the xdsChannel.
   864  func newTestEventHandler() *testEventHandler {
   865  	return &testEventHandler{
   866  		typeCh:    make(chan xdsresource.Type, 1),
   867  		updateCh:  make(chan map[string]ads.DataAndErrTuple, 1),
   868  		mdCh:      make(chan xdsresource.UpdateMetadata, 1),
   869  		nameCh:    make(chan string, 1),
   870  		connErrCh: make(chan error, 1),
   871  	}
   872  }
   873  
   874  // testEventHandler is a struct that implements the xdsChannelEventhandler
   875  // interface.  It is used to receive events from an xdsChannel, and has multiple
   876  // channels on which it makes these events available to the test.
   877  type testEventHandler struct {
   878  	typeCh    chan xdsresource.Type               // Resource type of an update or resource-does-not-exist error.
   879  	updateCh  chan map[string]ads.DataAndErrTuple // Resource updates.
   880  	mdCh      chan xdsresource.UpdateMetadata     // Metadata from an update.
   881  	nameCh    chan string                         // Name of the non-existent resource.
   882  	connErrCh chan error                          // Connectivity error.
   883  
   884  }
   885  
   886  func (ta *testEventHandler) adsStreamFailure(err error) {
   887  	ta.connErrCh <- err
   888  }
   889  
   890  func (ta *testEventHandler) waitForStreamFailure(ctx context.Context) error {
   891  	select {
   892  	case <-ctx.Done():
   893  		return ctx.Err()
   894  	case <-ta.connErrCh:
   895  	}
   896  	return nil
   897  }
   898  
   899  func (ta *testEventHandler) adsResourceUpdate(typ xdsresource.Type, updates map[string]ads.DataAndErrTuple, md xdsresource.UpdateMetadata, onDone func()) {
   900  	ta.typeCh <- typ
   901  	ta.updateCh <- updates
   902  	ta.mdCh <- md
   903  	onDone()
   904  }
   905  
   906  // waitForUpdate waits for the next resource update event from the xdsChannel.
   907  // It returns the resource type, the resource updates, and the update metadata.
   908  // If the context is canceled, it returns an error.
   909  func (ta *testEventHandler) waitForUpdate(ctx context.Context) (xdsresource.Type, map[string]ads.DataAndErrTuple, xdsresource.UpdateMetadata, error) {
   910  	var typ xdsresource.Type
   911  	var updates map[string]ads.DataAndErrTuple
   912  	var md xdsresource.UpdateMetadata
   913  
   914  	select {
   915  	case typ = <-ta.typeCh:
   916  	case <-ctx.Done():
   917  		return nil, nil, xdsresource.UpdateMetadata{}, ctx.Err()
   918  	}
   919  
   920  	select {
   921  	case updates = <-ta.updateCh:
   922  	case <-ctx.Done():
   923  		return nil, nil, xdsresource.UpdateMetadata{}, ctx.Err()
   924  	}
   925  
   926  	select {
   927  	case md = <-ta.mdCh:
   928  	case <-ctx.Done():
   929  		return nil, nil, xdsresource.UpdateMetadata{}, ctx.Err()
   930  	}
   931  	return typ, updates, md, nil
   932  }
   933  
   934  func (ta *testEventHandler) adsResourceDoesNotExist(typ xdsresource.Type, name string) {
   935  	ta.typeCh <- typ
   936  	ta.nameCh <- name
   937  }
   938  
   939  // waitForResourceDoesNotExist waits for the next resource-does-not-exist event
   940  // from the xdsChannel. It returns the resource type and the resource name. If
   941  // the context is canceled, it returns an error.
   942  func (ta *testEventHandler) waitForResourceDoesNotExist(ctx context.Context) (xdsresource.Type, string, error) {
   943  	var typ xdsresource.Type
   944  	var name string
   945  
   946  	select {
   947  	case typ = <-ta.typeCh:
   948  	case <-ctx.Done():
   949  		return nil, "", ctx.Err()
   950  	}
   951  
   952  	select {
   953  	case name = <-ta.nameCh:
   954  	case <-ctx.Done():
   955  		return nil, "", ctx.Err()
   956  	}
   957  	return typ, name, nil
   958  }
   959  
   960  func newStringP(s string) *string {
   961  	return &s
   962  }
   963  
   964  func makeRouterFilter(t *testing.T) xdsresource.HTTPFilter {
   965  	routerBuilder := httpfilter.Get(router.TypeURL)
   966  	routerConfig, _ := routerBuilder.ParseFilterConfig(testutils.MarshalAny(t, &v3routerpb.Router{}))
   967  	return xdsresource.HTTPFilter{Name: "router", Filter: routerBuilder, Config: routerConfig}
   968  }
   969  
   970  func makeRouterFilterList(t *testing.T) []xdsresource.HTTPFilter {
   971  	return []xdsresource.HTTPFilter{makeRouterFilter(t)}
   972  }