github.com/vmware/govmomi@v0.43.0/govc/flags/output.go (about)

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