blob: e0057a42156d76e449aef7bac1792209fd824e81 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
*
* Copyright 2019 gRPC authors.
*
*/
// Package bootstrap provides the functionality to initialize certain aspects
// of an xDS client by reading a bootstrap file.
package bootstrap
import (
"bytes"
"encoding/json"
"fmt"
"os"
"strings"
dubbogoLogger "github.com/dubbogo/gost/log/logger"
v2corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
"dubbo.apache.org/dubbo-go/v3/xds/client/resource/version"
"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider"
"dubbo.apache.org/dubbo-go/v3/xds/internal"
"dubbo.apache.org/dubbo-go/v3/xds/utils/envconfig"
"dubbo.apache.org/dubbo-go/v3/xds/utils/pretty"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/google"
"google.golang.org/grpc/credentials/insecure"
)
const (
// The "server_features" field in the bootstrap file contains a list of
// features supported by the server. A value of "xds_v3" indicates that the
// server supports the v3 version of the xDS transport protocol.
serverFeaturesV3 = "xds_v3"
// Type name for Google default credentials.
credsGoogleDefault = "google_default"
credsInsecure = "insecure"
gRPCUserAgentName = "gRPC Go"
clientFeatureNoOverprovisioning = "envoy.lb.does_not_support_overprovisioning"
)
var gRPCVersion = fmt.Sprintf("%s %s", gRPCUserAgentName, grpc.Version)
// For overriding in unit tests.
var bootstrapFileReadFunc = os.ReadFile
// ServerConfig contains the configuration to connect to a server, including
// URI, creds, and transport API version (e.g. v2 or v3).
type ServerConfig struct {
// ServerURI is the management server to connect to.
//
// The bootstrap file contains an ordered list of xDS servers to contact for
// this authority. The first one is picked.
ServerURI string
// Creds contains the credentials to be used while talking to the xDS
// server, as a grpc.DialOption.
Creds grpc.DialOption
// CredsType is the type of the creds. It will be used to dedup servers.
CredsType string
// TransportAPI indicates the API version of xDS transport protocol to use.
// This describes the xDS gRPC endpoint and version of
// DiscoveryRequest/Response used on the wire.
TransportAPI version.TransportAPI
// NodeProto contains the Node proto to be used in xDS requests. The actual
// type depends on the transport protocol version used.
//
// Note that it's specified in the bootstrap globally for all the servers,
// but we keep it in each server config so that its type (e.g. *v2pb.Node or
// *v3pb.Node) is consistent with the transport API version.
NodeProto proto.Message
}
// String returns the string representation of the ServerConfig.
//
// This string representation will be used as map keys in federation
// (`map[ServerConfig]authority`), so that the xDS ClientConn and stream will be
// shared by authorities with different names but the same server config.
//
// It covers (almost) all the fields so the string can represent the config
// content. It doesn't cover NodeProto because NodeProto isn't used by
// federation.
func (sc *ServerConfig) String() string {
var ver string
switch sc.TransportAPI {
case version.TransportV3:
ver = "xDSv3"
case version.TransportV2:
ver = "xDSv2"
}
return strings.Join([]string{sc.ServerURI, sc.CredsType, ver}, "-")
}
// UnmarshalJSON takes the json data (a list of servers) and unmarshals the
// first one in the list.
func (sc *ServerConfig) UnmarshalJSON(data []byte) error {
var servers []*xdsServer
if err := json.Unmarshal(data, &servers); err != nil {
return fmt.Errorf("xds: json.Unmarshal(data) for field xds_servers failed during bootstrap: %v", err)
}
if len(servers) < 1 {
return fmt.Errorf("xds: bootstrap file parsing failed during bootstrap: file doesn't contain any management server to connect to")
}
xs := servers[0]
sc.ServerURI = xs.ServerURI
for _, cc := range xs.ChannelCreds {
// We stop at the first credential type that we support.
sc.CredsType = cc.Type
if cc.Type == credsGoogleDefault {
sc.Creds = grpc.WithCredentialsBundle(google.NewDefaultCredentials())
break
} else if cc.Type == credsInsecure {
sc.Creds = grpc.WithTransportCredentials(insecure.NewCredentials())
break
}
}
for _, f := range xs.ServerFeatures {
if f == serverFeaturesV3 {
sc.TransportAPI = version.TransportV3
}
}
return nil
}
// Authority contains configuration for an Authority for an xDS control plane
// server. See the Authorities field in the Config struct for how it's used.
type Authority struct {
// ClientListenerResourceNameTemplate is template for the name of the
// Listener resource to subscribe to for a gRPC client channel. Used only
// when the channel is created using an "xds:" URI with this authority name.
//
// The token "%s", if present in this string, will be replaced
// with %-encoded service authority (i.e., the path part of the target
// URI used to create the gRPC channel).
//
// Must start with "xdstp://<authority_name>/". If it does not,
// that is considered a bootstrap file parsing error.
//
// If not present in the bootstrap file, defaults to
// "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s".
ClientListenerResourceNameTemplate string
// XDSServer contains the management server and config to connect to for
// this authority.
XDSServer *ServerConfig
}
// UnmarshalJSON implement json unmarshaller.
func (a *Authority) UnmarshalJSON(data []byte) error {
var jsonData map[string]json.RawMessage
if err := json.Unmarshal(data, &jsonData); err != nil {
return fmt.Errorf("xds: failed to parse authority: %v", err)
}
for k, v := range jsonData {
switch k {
case "xds_servers":
if err := json.Unmarshal(v, &a.XDSServer); err != nil {
return fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
}
case "client_listener_resource_name_template":
if err := json.Unmarshal(v, &a.ClientListenerResourceNameTemplate); err != nil {
return fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
}
}
}
return nil
}
// Config provides the xDS client with several key bits of information that it
// requires in its interaction with the management server. The Config is
// initialized from the bootstrap file.
type Config struct {
// XDSServer is the management server to connect to.
//
// The bootstrap file contains a list of servers (with name+creds), but we
// pick the first one.
XDSServer *ServerConfig
// CertProviderConfigs contains a mapping from certificate provider plugin
// instance names to parsed buildable configs.
CertProviderConfigs map[string]*certprovider.BuildableConfig
// ServerListenerResourceNameTemplate is a template for the name of the
// Listener resource to subscribe to for a gRPC server.
//
// If starts with "xdstp:", will be interpreted as a new-style name,
// in which case the authority of the URI will be used to select the
// relevant configuration in the "authorities" map.
//
// The token "%s", if present in this string, will be replaced with the IP
// and port on which the server is listening. (e.g., "0.0.0.0:8080",
// "[::]:8080"). For example, a value of "example/resource/%s" could become
// "example/resource/0.0.0.0:8080". If the template starts with "xdstp:",
// the replaced string will be %-encoded.
//
// There is no default; if unset, xDS-based server creation fails.
ServerListenerResourceNameTemplate string
// A template for the name of the Listener resource to subscribe to
// for a gRPC client channel. Used only when the channel is created
// with an "xds:" URI with no authority.
//
// If starts with "xdstp:", will be interpreted as a new-style name,
// in which case the authority of the URI will be used to select the
// relevant configuration in the "authorities" map.
//
// The token "%s", if present in this string, will be replaced with
// the service authority (i.e., the path part of the target URI
// used to create the gRPC channel). If the template starts with
// "xdstp:", the replaced string will be %-encoded.
//
// Defaults to "%s".
ClientDefaultListenerResourceNameTemplate string
// Authorities is a map of authority name to corresponding configuration.
//
// This is used in the following cases:
// - A gRPC client channel is created using an "xds:" URI that includes
// an authority.
// - A gRPC client channel is created using an "xds:" URI with no
// authority, but the "client_default_listener_resource_name_template"
// field above turns it into an "xdstp:" URI.
// - A gRPC server is created and the
// "server_listener_resource_name_template" field is an "xdstp:" URI.
//
// In any of those cases, it is an error if the specified authority is
// not present in this map.
Authorities map[string]*Authority
}
type channelCreds struct {
Type string `json:"type"`
Config json.RawMessage `json:"config"`
}
type xdsServer struct {
ServerURI string `json:"server_uri"`
ChannelCreds []channelCreds `json:"channel_creds"`
ServerFeatures []string `json:"server_features"`
}
func bootstrapConfigFromEnvVariable() ([]byte, error) {
fName := envconfig.XDSBootstrapFileName
fContent := envconfig.XDSBootstrapFileContent
// Bootstrap file name has higher priority than bootstrap content.
if fName != "" {
// If file name is set
// - If file not found (or other errors), fail
// - Otherwise, use the content.
//
// Note that even if the content is invalid, we don't failover to the
// file content env variable.
dubbogoLogger.Debugf("xds: using bootstrap file with name %q", fName)
return bootstrapFileReadFunc(fName)
}
if fContent != "" {
return []byte(fContent), nil
}
return nil, fmt.Errorf("none of the bootstrap environment variables (%q or %q) defined",
envconfig.XDSBootstrapFileNameEnv, envconfig.XDSBootstrapFileContentEnv)
}
// NewConfig returns a new instance of Config initialized by reading the
// bootstrap file found at ${GRPC_XDS_BOOTSTRAP}.
//
// Currently, we support exactly one type of credential, which is
// "google_default", where we use the host's default certs for transport
// credentials and a Google oauth token for call credentials.
//
// This function tries to process as much of the bootstrap file as possible (in
// the presence of the errors) and may return a Config object with certain
// fields left unspecified, in which case the caller should use some sane
// defaults.
func NewConfig() (*Config, error) {
// Examples of the bootstrap json can be found in the generator tests
// https://github.com/GoogleCloudPlatform/traffic-director-grpc-bootstrap/blob/master/main_test.go.
data, err := bootstrapConfigFromEnvVariable()
if err != nil {
return nil, fmt.Errorf("xds: Failed to read bootstrap config: %v", err)
}
dubbogoLogger.Debugf("Bootstrap content: %s", data)
return NewConfigFromContents(data)
}
// NewConfigFromContents returns a new Config using the specified bootstrap
// file contents instead of reading the environment variable. This is only
// suitable for testing purposes.
func NewConfigFromContents(data []byte) (*Config, error) {
config := &Config{}
var jsonData map[string]json.RawMessage
if err := json.Unmarshal(data, &jsonData); err != nil {
return nil, fmt.Errorf("xds: Failed to parse bootstrap config: %v", err)
}
var node *v3corepb.Node
m := jsonpb.Unmarshaler{AllowUnknownFields: true}
for k, v := range jsonData {
switch k {
case "node":
// We unconditionally convert the JSON into a v3.Node proto. The v3
// proto does not contain the deprecated field "build_version" from
// the v2 proto. We do not expect the bootstrap file to contain the
// "build_version" field. In any case, the unmarshal will succeed
// because we have set the `AllowUnknownFields` option on the
// unmarshaler.
node = &v3corepb.Node{}
if err := m.Unmarshal(bytes.NewReader(v), node); err != nil {
return nil, fmt.Errorf("xds: jsonpb.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
}
case "xds_servers":
if err := json.Unmarshal(v, &config.XDSServer); err != nil {
return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
}
case "certificate_providers":
var providerInstances map[string]json.RawMessage
if err := json.Unmarshal(v, &providerInstances); err != nil {
return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
}
configs := make(map[string]*certprovider.BuildableConfig)
getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder)
for instance, data := range providerInstances {
var nameAndConfig struct {
PluginName string `json:"plugin_name"`
Config json.RawMessage `json:"config"`
}
if err := json.Unmarshal(data, &nameAndConfig); err != nil {
return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), instance, err)
}
name := nameAndConfig.PluginName
parser := getBuilder(nameAndConfig.PluginName)
if parser == nil {
// We ignore plugins that we do not know about.
continue
}
bc, err := parser.ParseConfig(nameAndConfig.Config)
if err != nil {
return nil, fmt.Errorf("xds: Config parsing for plugin %q failed: %v", name, err)
}
configs[instance] = bc
}
config.CertProviderConfigs = configs
case "server_listener_resource_name_template":
if err := json.Unmarshal(v, &config.ServerListenerResourceNameTemplate); err != nil {
return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
}
case "client_default_listener_resource_name_template":
if !envconfig.XDSFederation {
dubbogoLogger.Warnf("xds: bootstrap field %v is not support when Federation is disabled", k)
continue
}
if err := json.Unmarshal(v, &config.ClientDefaultListenerResourceNameTemplate); err != nil {
return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
}
case "authorities":
if !envconfig.XDSFederation {
dubbogoLogger.Warnf("xds: bootstrap field %v is not support when Federation is disabled", k)
continue
}
if err := json.Unmarshal(v, &config.Authorities); err != nil {
return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
}
default:
dubbogoLogger.Warnf("Bootstrap content has unknown field: %s", k)
}
// Do not fail the xDS bootstrap when an unknown field is seen. This can
// happen when an older version client reads a newer version bootstrap
// file with new fields.
}
if config.ClientDefaultListenerResourceNameTemplate == "" {
// Default value of the default client listener name template is "%s".
config.ClientDefaultListenerResourceNameTemplate = "%s"
}
if config.XDSServer == nil {
return nil, fmt.Errorf("xds: Required field %q not found in bootstrap %s", "xds_servers", jsonData["xds_servers"])
}
if config.XDSServer.ServerURI == "" {
return nil, fmt.Errorf("xds: Required field %q not found in bootstrap %s", "xds_servers.server_uri", jsonData["xds_servers"])
}
if config.XDSServer.Creds == nil {
return nil, fmt.Errorf("xds: Required field %q doesn't contain valid value in bootstrap %s", "xds_servers.channel_creds", jsonData["xds_servers"])
}
// Post-process the authorities' client listener resource template field:
// - if set, it must start with "xdstp://<authority_name>/"
// - if not set, it defaults to "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s"
for name, authority := range config.Authorities {
prefix := fmt.Sprintf("xdstp://%s", name)
if authority.ClientListenerResourceNameTemplate == "" {
authority.ClientListenerResourceNameTemplate = prefix + "/envoy.config.listener.v3.Listener/%s"
continue
}
if !strings.HasPrefix(authority.ClientListenerResourceNameTemplate, prefix) {
return nil, fmt.Errorf("xds: field ClientListenerResourceNameTemplate %q of authority %q doesn't start with prefix %q", authority.ClientListenerResourceNameTemplate, name, prefix)
}
}
if err := config.updateNodeProto(node); err != nil {
return nil, err
}
dubbogoLogger.Infof("Bootstrap config for creating xds-client: %v", pretty.ToJSON(config))
return config, nil
}
// updateNodeProto updates the node proto read from the bootstrap file.
//
// The input node is a v3.Node protobuf message corresponding to the JSON
// contents found in the bootstrap file. This method performs some post
// processing on it:
// 1. If the node is nil, we create an empty one here. That way, callers of this
// function can always expect that the NodeProto field is non-nil.
// 2. Some additional fields which are not expected to be set in the bootstrap
// file are populated here.
// 3. For each server config (both top level and in each authority), we set its
// node field to the v3.Node, or a v2.Node with the same content, depending on
// the server's transprot API version.
func (c *Config) updateNodeProto(node *v3corepb.Node) error {
v3 := node
if v3 == nil {
v3 = &v3corepb.Node{}
}
v3.UserAgentName = gRPCUserAgentName
v3.UserAgentVersionType = &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}
v3.ClientFeatures = append(v3.ClientFeatures, clientFeatureNoOverprovisioning)
v2 := &v2corepb.Node{}
v3bytes, err := proto.Marshal(v3)
if err != nil {
return fmt.Errorf("xds: proto.Marshal(%v): %v", v3, err)
}
if err := proto.Unmarshal(v3bytes, v2); err != nil {
return fmt.Errorf("xds: proto.Unmarshal(%v): %v", v3bytes, err)
}
// BuildVersion is deprecated, and is replaced by user_agent_name and
// user_agent_version. But the management servers are still using the old
// field, so we will keep both set.
v2.BuildVersion = gRPCVersion
v2.UserAgentVersionType = &v2corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}
switch c.XDSServer.TransportAPI {
case version.TransportV2:
c.XDSServer.NodeProto = v2
case version.TransportV3:
c.XDSServer.NodeProto = v3
}
for _, a := range c.Authorities {
if a.XDSServer == nil {
continue
}
switch a.XDSServer.TransportAPI {
case version.TransportV2:
a.XDSServer.NodeProto = v2
case version.TransportV3:
a.XDSServer.NodeProto = v3
}
}
return nil
}