/* * * 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" "net/url" "os" "strings" "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/internal/pretty" "google.golang.org/grpc/xds/bootstrap" "google.golang.org/protobuf/encoding/protojson" v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" ) 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. // - A value of "ignore_resource_deletion" indicates that the client should // ignore deletion of Listener and Cluster resources in updates from the // server. serverFeaturesV3 = "xds_v3" 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. type ChannelCreds struct { // Type contains a unique name identifying the credentials type. The only // supported types currently are "google_default" and "insecure". Type string // Config contains the JSON configuration associated with the credentials. Config json.RawMessage } // 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) } // ServerConfig contains the configuration to connect to a server, including // URI, creds, and transport API version (e.g. v2 or v3). // // It contains unexported fields that are initialized when unmarshaled from JSON // using either the UnmarshalJSON() method or the ServerConfigFromJSON() // function. Hence users are strongly encouraged not to use a literal struct // initialization to create an instance of this type, but instead unmarshal from // JSON using one of the two available options. 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 communicationg with this // xDS server. It is also used to dedup servers with the same server URI. Creds ChannelCreds // ServerFeatures contains a list of features supported by this xDS server. // It is also used to dedup servers with the same server URI and creds. 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 as a grpc.DialOption for easy access when // dialing this xDS server. credsDialOption grpc.DialOption // IgnoreResourceDeletion 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. IgnoreResourceDeletion bool // Cleanups are called when the xDS client for this server is closed. Allows // cleaning up resources created specifically for this ServerConfig. Cleanups []func() } // CredsDialOption returns the configured credentials as a grpc dial option. func (sc *ServerConfig) CredsDialOption() grpc.DialOption { return sc.credsDialOption } // 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 { features := strings.Join(sc.ServerFeatures, "-") return strings.Join([]string{sc.ServerURI, sc.Creds.String(), features}, "-") } // MarshalJSON marshals the ServerConfig to json. func (sc ServerConfig) MarshalJSON() ([]byte, error) { server := xdsServer{ ServerURI: sc.ServerURI, ChannelCreds: []channelCreds{{Type: sc.Creds.Type, Config: sc.Creds.Config}}, ServerFeatures: sc.ServerFeatures, } server.ServerFeatures = []string{serverFeaturesV3} if sc.IgnoreResourceDeletion { server.ServerFeatures = append(server.ServerFeatures, serverFeaturesIgnoreResourceDeletion) } return json.Marshal(server) } // UnmarshalJSON takes the json data (a server) and unmarshals it to the struct. func (sc *ServerConfig) UnmarshalJSON(data []byte) error { var server xdsServer if err := json.Unmarshal(data, &server); err != nil { return fmt.Errorf("xds: json.Unmarshal(data) for field ServerConfig failed during bootstrap: %v", err) } sc.ServerURI = server.ServerURI sc.ServerFeatures = server.ServerFeatures for _, f := range server.ServerFeatures { if f == serverFeaturesIgnoreResourceDeletion { sc.IgnoreResourceDeletion = true } } 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.Creds = ChannelCreds(cc) sc.credsDialOption = grpc.WithCredentialsBundle(bundle) sc.Cleanups = append(sc.Cleanups, cancel) break } return nil } // ServerConfigFromJSON creates a new ServerConfig from the given JSON // configuration. This is the preferred way of creating a ServerConfig when // hand-crafting the JSON configuration. func ServerConfigFromJSON(data []byte) (*ServerConfig, error) { sc := new(ServerConfig) if err := sc.UnmarshalJSON(data); err != nil { return nil, err } return sc, nil } // 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 !sc.Creds.Equal(other.Creds): return false case !equalStringSlice(sc.ServerFeatures, other.ServerFeatures): return false } return true } func equalStringSlice(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } // unmarshalJSONServerConfigSlice unmarshals JSON to a slice. func unmarshalJSONServerConfigSlice(data []byte) ([]*ServerConfig, error) { var servers []*ServerConfig if err := json.Unmarshal(data, &servers); err != nil { return nil, fmt.Errorf("failed to unmarshal JSON to []*ServerConfig: %v", err) } if len(servers) < 1 { return nil, fmt.Errorf("no management server found in JSON") } return servers, 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:///". If it does not, // that is considered a bootstrap file parsing error. // // If not present in the bootstrap file, defaults to // "xdstp:///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": servers, err := unmarshalJSONServerConfigSlice(v) if err != nil { return fmt.Errorf("xds: json.Unmarshal(data) for field %q failed during bootstrap: %v", k, err) } a.XDSServer = servers[0] 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 // NodeProto contains the Node proto to be used in xDS requests. This will be // of type *v3corepb.Node. NodeProto *v3corepb.Node } type channelCreds struct { Type string `json:"type"` Config json.RawMessage `json:"config,omitempty"` } 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. logger.Debugf("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} or bootstrap contents specified // at ${GRPC_XDS_BOOTSTRAP_CONFIG}. If both env vars are set, the former is // preferred. // // We support a credential registration mechanism and only credentials // registered through that mechanism will be accepted here. See package // `xds/bootstrap` for details. // // 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) } return newConfigFromContents(data) } // NewConfigFromContents returns a new Config using the specified // bootstrap file contents instead of reading the environment variable. func NewConfigFromContents(data []byte) (*Config, error) { return newConfigFromContents(data) } 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 opts := protojson.UnmarshalOptions{DiscardUnknown: true} for k, v := range jsonData { switch k { case "node": node = &v3corepb.Node{} if err := opts.Unmarshal(v, node); err != nil { return nil, fmt.Errorf("xds: protojson.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err) } case "xds_servers": servers, err := unmarshalJSONServerConfigSlice(v) if err != nil { return nil, fmt.Errorf("xds: json.Unmarshal(data) for field %q failed during bootstrap: %v", k, err) } config.XDSServer = servers[0] 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 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 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: logger.Warningf("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.CredsDialOption() == 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:///" // - if not set, it defaults to "xdstp:///envoy.config.listener.v3.Listener/%s" for name, authority := range config.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 nil, fmt.Errorf("xds: field ClientListenerResourceNameTemplate %q of authority %q doesn't start with prefix %q", authority.ClientListenerResourceNameTemplate, name, prefix) } } // Performing post-production on the node information. Some additional fields // which are not expected to be set in the bootstrap file are populated here. if node == nil { node = &v3corepb.Node{} } node.UserAgentName = gRPCUserAgentName node.UserAgentVersionType = &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version} node.ClientFeatures = append(node.ClientFeatures, clientFeatureNoOverprovisioning, clientFeatureResourceWrapper) config.NodeProto = node if logger.V(2) { logger.Infof("Bootstrap config for creating xds-client: %v", pretty.ToJSON(config)) } return config, nil }