mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
HTTP API for pinning plugin versions (#25105)
This commit is contained in:
@@ -40,9 +40,10 @@ type dbPluginInstance struct {
|
||||
sync.RWMutex
|
||||
database databaseVersionWrapper
|
||||
|
||||
id string
|
||||
name string
|
||||
closed bool
|
||||
id string
|
||||
name string
|
||||
runningPluginVersion string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (dbi *dbPluginInstance) ID() string {
|
||||
@@ -324,9 +325,10 @@ func (b *databaseBackend) GetConnectionWithConfig(ctx context.Context, name stri
|
||||
}
|
||||
|
||||
dbi = &dbPluginInstance{
|
||||
database: dbw,
|
||||
id: id,
|
||||
name: name,
|
||||
database: dbw,
|
||||
id: id,
|
||||
name: name,
|
||||
runningPluginVersion: pluginVersion,
|
||||
}
|
||||
oldConn := b.connections.Put(name, dbi)
|
||||
if oldConn != nil {
|
||||
|
||||
@@ -31,8 +31,9 @@ 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"`
|
||||
PluginVersion string `json:"plugin_version" structs:"plugin_version" mapstructure:"plugin_version"`
|
||||
PluginName string `json:"plugin_name" structs:"plugin_name" mapstructure:"plugin_name"`
|
||||
PluginVersion string `json:"plugin_version" structs:"plugin_version" mapstructure:"plugin_version"`
|
||||
RunningPluginVersion string `json:"running_plugin_version,omitempty" structs:"running_plugin_version,omitempty" mapstructure:"running_plugin_version,omitempty"`
|
||||
// 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"`
|
||||
@@ -376,9 +377,22 @@ func (b *databaseBackend) connectionReadHandler() framework.OperationFunc {
|
||||
delete(config.ConnectionDetails, "private_key")
|
||||
delete(config.ConnectionDetails, "service_account_json")
|
||||
|
||||
return &logical.Response{
|
||||
Data: structs.New(config).Map(),
|
||||
}, nil
|
||||
resp := &logical.Response{}
|
||||
if dbi, err := b.GetConnection(ctx, req.Storage, name); err == nil {
|
||||
config.RunningPluginVersion = dbi.runningPluginVersion
|
||||
if config.PluginVersion != "" && config.PluginVersion != config.RunningPluginVersion {
|
||||
warning := fmt.Sprintf("Plugin version is configured as %q, but running %q", config.PluginVersion, config.RunningPluginVersion)
|
||||
if pinnedVersion, _ := b.getPinnedVersion(ctx, config.PluginName); pinnedVersion == config.RunningPluginVersion {
|
||||
warning += " because that version is pinned"
|
||||
} else {
|
||||
warning += " either due to a pinned version or because the plugin was upgraded and not yet reloaded"
|
||||
}
|
||||
resp.AddWarning(warning)
|
||||
}
|
||||
}
|
||||
|
||||
resp.Data = structs.New(config).Map()
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,9 +521,10 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
|
||||
|
||||
// Close and remove the old connection
|
||||
oldConn := b.connections.Put(name, &dbPluginInstance{
|
||||
database: dbw,
|
||||
name: name,
|
||||
id: id,
|
||||
database: dbw,
|
||||
name: name,
|
||||
id: id,
|
||||
runningPluginVersion: pluginVersion,
|
||||
})
|
||||
if oldConn != nil {
|
||||
oldConn.Close()
|
||||
|
||||
6
changelog/25105.txt
Normal file
6
changelog/25105.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
```release-note:change
|
||||
plugins/database: Reading connection config at `database/config/:name` will now return a computed `running_plugin_version` field if a non-builtin version is running.
|
||||
```
|
||||
```release-note:improvement
|
||||
plugins: Add new pin version APIs to enforce all plugins of a specific type and name to run the same version.
|
||||
```
|
||||
@@ -38,6 +38,10 @@ check_fmt() {
|
||||
echo "--> The following files need to be reformatted with gofumpt"
|
||||
printf '%s\n' "${malformed[@]}"
|
||||
echo "Run \`make fmt\` to reformat code."
|
||||
for file in "${malformed[@]}"; do
|
||||
gofumpt -w "$file"
|
||||
echo "$(git diff --no-color "$file")"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/hashicorp/vault/helper/testhelpers/pluginhelpers"
|
||||
"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"
|
||||
"github.com/hashicorp/vault/sdk/plugin"
|
||||
"github.com/hashicorp/vault/sdk/plugin/mock"
|
||||
@@ -96,98 +95,6 @@ func TestCore_EnableExternalPlugin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCore_UpgradePluginUsingPinnedVersion tests a full workflow of upgrading
|
||||
// an external plugin gated by pinned versions.
|
||||
func TestCore_UpgradePluginUsingPinnedVersion(t *testing.T) {
|
||||
cluster := NewTestCluster(t, &CoreConfig{}, &TestClusterOptions{
|
||||
Plugins: []*TestPluginConfig{
|
||||
{
|
||||
Typ: consts.PluginTypeCredential,
|
||||
Versions: []string{""},
|
||||
},
|
||||
{
|
||||
Typ: consts.PluginTypeSecrets,
|
||||
Versions: []string{""},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cluster.Start()
|
||||
t.Cleanup(cluster.Cleanup)
|
||||
|
||||
c := cluster.Cores[0].Core
|
||||
TestWaitActive(t, c)
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
idx int
|
||||
}{
|
||||
"credential plugin": {
|
||||
idx: 0,
|
||||
},
|
||||
"secrets plugin": {
|
||||
idx: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
plugin := cluster.Plugins[tc.idx]
|
||||
for _, version := range []string{"v1.0.0", "v1.0.1"} {
|
||||
registerPlugin(t, c.systemBackend, plugin.Name, plugin.Typ.String(), version, plugin.Sha256, plugin.FileName)
|
||||
}
|
||||
|
||||
// Mount 1.0.0 then pin to 1.0.1
|
||||
mountPlugin(t, c.systemBackend, plugin.Name, plugin.Typ, "v1.0.0", "")
|
||||
err := c.pluginCatalog.SetPinnedVersion(context.Background(), &pluginutil.PinnedVersion{
|
||||
Name: plugin.Name,
|
||||
Type: plugin.Typ,
|
||||
Version: "v1.0.1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mountedPath := "foo/"
|
||||
if plugin.Typ == consts.PluginTypeCredential {
|
||||
mountedPath = "auth/" + mountedPath
|
||||
}
|
||||
expectRunningVersion(t, c, mountedPath, "v1.0.0")
|
||||
|
||||
reloaded, err := c.reloadMatchingPlugin(context.Background(), nil, plugin.Typ, plugin.Name)
|
||||
if reloaded != 1 || err != nil {
|
||||
t.Fatal(reloaded, err)
|
||||
}
|
||||
|
||||
// Pinned version should be in effect after reloading.
|
||||
expectRunningVersion(t, c, mountedPath, "v1.0.1")
|
||||
|
||||
err = c.pluginCatalog.DeletePinnedVersion(context.Background(), plugin.Typ, plugin.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reloaded, err = c.reloadMatchingPlugin(context.Background(), nil, plugin.Typ, plugin.Name)
|
||||
if reloaded != 1 || err != nil {
|
||||
t.Fatal(reloaded, err)
|
||||
}
|
||||
|
||||
// After pin is deleted, the previously configured version should stand.
|
||||
expectRunningVersion(t, c, mountedPath, "v1.0.0")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func expectRunningVersion(t *testing.T, c *Core, path, expectedVersion string) {
|
||||
t.Helper()
|
||||
match := c.router.MatchingMount(namespace.RootContext(context.Background()), path)
|
||||
if match != path {
|
||||
t.Fatalf("missing mount for %s, match: %q", path, match)
|
||||
}
|
||||
|
||||
raw, _ := c.router.root.Get(match)
|
||||
if actual := raw.(*routeEntry).mountEntry.RunningVersion; expectedVersion != actual {
|
||||
t.Fatalf("expected running_plugin_version to be %s but got %s", expectedVersion, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCore_EnableExternalPlugin_MultipleVersions(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
pluginType consts.PluginType
|
||||
|
||||
@@ -25,13 +25,14 @@ import (
|
||||
postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql"
|
||||
vaulthttp "github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
"github.com/hashicorp/vault/sdk/helper/pluginutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
|
||||
func getClusterWithFileAuditBackend(t *testing.T, typ consts.PluginType, numCores int) *vault.TestCluster {
|
||||
func getCluster(t *testing.T, numCores int, types ...consts.PluginType) *vault.TestCluster {
|
||||
pluginDir := corehelpers.MakeTestPluginDir(t)
|
||||
coreConfig := &vault.CoreConfig{
|
||||
PluginDirectory: pluginDir,
|
||||
@@ -46,39 +47,16 @@ func getClusterWithFileAuditBackend(t *testing.T, typ consts.PluginType, numCore
|
||||
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
||||
TempDir: pluginDir,
|
||||
NumCores: numCores,
|
||||
Plugins: []*vault.TestPluginConfig{
|
||||
{
|
||||
Typ: typ,
|
||||
Versions: []string{""},
|
||||
},
|
||||
},
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
|
||||
cluster.Start()
|
||||
vault.TestWaitActive(t, cluster.Cores[0].Core)
|
||||
|
||||
return cluster
|
||||
}
|
||||
|
||||
func getCluster(t *testing.T, typ consts.PluginType, numCores int) *vault.TestCluster {
|
||||
pluginDir := corehelpers.MakeTestPluginDir(t)
|
||||
coreConfig := &vault.CoreConfig{
|
||||
PluginDirectory: pluginDir,
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
"database": database.Factory,
|
||||
},
|
||||
}
|
||||
|
||||
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
||||
TempDir: pluginDir,
|
||||
NumCores: numCores,
|
||||
Plugins: []*vault.TestPluginConfig{
|
||||
{
|
||||
Typ: typ,
|
||||
Versions: []string{""},
|
||||
},
|
||||
},
|
||||
Plugins: func() []*vault.TestPluginConfig {
|
||||
var plugins []*vault.TestPluginConfig
|
||||
for _, typ := range types {
|
||||
plugins = append(plugins, &vault.TestPluginConfig{
|
||||
Typ: typ,
|
||||
Versions: []string{""},
|
||||
})
|
||||
}
|
||||
return plugins
|
||||
}(),
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
|
||||
@@ -127,34 +105,49 @@ func TestExternalPlugin_RollbackAndReload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testRegisterAndEnable(t *testing.T, client *api.Client, plugin pluginhelpers.TestPlugin) {
|
||||
func testRegisterVersion(t *testing.T, client *api.Client, plugin pluginhelpers.TestPlugin, version string) {
|
||||
t.Helper()
|
||||
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
|
||||
Name: plugin.Name,
|
||||
Type: api.PluginType(plugin.Typ),
|
||||
Command: plugin.Name,
|
||||
SHA256: plugin.Sha256,
|
||||
Version: plugin.Version,
|
||||
Version: version,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func testEnableVersion(t *testing.T, client *api.Client, plugin pluginhelpers.TestPlugin, version string) {
|
||||
t.Helper()
|
||||
switch plugin.Typ {
|
||||
case consts.PluginTypeSecrets:
|
||||
if err := client.Sys().Mount(plugin.Name, &api.MountInput{
|
||||
Type: plugin.Name,
|
||||
Config: api.MountConfigInput{
|
||||
PluginVersion: version,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case consts.PluginTypeCredential:
|
||||
if err := client.Sys().EnableAuthWithOptions(plugin.Name, &api.EnableAuthOptions{
|
||||
Type: plugin.Name,
|
||||
Config: api.MountConfigInput{
|
||||
PluginVersion: version,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRegisterAndEnable(t *testing.T, client *api.Client, plugin pluginhelpers.TestPlugin) {
|
||||
t.Helper()
|
||||
testRegisterVersion(t, client, plugin, plugin.Version)
|
||||
testEnableVersion(t, client, plugin, plugin.Version)
|
||||
}
|
||||
|
||||
// TestExternalPlugin_ContinueOnError tests that vault can recover from a
|
||||
// sha256 mismatch or missing plugin binary scenario
|
||||
func TestExternalPlugin_ContinueOnError(t *testing.T) {
|
||||
@@ -186,7 +179,7 @@ func TestExternalPlugin_ContinueOnError(t *testing.T) {
|
||||
}
|
||||
|
||||
func testExternalPlugin_ContinueOnError(t *testing.T, mismatch bool, pluginType consts.PluginType) {
|
||||
cluster := getCluster(t, pluginType, 1)
|
||||
cluster := getCluster(t, 1, pluginType)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
core := cluster.Cores[0]
|
||||
@@ -222,7 +215,7 @@ func testExternalPlugin_ContinueOnError(t *testing.T, mismatch bool, pluginType
|
||||
t.Fatalf("err:%v resp:%#v", err, resp)
|
||||
}
|
||||
} else {
|
||||
err := os.Remove(filepath.Join(cluster.TempDir, filepath.Base(command)))
|
||||
err := os.Remove(filepath.Join(cluster.Cores[0].CoreConfig.PluginDirectory, filepath.Base(command)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -294,7 +287,7 @@ func testExternalPlugin_ContinueOnError(t *testing.T, mismatch bool, pluginType
|
||||
// TestExternalPlugin_AuthMethod tests that we can build, register and use an
|
||||
// external auth method
|
||||
func TestExternalPlugin_AuthMethod(t *testing.T) {
|
||||
cluster := getCluster(t, consts.PluginTypeCredential, 5)
|
||||
cluster := getCluster(t, 5, consts.PluginTypeCredential)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
plugin := cluster.Plugins[0]
|
||||
@@ -302,15 +295,7 @@ func TestExternalPlugin_AuthMethod(t *testing.T) {
|
||||
client.SetToken(cluster.RootToken)
|
||||
|
||||
// Register
|
||||
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
|
||||
Name: plugin.Name,
|
||||
Type: api.PluginType(plugin.Typ),
|
||||
Command: plugin.Name,
|
||||
SHA256: plugin.Sha256,
|
||||
Version: plugin.Version,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testRegisterVersion(t, client, plugin, plugin.Version)
|
||||
|
||||
// define a group of parallel tests so we wait for their execution before
|
||||
// continuing on to cleanup
|
||||
@@ -413,7 +398,7 @@ func TestExternalPlugin_AuthMethod(t *testing.T) {
|
||||
// TestExternalPlugin_AuthMethodReload tests that we can use an external auth
|
||||
// method after reload
|
||||
func TestExternalPlugin_AuthMethodReload(t *testing.T) {
|
||||
cluster := getCluster(t, consts.PluginTypeCredential, 1)
|
||||
cluster := getCluster(t, 1, consts.PluginTypeCredential)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
plugin := cluster.Plugins[0]
|
||||
@@ -488,7 +473,7 @@ func TestExternalPlugin_AuthMethodReload(t *testing.T) {
|
||||
// TestExternalPlugin_SecretsEngine tests that we can build, register and use an
|
||||
// external secrets engine
|
||||
func TestExternalPlugin_SecretsEngine(t *testing.T) {
|
||||
cluster := getCluster(t, consts.PluginTypeSecrets, 1)
|
||||
cluster := getCluster(t, 1, consts.PluginTypeSecrets)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
plugin := cluster.Plugins[0]
|
||||
@@ -496,15 +481,7 @@ func TestExternalPlugin_SecretsEngine(t *testing.T) {
|
||||
client.SetToken(cluster.RootToken)
|
||||
|
||||
// Register
|
||||
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
|
||||
Name: plugin.Name,
|
||||
Type: api.PluginType(plugin.Typ),
|
||||
Command: plugin.Name,
|
||||
SHA256: plugin.Sha256,
|
||||
Version: plugin.Version,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testRegisterVersion(t, client, plugin, plugin.Version)
|
||||
|
||||
// define a group of parallel tests so we wait for their execution before
|
||||
// continuing on to cleanup
|
||||
@@ -568,7 +545,7 @@ func TestExternalPlugin_SecretsEngine(t *testing.T) {
|
||||
// TestExternalPlugin_SecretsEngineReload tests that we can use an external
|
||||
// secrets engine after reload
|
||||
func TestExternalPlugin_SecretsEngineReload(t *testing.T) {
|
||||
cluster := getCluster(t, consts.PluginTypeSecrets, 1)
|
||||
cluster := getCluster(t, 1, consts.PluginTypeSecrets)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
plugin := cluster.Plugins[0]
|
||||
@@ -634,7 +611,7 @@ func TestExternalPlugin_SecretsEngineReload(t *testing.T) {
|
||||
// TestExternalPlugin_Database tests that we can build, register and use an
|
||||
// external database secrets engine
|
||||
func TestExternalPlugin_Database(t *testing.T) {
|
||||
cluster := getCluster(t, consts.PluginTypeDatabase, 1)
|
||||
cluster := getCluster(t, 1, consts.PluginTypeDatabase)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
plugin := cluster.Plugins[0]
|
||||
@@ -642,15 +619,7 @@ func TestExternalPlugin_Database(t *testing.T) {
|
||||
client.SetToken(cluster.RootToken)
|
||||
|
||||
// Register
|
||||
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
|
||||
Name: plugin.Name,
|
||||
Type: api.PluginType(consts.PluginTypeDatabase),
|
||||
Command: plugin.Name,
|
||||
SHA256: plugin.Sha256,
|
||||
Version: plugin.Version,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testRegisterVersion(t, client, plugin, plugin.Version)
|
||||
|
||||
// Enable
|
||||
if err := client.Sys().Mount(consts.PluginTypeDatabase.String(), &api.MountInput{
|
||||
@@ -749,7 +718,7 @@ func TestExternalPlugin_Database(t *testing.T) {
|
||||
client.SetToken(cluster.RootToken)
|
||||
|
||||
// Lookup - expect FAILURE
|
||||
resp, err = client.Sys().Lookup(revokeLease)
|
||||
_, err = client.Sys().Lookup(revokeLease)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
@@ -770,7 +739,7 @@ func TestExternalPlugin_Database(t *testing.T) {
|
||||
// TestExternalPlugin_DatabaseReload tests that we can use an external database
|
||||
// secrets engine after reload
|
||||
func TestExternalPlugin_DatabaseReload(t *testing.T) {
|
||||
cluster := getCluster(t, consts.PluginTypeDatabase, 1)
|
||||
cluster := getCluster(t, 1, consts.PluginTypeDatabase)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
plugin := cluster.Plugins[0]
|
||||
@@ -778,15 +747,7 @@ func TestExternalPlugin_DatabaseReload(t *testing.T) {
|
||||
client.SetToken(cluster.RootToken)
|
||||
|
||||
// Register
|
||||
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
|
||||
Name: plugin.Name,
|
||||
Type: api.PluginType(consts.PluginTypeDatabase),
|
||||
Command: plugin.Name,
|
||||
SHA256: plugin.Sha256,
|
||||
Version: plugin.Version,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testRegisterVersion(t, client, plugin, plugin.Version)
|
||||
|
||||
// Enable
|
||||
if err := client.Sys().Mount(consts.PluginTypeDatabase.String(), &api.MountInput{
|
||||
@@ -884,7 +845,7 @@ func testExternalPluginMetadataAuditLog(t *testing.T, log map[string]interface{}
|
||||
// TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Auth tests that we have plugin metadata of an auth plugin
|
||||
// in audit log when it is enabled
|
||||
func TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Auth(t *testing.T) {
|
||||
cluster := getClusterWithFileAuditBackend(t, consts.PluginTypeCredential, 1)
|
||||
cluster := getCluster(t, 1, consts.PluginTypeCredential)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
plugin := cluster.Plugins[0]
|
||||
@@ -899,6 +860,7 @@ func TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Auth(t *testing.T)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer auditLogFile.Close()
|
||||
|
||||
err = client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{
|
||||
Type: "file",
|
||||
@@ -954,7 +916,7 @@ func TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Auth(t *testing.T)
|
||||
// TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Secret tests that we have plugin metadata of a secret plugin
|
||||
// in audit log when it is enabled
|
||||
func TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Secret(t *testing.T) {
|
||||
cluster := getClusterWithFileAuditBackend(t, consts.PluginTypeSecrets, 1)
|
||||
cluster := getCluster(t, 1, consts.PluginTypeSecrets)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
plugin := cluster.Plugins[0]
|
||||
@@ -969,6 +931,7 @@ func TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Secret(t *testing.T
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer auditLogFile.Close()
|
||||
|
||||
err = client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{
|
||||
Type: "file",
|
||||
@@ -1023,3 +986,190 @@ func TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Secret(t *testing.T
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func testPin(t *testing.T, client *api.Client, op logical.Operation, pin *pluginutil.PinnedVersion) *api.Secret {
|
||||
t.Helper()
|
||||
switch op {
|
||||
case logical.CreateOperation, logical.UpdateOperation:
|
||||
resp, err := client.Logical().Write(fmt.Sprintf("sys/plugins/pins/%s/%s", pin.Type.String(), pin.Name), map[string]any{
|
||||
"version": pin.Version,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return resp
|
||||
case logical.DeleteOperation:
|
||||
resp, err := client.Logical().Delete(fmt.Sprintf("sys/plugins/pins/%s/%s", pin.Type.String(), pin.Name))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return resp
|
||||
default:
|
||||
t.Fatal("unsupported operation")
|
||||
// Satisfy the compiler that there's no escape from the switch statement.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testReload(t *testing.T, client *api.Client, plugin pluginhelpers.TestPlugin) {
|
||||
_, err := client.Sys().RootReloadPlugin(context.Background(), &api.RootReloadPluginInput{
|
||||
Plugin: plugin.Name,
|
||||
Type: api.PluginType(plugin.Typ),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func expectRunningVersion(t *testing.T, client *api.Client, plugin pluginhelpers.TestPlugin, expectedVersion string) {
|
||||
t.Helper()
|
||||
switch plugin.Typ {
|
||||
case consts.PluginTypeCredential:
|
||||
auth, err := client.Logical().Read("sys/auth/" + plugin.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if auth.Data["running_plugin_version"] != expectedVersion {
|
||||
t.Fatalf("expected running_plugin_version to be %s but got %s", expectedVersion, auth.Data["running_plugin_version"])
|
||||
}
|
||||
case consts.PluginTypeSecrets:
|
||||
mount, err := client.Logical().Read("sys/mounts/" + plugin.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mount.Data["running_plugin_version"] != expectedVersion {
|
||||
t.Fatalf("expected running_plugin_version to be %s but got %s", expectedVersion, mount.Data["running_plugin_version"])
|
||||
}
|
||||
case consts.PluginTypeDatabase:
|
||||
resp, err := client.Logical().Read("database/config/" + plugin.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.Data["running_plugin_version"] != expectedVersion {
|
||||
t.Fatalf("expected running_plugin_version to be %s but got %s", expectedVersion, resp.Data["running_plugin_version"])
|
||||
}
|
||||
expectedWarnings := 0
|
||||
if resp.Data["plugin_version"] != resp.Data["running_plugin_version"] {
|
||||
expectedWarnings = 1
|
||||
}
|
||||
|
||||
if expectedWarnings != len(resp.Warnings) {
|
||||
t.Fatalf("expected %d warning(s) but got %v", expectedWarnings, resp.Warnings)
|
||||
}
|
||||
default:
|
||||
t.Fatal("unsupported plugin type")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCore_UpgradePluginUsingPinnedVersion_AuthAndSecret tests a full workflow
|
||||
// of upgrading an external plugin gated by pinned versions.
|
||||
func TestCore_UpgradePluginUsingPinnedVersion_AuthAndSecret(t *testing.T) {
|
||||
cluster := getCluster(t, 1, consts.PluginTypeCredential, consts.PluginTypeSecrets)
|
||||
t.Cleanup(cluster.Cleanup)
|
||||
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
for name, idx := range map[string]int{
|
||||
"credential plugin": 0,
|
||||
"secrets plugin": 1,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
plugin := cluster.Plugins[idx]
|
||||
|
||||
// Register the same plugin with two versions.
|
||||
for _, version := range []string{"v1.0.0", "v1.0.1"} {
|
||||
testRegisterVersion(t, client, plugin, version)
|
||||
}
|
||||
|
||||
pin101 := &pluginutil.PinnedVersion{
|
||||
Name: plugin.Name,
|
||||
Type: plugin.Typ,
|
||||
Version: "v1.0.1",
|
||||
}
|
||||
|
||||
// Mount 1.0.0 then pin to 1.0.1
|
||||
testEnableVersion(t, client, plugin, "v1.0.0")
|
||||
testPin(t, client, logical.CreateOperation, pin101)
|
||||
expectRunningVersion(t, client, plugin, "v1.0.0")
|
||||
|
||||
// Pinned version should be in effect after reloading.
|
||||
testReload(t, client, plugin)
|
||||
expectRunningVersion(t, client, plugin, "v1.0.1")
|
||||
|
||||
// Deregistering a pinned plugin should fail.
|
||||
if err := client.Sys().DeregisterPlugin(&api.DeregisterPluginInput{
|
||||
Name: plugin.Name,
|
||||
Type: api.PluginType(plugin.Typ),
|
||||
Version: "v1.0.1",
|
||||
}); err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
|
||||
// Now delete, reload, and we should be back to 1.0.0
|
||||
testPin(t, client, logical.DeleteOperation, pin101)
|
||||
testReload(t, client, plugin)
|
||||
expectRunningVersion(t, client, plugin, "v1.0.0")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCore_UpgradePluginUsingPinnedVersion_Database tests a full workflow
|
||||
// of upgrading an external database plugin gated by pinned versions.
|
||||
func TestCore_UpgradePluginUsingPinnedVersion_Database(t *testing.T) {
|
||||
cluster := getCluster(t, 3, consts.PluginTypeDatabase)
|
||||
t.Cleanup(cluster.Cleanup)
|
||||
|
||||
client := cluster.Cores[0].Client
|
||||
plugin := cluster.Plugins[0]
|
||||
|
||||
// Register the same plugin with two versions.
|
||||
for _, version := range []string{"v1.0.0", "v1.0.1"} {
|
||||
testRegisterVersion(t, client, plugin, version)
|
||||
}
|
||||
|
||||
pin101 := &pluginutil.PinnedVersion{
|
||||
Name: plugin.Name,
|
||||
Type: plugin.Typ,
|
||||
Version: "v1.0.1",
|
||||
}
|
||||
|
||||
// Enable the combined db engine first.
|
||||
if err := client.Sys().Mount(consts.PluginTypeDatabase.String(), &api.MountInput{
|
||||
Type: consts.PluginTypeDatabase.String(),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanupPG, connURL := postgreshelper.PrepareTestContainerWithVaultUser(t, context.Background(), "13.4-buster")
|
||||
t.Cleanup(cleanupPG)
|
||||
|
||||
// Mount 1.0.0 then pin to 1.0.1
|
||||
_, err := client.Logical().Write("database/config/"+plugin.Name, map[string]interface{}{
|
||||
"plugin_name": plugin.Name,
|
||||
"plugin_version": "v1.0.0",
|
||||
"connection_url": connURL,
|
||||
"username": "vaultadmin",
|
||||
"password": "vaultpass",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testPin(t, client, logical.CreateOperation, pin101)
|
||||
expectRunningVersion(t, client, plugin, "v1.0.0")
|
||||
|
||||
// Pinned version should be in effect after reloading.
|
||||
testReload(t, client, plugin)
|
||||
// All nodes in the cluster should report the same info, because although
|
||||
// the running_plugin_version info is local to the leader, the standbys
|
||||
// should forward the request to the leader.
|
||||
for i := 0; i < 3; i++ {
|
||||
expectRunningVersion(t, cluster.Cores[i].Client, plugin, "v1.0.1")
|
||||
}
|
||||
|
||||
// Now delete, reload, and we should be back to 1.0.0
|
||||
testPin(t, client, logical.DeleteOperation, pin101)
|
||||
testReload(t, client, plugin)
|
||||
for i := 0; i < 3; i++ {
|
||||
expectRunningVersion(t, client, plugin, "v1.0.0")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ func TestSystemBackend_Plugin_MissingBinary(t *testing.T) {
|
||||
// since that's how we create the file for catalog registration in the test
|
||||
// helper.
|
||||
pluginFileName := filepath.Base(os.Args[0])
|
||||
err = os.Remove(filepath.Join(cluster.TempDir, pluginFileName))
|
||||
err = os.Remove(filepath.Join(cluster.Cores[0].CoreConfig.PluginDirectory, pluginFileName))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.statusPaths()...)
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.pluginsCatalogListPaths()...)
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.pluginsCatalogCRUDPath())
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.pluginsCatalogPinsListPath())
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.pluginsCatalogPinsCRUDPath())
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.pluginsReloadPath())
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.pluginsRootReloadPath())
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.pluginsRuntimesCatalogCRUDPath())
|
||||
@@ -849,6 +851,118 @@ func (b *SystemBackend) handleRootPluginReloadUpdate(ctx context.Context, req *l
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handlePluginCatalogPinUpdate(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
pluginType, pluginName, resp := requirePluginTypeAndName(d)
|
||||
if resp != nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pluginVersion, builtin, err := getVersion(d)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
if pluginVersion == "" {
|
||||
return logical.ErrorResponse("missing plugin version"), nil
|
||||
}
|
||||
if builtin {
|
||||
return logical.ErrorResponse("cannot pin a builtin plugin: %q", pluginVersion), nil
|
||||
}
|
||||
|
||||
err = b.Core.pluginCatalog.SetPinnedVersion(ctx, &pluginutil.PinnedVersion{
|
||||
Name: pluginName,
|
||||
Type: pluginType,
|
||||
Version: pluginVersion,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, plugincatalog.ErrPluginNotFound) {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &logical.Response{}, nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handlePluginCatalogPinRead(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
pluginType, pluginName, resp := requirePluginTypeAndName(d)
|
||||
if resp != nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pin, err := b.Core.pluginCatalog.GetPinnedVersion(ctx, pluginType, pluginName)
|
||||
if errors.Is(err, pluginutil.ErrPinnedVersionNotFound) {
|
||||
return nil, logical.CodedError(http.StatusNotFound, "no pinned version for this plugin")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"name": pin.Name,
|
||||
"type": pin.Type.String(),
|
||||
"version": pin.Version,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handlePluginCatalogPinDelete(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
pluginType, pluginName, resp := requirePluginTypeAndName(d)
|
||||
if resp != nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if err := b.Core.pluginCatalog.DeletePinnedVersion(ctx, pluginType, pluginName); err != nil {
|
||||
if errors.Is(err, pluginutil.ErrPinnedVersionNotFound) {
|
||||
return &logical.Response{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &logical.Response{}, nil
|
||||
}
|
||||
|
||||
func requirePluginTypeAndName(d *framework.FieldData) (consts.PluginType, string, *logical.Response) {
|
||||
pluginName := d.Get("name").(string)
|
||||
if pluginName == "" {
|
||||
return consts.PluginTypeUnknown, "", logical.ErrorResponse("missing plugin name")
|
||||
}
|
||||
|
||||
pluginTypeStr := d.Get("type").(string)
|
||||
if pluginTypeStr == "" {
|
||||
return consts.PluginTypeUnknown, "", logical.ErrorResponse("missing plugin type")
|
||||
}
|
||||
pluginType, err := consts.ParsePluginType(pluginTypeStr)
|
||||
if err != nil {
|
||||
return consts.PluginTypeUnknown, "", logical.ErrorResponse("invalid plugin type: %s", err)
|
||||
}
|
||||
|
||||
return pluginType, pluginName, nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handlePluginCatalogPinList(ctx context.Context, _ *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
pins, err := b.Core.pluginCatalog.ListPinnedVersions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pinnedVersions := []map[string]any{}
|
||||
for _, pin := range pins {
|
||||
pinnedVersions = append(pinnedVersions, map[string]any{
|
||||
"name": pin.Name,
|
||||
"type": pin.Type.String(),
|
||||
"version": pin.Version,
|
||||
})
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"pinned_versions": pinnedVersions,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handlePluginRuntimeCatalogUpdate(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
runtimeName := d.Get("name").(string)
|
||||
if runtimeName == "" {
|
||||
@@ -1599,9 +1713,20 @@ func (b *SystemBackend) handleReadMount(ctx context.Context, req *logical.Reques
|
||||
return logical.ErrorResponse("No secret engine mount at %s", path), nil
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
resp := &logical.Response{
|
||||
Data: b.mountInfo(ctx, entry),
|
||||
}, nil
|
||||
}
|
||||
if entry.Version != "" && entry.Version != entry.RunningVersion {
|
||||
warning := fmt.Sprintf("Plugin version is configured as %q, but running %q", entry.Version, entry.RunningVersion)
|
||||
if pin, _ := b.Core.pluginCatalog.GetPinnedVersion(ctx, consts.PluginTypeSecrets, entry.Type); pin != nil && pin.Version == entry.RunningVersion {
|
||||
warning += " because that version is pinned"
|
||||
} else {
|
||||
warning += " either due to a pinned version or because the plugin was upgraded and not yet reloaded"
|
||||
}
|
||||
resp.AddWarning(warning)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// used to intercept an HTTPCodedError so it goes back to callee
|
||||
@@ -6530,6 +6655,28 @@ Must already be present on the machine.`,
|
||||
`The Vault plugin runtime to use when running the plugin.`,
|
||||
"",
|
||||
},
|
||||
"plugin-catalog-pins": {
|
||||
"Configures pinned plugin versions from the plugin catalog",
|
||||
`
|
||||
This path responds to the following HTTP methods.
|
||||
GET /<type>/<name>
|
||||
Retrieve the pinned version for the named plugin.
|
||||
|
||||
PUT /<type>/<name>
|
||||
Add or update a pinned version for the named plugin. Does not trigger changes until the plugin is reloaded.
|
||||
|
||||
DELETE /<type>/<name>
|
||||
Delete the pinned version for the named plugin. Does not trigger changes until the plugin is reloaded.
|
||||
`,
|
||||
},
|
||||
"plugin-catalog-pins-list-all": {
|
||||
"Lists all the pinned plugin versions known to Vault",
|
||||
`
|
||||
This path responds to the following HTTP methods.
|
||||
LIST /
|
||||
Returns a list of configured pinned versions.
|
||||
`,
|
||||
},
|
||||
"plugin-runtime-catalog": {
|
||||
"Configures plugin runtimes",
|
||||
`
|
||||
|
||||
@@ -2051,6 +2051,126 @@ func (b *SystemBackend) pluginsCatalogListPaths() []*framework.Path {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SystemBackend) pluginsCatalogPinsCRUDPath() *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "plugins/pins/(?P<type>auth|database|secret)/" + framework.GenericNameRegex("name") + "$",
|
||||
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: "plugins-catalog-pins",
|
||||
},
|
||||
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["plugin-catalog_name"][0]),
|
||||
},
|
||||
"type": {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["plugin-catalog_type"][0]),
|
||||
},
|
||||
"version": {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["plugin-catalog_version"][0]),
|
||||
},
|
||||
},
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: b.handlePluginCatalogPinUpdate,
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationVerb: "create",
|
||||
OperationSuffix: "pinned-version",
|
||||
},
|
||||
Responses: map[int][]framework.Response{
|
||||
http.StatusOK: {{
|
||||
Description: "OK",
|
||||
}},
|
||||
},
|
||||
Summary: "Create or update the pinned version for a plugin with a given type and name.",
|
||||
},
|
||||
logical.DeleteOperation: &framework.PathOperation{
|
||||
Callback: b.handlePluginCatalogPinDelete,
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationVerb: "remove",
|
||||
OperationSuffix: "pinned-version",
|
||||
},
|
||||
Responses: map[int][]framework.Response{
|
||||
http.StatusOK: {{
|
||||
Description: "OK",
|
||||
Fields: map[string]*framework.FieldSchema{},
|
||||
}},
|
||||
},
|
||||
Summary: "Remove any pinned version for the plugin with the given type and name.",
|
||||
},
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: b.handlePluginCatalogPinRead,
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationVerb: "read",
|
||||
OperationSuffix: "pinned-version",
|
||||
},
|
||||
Responses: map[int][]framework.Response{
|
||||
http.StatusOK: {{
|
||||
Description: "OK",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["plugin-catalog_name"][0]),
|
||||
Required: true,
|
||||
},
|
||||
"type": {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["plugin-catalog_type"][0]),
|
||||
Required: true,
|
||||
},
|
||||
"version": {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["plugin-catalog_version"][0]),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
Summary: "Return the pinned version for the plugin with the given type and name.",
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["plugin-catalog-pins"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["plugin-catalog-pins"][1]),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SystemBackend) pluginsCatalogPinsListPath() *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "plugins/pins/?$",
|
||||
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: "plugins-catalog-pins",
|
||||
OperationVerb: "list",
|
||||
OperationSuffix: "pinned-versions",
|
||||
},
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: b.handlePluginCatalogPinList,
|
||||
Responses: map[int][]framework.Response{
|
||||
http.StatusOK: {{
|
||||
Description: "OK",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"pinned_versions": {
|
||||
Type: framework.TypeMap,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["plugin-catalog-pins-list-all"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["plugin-catalog-pins-list-all"][1]),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SystemBackend) pluginsReloadPath() *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "plugins/reload/backend$",
|
||||
|
||||
@@ -3740,6 +3740,142 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSystemBackend_PluginCatalogPins_CRUD tests CRUD operations for pinning
|
||||
// plugin versions.
|
||||
func TestSystemBackend_PluginCatalogPins_CRUD(t *testing.T) {
|
||||
sym, err := filepath.EvalSymlinks(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, _, _ := TestCoreUnsealedWithConfig(t, &CoreConfig{
|
||||
PluginDirectory: sym,
|
||||
})
|
||||
b := c.systemBackend
|
||||
ctx := namespace.RootContext(context.Background())
|
||||
|
||||
// List pins.
|
||||
req := logical.TestRequest(t, logical.ReadOperation, "plugins/pins")
|
||||
resp, err := b.HandleRequest(ctx, req)
|
||||
if err != nil || resp.IsError() {
|
||||
t.Fatal(resp, err)
|
||||
}
|
||||
|
||||
schema.ValidateResponse(
|
||||
t,
|
||||
schema.GetResponseSchema(t, b.Route(req.Path), req.Operation),
|
||||
resp,
|
||||
true,
|
||||
)
|
||||
|
||||
if len(resp.Data["pinned_versions"].([]map[string]any)) != 0 {
|
||||
t.Fatalf("Wrong number of plugins, expected %d, got %d", 0, len(resp.Data["pins"].([]string)))
|
||||
}
|
||||
|
||||
// Set a plugin so we can pin to it.
|
||||
file, err := os.CreateTemp(sym, "temp")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
req = logical.TestRequest(t, logical.UpdateOperation, "plugins/catalog/database/test-plugin")
|
||||
req.Data["sha_256"] = hex.EncodeToString([]byte{'1'})
|
||||
req.Data["command"] = filepath.Base(file.Name())
|
||||
req.Data["version"] = "v1.0.0"
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
if err != nil || resp.IsError() {
|
||||
t.Fatal(resp, err)
|
||||
}
|
||||
|
||||
schema.ValidateResponse(
|
||||
t,
|
||||
schema.GetResponseSchema(t, b.Route(req.Path), req.Operation),
|
||||
resp,
|
||||
true,
|
||||
)
|
||||
|
||||
// Now create a pin.
|
||||
req = logical.TestRequest(t, logical.UpdateOperation, "plugins/pins/database/test-plugin")
|
||||
req.Data["version"] = "v1.0.0"
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
if err != nil || resp.IsError() {
|
||||
t.Fatal(resp, err)
|
||||
}
|
||||
|
||||
schema.ValidateResponse(
|
||||
t,
|
||||
schema.GetResponseSchema(t, b.Route(req.Path), req.Operation),
|
||||
resp,
|
||||
true,
|
||||
)
|
||||
|
||||
// Read the pin.
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "plugins/pins/database/test-plugin")
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
if err != nil || resp.Error() != nil {
|
||||
t.Fatal(resp, err)
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"name": "test-plugin",
|
||||
"type": "database",
|
||||
"version": "v1.0.0",
|
||||
}
|
||||
|
||||
actual := resp.Data
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", actual, expected)
|
||||
}
|
||||
|
||||
// List pins again.
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "plugins/pins/")
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
if err != nil || resp.IsError() {
|
||||
t.Fatal(resp, err)
|
||||
}
|
||||
|
||||
schema.ValidateResponse(
|
||||
t,
|
||||
schema.GetResponseSchema(t, b.Route(req.Path), req.Operation),
|
||||
resp,
|
||||
true,
|
||||
)
|
||||
|
||||
pinnedVersions := resp.Data["pinned_versions"].([]map[string]any)
|
||||
if len(pinnedVersions) != 1 {
|
||||
t.Fatalf("Wrong number of plugins, expected %d, got %d", 1, len(resp.Data["pins"].([]string)))
|
||||
}
|
||||
// Check the pin is correct.
|
||||
actual = pinnedVersions[0]
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", actual, expected)
|
||||
}
|
||||
|
||||
// Delete the pin.
|
||||
req = logical.TestRequest(t, logical.DeleteOperation, "plugins/pins/database/test-plugin")
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
if err != nil || resp.IsError() {
|
||||
t.Fatal(resp, err)
|
||||
}
|
||||
|
||||
schema.ValidateResponse(
|
||||
t,
|
||||
schema.GetResponseSchema(t, b.Route(req.Path), req.Operation),
|
||||
resp,
|
||||
true,
|
||||
)
|
||||
|
||||
// Should now get a 404 when reading the pin.
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "plugins/pins/database/test-plugin")
|
||||
_, err = b.HandleRequest(ctx, req)
|
||||
var codedErr logical.HTTPCodedError
|
||||
if !errors.As(err, &codedErr) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if codedErr.Code() != http.StatusNotFound {
|
||||
t.Fatal(codedErr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSystemBackend_PluginCatalog_ContainerCRUD tests that plugins registered
|
||||
// with oci_image set get recorded properly in the catalog.
|
||||
func TestSystemBackend_PluginCatalog_ContainerCRUD(t *testing.T) {
|
||||
|
||||
@@ -33,7 +33,7 @@ func (c *PluginCatalog) SetPinnedVersion(ctx context.Context, pin *pluginutil.Pi
|
||||
return err
|
||||
}
|
||||
if plugin == nil {
|
||||
return fmt.Errorf("%s plugin %q version %s does not exist", pin.Type.String(), pin.Name, pin.Version)
|
||||
return fmt.Errorf("%w; %s plugin %q version %s does not exist", ErrPluginNotFound, pin.Type.String(), pin.Name, pin.Version)
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(pin)
|
||||
|
||||
Reference in New Issue
Block a user