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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
|
@ -18,6 +19,7 @@ var (
|
||||||
|
|
||||||
type KVListCommand struct {
|
type KVListCommand struct {
|
||||||
*BaseCommand
|
*BaseCommand
|
||||||
|
flagMount string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *KVListCommand) Synopsis() string {
|
func (c *KVListCommand) Synopsis() string {
|
||||||
|
@ -43,7 +45,23 @@ Usage: vault kv list [options] PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *KVListCommand) Flags() *FlagSets {
|
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 {
|
func (c *KVListCommand) AutocompleteArgs() complete.Predictor {
|
||||||
|
@ -65,8 +83,11 @@ func (c *KVListCommand) Run(args []string) int {
|
||||||
args = f.Args()
|
args = f.Args()
|
||||||
switch {
|
switch {
|
||||||
case len(args) < 1:
|
case len(args) < 1:
|
||||||
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
|
if c.flagMount == "" {
|
||||||
return 1
|
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
args = []string{""}
|
||||||
case len(args) > 1:
|
case len(args) > 1:
|
||||||
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
|
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
|
||||||
return 1
|
return 1
|
||||||
|
@ -78,25 +99,56 @@ func (c *KVListCommand) Run(args []string) int {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize path
|
// If true, we're working with "-mount=secret foo" syntax.
|
||||||
path := sanitizePath(args[0])
|
// If false, we're using "secret/foo" syntax.
|
||||||
mountPath, v2, err := isKVv2(path, client)
|
mountFlagSyntax := c.flagMount != ""
|
||||||
if err != nil {
|
|
||||||
c.UI.Error(err.Error())
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
if v2 {
|
var (
|
||||||
path = addPrefixToKVPath(path, mountPath, "metadata")
|
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 {
|
||||||
|
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 {
|
if err != nil {
|
||||||
c.UI.Error(err.Error())
|
c.UI.Error(err.Error())
|
||||||
return 2
|
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 {
|
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
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,12 +166,12 @@ func (c *KVListCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
if secret == nil || secret.Data == nil {
|
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
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
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
|
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) {
|
func testKVMetadataGetCommand(tb testing.TB) (*cli.MockUi, *KVMetadataGetCommand) {
|
||||||
tb.Helper()
|
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":
|
List values under the key "my-app":
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ vault kv list secret/my-app/
|
$ vault kv list -mount=secret my-app/
|
||||||
Keys
|
Keys
|
||||||
----
|
----
|
||||||
admin_creds
|
admin_creds
|
||||||
|
@ -38,6 +38,12 @@ included on all commands.
|
||||||
|
|
||||||
### Output Options
|
### 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
|
- `-format` `(string: "table")` - Print the output in the given format. Valid
|
||||||
formats are "table", "json", or "yaml". This can also be specified via the
|
formats are "table", "json", or "yaml". This can also be specified via the
|
||||||
`VAULT_FORMAT` environment variable.
|
`VAULT_FORMAT` environment variable.
|
||||||
|
|
Loading…
Reference in New Issue