Update write command

This commit is contained in:
Seth Vargo 2017-09-05 00:05:47 -04:00
parent f7c9fe6d20
commit f7782df97e
No known key found for this signature in database
GPG Key ID: C921994F9C27E0FF
2 changed files with 331 additions and 360 deletions

View File

@ -6,149 +6,145 @@ import (
"os"
"strings"
"github.com/hashicorp/vault/helper/kv-builder"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// Ensure we are implementing the right interfaces.
var _ cli.Command = (*WriteCommand)(nil)
var _ cli.CommandAutocomplete = (*WriteCommand)(nil)
// WriteCommand is a Command that puts data into the Vault.
type WriteCommand struct {
meta.Meta
*BaseCommand
// The fields below can be overwritten for tests
testStdin io.Reader
flagForce bool
testStdin io.Reader // for tests
}
func (c *WriteCommand) Synopsis() string {
return "Writes data, configuration, and secrets"
}
func (c *WriteCommand) Help() string {
helpText := `
Usage: vault write [options] PATH [DATA K=V...]
Writes data to Vault at the given path. The data can be credentials, secrets,
configuration, or arbitrary data. The specific behavior of this command is
determined at the backend mounted at the path.
Data is specified as "key=value" pairs. If the value begins with an "@", then
it is loaded from a file. If the value is "-", Vault will read the value from
stdin.
Persist data in the static secret backend:
$ vault write secret/my-secret foo=bar
Create a new encryption key in the transit backend:
$ vault write -f transit/keys/my-key
Upload an AWS IAM policy from a file on disk:
$ vault write aws/roles/ops policy=@policy.json
Configure access to Consul by providing an access token:
$ echo $MY_TOKEN | vault write consul/config/access token=-
For a full list of examples and paths, please see the documentation that
corresponds to the secret backend in use.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *WriteCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
f.BoolVar(&BoolVar{
Name: "force",
Aliases: []string{"f"},
Target: &c.flagForce,
Default: false,
EnvVar: "",
Completion: complete.PredictNothing,
Usage: "Allow the operation to continue with no key=value pairs. This " +
"allows writing to keys that do not need or expect data.",
})
return set
}
func (c *WriteCommand) AutocompleteArgs() complete.Predictor {
// Return an anything predictor here. Without a way to access help
// information, we don't know what paths we could write to.
return complete.PredictAnything
}
func (c *WriteCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *WriteCommand) Run(args []string) int {
var field, format string
var force bool
flags := c.Meta.FlagSet("write", meta.FlagSetDefault)
flags.StringVar(&format, "format", "table", "")
flags.StringVar(&field, "field", "", "")
flags.BoolVar(&force, "force", false, "")
flags.BoolVar(&force, "f", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = flags.Args()
if len(args) < 1 {
c.Ui.Error("write requires a path")
flags.Usage()
return 1
}
if len(args) < 2 && !force {
c.Ui.Error("write expects at least two arguments; use -f to perform the write anyways")
flags.Usage()
return 1
}
path := args[0]
if path[0] == '/' {
path = path[1:]
}
data, err := c.parseData(args[1:])
path, kvs, err := extractPath(f.Args())
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error loading data: %s", err))
c.UI.Error(err.Error())
return 1
}
if len(kvs) == 0 && !c.flagForce {
c.UI.Error("Missing DATA! Specify at least one K=V pair or use -force.")
return 1
}
// Pull our fake stdin if needed
stdin := (io.Reader)(os.Stdin)
if c.testStdin != nil {
stdin = c.testStdin
}
data, err := parseArgsData(stdin, kvs)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err))
return 1
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
c.UI.Error(err.Error())
return 2
}
secret, err := client.Logical().Write(path, data)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error writing data to %s: %s", path, err))
return 1
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
return 2
}
if secret == nil {
// Don't output anything if people aren't using the "human" output
if format == "table" {
c.Ui.Output(fmt.Sprintf("Success! Data written to: %s", path))
// Don't output anything unless using the "table" format
if c.flagFormat == "table" {
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path))
}
return 0
}
// Handle single field output
if field != "" {
return PrintRawField(c.Ui, secret, field)
if c.flagField != "" {
return PrintRawField(c.UI, secret, c.flagField)
}
return OutputSecret(c.Ui, format, secret)
}
func (c *WriteCommand) parseData(args []string) (map[string]interface{}, error) {
var stdin io.Reader = os.Stdin
if c.testStdin != nil {
stdin = c.testStdin
}
builder := &kvbuilder.Builder{Stdin: stdin}
if err := builder.Add(args...); err != nil {
return nil, err
}
return builder.Map(), nil
}
func (c *WriteCommand) Synopsis() string {
return "Write secrets or configuration into Vault"
}
func (c *WriteCommand) Help() string {
helpText := `
Usage: vault write [options] path [data]
Write data (secrets or configuration) into Vault.
Write sends data into Vault at the given path. The behavior of the write is
determined by the backend at the given path. For example, writing to
"aws/policy/ops" will create an "ops" IAM policy for the AWS backend
(configuration), but writing to "consul/foo" will write a value directly into
Consul at that key. Check the documentation of the logical backend you're
using for more information on key structure.
Data is sent via additional arguments in "key=value" pairs. If value begins
with an "@", then it is loaded from a file. Write expects data in the file to
be in JSON format. If you want to start the value with a literal "@", then
prefix the "@" with a slash: "\@".
General Options:
` + meta.GeneralOptionsUsage() + `
Write Options:
-f | -force Force the write to continue without any data values
specified. This allows writing to keys that do not
need or expect any fields to be specified.
-format=table The format for output. By default it is a whitespace-
delimited table. This can also be json or yaml.
-field=field If included, the raw value of the specified field
will be output raw to stdout.
`
return strings.TrimSpace(helpText)
}
func (c *WriteCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (c *WriteCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{
"-force": complete.PredictNothing,
"-format": predictFormat,
"-field": complete.PredictNothing,
}
return OutputSecret(c.UI, c.flagFormat, secret)
}

View File

@ -2,271 +2,246 @@ package command
import (
"io"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestWrite(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func testWriteCommand(tb testing.TB) (*cli.MockUi, *WriteCommand) {
tb.Helper()
ui := new(cli.MockUi)
c := &WriteCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
ui := cli.NewMockUi()
return ui, &WriteCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
args := []string{
"-address", addr,
"secret/foo",
"value=bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
resp, err := client.Logical().Read("secret/foo")
if err != nil {
t.Fatalf("err: %s", err)
}
if resp.Data["value"] != "bar" {
t.Fatalf("bad: %#v", resp)
}
}
func TestWrite_arbitrary(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func TestWriteCommand_Run(t *testing.T) {
t.Parallel()
stdinR, stdinW := io.Pipe()
ui := new(cli.MockUi)
c := &WriteCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
cases := []struct {
name string
args []string
out string
code int
}{
{
"empty_path",
nil,
"Missing PATH!",
1,
},
testStdin: stdinR,
}
go func() {
stdinW.Write([]byte(`{"foo":"bar"}`))
stdinW.Close()
}()
args := []string{
"-address", addr,
"secret/foo",
"-",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
resp, err := client.Logical().Read("secret/foo")
if err != nil {
t.Fatalf("err: %s", err)
}
if resp.Data["foo"] != "bar" {
t.Fatalf("bad: %#v", resp)
}
}
func TestWrite_escaped(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &WriteCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
{
"empty_kvs",
[]string{"secret/write/foo"},
"Missing DATA!",
1,
},
{
"force_kvs",
[]string{"-force", "auth/token/create"},
"token",
0,
},
{
"force_f_kvs",
[]string{"-f", "auth/token/create"},
"token",
0,
},
{
"kvs_no_value",
[]string{"secret/write/foo", "foo"},
"Failed to parse K=V data",
1,
},
{
"single_value",
[]string{"secret/write/foo", "foo=bar"},
"Success!",
0,
},
{
"multi_value",
[]string{"secret/write/foo", "foo=bar", "zip=zap"},
"Success!",
0,
},
{
"field",
[]string{
"-field", "token_renewable",
"auth/token/create", "display_name=foo",
},
"false",
0,
},
{
"field_not_found",
[]string{
"-field", "not-a-real-field",
"auth/token/create", "display_name=foo",
},
"not present in secret",
1,
},
}
args := []string{
"-address", addr,
"secret/foo",
"value=\\@bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testWriteCommand(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)
}
})
}
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
t.Run("stdin_full", func(t *testing.T) {
t.Parallel()
resp, err := client.Logical().Read("secret/foo")
if err != nil {
t.Fatalf("err: %s", err)
}
client, closer := testVaultServer(t)
defer closer()
if resp.Data["value"] != "@bar" {
t.Fatalf("bad: %#v", resp)
}
}
func TestWrite_file(t *testing.T) {
tf, err := ioutil.TempFile("", "vault")
if err != nil {
t.Fatalf("err: %s", err)
}
tf.Write([]byte(`{"foo":"bar"}`))
tf.Close()
defer os.Remove(tf.Name())
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &WriteCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
"secret/foo",
"@" + tf.Name(),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
resp, err := client.Logical().Read("secret/foo")
if err != nil {
t.Fatalf("err: %s", err)
}
if resp.Data["foo"] != "bar" {
t.Fatalf("bad: %#v", resp)
}
}
func TestWrite_fileValue(t *testing.T) {
tf, err := ioutil.TempFile("", "vault")
if err != nil {
t.Fatalf("err: %s", err)
}
tf.Write([]byte("foo"))
tf.Close()
defer os.Remove(tf.Name())
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &WriteCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
"secret/foo",
"value=@" + tf.Name(),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
resp, err := client.Logical().Read("secret/foo")
if err != nil {
t.Fatalf("err: %s", err)
}
if resp.Data["value"] != "foo" {
t.Fatalf("bad: %#v", resp)
}
}
func TestWrite_Output(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &WriteCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
"auth/token/create",
"display_name=foo",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !strings.Contains(ui.OutputWriter.String(), "Key") {
t.Fatalf("bad: %s", ui.OutputWriter.String())
}
}
func TestWrite_force(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &WriteCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
"-force",
"sys/rotate",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte(`{"foo":"bar"}`))
stdinW.Close()
}()
_, cmd := testWriteCommand(t)
cmd.client = client
cmd.testStdin = stdinR
code := cmd.Run([]string{
"secret/write/stdin_full", "-",
})
if code != 0 {
t.Fatalf("expected 0 to be %d", code)
}
secret, err := client.Logical().Read("secret/write/stdin_full")
if err != nil {
t.Fatal(err)
}
if secret == nil || secret.Data == nil {
t.Fatal("expected secret to have data")
}
if exp, act := "bar", secret.Data["foo"].(string); exp != act {
t.Errorf("expected %q to be %q", act, exp)
}
})
t.Run("stdin_value", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte("bar"))
stdinW.Close()
}()
_, cmd := testWriteCommand(t)
cmd.client = client
cmd.testStdin = stdinR
code := cmd.Run([]string{
"secret/write/stdin_value", "foo=-",
})
if code != 0 {
t.Fatalf("expected 0 to be %d", code)
}
secret, err := client.Logical().Read("secret/write/stdin_value")
if err != nil {
t.Fatal(err)
}
if secret == nil || secret.Data == nil {
t.Fatal("expected secret to have data")
}
if exp, act := "bar", secret.Data["foo"].(string); exp != act {
t.Errorf("expected %q to be %q", act, exp)
}
})
t.Run("integration", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
_, cmd := testWriteCommand(t)
cmd.client = client
code := cmd.Run([]string{
"secret/write/integration", "foo=bar", "zip=zap",
})
if code != 0 {
t.Fatalf("expected 0 to be %d", code)
}
secret, err := client.Logical().Read("secret/write/integration")
if err != nil {
t.Fatal(err)
}
if secret == nil || secret.Data == nil {
t.Fatal("expected secret to have data")
}
if exp, act := "bar", secret.Data["foo"].(string); exp != act {
t.Errorf("expected %q to be %q", act, exp)
}
if exp, act := "zap", secret.Data["zip"].(string); exp != act {
t.Errorf("expected %q to be %q", act, exp)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testWriteCommand(t)
cmd.client = client
code := cmd.Run([]string{
"foo/bar", "a=b",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error writing data to foo/bar: "
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 := testWriteCommand(t)
assertNoTabs(t, cmd)
})
}