Add `-mount` flag to kv list command (#19378)
* add flag Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * handle kv paths Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * scaffold test Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * need metadata for list paths Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * add (broken) test Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * fix test Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * update docs Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * add changelog Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * format Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * add godoc Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * add test case for mount only Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * handle case of no unnamed arg Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * add non-mount behavior Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * add more detail to comment Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> * add v1 tests Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com> --------- Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>
This commit is contained in:
parent
c581f90c05
commit
058710d33d
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
cli/kv: add -mount flag to kv list
|
||||
```
|
|
@ -5,6 +5,7 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
|
@ -18,6 +19,7 @@ var (
|
|||
|
||||
type KVListCommand struct {
|
||||
*BaseCommand
|
||||
flagMount string
|
||||
}
|
||||
|
||||
func (c *KVListCommand) Synopsis() string {
|
||||
|
@ -43,7 +45,23 @@ Usage: vault kv list [options] PATH
|
|||
}
|
||||
|
||||
func (c *KVListCommand) Flags() *FlagSets {
|
||||
return c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
|
||||
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
|
||||
|
||||
// Common Options
|
||||
f := set.NewFlagSet("Common Options")
|
||||
|
||||
f.StringVar(&StringVar{
|
||||
Name: "mount",
|
||||
Target: &c.flagMount,
|
||||
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
|
||||
Usage: `Specifies the path where the KV backend is mounted. If specified,
|
||||
the next argument will be interpreted as the secret path. If this flag is
|
||||
not specified, the next argument will be interpreted as the combined mount
|
||||
path and secret path, with /data/ automatically appended between KV
|
||||
v2 secrets.`,
|
||||
})
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *KVListCommand) AutocompleteArgs() complete.Predictor {
|
||||
|
@ -65,8 +83,11 @@ func (c *KVListCommand) Run(args []string) int {
|
|||
args = f.Args()
|
||||
switch {
|
||||
case len(args) < 1:
|
||||
if c.flagMount == "" {
|
||||
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
|
||||
return 1
|
||||
}
|
||||
args = []string{""}
|
||||
case len(args) > 1:
|
||||
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
|
||||
return 1
|
||||
|
@ -78,25 +99,56 @@ func (c *KVListCommand) Run(args []string) int {
|
|||
return 2
|
||||
}
|
||||
|
||||
// Sanitize path
|
||||
path := sanitizePath(args[0])
|
||||
mountPath, v2, err := isKVv2(path, client)
|
||||
// If true, we're working with "-mount=secret foo" syntax.
|
||||
// If false, we're using "secret/foo" syntax.
|
||||
mountFlagSyntax := c.flagMount != ""
|
||||
|
||||
var (
|
||||
mountPath string
|
||||
partialPath string
|
||||
v2 bool
|
||||
)
|
||||
|
||||
// Parse the paths and grab the KV version
|
||||
if mountFlagSyntax {
|
||||
// In this case, this arg is the secret path (e.g. "foo").
|
||||
partialPath = sanitizePath(args[0])
|
||||
mountPath, v2, err = isKVv2(sanitizePath(c.flagMount), client)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
if v2 {
|
||||
path = addPrefixToKVPath(path, mountPath, "metadata")
|
||||
partialPath = path.Join(mountPath, partialPath)
|
||||
}
|
||||
} else {
|
||||
// In this case, this arg is a path-like combination of mountPath/secretPath.
|
||||
// (e.g. "secret/foo")
|
||||
partialPath = sanitizePath(args[0])
|
||||
mountPath, v2, err = isKVv2(partialPath, client)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
secret, err := client.Logical().List(path)
|
||||
// Add /metadata to v2 paths only
|
||||
var fullPath string
|
||||
if v2 {
|
||||
fullPath = addPrefixToKVPath(partialPath, mountPath, "metadata")
|
||||
} else {
|
||||
// v1
|
||||
if mountFlagSyntax {
|
||||
fullPath = path.Join(mountPath, partialPath)
|
||||
} else {
|
||||
fullPath = partialPath
|
||||
}
|
||||
}
|
||||
|
||||
secret, err := client.Logical().List(fullPath)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error listing %s: %s", path, err))
|
||||
c.UI.Error(fmt.Sprintf("Error listing %s: %s", fullPath, err))
|
||||
return 2
|
||||
}
|
||||
|
||||
|
@ -114,12 +166,12 @@ func (c *KVListCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
if secret == nil || secret.Data == nil {
|
||||
c.UI.Error(fmt.Sprintf("No value found at %s", path))
|
||||
c.UI.Error(fmt.Sprintf("No value found at %s", fullPath))
|
||||
return 2
|
||||
}
|
||||
|
||||
if !ok {
|
||||
c.UI.Error(fmt.Sprintf("No entries found at %s", path))
|
||||
c.UI.Error(fmt.Sprintf("No entries found at %s", fullPath))
|
||||
return 2
|
||||
}
|
||||
|
||||
|
|
|
@ -593,6 +593,131 @@ func TestKVGetCommand(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func testKVListCommand(tb testing.TB) (*cli.MockUi, *KVListCommand) {
|
||||
tb.Helper()
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &KVListCommand{
|
||||
BaseCommand: &BaseCommand{
|
||||
UI: ui,
|
||||
},
|
||||
}
|
||||
|
||||
return ui, cmd
|
||||
}
|
||||
|
||||
// TestKVListCommand runs tests for `vault kv list`
|
||||
func TestKVListCommand(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
outStrings []string
|
||||
code int
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
args: []string{"kv/my-prefix"},
|
||||
outStrings: []string{"secret-0", "secret-1", "secret-2"},
|
||||
code: 0,
|
||||
},
|
||||
{
|
||||
name: "not_enough_args",
|
||||
args: []string{},
|
||||
outStrings: []string{"Not enough arguments"},
|
||||
code: 1,
|
||||
},
|
||||
{
|
||||
name: "v2_default_with_mount",
|
||||
args: []string{"-mount", "kv", "my-prefix"},
|
||||
outStrings: []string{"secret-0", "secret-1", "secret-2"},
|
||||
code: 0,
|
||||
},
|
||||
{
|
||||
name: "v1_default_with_mount",
|
||||
args: []string{"kv/my-prefix"},
|
||||
outStrings: []string{"secret-0", "secret-1", "secret-2"},
|
||||
code: 0,
|
||||
},
|
||||
{
|
||||
name: "v2_not_found",
|
||||
args: []string{"kv/nope/not/once/never"},
|
||||
outStrings: []string{"No value found at kv/metadata/nope/not/once/never"},
|
||||
code: 2,
|
||||
},
|
||||
{
|
||||
name: "v1_mount_only",
|
||||
args: []string{"kv"},
|
||||
outStrings: []string{"my-prefix"},
|
||||
code: 0,
|
||||
},
|
||||
{
|
||||
name: "v2_mount_only",
|
||||
args: []string{"-mount", "kv"},
|
||||
outStrings: []string{"my-prefix"},
|
||||
code: 0,
|
||||
},
|
||||
{
|
||||
// this is behavior that should be tested
|
||||
// `kv` here is an explicit mount
|
||||
// `my-prefix` is not
|
||||
// the current kv code will ignore `my-prefix`
|
||||
name: "ignore_multi_part_mounts",
|
||||
args: []string{"-mount", "kv/my-prefix"},
|
||||
outStrings: []string{"my-prefix"},
|
||||
code: 0,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("validations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test setup
|
||||
client, closer := testVaultServer(t)
|
||||
defer closer()
|
||||
|
||||
// enable kv-v2 backend
|
||||
if err := client.Sys().Mount("kv/", &api.MountInput{
|
||||
Type: "kv-v2",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
|
||||
ctx := context.Background()
|
||||
for i := 0; i < 3; i++ {
|
||||
path := fmt.Sprintf("my-prefix/secret-%d", i)
|
||||
_, err := client.KVv2("kv/").Put(ctx, path, map[string]interface{}{
|
||||
"foo": "bar",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
ui, cmd := testKVListCommand(t)
|
||||
cmd.client = client
|
||||
|
||||
code := cmd.Run(testCase.args)
|
||||
if code != testCase.code {
|
||||
t.Errorf("expected %d to be %d", code, testCase.code)
|
||||
}
|
||||
|
||||
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||
for _, str := range testCase.outStrings {
|
||||
if !strings.Contains(combined, str) {
|
||||
t.Errorf("expected %q to contain %q", combined, str)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testKVMetadataGetCommand(tb testing.TB) (*cli.MockUi, *KVMetadataGetCommand) {
|
||||
tb.Helper()
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ Use this command to list all existing key names at a specific path.
|
|||
List values under the key "my-app":
|
||||
|
||||
```shell-session
|
||||
$ vault kv list secret/my-app/
|
||||
$ vault kv list -mount=secret my-app/
|
||||
Keys
|
||||
----
|
||||
admin_creds
|
||||
|
@ -38,6 +38,12 @@ included on all commands.
|
|||
|
||||
### Output Options
|
||||
|
||||
- `-mount` `(string: "")` - Specifies the path where the KV backend is mounted.
|
||||
If specified, the next argument will be interpreted as the secret path. If
|
||||
this flag is not specified, the next argument will be interpreted as the
|
||||
combined mount path and secret path, with /data/ automatically inserted for
|
||||
KV v2 secrets.
|
||||
|
||||
- `-format` `(string: "table")` - Print the output in the given format. Valid
|
||||
formats are "table", "json", or "yaml". This can also be specified via the
|
||||
`VAULT_FORMAT` environment variable.
|
||||
|
|
Loading…
Reference in New Issue