| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package command |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "os" |
| "sort" |
| "strings" |
| "time" |
| |
| "github.com/ghodss/yaml" |
| "github.com/hashicorp/vault/api" |
| "github.com/mitchellh/cli" |
| "github.com/ryanuber/columnize" |
| ) |
| |
| 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 = "♨" |
| ) |
| |
| type FormatOptions struct { |
| Format string |
| } |
| |
| func OutputSecret(ui cli.Ui, secret *api.Secret) int { |
| return outputWithFormat(ui, secret, secret) |
| } |
| |
| func OutputList(ui cli.Ui, data interface{}) int { |
| switch data := data.(type) { |
| case *api.Secret: |
| secret := data |
| return outputWithFormat(ui, secret, secret.Data["keys"]) |
| default: |
| return outputWithFormat(ui, nil, data) |
| } |
| } |
| |
| func OutputData(ui cli.Ui, data interface{}) int { |
| return outputWithFormat(ui, nil, data) |
| } |
| |
| func outputWithFormat(ui cli.Ui, secret *api.Secret, data interface{}) int { |
| format := Format(ui) |
| formatter, ok := Formatters[format] |
| if !ok { |
| ui.Error(fmt.Sprintf("Invalid output format: %s", format)) |
| return 1 |
| } |
| |
| if err := formatter.Output(ui, secret, data); err != nil { |
| ui.Error(fmt.Sprintf("Could not parse output: %s", err.Error())) |
| return 1 |
| } |
| return 0 |
| } |
| |
| type Formatter interface { |
| Output(ui cli.Ui, secret *api.Secret, data interface{}) error |
| Format(data interface{}) ([]byte, error) |
| } |
| |
| var Formatters = map[string]Formatter{ |
| "json": JsonFormatter{}, |
| "table": TableFormatter{}, |
| "yaml": YamlFormatter{}, |
| "yml": YamlFormatter{}, |
| "pretty": PrettyFormatter{}, |
| "raw": RawFormatter{}, |
| } |
| |
| func Format(ui cli.Ui) string { |
| switch ui := ui.(type) { |
| case *VaultUI: |
| return ui.format |
| } |
| |
| format := os.Getenv(EnvVaultFormat) |
| if format == "" { |
| format = "table" |
| } |
| |
| 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{} |
| |
| func (j JsonFormatter) Format(data interface{}) ([]byte, error) { |
| return json.MarshalIndent(data, "", " ") |
| } |
| |
| func (j JsonFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error { |
| b, err := j.Format(data) |
| 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 |
| } |
| |
| // An output formatter for raw output of the original request object |
| type RawFormatter struct{} |
| |
| func (r RawFormatter) Format(data interface{}) ([]byte, error) { |
| byte_data, ok := data.([]byte) |
| if !ok { |
| return nil, fmt.Errorf("This command does not support the -format=raw option; only `vault read` does.") |
| } |
| |
| return byte_data, nil |
| } |
| |
| func (r RawFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error { |
| b, err := r.Format(data) |
| if err != nil { |
| return err |
| } |
| ui.Output(string(b)) |
| return nil |
| } |
| |
| // An output formatter for yaml output format of an object |
| type YamlFormatter struct{} |
| |
| func (y YamlFormatter) Format(data interface{}) ([]byte, error) { |
| return yaml.Marshal(data) |
| } |
| |
| func (y YamlFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error { |
| b, err := y.Format(data) |
| if err == nil { |
| ui.Output(strings.TrimSpace(string(b))) |
| } |
| return err |
| } |
| |
| type PrettyFormatter struct{} |
| |
| func (p PrettyFormatter) Format(data interface{}) ([]byte, error) { |
| return nil, nil |
| } |
| |
| func (p PrettyFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error { |
| switch data.(type) { |
| case *api.AutopilotState: |
| p.OutputAutopilotState(ui, data) |
| default: |
| return errors.New("cannot use the pretty formatter for this type") |
| } |
| return nil |
| } |
| |
| func outputStringSlice(buffer *bytes.Buffer, indent string, values []string) { |
| for _, val := range values { |
| buffer.WriteString(fmt.Sprintf("%s%s\n", indent, val)) |
| } |
| } |
| |
| type mapOutput struct { |
| key string |
| value string |
| } |
| |
| func formatServer(srv *api.AutopilotServer) string { |
| var buffer bytes.Buffer |
| |
| buffer.WriteString(fmt.Sprintf(" %s\n", srv.ID)) |
| buffer.WriteString(fmt.Sprintf(" Name: %s\n", srv.Name)) |
| buffer.WriteString(fmt.Sprintf(" Address: %s\n", srv.Address)) |
| buffer.WriteString(fmt.Sprintf(" Status: %s\n", srv.Status)) |
| buffer.WriteString(fmt.Sprintf(" Node Status: %s\n", srv.NodeStatus)) |
| buffer.WriteString(fmt.Sprintf(" Healthy: %t\n", srv.Healthy)) |
| buffer.WriteString(fmt.Sprintf(" Last Contact: %s\n", srv.LastContact)) |
| buffer.WriteString(fmt.Sprintf(" Last Term: %d\n", srv.LastTerm)) |
| buffer.WriteString(fmt.Sprintf(" Last Index: %d\n", srv.LastIndex)) |
| buffer.WriteString(fmt.Sprintf(" Version: %s\n", srv.Version)) |
| |
| if srv.UpgradeVersion != "" { |
| buffer.WriteString(fmt.Sprintf(" Upgrade Version: %s\n", srv.UpgradeVersion)) |
| } |
| if srv.RedundancyZone != "" { |
| buffer.WriteString(fmt.Sprintf(" Redundancy Zone: %s\n", srv.RedundancyZone)) |
| } |
| if srv.NodeType != "" { |
| buffer.WriteString(fmt.Sprintf(" Node Type: %s\n", srv.NodeType)) |
| } |
| |
| return buffer.String() |
| } |
| |
| func (p PrettyFormatter) OutputAutopilotState(ui cli.Ui, data interface{}) { |
| state := data.(*api.AutopilotState) |
| |
| var buffer bytes.Buffer |
| buffer.WriteString(fmt.Sprintf("Healthy: %t\n", state.Healthy)) |
| buffer.WriteString(fmt.Sprintf("Failure Tolerance: %d\n", state.FailureTolerance)) |
| buffer.WriteString(fmt.Sprintf("Leader: %s\n", state.Leader)) |
| buffer.WriteString("Voters:\n") |
| outputStringSlice(&buffer, " ", state.Voters) |
| |
| if len(state.NonVoters) > 0 { |
| buffer.WriteString("Non Voters:\n") |
| outputStringSlice(&buffer, " ", state.NonVoters) |
| } |
| |
| if state.OptimisticFailureTolerance > 0 { |
| buffer.WriteString(fmt.Sprintf("Optimistic Failure Tolerance: %d\n", state.OptimisticFailureTolerance)) |
| } |
| |
| // Servers |
| buffer.WriteString("Servers:\n") |
| var outputs []mapOutput |
| for id, srv := range state.Servers { |
| outputs = append(outputs, mapOutput{key: id, value: formatServer(srv)}) |
| } |
| sort.Slice(outputs, func(i, j int) bool { |
| return outputs[i].key < outputs[j].key |
| }) |
| for _, output := range outputs { |
| buffer.WriteString(output.value) |
| } |
| |
| // Redundancy Zones |
| if len(state.RedundancyZones) > 0 { |
| buffer.WriteString("Redundancy Zones:\n") |
| zoneList := make([]string, 0, len(state.RedundancyZones)) |
| for z := range state.RedundancyZones { |
| zoneList = append(zoneList, z) |
| } |
| sort.Strings(zoneList) |
| for _, zoneName := range zoneList { |
| zone := state.RedundancyZones[zoneName] |
| servers := zone.Servers |
| voters := zone.Voters |
| sort.Strings(servers) |
| sort.Strings(voters) |
| buffer.WriteString(fmt.Sprintf(" %s\n", zoneName)) |
| buffer.WriteString(fmt.Sprintf(" Servers: %s\n", strings.Join(servers, ", "))) |
| buffer.WriteString(fmt.Sprintf(" Voters: %s\n", strings.Join(voters, ", "))) |
| buffer.WriteString(fmt.Sprintf(" Failure Tolerance: %d\n", zone.FailureTolerance)) |
| } |
| } |
| |
| // Upgrade Info |
| if state.Upgrade != nil { |
| buffer.WriteString("Upgrade Info:\n") |
| buffer.WriteString(fmt.Sprintf(" Status: %s\n", state.Upgrade.Status)) |
| buffer.WriteString(fmt.Sprintf(" Target Version: %s\n", state.Upgrade.TargetVersion)) |
| buffer.WriteString(fmt.Sprintf(" Target Version Voters: %s\n", strings.Join(state.Upgrade.TargetVersionVoters, ", "))) |
| buffer.WriteString(fmt.Sprintf(" Target Version Non-Voters: %s\n", strings.Join(state.Upgrade.TargetVersionNonVoters, ", "))) |
| buffer.WriteString(fmt.Sprintf(" Other Version Voters: %s\n", strings.Join(state.Upgrade.OtherVersionVoters, ", "))) |
| buffer.WriteString(fmt.Sprintf(" Other Version Non-Voters: %s\n", strings.Join(state.Upgrade.OtherVersionNonVoters, ", "))) |
| |
| if len(state.Upgrade.RedundancyZones) > 0 { |
| buffer.WriteString(" Redundancy Zones:\n") |
| for zoneName, zoneVersion := range state.Upgrade.RedundancyZones { |
| buffer.WriteString(fmt.Sprintf(" %s\n", zoneName)) |
| buffer.WriteString(fmt.Sprintf(" Target Version Voters: %s\n", strings.Join(zoneVersion.TargetVersionVoters, ", "))) |
| buffer.WriteString(fmt.Sprintf(" Target Version Non-Voters: %s\n", strings.Join(zoneVersion.TargetVersionNonVoters, ", "))) |
| buffer.WriteString(fmt.Sprintf(" Other Version Voters: %s\n", strings.Join(zoneVersion.OtherVersionVoters, ", "))) |
| buffer.WriteString(fmt.Sprintf(" Other Version Non-Voters: %s\n", strings.Join(zoneVersion.OtherVersionNonVoters, ", "))) |
| } |
| } |
| } |
| |
| ui.Output(buffer.String()) |
| } |
| |
| // An output formatter for table output of an object |
| type TableFormatter struct{} |
| |
| // We don't use this due to the TableFormatter introducing a bug when the -field flag is supplied: |
| // https://github.com/hashicorp/vault/commit/b24cf9a8af2190e96c614205b8cdf06d8c4b6718 . |
| func (t TableFormatter) Format(data interface{}) ([]byte, error) { |
| return nil, nil |
| } |
| |
| func (t TableFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error { |
| switch data := data.(type) { |
| case *api.Secret: |
| return t.OutputSecret(ui, secret) |
| case []interface{}: |
| return t.OutputList(ui, secret, data) |
| case []string: |
| return t.OutputList(ui, nil, data) |
| case map[string]interface{}: |
| return t.OutputMap(ui, data) |
| case SealStatusOutput: |
| return t.OutputSealStatusStruct(ui, nil, data) |
| default: |
| return errors.New("cannot use the table formatter for this type") |
| } |
| } |
| |
| func (t TableFormatter) OutputSealStatusStruct(ui cli.Ui, secret *api.Secret, data interface{}) error { |
| var status SealStatusOutput = data.(SealStatusOutput) |
| var sealPrefix string |
| if status.RecoverySeal { |
| sealPrefix = "Recovery " |
| } |
| |
| out := []string{} |
| out = append(out, "Key | Value") |
| out = append(out, fmt.Sprintf("%sSeal Type | %s", sealPrefix, status.Type)) |
| out = append(out, fmt.Sprintf("Initialized | %t", status.Initialized)) |
| out = append(out, fmt.Sprintf("Sealed | %t", status.Sealed)) |
| out = append(out, fmt.Sprintf("Total %sShares | %d", sealPrefix, status.N)) |
| out = append(out, fmt.Sprintf("Threshold | %d", status.T)) |
| |
| if status.Sealed { |
| out = append(out, fmt.Sprintf("Unseal Progress | %d/%d", status.Progress, status.T)) |
| out = append(out, fmt.Sprintf("Unseal Nonce | %s", status.Nonce)) |
| } |
| |
| if status.Migration { |
| out = append(out, fmt.Sprintf("Seal Migration in Progress | %t", status.Migration)) |
| } |
| |
| out = append(out, fmt.Sprintf("Version | %s", status.Version)) |
| out = append(out, fmt.Sprintf("Build Date | %s", status.BuildDate)) |
| out = append(out, fmt.Sprintf("Storage Type | %s", status.StorageType)) |
| |
| if status.ClusterName != "" && status.ClusterID != "" { |
| out = append(out, fmt.Sprintf("Cluster Name | %s", status.ClusterName)) |
| out = append(out, fmt.Sprintf("Cluster ID | %s", status.ClusterID)) |
| } |
| |
| // Output if HCP link is configured |
| if status.HCPLinkStatus != "" { |
| out = append(out, fmt.Sprintf("HCP Link Status | %s", status.HCPLinkStatus)) |
| out = append(out, fmt.Sprintf("HCP Link Resource ID | %s", status.HCPLinkResourceID)) |
| } |
| |
| // Output if HA is enabled |
| out = append(out, fmt.Sprintf("HA Enabled | %t", status.HAEnabled)) |
| |
| if status.HAEnabled { |
| mode := "sealed" |
| if !status.Sealed { |
| out = append(out, fmt.Sprintf("HA Cluster | %s", status.LeaderClusterAddress)) |
| mode = "standby" |
| showLeaderAddr := false |
| if status.IsSelf { |
| mode = "active" |
| } else { |
| if status.LeaderAddress == "" { |
| status.LeaderAddress = "<none>" |
| } |
| showLeaderAddr = true |
| } |
| out = append(out, fmt.Sprintf("HA Mode | %s", mode)) |
| |
| if status.IsSelf && !status.ActiveTime.IsZero() { |
| out = append(out, fmt.Sprintf("Active Since | %s", status.ActiveTime.Format(time.RFC3339Nano))) |
| } |
| // This is down here just to keep ordering consistent |
| if showLeaderAddr { |
| out = append(out, fmt.Sprintf("Active Node Address | %s", status.LeaderAddress)) |
| } |
| |
| if status.PerfStandby { |
| out = append(out, fmt.Sprintf("Performance Standby Node | %t", status.PerfStandby)) |
| out = append(out, fmt.Sprintf("Performance Standby Last Remote WAL | %d", status.PerfStandbyLastRemoteWAL)) |
| } |
| } |
| } |
| |
| if status.RaftCommittedIndex > 0 { |
| out = append(out, fmt.Sprintf("Raft Committed Index | %d", status.RaftCommittedIndex)) |
| } |
| if status.RaftAppliedIndex > 0 { |
| out = append(out, fmt.Sprintf("Raft Applied Index | %d", status.RaftAppliedIndex)) |
| } |
| if status.LastWAL != 0 { |
| out = append(out, fmt.Sprintf("Last WAL | %d", status.LastWAL)) |
| } |
| if len(status.Warnings) > 0 { |
| out = append(out, fmt.Sprintf("Warnings | %v", status.Warnings)) |
| } |
| |
| ui.Output(tableOutput(out, &columnize.Config{ |
| Delim: "|", |
| })) |
| return nil |
| } |
| |
| 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: |
| ui.Output(tableOutput(data, nil)) |
| return nil |
| default: |
| return errors.New("error: table formatter cannot output list for this data type") |
| } |
| |
| list := data.([]interface{}) |
| |
| if len(list) > 0 { |
| keys := make([]string, len(list)) |
| for i, v := range list { |
| typed, ok := v.(string) |
| if !ok { |
| return fmt.Errorf("%v is not a string", v) |
| } |
| keys[i] = typed |
| } |
| sort.Strings(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) |
| } |
| } |
| |
| // Prepend the header to the formatted rows. |
| output := append([]string{header}, rows...) |
| ui.Output(tableOutput(output, &columnize.Config{ |
| Delim: hopeDelim, |
| })) |
| } |
| |
| return nil |
| } |
| |
| // 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("") |
| } |
| } |
| } |
| |
| func (t TableFormatter) OutputSecret(ui cli.Ui, secret *api.Secret) error { |
| if secret == nil { |
| return nil |
| } |
| |
| 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 %v", hopeDelim, humanDurationInt(secret.LeaseDuration))) |
| out = append(out, fmt.Sprintf("lease_renewable %s %t", hopeDelim, secret.Renewable)) |
| } else { |
| // 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 %v", hopeDelim, humanDurationInt(secret.LeaseDuration))) |
| } |
| } |
| |
| if secret.Auth != nil { |
| if secret.Auth.MFARequirement != nil { |
| out = append(out, fmt.Sprintf("mfa_request_id %s %s", hopeDelim, secret.Auth.MFARequirement.MFARequestID)) |
| |
| for k, constraintSet := range secret.Auth.MFARequirement.MFAConstraints { |
| for _, constraint := range constraintSet.Any { |
| out = append(out, fmt.Sprintf("mfa_constraint_%s_%s_id %s %s", k, constraint.Type, hopeDelim, constraint.ID)) |
| out = append(out, fmt.Sprintf("mfa_constraint_%s_%s_uses_passcode %s %t", k, constraint.Type, hopeDelim, constraint.UsesPasscode)) |
| if constraint.Name != "" { |
| out = append(out, fmt.Sprintf("mfa_constraint_%s_%s_name %s %s", k, constraint.Type, hopeDelim, constraint.Name)) |
| } |
| } |
| } |
| } else { // Token information only makes sense if no further MFA requirement (i.e. if we actually have a token) |
| 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 %v", 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 %q", hopeDelim, secret.Auth.TokenPolicies)) |
| out = append(out, fmt.Sprintf("identity_policies %s %q", hopeDelim, secret.Auth.IdentityPolicies)) |
| out = append(out, fmt.Sprintf("policies %s %q", 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 secret.WrapInfo != nil { |
| out = append(out, fmt.Sprintf("wrapping_token: %s %s", hopeDelim, secret.WrapInfo.Token)) |
| out = append(out, fmt.Sprintf("wrapping_accessor: %s %s", hopeDelim, secret.WrapInfo.Accessor)) |
| out = append(out, fmt.Sprintf("wrapping_token_ttl: %s %v", 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 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 { |
| v := secret.Data[k] |
| |
| // If the field "looks" like a TTL, print it as a time duration instead. |
| if looksLikeDuration(k) { |
| v = humanDurationInt(v) |
| } |
| |
| out = append(out, fmt.Sprintf("%s %s %v", k, hopeDelim, v)) |
| } |
| } |
| |
| // If we got this far and still don't have any data, there's nothing to print, |
| // sorry. |
| if len(out) == 0 { |
| return nil |
| } |
| |
| // Prepend the header |
| out = append([]string{"Key" + hopeDelim + "Value"}, out...) |
| |
| ui.Output(tableOutput(out, &columnize.Config{ |
| Delim: hopeDelim, |
| })) |
| return nil |
| } |
| |
| func (t TableFormatter) OutputMap(ui cli.Ui, data map[string]interface{}) error { |
| out := make([]string, 0, len(data)+1) |
| if len(data) > 0 { |
| keys := make([]string, 0, len(data)) |
| for k := range data { |
| keys = append(keys, k) |
| } |
| sort.Strings(keys) |
| |
| for _, k := range keys { |
| v := data[k] |
| |
| // If the field "looks" like a TTL, print it as a time duration instead. |
| if looksLikeDuration(k) { |
| v = humanDurationInt(v) |
| } |
| |
| out = append(out, fmt.Sprintf("%s %s %v", k, hopeDelim, v)) |
| } |
| } |
| |
| // If we got this far and still don't have any data, there's nothing to print, |
| // sorry. |
| if len(out) == 0 { |
| return nil |
| } |
| |
| // Prepend the header |
| out = append([]string{"Key" + hopeDelim + "Value"}, out...) |
| |
| ui.Output(tableOutput(out, &columnize.Config{ |
| Delim: hopeDelim, |
| })) |
| return nil |
| } |
| |
| // OutputSealStatus will print *api.SealStatusResponse in the CLI according to the format provided |
| func OutputSealStatus(ui cli.Ui, client *api.Client, status *api.SealStatusResponse) int { |
| sealStatusOutput := SealStatusOutput{SealStatusResponse: *status} |
| |
| // Mask the 'Vault is sealed' error, since this means HA is enabled, but that |
| // we cannot query for the leader since we are sealed. |
| leaderStatus, err := client.Sys().Leader() |
| if err != nil && strings.Contains(err.Error(), "Vault is sealed") { |
| leaderStatus = &api.LeaderResponse{HAEnabled: true} |
| err = nil |
| } |
| if err != nil { |
| ui.Error(fmt.Sprintf("Error checking leader status: %s", err)) |
| return 1 |
| } |
| |
| // copy leaderStatus fields into sealStatusOutput for display later |
| sealStatusOutput.HAEnabled = leaderStatus.HAEnabled |
| sealStatusOutput.IsSelf = leaderStatus.IsSelf |
| sealStatusOutput.ActiveTime = leaderStatus.ActiveTime |
| sealStatusOutput.LeaderAddress = leaderStatus.LeaderAddress |
| sealStatusOutput.LeaderClusterAddress = leaderStatus.LeaderClusterAddress |
| sealStatusOutput.PerfStandby = leaderStatus.PerfStandby |
| sealStatusOutput.PerfStandbyLastRemoteWAL = leaderStatus.PerfStandbyLastRemoteWAL |
| sealStatusOutput.LastWAL = leaderStatus.LastWAL |
| sealStatusOutput.RaftCommittedIndex = leaderStatus.RaftCommittedIndex |
| sealStatusOutput.RaftAppliedIndex = leaderStatus.RaftAppliedIndex |
| OutputData(ui, sealStatusOutput) |
| return 0 |
| } |
| |
| // looksLikeDuration checks if the given key "k" looks like a duration value. |
| // This is used to pretty-format duration values in responses, especially from |
| // plugins. |
| func looksLikeDuration(k string) bool { |
| return k == "period" || strings.HasSuffix(k, "_period") || |
| k == "ttl" || strings.HasSuffix(k, "_ttl") || |
| k == "duration" || strings.HasSuffix(k, "_duration") || |
| k == "lease_max" || k == "ttl_max" |
| } |
| |
| // This struct is responsible for capturing all the fields to be output by a |
| // vault status command, including fields that do not come from the status API. |
| // Currently we are adding the fields from api.LeaderResponse |
| type SealStatusOutput struct { |
| api.SealStatusResponse |
| HAEnabled bool `json:"ha_enabled"` |
| IsSelf bool `json:"is_self,omitempty"` |
| ActiveTime time.Time `json:"active_time,omitempty"` |
| LeaderAddress string `json:"leader_address,omitempty"` |
| LeaderClusterAddress string `json:"leader_cluster_address,omitempty"` |
| PerfStandby bool `json:"performance_standby,omitempty"` |
| PerfStandbyLastRemoteWAL uint64 `json:"performance_standby_last_remote_wal,omitempty"` |
| LastWAL uint64 `json:"last_wal,omitempty"` |
| RaftCommittedIndex uint64 `json:"raft_committed_index,omitempty"` |
| RaftAppliedIndex uint64 `json:"raft_applied_index,omitempty"` |
| } |