Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version-aware plugin catalog #16688

Merged
merged 9 commits into from Aug 25, 2022
2 changes: 1 addition & 1 deletion builtin/logical/database/rotation_test.go
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions builtin/logical/mongodb/backend_test.go
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions changelog/16688.txt
@@ -0,0 +1,9 @@
```release-note:change
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 and managing plugins with semantic version information.
```
12 changes: 6 additions & 6 deletions plugins/database/mongodb/mongodb_test.go
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions sdk/helper/pluginutil/run_config.go
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions sdk/helper/pluginutil/runner.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Type string `json:"type"` // string instead of consts.PluginType so that we get the string form in API responses.
Name string `json:"name"`
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.
Expand Down
4 changes: 2 additions & 2 deletions vault/auth.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion vault/dynamic_system_view.go
Expand Up @@ -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
}
Expand Down
91 changes: 81 additions & 10 deletions vault/logical_system.go
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -399,27 +401,61 @@ 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 {
return nil, err
}
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
sortVersionedPlugins(versionedPlugins)

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 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)
tomhjp marked this conversation as resolved.
Show resolved Hide resolved
}

return false
})
}

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
Expand All @@ -436,6 +472,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)
Expand Down Expand Up @@ -472,15 +513,15 @@ 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
}

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
Expand All @@ -501,7 +542,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
}
Expand All @@ -523,19 +569,25 @@ func (b *SystemBackend) handlePluginCatalogRead(ctx context.Context, req *logica
"command": command,
"sha256": hex.EncodeToString(plugin.Sha256),
"builtin": plugin.Builtin,
"version": plugin.Version,
}

return &logical.Response{
Data: data,
}, 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 == "" {
Expand All @@ -552,13 +604,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)
Expand Down Expand Up @@ -5221,6 +5288,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.`,
`
Expand Down
4 changes: 4 additions & 0 deletions vault/logical_system_paths.go
Expand Up @@ -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{
Expand Down