mirror of https://github.com/grpc/grpc-go.git
829 lines
29 KiB
Go
829 lines
29 KiB
Go
/*
|
|
*
|
|
* Copyright 2019 gRPC authors.
|
|
*
|
|
* Licensed 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.
|
|
*
|
|
*/
|
|
|
|
// 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"
|
|
"maps"
|
|
"net/url"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/tls/certprovider"
|
|
"google.golang.org/grpc/internal"
|
|
"google.golang.org/grpc/internal/envconfig"
|
|
"google.golang.org/grpc/xds/bootstrap"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
|
|
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
|
)
|
|
|
|
const (
|
|
serverFeaturesIgnoreResourceDeletion = "ignore_resource_deletion"
|
|
gRPCUserAgentName = "gRPC Go"
|
|
clientFeatureNoOverprovisioning = "envoy.lb.does_not_support_overprovisioning"
|
|
clientFeatureResourceWrapper = "xds.config.resource-in-sotw"
|
|
)
|
|
|
|
// For overriding in unit tests.
|
|
var bootstrapFileReadFunc = os.ReadFile
|
|
|
|
// ChannelCreds contains the credentials to be used while communicating with an
|
|
// xDS server. It is also used to dedup servers with the same server URI.
|
|
//
|
|
// This type does not implement custom JSON marshal/unmarshal logic because it
|
|
// is straightforward to accomplish the same with json struct tags.
|
|
type ChannelCreds struct {
|
|
// Type contains a unique name identifying the credentials type. The only
|
|
// supported types currently are "google_default" and "insecure".
|
|
Type string `json:"type,omitempty"`
|
|
// Config contains the JSON configuration associated with the credentials.
|
|
Config json.RawMessage `json:"config,omitempty"`
|
|
}
|
|
|
|
// Equal reports whether cc and other are considered equal.
|
|
func (cc ChannelCreds) Equal(other ChannelCreds) bool {
|
|
return cc.Type == other.Type && bytes.Equal(cc.Config, other.Config)
|
|
}
|
|
|
|
// String returns a string representation of the credentials. It contains the
|
|
// type and the config (if non-nil) separated by a "-".
|
|
func (cc ChannelCreds) String() string {
|
|
if cc.Config == nil {
|
|
return cc.Type
|
|
}
|
|
|
|
// We do not expect the Marshal call to fail since we wrote to cc.Config
|
|
// after a successful unmarshalling from JSON configuration. Therefore,
|
|
// it is safe to ignore the error here.
|
|
b, _ := json.Marshal(cc.Config)
|
|
return cc.Type + "-" + string(b)
|
|
}
|
|
|
|
// ServerConfigs represents a collection of server configurations.
|
|
type ServerConfigs []*ServerConfig
|
|
|
|
// Equal returns true if scs equals other.
|
|
func (scs *ServerConfigs) Equal(other *ServerConfigs) bool {
|
|
if len(*scs) != len(*other) {
|
|
return false
|
|
}
|
|
for i := range *scs {
|
|
if !(*scs)[i].Equal((*other)[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// UnmarshalJSON takes the json data (a list of server configurations) and
|
|
// unmarshals it to the struct.
|
|
func (scs *ServerConfigs) UnmarshalJSON(data []byte) error {
|
|
servers := []*ServerConfig{}
|
|
if err := json.Unmarshal(data, &servers); err != nil {
|
|
return fmt.Errorf("xds: failed to JSON unmarshal server configurations during bootstrap: %v, config:\n%s", err, string(data))
|
|
}
|
|
// Only use the first server config if fallback support is disabled.
|
|
if !envconfig.XDSFallbackSupport {
|
|
if len(servers) > 1 {
|
|
servers = servers[:1]
|
|
}
|
|
}
|
|
*scs = servers
|
|
return nil
|
|
}
|
|
|
|
// String returns a string representation of the ServerConfigs, by concatenating
|
|
// the string representations of the underlying server configs.
|
|
func (scs *ServerConfigs) String() string {
|
|
ret := ""
|
|
for i, sc := range *scs {
|
|
if i > 0 {
|
|
ret += ", "
|
|
}
|
|
ret += sc.String()
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// Authority contains configuration for an xDS control plane authority.
|
|
//
|
|
// This type does not implement custom JSON marshal/unmarshal logic because it
|
|
// is straightforward to accomplish the same with json struct tags.
|
|
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 `json:"client_listener_resource_name_template,omitempty"`
|
|
// XDSServers contains the list of server configurations for this authority.
|
|
XDSServers ServerConfigs `json:"xds_servers,omitempty"`
|
|
}
|
|
|
|
// Equal returns true if a equals other.
|
|
func (a *Authority) Equal(other *Authority) bool {
|
|
switch {
|
|
case a == nil && other == nil:
|
|
return true
|
|
case (a != nil) != (other != nil):
|
|
return false
|
|
case a.ClientListenerResourceNameTemplate != other.ClientListenerResourceNameTemplate:
|
|
return false
|
|
case !a.XDSServers.Equal(&other.XDSServers):
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ServerConfig contains the configuration to connect to a server.
|
|
type ServerConfig struct {
|
|
serverURI string
|
|
channelCreds []ChannelCreds
|
|
serverFeatures []string
|
|
|
|
// As part of unmarshalling the JSON config into this struct, we ensure that
|
|
// the credentials config is valid by building an instance of the specified
|
|
// credentials and store it here for easy access.
|
|
selectedCreds ChannelCreds
|
|
credsDialOption grpc.DialOption
|
|
|
|
cleanups []func()
|
|
}
|
|
|
|
// ServerURI returns the URI of the management server to connect to.
|
|
func (sc *ServerConfig) ServerURI() string {
|
|
return sc.serverURI
|
|
}
|
|
|
|
// ChannelCreds returns the credentials configuration to use when communicating
|
|
// with this server. Also used to dedup servers with the same server URI.
|
|
func (sc *ServerConfig) ChannelCreds() []ChannelCreds {
|
|
return sc.channelCreds
|
|
}
|
|
|
|
// ServerFeatures returns the list of features supported by this server. Also
|
|
// used to dedup servers with the same server URI and channel creds.
|
|
func (sc *ServerConfig) ServerFeatures() []string {
|
|
return sc.serverFeatures
|
|
}
|
|
|
|
// ServerFeaturesIgnoreResourceDeletion returns true if this server supports a
|
|
// feature where the xDS client can ignore resource deletions from this server,
|
|
// as described in gRFC A53.
|
|
//
|
|
// This feature controls the behavior of the xDS client when the server deletes
|
|
// a previously sent Listener or Cluster resource. If set, the xDS client will
|
|
// not invoke the watchers' OnResourceDoesNotExist() method when a resource is
|
|
// deleted, nor will it remove the existing resource value from its cache.
|
|
func (sc *ServerConfig) ServerFeaturesIgnoreResourceDeletion() bool {
|
|
for _, sf := range sc.serverFeatures {
|
|
if sf == serverFeaturesIgnoreResourceDeletion {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CredsDialOption returns the first supported transport credentials from the
|
|
// configuration, as a dial option.
|
|
func (sc *ServerConfig) CredsDialOption() grpc.DialOption {
|
|
return sc.credsDialOption
|
|
}
|
|
|
|
// Cleanups returns a collection of functions to be called when the xDS client
|
|
// for this server is closed. Allows cleaning up resources created specifically
|
|
// for this server.
|
|
func (sc *ServerConfig) Cleanups() []func() {
|
|
return sc.cleanups
|
|
}
|
|
|
|
// Equal reports whether sc and other are considered equal.
|
|
func (sc *ServerConfig) Equal(other *ServerConfig) bool {
|
|
switch {
|
|
case sc == nil && other == nil:
|
|
return true
|
|
case (sc != nil) != (other != nil):
|
|
return false
|
|
case sc.serverURI != other.serverURI:
|
|
return false
|
|
case !slices.EqualFunc(sc.channelCreds, other.channelCreds, func(a, b ChannelCreds) bool { return a.Equal(b) }):
|
|
return false
|
|
case !slices.Equal(sc.serverFeatures, other.serverFeatures):
|
|
return false
|
|
case !sc.selectedCreds.Equal(other.selectedCreds):
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// String returns the string representation of the ServerConfig.
|
|
func (sc *ServerConfig) String() string {
|
|
if len(sc.serverFeatures) == 0 {
|
|
return fmt.Sprintf("%s-%s", sc.serverURI, sc.selectedCreds.String())
|
|
}
|
|
features := strings.Join(sc.serverFeatures, "-")
|
|
return strings.Join([]string{sc.serverURI, sc.selectedCreds.String(), features}, "-")
|
|
}
|
|
|
|
// The following fields correspond 1:1 with the JSON schema for ServerConfig.
|
|
type serverConfigJSON struct {
|
|
ServerURI string `json:"server_uri,omitempty"`
|
|
ChannelCreds []ChannelCreds `json:"channel_creds,omitempty"`
|
|
ServerFeatures []string `json:"server_features,omitempty"`
|
|
}
|
|
|
|
// MarshalJSON returns marshaled JSON bytes corresponding to this server config.
|
|
func (sc *ServerConfig) MarshalJSON() ([]byte, error) {
|
|
server := &serverConfigJSON{
|
|
ServerURI: sc.serverURI,
|
|
ChannelCreds: sc.channelCreds,
|
|
ServerFeatures: sc.serverFeatures,
|
|
}
|
|
return json.Marshal(server)
|
|
}
|
|
|
|
// UnmarshalJSON takes the json data (a server) and unmarshals it to the struct.
|
|
func (sc *ServerConfig) UnmarshalJSON(data []byte) error {
|
|
server := serverConfigJSON{}
|
|
if err := json.Unmarshal(data, &server); err != nil {
|
|
return fmt.Errorf("xds: failed to JSON unmarshal server configuration during bootstrap: %v, config:\n%s", err, string(data))
|
|
}
|
|
|
|
sc.serverURI = server.ServerURI
|
|
sc.channelCreds = server.ChannelCreds
|
|
sc.serverFeatures = server.ServerFeatures
|
|
|
|
for _, cc := range server.ChannelCreds {
|
|
// We stop at the first credential type that we support.
|
|
c := bootstrap.GetCredentials(cc.Type)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
bundle, cancel, err := c.Build(cc.Config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to build credentials bundle from bootstrap for %q: %v", cc.Type, err)
|
|
}
|
|
sc.selectedCreds = cc
|
|
sc.credsDialOption = grpc.WithCredentialsBundle(bundle)
|
|
sc.cleanups = append(sc.cleanups, cancel)
|
|
break
|
|
}
|
|
if sc.serverURI == "" {
|
|
return fmt.Errorf("xds: `server_uri` field in server config cannot be empty: %s", string(data))
|
|
}
|
|
if sc.credsDialOption == nil {
|
|
return fmt.Errorf("xds: `channel_creds` field in server config cannot be empty: %s", string(data))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ServerConfigTestingOptions specifies options for creating a new ServerConfig
|
|
// for testing purposes.
|
|
//
|
|
// # Testing-Only
|
|
type ServerConfigTestingOptions struct {
|
|
// URI is the name of the server corresponding to this server config.
|
|
URI string
|
|
// ChannelCreds contains a list of channel credentials to use when talking
|
|
// to this server. If unspecified, `insecure` credentials will be used.
|
|
ChannelCreds []ChannelCreds
|
|
// ServerFeatures represents the list of features supported by this server.
|
|
ServerFeatures []string
|
|
}
|
|
|
|
// ServerConfigForTesting creates a new ServerConfig from the passed in options,
|
|
// for testing purposes.
|
|
//
|
|
// # Testing-Only
|
|
func ServerConfigForTesting(opts ServerConfigTestingOptions) (*ServerConfig, error) {
|
|
cc := opts.ChannelCreds
|
|
if cc == nil {
|
|
cc = []ChannelCreds{{Type: "insecure"}}
|
|
}
|
|
scInternal := &serverConfigJSON{
|
|
ServerURI: opts.URI,
|
|
ChannelCreds: cc,
|
|
ServerFeatures: opts.ServerFeatures,
|
|
}
|
|
scJSON, err := json.Marshal(scInternal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sc := new(ServerConfig)
|
|
if err := sc.UnmarshalJSON(scJSON); err != nil {
|
|
return nil, err
|
|
}
|
|
return sc, nil
|
|
}
|
|
|
|
// Config is the internal representation of the bootstrap configuration provided
|
|
// to the xDS client.
|
|
type Config struct {
|
|
xDSServers ServerConfigs
|
|
cpcs map[string]certproviderNameAndConfig
|
|
serverListenerResourceNameTemplate string
|
|
clientDefaultListenerResourceNameTemplate string
|
|
authorities map[string]*Authority
|
|
node node
|
|
|
|
// A map from certprovider instance names to parsed buildable configs.
|
|
certProviderConfigs map[string]*certprovider.BuildableConfig
|
|
}
|
|
|
|
// XDSServers returns the top-level list of management servers to connect to,
|
|
// ordered by priority.
|
|
func (c *Config) XDSServers() ServerConfigs {
|
|
return c.xDSServers
|
|
}
|
|
|
|
// CertProviderConfigs returns a map from certificate provider plugin instance
|
|
// name to their configuration. Callers must not modify the returned map.
|
|
func (c *Config) CertProviderConfigs() map[string]*certprovider.BuildableConfig {
|
|
return c.certProviderConfigs
|
|
}
|
|
|
|
// ServerListenerResourceNameTemplate returns 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.
|
|
func (c *Config) ServerListenerResourceNameTemplate() string {
|
|
return c.serverListenerResourceNameTemplate
|
|
}
|
|
|
|
// ClientDefaultListenerResourceNameTemplate returns 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".
|
|
func (c *Config) ClientDefaultListenerResourceNameTemplate() string {
|
|
return c.clientDefaultListenerResourceNameTemplate
|
|
}
|
|
|
|
// Authorities returns a map of authority name to corresponding configuration.
|
|
// Callers must not modify the returned map.
|
|
//
|
|
// 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.
|
|
func (c *Config) Authorities() map[string]*Authority {
|
|
return c.authorities
|
|
}
|
|
|
|
// Node returns xDS a v3 Node proto corresponding to the node field in the
|
|
// bootstrap configuration, which identifies a specific gRPC instance.
|
|
func (c *Config) Node() *v3corepb.Node {
|
|
return c.node.toProto()
|
|
}
|
|
|
|
// Equal returns true if c equals other.
|
|
func (c *Config) Equal(other *Config) bool {
|
|
switch {
|
|
case c == nil && other == nil:
|
|
return true
|
|
case (c != nil) != (other != nil):
|
|
return false
|
|
case !c.xDSServers.Equal(&other.xDSServers):
|
|
return false
|
|
case !maps.EqualFunc(c.certProviderConfigs, other.certProviderConfigs, func(a, b *certprovider.BuildableConfig) bool { return a.String() == b.String() }):
|
|
return false
|
|
case c.serverListenerResourceNameTemplate != other.serverListenerResourceNameTemplate:
|
|
return false
|
|
case c.clientDefaultListenerResourceNameTemplate != other.clientDefaultListenerResourceNameTemplate:
|
|
return false
|
|
case !maps.EqualFunc(c.authorities, other.authorities, func(a, b *Authority) bool { return a.Equal(b) }):
|
|
return false
|
|
case !c.node.Equal(other.node):
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// String returns a string representation of the Config.
|
|
func (c *Config) String() string {
|
|
s, _ := c.MarshalJSON()
|
|
return string(s)
|
|
}
|
|
|
|
// The following fields correspond 1:1 with the JSON schema for Config.
|
|
type configJSON struct {
|
|
XDSServers ServerConfigs `json:"xds_servers,omitempty"`
|
|
CertificateProviders map[string]certproviderNameAndConfig `json:"certificate_providers,omitempty"`
|
|
ServerListenerResourceNameTemplate string `json:"server_listener_resource_name_template,omitempty"`
|
|
ClientDefaultListenerResourceNameTemplate string `json:"client_default_listener_resource_name_template,omitempty"`
|
|
Authorities map[string]*Authority `json:"authorities,omitempty"`
|
|
Node node `json:"node,omitempty"`
|
|
}
|
|
|
|
// MarshalJSON returns marshaled JSON bytes corresponding to this config.
|
|
func (c *Config) MarshalJSON() ([]byte, error) {
|
|
config := &configJSON{
|
|
XDSServers: c.xDSServers,
|
|
CertificateProviders: c.cpcs,
|
|
ServerListenerResourceNameTemplate: c.serverListenerResourceNameTemplate,
|
|
ClientDefaultListenerResourceNameTemplate: c.clientDefaultListenerResourceNameTemplate,
|
|
Authorities: c.authorities,
|
|
Node: c.node,
|
|
}
|
|
return json.MarshalIndent(config, " ", " ")
|
|
}
|
|
|
|
// UnmarshalJSON takes the json data (the complete bootstrap configuration) and
|
|
// unmarshals it to the struct.
|
|
func (c *Config) UnmarshalJSON(data []byte) error {
|
|
// Initialize the node field with client controlled values. This ensures
|
|
// even if the bootstrap configuration did not contain the node field, we
|
|
// will have a node field with client controlled fields alone.
|
|
config := configJSON{Node: newNode()}
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return fmt.Errorf("xds: json.Unmarshal(%s) failed during bootstrap: %v", string(data), err)
|
|
}
|
|
|
|
c.xDSServers = config.XDSServers
|
|
c.cpcs = config.CertificateProviders
|
|
c.serverListenerResourceNameTemplate = config.ServerListenerResourceNameTemplate
|
|
c.clientDefaultListenerResourceNameTemplate = config.ClientDefaultListenerResourceNameTemplate
|
|
c.authorities = config.Authorities
|
|
c.node = config.Node
|
|
|
|
// Build the certificate providers configuration to ensure that it is valid.
|
|
cpcCfgs := make(map[string]*certprovider.BuildableConfig)
|
|
getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder)
|
|
for instance, nameAndConfig := range c.cpcs {
|
|
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 fmt.Errorf("xds: config parsing for certificate provider plugin %q failed during bootstrap: %v", name, err)
|
|
}
|
|
cpcCfgs[instance] = bc
|
|
}
|
|
c.certProviderConfigs = cpcCfgs
|
|
|
|
// Default value of the default client listener name template is "%s".
|
|
if c.clientDefaultListenerResourceNameTemplate == "" {
|
|
c.clientDefaultListenerResourceNameTemplate = "%s"
|
|
}
|
|
if len(c.xDSServers) == 0 {
|
|
return fmt.Errorf("xds: required field `xds_servers` not found in bootstrap configuration: %s", string(data))
|
|
}
|
|
|
|
// 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 c.authorities {
|
|
prefix := fmt.Sprintf("xdstp://%s", url.PathEscape(name))
|
|
if authority.ClientListenerResourceNameTemplate == "" {
|
|
authority.ClientListenerResourceNameTemplate = prefix + "/envoy.config.listener.v3.Listener/%s"
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(authority.ClientListenerResourceNameTemplate, prefix) {
|
|
return fmt.Errorf("xds: field clientListenerResourceNameTemplate %q of authority %q doesn't start with prefix %q", authority.ClientListenerResourceNameTemplate, name, prefix)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetConfiguration returns the bootstrap configuration initialized by reading
|
|
// the bootstrap file found at ${GRPC_XDS_BOOTSTRAP} or bootstrap contents
|
|
// specified at ${GRPC_XDS_BOOTSTRAP_CONFIG}. If both env vars are set, the
|
|
// former is preferred.
|
|
//
|
|
// If none of the env vars are set, this function returns the fallback
|
|
// configuration if it is not nil. Else, it returns an error.
|
|
//
|
|
// 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 GetConfiguration() (*Config, error) {
|
|
fName := envconfig.XDSBootstrapFileName
|
|
fContent := envconfig.XDSBootstrapFileContent
|
|
|
|
if fName != "" {
|
|
if logger.V(2) {
|
|
logger.Infof("Using bootstrap file with name %q from GRPC_XDS_BOOTSTRAP environment variable", fName)
|
|
}
|
|
cfg, err := bootstrapFileReadFunc(fName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("xds: failed to read bootstrap config from file %q: %v", fName, err)
|
|
}
|
|
return newConfigFromContents(cfg)
|
|
}
|
|
|
|
if fContent != "" {
|
|
if logger.V(2) {
|
|
logger.Infof("Using bootstrap contents from GRPC_XDS_BOOTSTRAP_CONFIG environment variable")
|
|
}
|
|
return newConfigFromContents([]byte(fContent))
|
|
}
|
|
|
|
if cfg := fallbackBootstrapConfig(); cfg != nil {
|
|
if logger.V(2) {
|
|
logger.Infof("Using bootstrap contents from fallback config")
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("bootstrap environment variables (%q or %q) not defined, and no fallback config set", envconfig.XDSBootstrapFileNameEnv, envconfig.XDSBootstrapFileContentEnv)
|
|
}
|
|
|
|
func newConfigFromContents(data []byte) (*Config, error) {
|
|
// Normalize the input configuration.
|
|
buf := bytes.Buffer{}
|
|
err := json.Indent(&buf, data, "", "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("xds: error normalizing JSON bootstrap configuration: %v", err)
|
|
}
|
|
data = bytes.TrimSpace(buf.Bytes())
|
|
|
|
config := &Config{}
|
|
if err := config.UnmarshalJSON(data); err != nil {
|
|
return nil, err
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// ConfigOptionsForTesting specifies options for creating a new bootstrap
|
|
// configuration for testing purposes.
|
|
//
|
|
// # Testing-Only
|
|
type ConfigOptionsForTesting struct {
|
|
// Servers is the top-level xDS server configuration. It contains a list of
|
|
// server configurations.
|
|
Servers json.RawMessage
|
|
// CertificateProviders is the certificate providers configuration.
|
|
CertificateProviders map[string]json.RawMessage
|
|
// ServerListenerResourceNameTemplate is the listener resource name template
|
|
// to be used on the gRPC server.
|
|
ServerListenerResourceNameTemplate string
|
|
// ClientDefaultListenerResourceNameTemplate is the default listener
|
|
// resource name template to be used on the gRPC client.
|
|
ClientDefaultListenerResourceNameTemplate string
|
|
// Authorities is a list of non-default authorities.
|
|
Authorities map[string]json.RawMessage
|
|
// Node identifies the gRPC client/server node in the
|
|
// proxyless service mesh.
|
|
Node json.RawMessage
|
|
}
|
|
|
|
// NewContentsForTesting creates a new bootstrap configuration from the passed in
|
|
// options, for testing purposes.
|
|
//
|
|
// # Testing-Only
|
|
func NewContentsForTesting(opts ConfigOptionsForTesting) ([]byte, error) {
|
|
var servers ServerConfigs
|
|
if err := json.Unmarshal(opts.Servers, &servers); err != nil {
|
|
return nil, err
|
|
}
|
|
certProviders := make(map[string]certproviderNameAndConfig)
|
|
for k, v := range opts.CertificateProviders {
|
|
cp := certproviderNameAndConfig{}
|
|
if err := json.Unmarshal(v, &cp); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal certificate provider configuration for %s: %s", k, string(v))
|
|
}
|
|
certProviders[k] = cp
|
|
}
|
|
authorities := make(map[string]*Authority)
|
|
for k, v := range opts.Authorities {
|
|
a := &Authority{}
|
|
if err := json.Unmarshal(v, a); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal authority configuration for %s: %s", k, string(v))
|
|
}
|
|
authorities[k] = a
|
|
}
|
|
node := newNode()
|
|
if err := json.Unmarshal(opts.Node, &node); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal node configuration %s: %v", string(opts.Node), err)
|
|
}
|
|
cfgJSON := configJSON{
|
|
XDSServers: servers,
|
|
CertificateProviders: certProviders,
|
|
ServerListenerResourceNameTemplate: opts.ServerListenerResourceNameTemplate,
|
|
ClientDefaultListenerResourceNameTemplate: opts.ClientDefaultListenerResourceNameTemplate,
|
|
Authorities: authorities,
|
|
Node: node,
|
|
}
|
|
contents, err := json.MarshalIndent(cfgJSON, " ", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal bootstrap configuration for provided options %+v: %v", opts, err)
|
|
}
|
|
return contents, nil
|
|
}
|
|
|
|
// NewConfigForTesting creates a new bootstrap configuration from the provided
|
|
// contents, for testing purposes.
|
|
//
|
|
// # Testing-Only
|
|
func NewConfigForTesting(contents []byte) (*Config, error) {
|
|
return newConfigFromContents(contents)
|
|
}
|
|
|
|
// certproviderNameAndConfig is the internal representation of
|
|
// the`certificate_providers` field in the bootstrap configuration.
|
|
type certproviderNameAndConfig struct {
|
|
PluginName string `json:"plugin_name"`
|
|
Config json.RawMessage `json:"config"`
|
|
}
|
|
|
|
// locality is the internal representation of the locality field within node.
|
|
type locality struct {
|
|
Region string `json:"region,omitempty"`
|
|
Zone string `json:"zone,omitempty"`
|
|
SubZone string `json:"sub_zone,omitempty"`
|
|
}
|
|
|
|
func (l locality) Equal(other locality) bool {
|
|
return l.Region == other.Region && l.Zone == other.Zone && l.SubZone == other.SubZone
|
|
}
|
|
|
|
func (l locality) isEmpty() bool {
|
|
return l.Equal(locality{})
|
|
}
|
|
|
|
type userAgentVersion struct {
|
|
UserAgentVersion string `json:"user_agent_version,omitempty"`
|
|
}
|
|
|
|
// node is the internal representation of the node field in the bootstrap
|
|
// configuration.
|
|
type node struct {
|
|
ID string `json:"id,omitempty"`
|
|
Cluster string `json:"cluster,omitempty"`
|
|
Locality locality `json:"locality,omitempty"`
|
|
Metadata *structpb.Struct `json:"metadata,omitempty"`
|
|
|
|
// The following fields are controlled by the client implementation and
|
|
// should not unmarshaled from JSON.
|
|
userAgentName string
|
|
userAgentVersionType userAgentVersion
|
|
clientFeatures []string
|
|
}
|
|
|
|
// newNode is a convenience function to create a new node instance with fields
|
|
// controlled by the client implementation set to the desired values.
|
|
func newNode() node {
|
|
return node{
|
|
userAgentName: gRPCUserAgentName,
|
|
userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version},
|
|
clientFeatures: []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper},
|
|
}
|
|
}
|
|
|
|
func (n node) Equal(other node) bool {
|
|
switch {
|
|
case n.ID != other.ID:
|
|
return false
|
|
case n.Cluster != other.Cluster:
|
|
return false
|
|
case !n.Locality.Equal(other.Locality):
|
|
return false
|
|
case n.userAgentName != other.userAgentName:
|
|
return false
|
|
case n.userAgentVersionType != other.userAgentVersionType:
|
|
return false
|
|
}
|
|
|
|
// Consider failures in JSON marshaling as being unable to perform the
|
|
// comparison, and hence return false.
|
|
nMetadata, err := n.Metadata.MarshalJSON()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
otherMetadata, err := other.Metadata.MarshalJSON()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if !bytes.Equal(nMetadata, otherMetadata) {
|
|
return false
|
|
}
|
|
|
|
return slices.Equal(n.clientFeatures, other.clientFeatures)
|
|
}
|
|
|
|
func (n node) toProto() *v3corepb.Node {
|
|
return &v3corepb.Node{
|
|
Id: n.ID,
|
|
Cluster: n.Cluster,
|
|
Locality: func() *v3corepb.Locality {
|
|
if n.Locality.isEmpty() {
|
|
return nil
|
|
}
|
|
return &v3corepb.Locality{
|
|
Region: n.Locality.Region,
|
|
Zone: n.Locality.Zone,
|
|
SubZone: n.Locality.SubZone,
|
|
}
|
|
}(),
|
|
Metadata: proto.Clone(n.Metadata).(*structpb.Struct),
|
|
UserAgentName: n.userAgentName,
|
|
UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: n.userAgentVersionType.UserAgentVersion},
|
|
ClientFeatures: slices.Clone(n.clientFeatures),
|
|
}
|
|
}
|
|
|
|
// SetFallbackBootstrapConfig sets the fallback bootstrap configuration to be
|
|
// used when the bootstrap environment variables are unset.
|
|
//
|
|
// The provided configuration must be valid JSON. Returns a non-nil error if
|
|
// parsing the provided configuration fails.
|
|
func SetFallbackBootstrapConfig(cfgJSON []byte) error {
|
|
config, err := newConfigFromContents(cfgJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
configMu.Lock()
|
|
defer configMu.Unlock()
|
|
fallbackBootstrapCfg = config
|
|
return nil
|
|
}
|
|
|
|
// UnsetFallbackBootstrapConfigForTesting unsets the fallback bootstrap
|
|
// configuration to be used when the bootstrap environment variables are unset.
|
|
//
|
|
// # Testing-Only
|
|
func UnsetFallbackBootstrapConfigForTesting() {
|
|
configMu.Lock()
|
|
defer configMu.Unlock()
|
|
fallbackBootstrapCfg = nil
|
|
}
|
|
|
|
// fallbackBootstrapConfig returns the fallback bootstrap configuration
|
|
// that will be used by the xDS client when the bootstrap environment
|
|
// variables are unset.
|
|
func fallbackBootstrapConfig() *Config {
|
|
configMu.Lock()
|
|
defer configMu.Unlock()
|
|
return fallbackBootstrapCfg
|
|
}
|
|
|
|
var (
|
|
configMu sync.Mutex
|
|
fallbackBootstrapCfg *Config
|
|
)
|