google.golang.org/grpc@v1.74.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", "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  	const name = "empty"
   393  	t.Run(name, func(t *testing.T) {
   394  		testGetConfigurationWithFileNameEnv(t, name, true, nil)
   395  		// If both the env vars are empty, a nil config with a nil error must be
   396  		// returned.
   397  		testGetConfigurationWithFileContentEnv(t, name, false, nil)
   398  	})
   399  }
   400  
   401  // Tests the functionality in GetConfiguration with different bootstrap file
   402  // contents. It overrides the fileReadFunc by returning bootstrap file contents
   403  // defined in this test, instead of reading from a file.
   404  func (s) TestGetConfiguration_Success(t *testing.T) {
   405  	cancel := setupBootstrapOverride(v3BootstrapFileMap)
   406  	defer cancel()
   407  
   408  	tests := []struct {
   409  		name       string
   410  		wantConfig *Config
   411  	}{
   412  		{
   413  			name: "emptyNodeProto",
   414  			wantConfig: &Config{
   415  				xDSServers: []*ServerConfig{{
   416  					serverURI:     "trafficdirector.googleapis.com:443",
   417  					channelCreds:  []ChannelCreds{{Type: "insecure"}},
   418  					selectedCreds: ChannelCreds{Type: "insecure"},
   419  				}},
   420  				node: node{
   421  					userAgentName:        gRPCUserAgentName,
   422  					userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version},
   423  					clientFeatures:       []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper},
   424  				},
   425  				clientDefaultListenerResourceNameTemplate: "%s",
   426  			},
   427  		},
   428  		{"unknownTopLevelFieldInFile", configWithInsecureCreds},
   429  		{"unknownFieldInNodeProto", configWithInsecureCreds},
   430  		{"unknownFieldInXdsServer", configWithInsecureCreds},
   431  		{"multipleChannelCreds", configWithMultipleChannelCredsAndV3},
   432  		{"goodBootstrap", configWithGoogleDefaultCredsAndV3},
   433  		{"multipleXDSServers", configWithMultipleServers},
   434  		{"serverSupportsIgnoreResourceDeletion", configWithGoogleDefaultCredsAndIgnoreResourceDeletion},
   435  	}
   436  
   437  	for _, test := range tests {
   438  		t.Run(test.name, func(t *testing.T) {
   439  			testGetConfigurationWithFileNameEnv(t, test.name, false, test.wantConfig)
   440  			testGetConfigurationWithFileContentEnv(t, test.name, false, test.wantConfig)
   441  		})
   442  	}
   443  }
   444  
   445  // Tests that the two bootstrap env variables are read in correct priority.
   446  //
   447  // "GRPC_XDS_BOOTSTRAP" which specifies the file name containing the bootstrap
   448  // configuration takes precedence over "GRPC_XDS_BOOTSTRAP_CONFIG", which
   449  // directly specifies the bootstrap configuration in itself.
   450  func (s) TestGetConfiguration_BootstrapEnvPriority(t *testing.T) {
   451  	oldFileReadFunc := bootstrapFileReadFunc
   452  	bootstrapFileReadFunc = func(filename string) ([]byte, error) {
   453  		return fileReadFromFileMap(v3BootstrapFileMap, filename)
   454  	}
   455  	defer func() { bootstrapFileReadFunc = oldFileReadFunc }()
   456  
   457  	goodFileName1 := "serverFeaturesIncludesXDSV3"
   458  	goodConfig1 := configWithGoogleDefaultCredsAndV3
   459  
   460  	goodFileName2 := "serverFeaturesExcludesXDSV3"
   461  	goodFileContent2 := v3BootstrapFileMap[goodFileName2]
   462  	goodConfig2 := configWithGoogleDefaultCredsAndNoServerFeatures
   463  
   464  	origBootstrapFileName := envconfig.XDSBootstrapFileName
   465  	envconfig.XDSBootstrapFileName = ""
   466  	defer func() { envconfig.XDSBootstrapFileName = origBootstrapFileName }()
   467  
   468  	origBootstrapContent := envconfig.XDSBootstrapFileContent
   469  	envconfig.XDSBootstrapFileContent = ""
   470  	defer func() { envconfig.XDSBootstrapFileContent = origBootstrapContent }()
   471  
   472  	// When both env variables are empty, GetConfiguration should return nil.
   473  	if cfg, err := GetConfiguration(); err != nil || cfg != nil {
   474  		t.Errorf("GetConfiguration() returned (%v, %v), want (<nil>, <nil>)", cfg, err)
   475  	}
   476  
   477  	// When one of them is set, it should be used.
   478  	envconfig.XDSBootstrapFileName = goodFileName1
   479  	envconfig.XDSBootstrapFileContent = ""
   480  	c, err := GetConfiguration()
   481  	if err != nil {
   482  		t.Errorf("GetConfiguration() failed: %v", err)
   483  	}
   484  	if diff := cmp.Diff(goodConfig1, c); diff != "" {
   485  		t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff)
   486  	}
   487  
   488  	envconfig.XDSBootstrapFileName = ""
   489  	envconfig.XDSBootstrapFileContent = goodFileContent2
   490  	c, err = GetConfiguration()
   491  	if err != nil {
   492  		t.Errorf("GetConfiguration() failed: %v", err)
   493  	}
   494  	if diff := cmp.Diff(goodConfig2, c); diff != "" {
   495  		t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff)
   496  	}
   497  
   498  	// Set both, file name should be read.
   499  	envconfig.XDSBootstrapFileName = goodFileName1
   500  	envconfig.XDSBootstrapFileContent = goodFileContent2
   501  	c, err = GetConfiguration()
   502  	if err != nil {
   503  		t.Errorf("GetConfiguration() failed: %v", err)
   504  	}
   505  	if diff := cmp.Diff(goodConfig1, c); diff != "" {
   506  		t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff)
   507  	}
   508  }
   509  
   510  func init() {
   511  	certprovider.Register(&fakeCertProviderBuilder{})
   512  }
   513  
   514  const fakeCertProviderName = "fake-certificate-provider"
   515  
   516  // fakeCertProviderBuilder builds new instances of fakeCertProvider and
   517  // interprets the config provided to it as JSON with a single key and value.
   518  type fakeCertProviderBuilder struct{}
   519  
   520  // ParseConfig expects input in JSON format containing a map from string to
   521  // string, with a single entry and mapKey being "configKey".
   522  func (b *fakeCertProviderBuilder) ParseConfig(cfg any) (*certprovider.BuildableConfig, error) {
   523  	config, ok := cfg.(json.RawMessage)
   524  	if !ok {
   525  		return nil, fmt.Errorf("fakeCertProviderBuilder received config of type %T, want []byte", config)
   526  	}
   527  	var cfgData map[string]string
   528  	if err := json.Unmarshal(config, &cfgData); err != nil {
   529  		return nil, fmt.Errorf("fakeCertProviderBuilder config parsing failed: %v", err)
   530  	}
   531  	if len(cfgData) != 1 || cfgData["configKey"] == "" {
   532  		return nil, errors.New("fakeCertProviderBuilder received invalid config")
   533  	}
   534  	fc := &fakeStableConfig{config: cfgData}
   535  	return certprovider.NewBuildableConfig(fakeCertProviderName, fc.canonical(), func(certprovider.BuildOptions) certprovider.Provider {
   536  		return &fakeCertProvider{}
   537  	}), nil
   538  }
   539  
   540  func (b *fakeCertProviderBuilder) Name() string {
   541  	return fakeCertProviderName
   542  }
   543  
   544  type fakeStableConfig struct {
   545  	config map[string]string
   546  }
   547  
   548  func (c *fakeStableConfig) canonical() []byte {
   549  	var cfg string
   550  	for k, v := range c.config {
   551  		cfg = fmt.Sprintf("%s:%s", k, v)
   552  	}
   553  	return []byte(cfg)
   554  }
   555  
   556  // fakeCertProvider is an empty implementation of the Provider interface.
   557  type fakeCertProvider struct {
   558  	certprovider.Provider
   559  }
   560  
   561  func (s) TestGetConfiguration_CertificateProviders(t *testing.T) {
   562  	bootstrapFileMap := map[string]string{
   563  		"badJSONCertProviderConfig": `
   564  		{
   565  			"node": {
   566  				"id": "ENVOY_NODE_ID",
   567  				"metadata": {
   568  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   569  			    }
   570  			},
   571  			"xds_servers" : [{
   572  				"server_uri": "trafficdirector.googleapis.com:443",
   573  				"channel_creds": [
   574  					{ "type": "google_default" }
   575  				],
   576  				"server_features" : ["foo", "bar", "xds_v3"],
   577  			}],
   578  			"certificate_providers": "bad JSON"
   579  		}`,
   580  		"allUnknownCertProviders": `
   581  		{
   582  			"node": {
   583  				"id": "ENVOY_NODE_ID",
   584  				"metadata": {
   585  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   586  			    }
   587  			},
   588  			"xds_servers" : [{
   589  				"server_uri": "trafficdirector.googleapis.com:443",
   590  				"channel_creds": [
   591  					{ "type": "google_default" }
   592  				],
   593  				"server_features" : ["xds_v3"]
   594  			}],
   595  			"certificate_providers": {
   596  				"unknownProviderInstance1": {
   597  					"plugin_name": "foo",
   598  					"config": {"foo": "bar"}
   599  				},
   600  				"unknownProviderInstance2": {
   601  					"plugin_name": "bar",
   602  					"config": {"foo": "bar"}
   603  				}
   604  			}
   605  		}`,
   606  		"badCertProviderConfig": `
   607  		{
   608  			"node": {
   609  				"id": "ENVOY_NODE_ID",
   610  				"metadata": {
   611  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   612  			    }
   613  			},
   614  			"xds_servers" : [{
   615  				"server_uri": "trafficdirector.googleapis.com:443",
   616  				"channel_creds": [
   617  					{ "type": "google_default" }
   618  				],
   619  				"server_features" : ["xds_v3"],
   620  			}],
   621  			"certificate_providers": {
   622  				"unknownProviderInstance": {
   623  					"plugin_name": "foo",
   624  					"config": {"foo": "bar"}
   625  				},
   626  				"fakeProviderInstanceBad": {
   627  					"plugin_name": "fake-certificate-provider",
   628  					"config": {"configKey": 666}
   629  				}
   630  			}
   631  		}`,
   632  		"goodCertProviderConfig": `
   633  		{
   634  			"node": {
   635  				"id": "ENVOY_NODE_ID",
   636  				"metadata": {
   637  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   638  			    }
   639  			},
   640  			"xds_servers" : [{
   641  				"server_uri": "trafficdirector.googleapis.com:443",
   642  				"channel_creds": [
   643  					{ "type": "insecure" }
   644  				],
   645  				"server_features" : ["xds_v3"]
   646  			}],
   647  			"certificate_providers": {
   648  				"unknownProviderInstance": {
   649  					"plugin_name": "foo",
   650  					"config": {"foo": "bar"}
   651  				},
   652  				"fakeProviderInstance": {
   653  					"plugin_name": "fake-certificate-provider",
   654  					"config": {"configKey": "configValue"}
   655  				}
   656  			}
   657  		}`,
   658  	}
   659  
   660  	getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder)
   661  	parser := getBuilder(fakeCertProviderName)
   662  	if parser == nil {
   663  		t.Fatalf("Missing certprovider plugin %q", fakeCertProviderName)
   664  	}
   665  	wantCfg, err := parser.ParseConfig(json.RawMessage(`{"configKey": "configValue"}`))
   666  	if err != nil {
   667  		t.Fatalf("config parsing for plugin %q failed: %v", fakeCertProviderName, err)
   668  	}
   669  
   670  	cancel := setupBootstrapOverride(bootstrapFileMap)
   671  	defer cancel()
   672  
   673  	goodConfig := &Config{
   674  		xDSServers: []*ServerConfig{{
   675  			serverURI:      "trafficdirector.googleapis.com:443",
   676  			channelCreds:   []ChannelCreds{{Type: "insecure"}},
   677  			serverFeatures: []string{"xds_v3"},
   678  			selectedCreds:  ChannelCreds{Type: "insecure"},
   679  		}},
   680  		certProviderConfigs: map[string]*certprovider.BuildableConfig{
   681  			"fakeProviderInstance": wantCfg,
   682  		},
   683  		clientDefaultListenerResourceNameTemplate: "%s",
   684  		node: v3Node,
   685  	}
   686  	tests := []struct {
   687  		name       string
   688  		wantConfig *Config
   689  		wantErr    bool
   690  	}{
   691  		{
   692  			name:    "badJSONCertProviderConfig",
   693  			wantErr: true,
   694  		},
   695  		{
   696  
   697  			name:    "badCertProviderConfig",
   698  			wantErr: true,
   699  		},
   700  		{
   701  
   702  			name:       "allUnknownCertProviders",
   703  			wantConfig: configWithGoogleDefaultCredsAndV3,
   704  		},
   705  		{
   706  			name:       "goodCertProviderConfig",
   707  			wantConfig: goodConfig,
   708  		},
   709  	}
   710  
   711  	for _, test := range tests {
   712  		t.Run(test.name, func(t *testing.T) {
   713  			testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
   714  			testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
   715  		})
   716  	}
   717  }
   718  
   719  func (s) TestGetConfiguration_ServerListenerResourceNameTemplate(t *testing.T) {
   720  	cancel := setupBootstrapOverride(map[string]string{
   721  		"badServerListenerResourceNameTemplate:": `
   722  		{
   723  			"node": {
   724  				"id": "ENVOY_NODE_ID",
   725  				"metadata": {
   726  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   727  			    }
   728  			},
   729  			"xds_servers" : [{
   730  				"server_uri": "trafficdirector.googleapis.com:443",
   731  				"channel_creds": [
   732  					{ "type": "google_default" }
   733  				]
   734  			}],
   735  			"server_listener_resource_name_template": 123456789
   736  		}`,
   737  		"goodServerListenerResourceNameTemplate": `
   738  		{
   739  			"node": {
   740  				"id": "ENVOY_NODE_ID",
   741  				"metadata": {
   742  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   743  			    }
   744  			},
   745  			"xds_servers" : [{
   746  				"server_uri": "trafficdirector.googleapis.com:443",
   747  				"channel_creds": [
   748  					{ "type": "google_default" }
   749  				]
   750  			}],
   751  			"server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s"
   752  		}`,
   753  	})
   754  	defer cancel()
   755  
   756  	tests := []struct {
   757  		name       string
   758  		wantConfig *Config
   759  		wantErr    bool
   760  	}{
   761  		{
   762  			name:    "badServerListenerResourceNameTemplate",
   763  			wantErr: true,
   764  		},
   765  		{
   766  			name: "goodServerListenerResourceNameTemplate",
   767  			wantConfig: &Config{
   768  				xDSServers: []*ServerConfig{{
   769  					serverURI:     "trafficdirector.googleapis.com:443",
   770  					channelCreds:  []ChannelCreds{{Type: "google_default"}},
   771  					selectedCreds: ChannelCreds{Type: "google_default"},
   772  				}},
   773  				node:                               v3Node,
   774  				serverListenerResourceNameTemplate: "grpc/server?xds.resource.listening_address=%s",
   775  				clientDefaultListenerResourceNameTemplate: "%s",
   776  			},
   777  		},
   778  	}
   779  
   780  	for _, test := range tests {
   781  		t.Run(test.name, func(t *testing.T) {
   782  			testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
   783  			testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
   784  		})
   785  	}
   786  }
   787  
   788  func (s) TestGetConfiguration_Federation(t *testing.T) {
   789  	cancel := setupBootstrapOverride(map[string]string{
   790  		"badclientListenerResourceNameTemplate": `
   791  		{
   792  			"node": { "id": "ENVOY_NODE_ID" },
   793  			"xds_servers" : [{
   794  				"server_uri": "trafficdirector.googleapis.com:443"
   795  			}],
   796  			"client_default_listener_resource_name_template": 123456789
   797  		}`,
   798  		"badclientListenerResourceNameTemplatePerAuthority": `
   799  		{
   800  			"node": { "id": "ENVOY_NODE_ID" },
   801  			"xds_servers" : [{
   802  				"server_uri": "trafficdirector.googleapis.com:443",
   803  				"channel_creds": [ { "type": "google_default" } ]
   804  			}],
   805  			"authorities": {
   806  				"xds.td.com": {
   807  					"client_listener_resource_name_template": "some/template/%s",
   808  					"xds_servers": [{
   809  						"server_uri": "td.com",
   810  						"channel_creds": [ { "type": "google_default" } ],
   811  						"server_features" : ["foo", "bar", "xds_v3"]
   812  					}]
   813  				}
   814  			}
   815  		}`,
   816  		"good": `
   817  		{
   818  			"node": {
   819  				"id": "ENVOY_NODE_ID",
   820  				"metadata": {
   821  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   822  			    }
   823  			},
   824  			"xds_servers" : [{
   825  				"server_uri": "trafficdirector.googleapis.com:443",
   826  				"channel_creds": [ { "type": "google_default" } ]
   827  			}],
   828  			"server_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/grpc/server?listening_address=%s",
   829  			"client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   830  			"authorities": {
   831  				"xds.td.com": {
   832  					"client_listener_resource_name_template": "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s",
   833  					"xds_servers": [{
   834  						"server_uri": "td.com",
   835  						"channel_creds": [ { "type": "google_default" } ],
   836  						"server_features" : ["xds_v3"]
   837  					}]
   838  				}
   839  			}
   840  		}`,
   841  		// If client_default_listener_resource_name_template is not set, it
   842  		// defaults to "%s".
   843  		"goodWithDefaultDefaultClientListenerTemplate": `
   844  		{
   845  			"node": {
   846  				"id": "ENVOY_NODE_ID",
   847  				"metadata": {
   848  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   849  			    }
   850  			},
   851  			"xds_servers" : [{
   852  				"server_uri": "trafficdirector.googleapis.com:443",
   853  				"channel_creds": [ { "type": "google_default" } ]
   854  			}]
   855  		}`,
   856  		// If client_listener_resource_name_template in authority is not set, it
   857  		// defaults to
   858  		// "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s".
   859  		"goodWithDefaultClientListenerTemplatePerAuthority": `
   860  		{
   861  			"node": {
   862  				"id": "ENVOY_NODE_ID",
   863  				"metadata": {
   864  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   865  			    }
   866  			},
   867  			"xds_servers" : [{
   868  				"server_uri": "trafficdirector.googleapis.com:443",
   869  				"channel_creds": [ { "type": "google_default" } ]
   870  			}],
   871  			"client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   872  			"authorities": {
   873  				"xds.td.com": { },
   874  				"#.com": { }
   875  			}
   876  		}`,
   877  		// It's OK for an authority to not have servers. The top-level server
   878  		// will be used.
   879  		"goodWithNoServerPerAuthority": `
   880  		{
   881  			"node": {
   882  				"id": "ENVOY_NODE_ID",
   883  				"metadata": {
   884  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
   885  			    }
   886  			},
   887  			"xds_servers" : [{
   888  				"server_uri": "trafficdirector.googleapis.com:443",
   889  				"channel_creds": [ { "type": "google_default" } ]
   890  			}],
   891  			"client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   892  			"authorities": {
   893  				"xds.td.com": {
   894  					"client_listener_resource_name_template": "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s"
   895  				}
   896  			}
   897  		}`,
   898  	})
   899  	defer cancel()
   900  
   901  	tests := []struct {
   902  		name       string
   903  		wantConfig *Config
   904  		wantErr    bool
   905  	}{
   906  		{
   907  			name:    "badclientListenerResourceNameTemplate",
   908  			wantErr: true,
   909  		},
   910  		{
   911  			name:    "badclientListenerResourceNameTemplatePerAuthority",
   912  			wantErr: true,
   913  		},
   914  		{
   915  			name: "good",
   916  			wantConfig: &Config{
   917  				xDSServers: []*ServerConfig{{
   918  					serverURI:     "trafficdirector.googleapis.com:443",
   919  					channelCreds:  []ChannelCreds{{Type: "google_default"}},
   920  					selectedCreds: ChannelCreds{Type: "google_default"},
   921  				}},
   922  				node:                               v3Node,
   923  				serverListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/grpc/server?listening_address=%s",
   924  				clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   925  				authorities: map[string]*Authority{
   926  					"xds.td.com": {
   927  						ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s",
   928  						XDSServers: []*ServerConfig{{
   929  							serverURI:      "td.com",
   930  							channelCreds:   []ChannelCreds{{Type: "google_default"}},
   931  							serverFeatures: []string{"xds_v3"},
   932  							selectedCreds:  ChannelCreds{Type: "google_default"},
   933  						}},
   934  					},
   935  				},
   936  			},
   937  		},
   938  		{
   939  			name: "goodWithDefaultDefaultClientListenerTemplate",
   940  			wantConfig: &Config{
   941  				xDSServers: []*ServerConfig{{
   942  					serverURI:     "trafficdirector.googleapis.com:443",
   943  					channelCreds:  []ChannelCreds{{Type: "google_default"}},
   944  					selectedCreds: ChannelCreds{Type: "google_default"},
   945  				}},
   946  				node: v3Node,
   947  				clientDefaultListenerResourceNameTemplate: "%s",
   948  			},
   949  		},
   950  		{
   951  			name: "goodWithDefaultClientListenerTemplatePerAuthority",
   952  			wantConfig: &Config{
   953  				xDSServers: []*ServerConfig{{
   954  					serverURI:     "trafficdirector.googleapis.com:443",
   955  					channelCreds:  []ChannelCreds{{Type: "google_default"}},
   956  					selectedCreds: ChannelCreds{Type: "google_default"},
   957  				}},
   958  				node: v3Node,
   959  				clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   960  				authorities: map[string]*Authority{
   961  					"xds.td.com": {
   962  						ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s",
   963  					},
   964  					"#.com": {
   965  						ClientListenerResourceNameTemplate: "xdstp://%23.com/envoy.config.listener.v3.Listener/%s",
   966  					},
   967  				},
   968  			},
   969  		},
   970  		{
   971  			name: "goodWithNoServerPerAuthority",
   972  			wantConfig: &Config{
   973  				xDSServers: []*ServerConfig{{
   974  					serverURI:     "trafficdirector.googleapis.com:443",
   975  					channelCreds:  []ChannelCreds{{Type: "google_default"}},
   976  					selectedCreds: ChannelCreds{Type: "google_default"},
   977  				}},
   978  				node: v3Node,
   979  				clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s",
   980  				authorities: map[string]*Authority{
   981  					"xds.td.com": {
   982  						ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s",
   983  					},
   984  				},
   985  			},
   986  		},
   987  	}
   988  
   989  	for _, test := range tests {
   990  		t.Run(test.name, func(t *testing.T) {
   991  			testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
   992  			testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
   993  		})
   994  	}
   995  }
   996  
   997  func (s) TestServerConfigMarshalAndUnmarshal(t *testing.T) {
   998  	origConfig, err := ServerConfigForTesting(ServerConfigTestingOptions{URI: "test-server", ServerFeatures: []string{"xds_v3"}})
   999  	if err != nil {
  1000  		t.Fatalf("Failed to create server config for testing: %v", err)
  1001  	}
  1002  	marshaledCfg, err := json.Marshal(origConfig)
  1003  	if err != nil {
  1004  		t.Fatalf("failed to marshal: %v", err)
  1005  	}
  1006  
  1007  	unmarshaledConfig := new(ServerConfig)
  1008  	if err := json.Unmarshal(marshaledCfg, unmarshaledConfig); err != nil {
  1009  		t.Fatalf("failed to unmarshal: %v", err)
  1010  	}
  1011  	if diff := cmp.Diff(origConfig, unmarshaledConfig); diff != "" {
  1012  		t.Fatalf("Unexpected diff in server config (-want, +got):\n%s", diff)
  1013  	}
  1014  }
  1015  
  1016  func (s) TestDefaultBundles(t *testing.T) {
  1017  	tests := []string{"google_default", "insecure", "tls"}
  1018  
  1019  	for _, typename := range tests {
  1020  		t.Run(typename, func(t *testing.T) {
  1021  			if c := bootstrap.GetCredentials(typename); c == nil {
  1022  				t.Errorf(`bootstrap.GetCredentials(%s) credential is nil, want non-nil`, typename)
  1023  			}
  1024  		})
  1025  	}
  1026  }
  1027  
  1028  type s struct {
  1029  	grpctest.Tester
  1030  }
  1031  
  1032  func Test(t *testing.T) {
  1033  	grpctest.RunSubTests(t, s{})
  1034  }
  1035  
  1036  func newStructProtoFromMap(t *testing.T, input map[string]any) *structpb.Struct {
  1037  	t.Helper()
  1038  
  1039  	ret, err := structpb.NewStruct(input)
  1040  	if err != nil {
  1041  		t.Fatalf("Failed to create new struct proto from map %v: %v", input, err)
  1042  	}
  1043  	return ret
  1044  }
  1045  
  1046  func (s) TestNode_MarshalAndUnmarshal(t *testing.T) {
  1047  	tests := []struct {
  1048  		desc      string
  1049  		inputJSON []byte
  1050  		wantNode  node
  1051  	}{
  1052  		{
  1053  			desc: "basic happy case",
  1054  			inputJSON: []byte(`{
  1055    "id": "id",
  1056    "cluster": "cluster",
  1057    "locality": {
  1058      "region": "region",
  1059      "zone": "zone",
  1060      "sub_zone": "sub_zone"
  1061    },
  1062    "metadata": {
  1063  	"k1": "v1",
  1064  	"k2": 101,
  1065  	"k3": 280.0
  1066    }
  1067  }`),
  1068  			wantNode: node{
  1069  				ID:      "id",
  1070  				Cluster: "cluster",
  1071  				Locality: locality{
  1072  					Region:  "region",
  1073  					Zone:    "zone",
  1074  					SubZone: "sub_zone",
  1075  				},
  1076  				Metadata: newStructProtoFromMap(t, map[string]any{
  1077  					"k1": "v1",
  1078  					"k2": 101,
  1079  					"k3": 280.0,
  1080  				}),
  1081  				userAgentName:        "gRPC Go",
  1082  				userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version},
  1083  				clientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
  1084  			},
  1085  		},
  1086  		{
  1087  			desc: "client controlled fields",
  1088  			inputJSON: []byte(`{
  1089    "id": "id",
  1090    "cluster": "cluster",
  1091    "user_agent_name": "user_agent_name",
  1092    "user_agent_version_type": {
  1093  	"user_agent_version": "version"
  1094    },
  1095    "client_features": ["feature1", "feature2"]
  1096  }`),
  1097  			wantNode: node{
  1098  				ID:                   "id",
  1099  				Cluster:              "cluster",
  1100  				userAgentName:        "gRPC Go",
  1101  				userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version},
  1102  				clientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
  1103  			},
  1104  		},
  1105  	}
  1106  
  1107  	for _, test := range tests {
  1108  		t.Run(test.desc, func(t *testing.T) {
  1109  			// Unmarshal the input JSON into a node struct and check if it
  1110  			// matches expectations.
  1111  			unmarshaledNode := newNode()
  1112  			if err := json.Unmarshal([]byte(test.inputJSON), &unmarshaledNode); err != nil {
  1113  				t.Fatal(err)
  1114  			}
  1115  			if diff := cmp.Diff(test.wantNode, unmarshaledNode); diff != "" {
  1116  				t.Fatalf("Unexpected diff in node: (-want, +got):\n%s", diff)
  1117  			}
  1118  
  1119  			// Marshal the recently unmarshaled node struct into JSON and
  1120  			// remarshal it into another node struct, and check that it still
  1121  			// matches expectations.
  1122  			marshaledJSON, err := json.Marshal(unmarshaledNode)
  1123  			if err != nil {
  1124  				t.Fatalf("node.MarshalJSON() failed: %v", err)
  1125  			}
  1126  			reUnmarshaledNode := newNode()
  1127  			if err := json.Unmarshal([]byte(marshaledJSON), &reUnmarshaledNode); err != nil {
  1128  				t.Fatal(err)
  1129  			}
  1130  			if diff := cmp.Diff(test.wantNode, reUnmarshaledNode); diff != "" {
  1131  				t.Fatalf("Unexpected diff in node: (-want, +got):\n%s", diff)
  1132  			}
  1133  		})
  1134  	}
  1135  }
  1136  
  1137  func (s) TestNode_ToProto(t *testing.T) {
  1138  	tests := []struct {
  1139  		desc      string
  1140  		inputNode node
  1141  		wantProto *v3corepb.Node
  1142  	}{
  1143  		{
  1144  			desc: "all fields set",
  1145  			inputNode: func() node {
  1146  				n := newNode()
  1147  				n.ID = "id"
  1148  				n.Cluster = "cluster"
  1149  				n.Locality = locality{
  1150  					Region:  "region",
  1151  					Zone:    "zone",
  1152  					SubZone: "sub_zone",
  1153  				}
  1154  				n.Metadata = newStructProtoFromMap(t, map[string]any{
  1155  					"k1": "v1",
  1156  					"k2": 101,
  1157  					"k3": 280.0,
  1158  				})
  1159  				return n
  1160  			}(),
  1161  			wantProto: &v3corepb.Node{
  1162  				Id:      "id",
  1163  				Cluster: "cluster",
  1164  				Locality: &v3corepb.Locality{
  1165  					Region:  "region",
  1166  					Zone:    "zone",
  1167  					SubZone: "sub_zone",
  1168  				},
  1169  				Metadata: newStructProtoFromMap(t, map[string]any{
  1170  					"k1": "v1",
  1171  					"k2": 101,
  1172  					"k3": 280.0,
  1173  				}),
  1174  				UserAgentName:        "gRPC Go",
  1175  				UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
  1176  				ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
  1177  			},
  1178  		},
  1179  		{
  1180  			desc: "some fields unset",
  1181  			inputNode: func() node {
  1182  				n := newNode()
  1183  				n.ID = "id"
  1184  				return n
  1185  			}(),
  1186  			wantProto: &v3corepb.Node{
  1187  				Id:                   "id",
  1188  				UserAgentName:        "gRPC Go",
  1189  				UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
  1190  				ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
  1191  			},
  1192  		},
  1193  	}
  1194  
  1195  	for _, test := range tests {
  1196  		t.Run(test.desc, func(t *testing.T) {
  1197  			gotProto := test.inputNode.toProto()
  1198  			if diff := cmp.Diff(test.wantProto, gotProto, protocmp.Transform()); diff != "" {
  1199  				t.Fatalf("Unexpected diff in node proto: (-want, +got):\n%s", diff)
  1200  			}
  1201  		})
  1202  	}
  1203  }
  1204  
  1205  // Tests the case where the xDS fallback env var is set to false, and verifies
  1206  // that only the first server from the list of server configurations is used.
  1207  func (s) TestGetConfiguration_FallbackDisabled(t *testing.T) {
  1208  	origFallbackEnv := envconfig.XDSFallbackSupport
  1209  	envconfig.XDSFallbackSupport = false
  1210  	defer func() { envconfig.XDSFallbackSupport = origFallbackEnv }()
  1211  
  1212  	cancel := setupBootstrapOverride(map[string]string{
  1213  		"multipleXDSServers": `
  1214  		{
  1215  			"node": {
  1216  				"id": "ENVOY_NODE_ID",
  1217  				"metadata": {
  1218  				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
  1219  			    }
  1220  			},
  1221  			"xds_servers" : [
  1222  				{
  1223  					"server_uri": "trafficdirector.googleapis.com:443",
  1224  					"channel_creds": [{ "type": "google_default" }],
  1225  					"server_features": ["xds_v3"]
  1226  				},
  1227  				{
  1228  					"server_uri": "backup.never.use.com:1234",
  1229  					"channel_creds": [{ "type": "google_default" }]
  1230  				}
  1231  			],
  1232  			"authorities": {
  1233  				"xds.td.com": {
  1234  					"xds_servers": [
  1235  						{
  1236  							"server_uri": "td.com",
  1237  							"channel_creds": [ { "type": "google_default" } ],
  1238  							"server_features" : ["xds_v3"]
  1239  						},
  1240  						{
  1241  							"server_uri": "backup.never.use.com:1234",
  1242  							"channel_creds": [{ "type": "google_default" }]
  1243  						}
  1244  					]
  1245  				}
  1246  			}
  1247  		}`,
  1248  	})
  1249  	defer cancel()
  1250  
  1251  	wantConfig := &Config{
  1252  		xDSServers: []*ServerConfig{{
  1253  			serverURI:      "trafficdirector.googleapis.com:443",
  1254  			channelCreds:   []ChannelCreds{{Type: "google_default"}},
  1255  			serverFeatures: []string{"xds_v3"},
  1256  			selectedCreds:  ChannelCreds{Type: "google_default"},
  1257  		}},
  1258  		node: v3Node,
  1259  		clientDefaultListenerResourceNameTemplate: "%s",
  1260  		authorities: map[string]*Authority{
  1261  			"xds.td.com": {
  1262  				ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s",
  1263  				XDSServers: []*ServerConfig{{
  1264  					serverURI:      "td.com",
  1265  					channelCreds:   []ChannelCreds{{Type: "google_default"}},
  1266  					serverFeatures: []string{"xds_v3"},
  1267  					selectedCreds:  ChannelCreds{Type: "google_default"},
  1268  				}},
  1269  			},
  1270  		},
  1271  	}
  1272  	t.Run("bootstrap_file_name", func(t *testing.T) {
  1273  		testGetConfigurationWithFileNameEnv(t, "multipleXDSServers", false, wantConfig)
  1274  	})
  1275  	t.Run("bootstrap_file_contents", func(t *testing.T) {
  1276  		testGetConfigurationWithFileContentEnv(t, "multipleXDSServers", false, wantConfig)
  1277  	})
  1278  }