From 37b54465482f882fb352b3e774220b0a04a97d7d Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Thu, 11 Aug 2022 14:41:25 +0100 Subject: [PATCH 1/9] Version-aware plugin catalog --- changelog/xyz.txt | 6 + sdk/helper/pluginutil/run_config.go | 1 + sdk/helper/pluginutil/runner.go | 15 ++ vault/auth.go | 4 +- vault/dynamic_system_view.go | 2 +- vault/logical_system.go | 86 +++++++++-- vault/logical_system_paths.go | 4 + vault/mount.go | 2 +- vault/plugin_catalog.go | 232 ++++++++++++++++++++++------ vault/plugin_catalog_test.go | 207 +++++++++++++++++++++---- vault/testing.go | 3 +- 11 files changed, 468 insertions(+), 94 deletions(-) create mode 100644 changelog/xyz.txt diff --git a/changelog/xyz.txt b/changelog/xyz.txt new file mode 100644 index 0000000000000..fc83c7c916dab --- /dev/null +++ b/changelog/xyz.txt @@ -0,0 +1,6 @@ +```change +plugins: `GET /sys/plugins/catalog` endpoint now returns an additional `detailed` field in the response with additional plugin metadata. +``` +```improvement +plugins: Plugin catalog supports registering plugins with a semantic version +``` diff --git a/sdk/helper/pluginutil/run_config.go b/sdk/helper/pluginutil/run_config.go index cb804f60d8738..47228abb9dd7e 100644 --- a/sdk/helper/pluginutil/run_config.go +++ b/sdk/helper/pluginutil/run_config.go @@ -16,6 +16,7 @@ import ( type PluginClientConfig struct { Name string PluginType consts.PluginType + Version string PluginSets map[int]plugin.PluginSet HandshakeConfig plugin.HandshakeConfig Logger log.Logger diff --git a/sdk/helper/pluginutil/runner.go b/sdk/helper/pluginutil/runner.go index f2822efc10408..762e4673c5c35 100644 --- a/sdk/helper/pluginutil/runner.go +++ b/sdk/helper/pluginutil/runner.go @@ -6,6 +6,7 @@ import ( log "github.com/hashicorp/go-hclog" plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-version" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/wrapping" "google.golang.org/grpc" @@ -45,6 +46,7 @@ const MultiplexingCtxKey string = "multiplex_id" type PluginRunner struct { Name string `json:"name" structs:"name"` Type consts.PluginType `json:"type" structs:"type"` + Version string `json:"version" structs:"version"` Command string `json:"command" structs:"command"` Args []string `json:"args" structs:"args"` Env []string `json:"env" structs:"env"` @@ -81,6 +83,19 @@ func (r *PluginRunner) RunMetadataMode(ctx context.Context, wrapper RunnerUtil, ) } +// VersionedPlugin holds any versioning information stored about a plugin in the +// plugin catalog. +type VersionedPlugin struct { + Name string `json:"name"` + Type string `json:"type"` + Version string `json:"version"` + SHA256 string `json:"sha256,omitempty"` + Builtin bool `json:"builtin"` + + // Pre-parsed semver struct of the Version field + SemanticVersion *version.Version `json:"-"` +} + // CtxCancelIfCanceled takes a context cancel func and a context. If the context is // shutdown the cancelfunc is called. This is useful for merging two cancel // functions. diff --git a/vault/auth.go b/vault/auth.go index 5f5762da18c83..58cbd2e38732e 100644 --- a/vault/auth.go +++ b/vault/auth.go @@ -787,7 +787,7 @@ func (c *Core) setupCredentials(ctx context.Context) error { backend, err = c.newCredentialBackend(ctx, entry, sysView, view) if err != nil { c.logger.Error("failed to create credential entry", "path", entry.Path, "error", err) - if plug, plugerr := c.pluginCatalog.Get(ctx, entry.Type, consts.PluginTypeCredential); plugerr == nil && !plug.Builtin { + if plug, plugerr := c.pluginCatalog.Get(ctx, entry.Type, consts.PluginTypeCredential, ""); plugerr == nil && !plug.Builtin { // If we encounter an error instantiating the backend due to an error, // skip backend initialization but register the entry to the mount table // to preserve storage and path. @@ -911,7 +911,7 @@ func (c *Core) newCredentialBackend(ctx context.Context, entry *MountEntry, sysV f, ok := c.credentialBackends[t] if !ok { - plug, err := c.pluginCatalog.Get(ctx, t, consts.PluginTypeCredential) + plug, err := c.pluginCatalog.Get(ctx, t, consts.PluginTypeCredential, "") if err != nil { return nil, err } diff --git a/vault/dynamic_system_view.go b/vault/dynamic_system_view.go index b6f2e8e7343fc..124a134d96101 100644 --- a/vault/dynamic_system_view.go +++ b/vault/dynamic_system_view.go @@ -240,7 +240,7 @@ func (d dynamicSystemView) LookupPlugin(ctx context.Context, name string, plugin if d.core.pluginCatalog == nil { return nil, fmt.Errorf("system view core plugin catalog is nil") } - r, err := d.core.pluginCatalog.Get(ctx, name, pluginType) + r, err := d.core.pluginCatalog.Get(ctx, name, pluginType, "") if err != nil { return nil, err } diff --git a/vault/logical_system.go b/vault/logical_system.go index f0b0d3ef82c97..672e4070b62da 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -29,6 +29,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/go-secure-stdlib/strutil" + semver "github.com/hashicorp/go-version" "github.com/hashicorp/vault/helper/hostutil" "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/metricsutil" @@ -38,6 +39,7 @@ import ( "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/jsonutil" + "github.com/hashicorp/vault/sdk/helper/pluginutil" "github.com/hashicorp/vault/sdk/helper/wrapping" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/version" @@ -399,11 +401,13 @@ func (b *SystemBackend) handlePluginCatalogTypedList(ctx context.Context, req *l if err != nil { return nil, err } + sort.Strings(plugins) return logical.ListResponse(plugins), nil } -func (b *SystemBackend) handlePluginCatalogUntypedList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - pluginsByType := make(map[string]interface{}) +func (b *SystemBackend) handlePluginCatalogUntypedList(ctx context.Context, _ *logical.Request, _ *framework.FieldData) (*logical.Response, error) { + data := make(map[string]interface{}) + var versionedPlugins []pluginutil.VersionedPlugin for _, pluginType := range consts.PluginTypes { plugins, err := b.Core.pluginCatalog.List(ctx, pluginType) if err != nil { @@ -411,15 +415,43 @@ func (b *SystemBackend) handlePluginCatalogUntypedList(ctx context.Context, req } if len(plugins) > 0 { sort.Strings(plugins) - pluginsByType[pluginType.String()] = plugins + data[pluginType.String()] = plugins } + + versioned, err := b.Core.pluginCatalog.ListVersionedPlugins(ctx, pluginType) + if err != nil { + return nil, err + } + + // Sort for consistent ordering + sort.SliceStable(versionedPlugins, func(i, j int) bool { + left, right := versionedPlugins[i], versionedPlugins[j] + if left.Type != right.Type { + return left.Type < right.Type + } + if left.Name != right.Name { + return left.Name < right.Name + } + if left.Version != right.Version { + return right.SemanticVersion.GreaterThan(left.SemanticVersion) + } + + return true + }) + + versionedPlugins = append(versionedPlugins, versioned...) } + + if len(versionedPlugins) != 0 { + data["detailed"] = versionedPlugins + } + return &logical.Response{ - Data: pluginsByType, + Data: data, }, nil } -func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { +func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) { pluginName := d.Get("name").(string) if pluginName == "" { return logical.ErrorResponse("missing plugin name"), nil @@ -436,6 +468,11 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, req *logi return nil, err } + pluginVersion, err := getVersion(d) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + sha256 := d.Get("sha256").(string) if sha256 == "" { sha256 = d.Get("sha_256").(string) @@ -472,7 +509,7 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, req *logi return logical.ErrorResponse("Could not decode SHA-256 value from Hex"), err } - err = b.Core.pluginCatalog.Set(ctx, pluginName, pluginType, parts[0], args, env, sha256Bytes) + err = b.Core.pluginCatalog.Set(ctx, pluginName, pluginType, pluginVersion, parts[0], args, env, sha256Bytes) if err != nil { return nil, err } @@ -480,7 +517,7 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, req *logi return nil, nil } -func (b *SystemBackend) handlePluginCatalogRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { +func (b *SystemBackend) handlePluginCatalogRead(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) { pluginName := d.Get("name").(string) if pluginName == "" { return logical.ErrorResponse("missing plugin name"), nil @@ -501,7 +538,12 @@ func (b *SystemBackend) handlePluginCatalogRead(ctx context.Context, req *logica return nil, err } - plugin, err := b.Core.pluginCatalog.Get(ctx, pluginName, pluginType) + pluginVersion, err := getVersion(d) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + plugin, err := b.Core.pluginCatalog.Get(ctx, pluginName, pluginType, pluginVersion) if err != nil { return nil, err } @@ -530,12 +572,17 @@ func (b *SystemBackend) handlePluginCatalogRead(ctx context.Context, req *logica }, nil } -func (b *SystemBackend) handlePluginCatalogDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { +func (b *SystemBackend) handlePluginCatalogDelete(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) { pluginName := d.Get("name").(string) if pluginName == "" { return logical.ErrorResponse("missing plugin name"), nil } + pluginVersion, err := getVersion(d) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + var resp *logical.Response pluginTypeStr := d.Get("type").(string) if pluginTypeStr == "" { @@ -552,13 +599,28 @@ func (b *SystemBackend) handlePluginCatalogDelete(ctx context.Context, req *logi if err != nil { return nil, err } - if err := b.Core.pluginCatalog.Delete(ctx, pluginName, pluginType); err != nil { + if err := b.Core.pluginCatalog.Delete(ctx, pluginName, pluginType, pluginVersion); err != nil { return nil, err } return resp, nil } +func getVersion(d *framework.FieldData) (string, error) { + version := d.Get("version").(string) + if version != "" { + semanticVersion, err := semver.NewSemver(version) + if err != nil { + return "", fmt.Errorf("version %q is not a valid semantic version: %w", version, err) + } + + // Canonicalize the version string. + version = semanticVersion.String() + } + + return version, nil +} + func (b *SystemBackend) handlePluginReloadUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { pluginName := d.Get("plugin").(string) pluginMounts := d.Get("mounts").([]string) @@ -5221,6 +5283,10 @@ plugin directory.`, Each entry is of the form "key=value".`, "", }, + "plugin-catalog_version": { + "The semantic version of the plugin to use.", + "", + }, "leases": { `View or list lease metadata.`, ` diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index f004d3dfa1c2e..9a66f83901d6e 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -766,6 +766,10 @@ func (b *SystemBackend) pluginsCatalogCRUDPath() *framework.Path { Type: framework.TypeStringSlice, Description: strings.TrimSpace(sysHelp["plugin-catalog_env"][0]), }, + "version": { + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["plugin-catalog_version"][0]), + }, }, Operations: map[logical.Operation]framework.OperationHandler{ diff --git a/vault/mount.go b/vault/mount.go index 06671ba99ae38..4ab0e5200c698 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -1419,7 +1419,7 @@ func (c *Core) newLogicalBackend(ctx context.Context, entry *MountEntry, sysView f, ok := c.logicalBackends[t] if !ok { - plug, err := c.pluginCatalog.Get(ctx, t, consts.PluginTypeSecrets) + plug, err := c.pluginCatalog.Get(ctx, t, consts.PluginTypeSecrets, "") if err != nil { return nil, err } diff --git a/vault/plugin_catalog.go b/vault/plugin_catalog.go index 9caaa2410f2f7..df87c7ffa645d 100644 --- a/vault/plugin_catalog.go +++ b/vault/plugin_catalog.go @@ -2,11 +2,13 @@ package vault import ( "context" + "encoding/hex" "encoding/json" "errors" "fmt" + "path" "path/filepath" - "sort" + "runtime/debug" "strings" "sync" @@ -14,6 +16,7 @@ import ( multierror "github.com/hashicorp/go-multierror" plugin "github.com/hashicorp/go-plugin" "github.com/hashicorp/go-secure-stdlib/base62" + semver "github.com/hashicorp/go-version" v4 "github.com/hashicorp/vault/sdk/database/dbplugin" v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/helper/consts" @@ -21,6 +24,7 @@ import ( "github.com/hashicorp/vault/sdk/helper/pluginutil" "github.com/hashicorp/vault/sdk/logical" backendplugin "github.com/hashicorp/vault/sdk/plugin" + "github.com/hashicorp/vault/sdk/version" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -50,6 +54,10 @@ type PluginCatalog struct { externalPlugins map[string]*externalPlugin mlockPlugins bool + // once is used to ensure we only parse build info once. + once sync.Once + buildInfo *debug.BuildInfo + lock sync.RWMutex } @@ -212,7 +220,7 @@ func (c *PluginCatalog) NewPluginClient(ctx context.Context, config pluginutil.P return nil, fmt.Errorf("no plugin type provided") } - pluginRunner, err := c.get(ctx, config.Name, config.PluginType) + pluginRunner, err := c.get(ctx, config.Name, config.PluginType, config.Version) if err != nil { return nil, fmt.Errorf("failed to lookup plugin: %w", err) } @@ -368,6 +376,7 @@ func (c *PluginCatalog) isDatabasePlugin(ctx context.Context, pluginRunner *plug Name: pluginRunner.Name, PluginSets: v5.PluginSets, PluginType: consts.PluginTypeDatabase, + Version: pluginRunner.Version, HandshakeConfig: v5.HandshakeConfig, Logger: log.NewNullLogger(), IsMetadataMode: true, @@ -447,8 +456,8 @@ func (c *PluginCatalog) UpgradePlugins(ctx context.Context, logger log.Logger) e cmdOld := plugin.Command plugin.Command = filepath.Join(c.directory, plugin.Command) - // Upgrade the storage. At this point we don't know what type of plugin this is so pass in the unkonwn type. - runner, err := c.setInternal(ctx, pluginName, consts.PluginTypeUnknown, cmdOld, plugin.Args, plugin.Env, plugin.Sha256) + // Upgrade the storage. At this point we don't know what type of plugin this is so pass in the unknown type. + runner, err := c.setInternal(ctx, pluginName, consts.PluginTypeUnknown, plugin.Version, cmdOld, plugin.Args, plugin.Env, plugin.Sha256) if err != nil { if errors.Is(err, ErrPluginBadType) { retErr = multierror.Append(retErr, fmt.Errorf("could not upgrade plugin %s: plugin of unknown type", pluginName)) @@ -473,22 +482,26 @@ func (c *PluginCatalog) UpgradePlugins(ctx context.Context, logger log.Logger) e // Get retrieves a plugin with the specified name from the catalog. It first // looks for external plugins with this name and then looks for builtin plugins. // It returns a PluginRunner or an error if no plugin was found. -func (c *PluginCatalog) Get(ctx context.Context, name string, pluginType consts.PluginType) (*pluginutil.PluginRunner, error) { +func (c *PluginCatalog) Get(ctx context.Context, name string, pluginType consts.PluginType, version string) (*pluginutil.PluginRunner, error) { c.lock.RLock() - runner, err := c.get(ctx, name, pluginType) + runner, err := c.get(ctx, name, pluginType, version) c.lock.RUnlock() return runner, err } -func (c *PluginCatalog) get(ctx context.Context, name string, pluginType consts.PluginType) (*pluginutil.PluginRunner, error) { +func (c *PluginCatalog) get(ctx context.Context, name string, pluginType consts.PluginType, version string) (*pluginutil.PluginRunner, error) { // If the directory isn't set only look for builtin plugins. if c.directory != "" { // Look for external plugins in the barrier - out, err := c.catalogView.Get(ctx, pluginType.String()+"/"+name) + storageKey := path.Join(pluginType.String(), name) + if version != "" { + storageKey = path.Join(storageKey, version) + } + out, err := c.catalogView.Get(ctx, storageKey) if err != nil { return nil, fmt.Errorf("failed to retrieve plugin %q: %w", name, err) } - if out == nil { + if out == nil && version == "" { // Also look for external plugins under what their name would have been if they // were registered before plugin types existed. out, err = c.catalogView.Get(ctx, name) @@ -511,14 +524,17 @@ func (c *PluginCatalog) get(ctx context.Context, name string, pluginType consts. return entry, nil } } - // Look for builtin plugins - if factory, ok := c.builtinRegistry.Get(name, pluginType); ok { - return &pluginutil.PluginRunner{ - Name: name, - Type: pluginType, - Builtin: true, - BuiltinFactory: factory, - }, nil + + if version == "" { + // Look for builtin plugins + if factory, ok := c.builtinRegistry.Get(name, pluginType); ok { + return &pluginutil.PluginRunner{ + Name: name, + Type: pluginType, + Builtin: true, + BuiltinFactory: factory, + }, nil + } } return nil, nil @@ -526,7 +542,7 @@ func (c *PluginCatalog) get(ctx context.Context, name string, pluginType consts. // Set registers a new external plugin with the catalog, or updates an existing // external plugin. It takes the name, command and SHA256 of the plugin. -func (c *PluginCatalog) Set(ctx context.Context, name string, pluginType consts.PluginType, command string, args []string, env []string, sha256 []byte) error { +func (c *PluginCatalog) Set(ctx context.Context, name string, pluginType consts.PluginType, version string, command string, args []string, env []string, sha256 []byte) error { if c.directory == "" { return ErrDirectoryNotConfigured } @@ -541,11 +557,11 @@ func (c *PluginCatalog) Set(ctx context.Context, name string, pluginType consts. c.lock.Lock() defer c.lock.Unlock() - _, err := c.setInternal(ctx, name, pluginType, command, args, env, sha256) + _, err := c.setInternal(ctx, name, pluginType, version, command, args, env, sha256) return err } -func (c *PluginCatalog) setInternal(ctx context.Context, name string, pluginType consts.PluginType, command string, args []string, env []string, sha256 []byte) (*pluginutil.PluginRunner, error) { +func (c *PluginCatalog) setInternal(ctx context.Context, name string, pluginType consts.PluginType, version string, command string, args []string, env []string, sha256 []byte) (*pluginutil.PluginRunner, error) { // Best effort check to make sure the command isn't breaking out of the // configured plugin directory. commandFull := filepath.Join(c.directory, command) @@ -587,6 +603,7 @@ func (c *PluginCatalog) setInternal(ctx context.Context, name string, pluginType entry := &pluginutil.PluginRunner{ Name: name, Type: pluginType, + Version: version, Command: command, Args: args, Env: env, @@ -599,8 +616,12 @@ func (c *PluginCatalog) setInternal(ctx context.Context, name string, pluginType return nil, fmt.Errorf("failed to encode plugin entry: %w", err) } + storageKey := path.Join(pluginType.String(), name) + if version != "" { + storageKey = path.Join(storageKey, version) + } logicalEntry := logical.StorageEntry{ - Key: pluginType.String() + "/" + name, + Key: storageKey, Value: buf, } if err := c.catalogView.Put(ctx, &logicalEntry); err != nil { @@ -611,12 +632,15 @@ func (c *PluginCatalog) setInternal(ctx context.Context, name string, pluginType // Delete is used to remove an external plugin from the catalog. Builtin plugins // can not be deleted. -func (c *PluginCatalog) Delete(ctx context.Context, name string, pluginType consts.PluginType) error { +func (c *PluginCatalog) Delete(ctx context.Context, name string, pluginType consts.PluginType, pluginVersion string) error { c.lock.Lock() defer c.lock.Unlock() // Check the name under which the plugin exists, but if it's unfound, don't return any error. - pluginKey := pluginType.String() + "/" + name + pluginKey := path.Join(pluginType.String(), name) + if pluginVersion != "" { + pluginKey = path.Join(pluginKey, pluginVersion) + } out, err := c.catalogView.Get(ctx, pluginKey) if err != nil || out == nil { pluginKey = name @@ -628,49 +652,159 @@ func (c *PluginCatalog) Delete(ctx context.Context, name string, pluginType cons // 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) { + 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) { + return c.listInternal(ctx, pluginType, true) +} + +func (c *PluginCatalog) listInternal(ctx context.Context, pluginType consts.PluginType, includeVersioned bool) ([]pluginutil.VersionedPlugin, error) { c.lock.RLock() defer c.lock.RUnlock() + var result []pluginutil.VersionedPlugin + // Collect keys for external plugins in the barrier. - keys, err := logical.CollectKeys(ctx, c.catalogView) + plugins, err := logical.CollectKeys(ctx, c.catalogView) if err != nil { return nil, err } - // Get the builtin plugins. - builtinKeys := c.builtinRegistry.Keys(pluginType) - - // Use a map to unique the two lists. - mapKeys := make(map[string]bool) + unversionedPlugins := make(map[string]struct{}) + for _, plugin := range plugins { + // Some keys will be prepended with the plugin type, but other ones won't. + // Users don't expect to see the plugin type, so we need to strip that here. + var normalizedName, version string + var semanticVersion *semver.Version + parts := strings.Split(plugin, "/") + + switch len(parts) { + case 1: // Unversioned, no type (legacy) + normalizedName = parts[0] + case 2: // Unversioned + if isPluginType(parts[0]) { + normalizedName = parts[1] + } else { + return nil, fmt.Errorf("unknown plugin type in plugin catalog: %s", plugin) + } + case 3: // Versioned, with type + if !includeVersioned { + continue + } - pluginTypePrefix := pluginType.String() + "/" + normalizedName, version = parts[1], parts[2] + semanticVersion, err = semver.NewVersion(version) + if err != nil { + return nil, fmt.Errorf("unexpected error parsing version from plugin catalog entry %q: %w", plugin, err) + } + default: + return nil, fmt.Errorf("unexpected entry in plugin catalog: %s", plugin) + } - for _, plugin := range keys { // Only list user-added plugins if they're of the given type. - if entry, err := c.get(ctx, plugin, pluginType); err == nil && entry != nil { - - // Some keys will be prepended with the plugin type, but other ones won't. - // Users don't expect to see the plugin type, so we need to strip that here. - idx := strings.Index(plugin, pluginTypePrefix) - if idx == 0 { - plugin = plugin[len(pluginTypePrefix):] + if entry, err := c.get(ctx, normalizedName, pluginType, version); err == nil && entry != nil { + result = append(result, pluginutil.VersionedPlugin{ + Name: normalizedName, + Type: pluginType.String(), + Version: version, + SHA256: hex.EncodeToString(entry.Sha256), + SemanticVersion: semanticVersion, + }) + + if version == "" { + unversionedPlugins[normalizedName] = struct{}{} } - mapKeys[plugin] = true } } - for _, plugin := range builtinKeys { - mapKeys[plugin] = true + // Get the builtin plugins. + builtinPlugins := c.builtinRegistry.Keys(pluginType) + for _, plugin := range builtinPlugins { + // Unversioned plugins fully replace builtins of the same name. + if _, ok := unversionedPlugins[plugin]; ok { + continue + } + + version := c.getBuiltinVersion(pluginType, plugin) + semanticVersion, err := semver.NewVersion(version) + if err != nil { + return nil, err + } + result = append(result, pluginutil.VersionedPlugin{ + Name: plugin, + Type: pluginType.String(), + Version: version, + Builtin: true, + SemanticVersion: semanticVersion, + }) } - retList := make([]string, len(mapKeys)) - i := 0 - for k := range mapKeys { - retList[i] = k - i++ + return result, nil +} + +func isPluginType(s string) bool { + for _, t := range consts.PluginTypes { + if s == t.String() { + return true + } } - // sort for consistent ordering of builtin plugins - sort.Strings(retList) - return retList, nil + return false +} + +func (c *PluginCatalog) getBuiltinVersion(pluginType consts.PluginType, pluginName string) string { + defaultBuiltinVersion := "v" + version.GetVersion().Version + "+builtin.vault" + + c.once.Do(func() { + c.buildInfo, _ = debug.ReadBuildInfo() + }) + + // Should never happen, means the binary was built without Go modules. + // Fall back to just the Vault version. + if c.buildInfo == nil { + return defaultBuiltinVersion + } + + // Vault builtin plugins are all either: + // a) An external repo within the hashicorp org - return external repo version with +builtin + // b) Within the Vault repo itself - return Vault version with +builtin.vault + // + // The repo names are predictable, but follow slightly different patterns + // for each plugin type. + t := pluginType.String() + switch pluginType { + case consts.PluginTypeDatabase: + // Database plugin built-ins are registered as e.g. "postgresql-database-plugin" + pluginName = strings.TrimSuffix(pluginName, "-database-plugin") + case consts.PluginTypeSecrets: + // Repos use "secrets", pluginType.String() is "secret". + t = "secrets" + } + pluginModulePath := fmt.Sprintf("github.com/hashicorp/vault-plugin-%s-%s", t, pluginName) + + for _, dep := range c.buildInfo.Deps { + if dep.Path == pluginModulePath { + return dep.Version + "+builtin" + } + } + + return defaultBuiltinVersion } diff --git a/vault/plugin_catalog_test.go b/vault/plugin_catalog_test.go index 00925a87c6d0f..90b90d5c99ae8 100644 --- a/vault/plugin_catalog_test.go +++ b/vault/plugin_catalog_test.go @@ -21,15 +21,14 @@ import ( func TestPluginCatalog_CRUD(t *testing.T) { core, _, _ := TestCoreUnsealed(t) - - sym, err := filepath.EvalSymlinks(os.TempDir()) + tempDir, err := filepath.EvalSymlinks(t.TempDir()) if err != nil { - t.Fatalf("error: %v", err) + t.Fatal(err) } - core.pluginCatalog.directory = sym + core.pluginCatalog.directory = tempDir // Get builtin plugin - p, err := core.pluginCatalog.Get(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase) + p, err := core.pluginCatalog.Get(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "") if err != nil { t.Fatalf("unexpected error %v", err) } @@ -51,20 +50,20 @@ func TestPluginCatalog_CRUD(t *testing.T) { } // Set a plugin, test overwriting a builtin plugin - file, err := ioutil.TempFile(os.TempDir(), "temp") + file, err := ioutil.TempFile(tempDir, "temp") if err != nil { t.Fatal(err) } defer file.Close() command := fmt.Sprintf("%s", filepath.Base(file.Name())) - err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, command, []string{"--test"}, []string{"FOO=BAR"}, []byte{'1'}) + err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "", command, []string{"--test"}, []string{"FOO=BAR"}, []byte{'1'}) if err != nil { t.Fatal(err) } // Get the plugin - p, err = core.pluginCatalog.Get(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase) + p, err = core.pluginCatalog.Get(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "") if err != nil { t.Fatalf("unexpected error %v", err) } @@ -72,7 +71,7 @@ func TestPluginCatalog_CRUD(t *testing.T) { expected := &pluginutil.PluginRunner{ Name: "mysql-database-plugin", Type: consts.PluginTypeDatabase, - Command: filepath.Join(sym, filepath.Base(file.Name())), + Command: filepath.Join(tempDir, filepath.Base(file.Name())), Args: []string{"--test"}, Env: []string{"FOO=BAR"}, Sha256: []byte{'1'}, @@ -84,13 +83,13 @@ func TestPluginCatalog_CRUD(t *testing.T) { } // Delete the plugin - err = core.pluginCatalog.Delete(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase) + err = core.pluginCatalog.Delete(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "") if err != nil { t.Fatalf("unexpected err: %v", err) } // Get builtin plugin - p, err = core.pluginCatalog.Get(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase) + p, err = core.pluginCatalog.Get(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "") if err != nil { t.Fatalf("unexpected error %v", err) } @@ -112,14 +111,72 @@ func TestPluginCatalog_CRUD(t *testing.T) { } } -func TestPluginCatalog_List(t *testing.T) { +func TestPluginCatalog_VersionedCRUD(t *testing.T) { core, _, _ := TestCoreUnsealed(t) + tempDir, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + core.pluginCatalog.directory = tempDir + + // Set a versioned plugin. + file, err := ioutil.TempFile(tempDir, "temp") + if err != nil { + t.Fatal(err) + } + defer file.Close() - sym, err := filepath.EvalSymlinks(os.TempDir()) + const version = "1.0.0" + command := fmt.Sprintf("%s", filepath.Base(file.Name())) + err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, version, command, []string{"--test"}, []string{"FOO=BAR"}, []byte{'1'}) if err != nil { - t.Fatalf("error: %v", err) + t.Fatal(err) + } + + // Get the plugin + plugin, err := core.pluginCatalog.Get(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, version) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + expected := &pluginutil.PluginRunner{ + Name: "mysql-database-plugin", + Type: consts.PluginTypeDatabase, + Version: version, + Command: filepath.Join(tempDir, filepath.Base(file.Name())), + Args: []string{"--test"}, + Env: []string{"FOO=BAR"}, + Sha256: []byte{'1'}, + Builtin: false, } - core.pluginCatalog.directory = sym + + if !reflect.DeepEqual(plugin, expected) { + t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", plugin, expected) + } + + // Delete the plugin + err = core.pluginCatalog.Delete(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, version) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + // Get plugin - should fail + plugin, err = core.pluginCatalog.Get(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, version) + if err != nil { + t.Fatal(err) + } + if plugin != nil { + t.Fatalf("expected no plugin with this version to be in the catalog, but found %+v", plugin) + } +} + +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) @@ -130,32 +187,31 @@ func TestPluginCatalog_List(t *testing.T) { 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)) } - for i, p := range builtinKeys { - if !reflect.DeepEqual(plugins[i], p) { - t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", plugins[i], p) - } + 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 := ioutil.TempFile(os.TempDir(), "temp") + file, err := ioutil.TempFile(tempDir, "temp") if err != nil { t.Fatal(err) } defer file.Close() command := filepath.Base(file.Name()) - err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, command, []string{"--test"}, []string{}, []byte{'1'}) + err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "", command, []string{"--test"}, []string{}, []byte{'1'}) if err != nil { t.Fatal(err) } // Set another plugin - err = core.pluginCatalog.Set(context.Background(), "aaaaaaa", consts.PluginTypeDatabase, command, []string{"--test"}, []string{}, []byte{'1'}) + err = core.pluginCatalog.Set(context.Background(), "aaaaaaa", consts.PluginTypeDatabase, "", command, []string{"--test"}, []string{}, []byte{'1'}) if err != nil { t.Fatal(err) } @@ -165,6 +221,7 @@ func TestPluginCatalog_List(t *testing.T) { 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 { @@ -177,21 +234,111 @@ func TestPluginCatalog_List(t *testing.T) { } // verify the builtin plugins are correct - for i, p := range builtinKeys { - if !reflect.DeepEqual(plugins[i+1], p) { - t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", plugins[i+1], p) + 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()) + 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.ListVersionedPlugins(context.Background(), consts.PluginTypeDatabase) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + sort.SliceStable(plugins, func(i, j int) bool { + return plugins[i].Name < plugins[j].Name + }) + + if len(plugins) != len(builtinKeys) { + t.Fatalf("unexpected length of plugin list, expected %d, got %d", len(builtinKeys), len(plugins)) + } + + for i, plugin := range plugins { + if plugin.Name != builtinKeys[i] { + t.Fatalf("expected plugin list with names %v but got %+v", builtinKeys, plugins) + } + } + + // Set a plugin, test overwriting a builtin plugin + file, err := ioutil.TempFile(tempDir, "temp") + if err != nil { + t.Fatal(err) + } + defer file.Close() + + command := filepath.Base(file.Name()) + err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "", command, []string{"--test"}, []string{}, []byte{'1'}) + if err != nil { + t.Fatal(err) + } + + // Set another plugin + err = core.pluginCatalog.Set(context.Background(), "aaaaaaa", consts.PluginTypeDatabase, "", command, []string{"--test"}, []string{}, []byte{'1'}) + if err != nil { + t.Fatal(err) + } + + // List the plugins + plugins, err = core.pluginCatalog.ListVersionedPlugins(context.Background(), consts.PluginTypeDatabase) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + sort.SliceStable(plugins, func(i, j int) bool { + return plugins[i].Name < plugins[j].Name + }) + + // 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].Name, "aaaaaaa") { + t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", plugins[0], "aaaaaaa") + } + + // verify the builtin plugins are correct + for i, plugin := range plugins[1:] { + if plugin.Name != builtinKeys[i] { + t.Fatalf("expected plugin list with names %v but got %+v", builtinKeys, plugins) + } + switch plugin.Name { + case "mysql-database-plugin": + if plugin.Builtin { + t.Fatalf("expected %v plugin to be an unversioned external plugin", plugin) + } + if plugin.Version != "" { + t.Fatalf("expected no version information for %v but got %s", plugin, plugin.Version) + } + default: + if !plugin.Builtin { + t.Fatalf("expected %v plugin to be builtin", plugin) + } + if plugin.SemanticVersion.Metadata() != "builtin" && plugin.SemanticVersion.Metadata() != "builtin.vault" { + t.Fatalf("expected +builtin metadata but got %s", plugin.Version) + } } } } func TestPluginCatalog_NewPluginClient(t *testing.T) { core, _, _ := TestCoreUnsealed(t) - - sym, err := filepath.EvalSymlinks(os.TempDir()) + tempDir, err := filepath.EvalSymlinks(t.TempDir()) if err != nil { - t.Fatalf("error: %v", err) + t.Fatal(err) } - core.pluginCatalog.directory = sym + core.pluginCatalog.directory = tempDir if extPlugins := len(core.pluginCatalog.externalPlugins); extPlugins != 0 { t.Fatalf("expected externalPlugins map to be of len 0 but got %d", extPlugins) @@ -259,7 +406,7 @@ func TestPluginCatalog_PluginMain_Postgres(t *testing.T) { v5.Serve(dbType.(v5.Database)) } -func TestPluginCatalog_PluginMain_PostgresMultiplexed(t *testing.T) { +func TestPluginCatalog_PluginMain_PostgresMultiplexed(_ *testing.T) { if os.Getenv(pluginutil.PluginVaultVersionEnv) == "" { return } diff --git a/vault/testing.go b/vault/testing.go index 7a9246090a5c4..ee4a047b704b3 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -556,7 +556,8 @@ func TestAddTestPlugin(t testing.T, c *Core, name string, pluginType consts.Plug c.pluginCatalog.directory = fullPath args := []string{fmt.Sprintf("--test.run=%s", testFunc)} - err = c.pluginCatalog.Set(context.Background(), name, pluginType, fileName, args, env, sum) + version := "" + err = c.pluginCatalog.Set(context.Background(), name, pluginType, version, fileName, args, env, sum) if err != nil { t.Fatal(err) } From 093a9434f64a8218b8e3808d0b4d5739eba6b017 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Mon, 15 Aug 2022 14:26:07 +0100 Subject: [PATCH 2/9] Less function should default to false Co-authored-by: Christopher Swenson --- vault/logical_system.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vault/logical_system.go b/vault/logical_system.go index 672e4070b62da..9cbadaa100d12 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -436,7 +436,7 @@ func (b *SystemBackend) handlePluginCatalogUntypedList(ctx context.Context, _ *l return right.SemanticVersion.GreaterThan(left.SemanticVersion) } - return true + return false }) versionedPlugins = append(versionedPlugins, versioned...) From 360056d6f08782a5796c18281525c7fe8b909de8 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Mon, 15 Aug 2022 15:28:58 +0100 Subject: [PATCH 3/9] Simplify isPluginType function, add sorting test, changelog PR number --- changelog/{xyz.txt => 16688.txt} | 0 sdk/helper/pluginutil/runner.go | 2 +- vault/logical_system.go | 32 +++++++++------- vault/logical_system_test.go | 63 ++++++++++++++++++++++++++++++++ vault/plugin_catalog.go | 9 +---- 5 files changed, 84 insertions(+), 22 deletions(-) rename changelog/{xyz.txt => 16688.txt} (100%) diff --git a/changelog/xyz.txt b/changelog/16688.txt similarity index 100% rename from changelog/xyz.txt rename to changelog/16688.txt diff --git a/sdk/helper/pluginutil/runner.go b/sdk/helper/pluginutil/runner.go index 762e4673c5c35..b18951e3777ad 100644 --- a/sdk/helper/pluginutil/runner.go +++ b/sdk/helper/pluginutil/runner.go @@ -86,8 +86,8 @@ func (r *PluginRunner) RunMetadataMode(ctx context.Context, wrapper RunnerUtil, // VersionedPlugin holds any versioning information stored about a plugin in the // plugin catalog. type VersionedPlugin struct { + Type string `json:"type"` // string instead of consts.PluginType so that we get the string form in API responses. Name string `json:"name"` - Type string `json:"type"` Version string `json:"version"` SHA256 string `json:"sha256,omitempty"` Builtin bool `json:"builtin"` diff --git a/vault/logical_system.go b/vault/logical_system.go index 9cbadaa100d12..9ae4395373208 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -424,20 +424,7 @@ func (b *SystemBackend) handlePluginCatalogUntypedList(ctx context.Context, _ *l } // Sort for consistent ordering - sort.SliceStable(versionedPlugins, func(i, j int) bool { - left, right := versionedPlugins[i], versionedPlugins[j] - if left.Type != right.Type { - return left.Type < right.Type - } - if left.Name != right.Name { - return left.Name < right.Name - } - if left.Version != right.Version { - return right.SemanticVersion.GreaterThan(left.SemanticVersion) - } - - return false - }) + sortVersionedPlugins(versionedPlugins) versionedPlugins = append(versionedPlugins, versioned...) } @@ -451,6 +438,23 @@ func (b *SystemBackend) handlePluginCatalogUntypedList(ctx context.Context, _ *l }, nil } +func sortVersionedPlugins(versionedPlugins []pluginutil.VersionedPlugin) { + sort.SliceStable(versionedPlugins, func(i, j int) bool { + left, right := versionedPlugins[i], versionedPlugins[j] + if left.Type != right.Type { + return left.Type < right.Type + } + if left.Name != right.Name { + return left.Name < right.Name + } + if left.Version != right.Version { + return right.SemanticVersion.GreaterThan(left.SemanticVersion) + } + + return false + }) +} + func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) { pluginName := d.Get("name").(string) if pluginName == "" { diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index c41a4a7650a58..c81438e3fae85 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -18,6 +18,7 @@ import ( "github.com/fatih/structs" "github.com/go-test/deep" hclog "github.com/hashicorp/go-hclog" + semver "github.com/hashicorp/go-version" "github.com/hashicorp/vault/audit" credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" "github.com/hashicorp/vault/helper/builtinplugins" @@ -28,6 +29,7 @@ import ( "github.com/hashicorp/vault/sdk/helper/compressutil" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/jsonutil" + "github.com/hashicorp/vault/sdk/helper/pluginutil" "github.com/hashicorp/vault/sdk/helper/salt" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/version" @@ -4878,3 +4880,64 @@ func TestSystemBackend_LoggersByName(t *testing.T) { }) } } + +func TestSortVersionedPlugins(t *testing.T) { + versionedPlugin := func(typ consts.PluginType, name string, version string) pluginutil.VersionedPlugin { + return pluginutil.VersionedPlugin{ + Type: typ.String(), + Name: name, + Version: version, + SHA256: "", + Builtin: false, + SemanticVersion: semver.Must(semver.NewVersion(version)), + } + } + + differingTypes := []pluginutil.VersionedPlugin{ + versionedPlugin(consts.PluginTypeSecrets, "c", "1.0.0"), + versionedPlugin(consts.PluginTypeDatabase, "c", "1.0.0"), + versionedPlugin(consts.PluginTypeCredential, "c", "1.0.0"), + } + differingNames := []pluginutil.VersionedPlugin{ + versionedPlugin(consts.PluginTypeCredential, "c", "1.0.0"), + versionedPlugin(consts.PluginTypeCredential, "b", "1.0.0"), + versionedPlugin(consts.PluginTypeCredential, "a", "1.0.0"), + } + differingVersions := []pluginutil.VersionedPlugin{ + versionedPlugin(consts.PluginTypeCredential, "c", "10.0.0"), + versionedPlugin(consts.PluginTypeCredential, "c", "2.0.1"), + versionedPlugin(consts.PluginTypeCredential, "c", "2.1.0"), + } + + for name, tc := range map[string][]pluginutil.VersionedPlugin{ + "ascending types": differingTypes, + "ascending names": differingNames, + "ascending versions": differingVersions, + // Include differing versions twice so we can test out equality too. + "all types": append(differingTypes, + append(differingNames, + append(differingVersions, differingVersions...)...)...), + } { + t.Run(name, func(t *testing.T) { + sortVersionedPlugins(tc) + for i := 1; i < len(tc); i++ { + previous := tc[i-1] + current := tc[i] + if current.Type > previous.Type { + continue + } + if current.Name > previous.Name { + continue + } + if current.SemanticVersion.GreaterThan(previous.SemanticVersion) { + continue + } + if current.Type == previous.Type && current.Name == previous.Name && current.SemanticVersion.Equal(previous.SemanticVersion) { + continue + } + + t.Fatalf("versioned plugins at index %d and %d were not properly sorted: %+v, %+v", i-1, i, previous, current) + } + }) + } +} diff --git a/vault/plugin_catalog.go b/vault/plugin_catalog.go index df87c7ffa645d..5d4cda443ecd6 100644 --- a/vault/plugin_catalog.go +++ b/vault/plugin_catalog.go @@ -761,13 +761,8 @@ func (c *PluginCatalog) listInternal(ctx context.Context, pluginType consts.Plug } func isPluginType(s string) bool { - for _, t := range consts.PluginTypes { - if s == t.String() { - return true - } - } - - return false + _, err := consts.ParsePluginType(s) + return err == nil } func (c *PluginCatalog) getBuiltinVersion(pluginType consts.PluginType, pluginName string) string { From 7e0106ed26ba4427a9c8aea46a1f7149a6a9616b Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Thu, 25 Aug 2022 10:54:26 +0100 Subject: [PATCH 4/9] Ensure SemanticVersion field is always non-nil to avoid panics, fix changelog --- changelog/16688.txt | 4 ++-- vault/logical_system_test.go | 46 +++++++++++++++++++++++------------- vault/plugin_catalog.go | 10 ++++++++ vault/plugin_catalog_test.go | 39 +++++++++++++++++++++++------- 4 files changed, 71 insertions(+), 28 deletions(-) diff --git a/changelog/16688.txt b/changelog/16688.txt index fc83c7c916dab..edc92dc72e8d1 100644 --- a/changelog/16688.txt +++ b/changelog/16688.txt @@ -1,6 +1,6 @@ -```change +```release-note:change plugins: `GET /sys/plugins/catalog` endpoint now returns an additional `detailed` field in the response with additional plugin metadata. ``` -```improvement +```release-note:improvement plugins: Plugin catalog supports registering plugins with a semantic version ``` diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index c81438e3fae85..a0ec34ed5d7a5 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -4882,31 +4882,42 @@ func TestSystemBackend_LoggersByName(t *testing.T) { } func TestSortVersionedPlugins(t *testing.T) { - versionedPlugin := func(typ consts.PluginType, name string, version string) pluginutil.VersionedPlugin { + versionedPlugin := func(typ consts.PluginType, name string, version string, builtin bool) pluginutil.VersionedPlugin { return pluginutil.VersionedPlugin{ - Type: typ.String(), - Name: name, - Version: version, - SHA256: "", - Builtin: false, - SemanticVersion: semver.Must(semver.NewVersion(version)), + Type: typ.String(), + Name: name, + Version: version, + SHA256: "", + Builtin: builtin, + SemanticVersion: func() *semver.Version { + if version != "" { + return semver.Must(semver.NewVersion(version)) + } + + return semver.Must(semver.NewVersion("0.0.0")) + }(), } } differingTypes := []pluginutil.VersionedPlugin{ - versionedPlugin(consts.PluginTypeSecrets, "c", "1.0.0"), - versionedPlugin(consts.PluginTypeDatabase, "c", "1.0.0"), - versionedPlugin(consts.PluginTypeCredential, "c", "1.0.0"), + versionedPlugin(consts.PluginTypeSecrets, "c", "1.0.0", false), + versionedPlugin(consts.PluginTypeDatabase, "c", "1.0.0", false), + versionedPlugin(consts.PluginTypeCredential, "c", "1.0.0", false), } differingNames := []pluginutil.VersionedPlugin{ - versionedPlugin(consts.PluginTypeCredential, "c", "1.0.0"), - versionedPlugin(consts.PluginTypeCredential, "b", "1.0.0"), - versionedPlugin(consts.PluginTypeCredential, "a", "1.0.0"), + versionedPlugin(consts.PluginTypeCredential, "c", "1.0.0", false), + versionedPlugin(consts.PluginTypeCredential, "b", "1.0.0", false), + versionedPlugin(consts.PluginTypeCredential, "a", "1.0.0", false), } differingVersions := []pluginutil.VersionedPlugin{ - versionedPlugin(consts.PluginTypeCredential, "c", "10.0.0"), - versionedPlugin(consts.PluginTypeCredential, "c", "2.0.1"), - versionedPlugin(consts.PluginTypeCredential, "c", "2.1.0"), + versionedPlugin(consts.PluginTypeCredential, "c", "10.0.0", false), + versionedPlugin(consts.PluginTypeCredential, "c", "2.0.1", false), + versionedPlugin(consts.PluginTypeCredential, "c", "2.1.0", false), + } + versionedUnversionedAndBuiltin := []pluginutil.VersionedPlugin{ + versionedPlugin(consts.PluginTypeCredential, "c", "1.0.0", false), + versionedPlugin(consts.PluginTypeCredential, "c", "", false), + versionedPlugin(consts.PluginTypeCredential, "c", "1.0.0", true), } for name, tc := range map[string][]pluginutil.VersionedPlugin{ @@ -4914,9 +4925,10 @@ func TestSortVersionedPlugins(t *testing.T) { "ascending names": differingNames, "ascending versions": differingVersions, // Include differing versions twice so we can test out equality too. - "all types": append(differingTypes, + "differing types, names and versions": append(differingTypes, append(differingNames, append(differingVersions, differingVersions...)...)...), + "mix of unversioned, versioned, and builtin": versionedUnversionedAndBuiltin, } { t.Run(name, func(t *testing.T) { sortVersionedPlugins(tc) diff --git a/vault/plugin_catalog.go b/vault/plugin_catalog.go index 5d4cda443ecd6..aaeabd2510bc4 100644 --- a/vault/plugin_catalog.go +++ b/vault/plugin_catalog.go @@ -699,9 +699,19 @@ func (c *PluginCatalog) listInternal(ctx context.Context, pluginType consts.Plug switch len(parts) { case 1: // Unversioned, no type (legacy) normalizedName = parts[0] + // Use 0.0.0 to ensure unversioned is sorted as the oldest version. + semanticVersion, err = semver.NewVersion("0.0.0") + if err != nil { + return nil, err + } case 2: // Unversioned if isPluginType(parts[0]) { normalizedName = parts[1] + // Use 0.0.0 to ensure unversioned is sorted as the oldest version. + semanticVersion, err = semver.NewVersion("0.0.0") + if err != nil { + return nil, err + } } else { return nil, fmt.Errorf("unknown plugin type in plugin catalog: %s", plugin) } diff --git a/vault/plugin_catalog_test.go b/vault/plugin_catalog_test.go index 90b90d5c99ae8..bf4c5b97605ac 100644 --- a/vault/plugin_catalog_test.go +++ b/vault/plugin_catalog_test.go @@ -256,9 +256,7 @@ func TestPluginCatalog_ListVersionedPlugins(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } - sort.SliceStable(plugins, func(i, j int) bool { - return plugins[i].Name < plugins[j].Name - }) + sortVersionedPlugins(plugins) if len(plugins) != len(builtinKeys) { t.Fatalf("unexpected length of plugin list, expected %d, got %d", len(builtinKeys), len(plugins)) @@ -278,13 +276,31 @@ func TestPluginCatalog_ListVersionedPlugins(t *testing.T) { defer file.Close() command := filepath.Base(file.Name()) - err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "", command, []string{"--test"}, []string{}, []byte{'1'}) + err = core.pluginCatalog.Set( + context.Background(), + "mysql-database-plugin", + consts.PluginTypeDatabase, + "", + command, + []string{"--test"}, + []string{}, + []byte{'1'}, + ) if err != nil { t.Fatal(err) } - // Set another plugin - err = core.pluginCatalog.Set(context.Background(), "aaaaaaa", consts.PluginTypeDatabase, "", command, []string{"--test"}, []string{}, []byte{'1'}) + // Set another plugin, with version information + err = core.pluginCatalog.Set( + context.Background(), + "aaaaaaa", + consts.PluginTypeDatabase, + "1.1.0", + command, + []string{"--test"}, + []string{}, + []byte{'1'}, + ) if err != nil { t.Fatal(err) } @@ -294,9 +310,7 @@ func TestPluginCatalog_ListVersionedPlugins(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } - sort.SliceStable(plugins, func(i, j int) bool { - return plugins[i].Name < plugins[j].Name - }) + sortVersionedPlugins(plugins) // plugins has a test-added plugin called "aaaaaaa" that is not built in if len(plugins) != len(builtinKeys)+1 { @@ -307,6 +321,9 @@ func TestPluginCatalog_ListVersionedPlugins(t *testing.T) { if !reflect.DeepEqual(plugins[0].Name, "aaaaaaa") { t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", plugins[0], "aaaaaaa") } + if plugins[0].SemanticVersion == nil { + t.Fatalf("expected non-nil semantic version for %v", plugins[0].Name) + } // verify the builtin plugins are correct for i, plugin := range plugins[1:] { @@ -329,6 +346,10 @@ func TestPluginCatalog_ListVersionedPlugins(t *testing.T) { t.Fatalf("expected +builtin metadata but got %s", plugin.Version) } } + + if plugin.SemanticVersion == nil { + t.Fatalf("expected non-nil semantic version for %v", plugin) + } } } From 403192c49578b31aaa00bb6813e711283cb909b8 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Thu, 25 Aug 2022 12:04:30 +0100 Subject: [PATCH 5/9] Longer timeout for mongodb test container --- helper/testhelpers/docker/testhelpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper/testhelpers/docker/testhelpers.go b/helper/testhelpers/docker/testhelpers.go index c841ecff6e16f..ff8df887273c4 100644 --- a/helper/testhelpers/docker/testhelpers.go +++ b/helper/testhelpers/docker/testhelpers.go @@ -164,7 +164,7 @@ func (d *Runner) StartService(ctx context.Context, connect ServiceAdapter) (*Ser bo := backoff.NewExponentialBackOff() bo.MaxInterval = time.Second * 5 - bo.MaxElapsedTime = 2 * time.Minute + bo.MaxElapsedTime = 5 * time.Minute pieces := strings.Split(hostIPs[0], ":") portInt, err := strconv.Atoi(pieces[1]) From 2f6cbefa949ab3d55436ec3faa600e9aa1000dcc Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Thu, 25 Aug 2022 12:25:10 +0100 Subject: [PATCH 6/9] Pin mongo test container to previous latest instead --- builtin/logical/database/rotation_test.go | 2 +- builtin/logical/mongodb/backend_test.go | 6 +++--- helper/testhelpers/docker/testhelpers.go | 2 +- plugins/database/mongodb/mongodb_test.go | 12 ++++++------ 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/builtin/logical/database/rotation_test.go b/builtin/logical/database/rotation_test.go index 9f6e65f9b79ef..1fdbba55f1d6d 100644 --- a/builtin/logical/database/rotation_test.go +++ b/builtin/logical/database/rotation_test.go @@ -708,7 +708,7 @@ func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) { } func TestBackend_StaticRole_Rotations_MongoDB(t *testing.T) { - cleanup, connURL := mongodb.PrepareTestContainerWithDatabase(t, "latest", "vaulttestdb") + cleanup, connURL := mongodb.PrepareTestContainerWithDatabase(t, "5.0.10", "vaulttestdb") defer cleanup() uc := userCreator(func(t *testing.T, username, password string) { diff --git a/builtin/logical/mongodb/backend_test.go b/builtin/logical/mongodb/backend_test.go index 1b85fef03c454..43cee7de981ca 100644 --- a/builtin/logical/mongodb/backend_test.go +++ b/builtin/logical/mongodb/backend_test.go @@ -57,7 +57,7 @@ func TestBackend_basic(t *testing.T) { t.Fatal(err) } - cleanup, connURI := mongodb.PrepareTestContainer(t, "latest") + cleanup, connURI := mongodb.PrepareTestContainer(t, "5.0.10") defer cleanup() connData := map[string]interface{}{ "uri": connURI, @@ -81,7 +81,7 @@ func TestBackend_roleCrud(t *testing.T) { t.Fatal(err) } - cleanup, connURI := mongodb.PrepareTestContainer(t, "latest") + cleanup, connURI := mongodb.PrepareTestContainer(t, "5.0.10") defer cleanup() connData := map[string]interface{}{ "uri": connURI, @@ -107,7 +107,7 @@ func TestBackend_leaseWriteRead(t *testing.T) { t.Fatal(err) } - cleanup, connURI := mongodb.PrepareTestContainer(t, "latest") + cleanup, connURI := mongodb.PrepareTestContainer(t, "5.0.10") defer cleanup() connData := map[string]interface{}{ "uri": connURI, diff --git a/helper/testhelpers/docker/testhelpers.go b/helper/testhelpers/docker/testhelpers.go index ff8df887273c4..c841ecff6e16f 100644 --- a/helper/testhelpers/docker/testhelpers.go +++ b/helper/testhelpers/docker/testhelpers.go @@ -164,7 +164,7 @@ func (d *Runner) StartService(ctx context.Context, connect ServiceAdapter) (*Ser bo := backoff.NewExponentialBackOff() bo.MaxInterval = time.Second * 5 - bo.MaxElapsedTime = 5 * time.Minute + bo.MaxElapsedTime = 2 * time.Minute pieces := strings.Split(hostIPs[0], ":") portInt, err := strconv.Atoi(pieces[1]) diff --git a/plugins/database/mongodb/mongodb_test.go b/plugins/database/mongodb/mongodb_test.go index 250f3083bbce9..d647a264f184a 100644 --- a/plugins/database/mongodb/mongodb_test.go +++ b/plugins/database/mongodb/mongodb_test.go @@ -27,7 +27,7 @@ import ( const mongoAdminRole = `{ "db": "admin", "roles": [ { "role": "readWrite" } ] }` func TestMongoDB_Initialize(t *testing.T) { - cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") + cleanup, connURL := mongodb.PrepareTestContainer(t, "5.0.10") defer cleanup() db := new() @@ -120,7 +120,7 @@ func TestNewUser_usernameTemplate(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") + cleanup, connURL := mongodb.PrepareTestContainer(t, "5.0.10") defer cleanup() db := new() @@ -146,7 +146,7 @@ func TestNewUser_usernameTemplate(t *testing.T) { } func TestMongoDB_CreateUser(t *testing.T) { - cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") + cleanup, connURL := mongodb.PrepareTestContainer(t, "5.0.10") defer cleanup() db := new() @@ -178,7 +178,7 @@ func TestMongoDB_CreateUser(t *testing.T) { } func TestMongoDB_CreateUser_writeConcern(t *testing.T) { - cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") + cleanup, connURL := mongodb.PrepareTestContainer(t, "5.0.10") defer cleanup() initReq := dbplugin.InitializeRequest{ @@ -212,7 +212,7 @@ func TestMongoDB_CreateUser_writeConcern(t *testing.T) { } func TestMongoDB_DeleteUser(t *testing.T) { - cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") + cleanup, connURL := mongodb.PrepareTestContainer(t, "5.0.10") defer cleanup() db := new() @@ -252,7 +252,7 @@ func TestMongoDB_DeleteUser(t *testing.T) { } func TestMongoDB_UpdateUser_Password(t *testing.T) { - cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") + cleanup, connURL := mongodb.PrepareTestContainer(t, "5.0.10") defer cleanup() // The docker test method PrepareTestContainer defaults to a database "test" From d6ca3b8edb47d295ef628eb47dea4022c83b53a9 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Thu, 25 Aug 2022 17:24:07 +0100 Subject: [PATCH 7/9] Add version to catalog read response --- vault/logical_system.go | 1 + vault/plugin_catalog.go | 1 + 2 files changed, 2 insertions(+) diff --git a/vault/logical_system.go b/vault/logical_system.go index 9ae4395373208..fde48ee0294a4 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -569,6 +569,7 @@ func (b *SystemBackend) handlePluginCatalogRead(ctx context.Context, _ *logical. "command": command, "sha256": hex.EncodeToString(plugin.Sha256), "builtin": plugin.Builtin, + "version": plugin.Version, } return &logical.Response{ diff --git a/vault/plugin_catalog.go b/vault/plugin_catalog.go index aaeabd2510bc4..155e82ca1b14d 100644 --- a/vault/plugin_catalog.go +++ b/vault/plugin_catalog.go @@ -533,6 +533,7 @@ func (c *PluginCatalog) get(ctx context.Context, name string, pluginType consts. Type: pluginType, Builtin: true, BuiltinFactory: factory, + Version: c.getBuiltinVersion(pluginType, name), }, nil } } From 8f204af96a8cc059de89c0b541e6bc53f70bd501 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Thu, 25 Aug 2022 18:18:26 +0100 Subject: [PATCH 8/9] Fix and expand tests for version in read plugin response --- vault/logical_system_test.go | 47 ++++++++++++++++++++++++++++++++++++ vault/plugin_catalog_test.go | 25 +++++++++++-------- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index a0ec34ed5d7a5..45aca583f7b85 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -2944,6 +2944,7 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) { "args": []string(nil), "sha256": "", "builtin": true, + "version": c.pluginCatalog.getBuiltinVersion(consts.PluginTypeDatabase, "mysql-database-plugin"), } if !reflect.DeepEqual(actualRespData, expectedRespData) { t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", actualRespData, expectedRespData) @@ -2989,6 +2990,7 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) { "args": []string{"--test"}, "sha256": "31", "builtin": false, + "version": "", } if !reflect.DeepEqual(actual, expected) { t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", actual, expected) @@ -3006,6 +3008,51 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) { if resp != nil || err != nil { t.Fatalf("expected nil response, plugin not deleted correctly got resp: %v, err: %v", resp, err) } + + // Add a versioned plugin, and check we get the version back in the right form when we read. + req = logical.TestRequest(t, logical.UpdateOperation, "plugins/catalog/database/test-plugin") + req.Data["version"] = "v0.1.0" + req.Data["sha_256"] = hex.EncodeToString([]byte{'1'}) + req.Data["command"] = command + resp, err = b.HandleRequest(namespace.RootContext(nil), req) + if err != nil || resp.Error() != nil { + t.Fatalf("err: %v %v", err, resp.Error()) + } + + req = logical.TestRequest(t, logical.ReadOperation, "plugins/catalog/database/test-plugin") + req.Data["version"] = "v0.1.0" + resp, err = b.HandleRequest(namespace.RootContext(nil), req) + if err != nil { + t.Fatalf("err: %v", err) + } + + actual = resp.Data + expected = map[string]interface{}{ + "name": "test-plugin", + "command": filepath.Base(file.Name()), + "args": []string{"--test"}, + "sha256": "31", + "builtin": false, + "version": "0.1.0", + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", actual, expected) + } + + // Delete versioned plugin + req = logical.TestRequest(t, logical.DeleteOperation, "plugins/catalog/database/test-plugin") + req.Data["version"] = "0.1.0" + resp, err = b.HandleRequest(namespace.RootContext(nil), req) + if err != nil { + t.Fatalf("err: %v", err) + } + + req = logical.TestRequest(t, logical.ReadOperation, "plugins/catalog/database/test-plugin") + req.Data["version"] = "0.1.0" + resp, err = b.HandleRequest(namespace.RootContext(nil), req) + if resp != nil || err != nil { + t.Fatalf("expected nil response, plugin not deleted correctly got resp: %v, err: %v", resp, err) + } } func TestSystemBackend_ToolsHash(t *testing.T) { diff --git a/vault/plugin_catalog_test.go b/vault/plugin_catalog_test.go index bf4c5b97605ac..0e3b31e54fe35 100644 --- a/vault/plugin_catalog_test.go +++ b/vault/plugin_catalog_test.go @@ -27,18 +27,21 @@ func TestPluginCatalog_CRUD(t *testing.T) { } core.pluginCatalog.directory = tempDir + const pluginName = "mysql-database-plugin" + // Get builtin plugin - p, err := core.pluginCatalog.Get(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "") + p, err := core.pluginCatalog.Get(context.Background(), pluginName, consts.PluginTypeDatabase, "") if err != nil { t.Fatalf("unexpected error %v", err) } expectedBuiltin := &pluginutil.PluginRunner{ - Name: "mysql-database-plugin", + Name: pluginName, Type: consts.PluginTypeDatabase, Builtin: true, + Version: core.pluginCatalog.getBuiltinVersion(consts.PluginTypeDatabase, pluginName), } - expectedBuiltin.BuiltinFactory, _ = builtinplugins.Registry.Get("mysql-database-plugin", consts.PluginTypeDatabase) + expectedBuiltin.BuiltinFactory, _ = builtinplugins.Registry.Get(pluginName, consts.PluginTypeDatabase) if &(p.BuiltinFactory) == &(expectedBuiltin.BuiltinFactory) { t.Fatal("expected BuiltinFactory did not match actual") @@ -57,25 +60,26 @@ func TestPluginCatalog_CRUD(t *testing.T) { defer file.Close() command := fmt.Sprintf("%s", filepath.Base(file.Name())) - err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "", command, []string{"--test"}, []string{"FOO=BAR"}, []byte{'1'}) + err = core.pluginCatalog.Set(context.Background(), pluginName, consts.PluginTypeDatabase, "", command, []string{"--test"}, []string{"FOO=BAR"}, []byte{'1'}) if err != nil { t.Fatal(err) } // Get the plugin - p, err = core.pluginCatalog.Get(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "") + p, err = core.pluginCatalog.Get(context.Background(), pluginName, consts.PluginTypeDatabase, "") if err != nil { t.Fatalf("unexpected error %v", err) } expected := &pluginutil.PluginRunner{ - Name: "mysql-database-plugin", + Name: pluginName, Type: consts.PluginTypeDatabase, Command: filepath.Join(tempDir, filepath.Base(file.Name())), Args: []string{"--test"}, Env: []string{"FOO=BAR"}, Sha256: []byte{'1'}, Builtin: false, + Version: "", } if !reflect.DeepEqual(p, expected) { @@ -83,23 +87,24 @@ func TestPluginCatalog_CRUD(t *testing.T) { } // Delete the plugin - err = core.pluginCatalog.Delete(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "") + err = core.pluginCatalog.Delete(context.Background(), pluginName, consts.PluginTypeDatabase, "") if err != nil { t.Fatalf("unexpected err: %v", err) } // Get builtin plugin - p, err = core.pluginCatalog.Get(context.Background(), "mysql-database-plugin", consts.PluginTypeDatabase, "") + p, err = core.pluginCatalog.Get(context.Background(), pluginName, consts.PluginTypeDatabase, "") if err != nil { t.Fatalf("unexpected error %v", err) } expectedBuiltin = &pluginutil.PluginRunner{ - Name: "mysql-database-plugin", + Name: pluginName, Type: consts.PluginTypeDatabase, Builtin: true, + Version: core.pluginCatalog.getBuiltinVersion(consts.PluginTypeDatabase, pluginName), } - expectedBuiltin.BuiltinFactory, _ = builtinplugins.Registry.Get("mysql-database-plugin", consts.PluginTypeDatabase) + expectedBuiltin.BuiltinFactory, _ = builtinplugins.Registry.Get(pluginName, consts.PluginTypeDatabase) if &(p.BuiltinFactory) == &(expectedBuiltin.BuiltinFactory) { t.Fatal("expected BuiltinFactory did not match actual") From 138dfcdbaff8d98a15f75a76254fe58d3a59efde Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Thu, 25 Aug 2022 18:31:09 +0100 Subject: [PATCH 9/9] Update changelog --- changelog/16688.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/changelog/16688.txt b/changelog/16688.txt index edc92dc72e8d1..ce192d5f220f8 100644 --- a/changelog/16688.txt +++ b/changelog/16688.txt @@ -1,6 +1,9 @@ ```release-note:change -plugins: `GET /sys/plugins/catalog` endpoint now returns an additional `detailed` field in the response with additional plugin metadata. +plugins: `GET /sys/plugins/catalog` endpoint now returns an additional `detailed` field in the response data with a list of additional plugin metadata. +``` +```release-note:change +plugins: `GET /sys/plugins/catalog/:type/:name` endpoint now returns an additional `version` field in the response data. ``` ```release-note:improvement -plugins: Plugin catalog supports registering plugins with a semantic version +plugins: Plugin catalog supports registering and managing plugins with semantic version information. ```