mirror of
https://github.com/openbao/openbao.git
synced 2026-06-01 18:57:37 +02:00
fix: versioned plugins missing in catalog response (#3186)
* fix: versioned plugins missing in catalog response Signed-off-by: Philipp Stehle <philipp.stehle@secretz.io> * drop unversioned only plugin list Signed-off-by: Philipp Stehle <philipp.stehle@secretz.io> * update changelog Co-authored-by: Wojciech Slabosz <wojciech.slabosz@sap.com> Signed-off-by: Philipp Stehle <philipp.stehle@secretz.io> * fix lint Signed-off-by: Philipp Stehle <philipp.stehle@secretz.io> --------- Signed-off-by: Philipp Stehle <philipp.stehle@secretz.io> Co-authored-by: Wojciech Slabosz <wojciech.slabosz@sap.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
```release-note:bug
|
||||
sys: fix `/sys/plugins/catalog` and `/sys/plugins/catalog/<type>` not returning versioned plugins
|
||||
```
|
||||
+25
-16
@@ -346,36 +346,31 @@ func (b *SystemBackend) handlePluginCatalogTypedList(ctx context.Context, req *l
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plugins, err := b.Core.pluginCatalog.List(ctx, pluginType)
|
||||
plugins, err := b.Core.pluginCatalog.ListVersionedPlugins(ctx, pluginType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(plugins)
|
||||
return logical.ListResponse(plugins), nil
|
||||
|
||||
return logical.ListResponse(uniquePluginNames(plugins)), nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handlePluginCatalogUntypedList(ctx context.Context, _ *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
data := make(map[string]interface{})
|
||||
data := make(map[string]any)
|
||||
var versionedPlugins []pluginutil.VersionedPlugin
|
||||
for _, pluginType := range pluginTypes {
|
||||
plugins, err := b.Core.pluginCatalog.List(ctx, pluginType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(plugins) > 0 {
|
||||
sort.Strings(plugins)
|
||||
data[pluginType.String()] = plugins
|
||||
}
|
||||
|
||||
versioned, err := b.Core.pluginCatalog.ListVersionedPlugins(ctx, pluginType)
|
||||
plugins, err := b.Core.pluginCatalog.ListVersionedPlugins(ctx, pluginType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sort for consistent ordering
|
||||
sortVersionedPlugins(versioned)
|
||||
sortVersionedPlugins(plugins)
|
||||
|
||||
versionedPlugins = append(versionedPlugins, versioned...)
|
||||
if len(plugins) > 0 {
|
||||
data[pluginType.String()] = uniquePluginNames(plugins)
|
||||
}
|
||||
|
||||
versionedPlugins = append(versionedPlugins, plugins...)
|
||||
}
|
||||
|
||||
if len(versionedPlugins) != 0 {
|
||||
@@ -408,6 +403,20 @@ func (b *SystemBackend) handlePluginCatalogUntypedList(ctx context.Context, _ *l
|
||||
}, nil
|
||||
}
|
||||
|
||||
func uniquePluginNames(plugins []pluginutil.VersionedPlugin) []string {
|
||||
pluginNames := make([]string, 0, len(plugins))
|
||||
|
||||
for _, plugin := range plugins {
|
||||
index, match := slices.BinarySearch(pluginNames, plugin.Name)
|
||||
if match {
|
||||
continue
|
||||
}
|
||||
pluginNames = slices.Insert(pluginNames, index, plugin.Name)
|
||||
}
|
||||
|
||||
return pluginNames
|
||||
}
|
||||
|
||||
func sortVersionedPlugins(versionedPlugins []pluginutil.VersionedPlugin) {
|
||||
sort.SliceStable(versionedPlugins, func(i, j int) bool {
|
||||
left, right := versionedPlugins[i], versionedPlugins[j]
|
||||
|
||||
@@ -1892,6 +1892,15 @@ func (b *SystemBackend) pluginsCatalogListPaths() []*framework.Path {
|
||||
Type: framework.TypeMap,
|
||||
Required: false,
|
||||
},
|
||||
"auth": {
|
||||
Type: framework.TypeStringSlice,
|
||||
},
|
||||
"database": {
|
||||
Type: framework.TypeStringSlice,
|
||||
},
|
||||
"secret": {
|
||||
Type: framework.TypeStringSlice,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
|
||||
+173
-19
@@ -11,6 +11,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -25,7 +26,6 @@ import (
|
||||
auditFile "github.com/openbao/openbao/builtin/audit/file"
|
||||
credUserpass "github.com/openbao/openbao/builtin/credential/userpass"
|
||||
"github.com/openbao/openbao/command/server"
|
||||
"github.com/openbao/openbao/helper/builtinplugins"
|
||||
"github.com/openbao/openbao/helper/identity"
|
||||
"github.com/openbao/openbao/helper/namespace"
|
||||
"github.com/openbao/openbao/helper/random"
|
||||
@@ -3627,7 +3627,7 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) {
|
||||
}
|
||||
c.pluginCatalog.directory = sym
|
||||
|
||||
req := logical.TestRequest(t, logical.ListOperation, "plugins/catalog/database")
|
||||
req := logical.TestRequest(t, logical.ReadOperation, "plugins/catalog/database/mysql-database-plugin")
|
||||
resp, err := b.HandleRequest(namespace.RootContext(t.Context()), req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
@@ -3640,23 +3640,6 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) {
|
||||
true,
|
||||
)
|
||||
|
||||
if len(resp.Data["keys"].([]string)) != len(c.builtinRegistry.Keys(consts.PluginTypeDatabase)) {
|
||||
t.Fatalf("Wrong number of plugins, got %d, expected %d", len(resp.Data["keys"].([]string)), len(builtinplugins.Registry.Keys(consts.PluginTypeDatabase)))
|
||||
}
|
||||
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "plugins/catalog/database/mysql-database-plugin")
|
||||
resp, err = b.HandleRequest(namespace.RootContext(t.Context()), req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
schema.ValidateResponse(
|
||||
t,
|
||||
schema.GetResponseSchema(t, b.(*SystemBackend).Route(req.Path), req.Operation),
|
||||
resp,
|
||||
true,
|
||||
)
|
||||
|
||||
// Get deprecation status directly from the registry so we can compare it to the API response
|
||||
deprecationStatus, _ := c.builtinRegistry.DeprecationStatus("mysql-database-plugin", consts.PluginTypeDatabase)
|
||||
|
||||
@@ -3859,6 +3842,177 @@ func TestSystemBackend_PluginCatalog_CannotRegisterBuiltinPlugins(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemBackend_PluginCatalog_List(t *testing.T) {
|
||||
pluginDir := t.TempDir()
|
||||
file, err := os.Create(path.Join(pluginDir, "foo"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, file.Close())
|
||||
|
||||
c, b, _ := testCoreSystemBackend(t)
|
||||
// Bootstrap the pluginCatalog
|
||||
sym, err := filepath.EvalSymlinks(pluginDir)
|
||||
require.NoError(t, err)
|
||||
c.pluginCatalog.directory = sym
|
||||
|
||||
// Set a plugin
|
||||
req := logical.TestRequest(t, logical.UpdateOperation, "plugins/catalog/database/test-plugin")
|
||||
req.Data["sha256"] = hex.EncodeToString([]byte{'1'})
|
||||
req.Data["command"] = "foo"
|
||||
req.Data["version"] = "v1.2.3"
|
||||
resp, err := b.HandleRequest(namespace.RootContext(t.Context()), req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Error())
|
||||
|
||||
t.Run("typed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := logical.TestRequest(t, logical.ListOperation, "plugins/catalog/database")
|
||||
resp, err := b.HandleRequest(namespace.RootContext(t.Context()), req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Error())
|
||||
|
||||
schema.ValidateResponse(
|
||||
t,
|
||||
schema.GetResponseSchema(t, b.(*SystemBackend).Route(req.Path), req.Operation),
|
||||
resp,
|
||||
true,
|
||||
)
|
||||
|
||||
if diff := deep.Equal(resp.Data, map[string]any{
|
||||
"keys": []string{
|
||||
"cassandra-database-plugin", "influxdb-database-plugin", "mysql-aurora-database-plugin",
|
||||
"mysql-database-plugin", "mysql-legacy-database-plugin", "mysql-rds-database-plugin",
|
||||
"postgresql-database-plugin", "redis-database-plugin", "test-plugin", "valkey-database-plugin",
|
||||
},
|
||||
}); diff != nil {
|
||||
t.Fatal(strings.Join(diff, "\n"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("untyped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := logical.TestRequest(t, logical.ReadOperation, "plugins/catalog")
|
||||
resp, err := b.HandleRequest(namespace.RootContext(t.Context()), req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Error())
|
||||
|
||||
schema.ValidateResponse(
|
||||
t,
|
||||
schema.GetResponseSchema(t, b.(*SystemBackend).Route(req.Path), req.Operation),
|
||||
resp,
|
||||
true,
|
||||
)
|
||||
|
||||
require.Equal(t, resp.Data, map[string]any{
|
||||
"secret": []string{"keymgmt", "kmip", "kv", "transform"},
|
||||
"auth": []string{"approle", "pending-removal-test-plugin"},
|
||||
"database": []string{
|
||||
"cassandra-database-plugin", "influxdb-database-plugin", "mysql-aurora-database-plugin",
|
||||
"mysql-database-plugin", "mysql-legacy-database-plugin", "mysql-rds-database-plugin",
|
||||
"postgresql-database-plugin", "redis-database-plugin", "test-plugin", "valkey-database-plugin",
|
||||
},
|
||||
"detailed": []map[string]any{{
|
||||
"name": "approle",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "auth",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "pending-removal-test-plugin",
|
||||
"builtin": true,
|
||||
"deprecation_status": "pending removal",
|
||||
"type": "auth",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "cassandra-database-plugin",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "database",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "influxdb-database-plugin",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "database",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "mysql-aurora-database-plugin",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "database",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "mysql-database-plugin",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "database",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "mysql-legacy-database-plugin",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "database",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "mysql-rds-database-plugin",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "database",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "postgresql-database-plugin",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "database",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "redis-database-plugin",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "database",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"sha256": "31",
|
||||
"builtin": false,
|
||||
"name": "test-plugin",
|
||||
"type": "database",
|
||||
"version": "v1.2.3",
|
||||
}, {
|
||||
"name": "valkey-database-plugin",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "database",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "keymgmt",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "secret",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "kmip",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "secret",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "kv",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "secret",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}, {
|
||||
"name": "transform",
|
||||
"builtin": true,
|
||||
"deprecation_status": "supported",
|
||||
"type": "secret",
|
||||
"version": "v2.0.0+builtin.bao",
|
||||
}},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSystemBackend_ToolsHash(t *testing.T) {
|
||||
b := testSystemBackend(t)
|
||||
req := logical.TestRequest(t, logical.UpdateOperation, "tools/hash")
|
||||
|
||||
+3
-33
@@ -373,7 +373,7 @@ func (c *PluginCatalog) registerDeclarativePlugins(ctx context.Context, plugins
|
||||
// Check if we need to remove any plugins.
|
||||
for _, pluginType := range pluginTypes {
|
||||
if err := func() error {
|
||||
storedPlugins, err := c.listInternal(ctx, pluginType, true /* versioned */)
|
||||
storedPlugins, err := c.listInternal(ctx, pluginType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list plugins: %w", err)
|
||||
}
|
||||
@@ -1285,40 +1285,14 @@ func (c *PluginCatalog) deleteInternal(ctx context.Context, name string, pluginT
|
||||
return c.catalogView.Delete(ctx, pluginKey)
|
||||
}
|
||||
|
||||
// List returns a list of all the known plugin names. If an external and builtin
|
||||
// plugin share the same name, only one instance of the name will be returned.
|
||||
func (c *PluginCatalog) List(ctx context.Context, pluginType consts.PluginType) ([]string, error) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
plugins, err := c.listInternal(ctx, pluginType, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use a set to de-dupe between builtin and unversioned external plugins.
|
||||
// External plugins with the same name as a builtin override the builtin.
|
||||
uniquePluginNames := make(map[string]struct{})
|
||||
for _, plugin := range plugins {
|
||||
uniquePluginNames[plugin.Name] = struct{}{}
|
||||
}
|
||||
|
||||
retList := make([]string, 0, len(uniquePluginNames))
|
||||
for plugin := range uniquePluginNames {
|
||||
retList = append(retList, plugin)
|
||||
}
|
||||
|
||||
return retList, nil
|
||||
}
|
||||
|
||||
func (c *PluginCatalog) ListVersionedPlugins(ctx context.Context, pluginType consts.PluginType) ([]pluginutil.VersionedPlugin, error) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
return c.listInternal(ctx, pluginType, true)
|
||||
return c.listInternal(ctx, pluginType)
|
||||
}
|
||||
|
||||
func (c *PluginCatalog) listInternal(ctx context.Context, pluginType consts.PluginType, includeVersioned bool) ([]pluginutil.VersionedPlugin, error) {
|
||||
func (c *PluginCatalog) listInternal(ctx context.Context, pluginType consts.PluginType) ([]pluginutil.VersionedPlugin, error) {
|
||||
var result []pluginutil.VersionedPlugin
|
||||
|
||||
// Collect keys for external plugins in the barrier.
|
||||
@@ -1347,10 +1321,6 @@ func (c *PluginCatalog) listInternal(ctx context.Context, pluginType consts.Plug
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if !includeVersioned {
|
||||
continue
|
||||
}
|
||||
|
||||
semanticVersion, err = semver.NewVersion(plugin.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected error parsing version from plugin catalog entry %q: %w", key, err)
|
||||
|
||||
@@ -232,75 +232,6 @@ func TestPluginCatalog_VersionedCRUD(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginCatalog_List(t *testing.T) {
|
||||
core, _, _ := TestCoreUnsealed(t)
|
||||
tempDir, err := filepath.EvalSymlinks(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
core.pluginCatalog.directory = tempDir
|
||||
|
||||
// Get builtin plugins and sort them
|
||||
builtinKeys := builtinplugins.Registry.Keys(consts.PluginTypeDatabase)
|
||||
sort.Strings(builtinKeys)
|
||||
|
||||
// List only builtin plugins
|
||||
plugins, err := core.pluginCatalog.List(t.Context(), consts.PluginTypeDatabase)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
sort.Strings(plugins)
|
||||
|
||||
if len(plugins) != len(builtinKeys) {
|
||||
t.Fatalf("unexpected length of plugin list, expected %d, got %d", len(builtinKeys), len(plugins))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(plugins, builtinKeys) {
|
||||
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", plugins, builtinKeys)
|
||||
}
|
||||
|
||||
// Set a plugin, test overwriting a builtin plugin
|
||||
file, err := os.CreateTemp(tempDir, "temp")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
command := filepath.Base(file.Name())
|
||||
err = core.pluginCatalog.Set(t.Context(), "mysql-database-plugin", consts.PluginTypeDatabase, "", command, []string{"--test"}, []string{}, []byte{'1'}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set another plugin
|
||||
err = core.pluginCatalog.Set(t.Context(), "aaaaaaa", consts.PluginTypeDatabase, "", command, []string{"--test"}, []string{}, []byte{'1'}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// List the plugins
|
||||
plugins, err = core.pluginCatalog.List(t.Context(), consts.PluginTypeDatabase)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
sort.Strings(plugins)
|
||||
|
||||
// plugins has a test-added plugin called "aaaaaaa" that is not built in
|
||||
if len(plugins) != len(builtinKeys)+1 {
|
||||
t.Fatalf("unexpected length of plugin list, expected %d, got %d", len(builtinKeys)+1, len(plugins))
|
||||
}
|
||||
|
||||
// verify the first plugin is the one we just created.
|
||||
if !reflect.DeepEqual(plugins[0], "aaaaaaa") {
|
||||
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", plugins[0], "aaaaaaa")
|
||||
}
|
||||
|
||||
// verify the builtin plugins are correct
|
||||
if !reflect.DeepEqual(plugins[1:], builtinKeys) {
|
||||
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", plugins[1:], builtinKeys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginCatalog_ListVersionedPlugins(t *testing.T) {
|
||||
core, _, _ := TestCoreUnsealed(t)
|
||||
tempDir, err := filepath.EvalSymlinks(t.TempDir())
|
||||
|
||||
Reference in New Issue
Block a user