Support version selection for database plugins (#16982)
* Support version selection for database plugins * Don't consider unversioned plugins for version selection algorithm * Added version to 'plugin not found' error * Add PluginFactoryVersion function to avoid changing sdk/ API
This commit is contained in:
parent
3075c5bd65
commit
aa50e42fca
|
@ -336,7 +336,7 @@ func (b *databaseBackend) GetConnectionWithConfig(ctx context.Context, name stri
|
|||
return nil, err
|
||||
}
|
||||
|
||||
dbw, err := newDatabaseWrapper(ctx, config.PluginName, b.System(), b.logger)
|
||||
dbw, err := newDatabaseWrapper(ctx, config.PluginName, config.PluginVersion, b.System(), b.logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create database instance: %w", err)
|
||||
}
|
||||
|
|
|
@ -48,12 +48,12 @@ func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) {
|
|||
os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile)
|
||||
|
||||
sys := vault.TestDynamicSystemView(cores[0].Core, nil)
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Postgres", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin-muxed", consts.PluginTypeDatabase, "TestBackend_PluginMain_PostgresMultiplexed", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "mongodb-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Mongo", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "mongodb-database-plugin-muxed", consts.PluginTypeDatabase, "TestBackend_PluginMain_MongoMultiplexed", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "mongodbatlas-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_MongoAtlas", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "mongodbatlas-database-plugin-muxed", consts.PluginTypeDatabase, "TestBackend_PluginMain_MongoAtlasMultiplexed", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_Postgres", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin-muxed", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_PostgresMultiplexed", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "mongodb-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_Mongo", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "mongodb-database-plugin-muxed", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MongoMultiplexed", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "mongodbatlas-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MongoAtlas", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "mongodbatlas-database-plugin-muxed", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MongoAtlasMultiplexed", []string{}, "")
|
||||
|
||||
return cluster, sys
|
||||
}
|
||||
|
@ -236,6 +236,7 @@ func TestBackend_config_connection(t *testing.T) {
|
|||
"allowed_roles": []string{"*"},
|
||||
"root_credentials_rotate_statements": []string{},
|
||||
"password_policy": "",
|
||||
"plugin_version": "",
|
||||
}
|
||||
configReq.Operation = logical.ReadOperation
|
||||
resp, err = b.HandleRequest(namespace.RootContext(nil), configReq)
|
||||
|
@ -289,6 +290,7 @@ func TestBackend_config_connection(t *testing.T) {
|
|||
"allowed_roles": []string{"*"},
|
||||
"root_credentials_rotate_statements": []string{},
|
||||
"password_policy": "",
|
||||
"plugin_version": "",
|
||||
}
|
||||
configReq.Operation = logical.ReadOperation
|
||||
resp, err = b.HandleRequest(namespace.RootContext(nil), configReq)
|
||||
|
@ -331,6 +333,7 @@ func TestBackend_config_connection(t *testing.T) {
|
|||
"allowed_roles": []string{"flu", "barre"},
|
||||
"root_credentials_rotate_statements": []string{},
|
||||
"password_policy": "",
|
||||
"plugin_version": "",
|
||||
}
|
||||
configReq.Operation = logical.ReadOperation
|
||||
resp, err = b.HandleRequest(namespace.RootContext(nil), configReq)
|
||||
|
@ -728,6 +731,7 @@ func TestBackend_connectionCrud(t *testing.T) {
|
|||
"allowed_roles": []string{"plugin-role-test"},
|
||||
"root_credentials_rotate_statements": []string(nil),
|
||||
"password_policy": "",
|
||||
"plugin_version": "",
|
||||
}
|
||||
req.Operation = logical.ReadOperation
|
||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||
|
@ -1503,7 +1507,7 @@ func TestBackend_AsyncClose(t *testing.T) {
|
|||
// Test that having a plugin that takes a LONG time to close will not cause the cleanup function to take
|
||||
// longer than 750ms.
|
||||
cluster, sys := getCluster(t)
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "hanging-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Hanging", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "hanging-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_Hanging", []string{}, "")
|
||||
t.Cleanup(cluster.Cleanup)
|
||||
|
||||
config := logical.TestBackendConfig()
|
||||
|
|
|
@ -110,7 +110,7 @@ func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) {
|
|||
cores := cluster.Cores
|
||||
|
||||
sys := vault.TestDynamicSystemView(cores[0].Core, nil)
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "test-plugin", consts.PluginTypeDatabase, "TestPlugin_GRPC_Main", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cores[0].Core, "test-plugin", consts.PluginTypeDatabase, "", "TestPlugin_GRPC_Main", []string{}, "")
|
||||
|
||||
return cluster, sys
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ func TestPlugin_Init(t *testing.T) {
|
|||
cluster, sys := getCluster(t)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
dbRaw, err := dbplugin.PluginFactory(namespace.RootContext(nil), "test-plugin", sys, log.NewNullLogger())
|
||||
dbRaw, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ func TestPlugin_CreateUser(t *testing.T) {
|
|||
cluster, sys := getCluster(t)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
db, err := dbplugin.PluginFactory(namespace.RootContext(nil), "test-plugin", sys, log.NewNullLogger())
|
||||
db, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ func TestPlugin_RenewUser(t *testing.T) {
|
|||
cluster, sys := getCluster(t)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
db, err := dbplugin.PluginFactory(namespace.RootContext(nil), "test-plugin", sys, log.NewNullLogger())
|
||||
db, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
@ -237,7 +237,7 @@ func TestPlugin_RevokeUser(t *testing.T) {
|
|||
cluster, sys := getCluster(t)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
db, err := dbplugin.PluginFactory(namespace.RootContext(nil), "test-plugin", sys, log.NewNullLogger())
|
||||
db, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
|
|
@ -5,12 +5,16 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/go-version"
|
||||
|
||||
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
"github.com/hashicorp/vault/sdk/helper/pluginutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
|
@ -22,7 +26,8 @@ var (
|
|||
// DatabaseConfig is used by the Factory function to configure a Database
|
||||
// object.
|
||||
type DatabaseConfig struct {
|
||||
PluginName string `json:"plugin_name" structs:"plugin_name" mapstructure:"plugin_name"`
|
||||
PluginName string `json:"plugin_name" structs:"plugin_name" mapstructure:"plugin_name"`
|
||||
PluginVersion string `json:"plugin_version" structs:"plugin_version" mapstructure:"plugin_version"`
|
||||
// ConnectionDetails stores the database specific connection settings needed
|
||||
// by each database type.
|
||||
ConnectionDetails map[string]interface{} `json:"connection_details" structs:"connection_details" mapstructure:"connection_details"`
|
||||
|
@ -110,6 +115,11 @@ func pathConfigurePluginConnection(b *databaseBackend) *framework.Path {
|
|||
that plugin type.`,
|
||||
},
|
||||
|
||||
"plugin_version": {
|
||||
Type: framework.TypeString,
|
||||
Description: `The version of the plugin to use.`,
|
||||
},
|
||||
|
||||
"verify_connection": {
|
||||
Type: framework.TypeBool,
|
||||
Default: true,
|
||||
|
@ -281,6 +291,48 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
|
|||
return logical.ErrorResponse(respErrEmptyPluginName), nil
|
||||
}
|
||||
|
||||
if pluginVersionRaw, ok := data.GetOk("plugin_version"); ok {
|
||||
config.PluginVersion = pluginVersionRaw.(string)
|
||||
}
|
||||
|
||||
unversionedPlugin, err := b.System().LookupPlugin(ctx, config.PluginName, consts.PluginTypeDatabase)
|
||||
switch {
|
||||
case config.PluginVersion != "":
|
||||
semanticVersion, err := version.NewVersion(config.PluginVersion)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("version %q is not a valid semantic version: %s", config.PluginVersion, err), nil
|
||||
}
|
||||
|
||||
// Canonicalize the version.
|
||||
config.PluginVersion = "v" + semanticVersion.String()
|
||||
case err == nil && !unversionedPlugin.Builtin:
|
||||
// We'll select the unversioned plugin that's been registered.
|
||||
case req.Operation == logical.CreateOperation:
|
||||
// No version provided and no unversioned plugin of that name available.
|
||||
// Pin to the current latest version if any versioned plugins are registered.
|
||||
plugins, err := b.System().ListVersionedPlugins(ctx, consts.PluginTypeDatabase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var versionedCandidates []pluginutil.VersionedPlugin
|
||||
for _, plugin := range plugins {
|
||||
if !plugin.Builtin && plugin.Name == config.PluginName && plugin.Version != "" {
|
||||
versionedCandidates = append(versionedCandidates, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
if len(versionedCandidates) != 0 {
|
||||
// Sort in reverse order.
|
||||
sort.SliceStable(versionedCandidates, func(i, j int) bool {
|
||||
return versionedCandidates[i].SemanticVersion.GreaterThan(versionedCandidates[j].SemanticVersion)
|
||||
})
|
||||
|
||||
config.PluginVersion = "v" + versionedCandidates[0].SemanticVersion.String()
|
||||
b.logger.Debug(fmt.Sprintf("pinning %q database plugin version %q from candidates %v", config.PluginName, config.PluginVersion, versionedCandidates))
|
||||
}
|
||||
}
|
||||
|
||||
if allowedRolesRaw, ok := data.GetOk("allowed_roles"); ok {
|
||||
config.AllowedRoles = allowedRolesRaw.([]string)
|
||||
} else if req.Operation == logical.CreateOperation {
|
||||
|
@ -301,6 +353,7 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
|
|||
// ConnectionDetails.
|
||||
delete(data.Raw, "name")
|
||||
delete(data.Raw, "plugin_name")
|
||||
delete(data.Raw, "plugin_version")
|
||||
delete(data.Raw, "allowed_roles")
|
||||
delete(data.Raw, "verify_connection")
|
||||
delete(data.Raw, "root_rotation_statements")
|
||||
|
@ -326,7 +379,7 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
|
|||
}
|
||||
|
||||
// Create a database plugin and initialize it.
|
||||
dbw, err := newDatabaseWrapper(ctx, config.PluginName, b.System(), b.logger)
|
||||
dbw, err := newDatabaseWrapper(ctx, config.PluginName, config.PluginVersion, b.System(), b.logger)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("error creating database object: %s", err), nil
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ type databaseVersionWrapper struct {
|
|||
|
||||
// newDatabaseWrapper figures out which version of the database the pluginName is referring to and returns a wrapper object
|
||||
// that can be used to make operations on the underlying database plugin.
|
||||
func newDatabaseWrapper(ctx context.Context, pluginName string, sys pluginutil.LookRunnerUtil, logger log.Logger) (dbw databaseVersionWrapper, err error) {
|
||||
newDB, err := v5.PluginFactory(ctx, pluginName, sys, logger)
|
||||
func newDatabaseWrapper(ctx context.Context, pluginName string, pluginVersion string, sys pluginutil.LookRunnerUtil, logger log.Logger) (dbw databaseVersionWrapper, err error) {
|
||||
newDB, err := v5.PluginFactoryVersion(ctx, pluginName, pluginVersion, sys, logger)
|
||||
if err == nil {
|
||||
dbw = databaseVersionWrapper{
|
||||
v5: newDB,
|
||||
|
@ -32,7 +32,7 @@ func newDatabaseWrapper(ctx context.Context, pluginName string, sys pluginutil.L
|
|||
merr := &multierror.Error{}
|
||||
merr = multierror.Append(merr, err)
|
||||
|
||||
legacyDB, err := v4.PluginFactory(ctx, pluginName, sys, logger)
|
||||
legacyDB, err := v4.PluginFactoryVersion(ctx, pluginName, pluginVersion, sys, logger)
|
||||
if err == nil {
|
||||
dbw = databaseVersionWrapper{
|
||||
v4: legacyDB,
|
||||
|
|
|
@ -22,9 +22,9 @@ func TestPlugin_lifecycle(t *testing.T) {
|
|||
cluster, sys := getCluster(t)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v4-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_MockV4", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_MockV5", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v6-database-plugin-muxed", consts.PluginTypeDatabase, "TestBackend_PluginMain_MockV6Multiplexed", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v4-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV4", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV5", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v6-database-plugin-muxed", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV6Multiplexed", []string{}, "")
|
||||
|
||||
config := logical.TestBackendConfig()
|
||||
config.StorageView = &logical.InmemStorage{}
|
||||
|
@ -218,6 +218,216 @@ func TestPlugin_lifecycle(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPlugin_VersionSelection(t *testing.T) {
|
||||
cluster, sys := getCluster(t)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
for _, version := range []string{"v11.0.0", "v11.0.1-rc1", "v2.0.0"} {
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, version, "TestBackend_PluginMain_MockV5", []string{}, "")
|
||||
}
|
||||
|
||||
config := logical.TestBackendConfig()
|
||||
config.StorageView = &logical.InmemStorage{}
|
||||
config.System = sys
|
||||
lb, err := Factory(context.Background(), config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, ok := lb.(*databaseBackend)
|
||||
if !ok {
|
||||
t.Fatal("could not convert to database backend")
|
||||
}
|
||||
defer b.Cleanup(context.Background())
|
||||
|
||||
test := func(t *testing.T, selectVersion, expectedVersion string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := &logical.Request{
|
||||
Operation: logical.CreateOperation,
|
||||
Path: "config/db",
|
||||
Storage: config.StorageView,
|
||||
Data: map[string]interface{}{
|
||||
"connection_url": "sample_connection_url",
|
||||
"plugin_name": "mock-v5-database-plugin",
|
||||
"plugin_version": selectVersion,
|
||||
"verify_connection": true,
|
||||
"allowed_roles": []string{"*"},
|
||||
"name": "mockv5",
|
||||
"username": "mockv5-user",
|
||||
"password": "mysecurepassword",
|
||||
},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := b.HandleRequest(ctx, req)
|
||||
assertErrIsNil(t, err)
|
||||
assertRespHasNoErr(t, resp)
|
||||
assertNoRespData(t, resp)
|
||||
|
||||
defer func() {
|
||||
_, err := b.HandleRequest(context.Background(), &logical.Request{
|
||||
Operation: logical.DeleteOperation,
|
||||
Path: "config/db",
|
||||
Storage: config.StorageView,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
req = &logical.Request{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "config/db",
|
||||
Storage: config.StorageView,
|
||||
}
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
assertErrIsNil(t, err)
|
||||
assertRespHasNoErr(t, resp)
|
||||
if resp.Data["plugin_version"].(string) != expectedVersion {
|
||||
t.Fatalf("Expected version %q but got %q", expectedVersion, resp.Data["plugin_version"].(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
selectVersion string
|
||||
expectedVersion string
|
||||
}{
|
||||
"no version specified, selects latest in the absence of unversioned plugins": {
|
||||
selectVersion: "",
|
||||
expectedVersion: "v11.0.1-rc1",
|
||||
},
|
||||
"specific version selected": {
|
||||
selectVersion: "11.0.0",
|
||||
expectedVersion: "v11.0.0",
|
||||
},
|
||||
} {
|
||||
t.Run(name, test(t, tc.selectVersion, tc.expectedVersion))
|
||||
}
|
||||
|
||||
// Register a newer version of the plugin, and ensure that's the new default version selected.
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "v11.0.1", "TestBackend_PluginMain_MockV5", []string{}, "")
|
||||
t.Run("no version specified, new latest version selected", test(t, "", "v11.0.1"))
|
||||
|
||||
// Register an unversioned plugin and ensure that is now selected when no version is specified.
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV5", []string{}, "")
|
||||
for name, tc := range map[string]struct {
|
||||
selectVersion string
|
||||
expectedVersion string
|
||||
}{
|
||||
"no version specified, selects unversioned": {
|
||||
selectVersion: "",
|
||||
expectedVersion: "",
|
||||
},
|
||||
"specific version selected": {
|
||||
selectVersion: "v2.0.0",
|
||||
expectedVersion: "v2.0.0",
|
||||
},
|
||||
} {
|
||||
t.Run(name, test(t, tc.selectVersion, tc.expectedVersion))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlugin_VersionMustBeExplicitlyUpgraded(t *testing.T) {
|
||||
cluster, sys := getCluster(t)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
config := logical.TestBackendConfig()
|
||||
config.StorageView = &logical.InmemStorage{}
|
||||
config.System = sys
|
||||
lb, err := Factory(context.Background(), config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, ok := lb.(*databaseBackend)
|
||||
if !ok {
|
||||
t.Fatal("could not convert to database backend")
|
||||
}
|
||||
defer b.Cleanup(context.Background())
|
||||
|
||||
configData := func(extraData ...string) map[string]interface{} {
|
||||
data := map[string]interface{}{
|
||||
"connection_url": "sample_connection_url",
|
||||
"plugin_name": "mysql-database-plugin",
|
||||
"verify_connection": false,
|
||||
"allowed_roles": []string{"*"},
|
||||
"username": "mockv5-user",
|
||||
"password": "mysecurepassword",
|
||||
}
|
||||
if len(extraData)%2 != 0 {
|
||||
t.Fatal("Expected an even number of args in extraData")
|
||||
}
|
||||
for i := 0; i < len(extraData); i += 2 {
|
||||
data[extraData[i]] = extraData[i+1]
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
readVersion := func() string {
|
||||
resp, err := b.HandleRequest(context.Background(), &logical.Request{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "config/db",
|
||||
Storage: config.StorageView,
|
||||
})
|
||||
assertErrIsNil(t, err)
|
||||
assertRespHasNoErr(t, resp)
|
||||
return resp.Data["plugin_version"].(string)
|
||||
}
|
||||
|
||||
resp, err := b.HandleRequest(context.Background(), &logical.Request{
|
||||
Operation: logical.CreateOperation,
|
||||
Path: "config/db",
|
||||
Storage: config.StorageView,
|
||||
Data: configData(),
|
||||
})
|
||||
assertErrIsNil(t, err)
|
||||
assertRespHasNoErr(t, resp)
|
||||
assertNoRespData(t, resp)
|
||||
|
||||
version := readVersion()
|
||||
expectedVersion := ""
|
||||
if version != expectedVersion {
|
||||
t.Fatalf("Expected version %q but got %q", expectedVersion, version)
|
||||
}
|
||||
|
||||
// Register versioned plugin, and check that a new write to existing config doesn't upgrade the plugin implicitly.
|
||||
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mysql-database-plugin", consts.PluginTypeDatabase, "v1.0.0", "TestBackend_PluginMain_MockV5", []string{}, "")
|
||||
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "config/db",
|
||||
Storage: config.StorageView,
|
||||
Data: configData(),
|
||||
})
|
||||
assertErrIsNil(t, err)
|
||||
assertRespHasNoErr(t, resp)
|
||||
assertNoRespData(t, resp)
|
||||
|
||||
version = readVersion()
|
||||
if version != expectedVersion {
|
||||
t.Fatalf("Expected version %q but got %q", expectedVersion, version)
|
||||
}
|
||||
|
||||
// Now explicitly upgrade.
|
||||
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "config/db",
|
||||
Storage: config.StorageView,
|
||||
Data: configData("plugin_version", "1.0.0"),
|
||||
})
|
||||
assertErrIsNil(t, err)
|
||||
assertRespHasNoErr(t, resp)
|
||||
assertNoRespData(t, resp)
|
||||
|
||||
version = readVersion()
|
||||
expectedVersion = "v1.0.0"
|
||||
if version != expectedVersion {
|
||||
t.Fatalf("Expected version %q but got %q", expectedVersion, version)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanup(t *testing.T, b *databaseBackend, reqs []*logical.Request) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
|
|
@ -2,6 +2,7 @@ package plugin
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/helper/logging"
|
||||
|
@ -193,3 +194,7 @@ func (v testSystemView) LookupPluginVersion(context.Context, string, consts.Plug
|
|||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v testSystemView) ListVersionedPlugins(_ context.Context, _ consts.PluginType) ([]pluginutil.VersionedPlugin, error) {
|
||||
return nil, errors.New("ListVersionedPlugins not implemented for testSystemView")
|
||||
}
|
||||
|
|
|
@ -133,7 +133,7 @@ func testConfig(t *testing.T, pluginCmd string) (*logical.BackendConfig, func())
|
|||
|
||||
os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile)
|
||||
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, pluginCmd, []string{}, "")
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", pluginCmd, []string{}, "")
|
||||
|
||||
return config, func() {
|
||||
cluster.Cleanup()
|
||||
|
|
|
@ -52,7 +52,7 @@ func getPluginClusterAndCore(t testing.TB, logger log.Logger) (*vault.TestCluste
|
|||
os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile)
|
||||
|
||||
vault.TestWaitActive(benchhelpers.TBtoT(t), core.Core)
|
||||
vault.TestAddTestPlugin(benchhelpers.TBtoT(t), core.Core, "mock-plugin", consts.PluginTypeSecrets, "TestPlugin_PluginMain", []string{}, "")
|
||||
vault.TestAddTestPlugin(benchhelpers.TBtoT(t), core.Core, "mock-plugin", consts.PluginTypeSecrets, "", "TestPlugin_PluginMain", []string{}, "")
|
||||
|
||||
// Mount the mock plugin
|
||||
err = core.Client.Sys().Mount("mock", &api.MountInput{
|
||||
|
|
|
@ -71,9 +71,15 @@ type Database interface {
|
|||
|
||||
// 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) {
|
||||
func PluginFactory(ctx context.Context, pluginName string, pluginVersion string, sys pluginutil.LookRunnerUtil, logger log.Logger) (Database, error) {
|
||||
return PluginFactoryVersion(ctx, pluginName, "", sys, logger)
|
||||
}
|
||||
|
||||
// PluginFactory is used to build plugin database types with a version specified.
|
||||
// It wraps the database object in a logging and metrics middleware.
|
||||
func PluginFactoryVersion(ctx context.Context, pluginName string, pluginVersion string, sys pluginutil.LookRunnerUtil, logger log.Logger) (Database, error) {
|
||||
// Look for plugin in the plugin catalog
|
||||
pluginRunner, err := sys.LookupPlugin(ctx, pluginName, consts.PluginTypeDatabase)
|
||||
pluginRunner, err := sys.LookupPluginVersion(ctx, pluginName, consts.PluginTypeDatabase, pluginVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -13,8 +13,14 @@ import (
|
|||
// 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) {
|
||||
return PluginFactoryVersion(ctx, pluginName, "", sys, logger)
|
||||
}
|
||||
|
||||
// PluginFactoryVersion is used to build plugin database types with a version specified.
|
||||
// It wraps the database object in a logging and metrics middleware.
|
||||
func PluginFactoryVersion(ctx context.Context, pluginName string, pluginVersion string, sys pluginutil.LookRunnerUtil, logger log.Logger) (Database, error) {
|
||||
// Look for plugin in the plugin catalog
|
||||
pluginRunner, err := sys.LookupPlugin(ctx, pluginName, consts.PluginTypeDatabase)
|
||||
pluginRunner, err := sys.LookupPluginVersion(ctx, pluginName, consts.PluginTypeDatabase, pluginVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -43,6 +49,7 @@ func PluginFactory(ctx context.Context, pluginName string, sys pluginutil.LookRu
|
|||
config := pluginutil.PluginClientConfig{
|
||||
Name: pluginName,
|
||||
PluginType: consts.PluginTypeDatabase,
|
||||
Version: pluginVersion,
|
||||
PluginSets: PluginSets,
|
||||
HandshakeConfig: HandshakeConfig,
|
||||
Logger: namedLogger,
|
||||
|
|
|
@ -60,6 +60,10 @@ type SystemView interface {
|
|||
// name and version. Returns a PluginRunner or an error if a plugin can not be found.
|
||||
LookupPluginVersion(ctx context.Context, pluginName string, pluginType consts.PluginType, version string) (*pluginutil.PluginRunner, error)
|
||||
|
||||
// ListVersionedPlugins returns information about all plugins of a certain
|
||||
// type in the catalog, including any versioning information stored for them.
|
||||
ListVersionedPlugins(ctx context.Context, pluginType consts.PluginType) ([]pluginutil.VersionedPlugin, error)
|
||||
|
||||
// NewPluginClient returns a client for managing the lifecycle of plugin
|
||||
// processes
|
||||
NewPluginClient(ctx context.Context, config pluginutil.PluginClientConfig) (pluginutil.PluginClient, error)
|
||||
|
@ -176,6 +180,10 @@ func (d StaticSystemView) LookupPluginVersion(_ context.Context, _ string, _ con
|
|||
return nil, errors.New("LookupPluginVersion is not implemented in StaticSystemView")
|
||||
}
|
||||
|
||||
func (d StaticSystemView) ListVersionedPlugins(_ context.Context, _ consts.PluginType) ([]pluginutil.VersionedPlugin, error) {
|
||||
return nil, errors.New("ListVersionedPlugins is not implemented in StaticSystemView")
|
||||
}
|
||||
|
||||
func (d StaticSystemView) MlockEnabled() bool {
|
||||
return d.EnableMlock
|
||||
}
|
||||
|
|
|
@ -111,6 +111,10 @@ func (s *gRPCSystemViewClient) LookupPluginVersion(_ context.Context, _ string,
|
|||
return nil, fmt.Errorf("cannot call LookupPluginVersion from a plugin backend")
|
||||
}
|
||||
|
||||
func (s *gRPCSystemViewClient) ListVersionedPlugins(_ context.Context, _ consts.PluginType) ([]pluginutil.VersionedPlugin, error) {
|
||||
return nil, fmt.Errorf("cannot call ListVersionedPlugins from a plugin backend")
|
||||
}
|
||||
|
||||
func (s *gRPCSystemViewClient) MlockEnabled() bool {
|
||||
reply, err := s.client.MlockEnabled(context.Background(), &pb.Empty{})
|
||||
if err != nil {
|
||||
|
|
|
@ -916,7 +916,11 @@ func (c *Core) newCredentialBackend(ctx context.Context, entry *MountEntry, sysV
|
|||
return nil, err
|
||||
}
|
||||
if plug == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, t)
|
||||
errContext := t
|
||||
if entry.Version != "" {
|
||||
errContext += fmt.Sprintf(", version=%s", entry.Version)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, errContext)
|
||||
}
|
||||
|
||||
f = plugin.Factory
|
||||
|
|
|
@ -251,12 +251,28 @@ func (d dynamicSystemView) LookupPluginVersion(ctx context.Context, name string,
|
|||
return nil, err
|
||||
}
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, name)
|
||||
errContext := name
|
||||
if version != "" {
|
||||
errContext += fmt.Sprintf(", version=%s", version)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, errContext)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ListVersionedPlugins returns information about all plugins of a certain
|
||||
// typein the catalog, including any versioning information stored for them.
|
||||
func (d dynamicSystemView) ListVersionedPlugins(ctx context.Context, pluginType consts.PluginType) ([]pluginutil.VersionedPlugin, error) {
|
||||
if d.core == nil {
|
||||
return nil, fmt.Errorf("system view core is nil")
|
||||
}
|
||||
if d.core.pluginCatalog == nil {
|
||||
return nil, fmt.Errorf("system view core plugin catalog is nil")
|
||||
}
|
||||
return d.core.pluginCatalog.ListVersionedPlugins(ctx, pluginType)
|
||||
}
|
||||
|
||||
// MlockEnabled returns the configuration setting for enabling mlock on plugins.
|
||||
func (d dynamicSystemView) MlockEnabled() bool {
|
||||
return d.core.enableMlock
|
||||
|
|
|
@ -242,7 +242,7 @@ func TestSystemBackend_Plugin_MismatchType(t *testing.T) {
|
|||
core := cluster.Cores[0]
|
||||
|
||||
// Add a credential backend with the same name
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, "TestBackend_PluginMainCredentials", []string{}, "")
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, "", "TestBackend_PluginMainCredentials", []string{}, "")
|
||||
|
||||
// Make a request to lazy load the now-credential plugin
|
||||
// and expect an error
|
||||
|
@ -335,13 +335,13 @@ func testPlugin_CatalogRemoved(t *testing.T, btype logical.BackendType, testMoun
|
|||
switch btype {
|
||||
case logical.TypeLogical:
|
||||
// Add plugin back to the catalog
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, logicalVersionMap[tc.pluginVersion], []string{}, "")
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", logicalVersionMap[tc.pluginVersion], []string{}, "")
|
||||
_, err = core.Client.Logical().Write("sys/mounts/mock-0", map[string]interface{}{
|
||||
"type": "test",
|
||||
})
|
||||
case logical.TypeCredential:
|
||||
// Add plugin back to the catalog
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, credentialVersionMap[tc.pluginVersion], []string{}, "")
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, "", credentialVersionMap[tc.pluginVersion], []string{}, "")
|
||||
_, err = core.Client.Logical().Write("sys/auth/mock-0", map[string]interface{}{
|
||||
"type": "test",
|
||||
})
|
||||
|
@ -463,10 +463,10 @@ func testPlugin_continueOnError(t *testing.T, btype logical.BackendType, mismatc
|
|||
switch btype {
|
||||
case logical.TypeLogical:
|
||||
plugin := logicalVersionMap[tc.pluginVersion]
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, plugin, []string{}, cluster.TempDir)
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", plugin, []string{}, cluster.TempDir)
|
||||
case logical.TypeCredential:
|
||||
plugin := credentialVersionMap[tc.pluginVersion]
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, plugin, []string{}, cluster.TempDir)
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, "", plugin, []string{}, cluster.TempDir)
|
||||
}
|
||||
|
||||
// Reload the plugin
|
||||
|
@ -755,7 +755,7 @@ func testSystemBackendMock(t *testing.T, numCores, numMounts int, backendType lo
|
|||
switch backendType {
|
||||
case logical.TypeLogical:
|
||||
plugin := logicalVersionMap[pluginVersion]
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, plugin, []string{}, tempDir)
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", plugin, []string{}, tempDir)
|
||||
for i := 0; i < numMounts; i++ {
|
||||
// Alternate input styles for plugin_name on every other mount
|
||||
options := map[string]interface{}{
|
||||
|
@ -771,7 +771,7 @@ func testSystemBackendMock(t *testing.T, numCores, numMounts int, backendType lo
|
|||
}
|
||||
case logical.TypeCredential:
|
||||
plugin := credentialVersionMap[pluginVersion]
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, plugin, []string{}, tempDir)
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, "", plugin, []string{}, tempDir)
|
||||
for i := 0; i < numMounts; i++ {
|
||||
// Alternate input styles for plugin_name on every other mount
|
||||
options := map[string]interface{}{
|
||||
|
@ -826,7 +826,7 @@ func testSystemBackend_SingleCluster_Env(t *testing.T, env []string) *vault.Test
|
|||
|
||||
os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile)
|
||||
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "TestBackend_PluginMainEnv", env, tempDir)
|
||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", "TestBackend_PluginMainEnv", env, tempDir)
|
||||
options := map[string]interface{}{
|
||||
"type": "mock-plugin",
|
||||
}
|
||||
|
|
|
@ -306,7 +306,7 @@ const mountStateUnmounting = "unmounting"
|
|||
type MountEntry struct {
|
||||
Table string `json:"table"` // The table it belongs to
|
||||
Path string `json:"path"` // Mount Path
|
||||
Type string `json:"type"` // Logical backend Type
|
||||
Type string `json:"type"` // Logical backend Type. NB: This is the plugin name, e.g. my-vault-plugin, NOT plugin type (e.g. auth).
|
||||
Description string `json:"description"` // User-provided description
|
||||
UUID string `json:"uuid"` // Barrier view UUID
|
||||
BackendAwareUUID string `json:"backend_aware_uuid"` // UUID that can be used by the backend as a helper when a consistent value is needed outside of storage.
|
||||
|
@ -330,9 +330,9 @@ type MountEntry struct {
|
|||
synthesizedConfigCache sync.Map
|
||||
|
||||
// version info
|
||||
Version string `json:"version,omitempty"`
|
||||
Sha string `json:"sha,omitempty"`
|
||||
RunningVersion string `json:"running_version,omitempty"`
|
||||
Version string `json:"version,omitempty"` // The semantic version of the mounted plugin, e.g. v1.2.3.
|
||||
Sha string `json:"sha,omitempty"` // The SHA256 sum of the plugin binary.
|
||||
RunningVersion string `json:"running_version,omitempty"` // The semantic version of the mounted plugin as reported by the plugin.
|
||||
RunningSha string `json:"running_sha,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -1526,7 +1526,11 @@ func (c *Core) newLogicalBackend(ctx context.Context, entry *MountEntry, sysView
|
|||
return nil, err
|
||||
}
|
||||
if plug == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, t)
|
||||
errContext := t
|
||||
if entry.Version != "" {
|
||||
errContext += fmt.Sprintf(", version=%s", entry.Version)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, errContext)
|
||||
}
|
||||
|
||||
f = plugin.Factory
|
||||
|
@ -1548,6 +1552,7 @@ func (c *Core) newLogicalBackend(ctx context.Context, entry *MountEntry, sysView
|
|||
}
|
||||
|
||||
conf["plugin_type"] = consts.PluginTypeSecrets.String()
|
||||
conf["plugin_version"] = entry.Version
|
||||
|
||||
backendLogger := c.baseLogger.Named(fmt.Sprintf("secrets.%s.%s", t, entry.Accessor))
|
||||
c.AddLogger(backendLogger)
|
||||
|
|
|
@ -373,13 +373,13 @@ func TestPluginCatalog_NewPluginClient(t *testing.T) {
|
|||
}
|
||||
|
||||
// register plugins
|
||||
TestAddTestPlugin(t, core, "mux-postgres", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_PostgresMultiplexed", []string{}, "")
|
||||
TestAddTestPlugin(t, core, "single-postgres-1", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_Postgres", []string{}, "")
|
||||
TestAddTestPlugin(t, core, "single-postgres-2", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_Postgres", []string{}, "")
|
||||
TestAddTestPlugin(t, core, "mux-postgres", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_PostgresMultiplexed", []string{}, "")
|
||||
TestAddTestPlugin(t, core, "single-postgres-1", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_Postgres", []string{}, "")
|
||||
TestAddTestPlugin(t, core, "single-postgres-2", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_Postgres", []string{}, "")
|
||||
|
||||
TestAddTestPlugin(t, core, "mux-userpass", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_UserpassMultiplexed", []string{}, "")
|
||||
TestAddTestPlugin(t, core, "single-userpass-1", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_Userpass", []string{}, "")
|
||||
TestAddTestPlugin(t, core, "single-userpass-2", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_Userpass", []string{}, "")
|
||||
TestAddTestPlugin(t, core, "mux-userpass", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_UserpassMultiplexed", []string{}, "")
|
||||
TestAddTestPlugin(t, core, "single-userpass-1", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_Userpass", []string{}, "")
|
||||
TestAddTestPlugin(t, core, "single-userpass-2", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_Userpass", []string{}, "")
|
||||
|
||||
var pluginClients []*pluginClient
|
||||
// run plugins
|
||||
|
|
|
@ -513,7 +513,7 @@ func TestDynamicSystemView(c *Core, ns *namespace.Namespace) *dynamicSystemView
|
|||
|
||||
// TestAddTestPlugin registers the testFunc as part of the plugin command to the
|
||||
// plugin catalog. If provided, uses tmpDir as the plugin directory.
|
||||
func TestAddTestPlugin(t testing.T, c *Core, name string, pluginType consts.PluginType, testFunc string, env []string, tempDir string) {
|
||||
func TestAddTestPlugin(t testing.T, c *Core, name string, pluginType consts.PluginType, version string, testFunc string, env []string, tempDir string) {
|
||||
file, err := os.Open(os.Args[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -575,7 +575,6 @@ func TestAddTestPlugin(t testing.T, c *Core, name string, pluginType consts.Plug
|
|||
c.pluginCatalog.directory = fullPath
|
||||
|
||||
args := []string{fmt.Sprintf("--test.run=%s", testFunc)}
|
||||
version := ""
|
||||
err = c.pluginCatalog.Set(context.Background(), name, pluginType, version, fileName, args, env, sum)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
Loading…
Reference in New Issue