Skip to content

Commit

Permalink
Add plugin CLI for interacting with the plugin catalog (#4911)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sethvargo authored and briankassouf committed Jul 13, 2018
1 parent 80a0d56 commit c50881b
Show file tree
Hide file tree
Showing 19 changed files with 1,382 additions and 2 deletions.
6 changes: 4 additions & 2 deletions api/sys_plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
25 changes: 25 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
46 changes: 46 additions & 0 deletions command/plugin.go
Original file line number Diff line number Diff line change
@@ -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
}
86 changes: 86 additions & 0 deletions command/plugin_deregister.go
Original file line number Diff line number Diff line change
@@ -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
}
156 changes: 156 additions & 0 deletions command/plugin_deregister_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
Loading

0 comments on commit c50881b

Please sign in to comment.