mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-08 07:37:01 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
726 lines
24 KiB
Go
726 lines
24 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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"`
|
|
}
|