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

cli: Improve error handling for plugin commands #24250

Merged
merged 3 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog/24250.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
```release-note:change
cli: `vault plugin info` and `vault plugin deregister` now require 2 positional arguments instead of accepting either 1 or 2.
```
```release-note:improvement
cli: Improved error messages for `vault plugin` sub-commands.
```
48 changes: 39 additions & 9 deletions command/plugin_deregister.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
package command

import (
"context"
"fmt"
"net/http"
"strings"

semver "github.com/hashicorp/go-version"
Expand Down Expand Up @@ -83,18 +85,16 @@ func (c *PluginDeregisterCommand) Run(args []string) int {

var pluginNameRaw, pluginTypeRaw string
args = f.Args()
switch len(args) {
case 0:
c.UI.Error("Not enough arguments (expected 1, or 2, got 0)")
positionalArgsCount := len(args)
switch positionalArgsCount {
case 0, 1:
tomhjp marked this conversation as resolved.
Show resolved Hide resolved
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 2, got %d)", positionalArgsCount))
return 1
case 1:
pluginTypeRaw = "unknown"
pluginNameRaw = args[0]
case 2:
pluginTypeRaw = args[0]
pluginNameRaw = args[1]
default:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, or 2, got %d)", len(args)))
c.UI.Error(fmt.Sprintf("Too many arguments (expected 2, got %d)", positionalArgsCount))
return 1
}

Expand All @@ -118,7 +118,33 @@ func (c *PluginDeregisterCommand) Run(args []string) int {
}
}

if err := client.Sys().DeregisterPlugin(&api.DeregisterPluginInput{
// The deregister endpoint returns 200 if the plugin doesn't exist, so first
// try fetching the plugin to help improve info printed to the user.
// 404 => Return early with a descriptive message.
// Other error => Continue attempting to deregister the plugin anyway.
// Plugin exists but is builtin => Error early.
// Otherwise => If deregister succeeds, we can report that the plugin really
// was deregistered (and not just already absent).
var pluginExists bool
if info, err := client.Sys().GetPluginWithContext(context.Background(), &api.GetPluginInput{
Name: pluginName,
Type: pluginType,
Version: c.flagPluginVersion,
}); err != nil {
if respErr, ok := err.(*api.ResponseError); ok && respErr.StatusCode == http.StatusNotFound {
c.UI.Output(fmt.Sprintf("Plugin %q (type: %q, version %q) does not exist in the catalog", pluginName, pluginType, c.flagPluginVersion))
return 0
}
// Best-effort check, continue trying to deregister.
} else if info != nil {
if info.Builtin {
c.UI.Error(fmt.Sprintf("Plugin %q (type: %q) is a builtin plugin and cannot be deregistered", pluginName, pluginType))
return 2
}
pluginExists = true
}

if err := client.Sys().DeregisterPluginWithContext(context.Background(), &api.DeregisterPluginInput{
Name: pluginName,
Type: pluginType,
Version: c.flagPluginVersion,
Expand All @@ -127,6 +153,10 @@ func (c *PluginDeregisterCommand) Run(args []string) int {
return 2
}

c.UI.Output(fmt.Sprintf("Success! Deregistered plugin (if it was registered): %s", pluginName))
if pluginExists {
c.UI.Output(fmt.Sprintf("Success! Deregistered %s plugin: %s", pluginType, pluginName))
} else {
c.UI.Output(fmt.Sprintf("Success! Deregistered %s plugin (if it was registered): %s", pluginType, pluginName))
}
return 0
}
31 changes: 27 additions & 4 deletions command/plugin_deregister_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestPluginDeregisterCommand_Run(t *testing.T) {
}{
{
"not_enough_args",
nil,
[]string{"foo"},
"Not enough arguments",
1,
},
Expand Down Expand Up @@ -109,7 +109,7 @@ func TestPluginDeregisterCommand_Run(t *testing.T) {
t.Errorf("expected %d to be %d", code, exp)
}

expected := "Success! Deregistered plugin (if it was registered): "
expected := "Success! Deregistered auth plugin: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
Expand Down Expand Up @@ -159,7 +159,7 @@ func TestPluginDeregisterCommand_Run(t *testing.T) {
t.Errorf("expected %d to be %d", code, exp)
}

expected := "Success! Deregistered plugin (if it was registered): "
expected := "Success! Deregistered auth plugin: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
Expand Down Expand Up @@ -206,7 +206,7 @@ func TestPluginDeregisterCommand_Run(t *testing.T) {
t.Errorf("expected %d to be %d", code, exp)
}

expected := "Success! Deregistered plugin (if it was registered): "
expected := "does not exist in the catalog"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I wonder if we should change the test below to be a bit more specific by using strings.HasSuffix rather than strings.Contains.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm already pretty uncomfortable with how brittle these tests are tbh (string-matching on error messages that can be arbitrarily updated), and it doesn't feel like that meaningfully improves the intent of the test, but it does make it more brittle. Unless you disagree strongly, I'd prefer to leave it as-is.

combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
Expand All @@ -230,6 +230,29 @@ func TestPluginDeregisterCommand_Run(t *testing.T) {
}
})

t.Run("deregister builtin", func(t *testing.T) {
t.Parallel()

pluginDir, cleanup := corehelpers.MakeTestPluginDir(t)
defer cleanup(t)

client, _, closer := testVaultServerPluginDir(t, pluginDir)
defer closer()

ui, cmd := testPluginDeregisterCommand(t)
cmd.client = client

expected := "is a builtin plugin"
if code := cmd.Run([]string{
consts.PluginTypeCredential.String(),
"github",
}); code != 2 {
t.Errorf("expected %d to be %d", code, 2)
} else if !strings.Contains(ui.ErrorWriter.String(), expected) {
t.Errorf("expected %q to contain %q", ui.ErrorWriter.String(), expected)
}
})

t.Run("communication_failure", func(t *testing.T) {
t.Parallel()

Expand Down
20 changes: 8 additions & 12 deletions command/plugin_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,23 +77,19 @@ func (c *PluginInfoCommand) Run(args []string) int {

var pluginNameRaw, pluginTypeRaw string
args = f.Args()
positionalArgsCount := len(args)
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1 or 2, got %d)", len(args)))
case positionalArgsCount < 2:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 2, got %d)", positionalArgsCount))
return 1
case len(args) > 2:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1 or 2, got %d)", len(args)))
case positionalArgsCount > 2:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 2, got %d)", positionalArgsCount))
return 1

// These cases should come after invalid cases have been checked
case len(args) == 1:
pluginTypeRaw = "unknown"
pluginNameRaw = args[0]
case len(args) == 2:
pluginTypeRaw = args[0]
pluginNameRaw = args[1]
}

pluginTypeRaw = args[0]
pluginNameRaw = args[1]

client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
Expand Down
6 changes: 6 additions & 0 deletions command/plugin_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ func TestPluginInfoCommand_Run(t *testing.T) {
out string
code int
}{
{
"not_enough_args",
[]string{"foo"},
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"foo", "bar", "fizz"},
Expand Down
8 changes: 6 additions & 2 deletions command/plugin_reload.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,16 @@ func (c *PluginReloadCommand) Run(args []string) int {
return 1
}

positionalArgs := len(f.Args())
switch {
case positionalArgs != 0:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", positionalArgs))
return 1
case c.plugin == "" && len(c.mounts) == 0:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
c.UI.Error("No plugins specified, must specify exactly one of -plugin or -mounts")
return 1
case c.plugin != "" && len(c.mounts) > 0:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
c.UI.Error("Must specify exactly one of -plugin or -mounts")
return 1
case c.scope != "" && c.scope != "global":
c.UI.Error(fmt.Sprintf("Invalid reload scope: %s", c.scope))
Expand Down
6 changes: 3 additions & 3 deletions command/plugin_reload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ func TestPluginReloadCommand_Run(t *testing.T) {
{
"not_enough_args",
nil,
"Not enough arguments",
"No plugins specified, must specify exactly one of -plugin or -mounts",
1,
},
{
"too_many_args",
[]string{"-plugin", "foo", "-mounts", "bar"},
"Too many arguments",
"Must specify exactly one of -plugin or -mounts",
1,
},
}
Expand Down Expand Up @@ -147,7 +147,7 @@ func TestPluginReloadStatusCommand_Run(t *testing.T) {
client, closer := testVaultServer(t)
defer closer()

ui, cmd := testPluginReloadCommand(t)
ui, cmd := testPluginReloadStatusCommand(t)
cmd.client = client

args := append([]string{}, tc.args...)
Expand Down
Loading