google.golang.org/grpc@v1.72.2/internal/xds/bootstrap/bootstrap_test.go (about)

     1  /*
     2   *
     3   * Copyright 2019 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 bootstrap
    20  
    21  import (
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"os"
    26  	"testing"
    27  
    28  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    29  	"github.com/google/go-cmp/cmp"
    30  	"google.golang.org/grpc"
    31  	"google.golang.org/grpc/credentials/tls/certprovider"
    32  	"google.golang.org/grpc/internal"
    33  	"google.golang.org/grpc/internal/envconfig"
    34  	"google.golang.org/grpc/internal/grpctest"
    35  	"google.golang.org/grpc/xds/bootstrap"
    36  	"google.golang.org/protobuf/testing/protocmp"
    37  	"google.golang.org/protobuf/types/known/structpb"
    38  )
    39  
    40  var (
    41  	v3BootstrapFileMap = map[string]string{
    42  		"serverFeaturesIncludesXDSV3": `
    43  		{
    44  			"node": {
    45  				"id": "ENVOY_NODE_ID",
    46  				"metadata": {
    47  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
    48  			    }
    49  			},
    50  			"xds_servers" : [{
    51  				"server_uri": "trafficdirector.googleapis.com:443",
    52  				"channel_creds": [
    53  					{ "type": "google_default" }
    54  				],
    55  				"server_features" : ["xds_v3"]
    56  			}]
    57  		}`,
    58  		"serverFeaturesExcludesXDSV3": `
    59  		{
    60  			"node": {
    61  				"id": "ENVOY_NODE_ID",
    62  				"metadata": {
    63  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
    64  			    }
    65  			},
    66  			"xds_servers" : [{
    67  				"server_uri": "trafficdirector.googleapis.com:443",
    68  				"channel_creds": [
    69  					{ "type": "google_default" }
    70  				]
    71  			}]
    72  		}`,
    73  		"emptyNodeProto": `
    74  		{
    75  			"xds_servers" : [{
    76  				"server_uri": "trafficdirector.googleapis.com:443",
    77  				"channel_creds": [
    78  					{ "type": "insecure" }
    79  				]
    80  			}]
    81  		}`,
    82  		"unknownTopLevelFieldInFile": `
    83  		{
    84  			"node": {
    85  				"id": "ENVOY_NODE_ID",
    86  				"metadata": {
    87  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
    88  			    }
    89  			},
    90  			"xds_servers" : [{
    91  				"server_uri": "trafficdirector.googleapis.com:443",
    92  				"channel_creds": [
    93  					{ "type": "insecure" }
    94  				]
    95  			}],
    96  			"unknownField": "foobar"
    97  		}`,
    98  		"unknownFieldInNodeProto": `
    99  		{
   100  			"node": {
   101  				"id": "ENVOY_NODE_ID",
   102  				"unknownField": "foobar",
   103  				"metadata": {
   104  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   105  			    }
   106  			},
   107  			"xds_servers" : [{
   108  				"server_uri": "trafficdirector.googleapis.com:443",
   109  				"channel_creds": [
   110  					{ "type": "insecure" }
   111  				]
   112  			}]
   113  		}`,
   114  		"unknownFieldInXdsServer": `
   115  		{
   116  			"node": {
   117  				"id": "ENVOY_NODE_ID",
   118  				"metadata": {
   119  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   120  			    }
   121  			},
   122  			"xds_servers" : [{
   123  				"server_uri": "trafficdirector.googleapis.com:443",
   124  				"channel_creds": [
   125  					{ "type": "insecure" }
   126  				],
   127  				"unknownField": "foobar"
   128  			}]
   129  		}`,
   130  		"multipleChannelCreds": `
   131  		{
   132  			"node": {
   133  				"id": "ENVOY_NODE_ID",
   134  				"metadata": {
   135  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   136  			    }
   137  			},
   138  			"xds_servers" : [{
   139  				"server_uri": "trafficdirector.googleapis.com:443",
   140  				"channel_creds": [
   141  					{ "type": "not-google-default" },
   142  					{ "type": "google_default" }
   143  				],
   144  				"server_features": ["xds_v3"]
   145  			}]
   146  		}`,
   147  		"goodBootstrap": `
   148  		{
   149  			"node": {
   150  				"id": "ENVOY_NODE_ID",
   151  				"metadata": {
   152  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   153  			    }
   154  			},
   155  			"xds_servers" : [{
   156  				"server_uri": "trafficdirector.googleapis.com:443",
   157  				"channel_creds": [
   158  					{ "type": "google_default" }
   159  				],
   160  				"server_features": ["xds_v3"]
   161  			}]
   162  		}`,
   163  		"multipleXDSServers": `
   164  		{
   165  			"node": {
   166  				"id": "ENVOY_NODE_ID",
   167  				"metadata": {
   168  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   169  			    }
   170  			},
   171  			"xds_servers" : [
   172  				{
   173  					"server_uri": "trafficdirector.googleapis.com:443",
   174  					"channel_creds": [{ "type": "google_default" }],
   175  					"server_features": ["xds_v3"]
   176  				},
   177  				{
   178  					"server_uri": "backup.never.use.com:1234",
   179  					"channel_creds": [{ "type": "google_default" }]
   180  				}
   181  			]
   182  		}`,
   183  		"serverSupportsIgnoreResourceDeletion": `
   184  		{
   185  			"node": {
   186  				"id": "ENVOY_NODE_ID",
   187  				"metadata": {
   188  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   189  			    }
   190  			},
   191  			"xds_servers" : [{
   192  				"server_uri": "trafficdirector.googleapis.com:443",
   193  				"channel_creds": [
   194  					{ "type": "google_default" }
   195  				],
   196  				"server_features" : ["ignore_resource_deletion", "xds_v3"]
   197  			}]
   198  		}`,
   199  	}
   200  	metadata = &structpb.Struct{
   201  		Fields: map[string]*structpb.Value{
   202  			"TRAFFICDIRECTOR_GRPC_HOSTNAME": {
   203  				Kind: &structpb.Value_StringValue{StringValue: "trafficdirector"},
   204  			},
   205  		},
   206  	}
   207  	v3Node = node{
   208  		ID:                   "ENVOY_NODE_ID",
   209  		Metadata:             metadata,
   210  		userAgentName:        gRPCUserAgentName,
   211  		userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version},
   212  		clientFeatures:       []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper},
   213  	}
   214  	configWithInsecureCreds = &Config{
   215  		xDSServers: []*ServerConfig{{
   216  			serverURI:     "trafficdirector.googleapis.com:443",
   217  			channelCreds:  []ChannelCreds{{Type: "insecure"}},
   218  			selectedCreds: ChannelCreds{Type: "insecure"},
   219  		}},
   220  		node: v3Node,
   221  		clientDefaultListenerResourceNameTemplate: "%s",
   222  	}
   223  	configWithMultipleChannelCredsAndV3 = &Config{
   224  		xDSServers: []*ServerConfig{{
   225  			serverURI:      "trafficdirector.googleapis.com:443",
   226  			channelCreds:   []ChannelCreds{{Type: "not-google-default"}, {Type: "google_default"}},
   227  			serverFeatures: []string{"xds_v3"},
   228  			selectedCreds:  ChannelCreds{Type: "google_default"},
   229  		}},
   230  		node: v3Node,
   231  		clientDefaultListenerResourceNameTemplate: "%s",
   232  	}
   233  	configWithGoogleDefaultCredsAndV3 = &Config{
   234  		xDSServers: []*ServerConfig{{
   235  			serverURI:      "trafficdirector.googleapis.com:443",
   236  			channelCreds:   []ChannelCreds{{Type: "google_default"}},
   237  			serverFeatures: []string{"xds_v3"},
   238  			selectedCreds:  ChannelCreds{Type: "google_default"},
   239  		}},
   240  		node: v3Node,
   241  		clientDefaultListenerResourceNameTemplate: "%s",
   242  	}
   243  	configWithMultipleServers = &Config{
   244  		xDSServers: []*ServerConfig{
   245  			{
   246  				serverURI:      "trafficdirector.googleapis.com:443",
   247  				channelCreds:   []ChannelCreds{{Type: "google_default"}},
   248  				serverFeatures: []string{"xds_v3"},
   249  				selectedCreds:  ChannelCreds{Type: "google_default"},
   250  			},
   251  			{
   252  				serverURI:     "backup.never.use.com:1234",
   253  				channelCreds:  []ChannelCreds{{Type: "google_default"}},
   254  				selectedCreds: ChannelCreds{Type: "google_default"},
   255  			},
   256  		},
   257  		node: v3Node,
   258  		clientDefaultListenerResourceNameTemplate: "%s",
   259  	}
   260  	configWithGoogleDefaultCredsAndIgnoreResourceDeletion = &Config{
   261  		xDSServers: []*ServerConfig{{
   262  			serverURI:      "trafficdirector.googleapis.com:443",
   263  			channelCreds:   []ChannelCreds{{Type: "google_default"}},
   264  			serverFeatures: []string{"ignore_resource_deletion", "xds_v3"},
   265  			selectedCreds:  ChannelCreds{Type: "google_default"},
   266  		}},
   267  		node: v3Node,
   268  		clientDefaultListenerResourceNameTemplate: "%s",
   269  	}
   270  	configWithGoogleDefaultCredsAndNoServerFeatures = &Config{
   271  		xDSServers: []*ServerConfig{{
   272  			serverURI:     "trafficdirector.googleapis.com:443",
   273  			channelCreds:  []ChannelCreds{{Type: "google_default"}},
   274  			selectedCreds: ChannelCreds{Type: "google_default"},
   275  		}},
   276  		node: v3Node,
   277  		clientDefaultListenerResourceNameTemplate: "%s",
   278  	}
   279  )
   280  
   281  func fileReadFromFileMap(bootstrapFileMap map[string]string, name string) ([]byte, error) {
   282  	if b, ok := bootstrapFileMap[name]; ok {
   283  		return []byte(b), nil
   284  	}
   285  	return nil, os.ErrNotExist
   286  }
   287  
   288  func setupBootstrapOverride(bootstrapFileMap map[string]string) func() {
   289  	oldFileReadFunc := bootstrapFileReadFunc
   290  	bootstrapFileReadFunc = func(filename string) ([]byte, error) {
   291  		return fileReadFromFileMap(bootstrapFileMap, filename)
   292  	}
   293  	return func() { bootstrapFileReadFunc = oldFileReadFunc }
   294  }
   295  
   296  // This function overrides the bootstrap file NAME env variable, to test the
   297  // code that reads file with the given fileName.
   298  func testGetConfigurationWithFileNameEnv(t *testing.T, fileName string, wantError bool, wantConfig *Config) {
   299  	origBootstrapFileName := envconfig.XDSBootstrapFileName
   300  	envconfig.XDSBootstrapFileName = fileName
   301  	defer func() { envconfig.XDSBootstrapFileName = origBootstrapFileName }()
   302  
   303  	c, err := GetConfiguration()
   304  	if (err != nil) != wantError {
   305  		t.Fatalf("GetConfiguration() returned error %v, wantError: %v", err, wantError)
   306  	}
   307  	if wantError {
   308  		return
   309  	}
   310  	if diff := cmp.Diff(wantConfig, c); diff != "" {
   311  		t.Fatalf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff)
   312  	}
   313  }
   314  
   315  // This function overrides the bootstrap file CONTENT env variable, to test the
   316  // code that uses the content from env directly.
   317  func testGetConfigurationWithFileContentEnv(t *testing.T, fileName string, wantError bool, wantConfig *Config) {
   318  	t.Helper()
   319  	b, err := bootstrapFileReadFunc(fileName)
   320  	if err != nil {
   321  		t.Skip(err)
   322  	}
   323  	origBootstrapContent := envconfig.XDSBootstrapFileContent
   324  	envconfig.XDSBootstrapFileContent = string(b)
   325  	defer func() { envconfig.XDSBootstrapFileContent = origBootstrapContent }()
   326  
   327  	c, err := GetConfiguration()
   328  	if (err != nil) != wantError {
   329  		t.Fatalf("GetConfiguration() returned error %v, wantError: %v", err, wantError)
   330  	}
   331  	if wantError {
   332  		return
   333  	}
   334  	if diff := cmp.Diff(wantConfig, c); diff != "" {
   335  		t.Fatalf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff)
   336  	}
   337  }
   338  
   339  // Tests GetConfiguration with bootstrap file contents that are expected to
   340  // fail.
   341  func (s) TestGetConfiguration_Failure(t *testing.T) {
   342  	bootstrapFileMap := map[string]string{
   343  		"empty":          "",
   344  		"badJSON":        `["test": 123]`,
   345  		"noBalancerName": `{"node": {"id": "ENVOY_NODE_ID"}}`,
   346  		"emptyXdsServer": `
   347  		{
   348  			"node": {
   349  				"id": "ENVOY_NODE_ID",
   350  				"metadata": {
   351  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   352  			    }
   353  			}
   354  		}`,
   355  		"emptyChannelCreds": `
   356  		{
   357  			"node": {
   358  				"id": "ENVOY_NODE_ID",
   359  				"metadata": {
   360  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   361  			    }
   362  			},
   363  			"xds_servers" : [{
   364  				"server_uri": "trafficdirector.googleapis.com:443"
   365  			}]
   366  		}`,
   367  		"nonGoogleDefaultCreds": `
   368  		{
   369  			"node": {
   370  				"id": "ENVOY_NODE_ID",
   371  				"metadata": {
   372  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   373  			    }
   374  			},
   375  			"xds_servers" : [{
   376  				"server_uri": "trafficdirector.googleapis.com:443",
   377  				"channel_creds": [
   378  					{ "type": "not-google-default" }
   379  				]
   380  			}]
   381  		}`,
   382  	}
   383  	cancel := setupBootstrapOverride(bootstrapFileMap)
   384  	defer cancel()
   385  
   386  	for _, name := range []string{"nonExistentBootstrapFile", "empty", "badJSON", "noBalancerName", "emptyXdsServer"} {
   387  		t.Run(name, func(t *testing.T) {
   388  			testGetConfigurationWithFileNameEnv(t, name, true, nil)
   389  			testGetConfigurationWithFileContentEnv(t, name, true, nil)
   390  		})
   391  	}
   392  }
   393  
   394  // Tests the functionality in GetConfiguration with different bootstrap file
   395  // contents. It overrides the fileReadFunc by returning bootstrap file contents
   396  // defined in this test, instead of reading from a file.
   397  func (s) TestGetConfiguration_Success(t *testing.T) {
   398  	cancel := setupBootstrapOverride(v3BootstrapFileMap)
   399  	defer cancel()
   400  
   401  	tests := []struct {
   402  		name       string
   403  		wantConfig *Config
   404  	}{
   405  		{
   406  			name: "emptyNodeProto",
   407  			wantConfig: &Config{
   408  				xDSServers: []*ServerConfig{{
   409  					serverURI:     "trafficdirector.googleapis.com:443",
   410  					channelCreds:  []ChannelCreds{{Type: "insecure"}},
   411  					selectedCreds: ChannelCreds{Type: "insecure"},
   412  				}},
   413  				node: node{
   414  					userAgentName:        gRPCUserAgentName,
   415  					userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version},
   416  					clientFeatures:       []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper},
   417  				},
   418  				clientDefaultListenerResourceNameTemplate: "%s",
   419  			},
   420  		},
   421  		{"unknownTopLevelFieldInFile", configWithInsecureCreds},
   422  		{"unknownFieldInNodeProto", configWithInsecureCreds},
   423  		{"unknownFieldInXdsServer", configWithInsecureCreds},
   424  		{"multipleChannelCreds", configWithMultipleChannelCredsAndV3},
   425  		{"goodBootstrap", configWithGoogleDefaultCredsAndV3},
   426  		{"multipleXDSServers", configWithMultipleServers},
   427  		{"serverSupportsIgnoreResourceDeletion", configWithGoogleDefaultCredsAndIgnoreResourceDeletion},
   428  	}
   429  
   430  	for _, test := range tests {
   431  		t.Run(test.name, func(t *testing.T) {
   432  			testGetConfigurationWithFileNameEnv(t, test.name, false, test.wantConfig)
   433  			testGetConfigurationWithFileContentEnv(t, test.name, false, test.wantConfig)
   434  		})
   435  	}
   436  }
   437  
   438  // Tests that the two bootstrap env variables are read in correct priority.
   439  //
   440  // "GRPC_XDS_BOOTSTRAP" which specifies the file name containing the bootstrap
   441  // configuration takes precedence over "GRPC_XDS_BOOTSTRAP_CONFIG", which
   442  // directly specifies the bootstrap configuration in itself.
   443  func (s) TestGetConfiguration_BootstrapEnvPriority(t *testing.T) {
   444  	oldFileReadFunc := bootstrapFileReadFunc
   445  	bootstrapFileReadFunc = func(filename string) ([]byte, error) {
   446  		return fileReadFromFileMap(v3BootstrapFileMap, filename)
   447  	}
   448  	defer func() { bootstrapFileReadFunc = oldFileReadFunc }()
   449  
   450  	goodFileName1 := "serverFeaturesIncludesXDSV3"
   451  	goodConfig1 := configWithGoogleDefaultCredsAndV3
   452  
   453  	goodFileName2 := "serverFeaturesExcludesXDSV3"
   454  	goodFileContent2 := v3BootstrapFileMap[goodFileName2]
   455  	goodConfig2 := configWithGoogleDefaultCredsAndNoServerFeatures
   456  
   457  	origBootstrapFileName := envconfig.XDSBootstrapFileName
   458  	envconfig.XDSBootstrapFileName = ""
   459  	defer func() { envconfig.XDSBootstrapFileName = origBootstrapFileName }()
   460  
   461  	origBootstrapContent := envconfig.XDSBootstrapFileContent
   462  	envconfig.XDSBootstrapFileContent = ""
   463  	defer func() { envconfig.XDSBootstrapFileContent = origBootstrapContent }()
   464  
   465  	// When both env variables are empty, GetConfiguration should fail.
   466  	if _, err := GetConfiguration(); err == nil {
   467  		t.Errorf("GetConfiguration() returned nil error, expected to fail")
   468  	}
   469  
   470  	// When one of them is set, it should be used.
   471  	envconfig.XDSBootstrapFileName = goodFileName1
   472  	envconfig.XDSBootstrapFileContent = ""
   473  	c, err := GetConfiguration()
   474  	if err != nil {
   475  		t.Errorf("GetConfiguration() failed: %v", err)
   476  	}
   477  	if diff := cmp.Diff(goodConfig1, c); diff != "" {
   478  		t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff)
   479  	}
   480  
   481  	envconfig.XDSBootstrapFileName = ""
   482  	envconfig.XDSBootstrapFileContent = goodFileContent2
   483  	c, err = GetConfiguration()
   484  	if err != nil {
   485  		t.Errorf("GetConfiguration() failed: %v", err)
   486  	}
   487  	if diff := cmp.Diff(goodConfig2, c); diff != "" {
   488  		t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff)
   489  	}
   490  
   491  	// Set both, file name should be read.
   492  	envconfig.XDSBootstrapFileName = goodFileName1
   493  	envconfig.XDSBootstrapFileContent = goodFileContent2
   494  	c, err = GetConfiguration()
   495  	if err != nil {
   496  		t.Errorf("GetConfiguration() failed: %v", err)
   497  	}
   498  	if diff := cmp.Diff(goodConfig1, c); diff != "" {
   499  		t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff)
   500  	}
   501  }
   502  
   503  func init() {
   504  	certprovider.Register(&fakeCertProviderBuilder{})
   505  }
   506  
   507  const fakeCertProviderName = "fake-certificate-provider"
   508  
   509  // fakeCertProviderBuilder builds new instances of fakeCertProvider and
   510  // interprets the config provided to it as JSON with a single key and value.
   511  type fakeCertProviderBuilder struct{}
   512  
   513  // ParseConfig expects input in JSON format containing a map from string to
   514  // string, with a single entry and mapKey being "configKey".
   515  func (b *fakeCertProviderBuilder) ParseConfig(cfg any) (*certprovider.BuildableConfig, error) {
   516  	config, ok := cfg.(json.RawMessage)
   517  	if !ok {
   518  		return nil, fmt.Errorf("fakeCertProviderBuilder received config of type %T, want []byte", config)
   519  	}
   520  	var cfgData map[string]string
   521  	if err := json.Unmarshal(config, &cfgData); err != nil {
   522  		return nil, fmt.Errorf("fakeCertProviderBuilder config parsing failed: %v", err)
   523  	}
   524  	if len(cfgData) != 1 || cfgData["configKey"] == "" {
   525  		return nil, errors.New("fakeCertProviderBuilder received invalid config")
   526  	}
   527  	fc := &fakeStableConfig{config: cfgData}
   528  	return certprovider.NewBuildableConfig(fakeCertProviderName, fc.canonical(), func(certprovider.BuildOptions) certprovider.Provider {
   529  		return &fakeCertProvider{}
   530  	}), nil
   531  }
   532  
   533  func (b *fakeCertProviderBuilder) Name() string {
   534  	return fakeCertProviderName
   535  }
   536  
   537  type fakeStableConfig struct {
   538  	config map[string]string
   539  }
   540  
   541  func (c *fakeStableConfig) canonical() []byte {
   542  	var cfg string
   543  	for k, v := range c.config {
   544  		cfg = fmt.Sprintf("%s:%s", k, v)
   545  	}
   546  	return []byte(cfg)
   547  }
   548  
   549  // fakeCertProvider is an empty implementation of the Provider interface.
   550  type fakeCertProvider struct {
   551  	certprovider.Provider
   552  }
   553  
   554  func (s) TestGetConfiguration_CertificateProviders(t *testing.T) {
   555  	bootstrapFileMap := map[string]string{
   556  		"badJSONCertProviderConfig": `
   557  		{
   558  			"node": {
   559  				"id": "ENVOY_NODE_ID",
   560  				"metadata": {
   561  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   562  			    }
   563  			},
   564  			"xds_servers" : [{
   565  				"server_uri": "trafficdirector.googleapis.com:443",
   566  				"channel_creds": [
   567  					{ "type": "google_default" }
   568  				],
   569  				"server_features" : ["foo", "bar", "xds_v3"],
   570  			}],
   571  			"certificate_providers": "bad JSON"
   572  		}`,
   573  		"allUnknownCertProviders": `
   574  		{
   575  			"node": {
   576  				"id": "ENVOY_NODE_ID",
   577  				"metadata": {
   578  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   579  			    }
   580  			},
   581  			"xds_servers" : [{
   582  				"server_uri": "trafficdirector.googleapis.com:443",
   583  				"channel_creds": [
   584  					{ "type": "google_default" }
   585  				],
   586  				"server_features" : ["xds_v3"]
   587  			}],
   588  			"certificate_providers": {
   589  				"unknownProviderInstance1": {
   590  					"plugin_name": "foo",
   591  					"config": {"foo": "bar"}
   592  				},
   593  				"unknownProviderInstance2": {
   594  					"plugin_name": "bar",
   595  					"config": {"foo": "bar"}
   596  				}
   597  			}
   598  		}`,
   599  		"badCertProviderConfig": `
   600  		{
   601  			"node": {
   602  				"id": "ENVOY_NODE_ID",
   603  				"metadata": {
   604  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   605  			    }
   606  			},
   607  			"xds_servers" : [{
   608  				"server_uri": "trafficdirector.googleapis.com:443",
   609  				"channel_creds": [
   610  					{ "type": "google_default" }
   611  				],
   612  				"server_features" : ["xds_v3"],
   613  			}],
   614  			"certificate_providers": {
   615  				"unknownProviderInstance": {
   616  					"plugin_name": "foo",
   617  					"config": {"foo": "bar"}
   618  				},
   619  				"fakeProviderInstanceBad": {
   620  					"plugin_name": "fake-certificate-provider",
   621  					"config": {"configKey": 666}
   622  				}
   623  			}
   624  		}`,
   625  		"goodCertProviderConfig": `
   626  		{
   627  			"node": {
   628  				"id": "ENVOY_NODE_ID",
   629  				"metadata": {
   630  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   631  			    }
   632  			},
   633  			"xds_servers" : [{
   634  				"server_uri": "trafficdirector.googleapis.com:443",
   635  				"channel_creds": [
   636  					{ "type": "insecure" }
   637  				],
   638  				"server_features" : ["xds_v3"]
   639  			}],
   640  			"certificate_providers": {
   641  				"unknownProviderInstance": {
   642  					"plugin_name": "foo",
   643  					"config": {"foo": "bar"}
   644  				},
   645  				"fakeProviderInstance": {
   646  					"plugin_name": "fake-certificate-provider",
   647  					"config": {"configKey": "configValue"}
   648  				}
   649  			}
   650  		}`,
   651  	}
   652  
   653  	getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder)
   654  	parser := getBuilder(fakeCertProviderName)
   655  	if parser == nil {
   656  		t.Fatalf("Missing certprovider plugin %q", fakeCertProviderName)
   657  	}
   658  	wantCfg, err := parser.ParseConfig(json.RawMessage(`{"configKey": "configValue"}`))
   659  	if err != nil {
   660  		t.Fatalf("config parsing for plugin %q failed: %v", fakeCertProviderName, err)
   661  	}
   662  
   663  	cancel := setupBootstrapOverride(bootstrapFileMap)
   664  	defer cancel()
   665  
   666  	goodConfig := &Config{
   667  		xDSServers: []*ServerConfig{{
   668  			serverURI:      "trafficdirector.googleapis.com:443",
   669  			channelCreds:   []ChannelCreds{{Type: "insecure"}},
   670  			serverFeatures: []string{"xds_v3"},
   671  			selectedCreds:  ChannelCreds{Type: "insecure"},
   672  		}},
   673  		certProviderConfigs: map[string]*certprovider.BuildableConfig{
   674  			"fakeProviderInstance": wantCfg,
   675  		},
   676  		clientDefaultListenerResourceNameTemplate: "%s",
   677  		node: v3Node,
   678  	}
   679  	tests := []struct {
   680  		name       string
   681  		wantConfig *Config
   682  		wantErr    bool
   683  	}{
   684  		{
   685  			name:    "badJSONCertProviderConfig",
   686  			wantErr: true,
   687  		},
   688  		{
   689  
   690  			name:    "badCertProviderConfig",
   691  			wantErr: true,
   692  		},
   693  		{
   694  
   695  			name:       "allUnknownCertProviders",
   696  			wantConfig: configWithGoogleDefaultCredsAndV3,
   697  		},
   698  		{
   699  			name:       "goodCertProviderConfig",
   700  			wantConfig: goodConfig,
   701  		},
   702  	}
   703  
   704  	for _, test := range tests {
   705  		t.Run(test.name, func(t *testing.T) {
   706  			testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
   707  			testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
   708  		})
   709  	}
   710  }
   711  
   712  func (s) TestGetConfiguration_ServerListenerResourceNameTemplate(t *testing.T) {
   713  	cancel := setupBootstrapOverride(map[string]string{
   714  		"badServerListenerResourceNameTemplate:": `
   715  		{
   716  			"node": {
   717  				"id": "ENVOY_NODE_ID",
   718  				"metadata": {
   719  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   720  			    }
   721  			},
   722  			"xds_servers" : [{
   723  				"server_uri": "trafficdirector.googleapis.com:443",
   724  				"channel_creds": [
   725  					{ "type": "google_default" }
   726  				]
   727  			}],
   728  			"server_listener_resource_name_template": 123456789
   729  		}`,
   730  		"goodServerListenerResourceNameTemplate": `
   731  		{
   732  			"node": {
   733  				"id": "ENVOY_NODE_ID",
   734  				"metadata": {
   735  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   736  			    }
   737  			},
   738  			"xds_servers" : [{
   739  				"server_uri": "trafficdirector.googleapis.com:443",
   740  				"channel_creds": [
   741  					{ "type": "google_default" }
   742  				]
   743  			}],
   744  			"server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s"
   745  		}`,
   746  	})
   747  	defer cancel()
   748  
   749  	tests := []struct {
   750  		name       string
   751  		wantConfig *Config
   752  		wantErr    bool
   753  	}{
   754  		{
   755  			name:    "badServerListenerResourceNameTemplate",
   756  			wantErr: true,
   757  		},
   758  		{
   759  			name: "goodServerListenerResourceNameTemplate",
   760  			wantConfig: &Config{
   761  				xDSServers: []*ServerConfig{{
   762  					serverURI:     "trafficdirector.googleapis.com:443",
   763  					channelCreds:  []ChannelCreds{{Type: "google_default"}},
   764  					selectedCreds: ChannelCreds{Type: "google_default"},
   765  				}},
   766  				node:                               v3Node,
   767  				serverListenerResourceNameTemplate: "grpc/server?xds.resource.listening_address=%s",
   768  				clientDefaultListenerResourceNameTemplate: "%s",
   769  			},
   770  		},
   771  	}
   772  
   773  	for _, test := range tests {
   774  		t.Run(test.name, func(t *testing.T) {
   775  			testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
   776  			testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
   777  		})
   778  	}
   779  }
   780  
   781  func (s) TestGetConfiguration_Federation(t *testing.T) {
   782  	cancel := setupBootstrapOverride(map[string]string{
   783  		"badclientListenerResourceNameTemplate": `
   784  		{
   785  			"node": { "id": "ENVOY_NODE_ID" },
   786  			"xds_servers" : [{
   787  				"server_uri": "trafficdirector.googleapis.com:443"
   788  			}],
   789  			"client_default_listener_resource_name_template": 123456789
   790  		}`,
   791  		"badclientListenerResourceNameTemplatePerAuthority": `
   792  		{
   793  			"node": { "id": "ENVOY_NODE_ID" },
   794  			"xds_servers" : [{
   795  				"server_uri": "trafficdirector.googleapis.com:443",
   796  				"channel_creds": [ { "type": "google_default" } ]
   797  			}],
   798  			"authorities": {
   799  				"xds.td.com": {
   800  					"client_listener_resource_name_template": "some/template/%s",
   801  					"xds_servers": [{
   802  						"server_uri": "td.com",
   803  						"channel_creds": [ { "type": "google_default" } ],
   804  						"server_features" : ["foo", "bar", "xds_v3"]
   805  					}]
   806  				}
   807  			}
   808  		}`,
   809  		"good": `
   810  		{
   811  			"node": {
   812  				"id": "ENVOY_NODE_ID",
   813  				"metadata": {
   814  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   815  			    }
   816  			},
   817  			"xds_servers" : [{
   818  				"server_uri": "trafficdirector.googleapis.com:443",
   819  				"channel_creds": [ { "type": "google_default" } ]
   820  			}],
   821  			"server_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/grpc/server?listening_address=%s",
   822  			"client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   823  			"authorities": {
   824  				"xds.td.com": {
   825  					"client_listener_resource_name_template": "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s",
   826  					"xds_servers": [{
   827  						"server_uri": "td.com",
   828  						"channel_creds": [ { "type": "google_default" } ],
   829  						"server_features" : ["xds_v3"]
   830  					}]
   831  				}
   832  			}
   833  		}`,
   834  		// If client_default_listener_resource_name_template is not set, it
   835  		// defaults to "%s".
   836  		"goodWithDefaultDefaultClientListenerTemplate": `
   837  		{
   838  			"node": {
   839  				"id": "ENVOY_NODE_ID",
   840  				"metadata": {
   841  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   842  			    }
   843  			},
   844  			"xds_servers" : [{
   845  				"server_uri": "trafficdirector.googleapis.com:443",
   846  				"channel_creds": [ { "type": "google_default" } ]
   847  			}]
   848  		}`,
   849  		// If client_listener_resource_name_template in authority is not set, it
   850  		// defaults to
   851  		// "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s".
   852  		"goodWithDefaultClientListenerTemplatePerAuthority": `
   853  		{
   854  			"node": {
   855  				"id": "ENVOY_NODE_ID",
   856  				"metadata": {
   857  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   858  			    }
   859  			},
   860  			"xds_servers" : [{
   861  				"server_uri": "trafficdirector.googleapis.com:443",
   862  				"channel_creds": [ { "type": "google_default" } ]
   863  			}],
   864  			"client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   865  			"authorities": {
   866  				"xds.td.com": { },
   867  				"#.com": { }
   868  			}
   869  		}`,
   870  		// It's OK for an authority to not have servers. The top-level server
   871  		// will be used.
   872  		"goodWithNoServerPerAuthority": `
   873  		{
   874  			"node": {
   875  				"id": "ENVOY_NODE_ID",
   876  				"metadata": {
   877  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   878  			    }
   879  			},
   880  			"xds_servers" : [{
   881  				"server_uri": "trafficdirector.googleapis.com:443",
   882  				"channel_creds": [ { "type": "google_default" } ]
   883  			}],
   884  			"client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   885  			"authorities": {
   886  				"xds.td.com": {
   887  					"client_listener_resource_name_template": "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s"
   888  				}
   889  			}
   890  		}`,
   891  	})
   892  	defer cancel()
   893  
   894  	tests := []struct {
   895  		name       string
   896  		wantConfig *Config
   897  		wantErr    bool
   898  	}{
   899  		{
   900  			name:    "badclientListenerResourceNameTemplate",
   901  			wantErr: true,
   902  		},
   903  		{
   904  			name:    "badclientListenerResourceNameTemplatePerAuthority",
   905  			wantErr: true,
   906  		},
   907  		{
   908  			name: "good",
   909  			wantConfig: &Config{
   910  				xDSServers: []*ServerConfig{{
   911  					serverURI:     "trafficdirector.googleapis.com:443",
   912  					channelCreds:  []ChannelCreds{{Type: "google_default"}},
   913  					selectedCreds: ChannelCreds{Type: "google_default"},
   914  				}},
   915  				node:                               v3Node,
   916  				serverListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/grpc/server?listening_address=%s",
   917  				clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   918  				authorities: map[string]*Authority{
   919  					"xds.td.com": {
   920  						ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s",
   921  						XDSServers: []*ServerConfig{{
   922  							serverURI:      "td.com",
   923  							channelCreds:   []ChannelCreds{{Type: "google_default"}},
   924  							serverFeatures: []string{"xds_v3"},
   925  							selectedCreds:  ChannelCreds{Type: "google_default"},
   926  						}},
   927  					},
   928  				},
   929  			},
   930  		},
   931  		{
   932  			name: "goodWithDefaultDefaultClientListenerTemplate",
   933  			wantConfig: &Config{
   934  				xDSServers: []*ServerConfig{{
   935  					serverURI:     "trafficdirector.googleapis.com:443",
   936  					channelCreds:  []ChannelCreds{{Type: "google_default"}},
   937  					selectedCreds: ChannelCreds{Type: "google_default"},
   938  				}},
   939  				node: v3Node,
   940  				clientDefaultListenerResourceNameTemplate: "%s",
   941  			},
   942  		},
   943  		{
   944  			name: "goodWithDefaultClientListenerTemplatePerAuthority",
   945  			wantConfig: &Config{
   946  				xDSServers: []*ServerConfig{{
   947  					serverURI:     "trafficdirector.googleapis.com:443",
   948  					channelCreds:  []ChannelCreds{{Type: "google_default"}},
   949  					selectedCreds: ChannelCreds{Type: "google_default"},
   950  				}},
   951  				node: v3Node,
   952  				clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   953  				authorities: map[string]*Authority{
   954  					"xds.td.com": {
   955  						ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s",
   956  					},
   957  					"#.com": {
   958  						ClientListenerResourceNameTemplate: "xdstp://%23.com/envoy.config.listener.v3.Listener/%s",
   959  					},
   960  				},
   961  			},
   962  		},
   963  		{
   964  			name: "goodWithNoServerPerAuthority",
   965  			wantConfig: &Config{
   966  				xDSServers: []*ServerConfig{{
   967  					serverURI:     "trafficdirector.googleapis.com:443",
   968  					channelCreds:  []ChannelCreds{{Type: "google_default"}},
   969  					selectedCreds: ChannelCreds{Type: "google_default"},
   970  				}},
   971  				node: v3Node,
   972  				clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   973  				authorities: map[string]*Authority{
   974  					"xds.td.com": {
   975  						ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s",
   976  					},
   977  				},
   978  			},
   979  		},
   980  	}
   981  
   982  	for _, test := range tests {
   983  		t.Run(test.name, func(t *testing.T) {
   984  			testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
   985  			testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
   986  		})
   987  	}
   988  }
   989  
   990  func (s) TestServerConfigMarshalAndUnmarshal(t *testing.T) {
   991  	origConfig, err := ServerConfigForTesting(ServerConfigTestingOptions{URI: "test-server", ServerFeatures: []string{"xds_v3"}})
   992  	if err != nil {
   993  		t.Fatalf("Failed to create server config for testing: %v", err)
   994  	}
   995  	marshaledCfg, err := json.Marshal(origConfig)
   996  	if err != nil {
   997  		t.Fatalf("failed to marshal: %v", err)
   998  	}
   999  
  1000  	unmarshaledConfig := new(ServerConfig)
  1001  	if err := json.Unmarshal(marshaledCfg, unmarshaledConfig); err != nil {
  1002  		t.Fatalf("failed to unmarshal: %v", err)
  1003  	}
  1004  	if diff := cmp.Diff(origConfig, unmarshaledConfig); diff != "" {
  1005  		t.Fatalf("Unexpected diff in server config (-want, +got):\n%s", diff)
  1006  	}
  1007  }
  1008  
  1009  func (s) TestDefaultBundles(t *testing.T) {
  1010  	tests := []string{"google_default", "insecure", "tls"}
  1011  
  1012  	for _, typename := range tests {
  1013  		t.Run(typename, func(t *testing.T) {
  1014  			if c := bootstrap.GetCredentials(typename); c == nil {
  1015  				t.Errorf(`bootstrap.GetCredentials(%s) credential is nil, want non-nil`, typename)
  1016  			}
  1017  		})
  1018  	}
  1019  }
  1020  
  1021  type s struct {
  1022  	grpctest.Tester
  1023  }
  1024  
  1025  func Test(t *testing.T) {
  1026  	grpctest.RunSubTests(t, s{})
  1027  }
  1028  
  1029  func newStructProtoFromMap(t *testing.T, input map[string]any) *structpb.Struct {
  1030  	t.Helper()
  1031  
  1032  	ret, err := structpb.NewStruct(input)
  1033  	if err != nil {
  1034  		t.Fatalf("Failed to create new struct proto from map %v: %v", input, err)
  1035  	}
  1036  	return ret
  1037  }
  1038  
  1039  func (s) TestNode_MarshalAndUnmarshal(t *testing.T) {
  1040  	tests := []struct {
  1041  		desc      string
  1042  		inputJSON []byte
  1043  		wantNode  node
  1044  	}{
  1045  		{
  1046  			desc: "basic happy case",
  1047  			inputJSON: []byte(`{
  1048    "id": "id",
  1049    "cluster": "cluster",
  1050    "locality": {
  1051      "region": "region",
  1052      "zone": "zone",
  1053      "sub_zone": "sub_zone"
  1054    },
  1055    "metadata": {
  1056  	"k1": "v1",
  1057  	"k2": 101,
  1058  	"k3": 280.0
  1059    }
  1060  }`),
  1061  			wantNode: node{
  1062  				ID:      "id",
  1063  				Cluster: "cluster",
  1064  				Locality: locality{
  1065  					Region:  "region",
  1066  					Zone:    "zone",
  1067  					SubZone: "sub_zone",
  1068  				},
  1069  				Metadata: newStructProtoFromMap(t, map[string]any{
  1070  					"k1": "v1",
  1071  					"k2": 101,
  1072  					"k3": 280.0,
  1073  				}),
  1074  				userAgentName:        "gRPC Go",
  1075  				userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version},
  1076  				clientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
  1077  			},
  1078  		},
  1079  		{
  1080  			desc: "client controlled fields",
  1081  			inputJSON: []byte(`{
  1082    "id": "id",
  1083    "cluster": "cluster",
  1084    "user_agent_name": "user_agent_name",
  1085    "user_agent_version_type": {
  1086  	"user_agent_version": "version"
  1087    },
  1088    "client_features": ["feature1", "feature2"]
  1089  }`),
  1090  			wantNode: node{
  1091  				ID:                   "id",
  1092  				Cluster:              "cluster",
  1093  				userAgentName:        "gRPC Go",
  1094  				userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version},
  1095  				clientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
  1096  			},
  1097  		},
  1098  	}
  1099  
  1100  	for _, test := range tests {
  1101  		t.Run(test.desc, func(t *testing.T) {
  1102  			// Unmarshal the input JSON into a node struct and check if it
  1103  			// matches expectations.
  1104  			unmarshaledNode := newNode()
  1105  			if err := json.Unmarshal([]byte(test.inputJSON), &unmarshaledNode); err != nil {
  1106  				t.Fatal(err)
  1107  			}
  1108  			if diff := cmp.Diff(test.wantNode, unmarshaledNode); diff != "" {
  1109  				t.Fatalf("Unexpected diff in node: (-want, +got):\n%s", diff)
  1110  			}
  1111  
  1112  			// Marshal the recently unmarshaled node struct into JSON and
  1113  			// remarshal it into another node struct, and check that it still
  1114  			// matches expectations.
  1115  			marshaledJSON, err := json.Marshal(unmarshaledNode)
  1116  			if err != nil {
  1117  				t.Fatalf("node.MarshalJSON() failed: %v", err)
  1118  			}
  1119  			reUnmarshaledNode := newNode()
  1120  			if err := json.Unmarshal([]byte(marshaledJSON), &reUnmarshaledNode); err != nil {
  1121  				t.Fatal(err)
  1122  			}
  1123  			if diff := cmp.Diff(test.wantNode, reUnmarshaledNode); diff != "" {
  1124  				t.Fatalf("Unexpected diff in node: (-want, +got):\n%s", diff)
  1125  			}
  1126  		})
  1127  	}
  1128  }
  1129  
  1130  func (s) TestNode_ToProto(t *testing.T) {
  1131  	tests := []struct {
  1132  		desc      string
  1133  		inputNode node
  1134  		wantProto *v3corepb.Node
  1135  	}{
  1136  		{
  1137  			desc: "all fields set",
  1138  			inputNode: func() node {
  1139  				n := newNode()
  1140  				n.ID = "id"
  1141  				n.Cluster = "cluster"
  1142  				n.Locality = locality{
  1143  					Region:  "region",
  1144  					Zone:    "zone",
  1145  					SubZone: "sub_zone",
  1146  				}
  1147  				n.Metadata = newStructProtoFromMap(t, map[string]any{
  1148  					"k1": "v1",
  1149  					"k2": 101,
  1150  					"k3": 280.0,
  1151  				})
  1152  				return n
  1153  			}(),
  1154  			wantProto: &v3corepb.Node{
  1155  				Id:      "id",
  1156  				Cluster: "cluster",
  1157  				Locality: &v3corepb.Locality{
  1158  					Region:  "region",
  1159  					Zone:    "zone",
  1160  					SubZone: "sub_zone",
  1161  				},
  1162  				Metadata: newStructProtoFromMap(t, map[string]any{
  1163  					"k1": "v1",
  1164  					"k2": 101,
  1165  					"k3": 280.0,
  1166  				}),
  1167  				UserAgentName:        "gRPC Go",
  1168  				UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
  1169  				ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
  1170  			},
  1171  		},
  1172  		{
  1173  			desc: "some fields unset",
  1174  			inputNode: func() node {
  1175  				n := newNode()
  1176  				n.ID = "id"
  1177  				return n
  1178  			}(),
  1179  			wantProto: &v3corepb.Node{
  1180  				Id:                   "id",
  1181  				UserAgentName:        "gRPC Go",
  1182  				UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
  1183  				ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
  1184  			},
  1185  		},
  1186  	}
  1187  
  1188  	for _, test := range tests {
  1189  		t.Run(test.desc, func(t *testing.T) {
  1190  			gotProto := test.inputNode.toProto()
  1191  			if diff := cmp.Diff(test.wantProto, gotProto, protocmp.Transform()); diff != "" {
  1192  				t.Fatalf("Unexpected diff in node proto: (-want, +got):\n%s", diff)
  1193  			}
  1194  		})
  1195  	}
  1196  }
  1197  
  1198  // Tests the case where the xDS fallback env var is set to false, and verifies
  1199  // that only the first server from the list of server configurations is used.
  1200  func (s) TestGetConfiguration_FallbackDisabled(t *testing.T) {
  1201  	origFallbackEnv := envconfig.XDSFallbackSupport
  1202  	envconfig.XDSFallbackSupport = false
  1203  	defer func() { envconfig.XDSFallbackSupport = origFallbackEnv }()
  1204  
  1205  	cancel := setupBootstrapOverride(map[string]string{
  1206  		"multipleXDSServers": `
  1207  		{
  1208  			"node": {
  1209  				"id": "ENVOY_NODE_ID",
  1210  				"metadata": {
  1211  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
  1212  			    }
  1213  			},
  1214  			"xds_servers" : [
  1215  				{
  1216  					"server_uri": "trafficdirector.googleapis.com:443",
  1217  					"channel_creds": [{ "type": "google_default" }],
  1218  					"server_features": ["xds_v3"]
  1219  				},
  1220  				{
  1221  					"server_uri": "backup.never.use.com:1234",
  1222  					"channel_creds": [{ "type": "google_default" }]
  1223  				}
  1224  			],
  1225  			"authorities": {
  1226  				"xds.td.com": {
  1227  					"xds_servers": [
  1228  						{
  1229  							"server_uri": "td.com",
  1230  							"channel_creds": [ { "type": "google_default" } ],
  1231  							"server_features" : ["xds_v3"]
  1232  						},
  1233  						{
  1234  							"server_uri": "backup.never.use.com:1234",
  1235  							"channel_creds": [{ "type": "google_default" }]
  1236  						}
  1237  					]
  1238  				}
  1239  			}
  1240  		}`,
  1241  	})
  1242  	defer cancel()
  1243  
  1244  	wantConfig := &Config{
  1245  		xDSServers: []*ServerConfig{{
  1246  			serverURI:      "trafficdirector.googleapis.com:443",
  1247  			channelCreds:   []ChannelCreds{{Type: "google_default"}},
  1248  			serverFeatures: []string{"xds_v3"},
  1249  			selectedCreds:  ChannelCreds{Type: "google_default"},
  1250  		}},
  1251  		node: v3Node,
  1252  		clientDefaultListenerResourceNameTemplate: "%s",
  1253  		authorities: map[string]*Authority{
  1254  			"xds.td.com": {
  1255  				ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s",
  1256  				XDSServers: []*ServerConfig{{
  1257  					serverURI:      "td.com",
  1258  					channelCreds:   []ChannelCreds{{Type: "google_default"}},
  1259  					serverFeatures: []string{"xds_v3"},
  1260  					selectedCreds:  ChannelCreds{Type: "google_default"},
  1261  				}},
  1262  			},
  1263  		},
  1264  	}
  1265  	t.Run("bootstrap_file_name", func(t *testing.T) {
  1266  		testGetConfigurationWithFileNameEnv(t, "multipleXDSServers", false, wantConfig)
  1267  	})
  1268  	t.Run("bootstrap_file_contents", func(t *testing.T) {
  1269  		testGetConfigurationWithFileContentEnv(t, "multipleXDSServers", false, wantConfig)
  1270  	})
  1271  }