1/*
2 *
3 * Copyright 2019 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18
19// Package bootstrap provides the functionality to initialize certain aspects
20// of an xDS client by reading a bootstrap file.
21package bootstrap
22
23import (
24	"bytes"
25	"encoding/json"
26	"fmt"
27	"io/ioutil"
28
29	v2corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
30	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
31	"github.com/golang/protobuf/jsonpb"
32	"github.com/golang/protobuf/proto"
33	"google.golang.org/grpc"
34	"google.golang.org/grpc/credentials/google"
35	"google.golang.org/grpc/credentials/insecure"
36	"google.golang.org/grpc/credentials/tls/certprovider"
37	"google.golang.org/grpc/internal"
38	"google.golang.org/grpc/internal/pretty"
39	"google.golang.org/grpc/internal/xds/env"
40	"google.golang.org/grpc/xds/internal/version"
41)
42
43const (
44	// The "server_features" field in the bootstrap file contains a list of
45	// features supported by the server. A value of "xds_v3" indicates that the
46	// server supports the v3 version of the xDS transport protocol.
47	serverFeaturesV3 = "xds_v3"
48
49	// Type name for Google default credentials.
50	credsGoogleDefault              = "google_default"
51	credsInsecure                   = "insecure"
52	gRPCUserAgentName               = "gRPC Go"
53	clientFeatureNoOverprovisioning = "envoy.lb.does_not_support_overprovisioning"
54)
55
56var gRPCVersion = fmt.Sprintf("%s %s", gRPCUserAgentName, grpc.Version)
57
58// For overriding in unit tests.
59var bootstrapFileReadFunc = ioutil.ReadFile
60
61// Config provides the xDS client with several key bits of information that it
62// requires in its interaction with the management server. The Config is
63// initialized from the bootstrap file.
64type Config struct {
65	// BalancerName is the name of the management server to connect to.
66	//
67	// The bootstrap file contains a list of servers (with name+creds), but we
68	// pick the first one.
69	BalancerName string
70	// Creds contains the credentials to be used while talking to the xDS
71	// server, as a grpc.DialOption.
72	Creds grpc.DialOption
73	// TransportAPI indicates the API version of xDS transport protocol to use.
74	// This describes the xDS gRPC endpoint and version of
75	// DiscoveryRequest/Response used on the wire.
76	TransportAPI version.TransportAPI
77	// NodeProto contains the Node proto to be used in xDS requests. The actual
78	// type depends on the transport protocol version used.
79	NodeProto proto.Message
80	// CertProviderConfigs contains a mapping from certificate provider plugin
81	// instance names to parsed buildable configs.
82	CertProviderConfigs map[string]*certprovider.BuildableConfig
83	// ServerListenerResourceNameTemplate is a template for the name of the
84	// Listener resource to subscribe to for a gRPC server. If the token `%s` is
85	// present in the string, it will be replaced with the server's listening
86	// "IP:port" (e.g., "0.0.0.0:8080", "[::]:8080"). For example, a value of
87	// "example/resource/%s" could become "example/resource/0.0.0.0:8080".
88	ServerListenerResourceNameTemplate string
89}
90
91type channelCreds struct {
92	Type   string          `json:"type"`
93	Config json.RawMessage `json:"config"`
94}
95
96type xdsServer struct {
97	ServerURI      string         `json:"server_uri"`
98	ChannelCreds   []channelCreds `json:"channel_creds"`
99	ServerFeatures []string       `json:"server_features"`
100}
101
102func bootstrapConfigFromEnvVariable() ([]byte, error) {
103	fName := env.BootstrapFileName
104	fContent := env.BootstrapFileContent
105
106	// Bootstrap file name has higher priority than bootstrap content.
107	if fName != "" {
108		// If file name is set
109		// - If file not found (or other errors), fail
110		// - Otherwise, use the content.
111		//
112		// Note that even if the content is invalid, we don't failover to the
113		// file content env variable.
114		logger.Debugf("xds: using bootstrap file with name %q", fName)
115		return bootstrapFileReadFunc(fName)
116	}
117
118	if fContent != "" {
119		return []byte(fContent), nil
120	}
121
122	return nil, fmt.Errorf("none of the bootstrap environment variables (%q or %q) defined", env.BootstrapFileNameEnv, env.BootstrapFileContentEnv)
123}
124
125// NewConfig returns a new instance of Config initialized by reading the
126// bootstrap file found at ${GRPC_XDS_BOOTSTRAP}.
127//
128// The format of the bootstrap file will be as follows:
129// {
130//    "xds_servers": [
131//      {
132//        "server_uri": <string containing URI of management server>,
133//        "channel_creds": [
134//          {
135//            "type": <string containing channel cred type>,
136//            "config": <JSON object containing config for the type>
137//          }
138//        ],
139//        "server_features": [ ... ],
140//      }
141//    ],
142//    "node": <JSON form of Node proto>,
143//    "certificate_providers" : {
144//      "default": {
145//        "plugin_name": "default-plugin-name",
146//        "config": { default plugin config in JSON }
147//       },
148//      "foo": {
149//        "plugin_name": "foo",
150//        "config": { foo plugin config in JSON }
151//      }
152//    },
153//    "server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s"
154// }
155//
156// Currently, we support exactly one type of credential, which is
157// "google_default", where we use the host's default certs for transport
158// credentials and a Google oauth token for call credentials.
159//
160// This function tries to process as much of the bootstrap file as possible (in
161// the presence of the errors) and may return a Config object with certain
162// fields left unspecified, in which case the caller should use some sane
163// defaults.
164func NewConfig() (*Config, error) {
165	data, err := bootstrapConfigFromEnvVariable()
166	if err != nil {
167		return nil, fmt.Errorf("xds: Failed to read bootstrap config: %v", err)
168	}
169	logger.Debugf("Bootstrap content: %s", data)
170	return NewConfigFromContents(data)
171}
172
173// NewConfigFromContents returns a new Config using the specified bootstrap
174// file contents instead of reading the environment variable.  This is only
175// suitable for testing purposes.
176func NewConfigFromContents(data []byte) (*Config, error) {
177	config := &Config{}
178
179	var jsonData map[string]json.RawMessage
180	if err := json.Unmarshal(data, &jsonData); err != nil {
181		return nil, fmt.Errorf("xds: Failed to parse bootstrap config: %v", err)
182	}
183
184	serverSupportsV3 := false
185	m := jsonpb.Unmarshaler{AllowUnknownFields: true}
186	for k, v := range jsonData {
187		switch k {
188		case "node":
189			// We unconditionally convert the JSON into a v3.Node proto. The v3
190			// proto does not contain the deprecated field "build_version" from
191			// the v2 proto. We do not expect the bootstrap file to contain the
192			// "build_version" field. In any case, the unmarshal will succeed
193			// because we have set the `AllowUnknownFields` option on the
194			// unmarshaler.
195			n := &v3corepb.Node{}
196			if err := m.Unmarshal(bytes.NewReader(v), n); err != nil {
197				return nil, fmt.Errorf("xds: jsonpb.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
198			}
199			config.NodeProto = n
200		case "xds_servers":
201			var servers []*xdsServer
202			if err := json.Unmarshal(v, &servers); err != nil {
203				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
204			}
205			if len(servers) < 1 {
206				return nil, fmt.Errorf("xds: bootstrap file parsing failed during bootstrap: file doesn't contain any management server to connect to")
207			}
208			xs := servers[0]
209			config.BalancerName = xs.ServerURI
210			for _, cc := range xs.ChannelCreds {
211				// We stop at the first credential type that we support.
212				if cc.Type == credsGoogleDefault {
213					config.Creds = grpc.WithCredentialsBundle(google.NewDefaultCredentials())
214					break
215				} else if cc.Type == credsInsecure {
216					config.Creds = grpc.WithTransportCredentials(insecure.NewCredentials())
217					break
218				}
219			}
220			for _, f := range xs.ServerFeatures {
221				switch f {
222				case serverFeaturesV3:
223					serverSupportsV3 = true
224				}
225			}
226		case "certificate_providers":
227			var providerInstances map[string]json.RawMessage
228			if err := json.Unmarshal(v, &providerInstances); err != nil {
229				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
230			}
231			configs := make(map[string]*certprovider.BuildableConfig)
232			getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder)
233			for instance, data := range providerInstances {
234				var nameAndConfig struct {
235					PluginName string          `json:"plugin_name"`
236					Config     json.RawMessage `json:"config"`
237				}
238				if err := json.Unmarshal(data, &nameAndConfig); err != nil {
239					return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), instance, err)
240				}
241
242				name := nameAndConfig.PluginName
243				parser := getBuilder(nameAndConfig.PluginName)
244				if parser == nil {
245					// We ignore plugins that we do not know about.
246					continue
247				}
248				bc, err := parser.ParseConfig(nameAndConfig.Config)
249				if err != nil {
250					return nil, fmt.Errorf("xds: Config parsing for plugin %q failed: %v", name, err)
251				}
252				configs[instance] = bc
253			}
254			config.CertProviderConfigs = configs
255		case "server_listener_resource_name_template":
256			if err := json.Unmarshal(v, &config.ServerListenerResourceNameTemplate); err != nil {
257				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
258			}
259		}
260		// Do not fail the xDS bootstrap when an unknown field is seen. This can
261		// happen when an older version client reads a newer version bootstrap
262		// file with new fields.
263	}
264
265	if config.BalancerName == "" {
266		return nil, fmt.Errorf("xds: Required field %q not found in bootstrap %s", "xds_servers.server_uri", jsonData["xds_servers"])
267	}
268	if config.Creds == nil {
269		return nil, fmt.Errorf("xds: Required field %q doesn't contain valid value in bootstrap %s", "xds_servers.channel_creds", jsonData["xds_servers"])
270	}
271
272	// We end up using v3 transport protocol version only if the server supports
273	// v3, indicated by the presence of "xds_v3" in server_features. The default
274	// value of the enum type "version.TransportAPI" is v2.
275	if serverSupportsV3 {
276		config.TransportAPI = version.TransportV3
277	}
278
279	if err := config.updateNodeProto(); err != nil {
280		return nil, err
281	}
282	logger.Infof("Bootstrap config for creating xds-client: %v", pretty.ToJSON(config))
283	return config, nil
284}
285
286// updateNodeProto updates the node proto read from the bootstrap file.
287//
288// Node proto in Config contains a v3.Node protobuf message corresponding to the
289// JSON contents found in the bootstrap file. This method performs some post
290// processing on it:
291// 1. If we don't find a nodeProto in the bootstrap file, we create an empty one
292// here. That way, callers of this function can always expect that the NodeProto
293// field is non-nil.
294// 2. If the transport protocol version to be used is not v3, we convert the
295// current v3.Node proto in a v2.Node proto.
296// 3. Some additional fields which are not expected to be set in the bootstrap
297// file are populated here.
298func (c *Config) updateNodeProto() error {
299	if c.TransportAPI == version.TransportV3 {
300		v3, _ := c.NodeProto.(*v3corepb.Node)
301		if v3 == nil {
302			v3 = &v3corepb.Node{}
303		}
304		v3.UserAgentName = gRPCUserAgentName
305		v3.UserAgentVersionType = &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}
306		v3.ClientFeatures = append(v3.ClientFeatures, clientFeatureNoOverprovisioning)
307		c.NodeProto = v3
308		return nil
309	}
310
311	v2 := &v2corepb.Node{}
312	if c.NodeProto != nil {
313		v3, err := proto.Marshal(c.NodeProto)
314		if err != nil {
315			return fmt.Errorf("xds: proto.Marshal(%v): %v", c.NodeProto, err)
316		}
317		if err := proto.Unmarshal(v3, v2); err != nil {
318			return fmt.Errorf("xds: proto.Unmarshal(%v): %v", v3, err)
319		}
320	}
321	c.NodeProto = v2
322
323	// BuildVersion is deprecated, and is replaced by user_agent_name and
324	// user_agent_version. But the management servers are still using the old
325	// field, so we will keep both set.
326	v2.BuildVersion = gRPCVersion
327	v2.UserAgentName = gRPCUserAgentName
328	v2.UserAgentVersionType = &v2corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}
329	v2.ClientFeatures = append(v2.ClientFeatures, clientFeatureNoOverprovisioning)
330	return nil
331}
332