mirror of
https://github.com/coredns/coredns.git
synced 2025-11-21 19:32:21 -05:00
Copy grpc xDS client from grpc-go
Signed-off-by: Miek Gieben <miek@miek.nl>
This commit is contained in:
168
plugin/traffic/xds/bootstrap/bootstrap.go
Normal file
168
plugin/traffic/xds/bootstrap/bootstrap.go
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
*
|
||||
* 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"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/google"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
|
||||
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
||||
)
|
||||
|
||||
const (
|
||||
// Environment variable which holds the name of the xDS bootstrap file.
|
||||
fileEnv = "GRPC_XDS_BOOTSTRAP"
|
||||
// Type name for Google default credentials.
|
||||
googleDefaultCreds = "google_default"
|
||||
)
|
||||
|
||||
var gRPCVersion = fmt.Sprintf("gRPC-Go %s", grpc.Version)
|
||||
|
||||
// For overriding in unit tests.
|
||||
var fileReadFunc = ioutil.ReadFile
|
||||
|
||||
// Config provides the xDS client with several key bits of information that it
|
||||
// requires in its interaction with an xDS server. The Config is initialized
|
||||
// from the bootstrap file.
|
||||
type Config struct {
|
||||
// BalancerName is the name of the xDS server to connect to.
|
||||
//
|
||||
// The bootstrap file contains a list of servers (with name+creds), but we
|
||||
// pick the first one.
|
||||
BalancerName string
|
||||
// Creds contains the credentials to be used while talking to the xDS
|
||||
// server, as a grpc.DialOption.
|
||||
Creds grpc.DialOption
|
||||
// NodeProto contains the node proto to be used in xDS requests.
|
||||
NodeProto *corepb.Node
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// NewConfig returns a new instance of Config initialized by reading the
|
||||
// bootstrap file found at ${GRPC_XDS_BOOTSTRAP}.
|
||||
//
|
||||
// The format of the bootstrap file will be as follows:
|
||||
// {
|
||||
// "xds_server": {
|
||||
// "server_uri": <string containing URI of xds server>,
|
||||
// "channel_creds": [
|
||||
// {
|
||||
// "type": <string containing channel cred type>,
|
||||
// "config": <JSON object containing config for the type>
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// "node": <JSON form of corepb.Node proto>
|
||||
// }
|
||||
//
|
||||
// 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 {
|
||||
config := &Config{}
|
||||
|
||||
fName, ok := os.LookupEnv(fileEnv)
|
||||
if !ok {
|
||||
grpclog.Errorf("xds: %s environment variable not set", fileEnv)
|
||||
return config
|
||||
}
|
||||
|
||||
grpclog.Infof("xds: Reading bootstrap file from %s", fName)
|
||||
data, err := fileReadFunc(fName)
|
||||
if err != nil {
|
||||
grpclog.Errorf("xds: bootstrap file {%v} read failed: %v", fName, err)
|
||||
return config
|
||||
}
|
||||
|
||||
var jsonData map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &jsonData); err != nil {
|
||||
grpclog.Errorf("xds: json.Unmarshal(%v) failed during bootstrap: %v", string(data), err)
|
||||
return config
|
||||
}
|
||||
|
||||
m := jsonpb.Unmarshaler{AllowUnknownFields: true}
|
||||
for k, v := range jsonData {
|
||||
switch k {
|
||||
case "node":
|
||||
n := &corepb.Node{}
|
||||
if err := m.Unmarshal(bytes.NewReader(v), n); err != nil {
|
||||
grpclog.Errorf("xds: jsonpb.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
|
||||
break
|
||||
}
|
||||
config.NodeProto = n
|
||||
case "xds_servers":
|
||||
var servers []*xdsServer
|
||||
if err := json.Unmarshal(v, &servers); err != nil {
|
||||
grpclog.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
|
||||
break
|
||||
}
|
||||
if len(servers) < 1 {
|
||||
grpclog.Errorf("xds: bootstrap file parsing failed during bootstrap: file doesn't contain any xds server to connect to")
|
||||
break
|
||||
}
|
||||
xs := servers[0]
|
||||
config.BalancerName = xs.ServerURI
|
||||
for _, cc := range xs.ChannelCreds {
|
||||
if cc.Type == googleDefaultCreds {
|
||||
config.Creds = grpc.WithCredentialsBundle(google.NewComputeEngineCredentials())
|
||||
// We stop at the first credential type that we support.
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Do not fail the xDS bootstrap when an unknown field is seen.
|
||||
grpclog.Warningf("xds: unexpected data in bootstrap file: {%v, %v}", k, string(v))
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't find a nodeProto in the bootstrap file, we just create an
|
||||
// empty one here. That way, callers of this function can always expect
|
||||
// that the NodeProto field is non-nil.
|
||||
if config.NodeProto == nil {
|
||||
config.NodeProto = &corepb.Node{}
|
||||
}
|
||||
config.NodeProto.BuildVersion = gRPCVersion
|
||||
|
||||
grpclog.Infof("xds: bootstrap.NewConfig returning: %+v", config)
|
||||
return config
|
||||
}
|
||||
260
plugin/traffic/xds/bootstrap/bootstrap_test.go
Normal file
260
plugin/traffic/xds/bootstrap/bootstrap_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
*
|
||||
* 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
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/google"
|
||||
|
||||
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
||||
structpb "github.com/golang/protobuf/ptypes/struct"
|
||||
)
|
||||
|
||||
var (
|
||||
nodeProto = &corepb.Node{
|
||||
Id: "ENVOY_NODE_ID",
|
||||
Metadata: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"TRAFFICDIRECTOR_GRPC_HOSTNAME": {
|
||||
Kind: &structpb.Value_StringValue{StringValue: "trafficdirector"},
|
||||
},
|
||||
},
|
||||
},
|
||||
BuildVersion: gRPCVersion,
|
||||
}
|
||||
nilCredsConfig = &Config{
|
||||
BalancerName: "trafficdirector.googleapis.com:443",
|
||||
Creds: nil,
|
||||
NodeProto: nodeProto,
|
||||
}
|
||||
nonNilCredsConfig = &Config{
|
||||
BalancerName: "trafficdirector.googleapis.com:443",
|
||||
Creds: grpc.WithCredentialsBundle(google.NewComputeEngineCredentials()),
|
||||
NodeProto: nodeProto,
|
||||
}
|
||||
)
|
||||
|
||||
// TestNewConfig exercises the functionality in NewConfig with different
|
||||
// bootstrap file contents. It overrides the fileReadFunc by returning
|
||||
// bootstrap file contents defined in this test, instead of reading from a
|
||||
// file.
|
||||
func TestNewConfig(t *testing.T) {
|
||||
bootstrapFileMap := map[string]string{
|
||||
"empty": "",
|
||||
"badJSON": `["test": 123]`,
|
||||
"emptyNodeProto": `
|
||||
{
|
||||
"xds_servers" : [{
|
||||
"server_uri": "trafficdirector.googleapis.com:443"
|
||||
}]
|
||||
}`,
|
||||
"emptyXdsServer": `
|
||||
{
|
||||
"node": {
|
||||
"id": "ENVOY_NODE_ID",
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"unknownTopLevelFieldInFile": `
|
||||
{
|
||||
"node": {
|
||||
"id": "ENVOY_NODE_ID",
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
|
||||
}
|
||||
},
|
||||
"xds_servers" : [{
|
||||
"server_uri": "trafficdirector.googleapis.com:443",
|
||||
"channel_creds": [
|
||||
{ "type": "not-google-default" }
|
||||
]
|
||||
}],
|
||||
"unknownField": "foobar"
|
||||
}`,
|
||||
"unknownFieldInNodeProto": `
|
||||
{
|
||||
"node": {
|
||||
"id": "ENVOY_NODE_ID",
|
||||
"unknownField": "foobar",
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"unknownFieldInXdsServer": `
|
||||
{
|
||||
"node": {
|
||||
"id": "ENVOY_NODE_ID",
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
|
||||
}
|
||||
},
|
||||
"xds_servers" : [{
|
||||
"server_uri": "trafficdirector.googleapis.com:443",
|
||||
"channel_creds": [
|
||||
{ "type": "not-google-default" }
|
||||
],
|
||||
"unknownField": "foobar"
|
||||
}]
|
||||
}`,
|
||||
"emptyChannelCreds": `
|
||||
{
|
||||
"node": {
|
||||
"id": "ENVOY_NODE_ID",
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
|
||||
}
|
||||
},
|
||||
"xds_servers" : [{
|
||||
"server_uri": "trafficdirector.googleapis.com:443"
|
||||
}]
|
||||
}`,
|
||||
"nonGoogleDefaultCreds": `
|
||||
{
|
||||
"node": {
|
||||
"id": "ENVOY_NODE_ID",
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
|
||||
}
|
||||
},
|
||||
"xds_servers" : [{
|
||||
"server_uri": "trafficdirector.googleapis.com:443",
|
||||
"channel_creds": [
|
||||
{ "type": "not-google-default" }
|
||||
]
|
||||
}]
|
||||
}`,
|
||||
"multipleChannelCreds": `
|
||||
{
|
||||
"node": {
|
||||
"id": "ENVOY_NODE_ID",
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
|
||||
}
|
||||
},
|
||||
"xds_servers" : [{
|
||||
"server_uri": "trafficdirector.googleapis.com:443",
|
||||
"channel_creds": [
|
||||
{ "type": "not-google-default" },
|
||||
{ "type": "google_default" }
|
||||
]
|
||||
}]
|
||||
}`,
|
||||
"goodBootstrap": `
|
||||
{
|
||||
"node": {
|
||||
"id": "ENVOY_NODE_ID",
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
|
||||
}
|
||||
},
|
||||
"xds_servers" : [{
|
||||
"server_uri": "trafficdirector.googleapis.com:443",
|
||||
"channel_creds": [
|
||||
{ "type": "google_default" }
|
||||
]
|
||||
}]
|
||||
}`,
|
||||
"multipleXDSServers": `
|
||||
{
|
||||
"node": {
|
||||
"id": "ENVOY_NODE_ID",
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
|
||||
}
|
||||
},
|
||||
"xds_servers" : [
|
||||
{
|
||||
"server_uri": "trafficdirector.googleapis.com:443",
|
||||
"channel_creds": [{ "type": "google_default" }]
|
||||
},
|
||||
{
|
||||
"server_uri": "backup.never.use.com:1234",
|
||||
"channel_creds": [{ "type": "not-google-default" }]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
}
|
||||
|
||||
oldFileReadFunc := fileReadFunc
|
||||
fileReadFunc = func(name string) ([]byte, error) {
|
||||
if b, ok := bootstrapFileMap[name]; ok {
|
||||
return []byte(b), nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
defer func() {
|
||||
fileReadFunc = oldFileReadFunc
|
||||
os.Unsetenv(fileEnv)
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wantConfig *Config
|
||||
}{
|
||||
{"nonExistentBootstrapFile", &Config{}},
|
||||
{"empty", &Config{}},
|
||||
{"badJSON", &Config{}},
|
||||
{"emptyNodeProto", &Config{
|
||||
BalancerName: "trafficdirector.googleapis.com:443",
|
||||
NodeProto: &corepb.Node{BuildVersion: gRPCVersion},
|
||||
}},
|
||||
{"emptyXdsServer", &Config{NodeProto: nodeProto}},
|
||||
{"unknownTopLevelFieldInFile", nilCredsConfig},
|
||||
{"unknownFieldInNodeProto", &Config{NodeProto: nodeProto}},
|
||||
{"unknownFieldInXdsServer", nilCredsConfig},
|
||||
{"emptyChannelCreds", nilCredsConfig},
|
||||
{"nonGoogleDefaultCreds", nilCredsConfig},
|
||||
{"multipleChannelCreds", nonNilCredsConfig},
|
||||
{"goodBootstrap", nonNilCredsConfig},
|
||||
{"multipleXDSServers", nonNilCredsConfig},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if err := os.Setenv(fileEnv, test.name); err != nil {
|
||||
t.Fatalf("os.Setenv(%s, %s) failed with error: %v", fileEnv, test.name, err)
|
||||
}
|
||||
config := NewConfig()
|
||||
if config.BalancerName != test.wantConfig.BalancerName {
|
||||
t.Errorf("config.BalancerName is %s, want %s", config.BalancerName, test.wantConfig.BalancerName)
|
||||
}
|
||||
if !proto.Equal(config.NodeProto, test.wantConfig.NodeProto) {
|
||||
t.Errorf("config.NodeProto is %#v, want %#v", config.NodeProto, test.wantConfig.NodeProto)
|
||||
}
|
||||
if (config.Creds != nil) != (test.wantConfig.Creds != nil) {
|
||||
t.Errorf("config.Creds is %#v, want %#v", config.Creds, test.wantConfig.Creds)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfigEnvNotSet(t *testing.T) {
|
||||
os.Unsetenv(fileEnv)
|
||||
wantConfig := Config{}
|
||||
if config := NewConfig(); *config != wantConfig {
|
||||
t.Errorf("NewConfig() returned : %#v, wanted an empty Config object", config)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user