github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/replyfmt/console.go (about)

     1  // Copyright (c) 2020-2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package replyfmt
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"sort"
    12  	"strings"
    13  
    14  	"github.com/fatih/color"
    15  	"github.com/tidwall/gjson"
    16  	"github.com/tidwall/pretty"
    17  
    18  	"github.com/choria-io/go-choria/providers/agent/mcorpc"
    19  	"github.com/choria-io/go-choria/providers/agent/mcorpc/client"
    20  )
    21  
    22  type ConsoleFormatter struct {
    23  	verbose         bool
    24  	silent          bool
    25  	disableColor    bool
    26  	displayOverride DisplayMode
    27  
    28  	actionInterface ActionDDL
    29  	out             io.Writer
    30  }
    31  
    32  type statusString struct {
    33  	color string
    34  	plain string
    35  }
    36  
    37  var statusStings = map[mcorpc.StatusCode]statusString{
    38  	mcorpc.OK:            {"", ""},
    39  	mcorpc.Aborted:       {color.RedString("Request Aborted"), "Request Aborted"},
    40  	mcorpc.InvalidData:   {color.YellowString("Invalid Request Data"), "Invalid Request Data"},
    41  	mcorpc.MissingData:   {color.YellowString("Missing Request Data"), "Missing Request Data"},
    42  	mcorpc.UnknownAction: {color.YellowString("Unknown Action"), "Unknown Action"},
    43  	mcorpc.UnknownError:  {color.RedString("Unknown Request Status"), "Unknown Request Status"},
    44  }
    45  
    46  func NewConsoleFormatter(opts ...Option) *ConsoleFormatter {
    47  	f := &ConsoleFormatter{}
    48  
    49  	for _, opt := range opts {
    50  		opt(f)
    51  	}
    52  
    53  	return f
    54  }
    55  
    56  // ConsoleNoColor disables color in the console formatter
    57  func ConsoleNoColor() Option {
    58  	return func(f Formatter) error {
    59  		i, ok := f.(*ConsoleFormatter)
    60  		if !ok {
    61  			return fmt.Errorf("formatter is not a ConsoleFormatter")
    62  		}
    63  
    64  		i.disableColor = true
    65  
    66  		return nil
    67  	}
    68  }
    69  
    70  func (c *ConsoleFormatter) FormatAggregates(w io.Writer, action ActionDDL) error {
    71  	summaries, err := action.AggregateSummaryFormattedStrings()
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	var keys []string
    77  	for k := range summaries {
    78  		keys = append(keys, k)
    79  	}
    80  	sort.Strings(keys)
    81  
    82  	for _, k := range keys {
    83  		descr := k
    84  		output, ok := action.GetOutput(k)
    85  		if ok {
    86  			descr = output.DisplayAs
    87  		}
    88  
    89  		if c.disableColor {
    90  			fmt.Fprintf(w, "Summary of %s:\n\n", descr)
    91  
    92  		} else {
    93  			fmt.Fprintln(w, color.HiWhiteString("Summary of %s:\n", descr))
    94  		}
    95  
    96  		if len(summaries[k]) == 0 {
    97  			if c.disableColor {
    98  				fmt.Fprintf(w, "   No summary received\n\n")
    99  			} else {
   100  				fmt.Fprintf(w, "   %s\n\n", color.YellowString("No summary received"))
   101  			}
   102  
   103  			continue
   104  		}
   105  
   106  		for _, v := range summaries[k] {
   107  			if strings.ContainsRune(v, '\n') {
   108  				fmt.Fprintln(w, v)
   109  			} else {
   110  				fmt.Fprintf(w, "   %s\n", v)
   111  			}
   112  
   113  		}
   114  		fmt.Fprintln(w)
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  func (c *ConsoleFormatter) FormatReply(w io.Writer, action ActionDDL, sender string, reply *client.RPCReply) error {
   121  	c.out = w
   122  	c.actionInterface = action
   123  
   124  	if !c.shouldDisplayReply(reply) {
   125  		return nil
   126  	}
   127  
   128  	f, ok := w.(flusher)
   129  	if ok {
   130  		f.Flush()
   131  	}
   132  
   133  	c.writeHeader(sender, reply)
   134  
   135  	if c.verbose {
   136  		c.basicPrinter(reply)
   137  		return nil
   138  	}
   139  
   140  	if reply.Statuscode > mcorpc.OK {
   141  		c.errorPrinter(reply)
   142  		return nil
   143  	}
   144  
   145  	c.ddlAssistedPrinter(reply)
   146  
   147  	return nil
   148  }
   149  
   150  func (c *ConsoleFormatter) SetVerbose() {
   151  	c.verbose = true
   152  }
   153  
   154  func (c *ConsoleFormatter) SetSilent() {
   155  	c.silent = true
   156  }
   157  
   158  func (c *ConsoleFormatter) SetDisplay(m DisplayMode) {
   159  	c.displayOverride = m
   160  }
   161  
   162  func (c *ConsoleFormatter) errorPrinter(reply *client.RPCReply) {
   163  	if c.disableColor {
   164  		fmt.Fprintf(c.out, "    %s\n", reply.Statusmsg)
   165  	} else {
   166  		fmt.Fprintf(c.out, "    %s\n", color.YellowString(reply.Statusmsg))
   167  	}
   168  
   169  	fmt.Fprintln(c.out)
   170  }
   171  
   172  func (c *ConsoleFormatter) writeHeader(sender string, reply *client.RPCReply) {
   173  	ss := statusStings[reply.Statuscode]
   174  	smsg := "%-40s %s\n\n"
   175  	if c.disableColor {
   176  		fmt.Fprintf(c.out, smsg, sender, ss.color)
   177  	} else {
   178  		fmt.Fprintf(c.out, smsg, sender, ss.plain)
   179  	}
   180  }
   181  
   182  func (c *ConsoleFormatter) ddlAssistedPrinter(reply *client.RPCReply) {
   183  	max := 0
   184  	keys := []string{}
   185  
   186  	parsed, ok := gjson.ParseBytes(reply.Data).Value().(map[string]any)
   187  	if ok {
   188  		c.actionInterface.SetOutputDefaults(parsed)
   189  	}
   190  
   191  	for key := range parsed {
   192  		output, ok := c.actionInterface.GetOutput(key)
   193  		if ok {
   194  			if len(output.DisplayAs) > max {
   195  				max = len(output.DisplayAs)
   196  			}
   197  		} else {
   198  			if len(key) > max {
   199  				max = len(key)
   200  			}
   201  		}
   202  
   203  		keys = append(keys, key)
   204  	}
   205  
   206  	formatStr := fmt.Sprintf("%%%ds: %%s\n", max+3)
   207  	prefixFormatStr := fmt.Sprintf("%%%ds", max+5)
   208  
   209  	sort.Strings(keys)
   210  
   211  	for _, key := range keys {
   212  		val := gjson.GetBytes(reply.Data, key)
   213  		keyStr := key
   214  		valStr := val.String()
   215  
   216  		output, ok := c.actionInterface.GetOutput(key)
   217  		if ok {
   218  			keyStr = output.DisplayAs
   219  		}
   220  
   221  		if val.IsArray() || val.IsObject() {
   222  			valStr = string(pretty.PrettyOptions([]byte(valStr), &pretty.Options{
   223  				SortKeys: true,
   224  				Prefix:   fmt.Sprintf(prefixFormatStr, " "),
   225  				Indent:   "  ",
   226  				Width:    80,
   227  			}))
   228  		}
   229  
   230  		fmt.Fprintf(c.out, formatStr, keyStr, strings.TrimSpace(valStr))
   231  	}
   232  
   233  	fmt.Fprintln(c.out)
   234  }
   235  
   236  func (c *ConsoleFormatter) basicPrinter(reply *client.RPCReply) {
   237  	j, err := json.MarshalIndent(reply.Data, "   ", "   ")
   238  	if err != nil {
   239  		fmt.Fprintf(c.out, "   %s\n", string(reply.Data))
   240  	}
   241  
   242  	fmt.Fprintf(c.out, "   %s\n", string(j))
   243  }
   244  
   245  func (c *ConsoleFormatter) shouldDisplayReply(reply *client.RPCReply) bool {
   246  	switch c.displayOverride {
   247  	case DisplayDDL:
   248  		displayMode := c.actionInterface.DisplayMode()
   249  
   250  		if reply.Statuscode > mcorpc.OK && displayMode == "failed" {
   251  			return true
   252  		} else if reply.Statuscode > mcorpc.OK && displayMode == "" {
   253  			return true
   254  		} else if displayMode == "ok" && reply.Statuscode == mcorpc.OK {
   255  			return true
   256  		} else if displayMode == "always" {
   257  			return true
   258  		}
   259  	case DisplayOK:
   260  		if reply.Statuscode == mcorpc.OK {
   261  			return true
   262  		}
   263  	case DisplayFailed:
   264  		if reply.Statuscode > mcorpc.OK {
   265  			return true
   266  		}
   267  	case DisplayAll:
   268  		return true
   269  	case DisplayNone:
   270  		return false
   271  	}
   272  
   273  	return false
   274  }