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 7 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 env var.
```
20 changes: 16 additions & 4 deletions command/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type BaseCommand struct {

flagFormat string
flagField string
flagDetailed bool
flagOutputCurlString bool
flagOutputPolicy bool
flagNonInteractive bool
Expand Down Expand Up @@ -304,6 +305,7 @@ const (
FlagSetHTTP
FlagSetOutputField
FlagSetOutputFormat
FlagSetOutputDetailed
)

// flagSet creates the flags for this command. The result is cached on the
Expand Down Expand Up @@ -496,11 +498,11 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets {

}

if bit&(FlagSetOutputField|FlagSetOutputFormat) != 0 {
f := set.NewFlagSet("Output Options")
if bit&(FlagSetOutputField|FlagSetOutputFormat|FlagSetOutputDetailed) != 0 {
outputSet := set.NewFlagSet("Output Options")

if bit&FlagSetOutputField != 0 {
f.StringVar(&StringVar{
outputSet.StringVar(&StringVar{
Name: "field",
Target: &c.flagField,
Default: "",
Expand All @@ -513,7 +515,7 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets {
}

if bit&FlagSetOutputFormat != 0 {
f.StringVar(&StringVar{
outputSet.StringVar(&StringVar{
Name: "format",
Target: &c.flagFormat,
Default: "table",
Expand All @@ -523,6 +525,16 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets {
are "table", "json", "yaml", or "pretty".`,
})
}

if bit&FlagSetOutputDetailed != 0 {
outputSet.BoolVar(&BoolVar{
Name: "detailed",
Target: &c.flagDetailed,
Default: false,
EnvVar: EnvVaultDetailed,
Usage: "Enables additional metadata during some operations",
})
}
}

c.flags = set
Expand Down
2 changes: 2 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ const (
// EnvVaultLicensePath is an env var used in Vault Enterprise to provide a
// path to a license file on disk
EnvVaultLicensePath = "VAULT_LICENSE_PATH"
// EnvVaultDetailed is to output detailed information (e.g., ListResponseWithInfo).
EnvVaultDetailed = `VAULT_DETAILED`

// DisableSSCTokens is an env var used to disable index bearing
// token functionality
Expand Down
99 changes: 96 additions & 3 deletions command/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ func Format(ui cli.Ui) string {
return format
}

func Detailed(ui cli.Ui) bool {
switch ui := ui.(type) {
case *VaultUI:
return ui.detailed
}

return false
}

// An output formatter for json output of an object
type JsonFormatter struct{}

Expand All @@ -98,6 +107,20 @@ func (j JsonFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) e
if err != nil {
return err
}

if secret != nil {
shouldListWithInfo := Detailed(ui)

// 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 +343,15 @@ 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 {
shouldListWithInfo := Detailed(ui)
if additional, ok := secret.Data["key_info"]; shouldListWithInfo && ok && len(additional.(map[string]interface{})) > 0 {
additionalInfo = additional.(map[string]interface{})
}
}

switch data := data.(type) {
case []interface{}:
case []string:
Expand All @@ -342,10 +374,71 @@ 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 {
seenHeaders := make(map[string]bool)
for key, rawValues := range additionalInfo {
// Most endpoints use the well-behaved ListResponseWithInfo.
// However, some use a hand-rolled equivalent, where the
// returned "keys" doesn't match the key of the "key_info"
// member (namely, /sys/policies/egp). We seek to exclude
// headers only visible from "non-visitable" key_info rows,
// to make table output less confusing. These non-visitable
// rows will still be visible in the JSON output.
index := sort.SearchStrings(keys, key)
if index < len(keys) && keys[index] != key {
continue
}

values := rawValues.(map[string]interface{})
for key := range values {
seenHeaders[key] = true
}
}

for key := range seenHeaders {
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
3 changes: 2 additions & 1 deletion command/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ Usage: vault list [options] PATH
}

func (c *ListCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat | FlagSetOutputDetailed)
return set
}

func (c *ListCommand) AutocompleteArgs() complete.Predictor {
Expand Down
47 changes: 42 additions & 5 deletions command/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"os"
"sort"
"strconv"
"strings"
"text/tabwriter"

Expand All @@ -19,13 +20,16 @@ import (

type VaultUI struct {
cli.Ui
format string
format string
detailed bool
}

// setupEnv parses args and may replace them and sets some env vars to known
// values based on format options
func setupEnv(args []string) (retArgs []string, format string, outputCurlString bool, outputPolicy bool) {
func setupEnv(args []string) (retArgs []string, format string, detailed bool, outputCurlString bool, outputPolicy bool) {
var err error
var nextArgFormat bool
var haveDetailed bool

for _, arg := range args {
if nextArgFormat {
Expand Down Expand Up @@ -64,6 +68,28 @@ func setupEnv(args []string) (retArgs []string, format string, outputCurlString
if arg == "-format" || arg == "--format" {
nextArgFormat = true
}

// Parse a given flag here, which overrides the env var
if strings.HasPrefix(arg, "--detailed=") {
detailed, err = strconv.ParseBool(strings.TrimPrefix(arg, "--detailed="))
if err != nil {
detailed = false
}
haveDetailed = true
}
if strings.HasPrefix(arg, "-detailed=") {
detailed, err = strconv.ParseBool(strings.TrimPrefix(arg, "-detailed="))
if err != nil {
detailed = false
}
haveDetailed = true
}
// For backwards compat, it could be specified without an equal sign to enable
// detailed output.
if arg == "-detailed" || arg == "--detailed" {
detailed = true
haveDetailed = true
}
}

envVaultFormat := os.Getenv(EnvVaultFormat)
Expand All @@ -77,7 +103,16 @@ func setupEnv(args []string) (retArgs []string, format string, outputCurlString
format = "table"
}

return args, format, outputCurlString, outputPolicy
envVaultDetailed := os.Getenv(EnvVaultDetailed)
// If we did not parse a value, fetch the env var
if !haveDetailed && envVaultDetailed != "" {
detailed, err = strconv.ParseBool(envVaultDetailed)
if err != nil {
detailed = false
}
}

return args, format, detailed, outputCurlString, outputPolicy
}

type RunOptions struct {
Expand All @@ -100,9 +135,10 @@ func RunCustom(args []string, runOpts *RunOptions) int {
}

var format string
var detailed bool
var outputCurlString bool
var outputPolicy bool
args, format, outputCurlString, outputPolicy = setupEnv(args)
args, format, detailed, outputCurlString, outputPolicy = setupEnv(args)

// Don't use color if disabled
useColor := true
Expand Down Expand Up @@ -145,7 +181,8 @@ func RunCustom(args []string, runOpts *RunOptions) int {
ErrorWriter: uiErrWriter,
},
},
format: format,
format: format,
detailed: detailed,
}

serverCmdUi := &VaultUI{
Expand Down