Added plugin reload function to api (#8777)

* Added plugin reload function to api

* Apply suggestions from code review

Co-Authored-By: Calvin Leung Huang <cleung2010@gmail.com>

* go mod vendor

* addressing comments

* addressing comments

* add docs

Co-authored-by: Calvin Leung Huang <cleung2010@gmail.com>
This commit is contained in:
Vladimir Dimitrov 2020-05-05 02:14:23 +08:00 committed by GitHub
parent acd7bfb83e
commit 34f01920e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 319 additions and 1 deletions

View File

@ -225,6 +225,35 @@ func (c *Sys) DeregisterPlugin(i *DeregisterPluginInput) error {
return err
}
// ReloadPluginInput is used as input to the ReloadPlugin function.
type ReloadPluginInput struct {
// Plugin is the name of the plugin to reload, as registered in the plugin catalog
Plugin string `json:"plugin"`
// Mounts is the array of string mount paths of the plugin backends to reload
Mounts []string `json:"mounts"`
}
// ReloadPlugin reloads mounted plugin backends
func (c *Sys) ReloadPlugin(i *ReloadPluginInput) error {
path := "/v1/sys/plugins/reload/backend"
req := c.c.NewRequest(http.MethodPut, path)
if err := req.SetJSONBody(i); err != nil {
return err
}
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
resp, err := c.c.RawRequestWithContext(ctx, req)
if err != nil {
return err
}
defer resp.Body.Close()
return err
}
// catalogPathByType is a helper to construct the proper API path by plugin type
func catalogPathByType(pluginType consts.PluginType, name string) string {
path := fmt.Sprintf("/v1/sys/plugins/catalog/%s/%s", pluginType, name)

View File

@ -442,6 +442,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
BaseCommand: getBaseCommand(),
}, nil
},
"plugin reload": func() (cli.Command, error) {
return &PluginReloadCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"policy": func() (cli.Command, error) {
return &PolicyCommand{
BaseCommand: getBaseCommand(),

110
command/plugin_reload.go Normal file
View File

@ -0,0 +1,110 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*PluginReloadCommand)(nil)
var _ cli.CommandAutocomplete = (*PluginReloadCommand)(nil)
type PluginReloadCommand struct {
*BaseCommand
plugin string
mounts []string
}
func (c *PluginReloadCommand) Synopsis() string {
return "Reload mounted plugin backend"
}
func (c *PluginReloadCommand) Help() string {
helpText := `
Usage: vault plugin reload [options]
Reloads mounted plugins. Either the plugin name or the desired plugin
mount(s) must be provided, but not both. In case the plugin name is provided,
all of its corresponding mounted paths that use the plugin backend will be reloaded.
Reload the plugin named "my-custom-plugin":
$ vault plugin reload -plugin=my-custom-plugin
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PluginReloadCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.StringVar(&StringVar{
Name: "plugin",
Target: &c.plugin,
Completion: complete.PredictAnything,
Usage: "The name of the plugin to reload, as registered in the plugin catalog.",
})
f.StringSliceVar(&StringSliceVar{
Name: "mounts",
Target: &c.mounts,
Completion: complete.PredictAnything,
Usage: "Array or comma-separated string mount paths of the plugin backends to reload.",
})
return set
}
func (c *PluginReloadCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *PluginReloadCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PluginReloadCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
switch {
case c.plugin == "" && len(c.mounts) == 0:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case c.plugin != "" && len(c.mounts) > 0:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
if err := client.Sys().ReloadPlugin(&api.ReloadPluginInput{
Plugin: c.plugin,
Mounts: c.mounts,
}); err != nil {
c.UI.Error(fmt.Sprintf("Error reloading plugin/mounts: %s", err))
return 2
}
if len(c.mounts) > 0 {
c.UI.Output(fmt.Sprintf("Success! Reloaded mounts: %s", c.mounts))
} else {
c.UI.Output(fmt.Sprintf("Success! Reloaded plugin: %s", c.plugin))
}
return 0
}

View File

@ -0,0 +1,110 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/mitchellh/cli"
)
func testPluginReloadCommand(tb testing.TB) (*cli.MockUi, *PluginReloadCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PluginReloadCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPluginReloadCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
nil,
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"-plugin", "foo", "-mounts", "bar"},
"Too many arguments",
1,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPluginReloadCommand(t)
cmd.client = client
args := append([]string{}, tc.args...)
code := cmd.Run(args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
t.Run("integration", func(t *testing.T) {
t.Parallel()
pluginDir, cleanup := testPluginDir(t)
defer cleanup(t)
client, _, closer := testVaultServerPluginDir(t, pluginDir)
defer closer()
pluginName := "my-plugin"
_, sha256Sum := testPluginCreateAndRegister(t, client, pluginDir, pluginName, consts.PluginTypeCredential)
ui, cmd := testPluginReloadCommand(t)
cmd.client = client
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
Name: pluginName,
Type: consts.PluginTypeCredential,
Command: pluginName,
SHA256: sha256Sum,
}); err != nil {
t.Fatal(err)
}
code := cmd.Run([]string{
"-plugin", pluginName,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Reloaded plugin: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
}

View File

@ -225,6 +225,34 @@ func (c *Sys) DeregisterPlugin(i *DeregisterPluginInput) error {
return err
}
// ReloadPluginInput is used as input fo the ReloadPlugin function.
type ReloadPluginInput struct {
// Plugin is the name of the plugin to reload, as registered in the plugin catalog
Plugin string `json:"plugin"`
// Mounts is the array of string mount paths of the plugin backends to reload
Mounts []string `json:"mounts"`
}
// ReloadPlugin reloads mounted plugin backends
func (c *Sys) ReloadPlugin(i *ReloadPluginInput) error {
path := "/v1/sys/plugins/reload/backend"
req := c.c.NewRequest(http.MethodPut, path)
if err := req.SetJSONBody(i); err != nil {
return err
}
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
resp, err := c.c.RawRequestWithContext(ctx, req)
if err == nil {
defer resp.Body.Close()
}
return err
}
// catalogPathByType is a helper to construct the proper API path by plugin type
func catalogPathByType(pluginType consts.PluginType, name string) string {
path := fmt.Sprintf("/v1/sys/plugins/catalog/%s/%s", pluginType, name)

View File

@ -54,9 +54,10 @@ Usage: vault plugin <subcommand> [options] [args]
Subcommands:
deregister Deregister an existing plugin in the catalog
info Read information about a plugin in the catalog
list Lists available plugins
read Read information about a plugin in the catalog
register Registers a new plugin in the catalog
reload Reload mounted plugin backend
```
For more information, examples, and usage about a subcommand, click on the name

View File

@ -0,0 +1,35 @@
---
layout: docs
page_title: plugin reload - Command
sidebar_title: <code>reload</code>
description: |-
The "plugin reload" command reloads mounted plugins.
---
# plugin reload
The `plugin reload` command is used to reload mounted plugin backends. Either
the plugin name (`plugin`) or the desired plugin backend mounts (`mounts`)
must be provided, but not both.
## Examples
Reload a plugin:
```text
$ vault plugin reload -plugin my-custom-plugin
Success! Reloaded plugin: my-custom-plugin
```
## Usage
The following flags are available in addition to the [standard set of
flags](/docs/commands) included on all commands.
### Command Options
- `-plugin` `(string: "")` - The name of the plugin to reload, as registered in
the plugin catalog.
- `-mounts` `(array: [])` - Array or comma-separated string mount paths of the
plugin backends to reload.