google.golang.org/grpc@v1.62.1/xds/internal/xdsclient/bootstrap/bootstrap.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 provides the functionality to initialize certain aspects
    20  // of an xDS client by reading a bootstrap file.
    21  package bootstrap
    22  
    23  import (
    24  	"bytes"
    25  	"encoding/json"
    26  	"fmt"
    27  	"net/url"
    28  	"os"
    29  	"strings"
    30  
    31  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    32  	"github.com/golang/protobuf/jsonpb"
    33  	"google.golang.org/grpc"
    34  	"google.golang.org/grpc/credentials"
    35  	"google.golang.org/grpc/credentials/google"
    36  	"google.golang.org/grpc/credentials/insecure"
    37  	"google.golang.org/grpc/credentials/tls/certprovider"
    38  	"google.golang.org/grpc/internal"
    39  	"google.golang.org/grpc/internal/envconfig"
    40  	"google.golang.org/grpc/internal/pretty"
    41  	"google.golang.org/grpc/xds/bootstrap"
    42  	"google.golang.org/grpc/xds/internal/xdsclient/tlscreds"
    43  )
    44  
    45  const (
    46  	// The "server_features" field in the bootstrap file contains a list of
    47  	// features supported by the server:
    48  	// - A value of "xds_v3" indicates that the server supports the v3 version of
    49  	//   the xDS transport protocol.
    50  	// - A value of "ignore_resource_deletion" indicates that the client should
    51  	//   ignore deletion of Listener and Cluster resources in updates from the
    52  	//   server.
    53  	serverFeaturesV3                     = "xds_v3"
    54  	serverFeaturesIgnoreResourceDeletion = "ignore_resource_deletion"
    55  
    56  	gRPCUserAgentName               = "gRPC Go"
    57  	clientFeatureNoOverprovisioning = "envoy.lb.does_not_support_overprovisioning"
    58  	clientFeatureResourceWrapper    = "xds.config.resource-in-sotw"
    59  )
    60  
    61  func init() {
    62  	bootstrap.RegisterCredentials(&insecureCredsBuilder{})
    63  	bootstrap.RegisterCredentials(&googleDefaultCredsBuilder{})
    64  	bootstrap.RegisterCredentials(&tlsCredsBuilder{})
    65  }
    66  
    67  // For overriding in unit tests.
    68  var bootstrapFileReadFunc = os.ReadFile
    69  
    70  // insecureCredsBuilder implements the `Credentials` interface defined in
    71  // package `xds/bootstrap` and encapsulates an insecure credential.
    72  type insecureCredsBuilder struct{}
    73  
    74  func (i *insecureCredsBuilder) Build(json.RawMessage) (credentials.Bundle, func(), error) {
    75  	return insecure.NewBundle(), func() {}, nil
    76  }
    77  
    78  func (i *insecureCredsBuilder) Name() string {
    79  	return "insecure"
    80  }
    81  
    82  // tlsCredsBuilder implements the `Credentials` interface defined in
    83  // package `xds/bootstrap` and encapsulates a TLS credential.
    84  type tlsCredsBuilder struct{}
    85  
    86  func (t *tlsCredsBuilder) Build(config json.RawMessage) (credentials.Bundle, func(), error) {
    87  	return tlscreds.NewBundle(config)
    88  }
    89  
    90  func (t *tlsCredsBuilder) Name() string {
    91  	return "tls"
    92  }
    93  
    94  // googleDefaultCredsBuilder implements the `Credentials` interface defined in
    95  // package `xds/boostrap` and encapsulates a Google Default credential.
    96  type googleDefaultCredsBuilder struct{}
    97  
    98  func (d *googleDefaultCredsBuilder) Build(json.RawMessage) (credentials.Bundle, func(), error) {
    99  	return google.NewDefaultCredentials(), func() {}, nil
   100  }
   101  
   102  func (d *googleDefaultCredsBuilder) Name() string {
   103  	return "google_default"
   104  }
   105  
   106  // ChannelCreds contains the credentials to be used while communicating with an
   107  // xDS server. It is also used to dedup servers with the same server URI.
   108  type ChannelCreds struct {
   109  	// Type contains a unique name identifying the credentials type. The only
   110  	// supported types currently are "google_default" and "insecure".
   111  	Type string
   112  	// Config contains the JSON configuration associated with the credentials.
   113  	Config json.RawMessage
   114  }
   115  
   116  // Equal reports whether cc and other are considered equal.
   117  func (cc ChannelCreds) Equal(other ChannelCreds) bool {
   118  	return cc.Type == other.Type && bytes.Equal(cc.Config, other.Config)
   119  }
   120  
   121  // String returns a string representation of the credentials. It contains the
   122  // type and the config (if non-nil) separated by a "-".
   123  func (cc ChannelCreds) String() string {
   124  	if cc.Config == nil {
   125  		return cc.Type
   126  	}
   127  
   128  	// We do not expect the Marshal call to fail since we wrote to cc.Config
   129  	// after a successful unmarshaling from JSON configuration. Therefore,
   130  	// it is safe to ignore the error here.
   131  	b, _ := json.Marshal(cc.Config)
   132  	return cc.Type + "-" + string(b)
   133  }
   134  
   135  // ServerConfig contains the configuration to connect to a server, including
   136  // URI, creds, and transport API version (e.g. v2 or v3).
   137  //
   138  // It contains unexported fields that are initialized when unmarshaled from JSON
   139  // using either the UnmarshalJSON() method or the ServerConfigFromJSON()
   140  // function. Hence users are strongly encouraged not to use a literal struct
   141  // initialization to create an instance of this type, but instead unmarshal from
   142  // JSON using one of the two available options.
   143  type ServerConfig struct {
   144  	// ServerURI is the management server to connect to.
   145  	//
   146  	// The bootstrap file contains an ordered list of xDS servers to contact for
   147  	// this authority. The first one is picked.
   148  	ServerURI string
   149  	// Creds contains the credentials to be used while communicationg with this
   150  	// xDS server. It is also used to dedup servers with the same server URI.
   151  	Creds ChannelCreds
   152  	// ServerFeatures contains a list of features supported by this xDS server.
   153  	// It is also used to dedup servers with the same server URI and creds.
   154  	ServerFeatures []string
   155  
   156  	// As part of unmarshaling the JSON config into this struct, we ensure that
   157  	// the credentials config is valid by building an instance of the specified
   158  	// credentials and store it here as a grpc.DialOption for easy access when
   159  	// dialing this xDS server.
   160  	credsDialOption grpc.DialOption
   161  
   162  	// IgnoreResourceDeletion controls the behavior of the xDS client when the
   163  	// server deletes a previously sent Listener or Cluster resource. If set, the
   164  	// xDS client will not invoke the watchers' OnResourceDoesNotExist() method
   165  	// when a resource is deleted, nor will it remove the existing resource value
   166  	// from its cache.
   167  	IgnoreResourceDeletion bool
   168  
   169  	// Cleanups are called when the xDS client for this server is closed. Allows
   170  	// cleaning up resources created specifically for this ServerConfig.
   171  	Cleanups []func()
   172  }
   173  
   174  // CredsDialOption returns the configured credentials as a grpc dial option.
   175  func (sc *ServerConfig) CredsDialOption() grpc.DialOption {
   176  	return sc.credsDialOption
   177  }
   178  
   179  // String returns the string representation of the ServerConfig.
   180  //
   181  // This string representation will be used as map keys in federation
   182  // (`map[ServerConfig]authority`), so that the xDS ClientConn and stream will be
   183  // shared by authorities with different names but the same server config.
   184  //
   185  // It covers (almost) all the fields so the string can represent the config
   186  // content. It doesn't cover NodeProto because NodeProto isn't used by
   187  // federation.
   188  func (sc *ServerConfig) String() string {
   189  	features := strings.Join(sc.ServerFeatures, "-")
   190  	return strings.Join([]string{sc.ServerURI, sc.Creds.String(), features}, "-")
   191  }
   192  
   193  // MarshalJSON marshals the ServerConfig to json.
   194  func (sc ServerConfig) MarshalJSON() ([]byte, error) {
   195  	server := xdsServer{
   196  		ServerURI:      sc.ServerURI,
   197  		ChannelCreds:   []channelCreds{{Type: sc.Creds.Type, Config: sc.Creds.Config}},
   198  		ServerFeatures: sc.ServerFeatures,
   199  	}
   200  	server.ServerFeatures = []string{serverFeaturesV3}
   201  	if sc.IgnoreResourceDeletion {
   202  		server.ServerFeatures = append(server.ServerFeatures, serverFeaturesIgnoreResourceDeletion)
   203  	}
   204  	return json.Marshal(server)
   205  }
   206  
   207  // UnmarshalJSON takes the json data (a server) and unmarshals it to the struct.
   208  func (sc *ServerConfig) UnmarshalJSON(data []byte) error {
   209  	var server xdsServer
   210  	if err := json.Unmarshal(data, &server); err != nil {
   211  		return fmt.Errorf("xds: json.Unmarshal(data) for field ServerConfig failed during bootstrap: %v", err)
   212  	}
   213  
   214  	sc.ServerURI = server.ServerURI
   215  	sc.ServerFeatures = server.ServerFeatures
   216  	for _, f := range server.ServerFeatures {
   217  		if f == serverFeaturesIgnoreResourceDeletion {
   218  			sc.IgnoreResourceDeletion = true
   219  		}
   220  	}
   221  	for _, cc := range server.ChannelCreds {
   222  		// We stop at the first credential type that we support.
   223  		c := bootstrap.GetCredentials(cc.Type)
   224  		if c == nil {
   225  			continue
   226  		}
   227  		bundle, cancel, err := c.Build(cc.Config)
   228  		if err != nil {
   229  			return fmt.Errorf("failed to build credentials bundle from bootstrap for %q: %v", cc.Type, err)
   230  		}
   231  		sc.Creds = ChannelCreds(cc)
   232  		sc.credsDialOption = grpc.WithCredentialsBundle(bundle)
   233  		sc.Cleanups = append(sc.Cleanups, cancel)
   234  		break
   235  	}
   236  	return nil
   237  }
   238  
   239  // ServerConfigFromJSON creates a new ServerConfig from the given JSON
   240  // configuration. This is the preferred way of creating a ServerConfig when
   241  // hand-crafting the JSON configuration.
   242  func ServerConfigFromJSON(data []byte) (*ServerConfig, error) {
   243  	sc := new(ServerConfig)
   244  	if err := sc.UnmarshalJSON(data); err != nil {
   245  		return nil, err
   246  	}
   247  	return sc, nil
   248  }
   249  
   250  // Equal reports whether sc and other are considered equal.
   251  func (sc *ServerConfig) Equal(other *ServerConfig) bool {
   252  	switch {
   253  	case sc == nil && other == nil:
   254  		return true
   255  	case (sc != nil) != (other != nil):
   256  		return false
   257  	case sc.ServerURI != other.ServerURI:
   258  		return false
   259  	case !sc.Creds.Equal(other.Creds):
   260  		return false
   261  	case !equalStringSlice(sc.ServerFeatures, other.ServerFeatures):
   262  		return false
   263  	}
   264  	return true
   265  }
   266  
   267  func equalStringSlice(a, b []string) bool {
   268  	if len(a) != len(b) {
   269  		return false
   270  	}
   271  	for i := range a {
   272  		if a[i] != b[i] {
   273  			return false
   274  		}
   275  	}
   276  	return true
   277  }
   278  
   279  // unmarshalJSONServerConfigSlice unmarshals JSON to a slice.
   280  func unmarshalJSONServerConfigSlice(data []byte) ([]*ServerConfig, error) {
   281  	var servers []*ServerConfig
   282  	if err := json.Unmarshal(data, &servers); err != nil {
   283  		return nil, fmt.Errorf("failed to unmarshal JSON to []*ServerConfig: %v", err)
   284  	}
   285  	if len(servers) < 1 {
   286  		return nil, fmt.Errorf("no management server found in JSON")
   287  	}
   288  	return servers, nil
   289  }
   290  
   291  // Authority contains configuration for an Authority for an xDS control plane
   292  // server. See the Authorities field in the Config struct for how it's used.
   293  type Authority struct {
   294  	// ClientListenerResourceNameTemplate is template for the name of the
   295  	// Listener resource to subscribe to for a gRPC client channel.  Used only
   296  	// when the channel is created using an "xds:" URI with this authority name.
   297  	//
   298  	// The token "%s", if present in this string, will be replaced
   299  	// with %-encoded service authority (i.e., the path part of the target
   300  	// URI used to create the gRPC channel).
   301  	//
   302  	// Must start with "xdstp://<authority_name>/".  If it does not,
   303  	// that is considered a bootstrap file parsing error.
   304  	//
   305  	// If not present in the bootstrap file, defaults to
   306  	// "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s".
   307  	ClientListenerResourceNameTemplate string
   308  	// XDSServer contains the management server and config to connect to for
   309  	// this authority.
   310  	XDSServer *ServerConfig
   311  }
   312  
   313  // UnmarshalJSON implement json unmarshaller.
   314  func (a *Authority) UnmarshalJSON(data []byte) error {
   315  	var jsonData map[string]json.RawMessage
   316  	if err := json.Unmarshal(data, &jsonData); err != nil {
   317  		return fmt.Errorf("xds: failed to parse authority: %v", err)
   318  	}
   319  
   320  	for k, v := range jsonData {
   321  		switch k {
   322  		case "xds_servers":
   323  			servers, err := unmarshalJSONServerConfigSlice(v)
   324  			if err != nil {
   325  				return fmt.Errorf("xds: json.Unmarshal(data) for field %q failed during bootstrap: %v", k, err)
   326  			}
   327  			a.XDSServer = servers[0]
   328  		case "client_listener_resource_name_template":
   329  			if err := json.Unmarshal(v, &a.ClientListenerResourceNameTemplate); err != nil {
   330  				return fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   331  			}
   332  		}
   333  	}
   334  	return nil
   335  }
   336  
   337  // Config provides the xDS client with several key bits of information that it
   338  // requires in its interaction with the management server. The Config is
   339  // initialized from the bootstrap file.
   340  type Config struct {
   341  	// XDSServer is the management server to connect to.
   342  	//
   343  	// The bootstrap file contains a list of servers (with name+creds), but we
   344  	// pick the first one.
   345  	XDSServer *ServerConfig
   346  	// CertProviderConfigs contains a mapping from certificate provider plugin
   347  	// instance names to parsed buildable configs.
   348  	CertProviderConfigs map[string]*certprovider.BuildableConfig
   349  	// ServerListenerResourceNameTemplate is a template for the name of the
   350  	// Listener resource to subscribe to for a gRPC server.
   351  	//
   352  	// If starts with "xdstp:", will be interpreted as a new-style name,
   353  	// in which case the authority of the URI will be used to select the
   354  	// relevant configuration in the "authorities" map.
   355  	//
   356  	// The token "%s", if present in this string, will be replaced with the IP
   357  	// and port on which the server is listening.  (e.g., "0.0.0.0:8080",
   358  	// "[::]:8080"). For example, a value of "example/resource/%s" could become
   359  	// "example/resource/0.0.0.0:8080". If the template starts with "xdstp:",
   360  	// the replaced string will be %-encoded.
   361  	//
   362  	// There is no default; if unset, xDS-based server creation fails.
   363  	ServerListenerResourceNameTemplate string
   364  	// A template for the name of the Listener resource to subscribe to
   365  	// for a gRPC client channel.  Used only when the channel is created
   366  	// with an "xds:" URI with no authority.
   367  	//
   368  	// If starts with "xdstp:", will be interpreted as a new-style name,
   369  	// in which case the authority of the URI will be used to select the
   370  	// relevant configuration in the "authorities" map.
   371  	//
   372  	// The token "%s", if present in this string, will be replaced with
   373  	// the service authority (i.e., the path part of the target URI
   374  	// used to create the gRPC channel).  If the template starts with
   375  	// "xdstp:", the replaced string will be %-encoded.
   376  	//
   377  	// Defaults to "%s".
   378  	ClientDefaultListenerResourceNameTemplate string
   379  	// Authorities is a map of authority name to corresponding configuration.
   380  	//
   381  	// This is used in the following cases:
   382  	// - A gRPC client channel is created using an "xds:" URI that includes
   383  	//   an authority.
   384  	// - A gRPC client channel is created using an "xds:" URI with no
   385  	//   authority, but the "client_default_listener_resource_name_template"
   386  	//   field above turns it into an "xdstp:" URI.
   387  	// - A gRPC server is created and the
   388  	//   "server_listener_resource_name_template" field is an "xdstp:" URI.
   389  	//
   390  	// In any of those cases, it is an error if the specified authority is
   391  	// not present in this map.
   392  	Authorities map[string]*Authority
   393  	// NodeProto contains the Node proto to be used in xDS requests. This will be
   394  	// of type *v3corepb.Node.
   395  	NodeProto *v3corepb.Node
   396  }
   397  
   398  type channelCreds struct {
   399  	Type   string          `json:"type"`
   400  	Config json.RawMessage `json:"config,omitempty"`
   401  }
   402  
   403  type xdsServer struct {
   404  	ServerURI      string         `json:"server_uri"`
   405  	ChannelCreds   []channelCreds `json:"channel_creds"`
   406  	ServerFeatures []string       `json:"server_features"`
   407  }
   408  
   409  func bootstrapConfigFromEnvVariable() ([]byte, error) {
   410  	fName := envconfig.XDSBootstrapFileName
   411  	fContent := envconfig.XDSBootstrapFileContent
   412  
   413  	// Bootstrap file name has higher priority than bootstrap content.
   414  	if fName != "" {
   415  		// If file name is set
   416  		// - If file not found (or other errors), fail
   417  		// - Otherwise, use the content.
   418  		//
   419  		// Note that even if the content is invalid, we don't failover to the
   420  		// file content env variable.
   421  		logger.Debugf("Using bootstrap file with name %q", fName)
   422  		return bootstrapFileReadFunc(fName)
   423  	}
   424  
   425  	if fContent != "" {
   426  		return []byte(fContent), nil
   427  	}
   428  
   429  	return nil, fmt.Errorf("none of the bootstrap environment variables (%q or %q) defined",
   430  		envconfig.XDSBootstrapFileNameEnv, envconfig.XDSBootstrapFileContentEnv)
   431  }
   432  
   433  // NewConfig returns a new instance of Config initialized by reading the
   434  // bootstrap file found at ${GRPC_XDS_BOOTSTRAP} or bootstrap contents specified
   435  // at ${GRPC_XDS_BOOTSTRAP_CONFIG}. If both env vars are set, the former is
   436  // preferred.
   437  //
   438  // We support a credential registration mechanism and only credentials
   439  // registered through that mechanism will be accepted here. See package
   440  // `xds/bootstrap` for details.
   441  //
   442  // This function tries to process as much of the bootstrap file as possible (in
   443  // the presence of the errors) and may return a Config object with certain
   444  // fields left unspecified, in which case the caller should use some sane
   445  // defaults.
   446  func NewConfig() (*Config, error) {
   447  	// Examples of the bootstrap json can be found in the generator tests
   448  	// https://github.com/GoogleCloudPlatform/traffic-director-grpc-bootstrap/blob/master/main_test.go.
   449  	data, err := bootstrapConfigFromEnvVariable()
   450  	if err != nil {
   451  		return nil, fmt.Errorf("xds: Failed to read bootstrap config: %v", err)
   452  	}
   453  	return newConfigFromContents(data)
   454  }
   455  
   456  // NewConfigFromContentsForTesting returns a new Config using the specified
   457  // bootstrap file contents instead of reading the environment variable.
   458  //
   459  // This is only suitable for testing purposes.
   460  func NewConfigFromContentsForTesting(data []byte) (*Config, error) {
   461  	return newConfigFromContents(data)
   462  }
   463  
   464  func newConfigFromContents(data []byte) (*Config, error) {
   465  	config := &Config{}
   466  
   467  	var jsonData map[string]json.RawMessage
   468  	if err := json.Unmarshal(data, &jsonData); err != nil {
   469  		return nil, fmt.Errorf("xds: failed to parse bootstrap config: %v", err)
   470  	}
   471  
   472  	var node *v3corepb.Node
   473  	m := jsonpb.Unmarshaler{AllowUnknownFields: true}
   474  	for k, v := range jsonData {
   475  		switch k {
   476  		case "node":
   477  			node = &v3corepb.Node{}
   478  			if err := m.Unmarshal(bytes.NewReader(v), node); err != nil {
   479  				return nil, fmt.Errorf("xds: jsonpb.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   480  			}
   481  		case "xds_servers":
   482  			servers, err := unmarshalJSONServerConfigSlice(v)
   483  			if err != nil {
   484  				return nil, fmt.Errorf("xds: json.Unmarshal(data) for field %q failed during bootstrap: %v", k, err)
   485  			}
   486  			config.XDSServer = servers[0]
   487  		case "certificate_providers":
   488  			var providerInstances map[string]json.RawMessage
   489  			if err := json.Unmarshal(v, &providerInstances); err != nil {
   490  				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   491  			}
   492  			configs := make(map[string]*certprovider.BuildableConfig)
   493  			getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder)
   494  			for instance, data := range providerInstances {
   495  				var nameAndConfig struct {
   496  					PluginName string          `json:"plugin_name"`
   497  					Config     json.RawMessage `json:"config"`
   498  				}
   499  				if err := json.Unmarshal(data, &nameAndConfig); err != nil {
   500  					return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), instance, err)
   501  				}
   502  
   503  				name := nameAndConfig.PluginName
   504  				parser := getBuilder(nameAndConfig.PluginName)
   505  				if parser == nil {
   506  					// We ignore plugins that we do not know about.
   507  					continue
   508  				}
   509  				bc, err := parser.ParseConfig(nameAndConfig.Config)
   510  				if err != nil {
   511  					return nil, fmt.Errorf("xds: config parsing for plugin %q failed: %v", name, err)
   512  				}
   513  				configs[instance] = bc
   514  			}
   515  			config.CertProviderConfigs = configs
   516  		case "server_listener_resource_name_template":
   517  			if err := json.Unmarshal(v, &config.ServerListenerResourceNameTemplate); err != nil {
   518  				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   519  			}
   520  		case "client_default_listener_resource_name_template":
   521  			if err := json.Unmarshal(v, &config.ClientDefaultListenerResourceNameTemplate); err != nil {
   522  				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   523  			}
   524  		case "authorities":
   525  			if err := json.Unmarshal(v, &config.Authorities); err != nil {
   526  				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   527  			}
   528  		default:
   529  			logger.Warningf("Bootstrap content has unknown field: %s", k)
   530  		}
   531  		// Do not fail the xDS bootstrap when an unknown field is seen. This can
   532  		// happen when an older version client reads a newer version bootstrap
   533  		// file with new fields.
   534  	}
   535  
   536  	if config.ClientDefaultListenerResourceNameTemplate == "" {
   537  		// Default value of the default client listener name template is "%s".
   538  		config.ClientDefaultListenerResourceNameTemplate = "%s"
   539  	}
   540  	if config.XDSServer == nil {
   541  		return nil, fmt.Errorf("xds: required field %q not found in bootstrap %s", "xds_servers", jsonData["xds_servers"])
   542  	}
   543  	if config.XDSServer.ServerURI == "" {
   544  		return nil, fmt.Errorf("xds: required field %q not found in bootstrap %s", "xds_servers.server_uri", jsonData["xds_servers"])
   545  	}
   546  	if config.XDSServer.CredsDialOption() == nil {
   547  		return nil, fmt.Errorf("xds: required field %q doesn't contain valid value in bootstrap %s", "xds_servers.channel_creds", jsonData["xds_servers"])
   548  	}
   549  	// Post-process the authorities' client listener resource template field:
   550  	// - if set, it must start with "xdstp://<authority_name>/"
   551  	// - if not set, it defaults to "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s"
   552  	for name, authority := range config.Authorities {
   553  		prefix := fmt.Sprintf("xdstp://%s", url.PathEscape(name))
   554  		if authority.ClientListenerResourceNameTemplate == "" {
   555  			authority.ClientListenerResourceNameTemplate = prefix + "/envoy.config.listener.v3.Listener/%s"
   556  			continue
   557  		}
   558  		if !strings.HasPrefix(authority.ClientListenerResourceNameTemplate, prefix) {
   559  			return nil, fmt.Errorf("xds: field ClientListenerResourceNameTemplate %q of authority %q doesn't start with prefix %q", authority.ClientListenerResourceNameTemplate, name, prefix)
   560  		}
   561  	}
   562  
   563  	// Performing post-production on the node information. Some additional fields
   564  	// which are not expected to be set in the bootstrap file are populated here.
   565  	if node == nil {
   566  		node = &v3corepb.Node{}
   567  	}
   568  	node.UserAgentName = gRPCUserAgentName
   569  	node.UserAgentVersionType = &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}
   570  	node.ClientFeatures = append(node.ClientFeatures, clientFeatureNoOverprovisioning, clientFeatureResourceWrapper)
   571  	config.NodeProto = node
   572  
   573  	logger.Debugf("Bootstrap config for creating xds-client: %v", pretty.ToJSON(config))
   574  	return config, nil
   575  }