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 }