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

Add plugin CLI for interacting with the plugin catalog #4911

Merged
merged 12 commits into from
Jul 13, 2018
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