Move more formatting into base_helpers

This commit is contained in:
Seth Vargo 2017-09-07 22:04:48 -04:00
parent 30cd478c01
commit b4d9d1517b
No known key found for this signature in database
GPG Key ID: C921994F9C27E0FF
3 changed files with 156 additions and 176 deletions

View File

@ -13,7 +13,6 @@ import (
"github.com/hashicorp/vault/api" "github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/token" "github.com/hashicorp/vault/command/token"
"github.com/kr/text"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/posener/complete" "github.com/posener/complete"
@ -274,22 +273,6 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets {
return c.flags return c.flags
} }
// wrapAtLengthWithPadding wraps the given text at the maxLineLength, taking
// into account any provided left padding.
func wrapAtLengthWithPadding(s string, pad int) string {
wrapped := text.Wrap(s, maxLineLength-pad)
lines := strings.Split(wrapped, "\n")
for i, line := range lines {
lines[i] = strings.Repeat(" ", pad) + line
}
return strings.Join(lines, "\n")
}
// wrapAtLength wraps the given text to maxLineLength.
func wrapAtLength(s string) string {
return wrapAtLengthWithPadding(s, 0)
}
// FlagSets is a group of flag sets. // FlagSets is a group of flag sets.
type FlagSets struct { type FlagSets struct {
flagSets []*FlagSet flagSets []*FlagSet

View File

@ -8,16 +8,13 @@ import (
"github.com/hashicorp/vault/api" "github.com/hashicorp/vault/api"
kvbuilder "github.com/hashicorp/vault/helper/kv-builder" kvbuilder "github.com/hashicorp/vault/helper/kv-builder"
"github.com/kr/text"
homedir "github.com/mitchellh/go-homedir" homedir "github.com/mitchellh/go-homedir"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/ryanuber/columnize" "github.com/ryanuber/columnize"
) )
var ErrMissingID = fmt.Errorf("Missing ID!")
var ErrMissingPath = fmt.Errorf("Missing PATH!")
var ErrMissingThing = fmt.Errorf("Missing THING!")
// extractListData reads the secret and returns a typed list of data and a // extractListData reads the secret and returns a typed list of data and a
// boolean indicating whether the extraction was successful. // boolean indicating whether the extraction was successful.
func extractListData(secret *api.Secret) ([]interface{}, bool) { func extractListData(secret *api.Secret) ([]interface{}, bool) {
@ -34,54 +31,9 @@ func extractListData(secret *api.Secret) ([]interface{}, bool) {
return i, ok return i, ok
} }
// extractPath extracts the path and list of arguments from the args. If there
// are no extra arguments, the remaining args will be nil.
func extractPath(args []string) (string, []string, error) {
str, remaining, err := extractThings(args)
if err == ErrMissingThing {
err = ErrMissingPath
}
return str, remaining, err
}
// extractID extracts the path and list of arguments from the args. If there
// are no extra arguments, the remaining args will be nil.
func extractID(args []string) (string, []string, error) {
str, remaining, err := extractThings(args)
if err == ErrMissingThing {
err = ErrMissingID
}
return str, remaining, err
}
func extractThings(args []string) (string, []string, error) {
if len(args) < 1 {
return "", nil, ErrMissingThing
}
// Path is always the first argument after all flags
thing := args[0]
// Strip leading and trailing slashes
thing = sanitizePath(thing)
// Verify we have a thing
if thing == "" {
return "", nil, ErrMissingThing
}
// Splice remaining args
var remaining []string
if len(args) > 1 {
remaining = args[1:]
}
return thing, remaining, nil
}
// sanitizePath removes any leading or trailing things from a "path". // sanitizePath removes any leading or trailing things from a "path".
func sanitizePath(s string) string { func sanitizePath(s string) string {
return ensureNoTrailingSlash(ensureNoLeadingSlash(s)) return ensureNoTrailingSlash(ensureNoLeadingSlash(strings.TrimSpace(s)))
} }
// ensureTrailingSlash ensures the given string has a trailing slash. // ensureTrailingSlash ensures the given string has a trailing slash.
@ -124,33 +76,45 @@ func ensureNoLeadingSlash(s string) string {
} }
// columnOuput prints the list of items as a table with no headers. // columnOuput prints the list of items as a table with no headers.
func columnOutput(list []string) string { func columnOutput(list []string, c *columnize.Config) string {
if len(list) == 0 { if len(list) == 0 {
return "" return ""
} }
return columnize.Format(list, &columnize.Config{ if c == nil {
Glue: " ", c = &columnize.Config{}
Empty: "n/a", }
}) if c.Glue == "" {
c.Glue = " "
}
if c.Empty == "" {
c.Empty = "n/a"
}
return columnize.Format(list, c)
} }
// tableOutput prints the list of items as columns, where the first row is // tableOutput prints the list of items as columns, where the first row is
// the list of headers. // the list of headers.
func tableOutput(list []string) string { func tableOutput(list []string, c *columnize.Config) string {
if len(list) == 0 { if len(list) == 0 {
return "" return ""
} }
delim := "|"
if c != nil && c.Delim != "" {
delim = c.Delim
}
underline := "" underline := ""
headers := strings.Split(list[0], "|") headers := strings.Split(list[0], delim)
for i, h := range headers { for i, h := range headers {
h = strings.TrimSpace(h) h = strings.TrimSpace(h)
u := strings.Repeat("-", len(h)) u := strings.Repeat("-", len(h))
underline = underline + u underline = underline + u
if i != len(headers)-1 { if i != len(headers)-1 {
underline = underline + " | " underline = underline + delim
} }
} }
@ -158,7 +122,7 @@ func tableOutput(list []string) string {
copy(list[2:], list[1:]) copy(list[2:], list[1:])
list[1] = underline list[1] = underline
return columnOutput(list) return columnOutput(list, c)
} }
// parseArgsData parses the given args in the format key=value into a map of // parseArgsData parses the given args in the format key=value into a map of
@ -207,7 +171,7 @@ func printKeyStatus(ks *api.KeyStatus) string {
return columnOutput([]string{ return columnOutput([]string{
fmt.Sprintf("Key Term | %d", ks.Term), fmt.Sprintf("Key Term | %d", ks.Term),
fmt.Sprintf("Install Time | %s", ks.InstallTime.UTC().Format(time.RFC822)), fmt.Sprintf("Install Time | %s", ks.InstallTime.UTC().Format(time.RFC822)),
}) }, nil)
} }
// expandPath takes a filepath and returns the full expanded path, accounting // expandPath takes a filepath and returns the full expanded path, accounting
@ -223,3 +187,57 @@ func expandPath(s string) string {
} }
return e return e
} }
// wrapAtLengthWithPadding wraps the given text at the maxLineLength, taking
// into account any provided left padding.
func wrapAtLengthWithPadding(s string, pad int) string {
wrapped := text.Wrap(s, maxLineLength-pad)
lines := strings.Split(wrapped, "\n")
for i, line := range lines {
lines[i] = strings.Repeat(" ", pad) + line
}
return strings.Join(lines, "\n")
}
// wrapAtLength wraps the given text to maxLineLength.
func wrapAtLength(s string) string {
return wrapAtLengthWithPadding(s, 0)
}
// ttlToAPI converts a user-supplied ttl into an API-compatible string. If
// the TTL is 0, this returns the empty string. If the TTL is negative, this
// returns "system" to indicate to use the system values. Otherwise, the
// time.Duration ttl is used.
func ttlToAPI(d time.Duration) string {
if d == 0 {
return ""
}
if d < 0 {
return "system"
}
return d.String()
}
// humanDuration prints the time duration without those pesky zeros.
func humanDuration(d time.Duration) string {
if d == 0 {
return "0s"
}
s := d.String()
if strings.HasSuffix(s, "m0s") {
s = s[:len(s)-2]
}
if idx := strings.Index(s, "h0m"); idx > 0 {
s = s[:idx+1] + s[idx+3:]
}
return s
}
// humanDurationInt prints the given int as if it were a time.Duration number
// of seconds.
func humanDurationInt(i int) string {
return humanDuration(time.Duration(i) * time.Second)
}

View File

@ -5,19 +5,19 @@ import (
"errors" "errors"
"fmt" "fmt"
"sort" "sort"
"strconv"
"strings" "strings"
"sync"
"time"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"github.com/hashicorp/vault/api" "github.com/hashicorp/vault/api"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/posener/complete"
"github.com/ryanuber/columnize" "github.com/ryanuber/columnize"
) )
var predictFormat complete.Predictor = complete.PredictSet("json", "yaml") 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 = "♨"
)
func OutputSecret(ui cli.Ui, format string, secret *api.Secret) int { func OutputSecret(ui cli.Ui, format string, secret *api.Secret) int {
return outputWithFormat(ui, format, secret, secret) return outputWithFormat(ui, format, secret, secret)
@ -89,7 +89,7 @@ type TableFormatter struct {
func (t TableFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error { func (t TableFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error {
// TODO: this should really use reflection like the other formatters do // TODO: this should really use reflection like the other formatters do
if s, ok := data.(*api.Secret); ok { if s, ok := data.(*api.Secret); ok {
return t.OutputSecret(ui, secret, s) return t.OutputSecret(ui, s)
} }
if s, ok := data.([]interface{}); ok { if s, ok := data.([]interface{}); ok {
return t.OutputList(ui, secret, s) return t.OutputList(ui, secret, s)
@ -98,132 +98,111 @@ func (t TableFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{})
} }
func (t TableFormatter) OutputList(ui cli.Ui, secret *api.Secret, list []interface{}) error { func (t TableFormatter) OutputList(ui cli.Ui, secret *api.Secret, list []interface{}) error {
config := columnize.DefaultConfig() t.printWarnings(ui, secret)
config.Delim = "♨"
config.Glue = "\t"
config.Prefix = ""
input := make([]string, 0, 5)
if len(list) > 0 { if len(list) > 0 {
input = append(input, "Keys") keys := make([]string, len(list))
input = append(input, "----") for i, v := range list {
typed, ok := v.(string)
keys := make([]string, 0, len(list)) if !ok {
for _, k := range list { return fmt.Errorf("Error: %v is not a string", v)
keys = append(keys, k.(string)) }
keys[i] = typed
} }
sort.Strings(keys) sort.Strings(keys)
for _, k := range keys { // Prepend the header
input = append(input, fmt.Sprintf("%s", k)) keys = append([]string{"Keys"}, keys...)
}
}
tableOutputStr := columnize.Format(input, config) ui.Output(tableOutput(keys, &columnize.Config{
Delim: hopeDelim,
// Print the warning separately because the length of first }))
// column in the output will be increased by the length of
// the longest warning string making the output look bad.
warningsInput := make([]string, 0, 5)
if len(secret.Warnings) != 0 {
warningsInput = append(warningsInput, "")
warningsInput = append(warningsInput, "The following warnings were returned from the Vault server:")
for _, warning := range secret.Warnings {
warningsInput = append(warningsInput, fmt.Sprintf("* %s", warning))
} }
}
warningsOutputStr := columnize.Format(warningsInput, config)
ui.Output(fmt.Sprintf("%s\n%s", tableOutputStr, warningsOutputStr))
return nil return nil
} }
func (t TableFormatter) OutputSecret(ui cli.Ui, secret, s *api.Secret) error { // printWarnings prints any warnings in the secret.
config := columnize.DefaultConfig() func (t TableFormatter) printWarnings(ui cli.Ui, secret *api.Secret) {
config.Delim = "♨" if secret != nil && len(secret.Warnings) > 0 {
config.Glue = "\t" ui.Warn("WARNING! The following warnings were returned from Vault:\n")
config.Prefix = "" for _, warning := range secret.Warnings {
ui.Warn(wrapAtLengthWithPadding(fmt.Sprintf("* %s", warning), 2))
input := make([]string, 0, 5) }
ui.Warn("")
onceHeader := &sync.Once{} }
headerFunc := func() {
input = append(input, fmt.Sprintf("Key %s Value", config.Delim))
input = append(input, fmt.Sprintf("--- %s -----", config.Delim))
} }
if s.LeaseDuration > 0 { func (t TableFormatter) OutputSecret(ui cli.Ui, secret *api.Secret) error {
onceHeader.Do(headerFunc) if secret == nil {
if s.LeaseID != "" { return nil
input = append(input, fmt.Sprintf("lease_id %s %s", config.Delim, s.LeaseID)) }
input = append(input, fmt.Sprintf(
"lease_duration %s %s", config.Delim, (time.Second*time.Duration(s.LeaseDuration)).String())) t.printWarnings(ui, secret)
out := make([]string, 0, 8)
if secret.LeaseDuration > 0 {
if secret.LeaseID != "" {
out = append(out, fmt.Sprintf("lease_id %s %s", hopeDelim, secret.LeaseID))
out = append(out, fmt.Sprintf("lease_duration %s %s", hopeDelim, humanDurationInt(secret.LeaseDuration)))
out = append(out, fmt.Sprintf("lease_renewable %s %t", hopeDelim, secret.Renewable))
} else { } else {
input = append(input, fmt.Sprintf( // This is probably the generic secret backend which has leases, but we
"refresh_interval %s %s", config.Delim, (time.Second*time.Duration(s.LeaseDuration)).String())) // print them as refresh_interval to reduce confusion.
} out = append(out, fmt.Sprintf("refresh_interval %s %s", hopeDelim, humanDurationInt(secret.LeaseDuration)))
if s.LeaseID != "" {
input = append(input, fmt.Sprintf(
"lease_renewable %s %s", config.Delim, strconv.FormatBool(s.Renewable)))
} }
} }
if s.Auth != nil { if secret.Auth != nil {
onceHeader.Do(headerFunc) out = append(out, fmt.Sprintf("token %s %s", hopeDelim, secret.Auth.ClientToken))
input = append(input, fmt.Sprintf("token %s %s", config.Delim, s.Auth.ClientToken)) out = append(out, fmt.Sprintf("token_accessor %s %s", hopeDelim, secret.Auth.Accessor))
input = append(input, fmt.Sprintf("token_accessor %s %s", config.Delim, s.Auth.Accessor)) // If the lease duration is 0, it's likely a root token, so output the
input = append(input, fmt.Sprintf("token_duration %s %s", config.Delim, (time.Second*time.Duration(s.Auth.LeaseDuration)).String())) // duration as "infinity" to clear things up.
input = append(input, fmt.Sprintf("token_renewable %s %v", config.Delim, s.Auth.Renewable)) if secret.Auth.LeaseDuration == 0 {
input = append(input, fmt.Sprintf("token_policies %s %v", config.Delim, s.Auth.Policies)) out = append(out, fmt.Sprintf("token_duration %s %s", hopeDelim, "∞"))
for k, v := range s.Auth.Metadata { } else {
input = append(input, fmt.Sprintf("token_meta_%s %s %#v", k, config.Delim, v)) out = append(out, fmt.Sprintf("token_duration %s %s", hopeDelim, humanDurationInt(secret.Auth.LeaseDuration)))
}
out = append(out, fmt.Sprintf("token_renewable %s %t", hopeDelim, secret.Auth.Renewable))
out = append(out, fmt.Sprintf("token_policies %s %v", hopeDelim, secret.Auth.Policies))
for k, v := range secret.Auth.Metadata {
out = append(out, fmt.Sprintf("token_meta_%s %s %v", k, hopeDelim, v))
} }
} }
if s.WrapInfo != nil { if secret.WrapInfo != nil {
onceHeader.Do(headerFunc) out = append(out, fmt.Sprintf("wrapping_token: %s %s", hopeDelim, secret.WrapInfo.Token))
input = append(input, fmt.Sprintf("wrapping_token: %s %s", config.Delim, s.WrapInfo.Token)) out = append(out, fmt.Sprintf("wrapping_token_ttl: %s %s", hopeDelim, humanDurationInt(secret.WrapInfo.TTL)))
input = append(input, fmt.Sprintf("wrapping_token_ttl: %s %s", config.Delim, (time.Second*time.Duration(s.WrapInfo.TTL)).String())) out = append(out, fmt.Sprintf("wrapping_token_creation_time: %s %s", hopeDelim, secret.WrapInfo.CreationTime.String()))
input = append(input, fmt.Sprintf("wrapping_token_creation_time: %s %s", config.Delim, s.WrapInfo.CreationTime.String())) out = append(out, fmt.Sprintf("wrapping_token_creation_path: %s %s", hopeDelim, secret.WrapInfo.CreationPath))
input = append(input, fmt.Sprintf("wrapping_token_creation_path: %s %s", config.Delim, s.WrapInfo.CreationPath)) if secret.WrapInfo.WrappedAccessor != "" {
if s.WrapInfo.WrappedAccessor != "" { out = append(out, fmt.Sprintf("wrapped_accessor: %s %s", hopeDelim, secret.WrapInfo.WrappedAccessor))
input = append(input, fmt.Sprintf("wrapped_accessor: %s %s", config.Delim, s.WrapInfo.WrappedAccessor))
} }
} }
if s.Data != nil && len(s.Data) > 0 { if len(secret.Data) > 0 {
onceHeader.Do(headerFunc) keys := make([]string, 0, len(secret.Data))
keys := make([]string, 0, len(s.Data)) for k := range secret.Data {
for k := range s.Data {
keys = append(keys, k) keys = append(keys, k)
} }
sort.Strings(keys) sort.Strings(keys)
for _, k := range keys { for _, k := range keys {
input = append(input, fmt.Sprintf("%s %s %v", k, config.Delim, s.Data[k])) out = append(out, fmt.Sprintf("%s %s %v", k, hopeDelim, secret.Data[k]))
} }
} }
tableOutputStr := columnize.Format(input, config) // If we got this far and still don't have any data, there's nothing to print,
// sorry.
// Print the warning separately because the length of first if len(out) == 0 {
// column in the output will be increased by the length of return nil
// the longest warning string making the output look bad. }
warningsInput := make([]string, 0, 5)
if len(s.Warnings) != 0 { // Prepend the header
warningsInput = append(warningsInput, "") out = append([]string{"Key" + hopeDelim + "Value"}, out...)
warningsInput = append(warningsInput, "The following warnings were returned from the Vault server:")
for _, warning := range s.Warnings { ui.Output(tableOutput(out, &columnize.Config{
warningsInput = append(warningsInput, fmt.Sprintf("* %s", warning)) Delim: hopeDelim,
} }))
}
warningsOutputStr := columnize.Format(warningsInput, config)
ui.Output(fmt.Sprintf("%s\n%s", tableOutputStr, warningsOutputStr))
return nil return nil
} }