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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
) | ||
|
||
|
@@ -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 { | ||
|
@@ -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> | ||
// } | ||
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably minor detail. Why does the This is similar to what we need for the balancing policy + config. We didn't make it a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we can have two different instances with same We don't stop at the first valid config here. We process all configs and use the |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we not need the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,8 @@ | |
package bootstrap | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"os" | ||
"testing" | ||
|
@@ -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" | ||
) | ||
|
||
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The key of this map should be There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
|
||
|
@@ -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) | ||
} | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.