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

Vault CLI: show detailed information with ListResponseWithInfo #15417

Merged
merged 8 commits into from
May 18, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions changelog/15417.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
command: Support the optional '-detailed' flag to be passed to 'vault list' command to show ListResponseWithInfo data. Also supports the VAULT_DETAILED_LISTS env var.
```
95 changes: 92 additions & 3 deletions command/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const (
// hopeDelim is the delimiter to use when splitting columns. We call it a
// hopeDelim because we hope that it's never contained in a secret.
hopeDelim = "♨"

// We hijack the secret's Data field, adding this constant in on list
// requests, to know if we should show the additional key_info from a
// ListResponseWithInfo.
doListWithInfoConstant = "vault_cli_do_list_with_info"
)

type FormatOptions struct {
Expand Down Expand Up @@ -98,6 +103,23 @@ func (j JsonFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) e
if err != nil {
return err
}

if secret != nil {
if rawShouldListWithInfo, ok := secret.Data[doListWithInfoConstant]; ok {
shouldListWithInfo := rawShouldListWithInfo.(bool)
delete(secret.Data, doListWithInfoConstant)

// Show the raw JSON of the LIST call, rather than only the
// list of keys.
if shouldListWithInfo {
b, err = j.Format(secret)
if err != nil {
return err
}
}
}
}

ui.Output(string(b))
return nil
}
Expand Down Expand Up @@ -320,6 +342,19 @@ func (t TableFormatter) OutputSealStatusStruct(ui cli.Ui, secret *api.Secret, da
func (t TableFormatter) OutputList(ui cli.Ui, secret *api.Secret, data interface{}) error {
t.printWarnings(ui, secret)

// Determine if we have additional information from a ListResponseWithInfo endpoint.
var additionalInfo map[string]interface{}
if secret != nil {
if rawShouldListWithInfo, ok := secret.Data[doListWithInfoConstant]; ok {
shouldListWithInfo := rawShouldListWithInfo.(bool)
if additional, ok := secret.Data["key_info"]; shouldListWithInfo && ok && len(additional.(map[string]interface{})) > 0 {
additionalInfo = additional.(map[string]interface{})
}

delete(secret.Data, doListWithInfoConstant)
}
}

switch data := data.(type) {
case []interface{}:
case []string:
Expand All @@ -342,10 +377,64 @@ func (t TableFormatter) OutputList(ui cli.Ui, secret *api.Secret, data interface
}
sort.Strings(keys)

// Prepend the header
keys = append([]string{"Keys"}, keys...)
// If we have a ListResponseWithInfo endpoint, we'll need to show
// additional headers. To satisfy the table outputter, we'll need
// to concat them with the deliminator.
var headers []string
header := "Keys"
if len(additionalInfo) > 0 {
for _, rawValues := range additionalInfo {
values := rawValues.(map[string]interface{})
for key := range values {
haveKey := false
for _, title := range headers {
if title == key {
haveKey = true
break
}
}

if !haveKey {
headers = append(headers, key)
}
}
}

sort.Strings(headers)
header = header + hopeDelim + strings.Join(headers, hopeDelim)
}

// Finally, if we have a ListResponseWithInfo, we'll need to update
// the returned rows to not just have the keys (in the sorted order),
// but also have the values for each header (in their sorted order).
rows := keys
if len(additionalInfo) > 0 && len(headers) > 0 {
for index, row := range rows {
formatted := []string{row}
if rawValues, ok := additionalInfo[row]; ok {
values := rawValues.(map[string]interface{})
for _, header := range headers {
if rawValue, ok := values[header]; ok {
if looksLikeDuration(header) {
rawValue = humanDurationInt(rawValue)
}

formatted = append(formatted, fmt.Sprintf("%v", rawValue))
} else {
// Show a default empty n/a when this field is
// missing from the additional information.
formatted = append(formatted, "n/a")
}
}
}

rows[index] = strings.Join(formatted, hopeDelim)
}
}

ui.Output(tableOutput(keys, &columnize.Config{
// Prepend the header to the formatted rows.
output := append([]string{header}, rows...)
ui.Output(tableOutput(output, &columnize.Config{
Delim: hopeDelim,
}))
}
Expand Down
18 changes: 17 additions & 1 deletion command/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ var (

type ListCommand struct {
*BaseCommand

flagDetailed bool
}

func (c *ListCommand) Synopsis() string {
Expand Down Expand Up @@ -42,7 +44,18 @@ Usage: vault list [options] PATH
}

func (c *ListCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
f := set.NewFlagSet("List Options")

f.BoolVar(&BoolVar{
Name: "detailed",
Target: &c.flagDetailed,
Default: false,
EnvVar: "VAULT_DETAILED_LISTS",
Usage: "Enables additional metadata during some LIST operations",
})

return set
}

func (c *ListCommand) AutocompleteArgs() complete.Predictor {
Expand Down Expand Up @@ -118,5 +131,8 @@ func (c *ListCommand) Run(args []string) int {
return 2
}

// Hijack the secret's data to show the list with information.
secret.Data[doListWithInfoConstant] = c.flagDetailed
cipherboy marked this conversation as resolved.
Show resolved Hide resolved

return OutputList(c.UI, secret)
}