From ad1482e123cf6330f4ec0f76698e889c1e2bcbd5 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:03:36 -0400 Subject: [PATCH] Update read command --- command/read.go | 143 +++++++++++------------- command/read_test.go | 256 +++++++++++++++++++++++-------------------- 2 files changed, 202 insertions(+), 197 deletions(-) diff --git a/command/read.go b/command/read.go index d989178229..99bfdf5b99 100644 --- a/command/read.go +++ b/command/read.go @@ -1,109 +1,96 @@ package command import ( - "flag" "fmt" "strings" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" "github.com/posener/complete" ) -// ReadCommand is a Command that reads data from the Vault. +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*ReadCommand)(nil) +var _ cli.CommandAutocomplete = (*ReadCommand)(nil) + +// ReadCommand is a command that reads data from the Vault. type ReadCommand struct { - meta.Meta -} - -func (c *ReadCommand) Run(args []string) int { - var format string - var field string - var err error - var secret *api.Secret - var flags *flag.FlagSet - flags = c.Meta.FlagSet("read", meta.FlagSetDefault) - flags.StringVar(&format, "format", "table", "") - flags.StringVar(&field, "field", "", "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - args = flags.Args() - if len(args) != 1 || len(args[0]) == 0 { - c.Ui.Error("read expects one argument") - flags.Usage() - return 1 - } - - path := args[0] - if path[0] == '/' { - path = path[1:] - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - secret, err = client.Logical().Read(path) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error reading %s: %s", path, err)) - return 1 - } - if secret == nil { - c.Ui.Error(fmt.Sprintf( - "No value found at %s", path)) - return 1 - } - - // Handle single field output - if field != "" { - return PrintRawField(c.Ui, secret, field) - } - - return OutputSecret(c.Ui, format, secret) + *BaseCommand } func (c *ReadCommand) Synopsis() string { - return "Read data or secrets from Vault" + return "Reads data and retrieves secrets" } func (c *ReadCommand) Help() string { helpText := ` -Usage: vault read [options] path +Usage: vault read [options] PATH - Read data from Vault. + Reads data from Vault at the given path. This can be used to read secrets, + generate dynamic credentials, get configuration details, and more. - Reads data at the given path from Vault. This can be used to read - secrets and configuration as well as generate dynamic values from - materialized backends. Please reference the documentation for the - backends in use to determine key structure. + Read a secret from the static secret backend: -General Options: -` + meta.GeneralOptionsUsage() + ` -Read Options: + $ vault read secret/my-secret - -format=table The format for output. By default it is a whitespace- - delimited table. This can also be json or yaml. + For a full list of examples and paths, please see the documentation that + corresponds to the secret backend in use. - -field=field If included, the raw value of the specified field - will be output raw to stdout. +` + c.Flags().Help() -` return strings.TrimSpace(helpText) } +func (c *ReadCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) +} + func (c *ReadCommand) AutocompleteArgs() complete.Predictor { - return complete.PredictNothing + return c.PredictVaultFiles() } func (c *ReadCommand) AutocompleteFlags() complete.Flags { - return complete.Flags{ - "-format": predictFormat, - "-field": complete.PredictNothing, - } + return c.Flags().Completions() +} + +func (c *ReadCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + path, kvs, err := extractPath(args) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + if len(kvs) > 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 + } + + secret, err := client.Logical().Read(path) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err)) + return 2 + } + if secret == nil { + c.UI.Error(fmt.Sprintf("No value found at %s", path)) + return 2 + } + + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + + return OutputSecret(c.UI, c.flagFormat, secret) } diff --git a/command/read_test.go b/command/read_test.go index 5cf0f08c3e..b880d96ba6 100644 --- a/command/read_test.go +++ b/command/read_test.go @@ -1,137 +1,155 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestRead(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testReadCommand(tb testing.TB) (*cli.MockUi, *ReadCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &ReadCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &ReadCommand{ + BaseCommand: &BaseCommand{ + UI: ui, }, } - - args := []string{ - "-address", addr, - "sys/mounts", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } } -func TestRead_notFound(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestReadCommand_Run(t *testing.T) { + t.Parallel() - ui := new(cli.MockUi) - c := &ReadCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty", + nil, + "Missing PATH!", + 1, + }, + { + "slash", + []string{"/"}, + "Missing PATH!", + 1, + }, + { + "not_found", + []string{"nope/not/once/never"}, + "", + 2, + }, + { + "default", + []string{"secret/read/foo"}, + "foo", + 0, + }, + { + "field", + []string{ + "-field", "foo", + "secret/read/foo", + }, + "bar", + 0, + }, + { + "field_not_found", + []string{ + "-field", "not-a-real-field", + "secret/read/foo", + }, + "not present in secret", + 1, + }, + { + "format", + []string{ + "-format", "json", + "secret/read/foo", + }, + "{", + 0, + }, + { + "format_bad", + []string{ + "-format", "nope-not-real", + "secret/read/foo", + }, + "Invalid output format", + 1, }, } - args := []string{ - "-address", addr, - "secret/nope", - } - if code := c.Run(args); code != 1 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} - -func TestRead_field(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &ReadCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - "-field", "value", - "secret/foo", - } - - // Run once so the client is setup, ignore errors - c.Run(args) - - // Get the client so we can write data - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - - data := map[string]interface{}{"value": "bar"} - if _, err := client.Logical().Write("secret/foo", data); err != nil { - t.Fatalf("err: %s", err) - } - - // Run the read - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - output := ui.OutputWriter.String() - if output != "bar\n" { - t.Fatalf("unexpectd output:\n%s", output) - } -} - -func TestRead_field_notFound(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &ReadCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - "-field", "nope", - "secret/foo", - } - - // Run once so the client is setup, ignore errors - c.Run(args) - - // Get the client so we can write data - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - - data := map[string]interface{}{"value": "bar"} - if _, err := client.Logical().Write("secret/foo", data); err != nil { - t.Fatalf("err: %s", err) - } - - // Run the read - if code := c.Run(args); code != 1 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + 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() + + if _, err := client.Logical().Write("secret/read/foo", map[string]interface{}{ + "foo": "bar", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testReadCommand(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 := testReadCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/foo", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error reading secret/foo: " + 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 := testReadCommand(t) + assertNoTabs(t, cmd) + }) }