github.com/vmware/govmomi@v0.51.0/cli/flags/output.go (about)

     1  // © Broadcom. All Rights Reserved.
     2  // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package flags
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"errors"
    11  	"flag"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"reflect"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/dougm/pretty"
    20  
    21  	"github.com/vmware/govmomi/cli"
    22  	"github.com/vmware/govmomi/task"
    23  	"github.com/vmware/govmomi/vim25/progress"
    24  	"github.com/vmware/govmomi/vim25/soap"
    25  	"github.com/vmware/govmomi/vim25/types"
    26  	"github.com/vmware/govmomi/vim25/xml"
    27  )
    28  
    29  type OutputWriter interface {
    30  	Write(io.Writer) error
    31  }
    32  
    33  type OutputFlag struct {
    34  	common
    35  
    36  	JSON bool
    37  	XML  bool
    38  	TTY  bool
    39  	Dump bool
    40  	Out  io.Writer
    41  	Spec bool
    42  
    43  	formatError  bool
    44  	formatIndent bool
    45  }
    46  
    47  var outputFlagKey = flagKey("output")
    48  
    49  func NewOutputFlag(ctx context.Context) (*OutputFlag, context.Context) {
    50  	if v := ctx.Value(outputFlagKey); v != nil {
    51  		return v.(*OutputFlag), ctx
    52  	}
    53  
    54  	v := &OutputFlag{Out: os.Stdout}
    55  	ctx = context.WithValue(ctx, outputFlagKey, v)
    56  	return v, ctx
    57  }
    58  
    59  func (flag *OutputFlag) Register(ctx context.Context, f *flag.FlagSet) {
    60  	flag.RegisterOnce(func() {
    61  		f.BoolVar(&flag.JSON, "json", false, "Enable JSON output")
    62  		f.BoolVar(&flag.XML, "xml", false, "Enable XML output")
    63  		f.BoolVar(&flag.Dump, "dump", false, "Enable Go output")
    64  		if cli.ShowUnreleased() {
    65  			f.BoolVar(&flag.Spec, "spec", false, "Output spec without sending request")
    66  		}
    67  		// Avoid adding more flags for now..
    68  		flag.formatIndent = os.Getenv("GOVC_INDENT") != "false"      // Default to indented output
    69  		flag.formatError = os.Getenv("GOVC_FORMAT_ERROR") != "false" // Default to formatted errors
    70  	})
    71  }
    72  
    73  func (flag *OutputFlag) Process(ctx context.Context) error {
    74  	return flag.ProcessOnce(func() error {
    75  		if !flag.All() {
    76  			// Assume we have a tty if not outputting JSON
    77  			flag.TTY = true
    78  		}
    79  
    80  		return nil
    81  	})
    82  }
    83  
    84  // Log outputs the specified string, prefixed with the current time.
    85  // A newline is not automatically added. If the specified string
    86  // starts with a '\r', the current line is cleared first.
    87  func (flag *OutputFlag) Log(s string) (int, error) {
    88  	if len(s) > 0 && s[0] == '\r' {
    89  		flag.Write([]byte{'\r', 033, '[', 'K'})
    90  		s = s[1:]
    91  	}
    92  
    93  	return flag.WriteString(time.Now().Format("[02-01-06 15:04:05] ") + s)
    94  }
    95  
    96  func (flag *OutputFlag) Write(b []byte) (int, error) {
    97  	if !flag.TTY {
    98  		return 0, nil
    99  	}
   100  
   101  	w := flag.Out
   102  	if w == nil {
   103  		w = os.Stdout
   104  	}
   105  	n, err := w.Write(b)
   106  	if w == os.Stdout {
   107  		os.Stdout.Sync()
   108  	}
   109  	return n, err
   110  }
   111  
   112  func (flag *OutputFlag) WriteString(s string) (int, error) {
   113  	return flag.Write([]byte(s))
   114  }
   115  
   116  func (flag *OutputFlag) All() bool {
   117  	return flag.JSON || flag.XML || flag.Dump
   118  }
   119  
   120  func dumpValue(val any) any {
   121  	type dumper interface {
   122  		Dump() any
   123  	}
   124  
   125  	if d, ok := val.(dumper); ok {
   126  		return d.Dump()
   127  	}
   128  
   129  	rval := reflect.ValueOf(val)
   130  	if rval.Type().Kind() != reflect.Ptr {
   131  		return val
   132  	}
   133  
   134  	rval = rval.Elem()
   135  	if rval.Type().Kind() == reflect.Struct {
   136  		f := rval.Field(0)
   137  		if f.Type().Kind() == reflect.Slice {
   138  			// common case for the various 'type infoResult'
   139  			if f.Len() == 1 {
   140  				return f.Index(0).Interface()
   141  			}
   142  			return f.Interface()
   143  		}
   144  
   145  		if rval.NumField() == 1 && rval.Type().Field(0).Anonymous {
   146  			// common case where govc type wraps govmomi type to implement OutputWriter
   147  			return f.Interface()
   148  		}
   149  	}
   150  
   151  	return val
   152  }
   153  
   154  type outputAny struct {
   155  	Value any
   156  }
   157  
   158  func (*outputAny) Write(io.Writer) error {
   159  	return nil
   160  }
   161  
   162  func (a *outputAny) Dump() any {
   163  	return a.Value
   164  }
   165  
   166  func (flag *OutputFlag) WriteAny(val any) error {
   167  	if !flag.All() {
   168  		flag.XML = true
   169  	}
   170  	return flag.WriteResult(&outputAny{val})
   171  }
   172  
   173  func (flag *OutputFlag) WriteResult(result OutputWriter) error {
   174  	var err error
   175  
   176  	switch {
   177  	case flag.Dump:
   178  		format := "%#v\n"
   179  		if flag.formatIndent {
   180  			format = "%# v\n"
   181  		}
   182  		_, err = pretty.Fprintf(flag.Out, format, dumpValue(result))
   183  	case flag.JSON:
   184  		e := json.NewEncoder(flag.Out)
   185  		if flag.formatIndent {
   186  			e.SetIndent("", "  ")
   187  		}
   188  		err = e.Encode(result)
   189  	case flag.XML:
   190  		e := xml.NewEncoder(flag.Out)
   191  		if flag.formatIndent {
   192  			e.Indent("", "  ")
   193  		}
   194  		err = e.Encode(dumpValue(result))
   195  		if err == nil {
   196  			fmt.Fprintln(flag.Out)
   197  		}
   198  	default:
   199  		err = result.Write(flag.Out)
   200  	}
   201  
   202  	return err
   203  }
   204  
   205  func (flag *OutputFlag) WriteError(err error) bool {
   206  	if flag.formatError {
   207  		flag.Out = os.Stderr
   208  		return flag.WriteResult(&errorOutput{err}) == nil
   209  	}
   210  	return false
   211  }
   212  
   213  type errorOutput struct {
   214  	error
   215  }
   216  
   217  func (e errorOutput) Write(w io.Writer) error {
   218  	reason := e.Error()
   219  	var messages []string
   220  	var faults []types.LocalizableMessage
   221  
   222  	switch err := e.error.(type) {
   223  	case task.Error:
   224  		faults = err.LocalizedMethodFault.Fault.GetMethodFault().FaultMessage
   225  		if err.Description != nil {
   226  			reason = fmt.Sprintf("%s (%s)", reason, err.Description.Message)
   227  		}
   228  	default:
   229  		if soap.IsSoapFault(err) {
   230  			detail := soap.ToSoapFault(err).Detail.Fault
   231  			if f, ok := detail.(types.BaseMethodFault); ok {
   232  				faults = f.GetMethodFault().FaultMessage
   233  			}
   234  		}
   235  	}
   236  
   237  	for _, m := range faults {
   238  		if m.Message != "" && !strings.HasPrefix(m.Message, "[context]") {
   239  			messages = append(messages, fmt.Sprintf("%s (%s)", m.Message, m.Key))
   240  		}
   241  	}
   242  
   243  	messages = append(messages, reason)
   244  
   245  	for _, message := range messages {
   246  		if _, err := fmt.Fprintf(w, "%s: %s\n", os.Args[0], message); err != nil {
   247  			return err
   248  		}
   249  	}
   250  
   251  	return nil
   252  }
   253  
   254  func (e errorOutput) Dump() any {
   255  	if f, ok := e.error.(task.Error); ok {
   256  		return f.LocalizedMethodFault
   257  	}
   258  	if soap.IsSoapFault(e.error) {
   259  		return soap.ToSoapFault(e.error)
   260  	}
   261  	if soap.IsVimFault(e.error) {
   262  		return soap.ToVimFault(e.error)
   263  	}
   264  	return e
   265  }
   266  
   267  func (e errorOutput) canEncode() bool {
   268  	switch e.error.(type) {
   269  	case task.Error:
   270  		return true
   271  	}
   272  	return soap.IsSoapFault(e.error) || soap.IsVimFault(e.error)
   273  }
   274  
   275  // errCannotEncode causes cli.Run to output err.Error() as it would without an error format specified
   276  var errCannotEncode = errors.New("cannot encode error")
   277  
   278  func (e errorOutput) MarshalJSON() ([]byte, error) {
   279  	_, ok := e.error.(json.Marshaler)
   280  	if ok || e.canEncode() {
   281  		return json.Marshal(e.error)
   282  	}
   283  	return nil, errCannotEncode
   284  }
   285  
   286  func (e errorOutput) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error {
   287  	_, ok := e.error.(xml.Marshaler)
   288  	if ok || e.canEncode() {
   289  		return encoder.Encode(e.error)
   290  	}
   291  	return errCannotEncode
   292  }
   293  
   294  func (flag *OutputFlag) ProgressLogger(prefix string) *progress.ProgressLogger {
   295  	return progress.NewProgressLogger(flag.Log, prefix)
   296  }