Add DBv5 plugin serving & management functions (#9745)

This mirrors what DBv4 is doing, but with the updated interface
This commit is contained in:
Michael Golowka 2020-09-14 16:03:56 -06:00 committed by GitHub
parent 9afb87ab34
commit 75b2f42ef2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 242 additions and 0 deletions

View File

@ -0,0 +1,42 @@
package newdbplugin
import (
"context"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/sdk/database/newdbplugin/proto"
"google.golang.org/grpc"
)
// handshakeConfigs are used to just do a basic handshake between
// a plugin and host. If the handshake fails, a user friendly error is shown.
// This prevents users from executing bad plugins or executing a plugin
// directory. It is a UX feature, not a security feature.
var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 5,
MagicCookieKey: "VAULT_DATABASE_PLUGIN",
MagicCookieValue: "926a0820-aea2-be28-51d6-83cdf00e8edb",
}
type GRPCDatabasePlugin struct {
Impl Database
// Embeding this will disable the netRPC protocol
plugin.NetRPCUnsupportedPlugin
}
var _ plugin.Plugin = &GRPCDatabasePlugin{}
var _ plugin.GRPCPlugin = &GRPCDatabasePlugin{}
func (d GRPCDatabasePlugin) GRPCServer(_ *plugin.GRPCBroker, s *grpc.Server) error {
proto.RegisterDatabaseServer(s, gRPCServer{impl: d.Impl})
return nil
}
func (GRPCDatabasePlugin) GRPCClient(doneCtx context.Context, _ *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
client := gRPCClient{
client: proto.NewDatabaseClient(c),
doneCtx: doneCtx,
}
return client, nil
}

View File

@ -0,0 +1,80 @@
package newdbplugin
import (
"context"
"errors"
"sync"
log "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
)
// DatabasePluginClient embeds a databasePluginRPCClient and wraps it's Close
// method to also call Kill() on the plugin.Client.
type DatabasePluginClient struct {
client *plugin.Client
sync.Mutex
Database
}
// This wraps the Close call and ensures we both close the database connection
// and kill the plugin.
func (dc *DatabasePluginClient) Close() error {
err := dc.Database.Close()
dc.client.Kill()
return err
}
// NewPluginClient returns a databaseRPCClient with a connection to a running
// plugin. The client is wrapped in a DatabasePluginClient object to ensure the
// plugin is killed on call of Close().
func NewPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunner *pluginutil.PluginRunner, logger log.Logger, isMetadataMode bool) (Database, error) {
// pluginSets is the map of plugins we can dispense.
pluginSets := map[int]plugin.PluginSet{
5: plugin.PluginSet{
"database": new(GRPCDatabasePlugin),
},
}
var client *plugin.Client
var err error
if isMetadataMode {
client, err = pluginRunner.RunMetadataMode(ctx, sys, pluginSets, handshakeConfig, []string{}, logger)
} else {
client, err = pluginRunner.Run(ctx, sys, pluginSets, handshakeConfig, []string{}, logger)
}
if err != nil {
return nil, err
}
// Connect via RPC
rpcClient, err := client.Client()
if err != nil {
return nil, err
}
// Request the plugin
raw, err := rpcClient.Dispense("database")
if err != nil {
return nil, err
}
// We should have a database type now. This feels like a normal interface
// implementation but is in fact over an RPC connection.
var db Database
switch raw.(type) {
case gRPCClient:
db = raw.(gRPCClient)
default:
return nil, errors.New("unsupported client type")
}
// Wrap RPC implementation in DatabasePluginClient
return &DatabasePluginClient{
client: client,
Database: db,
}, nil
}

View File

@ -0,0 +1,78 @@
package newdbplugin
import (
"context"
"fmt"
"github.com/hashicorp/errwrap"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
)
// PluginFactory is used to build plugin database types. It wraps the database
// object in a logging and metrics middleware.
func PluginFactory(ctx context.Context, pluginName string, sys pluginutil.LookRunnerUtil, logger log.Logger) (Database, error) {
// Look for plugin in the plugin catalog
pluginRunner, err := sys.LookupPlugin(ctx, pluginName, consts.PluginTypeDatabase)
if err != nil {
return nil, err
}
namedLogger := logger.Named(pluginName)
var transport string
var db Database
if pluginRunner.Builtin {
// Plugin is builtin so we can retrieve an instance of the interface
// from the pluginRunner. Then cast it to a Database.
dbRaw, err := pluginRunner.BuiltinFactory()
if err != nil {
return nil, errwrap.Wrapf("error initializing plugin: {{err}}", err)
}
var ok bool
db, ok = dbRaw.(Database)
if !ok {
return nil, fmt.Errorf("unsupported database type: %q", pluginName)
}
transport = "builtin"
} else {
// create a DatabasePluginClient instance
db, err = NewPluginClient(ctx, sys, pluginRunner, namedLogger, false)
if err != nil {
return nil, err
}
// Switch on the underlying database client type to get the transport
// method.
switch db.(*DatabasePluginClient).Database.(type) {
case *gRPCClient:
transport = "gRPC"
}
}
typeStr, err := db.Type()
if err != nil {
return nil, errwrap.Wrapf("error getting plugin type: {{err}}", err)
}
// Wrap with metrics middleware
db = &databaseMetricsMiddleware{
next: db,
typeStr: typeStr,
}
// Wrap with tracing middleware
if namedLogger.IsTrace() {
db = &databaseTracingMiddleware{
next: db,
logger: namedLogger.With("transport", transport),
}
}
return db, nil
}

View File

@ -0,0 +1,42 @@
package newdbplugin
import (
"crypto/tls"
"fmt"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
)
// Serve is called from within a plugin and wraps the provided
// Database implementation in a databasePluginRPCServer object and starts a
// RPC server.
func Serve(db Database, tlsProvider func() (*tls.Config, error)) {
plugin.Serve(ServeConfig(db, tlsProvider))
}
func ServeConfig(db Database, tlsProvider func() (*tls.Config, error)) *plugin.ServeConfig {
err := pluginutil.OptionallyEnableMlock()
if err != nil {
fmt.Println(err)
return nil
}
// pluginSets is the map of plugins we can dispense.
pluginSets := map[int]plugin.PluginSet{
5: plugin.PluginSet{
"database": &GRPCDatabasePlugin{
Impl: db,
},
},
}
conf := &plugin.ServeConfig{
HandshakeConfig: handshakeConfig,
VersionedPlugins: pluginSets,
TLSProvider: tlsProvider,
GRPCServer: plugin.DefaultGRPCServer,
}
return conf
}