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:
Philipp Stehle
2026-05-28 10:43:56 +02:00
committed by GitHub
parent 91d2fbf976
commit 64a92935d8
6 changed files with 213 additions and 137 deletions
+3
View File
@@ -0,0 +1,3 @@
```release-note:bug
sys: fix `/sys/plugins/catalog` and `/sys/plugins/catalog/<type>` not returning versioned plugins
```
+25 -16
View File
@@ -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]
+9
View File
@@ -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
View File
@@ -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
View File
@@ -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)
-69
View File
@@ -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())