dubbo.apache.org/dubbo-go/v3@v3.1.1/xds/client/bootstrap/bootstrap.go (about)

     1  /*
     2   * Licensed to the Apache Software Foundation (ASF) under one or more
     3   * contributor license agreements.  See the NOTICE file distributed with
     4   * this work for additional information regarding copyright ownership.
     5   * The ASF licenses this file to You under the Apache License, Version 2.0
     6   * (the "License"); you may not use this file except in compliance with
     7   * the License.  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   *
    20   * Copyright 2019 gRPC authors.
    21   *
    22   */
    23  
    24  // Package bootstrap provides the functionality to initialize certain aspects
    25  // of an xDS client by reading a bootstrap file.
    26  package bootstrap
    27  
    28  import (
    29  	"bytes"
    30  	"encoding/json"
    31  	"fmt"
    32  	"os"
    33  	"strings"
    34  
    35  	dubbogoLogger "github.com/dubbogo/gost/log/logger"
    36  
    37  	v2corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
    38  
    39  	"dubbo.apache.org/dubbo-go/v3/xds/client/resource/version"
    40  	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider"
    41  	"dubbo.apache.org/dubbo-go/v3/xds/internal"
    42  	"dubbo.apache.org/dubbo-go/v3/xds/utils/envconfig"
    43  	"dubbo.apache.org/dubbo-go/v3/xds/utils/pretty"
    44  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    45  	"github.com/golang/protobuf/jsonpb"
    46  	"github.com/golang/protobuf/proto"
    47  	"google.golang.org/grpc"
    48  	"google.golang.org/grpc/credentials/google"
    49  	"google.golang.org/grpc/credentials/insecure"
    50  )
    51  
    52  const (
    53  	// The "server_features" field in the bootstrap file contains a list of
    54  	// features supported by the server. A value of "xds_v3" indicates that the
    55  	// server supports the v3 version of the xDS transport protocol.
    56  	serverFeaturesV3 = "xds_v3"
    57  
    58  	// Type name for Google default credentials.
    59  	credsGoogleDefault              = "google_default"
    60  	credsInsecure                   = "insecure"
    61  	gRPCUserAgentName               = "gRPC Go"
    62  	clientFeatureNoOverprovisioning = "envoy.lb.does_not_support_overprovisioning"
    63  )
    64  
    65  var gRPCVersion = fmt.Sprintf("%s %s", gRPCUserAgentName, grpc.Version)
    66  
    67  // For overriding in unit tests.
    68  var bootstrapFileReadFunc = os.ReadFile
    69  
    70  // ServerConfig contains the configuration to connect to a server, including
    71  // URI, creds, and transport API version (e.g. v2 or v3).
    72  type ServerConfig struct {
    73  	// ServerURI is the management server to connect to.
    74  	//
    75  	// The bootstrap file contains an ordered list of xDS servers to contact for
    76  	// this authority. The first one is picked.
    77  	ServerURI string
    78  	// Creds contains the credentials to be used while talking to the xDS
    79  	// server, as a grpc.DialOption.
    80  	Creds grpc.DialOption
    81  	// CredsType is the type of the creds. It will be used to dedup servers.
    82  	CredsType string
    83  	// TransportAPI indicates the API version of xDS transport protocol to use.
    84  	// This describes the xDS gRPC endpoint and version of
    85  	// DiscoveryRequest/Response used on the wire.
    86  	TransportAPI version.TransportAPI
    87  	// NodeProto contains the Node proto to be used in xDS requests. The actual
    88  	// type depends on the transport protocol version used.
    89  	//
    90  	// Note that it's specified in the bootstrap globally for all the servers,
    91  	// but we keep it in each server config so that its type (e.g. *v2pb.Node or
    92  	// *v3pb.Node) is consistent with the transport API version.
    93  	NodeProto proto.Message
    94  }
    95  
    96  // String returns the string representation of the ServerConfig.
    97  //
    98  // This string representation will be used as map keys in federation
    99  // (`map[ServerConfig]authority`), so that the xDS ClientConn and stream will be
   100  // shared by authorities with different names but the same server config.
   101  //
   102  // It covers (almost) all the fields so the string can represent the config
   103  // content. It doesn't cover NodeProto because NodeProto isn't used by
   104  // federation.
   105  func (sc *ServerConfig) String() string {
   106  	var ver string
   107  	switch sc.TransportAPI {
   108  	case version.TransportV3:
   109  		ver = "xDSv3"
   110  	case version.TransportV2:
   111  		ver = "xDSv2"
   112  	}
   113  	return strings.Join([]string{sc.ServerURI, sc.CredsType, ver}, "-")
   114  }
   115  
   116  // UnmarshalJSON takes the json data (a list of servers) and unmarshals the
   117  // first one in the list.
   118  func (sc *ServerConfig) UnmarshalJSON(data []byte) error {
   119  	var servers []*xdsServer
   120  	if err := json.Unmarshal(data, &servers); err != nil {
   121  		return fmt.Errorf("xds: json.Unmarshal(data) for field xds_servers failed during bootstrap: %v", err)
   122  	}
   123  	if len(servers) < 1 {
   124  		return fmt.Errorf("xds: bootstrap file parsing failed during bootstrap: file doesn't contain any management server to connect to")
   125  	}
   126  	xs := servers[0]
   127  	sc.ServerURI = xs.ServerURI
   128  	for _, cc := range xs.ChannelCreds {
   129  		// We stop at the first credential type that we support.
   130  		sc.CredsType = cc.Type
   131  		if cc.Type == credsGoogleDefault {
   132  			sc.Creds = grpc.WithCredentialsBundle(google.NewDefaultCredentials())
   133  			break
   134  		} else if cc.Type == credsInsecure {
   135  			sc.Creds = grpc.WithTransportCredentials(insecure.NewCredentials())
   136  			break
   137  		}
   138  	}
   139  	for _, f := range xs.ServerFeatures {
   140  		if f == serverFeaturesV3 {
   141  			sc.TransportAPI = version.TransportV3
   142  		}
   143  	}
   144  	return nil
   145  }
   146  
   147  // Authority contains configuration for an Authority for an xDS control plane
   148  // server. See the Authorities field in the Config struct for how it's used.
   149  type Authority struct {
   150  	// ClientListenerResourceNameTemplate is template for the name of the
   151  	// Listener resource to subscribe to for a gRPC client channel.  Used only
   152  	// when the channel is created using an "xds:" URI with this authority name.
   153  	//
   154  	// The token "%s", if present in this string, will be replaced
   155  	// with %-encoded service authority (i.e., the path part of the target
   156  	// URI used to create the gRPC channel).
   157  	//
   158  	// Must start with "xdstp://<authority_name>/".  If it does not,
   159  	// that is considered a bootstrap file parsing error.
   160  	//
   161  	// If not present in the bootstrap file, defaults to
   162  	// "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s".
   163  	ClientListenerResourceNameTemplate string
   164  	// XDSServer contains the management server and config to connect to for
   165  	// this authority.
   166  	XDSServer *ServerConfig
   167  }
   168  
   169  // UnmarshalJSON implement json unmarshaller.
   170  func (a *Authority) UnmarshalJSON(data []byte) error {
   171  	var jsonData map[string]json.RawMessage
   172  	if err := json.Unmarshal(data, &jsonData); err != nil {
   173  		return fmt.Errorf("xds: failed to parse authority: %v", err)
   174  	}
   175  
   176  	for k, v := range jsonData {
   177  		switch k {
   178  		case "xds_servers":
   179  			if err := json.Unmarshal(v, &a.XDSServer); err != nil {
   180  				return fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   181  			}
   182  		case "client_listener_resource_name_template":
   183  			if err := json.Unmarshal(v, &a.ClientListenerResourceNameTemplate); err != nil {
   184  				return fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   185  			}
   186  		}
   187  	}
   188  	return nil
   189  }
   190  
   191  // Config provides the xDS client with several key bits of information that it
   192  // requires in its interaction with the management server. The Config is
   193  // initialized from the bootstrap file.
   194  type Config struct {
   195  	// XDSServer is the management server to connect to.
   196  	//
   197  	// The bootstrap file contains a list of servers (with name+creds), but we
   198  	// pick the first one.
   199  	XDSServer *ServerConfig
   200  	// CertProviderConfigs contains a mapping from certificate provider plugin
   201  	// instance names to parsed buildable configs.
   202  	CertProviderConfigs map[string]*certprovider.BuildableConfig
   203  	// ServerListenerResourceNameTemplate is a template for the name of the
   204  	// Listener resource to subscribe to for a gRPC server.
   205  	//
   206  	// If starts with "xdstp:", will be interpreted as a new-style name,
   207  	// in which case the authority of the URI will be used to select the
   208  	// relevant configuration in the "authorities" map.
   209  	//
   210  	// The token "%s", if present in this string, will be replaced with the IP
   211  	// and port on which the server is listening.  (e.g., "0.0.0.0:8080",
   212  	// "[::]:8080"). For example, a value of "example/resource/%s" could become
   213  	// "example/resource/0.0.0.0:8080". If the template starts with "xdstp:",
   214  	// the replaced string will be %-encoded.
   215  	//
   216  	// There is no default; if unset, xDS-based server creation fails.
   217  	ServerListenerResourceNameTemplate string
   218  	// A template for the name of the Listener resource to subscribe to
   219  	// for a gRPC client channel.  Used only when the channel is created
   220  	// with an "xds:" URI with no authority.
   221  	//
   222  	// If starts with "xdstp:", will be interpreted as a new-style name,
   223  	// in which case the authority of the URI will be used to select the
   224  	// relevant configuration in the "authorities" map.
   225  	//
   226  	// The token "%s", if present in this string, will be replaced with
   227  	// the service authority (i.e., the path part of the target URI
   228  	// used to create the gRPC channel).  If the template starts with
   229  	// "xdstp:", the replaced string will be %-encoded.
   230  	//
   231  	// Defaults to "%s".
   232  	ClientDefaultListenerResourceNameTemplate string
   233  
   234  	// Authorities is a map of authority name to corresponding configuration.
   235  	//
   236  	// This is used in the following cases:
   237  	// - A gRPC client channel is created using an "xds:" URI that includes
   238  	//   an authority.
   239  	// - A gRPC client channel is created using an "xds:" URI with no
   240  	//   authority, but the "client_default_listener_resource_name_template"
   241  	//   field above turns it into an "xdstp:" URI.
   242  	// - A gRPC server is created and the
   243  	//   "server_listener_resource_name_template" field is an "xdstp:" URI.
   244  	//
   245  	// In any of those cases, it is an error if the specified authority is
   246  	// not present in this map.
   247  	Authorities map[string]*Authority
   248  }
   249  
   250  type channelCreds struct {
   251  	Type   string          `json:"type"`
   252  	Config json.RawMessage `json:"config"`
   253  }
   254  
   255  type xdsServer struct {
   256  	ServerURI      string         `json:"server_uri"`
   257  	ChannelCreds   []channelCreds `json:"channel_creds"`
   258  	ServerFeatures []string       `json:"server_features"`
   259  }
   260  
   261  func bootstrapConfigFromEnvVariable() ([]byte, error) {
   262  	fName := envconfig.XDSBootstrapFileName
   263  	fContent := envconfig.XDSBootstrapFileContent
   264  
   265  	// Bootstrap file name has higher priority than bootstrap content.
   266  	if fName != "" {
   267  		// If file name is set
   268  		// - If file not found (or other errors), fail
   269  		// - Otherwise, use the content.
   270  		//
   271  		// Note that even if the content is invalid, we don't failover to the
   272  		// file content env variable.
   273  		dubbogoLogger.Debugf("xds: using bootstrap file with name %q", fName)
   274  		return bootstrapFileReadFunc(fName)
   275  	}
   276  
   277  	if fContent != "" {
   278  		return []byte(fContent), nil
   279  	}
   280  
   281  	return nil, fmt.Errorf("none of the bootstrap environment variables (%q or %q) defined",
   282  		envconfig.XDSBootstrapFileNameEnv, envconfig.XDSBootstrapFileContentEnv)
   283  }
   284  
   285  // NewConfig returns a new instance of Config initialized by reading the
   286  // bootstrap file found at ${GRPC_XDS_BOOTSTRAP}.
   287  //
   288  // Currently, we support exactly one type of credential, which is
   289  // "google_default", where we use the host's default certs for transport
   290  // credentials and a Google oauth token for call credentials.
   291  //
   292  // This function tries to process as much of the bootstrap file as possible (in
   293  // the presence of the errors) and may return a Config object with certain
   294  // fields left unspecified, in which case the caller should use some sane
   295  // defaults.
   296  func NewConfig() (*Config, error) {
   297  	// Examples of the bootstrap json can be found in the generator tests
   298  	// https://github.com/GoogleCloudPlatform/traffic-director-grpc-bootstrap/blob/master/main_test.go.
   299  	data, err := bootstrapConfigFromEnvVariable()
   300  	if err != nil {
   301  		return nil, fmt.Errorf("xds: Failed to read bootstrap config: %v", err)
   302  	}
   303  	dubbogoLogger.Debugf("Bootstrap content: %s", data)
   304  	return NewConfigFromContents(data)
   305  }
   306  
   307  // NewConfigFromContents returns a new Config using the specified bootstrap
   308  // file contents instead of reading the environment variable.  This is only
   309  // suitable for testing purposes.
   310  func NewConfigFromContents(data []byte) (*Config, error) {
   311  	config := &Config{}
   312  
   313  	var jsonData map[string]json.RawMessage
   314  	if err := json.Unmarshal(data, &jsonData); err != nil {
   315  		return nil, fmt.Errorf("xds: Failed to parse bootstrap config: %v", err)
   316  	}
   317  
   318  	var node *v3corepb.Node
   319  	m := jsonpb.Unmarshaler{AllowUnknownFields: true}
   320  	for k, v := range jsonData {
   321  		switch k {
   322  		case "node":
   323  			// We unconditionally convert the JSON into a v3.Node proto. The v3
   324  			// proto does not contain the deprecated field "build_version" from
   325  			// the v2 proto. We do not expect the bootstrap file to contain the
   326  			// "build_version" field. In any case, the unmarshal will succeed
   327  			// because we have set the `AllowUnknownFields` option on the
   328  			// unmarshaler.
   329  			node = &v3corepb.Node{}
   330  			if err := m.Unmarshal(bytes.NewReader(v), node); err != nil {
   331  				return nil, fmt.Errorf("xds: jsonpb.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   332  			}
   333  		case "xds_servers":
   334  			if err := json.Unmarshal(v, &config.XDSServer); err != nil {
   335  				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   336  			}
   337  		case "certificate_providers":
   338  			var providerInstances map[string]json.RawMessage
   339  			if err := json.Unmarshal(v, &providerInstances); err != nil {
   340  				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   341  			}
   342  			configs := make(map[string]*certprovider.BuildableConfig)
   343  			getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder)
   344  			for instance, data := range providerInstances {
   345  				var nameAndConfig struct {
   346  					PluginName string          `json:"plugin_name"`
   347  					Config     json.RawMessage `json:"config"`
   348  				}
   349  				if err := json.Unmarshal(data, &nameAndConfig); err != nil {
   350  					return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), instance, err)
   351  				}
   352  
   353  				name := nameAndConfig.PluginName
   354  				parser := getBuilder(nameAndConfig.PluginName)
   355  				if parser == nil {
   356  					// We ignore plugins that we do not know about.
   357  					continue
   358  				}
   359  				bc, err := parser.ParseConfig(nameAndConfig.Config)
   360  				if err != nil {
   361  					return nil, fmt.Errorf("xds: Config parsing for plugin %q failed: %v", name, err)
   362  				}
   363  				configs[instance] = bc
   364  			}
   365  			config.CertProviderConfigs = configs
   366  		case "server_listener_resource_name_template":
   367  			if err := json.Unmarshal(v, &config.ServerListenerResourceNameTemplate); err != nil {
   368  				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   369  			}
   370  		case "client_default_listener_resource_name_template":
   371  			if !envconfig.XDSFederation {
   372  				dubbogoLogger.Warnf("xds: bootstrap field %v is not support when Federation is disabled", k)
   373  				continue
   374  			}
   375  			if err := json.Unmarshal(v, &config.ClientDefaultListenerResourceNameTemplate); err != nil {
   376  				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   377  			}
   378  		case "authorities":
   379  			if !envconfig.XDSFederation {
   380  				dubbogoLogger.Warnf("xds: bootstrap field %v is not support when Federation is disabled", k)
   381  				continue
   382  			}
   383  			if err := json.Unmarshal(v, &config.Authorities); err != nil {
   384  				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
   385  			}
   386  		default:
   387  			dubbogoLogger.Warnf("Bootstrap content has unknown field: %s", k)
   388  		}
   389  		// Do not fail the xDS bootstrap when an unknown field is seen. This can
   390  		// happen when an older version client reads a newer version bootstrap
   391  		// file with new fields.
   392  	}
   393  
   394  	if config.ClientDefaultListenerResourceNameTemplate == "" {
   395  		// Default value of the default client listener name template is "%s".
   396  		config.ClientDefaultListenerResourceNameTemplate = "%s"
   397  	}
   398  	if config.XDSServer == nil {
   399  		return nil, fmt.Errorf("xds: Required field %q not found in bootstrap %s", "xds_servers", jsonData["xds_servers"])
   400  	}
   401  	if config.XDSServer.ServerURI == "" {
   402  		return nil, fmt.Errorf("xds: Required field %q not found in bootstrap %s", "xds_servers.server_uri", jsonData["xds_servers"])
   403  	}
   404  	if config.XDSServer.Creds == nil {
   405  		return nil, fmt.Errorf("xds: Required field %q doesn't contain valid value in bootstrap %s", "xds_servers.channel_creds", jsonData["xds_servers"])
   406  	}
   407  	// Post-process the authorities' client listener resource template field:
   408  	// - if set, it must start with "xdstp://<authority_name>/"
   409  	// - if not set, it defaults to "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s"
   410  	for name, authority := range config.Authorities {
   411  		prefix := fmt.Sprintf("xdstp://%s", name)
   412  		if authority.ClientListenerResourceNameTemplate == "" {
   413  			authority.ClientListenerResourceNameTemplate = prefix + "/envoy.config.listener.v3.Listener/%s"
   414  			continue
   415  		}
   416  		if !strings.HasPrefix(authority.ClientListenerResourceNameTemplate, prefix) {
   417  			return nil, fmt.Errorf("xds: field ClientListenerResourceNameTemplate %q of authority %q doesn't start with prefix %q", authority.ClientListenerResourceNameTemplate, name, prefix)
   418  		}
   419  	}
   420  
   421  	if err := config.updateNodeProto(node); err != nil {
   422  		return nil, err
   423  	}
   424  	dubbogoLogger.Infof("Bootstrap config for creating xds-client: %v", pretty.ToJSON(config))
   425  	return config, nil
   426  }
   427  
   428  // updateNodeProto updates the node proto read from the bootstrap file.
   429  //
   430  // The input node is a v3.Node protobuf message corresponding to the JSON
   431  // contents found in the bootstrap file. This method performs some post
   432  // processing on it:
   433  // 1. If the node is nil, we create an empty one here. That way, callers of this
   434  // function can always expect that the NodeProto field is non-nil.
   435  // 2. Some additional fields which are not expected to be set in the bootstrap
   436  // file are populated here.
   437  // 3. For each server config (both top level and in each authority), we set its
   438  // node field to the v3.Node, or a v2.Node with the same content, depending on
   439  // the server's transprot API version.
   440  func (c *Config) updateNodeProto(node *v3corepb.Node) error {
   441  	v3 := node
   442  	if v3 == nil {
   443  		v3 = &v3corepb.Node{}
   444  	}
   445  	v3.UserAgentName = gRPCUserAgentName
   446  	v3.UserAgentVersionType = &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}
   447  	v3.ClientFeatures = append(v3.ClientFeatures, clientFeatureNoOverprovisioning)
   448  
   449  	v2 := &v2corepb.Node{}
   450  	v3bytes, err := proto.Marshal(v3)
   451  	if err != nil {
   452  		return fmt.Errorf("xds: proto.Marshal(%v): %v", v3, err)
   453  	}
   454  	if err := proto.Unmarshal(v3bytes, v2); err != nil {
   455  		return fmt.Errorf("xds: proto.Unmarshal(%v): %v", v3bytes, err)
   456  	}
   457  	// BuildVersion is deprecated, and is replaced by user_agent_name and
   458  	// user_agent_version. But the management servers are still using the old
   459  	// field, so we will keep both set.
   460  	v2.BuildVersion = gRPCVersion
   461  	v2.UserAgentVersionType = &v2corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}
   462  
   463  	switch c.XDSServer.TransportAPI {
   464  	case version.TransportV2:
   465  		c.XDSServer.NodeProto = v2
   466  	case version.TransportV3:
   467  		c.XDSServer.NodeProto = v3
   468  	}
   469  
   470  	for _, a := range c.Authorities {
   471  		if a.XDSServer == nil {
   472  			continue
   473  		}
   474  		switch a.XDSServer.TransportAPI {
   475  		case version.TransportV2:
   476  			a.XDSServer.NodeProto = v2
   477  		case version.TransportV3:
   478  			a.XDSServer.NodeProto = v3
   479  		}
   480  	}
   481  
   482  	return nil
   483  }