google.golang.org/grpc@v1.74.2/xds/internal/clients/xdsclient/test/dump_test.go (about)

     1  /*
     2   *
     3   * Copyright 2022 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  package xdsclient_test
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"slices"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  	"github.com/google/uuid"
    31  	"google.golang.org/grpc/credentials/insecure"
    32  	"google.golang.org/grpc/xds/internal/clients"
    33  	"google.golang.org/grpc/xds/internal/clients/grpctransport"
    34  	"google.golang.org/grpc/xds/internal/clients/internal/pretty"
    35  	"google.golang.org/grpc/xds/internal/clients/internal/testutils"
    36  	"google.golang.org/grpc/xds/internal/clients/internal/testutils/e2e"
    37  	"google.golang.org/grpc/xds/internal/clients/xdsclient"
    38  	"google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource"
    39  	"google.golang.org/protobuf/proto"
    40  	"google.golang.org/protobuf/testing/protocmp"
    41  	"google.golang.org/protobuf/types/known/anypb"
    42  
    43  	v3adminpb "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
    44  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    45  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    46  	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    47  	v3statuspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
    48  )
    49  
    50  func makeGenericXdsConfig(typeURL, name, version string, status v3adminpb.ClientResourceStatus, config *anypb.Any, failure *v3adminpb.UpdateFailureState) *v3statuspb.ClientConfig_GenericXdsConfig {
    51  	return &v3statuspb.ClientConfig_GenericXdsConfig{
    52  		TypeUrl:      typeURL,
    53  		Name:         name,
    54  		VersionInfo:  version,
    55  		ClientStatus: status,
    56  		XdsConfig:    config,
    57  		ErrorState:   failure,
    58  	}
    59  }
    60  
    61  func checkResourceDump(ctx context.Context, want *v3statuspb.ClientStatusResponse, client *xdsclient.XDSClient) error {
    62  	var cmpOpts = cmp.Options{
    63  		protocmp.Transform(),
    64  		protocmp.IgnoreFields((*v3statuspb.ClientConfig_GenericXdsConfig)(nil), "last_updated"),
    65  		protocmp.IgnoreFields((*v3statuspb.ClientConfig)(nil), "client_scope"),
    66  		protocmp.IgnoreFields((*v3adminpb.UpdateFailureState)(nil), "last_update_attempt", "details"),
    67  	}
    68  
    69  	var lastErr error
    70  	for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) {
    71  		b, err := client.DumpResources()
    72  		if err != nil {
    73  			lastErr = err
    74  			continue
    75  		}
    76  		got := &v3statuspb.ClientStatusResponse{}
    77  		if err := proto.Unmarshal(b, got); err != nil {
    78  			lastErr = err
    79  			continue
    80  		}
    81  		// Sort the client configs based on the `client_scope` field.
    82  		slices.SortFunc(got.GetConfig(), func(a, b *v3statuspb.ClientConfig) int {
    83  			return strings.Compare(a.ClientScope, b.ClientScope)
    84  		})
    85  		// Sort the resource configs based on the type_url and name fields.
    86  		for _, cc := range got.GetConfig() {
    87  			slices.SortFunc(cc.GetGenericXdsConfigs(), func(a, b *v3statuspb.ClientConfig_GenericXdsConfig) int {
    88  				if strings.Compare(a.TypeUrl, b.TypeUrl) == 0 {
    89  					return strings.Compare(a.Name, b.Name)
    90  				}
    91  				return strings.Compare(a.TypeUrl, b.TypeUrl)
    92  			})
    93  		}
    94  		diff := cmp.Diff(want, got, cmpOpts)
    95  		if diff == "" {
    96  			return nil
    97  		}
    98  		lastErr = fmt.Errorf("received unexpected resource dump, diff (-got, +want):\n%s, got: %s\n want:%s", diff, pretty.ToJSON(got), pretty.ToJSON(want))
    99  	}
   100  	return fmt.Errorf("timeout when waiting for resource dump to reach expected state: %v", lastErr)
   101  }
   102  
   103  // Tests the scenario where there are multiple xDS clients talking to the same
   104  // management server, and requesting the same set of resources. Verifies that
   105  // under all circumstances, both xDS clients receive the same configuration from
   106  // the server.
   107  func (s) TestDumpResources_ManyToOne(t *testing.T) {
   108  	// Initialize the xDS resources to be used in this test.
   109  	ldsTargets := []string{"lds.target.good:0000", "lds.target.good:1111"}
   110  	rdsTargets := []string{"route-config-0", "route-config-1"}
   111  	listeners := make([]*v3listenerpb.Listener, len(ldsTargets))
   112  	listenerAnys := make([]*anypb.Any, len(ldsTargets))
   113  	for i := range ldsTargets {
   114  		listeners[i] = e2e.DefaultClientListener(ldsTargets[i], rdsTargets[i])
   115  		listenerAnys[i] = testutils.MarshalAny(t, listeners[i])
   116  	}
   117  
   118  	// Spin up an xDS management server on a local port.
   119  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   120  
   121  	nodeID := uuid.New().String()
   122  
   123  	resourceTypes := map[string]xdsclient.ResourceType{xdsresource.V3ListenerURL: listenerType}
   124  	si := clients.ServerIdentifier{
   125  		ServerURI:  mgmtServer.Address,
   126  		Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"},
   127  	}
   128  
   129  	configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}}
   130  	xdsClientConfig := xdsclient.Config{
   131  		Servers:          []xdsclient.ServerConfig{{ServerIdentifier: si}},
   132  		Node:             clients.Node{ID: nodeID, UserAgentName: "user-agent", UserAgentVersion: "0.0.0.0"},
   133  		TransportBuilder: grpctransport.NewBuilder(configs),
   134  		ResourceTypes:    resourceTypes,
   135  		// Xdstp resource names used in this test do not specify an
   136  		// authority. These will end up looking up an entry with the
   137  		// empty key in the authorities map. Having an entry with an
   138  		// empty key and empty configuration, results in these
   139  		// resources also using the top-level configuration.
   140  		Authorities: map[string]xdsclient.Authority{
   141  			"": {XDSServers: []xdsclient.ServerConfig{}},
   142  		},
   143  	}
   144  
   145  	// Create two xDS clients with the above config.
   146  	client1, err := xdsclient.New(xdsClientConfig)
   147  	if err != nil {
   148  		t.Fatalf("Failed to create xDS client: %v", err)
   149  	}
   150  	defer client1.Close()
   151  	client2, err := xdsclient.New(xdsClientConfig)
   152  	if err != nil {
   153  		t.Fatalf("Failed to create xDS client: %v", err)
   154  	}
   155  	defer client2.Close()
   156  
   157  	// Dump resources and expect empty configs.
   158  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   159  	defer cancel()
   160  	wantNode := &v3corepb.Node{
   161  		Id:                   nodeID,
   162  		UserAgentName:        "user-agent",
   163  		UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "0.0.0.0"},
   164  		ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   165  	}
   166  	wantResp := &v3statuspb.ClientStatusResponse{
   167  		Config: []*v3statuspb.ClientConfig{
   168  			{
   169  				Node: wantNode,
   170  			},
   171  		},
   172  	}
   173  	if err := checkResourceDump(ctx, wantResp, client1); err != nil {
   174  		t.Fatal(err)
   175  	}
   176  	if err := checkResourceDump(ctx, wantResp, client2); err != nil {
   177  		t.Fatal(err)
   178  	}
   179  
   180  	// Register watches, dump resources and expect configs in requested state.
   181  	for _, xdsC := range []*xdsclient.XDSClient{client1, client2} {
   182  		for _, target := range ldsTargets {
   183  			xdsC.WatchResource(xdsresource.V3ListenerURL, target, noopListenerWatcher{})
   184  		}
   185  	}
   186  	wantConfigs := []*v3statuspb.ClientConfig_GenericXdsConfig{
   187  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   188  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   189  	}
   190  	wantResp = &v3statuspb.ClientStatusResponse{
   191  		Config: []*v3statuspb.ClientConfig{
   192  			{
   193  				Node:              wantNode,
   194  				GenericXdsConfigs: wantConfigs,
   195  			},
   196  		},
   197  	}
   198  	if err := checkResourceDump(ctx, wantResp, client1); err != nil {
   199  		t.Fatal(err)
   200  	}
   201  	if err := checkResourceDump(ctx, wantResp, client2); err != nil {
   202  		t.Fatal(err)
   203  	}
   204  
   205  	// Configure the resources on the management server.
   206  	if err := mgmtServer.Update(ctx, e2e.UpdateOptions{
   207  		NodeID:         nodeID,
   208  		Listeners:      listeners,
   209  		SkipValidation: true,
   210  	}); err != nil {
   211  		t.Fatal(err)
   212  	}
   213  
   214  	// Dump resources and expect ACK configs.
   215  	wantConfigs = []*v3statuspb.ClientConfig_GenericXdsConfig{
   216  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[0], nil),
   217  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil),
   218  	}
   219  	wantResp = &v3statuspb.ClientStatusResponse{
   220  		Config: []*v3statuspb.ClientConfig{
   221  			{
   222  				Node:              wantNode,
   223  				GenericXdsConfigs: wantConfigs,
   224  			},
   225  		},
   226  	}
   227  	if err := checkResourceDump(ctx, wantResp, client1); err != nil {
   228  		t.Fatal(err)
   229  	}
   230  	if err := checkResourceDump(ctx, wantResp, client2); err != nil {
   231  		t.Fatal(err)
   232  	}
   233  
   234  	// Update the first resource of each type in the management server to a
   235  	// value which is expected to be NACK'ed by the xDS client.
   236  	listeners[0] = func() *v3listenerpb.Listener {
   237  		hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{})
   238  		return &v3listenerpb.Listener{
   239  			Name:        ldsTargets[0],
   240  			ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm},
   241  		}
   242  	}()
   243  	if err := mgmtServer.Update(ctx, e2e.UpdateOptions{
   244  		NodeID:         nodeID,
   245  		Listeners:      listeners,
   246  		SkipValidation: true,
   247  	}); err != nil {
   248  		t.Fatal(err)
   249  	}
   250  
   251  	wantConfigs = []*v3statuspb.ClientConfig_GenericXdsConfig{
   252  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_NACKED, listenerAnys[0], &v3adminpb.UpdateFailureState{VersionInfo: "2"}),
   253  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "2", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil),
   254  	}
   255  	wantResp = &v3statuspb.ClientStatusResponse{
   256  		Config: []*v3statuspb.ClientConfig{
   257  			{
   258  				Node:              wantNode,
   259  				GenericXdsConfigs: wantConfigs,
   260  			},
   261  		},
   262  	}
   263  	if err := checkResourceDump(ctx, wantResp, client1); err != nil {
   264  		t.Fatal(err)
   265  	}
   266  	if err := checkResourceDump(ctx, wantResp, client2); err != nil {
   267  		t.Fatal(err)
   268  	}
   269  }
   270  
   271  // Tests the scenario where there are multiple xDS client talking to different
   272  // management server, and requesting different set of resources.
   273  func (s) TestDumpResources_ManyToMany(t *testing.T) {
   274  	// Initialize the xDS resources to be used in this test:
   275  	// - The first xDS client watches old style resource names, and thereby
   276  	//   requests these resources from the top-level xDS server.
   277  	// - The second xDS client watches new style resource names with a non-empty
   278  	//   authority, and thereby requests these resources from the server
   279  	//   configuration for that authority.
   280  	authority := strings.Join(strings.Split(t.Name(), "/"), "")
   281  	ldsTargets := []string{
   282  		"lds.target.good:0000",
   283  		fmt.Sprintf("xdstp://%s/envoy.config.listener.v3.Listener/lds.targer.good:1111", authority),
   284  	}
   285  	rdsTargets := []string{
   286  		"route-config-0",
   287  		fmt.Sprintf("xdstp://%s/envoy.config.route.v3.RouteConfiguration/route-config-1", authority),
   288  	}
   289  	listeners := make([]*v3listenerpb.Listener, len(ldsTargets))
   290  	listenerAnys := make([]*anypb.Any, len(ldsTargets))
   291  	for i := range ldsTargets {
   292  		listeners[i] = e2e.DefaultClientListener(ldsTargets[i], rdsTargets[i])
   293  		listenerAnys[i] = testutils.MarshalAny(t, listeners[i])
   294  	}
   295  
   296  	// Start two management servers.
   297  	mgmtServer1 := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   298  	mgmtServer2 := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   299  
   300  	// The first of the above management servers will be the top-level xDS
   301  	// server in the bootstrap configuration, and the second will be the xDS
   302  	// server corresponding to the test authority.
   303  	nodeID := uuid.New().String()
   304  
   305  	resourceTypes := map[string]xdsclient.ResourceType{}
   306  	listenerType := listenerType
   307  	resourceTypes[xdsresource.V3ListenerURL] = listenerType
   308  	si1 := clients.ServerIdentifier{
   309  		ServerURI:  mgmtServer1.Address,
   310  		Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"},
   311  	}
   312  	si2 := clients.ServerIdentifier{
   313  		ServerURI:  mgmtServer2.Address,
   314  		Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"},
   315  	}
   316  
   317  	configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}}
   318  	xdsClientConfig := xdsclient.Config{
   319  		Servers:          []xdsclient.ServerConfig{{ServerIdentifier: si1}},
   320  		Node:             clients.Node{ID: nodeID, UserAgentName: "user-agent", UserAgentVersion: "0.0.0.0"},
   321  		TransportBuilder: grpctransport.NewBuilder(configs),
   322  		ResourceTypes:    resourceTypes,
   323  		// Xdstp style resource names used in this test use a slash removed
   324  		// version of t.Name as their authority, and the empty config
   325  		// results in the top-level xds server configuration being used for
   326  		// this authority.
   327  		Authorities: map[string]xdsclient.Authority{
   328  			authority: {XDSServers: []xdsclient.ServerConfig{{ServerIdentifier: si2}}},
   329  		},
   330  	}
   331  
   332  	// Create two xDS clients with the above config.
   333  	client1, err := xdsclient.New(xdsClientConfig)
   334  	if err != nil {
   335  		t.Fatalf("Failed to create xDS client: %v", err)
   336  	}
   337  	defer client1.Close()
   338  	client2, err := xdsclient.New(xdsClientConfig)
   339  	if err != nil {
   340  		t.Fatalf("Failed to create xDS client: %v", err)
   341  	}
   342  	defer client2.Close()
   343  
   344  	// Check the resource dump before configuring resources on the management server.
   345  	// Dump resources and expect empty configs.
   346  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   347  	defer cancel()
   348  	wantNode := &v3corepb.Node{
   349  		Id:                   nodeID,
   350  		UserAgentName:        "user-agent",
   351  		UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "0.0.0.0"},
   352  		ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   353  	}
   354  	wantResp := &v3statuspb.ClientStatusResponse{
   355  		Config: []*v3statuspb.ClientConfig{
   356  			{
   357  				Node: wantNode,
   358  			},
   359  		},
   360  	}
   361  	if err := checkResourceDump(ctx, wantResp, client1); err != nil {
   362  		t.Fatal(err)
   363  	}
   364  	if err := checkResourceDump(ctx, wantResp, client2); err != nil {
   365  		t.Fatal(err)
   366  	}
   367  
   368  	// Register watches, the first xDS client watches old style resource names,
   369  	// while the second xDS client watches new style resource names.
   370  	client1.WatchResource(xdsresource.V3ListenerURL, ldsTargets[0], noopListenerWatcher{})
   371  	client2.WatchResource(xdsresource.V3ListenerURL, ldsTargets[1], noopListenerWatcher{})
   372  
   373  	// Check the resource dump. Both clients should have all resources in
   374  	// REQUESTED state.
   375  	wantConfigs1 := []*v3statuspb.ClientConfig_GenericXdsConfig{
   376  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   377  	}
   378  	wantConfigs2 := []*v3statuspb.ClientConfig_GenericXdsConfig{
   379  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
   380  	}
   381  	wantResp = &v3statuspb.ClientStatusResponse{
   382  		Config: []*v3statuspb.ClientConfig{
   383  			{
   384  				Node:              wantNode,
   385  				GenericXdsConfigs: wantConfigs1,
   386  			},
   387  		},
   388  	}
   389  	if err := checkResourceDump(ctx, wantResp, client1); err != nil {
   390  		t.Fatal(err)
   391  	}
   392  	wantResp = &v3statuspb.ClientStatusResponse{
   393  		Config: []*v3statuspb.ClientConfig{
   394  			{
   395  				Node:              wantNode,
   396  				GenericXdsConfigs: wantConfigs2,
   397  			},
   398  		},
   399  	}
   400  	if err := checkResourceDump(ctx, wantResp, client2); err != nil {
   401  		t.Fatal(err)
   402  	}
   403  
   404  	// Configure resources on the first management server.
   405  	if err := mgmtServer1.Update(ctx, e2e.UpdateOptions{
   406  		NodeID:         nodeID,
   407  		Listeners:      listeners[:1],
   408  		SkipValidation: true,
   409  	}); err != nil {
   410  		t.Fatal(err)
   411  	}
   412  
   413  	// Check the resource dump. One client should have resources in ACKED state,
   414  	// while the other should still have resources in REQUESTED state.
   415  	wantConfigs1 = []*v3statuspb.ClientConfig_GenericXdsConfig{
   416  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[0], nil),
   417  	}
   418  	wantResp = &v3statuspb.ClientStatusResponse{
   419  		Config: []*v3statuspb.ClientConfig{
   420  			{
   421  				Node:              wantNode,
   422  				GenericXdsConfigs: wantConfigs1,
   423  			},
   424  		},
   425  	}
   426  	if err := checkResourceDump(ctx, wantResp, client1); err != nil {
   427  		t.Fatal(err)
   428  	}
   429  	wantResp = &v3statuspb.ClientStatusResponse{
   430  		Config: []*v3statuspb.ClientConfig{
   431  			{
   432  				Node:              wantNode,
   433  				GenericXdsConfigs: wantConfigs2,
   434  			},
   435  		},
   436  	}
   437  	if err := checkResourceDump(ctx, wantResp, client2); err != nil {
   438  		t.Fatal(err)
   439  	}
   440  
   441  	// Configure resources on the second management server.
   442  	if err := mgmtServer2.Update(ctx, e2e.UpdateOptions{
   443  		NodeID:         nodeID,
   444  		Listeners:      listeners[1:],
   445  		SkipValidation: true,
   446  	}); err != nil {
   447  		t.Fatal(err)
   448  	}
   449  
   450  	// Check the resource dump. Both clients should have appropriate resources
   451  	// in REQUESTED state.
   452  	wantConfigs2 = []*v3statuspb.ClientConfig_GenericXdsConfig{
   453  		makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil),
   454  	}
   455  	wantResp = &v3statuspb.ClientStatusResponse{
   456  		Config: []*v3statuspb.ClientConfig{
   457  			{
   458  				Node:              wantNode,
   459  				GenericXdsConfigs: wantConfigs1,
   460  			},
   461  		},
   462  	}
   463  	if err := checkResourceDump(ctx, wantResp, client1); err != nil {
   464  		t.Fatal(err)
   465  	}
   466  	wantResp = &v3statuspb.ClientStatusResponse{
   467  		Config: []*v3statuspb.ClientConfig{
   468  			{
   469  				Node:              wantNode,
   470  				GenericXdsConfigs: wantConfigs2,
   471  			},
   472  		},
   473  	}
   474  	if err := checkResourceDump(ctx, wantResp, client2); err != nil {
   475  		t.Fatal(err)
   476  	}
   477  }