Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

certprovider-api: bootstrap support for certificate providers #3901

Merged
merged 2 commits into from Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions credentials/tls/certprovider/provider.go
Expand Up @@ -47,9 +47,9 @@ func Register(b Builder) {
m[b.Name()] = b
}

// getBuilder returns the Provider builder registered with the given name.
// GetBuilder returns the Provider builder registered with the given name.
// If no builder is registered with the provided name, nil will be returned.
func getBuilder(name string) Builder {
func GetBuilder(name string) Builder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package is not internal, so this means the users can GetBuilder() directly.
Did we want users to use the store only?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched back to not exporting, and using the internal package trick now.

if b, ok := m[name]; ok {
return b
}
Expand Down
2 changes: 1 addition & 1 deletion credentials/tls/certprovider/store.go
Expand Up @@ -76,7 +76,7 @@ func GetProvider(name string, config interface{}, opts Options) (Provider, error
provStore.mu.Lock()
defer provStore.mu.Unlock()

builder := getBuilder(name)
builder := GetBuilder(name)
if builder == nil {
return nil, fmt.Errorf("no registered builder for provider name: %s", name)
}
Expand Down
33 changes: 33 additions & 0 deletions xds/internal/client/bootstrap/bootstrap.go
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/golang/protobuf/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/google"
"google.golang.org/grpc/credentials/tls/certprovider"
"google.golang.org/grpc/xds/internal/version"
)

Expand Down Expand Up @@ -77,6 +78,9 @@ type Config struct {
// NodeProto contains the Node proto to be used in xDS requests. The actual
// type depends on the transport protocol version used.
NodeProto proto.Message
// CertProviderConfigs contain parsed configs for supported certificate
// provider plugins found in the bootstrap file.
CertProviderConfigs map[string]certprovider.StableConfig
}

type channelCreds struct {
Expand All @@ -103,6 +107,10 @@ type xdsServer struct {
// }
// ],
// "server_features": [ ... ]
// "certificate_providers" : {
// "default": { default cert provider config },
// "foo": { config for provider foo }
// }
// },
// "node": <JSON form of Node proto>
// }
Expand Down Expand Up @@ -182,6 +190,31 @@ func NewConfig() (*Config, error) {
serverSupportsV3 = true
}
}
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.StableConfig)
for instance, data := range providerInstances {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is an instance?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A plugin_instance, which represents a plugin_name+plugin_config.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably minor detail.

Why does the instance need an "instance_name"? Would we have two instances with the same plugin_name, but different config? And since we always stop at the first valid config, what's the point?

This is similar to what we need for the balancing policy + config. We didn't make it a map[string]{policy+config}, but a []{policy+config}.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can have two different instances with same plugin_name but different config. An instance is supposed to represent an instantiation of a plugin (which includes the plugin_name+config).

We don't stop at the first valid config here. We process all configs and use the instance specified by the control plane.

var providerConfigs map[string]json.RawMessage
if err := json.Unmarshal(data, &providerConfigs); err != nil {
return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), instance, err)
}
for name, cfg := range providerConfigs {
parser := certprovider.GetBuilder(name)
if parser == nil {
// We ignore plugins that we do not know about.
continue
}
c, err := parser.ParseConfig(cfg)
if err != nil {
return nil, fmt.Errorf("xds: Config parsing for plugin %q failed: %v", name, err)
}
configs[instance] = c
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not need the name in the returned config as well? How will the consumer of this config know which builder to use?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Thank you. Fixed.

}
}
config.CertProviderConfigs = configs
}
// 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
Expand Down
247 changes: 247 additions & 0 deletions xds/internal/client/bootstrap/bootstrap_test.go
Expand Up @@ -19,6 +19,8 @@
package bootstrap

import (
"encoding/json"
"errors"
"fmt"
"os"
"testing"
Expand All @@ -28,8 +30,10 @@ import (
"github.com/golang/protobuf/proto"
structpb "github.com/golang/protobuf/ptypes/struct"
"github.com/google/go-cmp/cmp"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials/google"
"google.golang.org/grpc/credentials/tls/certprovider"
"google.golang.org/grpc/xds/internal/version"
)

Expand Down Expand Up @@ -233,6 +237,24 @@ func (c *Config) compare(want *Config) error {
if diff := cmp.Diff(want.NodeProto, c.NodeProto, cmp.Comparer(proto.Equal)); diff != "" {
return fmt.Errorf("config.NodeProto diff (-want, +got):\n%s", diff)
}

// A vanilla cmp.Equal or cmp.Diff will not produce useful error message
// here. So, we iterate through the list of configs and compare them one at
// a time.
gotCfgs := c.CertProviderConfigs
wantCfgs := want.CertProviderConfigs
if len(gotCfgs) != len(wantCfgs) {
return fmt.Errorf("config.CertProviderConfigs is %d entries, want %d", len(gotCfgs), len(wantCfgs))
}
for name, gotCfg := range gotCfgs {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key of this map should be instance, not name, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was going the right thing, just using the wrong variable name. :) Fixed it too.

wantCfg := wantCfgs[name]
if wantCfg == nil {
return fmt.Errorf("config.CertProviderConfigs has unexpected plugin %q with config %q", name, string(gotCfg.Canonical()))
}
if !cmp.Equal(gotCfg.Canonical(), wantCfg.Canonical()) {
return fmt.Errorf("config.CertProviderConfigs for plugin %q has config %q, want %q", name, string(gotCfg.Canonical()), string(wantCfg.Canonical()))
}
}
return nil
}

Expand Down Expand Up @@ -452,3 +474,228 @@ func TestNewConfigBootstrapFileEnvNotSet(t *testing.T) {
t.Errorf("NewConfig() returned nil error, expected to fail")
}
}

func init() {
certprovider.Register(&fakeCertProviderBuilder{})
}

const fakeCertProviderName = "fake-certificate-provider"

// fakeCertProviderBuilder builds new instances of fakeCertProvider and
// interprets the config provided to it as JSON with a single key and value.
type fakeCertProviderBuilder struct{}

func (b *fakeCertProviderBuilder) Build(certprovider.StableConfig, certprovider.Options) certprovider.Provider {
return &fakeCertProvider{}
}

// ParseConfig expects input in JSON format containing a map from string to
// string, with a single entry and mapKey being "configKey".
func (b *fakeCertProviderBuilder) ParseConfig(cfg interface{}) (certprovider.StableConfig, error) {
config, ok := cfg.(json.RawMessage)
if !ok {
return nil, fmt.Errorf("fakeCertProviderBuilder received config of type %T, want []byte", config)
}
var cfgData map[string]string
if err := json.Unmarshal(config, &cfgData); err != nil {
return nil, fmt.Errorf("fakeCertProviderBuilder config parsing failed: %v", err)
}
if len(cfgData) != 1 || cfgData["configKey"] == "" {
return nil, errors.New("fakeCertProviderBuilder received invalid config")
}
return &fakeStableConfig{config: cfgData}, nil
}

func (b *fakeCertProviderBuilder) Name() string {
return fakeCertProviderName
}

type fakeStableConfig struct {
config map[string]string
}

func (c *fakeStableConfig) Canonical() []byte {
var cfg string
for k, v := range c.config {
cfg = fmt.Sprintf("%s:%s", k, v)
}
return []byte(cfg)
}

// fakeCertProvider is an empty implementation of the Provider interface.
type fakeCertProvider struct {
certprovider.Provider
}

func TestNewConfigWithCertificateProviders(t *testing.T) {
bootstrapFileMap := map[string]string{
"badJSONCertProviderConfig": `
{
"node": {
"id": "ENVOY_NODE_ID",
"metadata": {
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
}
},
"xds_servers" : [{
"server_uri": "trafficdirector.googleapis.com:443",
"channel_creds": [
{ "type": "google_default" }
]
}],
"server_features" : ["foo", "bar", "xds_v3"],
"certificate_providers": "bad JSON"
}`,
"allUnknownCertProviders": `
{
"node": {
"id": "ENVOY_NODE_ID",
"metadata": {
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
}
},
"xds_servers" : [{
"server_uri": "trafficdirector.googleapis.com:443",
"channel_creds": [
{ "type": "google_default" }
]
}],
"server_features" : ["foo", "bar", "xds_v3"],
"certificate_providers": {
"unknownProviderInstance1": {
"foo1": "bar1"
},
"unknownProviderInstance2": {
"foo2": "bar2"
}
}
}`,
"badCertProviderConfig": `
{
"node": {
"id": "ENVOY_NODE_ID",
"metadata": {
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
}
},
"xds_servers" : [{
"server_uri": "trafficdirector.googleapis.com:443",
"channel_creds": [
{ "type": "google_default" }
]
}],
"server_features" : ["foo", "bar", "xds_v3"],
"certificate_providers": {
"unknownProviderInstance": {
"foo": "bar"
},
"fakeProviderInstance": {
"fake-certificate-provider": {
"configKey": "configValue"
}
},
"fakeProviderInstanceBad": {
"fake-certificate-provider": {
"configKey": 666
}
}
}
}`,
"goodCertProviderConfig": `
{
"node": {
"id": "ENVOY_NODE_ID",
"metadata": {
"TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
}
},
"xds_servers" : [{
"server_uri": "trafficdirector.googleapis.com:443",
"channel_creds": [
{ "type": "google_default" }
]
}],
"server_features" : ["foo", "bar", "xds_v3"],
"certificate_providers": {
"unknownProviderInstance": {
"foo": "bar"
},
"fakeProviderInstance": {
"fake-certificate-provider": {
"configKey": "configValue"
}
}
}
}`,
}
parser := certprovider.GetBuilder(fakeCertProviderName)
if parser == nil {
t.Fatalf("missing certprovider plugin %q", fakeCertProviderName)
}
wantCfg, err := parser.ParseConfig(json.RawMessage(`{
"configKey": "configValue"
}`))
if err != nil {
t.Fatalf("config parsing for plugin %q failed: %v", fakeCertProviderName, err)
}

if err := os.Setenv(v3SupportEnv, "true"); err != nil {
t.Fatalf("os.Setenv(%s, %s) failed with error: %v", v3SupportEnv, "true", err)
}
defer os.Unsetenv(v3SupportEnv)

cancel := setupBootstrapOverride(bootstrapFileMap)
defer cancel()

goodConfig := &Config{
BalancerName: "trafficdirector.googleapis.com:443",
Creds: grpc.WithCredentialsBundle(google.NewComputeEngineCredentials()),
TransportAPI: version.TransportV3,
NodeProto: v3NodeProto,
CertProviderConfigs: map[string]certprovider.StableConfig{
"fakeProviderInstance": wantCfg,
},
}
tests := []struct {
name string
wantConfig *Config
wantErr bool
}{
{
name: "badJSONCertProviderConfig",
wantErr: true,
},
{

name: "badCertProviderConfig",
wantErr: true,
},
{

name: "allUnknownCertProviders",
wantConfig: nonNilCredsConfigV3,
},
{
name: "goodCertProviderConfig",
wantConfig: goodConfig,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if err := os.Setenv(bootstrapFileEnv, test.name); err != nil {
t.Fatalf("os.Setenv(%s, %s) failed with error: %v", bootstrapFileEnv, test.name, err)
}
c, err := NewConfig()
if (err != nil) != test.wantErr {
t.Fatalf("NewConfig() returned: %v, wantErr: %v", err, test.wantErr)
}
if test.wantErr {
return
}
if err := c.compare(test.wantConfig); err != nil {
t.Fatal(err)
}
})
}
}