Add plugin CLI for interacting with the plugin catalog (#4911)

* Add 'plugin list' command

* Add 'plugin register' command

* Add 'plugin deregister' command

* Use a shared plugin helper

* Add 'plugin read' command

* Rename to plugin info

* Add base plugin for help text

* Fix arg ordering

* Add docs

* Rearrange to alphabetize

* Fix arg ordering in example

* Don't use "sudo" in command description
This commit is contained in:
Seth Vargo 2018-07-13 13:35:08 -04:00 committed by Brian Kassouf
parent 51d842bd0a
commit 1259ee6743
19 changed files with 1382 additions and 2 deletions

View File

@ -60,12 +60,14 @@ func (c *Sys) GetPlugin(i *GetPluginInput) (*GetPluginResponse, error) {
}
defer resp.Body.Close()
var result GetPluginResponse
var result struct {
Data GetPluginResponse
}
err = resp.DecodeJSON(&result)
if err != nil {
return nil, err
}
return &result, err
return &result.Data, err
}
// RegisterPluginInput is used as input to the RegisterPlugin function.

View File

@ -93,6 +93,23 @@ func testVaultServerUnseal(tb testing.TB) (*api.Client, []string, func()) {
})
}
// testVaultServerUnseal creates a test vault cluster and returns a configured
// API client, list of unseal keys (as strings), and a closer function
// configured with the given plugin directory.
func testVaultServerPluginDir(tb testing.TB, pluginDir string) (*api.Client, []string, func()) {
tb.Helper()
return testVaultServerCoreConfig(tb, &vault.CoreConfig{
DisableMlock: true,
DisableCache: true,
Logger: defaultVaultLogger,
CredentialBackends: defaultVaultCredentialBackends,
AuditBackends: defaultVaultAuditBackends,
LogicalBackends: defaultVaultLogicalBackends,
PluginDirectory: pluginDir,
})
}
// testVaultServerCoreConfig creates a new vault cluster with the given core
// configuration. This is a lower-level test helper.
func testVaultServerCoreConfig(tb testing.TB, coreConfig *vault.CoreConfig) (*api.Client, []string, func()) {

View File

@ -361,6 +361,31 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
BaseCommand: getBaseCommand(),
}, nil
},
"plugin": func() (cli.Command, error) {
return &PluginCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"plugin deregister": func() (cli.Command, error) {
return &PluginDeregisterCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"plugin info": func() (cli.Command, error) {
return &PluginInfoCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"plugin list": func() (cli.Command, error) {
return &PluginListCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"plugin register": func() (cli.Command, error) {
return &PluginRegisterCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"policy": func() (cli.Command, error) {
return &PolicyCommand{
BaseCommand: getBaseCommand(),

46
command/plugin.go Normal file
View File

@ -0,0 +1,46 @@
package command
import (
"strings"
"github.com/mitchellh/cli"
)
var _ cli.Command = (*PluginCommand)(nil)
type PluginCommand struct {
*BaseCommand
}
func (c *PluginCommand) Synopsis() string {
return "Interact with Vault plugins and catalog"
}
func (c *PluginCommand) Help() string {
helpText := `
Usage: vault plugin <subcommand> [options] [args]
This command groups subcommands for interacting with Vault's plugins and the
plugin catalog. Here are a few examples of the plugin commands:
List all available plugins in the catalog:
$ vault plugin list
Register a new plugin to the catalog:
$ vault plugin register -sha256=d3f0a8b... my-custom-plugin
Get information about a plugin in the catalog:
$ vault plugin info my-custom-plugin
Please see the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *PluginCommand) Run(args []string) int {
return cli.RunResultHelp
}

View File

@ -0,0 +1,86 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*PluginDeregisterCommand)(nil)
var _ cli.CommandAutocomplete = (*PluginDeregisterCommand)(nil)
type PluginDeregisterCommand struct {
*BaseCommand
}
func (c *PluginDeregisterCommand) Synopsis() string {
return "Deregister an existing plugin in the catalog"
}
func (c *PluginDeregisterCommand) Help() string {
helpText := `
Usage: vault plugin deregister [options] NAME
Deregister an existing plugin in the catalog. If the plugin does not exist,
no action is taken (the command is idempotent).
Deregister the plugin named my-custom-plugin:
$ vault plugin deregister my-custom-plugin
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PluginDeregisterCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *PluginDeregisterCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultPlugins()
}
func (c *PluginDeregisterCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PluginDeregisterCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
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
}
pluginName := strings.TrimSpace(args[0])
if err := client.Sys().DeregisterPlugin(&api.DeregisterPluginInput{
Name: pluginName,
}); err != nil {
c.UI.Error(fmt.Sprintf("Error deregistering plugin named %s: %s", pluginName, err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Deregistered plugin (if it was registered): %s", pluginName))
return 0
}

View File

@ -0,0 +1,156 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
)
func testPluginDeregisterCommand(tb testing.TB) (*cli.MockUi, *PluginDeregisterCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PluginDeregisterCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPluginDeregisterCommand_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{"foo", "bar"},
"Too many arguments",
1,
},
{
"not_a_plugin",
[]string{"nope_definitely_never_a_plugin_nope"},
"",
0,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPluginDeregisterCommand(t)
cmd.client = client
code := cmd.Run(tc.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)
ui, cmd := testPluginDeregisterCommand(t)
cmd.client = client
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
Name: pluginName,
Command: pluginName,
SHA256: sha256Sum,
}); err != nil {
t.Fatal(err)
}
code := cmd.Run([]string{
pluginName,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Deregistered plugin (if it was registered): "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{})
if err != nil {
t.Fatal(err)
}
found := false
for _, p := range resp.Names {
if p == pluginName {
found = true
}
}
if found {
t.Errorf("expected %q to not be in %q", pluginName, resp.Names)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testPluginDeregisterCommand(t)
cmd.client = client
code := cmd.Run([]string{
"my-plugin",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error deregistering plugin named my-plugin: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPluginDeregisterCommand(t)
assertNoTabs(t, cmd)
})
}

97
command/plugin_info.go Normal file
View File

@ -0,0 +1,97 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*PluginInfoCommand)(nil)
var _ cli.CommandAutocomplete = (*PluginInfoCommand)(nil)
type PluginInfoCommand struct {
*BaseCommand
}
func (c *PluginInfoCommand) Synopsis() string {
return "Read information about a plugin in the catalog"
}
func (c *PluginInfoCommand) Help() string {
helpText := `
Usage: vault plugin info [options] NAME
Displays information about a plugin in the catalog with the given name. If
the plugin does not exist, an error is returned.
Get info about a plugin:
$ vault plugin info mysql-database-plugin
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PluginInfoCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
}
func (c *PluginInfoCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultPlugins()
}
func (c *PluginInfoCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PluginInfoCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
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
}
pluginName := strings.TrimSpace(args[0])
resp, err := client.Sys().GetPlugin(&api.GetPluginInput{
Name: pluginName,
})
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading plugin named %s: %s", pluginName, err))
return 2
}
data := map[string]interface{}{
"args": resp.Args,
"builtin": resp.Builtin,
"command": resp.Command,
"name": resp.Name,
"sha256": resp.SHA256,
}
if c.flagField != "" {
return PrintRawField(c.UI, data, c.flagField)
}
return OutputData(c.UI, data)
}

161
command/plugin_info_test.go Normal file
View File

@ -0,0 +1,161 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testPluginInfoCommand(tb testing.TB) (*cli.MockUi, *PluginInfoCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PluginInfoCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPluginInfoCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
{
"no_plugin_exist",
[]string{"not-a-real-plugin-like-ever"},
"Error reading plugin",
2,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPluginInfoCommand(t)
cmd.client = client
code := cmd.Run(tc.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("default", 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)
ui, cmd := testPluginInfoCommand(t)
cmd.client = client
code := cmd.Run([]string{
pluginName,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, pluginName) {
t.Errorf("expected %q to contain %q", combined, pluginName)
}
if !strings.Contains(combined, sha256Sum) {
t.Errorf("expected %q to contain %q", combined, sha256Sum)
}
})
t.Run("field", func(t *testing.T) {
t.Parallel()
pluginDir, cleanup := testPluginDir(t)
defer cleanup(t)
client, _, closer := testVaultServerPluginDir(t, pluginDir)
defer closer()
pluginName := "my-plugin"
testPluginCreateAndRegister(t, client, pluginDir, pluginName)
ui, cmd := testPluginInfoCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-field", "builtin",
pluginName,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if exp := "false"; combined != exp {
t.Errorf("expected %q to be %q", combined, exp)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testPluginInfoCommand(t)
cmd.client = client
code := cmd.Run([]string{
"my-plugin",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error reading plugin named my-plugin: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPluginInfoCommand(t)
assertNoTabs(t, cmd)
})
}

89
command/plugin_list.go Normal file
View File

@ -0,0 +1,89 @@
package command
import (
"fmt"
"sort"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*PluginListCommand)(nil)
var _ cli.CommandAutocomplete = (*PluginListCommand)(nil)
type PluginListCommand struct {
*BaseCommand
}
func (c *PluginListCommand) Synopsis() string {
return "Lists available plugins"
}
func (c *PluginListCommand) Help() string {
helpText := `
Usage: vault plugin list [options]
Lists available plugins registered in the catalog. This does not list whether
plugins are in use, but rather just their availability.
List all available plugins in the catalog:
$ vault plugin list
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PluginListCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
}
func (c *PluginListCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (c *PluginListCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PluginListCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
if len(args) > 0 {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{})
if err != nil {
c.UI.Error(fmt.Sprintf("Error listing available plugins: %s", err))
return 2
}
pluginNames := resp.Names
sort.Strings(pluginNames)
switch Format(c.UI) {
case "table":
list := append([]string{"Plugins"}, pluginNames...)
c.UI.Output(tableOutput(list, nil))
return 0
default:
return OutputData(c.UI, pluginNames)
}
}

View File

@ -0,0 +1,99 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testPluginListCommand(tb testing.TB) (*cli.MockUi, *PluginListCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PluginListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPluginListCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"too_many_args",
[]string{"foo"},
"Too many arguments",
1,
},
{
"lists",
nil,
"Plugins",
0,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPluginListCommand(t)
cmd.client = client
code := cmd.Run(tc.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("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testPluginListCommand(t)
cmd.client = client
code := cmd.Run([]string{})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error listing available plugins: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPluginListCommand(t)
assertNoTabs(t, cmd)
})
}

135
command/plugin_register.go Normal file
View File

@ -0,0 +1,135 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*PluginRegisterCommand)(nil)
var _ cli.CommandAutocomplete = (*PluginRegisterCommand)(nil)
type PluginRegisterCommand struct {
*BaseCommand
flagArgs []string
flagCommand string
flagSHA256 string
}
func (c *PluginRegisterCommand) Synopsis() string {
return "Registers a new plugin in the catalog"
}
func (c *PluginRegisterCommand) Help() string {
helpText := `
Usage: vault plugin register [options] NAME
Registers a new plugin in the catalog. The plugin binary must exist in Vault's
configured plugin directory.
Register the plugin named my-custom-plugin:
$ vault plugin register -sha256=d3f0a8b... my-custom-plugin
Register a plugin with custom arguments:
$ vault plugin register \
-sha256=d3f0a8b... \
-args=--with-glibc,--with-cgo \
my-custom-plugin
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PluginRegisterCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.StringSliceVar(&StringSliceVar{
Name: "args",
Target: &c.flagArgs,
Completion: complete.PredictAnything,
Usage: "Arguments to pass to the plugin when starting. Separate " +
"multiple arguments with a comma.",
})
f.StringVar(&StringVar{
Name: "command",
Target: &c.flagCommand,
Completion: complete.PredictAnything,
Usage: "Command to spawn the plugin. This defaults to the name of the " +
"plugin if unspecified.",
})
f.StringVar(&StringVar{
Name: "sha256",
Target: &c.flagSHA256,
Completion: complete.PredictAnything,
Usage: "SHA256 of the plugin binary. This is required for all plugins.",
})
return set
}
func (c *PluginRegisterCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultPlugins()
}
func (c *PluginRegisterCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PluginRegisterCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
case c.flagSHA256 == "":
c.UI.Error("SHA256 is required for all plugins, please provide -sha256")
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
pluginName := strings.TrimSpace(args[0])
command := c.flagCommand
if command == "" {
command = pluginName
}
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
Name: pluginName,
Args: c.flagArgs,
Command: command,
SHA256: c.flagSHA256,
}); err != nil {
c.UI.Error(fmt.Sprintf("Error registering plugin %s: %s", pluginName, err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Registered plugin: %s", pluginName))
return 0
}

View File

@ -0,0 +1,151 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
)
func testPluginRegisterCommand(tb testing.TB) (*cli.MockUi, *PluginRegisterCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PluginRegisterCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPluginRegisterCommand_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{"foo", "bar"},
"Too many arguments",
1,
},
{
"not_a_plugin",
[]string{"nope_definitely_never_a_plugin_nope"},
"",
2,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPluginRegisterCommand(t)
cmd.client = client
args := append([]string{"-sha256", "abcd1234"}, 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 := testPluginCreate(t, pluginDir, pluginName)
ui, cmd := testPluginRegisterCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-sha256", sha256Sum,
pluginName,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Registered plugin: my-plugin"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{})
if err != nil {
t.Fatal(err)
}
found := false
for _, p := range resp.Names {
if p == pluginName {
found = true
}
}
if !found {
t.Errorf("expected %q to be in %q", pluginName, resp.Names)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testPluginRegisterCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-sha256", "abcd1234",
"my-plugin",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error registering plugin my-plugin:"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPluginRegisterCommand(t)
assertNoTabs(t, cmd)
})
}

78
command/plugin_test.go Normal file
View File

@ -0,0 +1,78 @@
package command
import (
"crypto/sha256"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/vault/api"
)
// testPluginDir creates a temporary directory suitable for holding plugins.
// This helper also resolves symlinks to make tests happy on OS X.
func testPluginDir(tb testing.TB) (string, func(tb testing.TB)) {
tb.Helper()
dir, err := ioutil.TempDir("", "")
if err != nil {
tb.Fatal(err)
}
// OSX tempdir are /var, but actually symlinked to /private/var
dir, err = filepath.EvalSymlinks(dir)
if err != nil {
tb.Fatal(err)
}
return dir, func(tb testing.TB) {
if err := os.RemoveAll(dir); err != nil {
tb.Fatal(err)
}
}
}
// testPluginCreate creates a sample plugin in a tempdir and returns the shasum
// and filepath to the plugin.
func testPluginCreate(tb testing.TB, dir, name string) (string, string) {
tb.Helper()
pth := dir + "/" + name
if err := ioutil.WriteFile(pth, nil, 0755); err != nil {
tb.Fatal(err)
}
f, err := os.Open(pth)
if err != nil {
tb.Fatal(err)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
tb.Fatal(err)
}
sha256Sum := fmt.Sprintf("%x", h.Sum(nil))
return pth, sha256Sum
}
// testPluginCreateAndRegister creates a plugin and registers it in the catalog.
func testPluginCreateAndRegister(tb testing.TB, client *api.Client, dir, name string) (string, string) {
tb.Helper()
pth, sha256Sum := testPluginCreate(tb, dir, name)
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
Name: name,
Command: name,
SHA256: sha256Sum,
}); err != nil {
tb.Fatal(err)
}
return pth, sha256Sum
}

View File

@ -0,0 +1,63 @@
---
layout: "docs"
page_title: "plugin - Command"
sidebar_current: "docs-commands-plugin"
description: |-
The "plugin" command groups subcommands for interacting with
Vault's plugins and the plugin catalog.
---
# plugin
The `plugin` command groups subcommands for interacting with Vault's plugins and
the plugin catalog
## Examples
List all available plugins in the catalog:
```text
$ vault plugin list
Plugins
-------
my-custom-plugin
# ...
```
Register a new plugin to the catalog:
```text
$ vault plugin register \
-sha256=d3f0a8be02f6c074cf38c9c99d4d04c9c6466249 \
my-custom-plugin
Success! Registered plugin: my-custom-plugin
```
Get information about a plugin in the catalog:
```text
$ vault plugin info my-custom-plugin
Key Value
--- -----
command my-custom-plugin
name my-custom-plugin
sha256 d3f0a8be02f6c074cf38c9c99d4d04c9c6466249
```
## Usage
```text
Usage: vault plugin <subcommand> [options] [args]
# ...
Subcommands:
deregister Deregister an existing 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
```
For more information, examples, and usage about a subcommand, click on the name
of the subcommand in the sidebar.

View File

@ -0,0 +1,27 @@
---
layout: "docs"
page_title: "plugin deregister - Command"
sidebar_current: "docs-commands-plugin-deregister"
description: |-
The "plugin deregister" command deregisters a new plugin in Vault's plugin
catalog.
---
# plugin deregister
The `plugin deregister` command deregisters an existing plugin from Vault's
plugin catalog. If the plugin does not exist, no error is returned.
## Examples
Deregister a plugin:
```text
$ vault plugin deregister my-custom-plugin
Success! Deregistered plugin (if it was registered): my-custom-plugin
```
## Usage
There are no flags beyond the [standard set of flags](/docs/commands/index.html)
included on all commands.

View File

@ -0,0 +1,43 @@
---
layout: "docs"
page_title: "plugin info - Command"
sidebar_current: "docs-commands-plugin-info"
description: |-
The "plugin info" command displays information about a plugin in the catalog.
---
# plugin info
The `plugin info` displays information about a plugin in the catalog.
## Examples
Display information about a plugin
```text
$ vault plugin info my-custom-plugin
Key Value
--- -----
args []
builtin false
command my-custom-plugin
name my-custom-plugin
sha256 d3f0a8be02f6c074cf38c9c99d4d04c9c6466249
```
## Usage
The following flags are available in addition to the [standard set of
flags](/docs/commands/index.html) included on all commands.
### Output Options
- `-field` `(string: "")` - Print only the field with the given name. Specifying
this option will take precedence over other formatting directives. The result
will not have a trailing newline making it ideal for piping to other
processes.
- `-format` `(string: "table")` - Print the output in the given format. Valid
formats are "table", "json", or "yaml". This can also be specified via the
`VAULT_FORMAT` environment variable.

View File

@ -0,0 +1,35 @@
---
layout: "docs"
page_title: "plugin list - Command"
sidebar_current: "docs-commands-plugin-list"
description: |-
The "plugin list" command lists all available plugins in the plugin catalog.
---
# plugin list
The `plugin list` command lists all available plugins in the plugin catalog.
## Examples
List all available plugins in the catalog.
```text
$ vault plugin list
Plugins
-------
my-custom-plugin
# ...
```
## Usage
The following flags are available in addition to the [standard set of
flags](/docs/commands/index.html) included on all commands.
### Output Options
- `-format` `(string: "table")` - Print the output in the given format. Valid
formats are "table", "json", or "yaml". This can also be specified via the
`VAULT_FORMAT` environment variable.

View File

@ -0,0 +1,53 @@
---
layout: "docs"
page_title: "plugin register - Command"
sidebar_current: "docs-commands-plugin-register"
description: |-
The "plugin register" command registers a new plugin in Vault's plugin
catalog.
---
# plugin register
The `plugin register` command registers a new plugin in Vault's plugin catalog.
## Examples
Register a plugin:
```text
$ vault plugin register \
-sha256=d3f0a8be02f6c074cf38c9c99d4d04c9c6466249 \
my-custom-plugin
Success! Registered plugin: my-custom-plugin
```
Register a plugin with custom args:
```text
$ vault plugin register \
-sha256=d3f0a8be02f6c074cf38c9c99d4d04c9c6466249 \
-args=--with-glibc,--with-curl-bindings \
my-custom-plugin
```
## Usage
The following flags are available in addition to the [standard set of
flags](/docs/commands/index.html) included on all commands.
### Output Options
- `-format` `(string: "table")` - Print the output in the given format. Valid
formats are "table", "json", or "yaml". This can also be specified via the
`VAULT_FORMAT` environment variable.
### Command Options
- `-sha256` `(string: <required>)` - Checksum (SHA256) of the plugin binary.
- `-args` `(string: "")` - List of arguments to pass to the binary plugin during
each invocation. Specify multiple arguments with commas.
- `-command` `(string: "")` - Name of the command to run to invoke the binary.
By default, this is the name of the plugin.

View File

@ -265,6 +265,23 @@
<li<%= sidebar_current("docs-commands-path-help") %>>
<a href="/docs/commands/path-help.html">path-help</a>
</li>
<li<%= sidebar_current("docs-commands-plugin") %>>
<a href="/docs/commands/plugin.html">plugin</a>
<ul class="nav">
<li<%= sidebar_current("docs-commands-plugin-deregister") %>>
<a href="/docs/commands/plugin/deregister.html">deregister</a>
</li>
<li<%= sidebar_current("docs-commands-plugin-info") %>>
<a href="/docs/commands/plugin/info.html">info</a>
</li>
<li<%= sidebar_current("docs-commands-plugin-list") %>>
<a href="/docs/commands/plugin/list.html">list</a>
</li>
<li<%= sidebar_current("docs-commands-plugin-register") %>>
<a href="/docs/commands/plugin/register.html">register</a>
</li>
</ul>
</li>
<li<%= sidebar_current("docs-commands-policy") %>>
<a href="/docs/commands/policy.html">policy</a>
<ul class="nav">