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

     1  /*
     2   *
     3   * Copyright 2025 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
    20  
    21  import (
    22  	"encoding/json"
    23  	"fmt"
    24  	"reflect"
    25  	"sync"
    26  	"testing"
    27  
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/google/go-cmp/cmp/cmpopts"
    30  	"google.golang.org/grpc"
    31  	"google.golang.org/grpc/credentials/insecure"
    32  	"google.golang.org/grpc/internal/testutils/stats"
    33  	"google.golang.org/grpc/internal/xds/bootstrap"
    34  	"google.golang.org/grpc/xds/internal/clients"
    35  	"google.golang.org/grpc/xds/internal/clients/grpctransport"
    36  	"google.golang.org/grpc/xds/internal/clients/xdsclient"
    37  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
    38  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version"
    39  	"google.golang.org/protobuf/testing/protocmp"
    40  )
    41  
    42  const (
    43  	testXDSServerURL    = "xds.example.com:8080"
    44  	testXDSServerURL2   = "xds.example.com:8081"
    45  	testNodeID          = "test-node-id"
    46  	testClusterName     = "test-cluster"
    47  	testUserAgentName   = "test-ua-name"
    48  	testUserAgentVer    = "test-ua-ver"
    49  	testLocalityRegion  = "test-region"
    50  	testLocalityZone    = "test-zone"
    51  	testLocalitySubZone = "test-sub-zone"
    52  	testTargetName      = "test-target"
    53  )
    54  
    55  var (
    56  	testMetadataJSON, _ = json.Marshal(map[string]any{"foo": "bar", "baz": float64(1)})
    57  )
    58  
    59  func (s) TestBuildXDSClientConfig_Success(t *testing.T) {
    60  	tests := []struct {
    61  		name                string
    62  		bootstrapContents   []byte
    63  		wantXDSClientConfig func(bootstrapCfg *bootstrap.Config) xdsclient.Config
    64  	}{
    65  		{
    66  			name: "without authorities",
    67  			bootstrapContents: []byte(fmt.Sprintf(`{
    68  				"xds_servers": [{"server_uri": "%s", "channel_creds": [{"type": "insecure"}]}],
    69  				"node": {
    70  					"id": "%s", "cluster": "%s", "metadata": %s,
    71  					"locality": {"region": "%s", "zone": "%s", "sub_zone": "%s"},
    72  					"user_agent_name": "%s", "user_agent_version": "%s"
    73  				}
    74  			}`, testXDSServerURL, testNodeID, testClusterName, testMetadataJSON, testLocalityRegion, testLocalityZone, testLocalitySubZone, testUserAgentName, testUserAgentVer)),
    75  			wantXDSClientConfig: func(c *bootstrap.Config) xdsclient.Config {
    76  				node, serverCfg := c.Node(), c.XDSServers()[0]
    77  				expectedServer := xdsclient.ServerConfig{ServerIdentifier: clients.ServerIdentifier{ServerURI: serverCfg.ServerURI(), Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}}}
    78  				gServerCfgMap := map[xdsclient.ServerConfig]*bootstrap.ServerConfig{expectedServer: serverCfg}
    79  				return xdsclient.Config{
    80  					Servers:     []xdsclient.ServerConfig{expectedServer},
    81  					Node:        clients.Node{ID: node.GetId(), Cluster: node.GetCluster(), Metadata: node.Metadata, Locality: clients.Locality{Region: node.Locality.Region, Zone: node.Locality.Zone, SubZone: node.Locality.SubZone}, UserAgentName: node.UserAgentName, UserAgentVersion: node.GetUserAgentVersion()},
    82  					Authorities: map[string]xdsclient.Authority{},
    83  					ResourceTypes: map[string]xdsclient.ResourceType{
    84  						version.V3ListenerURL:    {TypeURL: version.V3ListenerURL, TypeName: xdsresource.ListenerResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericListenerResourceTypeDecoder(c)},
    85  						version.V3RouteConfigURL: {TypeURL: version.V3RouteConfigURL, TypeName: xdsresource.RouteConfigTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericRouteConfigResourceTypeDecoder()},
    86  						version.V3ClusterURL:     {TypeURL: version.V3ClusterURL, TypeName: xdsresource.ClusterResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericClusterResourceTypeDecoder(c, gServerCfgMap)},
    87  						version.V3EndpointsURL:   {TypeURL: version.V3EndpointsURL, TypeName: xdsresource.EndpointsResourceTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericEndpointsResourceTypeDecoder()},
    88  					},
    89  					MetricsReporter: &metricsReporter{recorder: stats.NewTestMetricsRecorder(), target: testTargetName},
    90  					TransportBuilder: grpctransport.NewBuilder(map[string]grpctransport.Config{
    91  						"insecure": {
    92  							Credentials: insecure.NewBundle(),
    93  							GRPCNewClient: func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
    94  								opts = append(opts, serverCfg.DialOptions()...)
    95  								return grpc.NewClient(target, opts...)
    96  							}},
    97  					}),
    98  				}
    99  			},
   100  		},
   101  		{
   102  			name: "with authorities",
   103  			bootstrapContents: []byte(fmt.Sprintf(`{
   104  				"xds_servers": [{"server_uri": "%s", "channel_creds": [{"type": "insecure"}]}],
   105  				"node": {"id": "%s"},
   106  				"authorities": {
   107  					"auth1": {},
   108  					"auth2": {"xds_servers": [{"server_uri": "%s", "channel_creds": [{"type": "insecure"}]}]}
   109  				}
   110  			}`, testXDSServerURL, testNodeID, testXDSServerURL2)),
   111  			wantXDSClientConfig: func(c *bootstrap.Config) xdsclient.Config {
   112  				node := c.Node()
   113  				topLevelSCfg, auth2SCfg := c.XDSServers()[0], c.Authorities()["auth2"].XDSServers[0]
   114  				expTopLevelS := xdsclient.ServerConfig{ServerIdentifier: clients.ServerIdentifier{ServerURI: topLevelSCfg.ServerURI(), Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}}}
   115  				expAuth2S := xdsclient.ServerConfig{ServerIdentifier: clients.ServerIdentifier{ServerURI: auth2SCfg.ServerURI(), Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}}}
   116  				gSCfgMap := map[xdsclient.ServerConfig]*bootstrap.ServerConfig{expTopLevelS: topLevelSCfg, expAuth2S: auth2SCfg}
   117  				return xdsclient.Config{
   118  					Servers:     []xdsclient.ServerConfig{expTopLevelS},
   119  					Node:        clients.Node{ID: node.GetId(), Cluster: node.GetCluster(), Metadata: node.Metadata, UserAgentName: node.UserAgentName, UserAgentVersion: node.GetUserAgentVersion()},
   120  					Authorities: map[string]xdsclient.Authority{"auth1": {XDSServers: []xdsclient.ServerConfig{expTopLevelS}}, "auth2": {XDSServers: []xdsclient.ServerConfig{expAuth2S}}},
   121  					ResourceTypes: map[string]xdsclient.ResourceType{
   122  						version.V3ListenerURL:    {TypeURL: version.V3ListenerURL, TypeName: xdsresource.ListenerResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericListenerResourceTypeDecoder(c)},
   123  						version.V3RouteConfigURL: {TypeURL: version.V3RouteConfigURL, TypeName: xdsresource.RouteConfigTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericRouteConfigResourceTypeDecoder()},
   124  						version.V3ClusterURL:     {TypeURL: version.V3ClusterURL, TypeName: xdsresource.ClusterResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericClusterResourceTypeDecoder(c, gSCfgMap)},
   125  						version.V3EndpointsURL:   {TypeURL: version.V3EndpointsURL, TypeName: xdsresource.EndpointsResourceTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericEndpointsResourceTypeDecoder()},
   126  					},
   127  					MetricsReporter: &metricsReporter{recorder: stats.NewTestMetricsRecorder(), target: testTargetName},
   128  					TransportBuilder: grpctransport.NewBuilder(map[string]grpctransport.Config{
   129  						"insecure": {
   130  							Credentials: insecure.NewBundle(),
   131  							GRPCNewClient: func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
   132  								opts = append(opts, topLevelSCfg.DialOptions()...)
   133  								return grpc.NewClient(target, opts...)
   134  							}},
   135  					}),
   136  				}
   137  			},
   138  		},
   139  		{
   140  			name: "server features with ignore_resource_deletion",
   141  			bootstrapContents: []byte(fmt.Sprintf(`{
   142  				"xds_servers": [{"server_uri": "%s", "channel_creds": [{"type": "insecure"}], "server_features": ["ignore_resource_deletion"]}],
   143  				"node": {"id": "%s"}
   144  			}`, testXDSServerURL, testNodeID)),
   145  			wantXDSClientConfig: func(c *bootstrap.Config) xdsclient.Config {
   146  				node, serverCfg := c.Node(), c.XDSServers()[0]
   147  				expectedServer := xdsclient.ServerConfig{ServerIdentifier: clients.ServerIdentifier{ServerURI: serverCfg.ServerURI(), Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}}, IgnoreResourceDeletion: true}
   148  				gServerCfgMap := map[xdsclient.ServerConfig]*bootstrap.ServerConfig{expectedServer: serverCfg}
   149  				return xdsclient.Config{
   150  					Servers:     []xdsclient.ServerConfig{expectedServer},
   151  					Node:        clients.Node{ID: node.GetId(), Cluster: node.GetCluster(), Metadata: node.Metadata, UserAgentName: node.UserAgentName, UserAgentVersion: node.GetUserAgentVersion()},
   152  					Authorities: map[string]xdsclient.Authority{},
   153  					ResourceTypes: map[string]xdsclient.ResourceType{
   154  						version.V3ListenerURL:    {TypeURL: version.V3ListenerURL, TypeName: xdsresource.ListenerResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericListenerResourceTypeDecoder(c)},
   155  						version.V3RouteConfigURL: {TypeURL: version.V3RouteConfigURL, TypeName: xdsresource.RouteConfigTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericRouteConfigResourceTypeDecoder()},
   156  						version.V3ClusterURL:     {TypeURL: version.V3ClusterURL, TypeName: xdsresource.ClusterResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericClusterResourceTypeDecoder(c, gServerCfgMap)},
   157  						version.V3EndpointsURL:   {TypeURL: version.V3EndpointsURL, TypeName: xdsresource.EndpointsResourceTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericEndpointsResourceTypeDecoder()},
   158  					},
   159  					MetricsReporter: &metricsReporter{recorder: stats.NewTestMetricsRecorder(), target: testTargetName},
   160  					TransportBuilder: grpctransport.NewBuilder(map[string]grpctransport.Config{
   161  						"insecure": {
   162  							Credentials: insecure.NewBundle(),
   163  							GRPCNewClient: func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
   164  								opts = append(opts, serverCfg.DialOptions()...)
   165  								return grpc.NewClient(target, opts...)
   166  							}},
   167  					}),
   168  				}
   169  			},
   170  		},
   171  		{
   172  			name: "channel creds - unknown type skipped",
   173  			bootstrapContents: []byte(fmt.Sprintf(`{
   174  				"xds_servers": [{"server_uri": "%s", "channel_creds": [{"type": "unknown-type"}, {"type": "insecure"}]}],
   175  				"node": {"id": "%s"}
   176  			}`, testXDSServerURL, testNodeID)), // "insecure" is selected
   177  			wantXDSClientConfig: func(c *bootstrap.Config) xdsclient.Config {
   178  				node, serverCfg := c.Node(), c.XDSServers()[0] // SelectedCreds will be "insecure"
   179  				expectedServer := xdsclient.ServerConfig{ServerIdentifier: clients.ServerIdentifier{ServerURI: serverCfg.ServerURI(), Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}}}
   180  				gServerCfgMap := map[xdsclient.ServerConfig]*bootstrap.ServerConfig{expectedServer: serverCfg}
   181  				return xdsclient.Config{
   182  					Servers:     []xdsclient.ServerConfig{expectedServer},
   183  					Node:        clients.Node{ID: node.GetId(), Cluster: node.GetCluster(), Metadata: node.Metadata, UserAgentName: node.UserAgentName, UserAgentVersion: node.GetUserAgentVersion()},
   184  					Authorities: map[string]xdsclient.Authority{},
   185  					ResourceTypes: map[string]xdsclient.ResourceType{
   186  						version.V3ListenerURL:    {TypeURL: version.V3ListenerURL, TypeName: xdsresource.ListenerResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericListenerResourceTypeDecoder(c)},
   187  						version.V3RouteConfigURL: {TypeURL: version.V3RouteConfigURL, TypeName: xdsresource.RouteConfigTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericRouteConfigResourceTypeDecoder()},
   188  						version.V3ClusterURL:     {TypeURL: version.V3ClusterURL, TypeName: xdsresource.ClusterResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericClusterResourceTypeDecoder(c, gServerCfgMap)},
   189  						version.V3EndpointsURL:   {TypeURL: version.V3EndpointsURL, TypeName: xdsresource.EndpointsResourceTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericEndpointsResourceTypeDecoder()},
   190  					},
   191  					MetricsReporter: &metricsReporter{recorder: stats.NewTestMetricsRecorder(), target: testTargetName},
   192  					TransportBuilder: grpctransport.NewBuilder(map[string]grpctransport.Config{
   193  						"insecure": {
   194  							Credentials: insecure.NewBundle(),
   195  							GRPCNewClient: func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
   196  								opts = append(opts, serverCfg.DialOptions()...)
   197  								return grpc.NewClient(target, opts...)
   198  							}},
   199  					}),
   200  				}
   201  			},
   202  		},
   203  	}
   204  
   205  	for _, tt := range tests {
   206  		t.Run(tt.name, func(t *testing.T) {
   207  			bootstrapConfig, err := bootstrap.NewConfigFromContents(tt.bootstrapContents)
   208  			if err != nil {
   209  				t.Fatalf("Failed to create bootstrap config: %v", err)
   210  			}
   211  			gotCfg, err := buildXDSClientConfig(bootstrapConfig, stats.NewTestMetricsRecorder(), testTargetName)
   212  			if err != nil {
   213  				t.Fatalf("Failed to build XDSClientConfig: %v", err)
   214  			}
   215  
   216  			wantCfg := tt.wantXDSClientConfig(bootstrapConfig)
   217  
   218  			unexportedTypeOpts := cmpopts.IgnoreUnexported(clients.Node{}, grpctransport.Builder{})
   219  			ignoreTypeOpts := cmpopts.IgnoreTypes(sync.Mutex{})
   220  			resourceTypeCmpOpts := cmp.Comparer(func(a, b xdsclient.ResourceType) bool {
   221  				return a.TypeURL == b.TypeURL && a.TypeName == b.TypeName && a.AllResourcesRequiredInSotW == b.AllResourcesRequiredInSotW && reflect.TypeOf(a.Decoder) == reflect.TypeOf(b.Decoder)
   222  			})
   223  			metricsReporterCmpOpts := cmp.Comparer(func(a, b clients.MetricsReporter) bool {
   224  				if (a == nil) != (b == nil) {
   225  					return false
   226  				}
   227  				if a == nil { // Both are nil
   228  					return true
   229  				}
   230  				// Both are non-nil, compare type and target.
   231  				aConcrete, aOK := a.(*metricsReporter)
   232  				bConcrete, bOK := b.(*metricsReporter)
   233  				if !(aOK && bOK && aConcrete.target == bConcrete.target) {
   234  					return false
   235  				}
   236  				// Compare recorder by type.
   237  				if (aConcrete.recorder == nil) != (bConcrete.recorder == nil) {
   238  					return false
   239  				}
   240  				// If both are nil, recorder check passes. If both non-nil, check types.
   241  				return aConcrete.recorder == nil || reflect.TypeOf(aConcrete.recorder) == reflect.TypeOf(bConcrete.recorder)
   242  			})
   243  			transportBuilderCmpOpts := cmp.Comparer(func(a, b grpctransport.Config) bool {
   244  				// Compare Credentials by type
   245  				credsEqual := true
   246  				if (a.Credentials == nil) != (b.Credentials == nil) {
   247  					credsEqual = false
   248  				} else if a.Credentials != nil && reflect.TypeOf(a.Credentials) != reflect.TypeOf(b.Credentials) {
   249  					credsEqual = false
   250  				}
   251  				// Compare GRPCNewClient by nil-ness
   252  				newClientEqual := (a.GRPCNewClient == nil) == (b.GRPCNewClient == nil)
   253  				return credsEqual && newClientEqual
   254  			})
   255  
   256  			if diff := cmp.Diff(wantCfg, gotCfg, protocmp.Transform(), unexportedTypeOpts, ignoreTypeOpts, resourceTypeCmpOpts, metricsReporterCmpOpts, transportBuilderCmpOpts); diff != "" {
   257  				t.Errorf("buildXDSClientConfig() mismatch (-want +got):\n%s", diff)
   258  			}
   259  		})
   260  	}
   261  }