open-vault/builtin/plugin/v5/backend.go
Christopher Swenson b136a7ecd8
Add plugin version to GRPC interface (#17088)
Add plugin version to GRPC interface

Added a version interface in the sdk/logical so that it can be shared between all plugin types, and then wired it up to RunningVersion in the mounts, auth list, and database systems.

I've tested that this works with auth, database, and secrets plugin types, with the following logic to populate RunningVersion:

If a plugin has a PluginVersion() method implemented, then that is used
If not, and the plugin is built into the Vault binary, then the go.mod version is used
Otherwise, the it will be the empty string.
My apologies for the length of this PR.

* Placeholder backend should be external

We use a placeholder backend (previously a framework.Backend) before a
GRPC plugin is lazy-loaded. This makes us later think the plugin is a
builtin plugin.

So we added a `placeholderBackend` type that overrides the
`IsExternal()` method so that later we know that the plugin is external,
and don't give it a default builtin version.
2022-09-15 16:37:59 -07:00

158 lines
4.1 KiB
Go

package plugin
import (
"context"
"net/rpc"
"sync"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/plugin"
bplugin "github.com/hashicorp/vault/sdk/plugin"
)
// Backend returns an instance of the backend, either as a plugin if external
// or as a concrete implementation if builtin, casted as logical.Backend.
func Backend(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
var b backend
name := conf.Config["plugin_name"]
pluginType, err := consts.ParsePluginType(conf.Config["plugin_type"])
if err != nil {
return nil, err
}
pluginVersion := conf.Config["plugin_version"]
sys := conf.System
raw, err := plugin.NewBackendV5(ctx, name, pluginType, pluginVersion, sys, conf)
if err != nil {
return nil, err
}
b.Backend = raw
b.config = conf
return &b, nil
}
// backend is a thin wrapper around a builtin plugin or a plugin.BackendPluginClientV5
type backend struct {
logical.Backend
mu sync.RWMutex
config *logical.BackendConfig
// Used to detect if we already reloaded
canary string
}
func (b *backend) reloadBackend(ctx context.Context) error {
pluginName := b.config.Config["plugin_name"]
pluginType, err := consts.ParsePluginType(b.config.Config["plugin_type"])
if err != nil {
return err
}
pluginVersion := b.config.Config["plugin_version"]
b.Logger().Debug("plugin: reloading plugin backend", "plugin", pluginName)
// Ensure proper cleanup of the backend
// Pass a context value so that the plugin client will call the appropriate
// cleanup method for reloading
reloadCtx := context.WithValue(ctx, plugin.ContextKeyPluginReload, "reload")
b.Backend.Cleanup(reloadCtx)
nb, err := plugin.NewBackendV5(ctx, pluginName, pluginType, pluginVersion, b.config.System, b.config)
if err != nil {
return err
}
err = nb.Setup(ctx, b.config)
if err != nil {
return err
}
b.Backend = nb
return nil
}
// HandleRequest is a thin wrapper implementation of HandleRequest that includes automatic plugin reload.
func (b *backend) HandleRequest(ctx context.Context, req *logical.Request) (*logical.Response, error) {
b.mu.RLock()
canary := b.canary
resp, err := b.Backend.HandleRequest(ctx, req)
b.mu.RUnlock()
// Need to compare string value for case were err comes from plugin RPC
// and is returned as plugin.BasicError type.
if err != nil &&
(err.Error() == rpc.ErrShutdown.Error() || err == bplugin.ErrPluginShutdown) {
// Reload plugin if it's an rpc.ErrShutdown
b.mu.Lock()
if b.canary == canary {
err := b.reloadBackend(ctx)
if err != nil {
b.mu.Unlock()
return nil, err
}
b.canary, err = uuid.GenerateUUID()
if err != nil {
b.mu.Unlock()
return nil, err
}
}
b.mu.Unlock()
// Try request once more
b.mu.RLock()
defer b.mu.RUnlock()
return b.Backend.HandleRequest(ctx, req)
}
return resp, err
}
// HandleExistenceCheck is a thin wrapper implementation of HandleRequest that includes automatic plugin reload.
func (b *backend) HandleExistenceCheck(ctx context.Context, req *logical.Request) (bool, bool, error) {
b.mu.RLock()
canary := b.canary
checkFound, exists, err := b.Backend.HandleExistenceCheck(ctx, req)
b.mu.RUnlock()
if err != nil &&
(err.Error() == rpc.ErrShutdown.Error() || err == bplugin.ErrPluginShutdown) {
// Reload plugin if it's an rpc.ErrShutdown
b.mu.Lock()
if b.canary == canary {
err := b.reloadBackend(ctx)
if err != nil {
b.mu.Unlock()
return false, false, err
}
b.canary, err = uuid.GenerateUUID()
if err != nil {
b.mu.Unlock()
return false, false, err
}
}
b.mu.Unlock()
// Try request once more
b.mu.RLock()
defer b.mu.RUnlock()
return b.Backend.HandleExistenceCheck(ctx, req)
}
return checkFound, exists, err
}
// InvalidateKey is a thin wrapper used to ensure we grab the lock for race purposes
func (b *backend) InvalidateKey(ctx context.Context, key string) {
b.mu.RLock()
defer b.mu.RUnlock()
b.Backend.InvalidateKey(ctx, key)
}
func (b *backend) IsExternal() bool {
switch b.Backend.(type) {
case *plugin.BackendPluginClientV5:
return true
}
return false
}