diff --git a/command/base.go b/command/base.go index ec0fa67c81..7dcca7671b 100644 --- a/command/base.go +++ b/command/base.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/command/token" - "github.com/kr/text" "github.com/mitchellh/cli" "github.com/pkg/errors" "github.com/posener/complete" @@ -274,22 +273,6 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { 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. type FlagSets struct { flagSets []*FlagSet diff --git a/command/base_helpers.go b/command/base_helpers.go index 4c5bad6b88..75c0e80998 100644 --- a/command/base_helpers.go +++ b/command/base_helpers.go @@ -8,16 +8,13 @@ import ( "github.com/hashicorp/vault/api" kvbuilder "github.com/hashicorp/vault/helper/kv-builder" + "github.com/kr/text" homedir "github.com/mitchellh/go-homedir" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "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 // boolean indicating whether the extraction was successful. func extractListData(secret *api.Secret) ([]interface{}, bool) { @@ -34,54 +31,9 @@ func extractListData(secret *api.Secret) ([]interface{}, bool) { 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". func sanitizePath(s string) string { - return ensureNoTrailingSlash(ensureNoLeadingSlash(s)) + return ensureNoTrailingSlash(ensureNoLeadingSlash(strings.TrimSpace(s))) } // 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. -func columnOutput(list []string) string { +func columnOutput(list []string, c *columnize.Config) string { if len(list) == 0 { return "" } - return columnize.Format(list, &columnize.Config{ - Glue: " ", - Empty: "n/a", - }) + if c == nil { + c = &columnize.Config{} + } + 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 // the list of headers. -func tableOutput(list []string) string { +func tableOutput(list []string, c *columnize.Config) string { if len(list) == 0 { return "" } + delim := "|" + if c != nil && c.Delim != "" { + delim = c.Delim + } + underline := "" - headers := strings.Split(list[0], "|") + headers := strings.Split(list[0], delim) for i, h := range headers { h = strings.TrimSpace(h) u := strings.Repeat("-", len(h)) underline = underline + u if i != len(headers)-1 { - underline = underline + " | " + underline = underline + delim } } @@ -158,7 +122,7 @@ func tableOutput(list []string) string { copy(list[2:], list[1:]) list[1] = underline - return columnOutput(list) + return columnOutput(list, c) } // 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{ fmt.Sprintf("Key Term | %d", ks.Term), fmt.Sprintf("Install Time | %s", ks.InstallTime.UTC().Format(time.RFC822)), - }) + }, nil) } // expandPath takes a filepath and returns the full expanded path, accounting @@ -223,3 +187,57 @@ func expandPath(s string) string { } 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) +} diff --git a/command/format.go b/command/format.go index 5db41c7e11..35dcb482da 100644 --- a/command/format.go +++ b/command/format.go @@ -5,19 +5,19 @@ import ( "errors" "fmt" "sort" - "strconv" "strings" - "sync" - "time" "github.com/ghodss/yaml" "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" - "github.com/posener/complete" "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 { 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 { // TODO: this should really use reflection like the other formatters do if s, ok := data.(*api.Secret); ok { - return t.OutputSecret(ui, secret, s) + return t.OutputSecret(ui, s) } if s, ok := data.([]interface{}); ok { 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 { - config := columnize.DefaultConfig() - config.Delim = "♨" - config.Glue = "\t" - config.Prefix = "" - - input := make([]string, 0, 5) + t.printWarnings(ui, secret) if len(list) > 0 { - input = append(input, "Keys") - input = append(input, "----") - - keys := make([]string, 0, len(list)) - for _, k := range list { - keys = append(keys, k.(string)) + keys := make([]string, len(list)) + for i, v := range list { + typed, ok := v.(string) + if !ok { + return fmt.Errorf("Error: %v is not a string", v) + } + keys[i] = typed } sort.Strings(keys) - for _, k := range keys { - input = append(input, fmt.Sprintf("%s", k)) - } + // Prepend the header + keys = append([]string{"Keys"}, keys...) + + ui.Output(tableOutput(keys, &columnize.Config{ + Delim: hopeDelim, + })) } - tableOutputStr := columnize.Format(input, config) - - // 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 } -func (t TableFormatter) OutputSecret(ui cli.Ui, secret, s *api.Secret) error { - config := columnize.DefaultConfig() - config.Delim = "♨" - config.Glue = "\t" - config.Prefix = "" +// printWarnings prints any warnings in the secret. +func (t TableFormatter) printWarnings(ui cli.Ui, secret *api.Secret) { + if secret != nil && len(secret.Warnings) > 0 { + ui.Warn("WARNING! The following warnings were returned from Vault:\n") + for _, warning := range secret.Warnings { + ui.Warn(wrapAtLengthWithPadding(fmt.Sprintf("* %s", warning), 2)) + } + ui.Warn("") + } +} - input := make([]string, 0, 5) - - onceHeader := &sync.Once{} - headerFunc := func() { - input = append(input, fmt.Sprintf("Key %s Value", config.Delim)) - input = append(input, fmt.Sprintf("--- %s -----", config.Delim)) +func (t TableFormatter) OutputSecret(ui cli.Ui, secret *api.Secret) error { + if secret == nil { + return nil } - if s.LeaseDuration > 0 { - onceHeader.Do(headerFunc) - if s.LeaseID != "" { - 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 { - input = append(input, fmt.Sprintf( - "refresh_interval %s %s", config.Delim, (time.Second*time.Duration(s.LeaseDuration)).String())) - } - if s.LeaseID != "" { - input = append(input, fmt.Sprintf( - "lease_renewable %s %s", config.Delim, strconv.FormatBool(s.Renewable))) + // This is probably the generic secret backend which has leases, but we + // print them as refresh_interval to reduce confusion. + out = append(out, fmt.Sprintf("refresh_interval %s %s", hopeDelim, humanDurationInt(secret.LeaseDuration))) } } - if s.Auth != nil { - onceHeader.Do(headerFunc) - input = append(input, fmt.Sprintf("token %s %s", config.Delim, s.Auth.ClientToken)) - input = append(input, fmt.Sprintf("token_accessor %s %s", config.Delim, s.Auth.Accessor)) - input = append(input, fmt.Sprintf("token_duration %s %s", config.Delim, (time.Second*time.Duration(s.Auth.LeaseDuration)).String())) - input = append(input, fmt.Sprintf("token_renewable %s %v", config.Delim, s.Auth.Renewable)) - input = append(input, fmt.Sprintf("token_policies %s %v", config.Delim, s.Auth.Policies)) - for k, v := range s.Auth.Metadata { - input = append(input, fmt.Sprintf("token_meta_%s %s %#v", k, config.Delim, v)) + if secret.Auth != nil { + out = append(out, fmt.Sprintf("token %s %s", hopeDelim, secret.Auth.ClientToken)) + out = append(out, fmt.Sprintf("token_accessor %s %s", hopeDelim, secret.Auth.Accessor)) + // If the lease duration is 0, it's likely a root token, so output the + // duration as "infinity" to clear things up. + if secret.Auth.LeaseDuration == 0 { + out = append(out, fmt.Sprintf("token_duration %s %s", hopeDelim, "∞")) + } else { + 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 { - onceHeader.Do(headerFunc) - input = append(input, fmt.Sprintf("wrapping_token: %s %s", config.Delim, s.WrapInfo.Token)) - input = append(input, fmt.Sprintf("wrapping_token_ttl: %s %s", config.Delim, (time.Second*time.Duration(s.WrapInfo.TTL)).String())) - input = append(input, fmt.Sprintf("wrapping_token_creation_time: %s %s", config.Delim, s.WrapInfo.CreationTime.String())) - input = append(input, fmt.Sprintf("wrapping_token_creation_path: %s %s", config.Delim, s.WrapInfo.CreationPath)) - if s.WrapInfo.WrappedAccessor != "" { - input = append(input, fmt.Sprintf("wrapped_accessor: %s %s", config.Delim, s.WrapInfo.WrappedAccessor)) + if secret.WrapInfo != nil { + out = append(out, fmt.Sprintf("wrapping_token: %s %s", hopeDelim, secret.WrapInfo.Token)) + out = append(out, fmt.Sprintf("wrapping_token_ttl: %s %s", hopeDelim, humanDurationInt(secret.WrapInfo.TTL))) + out = append(out, fmt.Sprintf("wrapping_token_creation_time: %s %s", hopeDelim, secret.WrapInfo.CreationTime.String())) + out = append(out, fmt.Sprintf("wrapping_token_creation_path: %s %s", hopeDelim, secret.WrapInfo.CreationPath)) + if secret.WrapInfo.WrappedAccessor != "" { + out = append(out, fmt.Sprintf("wrapped_accessor: %s %s", hopeDelim, secret.WrapInfo.WrappedAccessor)) } } - if s.Data != nil && len(s.Data) > 0 { - onceHeader.Do(headerFunc) - keys := make([]string, 0, len(s.Data)) - for k := range s.Data { + if len(secret.Data) > 0 { + keys := make([]string, 0, len(secret.Data)) + for k := range secret.Data { keys = append(keys, k) } sort.Strings(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) - - // 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(s.Warnings) != 0 { - warningsInput = append(warningsInput, "") - warningsInput = append(warningsInput, "The following warnings were returned from the Vault server:") - for _, warning := range s.Warnings { - warningsInput = append(warningsInput, fmt.Sprintf("* %s", warning)) - } + // If we got this far and still don't have any data, there's nothing to print, + // sorry. + if len(out) == 0 { + return nil } - warningsOutputStr := columnize.Format(warningsInput, config) - - ui.Output(fmt.Sprintf("%s\n%s", tableOutputStr, warningsOutputStr)) + // Prepend the header + out = append([]string{"Key" + hopeDelim + "Value"}, out...) + ui.Output(tableOutput(out, &columnize.Config{ + Delim: hopeDelim, + })) return nil }