mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
359 lines
9.3 KiB
359 lines
9.3 KiB
package base
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/hashicorp/boundary/api"
|
|
"github.com/hashicorp/boundary/api/plugins"
|
|
"github.com/hashicorp/boundary/api/scopes"
|
|
"github.com/mitchellh/cli"
|
|
"github.com/mitchellh/go-wordwrap"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// This is adapted from the code in the strings package for TrimSpace
|
|
var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}
|
|
|
|
func ScopeInfoForOutput(scp *scopes.ScopeInfo, maxLength int) string {
|
|
if scp == nil {
|
|
return " <not included in response>"
|
|
}
|
|
vals := map[string]interface{}{
|
|
"ID": scp.Id,
|
|
"Type": scp.Type,
|
|
"Name": scp.Name,
|
|
}
|
|
if scp.ParentScopeId != "" {
|
|
vals["Parent Scope ID"] = scp.ParentScopeId
|
|
}
|
|
return WrapMap(4, maxLength, vals)
|
|
}
|
|
|
|
func PluginInfoForOutput(plg *plugins.PluginInfo, maxLength int) string {
|
|
if plg == nil {
|
|
return " <not included in response>"
|
|
}
|
|
vals := map[string]interface{}{
|
|
"ID": plg.Id,
|
|
"Name": plg.Name,
|
|
}
|
|
return WrapMap(4, maxLength, vals)
|
|
}
|
|
|
|
func MaxAttributesLength(nonAttributesMap, attributesMap map[string]interface{}, keySubstMap map[string]string) int {
|
|
// We always print a scope ID and in some cases this particular key ends up
|
|
// being the longest key, so start with it as a baseline. It's always
|
|
// indented by 2 in addition to the normal offset so take that into account.
|
|
maxLength := len("Parent Scope ID") + 2
|
|
for k := range nonAttributesMap {
|
|
if len(k) > maxLength {
|
|
maxLength = len(k)
|
|
}
|
|
}
|
|
if len(attributesMap) > 0 {
|
|
for k, v := range attributesMap {
|
|
if keySubstMap != nil {
|
|
if keySubstMap[k] != "" {
|
|
attributesMap[keySubstMap[k]] = v
|
|
delete(attributesMap, k)
|
|
}
|
|
}
|
|
}
|
|
for k := range attributesMap {
|
|
if len(k) > maxLength {
|
|
maxLength = len(k)
|
|
}
|
|
}
|
|
}
|
|
return maxLength
|
|
}
|
|
|
|
func trimSpaceRight(in string) string {
|
|
for stop := len(in); stop > 0; stop-- {
|
|
c := in[stop-1]
|
|
if c >= utf8.RuneSelf {
|
|
return strings.TrimFunc(in[:stop], unicode.IsSpace)
|
|
}
|
|
if asciiSpace[c] == 0 {
|
|
return in[0:stop]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func WrapForHelpText(lines []string) string {
|
|
var ret []string
|
|
for _, line := range lines {
|
|
line = trimSpaceRight(line)
|
|
trimmed := strings.TrimSpace(line)
|
|
diff := uint(len(line) - len(trimmed))
|
|
wrapped := wordwrap.WrapString(trimmed, TermWidth-diff)
|
|
splitWrapped := strings.Split(wrapped, "\n")
|
|
for i := range splitWrapped {
|
|
splitWrapped[i] = fmt.Sprintf("%s%s", strings.Repeat(" ", int(diff)), strings.TrimSpace(splitWrapped[i]))
|
|
}
|
|
ret = append(ret, strings.Join(splitWrapped, "\n"))
|
|
}
|
|
|
|
return strings.Join(ret, "\n")
|
|
}
|
|
|
|
func WrapSlice(prefixSpaces int, input []string) string {
|
|
var ret []string
|
|
for _, v := range input {
|
|
ret = append(ret, fmt.Sprintf("%s%s",
|
|
strings.Repeat(" ", prefixSpaces),
|
|
v,
|
|
))
|
|
}
|
|
|
|
return strings.Join(ret, "\n")
|
|
}
|
|
|
|
func WrapMap(prefixSpaces, maxLengthOverride int, input map[string]interface{}) string {
|
|
maxKeyLength := maxLengthOverride
|
|
if maxKeyLength == 0 {
|
|
for k := range input {
|
|
if len(k) > maxKeyLength {
|
|
maxKeyLength = len(k)
|
|
}
|
|
}
|
|
}
|
|
|
|
var sortedKeys []string
|
|
for k := range input {
|
|
sortedKeys = append(sortedKeys, k)
|
|
}
|
|
sort.Strings(sortedKeys)
|
|
|
|
var ret []string
|
|
for _, k := range sortedKeys {
|
|
v := input[k]
|
|
spaces := maxKeyLength - len(k)
|
|
if spaces < 0 {
|
|
spaces = 0
|
|
}
|
|
vOut := fmt.Sprintf("%v", v)
|
|
switch v.(type) {
|
|
case map[string]interface{}:
|
|
buf, err := json.MarshalIndent(v, strings.Repeat(" ", prefixSpaces), " ")
|
|
if err != nil {
|
|
vOut = "[Unable to Print]"
|
|
break
|
|
}
|
|
bStrings := strings.Split(string(buf), "\n")
|
|
if len(bStrings) > 0 {
|
|
// Indent doesn't apply to the first line 🙄
|
|
bStrings[0] = fmt.Sprintf("\n%s%s", strings.Repeat(" ", prefixSpaces), bStrings[0])
|
|
}
|
|
vOut = strings.Join(bStrings, "\n")
|
|
}
|
|
ret = append(ret, fmt.Sprintf("%s%s%s%s",
|
|
strings.Repeat(" ", prefixSpaces),
|
|
fmt.Sprintf("%s: ", k),
|
|
strings.Repeat(" ", spaces),
|
|
vOut,
|
|
))
|
|
}
|
|
|
|
return strings.Join(ret, "\n")
|
|
}
|
|
|
|
// PrintApiError prints the given API error, optionally with context
|
|
// information, to the UI in the appropriate format. WithAttributeFieldPrefix is
|
|
// used, all other options are ignored.
|
|
func (c *Command) PrintApiError(in *api.Error, contextStr string, opt ...Option) {
|
|
opts := getOpts(opt...)
|
|
switch Format(c.UI) {
|
|
case "json":
|
|
output := struct {
|
|
Context string `json:"context,omitempty"`
|
|
Status int `json:"status"`
|
|
ApiError json.RawMessage `json:"api_error"`
|
|
}{
|
|
Context: contextStr,
|
|
Status: in.Response().StatusCode(),
|
|
ApiError: in.Response().Body.Bytes(),
|
|
}
|
|
b, _ := JsonFormatter{}.Format(output)
|
|
c.UI.Error(string(b))
|
|
|
|
default:
|
|
nonAttributeMap := map[string]interface{}{
|
|
"Status": in.Response().StatusCode(),
|
|
"Kind": in.Kind,
|
|
"Message": in.Message,
|
|
}
|
|
if contextStr != "" {
|
|
nonAttributeMap["context"] = contextStr
|
|
}
|
|
if in.Op != "" {
|
|
nonAttributeMap["Operation"] = in.Op
|
|
}
|
|
|
|
maxLength := MaxAttributesLength(nonAttributeMap, nil, nil)
|
|
|
|
var output []string
|
|
if contextStr != "" {
|
|
output = append(output, contextStr)
|
|
}
|
|
output = append(output,
|
|
"",
|
|
"Error information:",
|
|
WrapMap(2, maxLength+2, nonAttributeMap),
|
|
)
|
|
|
|
if in.Details != nil {
|
|
if len(in.Details.WrappedErrors) > 0 {
|
|
output = append(output,
|
|
"",
|
|
" Wrapped Errors:",
|
|
)
|
|
for _, we := range in.Details.WrappedErrors {
|
|
output = append(output,
|
|
fmt.Sprintf(" Message: %s", we.Message),
|
|
fmt.Sprintf(" Operation: %s", we.Op),
|
|
)
|
|
}
|
|
}
|
|
|
|
if len(in.Details.RequestFields) > 0 {
|
|
output = append(output,
|
|
"",
|
|
" Field-specific Errors:",
|
|
)
|
|
for _, field := range in.Details.RequestFields {
|
|
if field.Name == "update_mask" {
|
|
// TODO: Report useful error messages related to "update_mask".
|
|
continue
|
|
}
|
|
var fNameParts []string
|
|
if opts.withAttributeFieldPrefix != "" && strings.HasPrefix(field.Name, "attributes.") {
|
|
fNameParts = append(fNameParts, opts.withAttributeFieldPrefix)
|
|
}
|
|
fNameParts = append(fNameParts, strings.ReplaceAll(strings.TrimPrefix(field.Name, "attributes."), "_", "-"))
|
|
fName := strings.Join(fNameParts, "-")
|
|
output = append(output,
|
|
fmt.Sprintf(" Name: -%s", fName),
|
|
fmt.Sprintf(" Error: %s", field.Description),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
c.UI.Error(WrapForHelpText(output))
|
|
}
|
|
}
|
|
|
|
// PrintCliError prints the given CLI error to the UI in the appropriate format
|
|
func (c *Command) PrintCliError(err error) {
|
|
switch Format(c.UI) {
|
|
case "table":
|
|
c.UI.Error(err.Error())
|
|
case "json":
|
|
output := struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: err.Error(),
|
|
}
|
|
b, _ := JsonFormatter{}.Format(output)
|
|
c.UI.Error(string(b))
|
|
}
|
|
}
|
|
|
|
// PrintJsonItem prints the given item to the UI in JSON format
|
|
func (c *Command) PrintJsonItem(result api.GenericResult, opt ...Option) bool {
|
|
resp := result.GetResponse()
|
|
if resp == nil {
|
|
c.PrintCliError(errors.New("Error formatting as JSON: no response given to item formatter"))
|
|
return false
|
|
}
|
|
if r := resp.HttpResponse(); r != nil {
|
|
opt = append(opt, WithStatusCode(r.StatusCode))
|
|
}
|
|
return c.PrintJson(resp.Body.Bytes(), opt...)
|
|
}
|
|
|
|
// PrintJson prints the given raw JSON in our common format
|
|
func (c *Command) PrintJson(input json.RawMessage, opt ...Option) bool {
|
|
opts := getOpts(opt...)
|
|
output := struct {
|
|
StatusCode int `json:"status_code,omitempty"`
|
|
Item json.RawMessage `json:"item,omitempty"`
|
|
}{
|
|
StatusCode: opts.withStatusCode,
|
|
Item: input,
|
|
}
|
|
b, err := JsonFormatter{}.Format(output)
|
|
if err != nil {
|
|
c.PrintCliError(fmt.Errorf("Error formatting as JSON: %w", err))
|
|
return false
|
|
}
|
|
c.UI.Output(string(b))
|
|
return true
|
|
}
|
|
|
|
// PrintJsonItems prints the given items to the UI in JSON format
|
|
func (c *Command) PrintJsonItems(result api.GenericListResult) bool {
|
|
resp := result.GetResponse()
|
|
if resp == nil {
|
|
c.PrintCliError(errors.New("Error formatting as JSON: no response given to items formatter"))
|
|
return false
|
|
}
|
|
// First we need to grab the items out. The reason is that if we simply
|
|
// embed the raw message as with PrintJsonItem above, it will have {"items":
|
|
// {"items": []}}. However, we decode into a RawMessage which makes it much
|
|
// more efficient on both the decoding and encoding side.
|
|
type inMsg struct {
|
|
Items json.RawMessage `json:"items"`
|
|
}
|
|
var input inMsg
|
|
if resp.Body.Bytes() != nil {
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &input); err != nil {
|
|
c.PrintCliError(fmt.Errorf("Error unmarshaling response body at format time: %w", err))
|
|
return false
|
|
}
|
|
}
|
|
output := struct {
|
|
StatusCode int `json:"status_code"`
|
|
Items json.RawMessage `json:"items"`
|
|
}{
|
|
StatusCode: resp.HttpResponse().StatusCode,
|
|
Items: input.Items,
|
|
}
|
|
b, err := JsonFormatter{}.Format(output)
|
|
if err != nil {
|
|
c.PrintCliError(fmt.Errorf("Error formatting as JSON: %w", err))
|
|
return false
|
|
}
|
|
c.UI.Output(string(b))
|
|
return true
|
|
}
|
|
|
|
// An output formatter for json output of an object
|
|
type JsonFormatter struct{}
|
|
|
|
func (j JsonFormatter) Format(data interface{}) ([]byte, error) {
|
|
return json.Marshal(data)
|
|
}
|
|
|
|
func Format(ui cli.Ui) string {
|
|
switch t := ui.(type) {
|
|
case *BoundaryUI:
|
|
return t.Format
|
|
}
|
|
|
|
format := os.Getenv(EnvBoundaryCLIFormat)
|
|
if format == "" {
|
|
format = "table"
|
|
}
|
|
|
|
return format
|
|
}
|