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

     1  /*
     2   *
     3   * Copyright 2022 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  package xdsclient_test
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"strings"
    26  	"testing"
    27  
    28  	"github.com/google/uuid"
    29  	"google.golang.org/grpc"
    30  	"google.golang.org/grpc/internal/testutils"
    31  	"google.golang.org/grpc/internal/testutils/xds/e2e"
    32  	"google.golang.org/grpc/internal/testutils/xds/fakeserver"
    33  	"google.golang.org/grpc/internal/xds/bootstrap"
    34  	"google.golang.org/grpc/xds/internal"
    35  	xdstestutils "google.golang.org/grpc/xds/internal/testutils"
    36  	"google.golang.org/grpc/xds/internal/xdsclient"
    37  	xdsclientinternal "google.golang.org/grpc/xds/internal/xdsclient/internal"
    38  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
    39  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version"
    40  	"google.golang.org/protobuf/types/known/anypb"
    41  
    42  	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    43  	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    44  )
    45  
    46  var (
    47  	// Resource type implementations retrieved from the resource type map in the
    48  	// internal package, which is initialized when the individual resource types
    49  	// are created.
    50  	listenerResourceType    = internal.ResourceTypeMapForTesting[version.V3ListenerURL].(xdsresource.Type)
    51  	routeConfigResourceType = internal.ResourceTypeMapForTesting[version.V3RouteConfigURL].(xdsresource.Type)
    52  )
    53  
    54  // This route configuration watcher registers two watches corresponding to the
    55  // names passed in at creation time on a valid update.
    56  type testRouteConfigWatcher struct {
    57  	client           xdsclient.XDSClient
    58  	name1, name2     string
    59  	rcw1, rcw2       *routeConfigWatcher
    60  	cancel1, cancel2 func()
    61  	updateCh         *testutils.Channel
    62  }
    63  
    64  func newTestRouteConfigWatcher(client xdsclient.XDSClient, name1, name2 string) *testRouteConfigWatcher {
    65  	return &testRouteConfigWatcher{
    66  		client:   client,
    67  		name1:    name1,
    68  		name2:    name2,
    69  		rcw1:     newRouteConfigWatcher(),
    70  		rcw2:     newRouteConfigWatcher(),
    71  		updateCh: testutils.NewChannel(),
    72  	}
    73  }
    74  
    75  func (rw *testRouteConfigWatcher) OnUpdate(update *xdsresource.RouteConfigResourceData, onDone xdsresource.OnDoneFunc) {
    76  	rw.updateCh.Send(routeConfigUpdateErrTuple{update: update.Resource})
    77  
    78  	rw.cancel1 = xdsresource.WatchRouteConfig(rw.client, rw.name1, rw.rcw1)
    79  	rw.cancel2 = xdsresource.WatchRouteConfig(rw.client, rw.name2, rw.rcw2)
    80  	onDone()
    81  }
    82  
    83  func (rw *testRouteConfigWatcher) OnError(err error, onDone xdsresource.OnDoneFunc) {
    84  	// When used with a go-control-plane management server that continuously
    85  	// resends resources which are NACKed by the xDS client, using a `Replace()`
    86  	// here and in OnResourceDoesNotExist() simplifies tests which will have
    87  	// access to the most recently received error.
    88  	rw.updateCh.Replace(routeConfigUpdateErrTuple{err: err})
    89  	onDone()
    90  }
    91  
    92  func (rw *testRouteConfigWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) {
    93  	rw.updateCh.Replace(routeConfigUpdateErrTuple{err: xdsresource.NewError(xdsresource.ErrorTypeResourceNotFound, "RouteConfiguration not found in received response")})
    94  	onDone()
    95  }
    96  
    97  func (rw *testRouteConfigWatcher) cancel() {
    98  	rw.cancel1()
    99  	rw.cancel2()
   100  }
   101  
   102  // TestWatchCallAnotherWatch tests the scenario where a watch is registered for
   103  // a resource, and more watches are registered from the first watch's callback.
   104  // The test verifies that this scenario does not lead to a deadlock.
   105  func (s) TestWatchCallAnotherWatch(t *testing.T) {
   106  	// Start an xDS management server and set the option to allow it to respond
   107  	// to requests which only specify a subset of the configured resources.
   108  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
   109  
   110  	nodeID := uuid.New().String()
   111  	authority := makeAuthorityName(t.Name())
   112  	bc, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
   113  		Servers: []byte(fmt.Sprintf(`[{
   114  			"server_uri": %q,
   115  			"channel_creds": [{"type": "insecure"}]
   116  		}]`, mgmtServer.Address)),
   117  		Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
   118  		Authorities: map[string]json.RawMessage{
   119  			// Xdstp style resource names used in this test use a slash removed
   120  			// version of t.Name as their authority, and the empty config
   121  			// results in the top-level xds server configuration being used for
   122  			// this authority.
   123  			authority: []byte(`{}`),
   124  		},
   125  	})
   126  	if err != nil {
   127  		t.Fatalf("Failed to create bootstrap configuration: %v", err)
   128  	}
   129  
   130  	// Create an xDS client with the above bootstrap contents.
   131  	config, err := bootstrap.NewConfigFromContents(bc)
   132  	if err != nil {
   133  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err)
   134  	}
   135  	pool := xdsclient.NewPool(config)
   136  	client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
   137  		Name: t.Name(),
   138  	})
   139  	if err != nil {
   140  		t.Fatalf("Failed to create xDS client: %v", err)
   141  	}
   142  	defer close()
   143  
   144  	// Configure the management server to respond with route config resources.
   145  	ldsNameNewStyle := makeNewStyleLDSName(authority)
   146  	rdsNameNewStyle := makeNewStyleRDSName(authority)
   147  	resources := e2e.UpdateOptions{
   148  		NodeID: nodeID,
   149  		Routes: []*v3routepb.RouteConfiguration{
   150  			e2e.DefaultRouteConfig(rdsName, ldsName, cdsName),
   151  			e2e.DefaultRouteConfig(rdsNameNewStyle, ldsNameNewStyle, cdsName),
   152  		},
   153  		SkipValidation: true,
   154  	}
   155  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   156  	defer cancel()
   157  	if err := mgmtServer.Update(ctx, resources); err != nil {
   158  		t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err)
   159  	}
   160  
   161  	// Create a route configuration watcher that registers two more watches from
   162  	// the OnUpdate callback:
   163  	// - one for the same resource name as this watch, which would be
   164  	//   satisfied from xdsClient cache
   165  	// - the other for a different resource name, which would be
   166  	//   satisfied from the server
   167  	rw := newTestRouteConfigWatcher(client, rdsName, rdsNameNewStyle)
   168  	defer rw.cancel()
   169  	rdsCancel := xdsresource.WatchRouteConfig(client, rdsName, rw)
   170  	defer rdsCancel()
   171  
   172  	// Verify the contents of the received update for the all watchers.
   173  	wantUpdate12 := routeConfigUpdateErrTuple{
   174  		update: xdsresource.RouteConfigUpdate{
   175  			VirtualHosts: []*xdsresource.VirtualHost{
   176  				{
   177  					Domains: []string{ldsName},
   178  					Routes: []*xdsresource.Route{
   179  						{
   180  							Prefix:           newStringP("/"),
   181  							ActionType:       xdsresource.RouteActionRoute,
   182  							WeightedClusters: map[string]xdsresource.WeightedCluster{cdsName: {Weight: 100}},
   183  						},
   184  					},
   185  				},
   186  			},
   187  		},
   188  	}
   189  	wantUpdate3 := routeConfigUpdateErrTuple{
   190  		update: xdsresource.RouteConfigUpdate{
   191  			VirtualHosts: []*xdsresource.VirtualHost{
   192  				{
   193  					Domains: []string{ldsNameNewStyle},
   194  					Routes: []*xdsresource.Route{
   195  						{
   196  							Prefix:           newStringP("/"),
   197  							ActionType:       xdsresource.RouteActionRoute,
   198  							WeightedClusters: map[string]xdsresource.WeightedCluster{cdsName: {Weight: 100}},
   199  						},
   200  					},
   201  				},
   202  			},
   203  		},
   204  	}
   205  	if err := verifyRouteConfigUpdate(ctx, rw.updateCh, wantUpdate12); err != nil {
   206  		t.Fatal(err)
   207  	}
   208  	if err := verifyRouteConfigUpdate(ctx, rw.rcw1.updateCh, wantUpdate12); err != nil {
   209  		t.Fatal(err)
   210  	}
   211  	if err := verifyRouteConfigUpdate(ctx, rw.rcw2.updateCh, wantUpdate3); err != nil {
   212  		t.Fatal(err)
   213  	}
   214  }
   215  
   216  // TestNodeProtoSentOnlyInFirstRequest verifies that a non-empty node proto gets
   217  // sent only on the first discovery request message on the ADS stream.
   218  //
   219  // It also verifies the same behavior holds after a stream restart.
   220  func (s) TestNodeProtoSentOnlyInFirstRequest(t *testing.T) {
   221  	// Create a restartable listener which can close existing connections.
   222  	l, err := testutils.LocalTCPListener()
   223  	if err != nil {
   224  		t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
   225  	}
   226  	lis := testutils.NewRestartableListener(l)
   227  
   228  	// Start a fake xDS management server with the above restartable listener.
   229  	//
   230  	// We are unable to use the go-control-plane server here, because it caches
   231  	// the node proto received in the first request message and adds it to
   232  	// subsequent requests before invoking the OnStreamRequest() callback.
   233  	// Therefore we cannot verify what is sent by the xDS client.
   234  	mgmtServer, cleanup, err := fakeserver.StartServer(lis)
   235  	if err != nil {
   236  		t.Fatalf("Failed to start fake xDS server: %v", err)
   237  	}
   238  	defer cleanup()
   239  
   240  	// Create a bootstrap file in a temporary directory.
   241  	nodeID := uuid.New().String()
   242  	bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
   243  
   244  	// Create an xDS client with the above bootstrap contents.
   245  	config, err := bootstrap.NewConfigFromContents(bc)
   246  	if err != nil {
   247  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err)
   248  	}
   249  	pool := xdsclient.NewPool(config)
   250  	client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
   251  		Name: t.Name(),
   252  	})
   253  	if err != nil {
   254  		t.Fatalf("Failed to create xDS client: %v", err)
   255  	}
   256  	defer close()
   257  
   258  	const (
   259  		serviceName     = "my-service-client-side-xds"
   260  		routeConfigName = "route-" + serviceName
   261  		clusterName     = "cluster-" + serviceName
   262  	)
   263  
   264  	// Register a watch for the Listener resource.
   265  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   266  	defer cancel()
   267  	watcher := xdstestutils.NewTestResourceWatcher()
   268  	client.WatchResource(listenerResourceType, serviceName, watcher)
   269  
   270  	// Ensure the watch results in a discovery request with an empty node proto.
   271  	if err := readDiscoveryResponseAndCheckForNonEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil {
   272  		t.Fatal(err)
   273  	}
   274  
   275  	// Configure a listener resource on the fake xDS server.
   276  	lisAny, err := anypb.New(e2e.DefaultClientListener(serviceName, routeConfigName))
   277  	if err != nil {
   278  		t.Fatalf("Failed to marshal listener resource into an Any proto: %v", err)
   279  	}
   280  	mgmtServer.XDSResponseChan <- &fakeserver.Response{
   281  		Resp: &v3discoverypb.DiscoveryResponse{
   282  			TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   283  			VersionInfo: "1",
   284  			Resources:   []*anypb.Any{lisAny},
   285  		},
   286  	}
   287  
   288  	// The xDS client is expected to ACK the Listener resource. The discovery
   289  	// request corresponding to the ACK must contain a nil node proto.
   290  	if err := readDiscoveryResponseAndCheckForEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil {
   291  		t.Fatal(err)
   292  	}
   293  
   294  	// Register a watch for a RouteConfiguration resource.
   295  	client.WatchResource(routeConfigResourceType, routeConfigName, watcher)
   296  
   297  	// Ensure the watch results in a discovery request with an empty node proto.
   298  	if err := readDiscoveryResponseAndCheckForEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil {
   299  		t.Fatal(err)
   300  	}
   301  
   302  	// Configure the route configuration resource on the fake xDS server.
   303  	rcAny, err := anypb.New(e2e.DefaultRouteConfig(routeConfigName, serviceName, clusterName))
   304  	if err != nil {
   305  		t.Fatalf("Failed to marshal route configuration resource into an Any proto: %v", err)
   306  	}
   307  	mgmtServer.XDSResponseChan <- &fakeserver.Response{
   308  		Resp: &v3discoverypb.DiscoveryResponse{
   309  			TypeUrl:     "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
   310  			VersionInfo: "1",
   311  			Resources:   []*anypb.Any{rcAny},
   312  		},
   313  	}
   314  
   315  	// Ensure the discovery request for the ACK contains an empty node proto.
   316  	if err := readDiscoveryResponseAndCheckForEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil {
   317  		t.Fatal(err)
   318  	}
   319  
   320  	// Stop the management server and expect the error callback to be invoked.
   321  	lis.Stop()
   322  	select {
   323  	case <-ctx.Done():
   324  		t.Fatal("Timeout when waiting for the connection error to be propagated to the watcher")
   325  	case <-watcher.ErrorCh:
   326  	}
   327  
   328  	// Restart the management server.
   329  	lis.Restart()
   330  
   331  	// The xDS client is expected to re-request previously requested resources.
   332  	// Hence, we expect two DiscoveryRequest messages (one for the Listener and
   333  	// one for the RouteConfiguration resource). The first message should contain
   334  	// a non-nil node proto and the second should contain a nil-proto.
   335  	//
   336  	// And since we don't push any responses on the response channel of the fake
   337  	// server, we do not expect any ACKs here.
   338  	if err := readDiscoveryResponseAndCheckForNonEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil {
   339  		t.Fatal(err)
   340  	}
   341  	if err := readDiscoveryResponseAndCheckForEmptyNodeProto(ctx, mgmtServer.XDSRequestChan); err != nil {
   342  		t.Fatal(err)
   343  	}
   344  }
   345  
   346  // readDiscoveryResponseAndCheckForEmptyNodeProto reads a discovery request
   347  // message out of the provided reqCh. It returns an error if it fails to read a
   348  // message before the context deadline expires, or if the read message contains
   349  // a non-empty node proto.
   350  func readDiscoveryResponseAndCheckForEmptyNodeProto(ctx context.Context, reqCh *testutils.Channel) error {
   351  	v, err := reqCh.Receive(ctx)
   352  	if err != nil {
   353  		return fmt.Errorf("Timeout when waiting for a DiscoveryRequest message")
   354  	}
   355  	req := v.(*fakeserver.Request).Req.(*v3discoverypb.DiscoveryRequest)
   356  	if node := req.GetNode(); node != nil {
   357  		return fmt.Errorf("Node proto received in DiscoveryRequest message is %v, want empty node proto", node)
   358  	}
   359  	return nil
   360  }
   361  
   362  // readDiscoveryResponseAndCheckForNonEmptyNodeProto reads a discovery request
   363  // message out of the provided reqCh. It returns an error if it fails to read a
   364  // message before the context deadline expires, or if the read message contains
   365  // an empty node proto.
   366  func readDiscoveryResponseAndCheckForNonEmptyNodeProto(ctx context.Context, reqCh *testutils.Channel) error {
   367  	v, err := reqCh.Receive(ctx)
   368  	if err != nil {
   369  		return fmt.Errorf("Timeout when waiting for a DiscoveryRequest message")
   370  	}
   371  	req := v.(*fakeserver.Request).Req.(*v3discoverypb.DiscoveryRequest)
   372  	if node := req.GetNode(); node == nil {
   373  		return fmt.Errorf("Empty node proto received in DiscoveryRequest message, want non-empty node proto")
   374  	}
   375  	return nil
   376  }
   377  
   378  type testRouteConfigResourceType struct{}
   379  
   380  func (testRouteConfigResourceType) TypeURL() string                  { return version.V3RouteConfigURL }
   381  func (testRouteConfigResourceType) TypeName() string                 { return "RouteConfigResource" }
   382  func (testRouteConfigResourceType) AllResourcesRequiredInSotW() bool { return false }
   383  func (testRouteConfigResourceType) Decode(*xdsresource.DecodeOptions, *anypb.Any) (*xdsresource.DecodeResult, error) {
   384  	return nil, nil
   385  }
   386  
   387  // Tests that the errors returned by the xDS client when watching a resource
   388  // contain the node ID that was used to create the client. This test covers two
   389  // scenarios:
   390  //
   391  //  1. When a watch is registered for an already registered resource type, but
   392  //     this time with a different implementation,
   393  //  2. When a watch is registered for a resource name whose authority is not
   394  //     found in the bootstrap configuration.
   395  func (s) TestWatchErrorsContainNodeID(t *testing.T) {
   396  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   397  
   398  	// Create bootstrap configuration pointing to the above management server.
   399  	nodeID := uuid.New().String()
   400  	bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
   401  
   402  	// Create an xDS client with the above bootstrap contents.
   403  	config, err := bootstrap.NewConfigFromContents(bc)
   404  	if err != nil {
   405  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err)
   406  	}
   407  	pool := xdsclient.NewPool(config)
   408  	client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
   409  		Name: t.Name(),
   410  	})
   411  	if err != nil {
   412  		t.Fatalf("Failed to create xDS client: %v", err)
   413  	}
   414  	defer close()
   415  
   416  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   417  	defer cancel()
   418  
   419  	t.Run("Multiple_ResourceType_Implementations", func(t *testing.T) {
   420  		const routeConfigName = "route-config-name"
   421  		watcher := xdstestutils.NewTestResourceWatcher()
   422  		client.WatchResource(routeConfigResourceType, routeConfigName, watcher)
   423  
   424  		sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
   425  		defer sCancel()
   426  		select {
   427  		case <-sCtx.Done():
   428  		case <-watcher.UpdateCh:
   429  			t.Fatal("Unexpected resource update")
   430  		case <-watcher.ErrorCh:
   431  			t.Fatal("Unexpected resource error")
   432  		case <-watcher.ResourceDoesNotExistCh:
   433  			t.Fatal("Unexpected resource does not exist")
   434  		}
   435  
   436  		client.WatchResource(testRouteConfigResourceType{}, routeConfigName, watcher)
   437  		select {
   438  		case <-ctx.Done():
   439  			t.Fatal("Timeout when waiting for error callback to be invoked")
   440  		case err := <-watcher.ErrorCh:
   441  			if err == nil || !strings.Contains(err.Error(), nodeID) {
   442  				t.Fatalf("Unexpected error: %v, want error with node ID: %q", err, nodeID)
   443  			}
   444  		}
   445  	})
   446  
   447  	t.Run("Missing_Authority", func(t *testing.T) {
   448  		const routeConfigName = "xdstp://nonexistant-authority/envoy.config.route.v3.RouteConfiguration/route-config-name"
   449  		watcher := xdstestutils.NewTestResourceWatcher()
   450  		client.WatchResource(routeConfigResourceType, routeConfigName, watcher)
   451  
   452  		select {
   453  		case <-ctx.Done():
   454  			t.Fatal("Timeout when waiting for error callback to be invoked")
   455  		case err := <-watcher.ErrorCh:
   456  			if err == nil || !strings.Contains(err.Error(), nodeID) {
   457  				t.Fatalf("Unexpected error: %v, want error with node ID: %q", err, nodeID)
   458  			}
   459  		}
   460  	})
   461  }
   462  
   463  // Tests that the errors returned by the xDS client when watching a resource
   464  // contain the node ID when channel creation to the management server fails.
   465  func (s) TestWatchErrorsContainNodeID_ChannelCreationFailure(t *testing.T) {
   466  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   467  
   468  	// Create bootstrap configuration pointing to the above management server.
   469  	nodeID := uuid.New().String()
   470  	bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
   471  
   472  	// Create an xDS client with the above bootstrap contents.
   473  	config, err := bootstrap.NewConfigFromContents(bc)
   474  	if err != nil {
   475  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err)
   476  	}
   477  	pool := xdsclient.NewPool(config)
   478  	client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
   479  		Name: t.Name(),
   480  	})
   481  	if err != nil {
   482  		t.Fatalf("Failed to create xDS client: %v", err)
   483  	}
   484  	defer close()
   485  
   486  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   487  	defer cancel()
   488  
   489  	// Override the xDS channel dialer with one that always fails.
   490  	origDialer := xdsclientinternal.GRPCNewClient
   491  	xdsclientinternal.GRPCNewClient = func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
   492  		return nil, fmt.Errorf("failed to create channel")
   493  	}
   494  	defer func() { xdsclientinternal.GRPCNewClient = origDialer }()
   495  
   496  	const routeConfigName = "route-config-name"
   497  	watcher := xdstestutils.NewTestResourceWatcher()
   498  	client.WatchResource(routeConfigResourceType, routeConfigName, watcher)
   499  
   500  	select {
   501  	case <-ctx.Done():
   502  		t.Fatal("Timeout when waiting for error callback to be invoked")
   503  	case err := <-watcher.ErrorCh:
   504  		if err == nil || !strings.Contains(err.Error(), nodeID) {
   505  			t.Fatalf("Unexpected error: %v, want error with node ID: %q", err, nodeID)
   506  		}
   507  	}
   508  }