github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/scout/cmd/validate.go (about) 1 // Copyright (c) 2022, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package scoutcmd 6 7 import ( 8 "bytes" 9 "context" 10 "encoding/json" 11 "fmt" 12 "os" 13 "sort" 14 "strings" 15 "sync" 16 "time" 17 18 "github.com/choria-io/go-choria/client/discovery" 19 "github.com/choria-io/go-choria/client/scoutclient" 20 "github.com/choria-io/go-choria/inter" 21 iu "github.com/choria-io/go-choria/internal/util" 22 scoutagent "github.com/choria-io/go-choria/scout/agent/scout" 23 "github.com/goss-org/goss" 24 gossoutputs "github.com/goss-org/goss/outputs" 25 "github.com/goss-org/goss/resource" 26 gossutil "github.com/goss-org/goss/util" 27 "github.com/sirupsen/logrus" 28 xtablewriter "github.com/xlab/tablewriter" 29 ) 30 31 type ValidateCommandOptions struct { 32 Variables []byte 33 NodeVarsFile string 34 Rules []byte 35 NodeRulesFile string 36 Display string 37 Table bool 38 Verbose bool 39 Json bool 40 Color bool 41 Local bool 42 } 43 44 type ValidateCommand struct { 45 sopts *discovery.StandardOptions 46 log *logrus.Entry 47 fw inter.Framework 48 opts *ValidateCommandOptions 49 } 50 51 func NewValidateCommand(sopts *discovery.StandardOptions, fw inter.Framework, opts *ValidateCommandOptions, log *logrus.Entry) (*ValidateCommand, error) { 52 return &ValidateCommand{ 53 sopts: sopts, 54 log: log, 55 fw: fw, 56 opts: opts, 57 }, nil 58 } 59 60 func (v *ValidateCommand) renderTableResult(table *xtablewriter.Table, vr *scoutagent.GossValidateResponse, reqOk bool, sender string, statusMsg string) bool { 61 fail := v.fw.Colorize("red", "X") 62 ok := v.fw.Colorize("green", "✓") 63 skip := v.fw.Colorize("yellow", "?") 64 errm := v.fw.Colorize("red", "!") 65 66 should := false 67 68 if !reqOk { 69 table.AddRow(fail, sender, "", "", statusMsg) 70 return true 71 } 72 73 if vr.Failures > 0 || vr.Tests == 0 { 74 should = true 75 table.AddRow(fail, sender, "", "", vr.Summary) 76 } else { 77 should = true 78 table.AddRow(ok, sender, "", "", vr.Summary) 79 } 80 81 sort.Slice(vr.Results, func(i, j int) bool { 82 return !vr.Results[i].Successful || vr.Results[i].Err != nil 83 }) 84 85 if v.opts.Display == "none" { 86 return should 87 } 88 89 for _, res := range vr.Results { 90 should = true 91 92 if res.Err != nil { 93 table.AddRow(errm, "", res.ResourceType, res.ResourceId, res.Err.Error()) 94 continue 95 } 96 97 switch { 98 case res.Result == resource.SKIP && v.opts.Display != "ok": 99 table.AddRow(skip, "", res.ResourceType, res.ResourceId, fmt.Sprintf("%s: skipped", res.Property)) 100 case res.Result == resource.SUCCESS && v.opts.Display != "failed": 101 table.AddRow(ok, "", res.ResourceType, res.ResourceId, res.SummaryLineCompact) 102 case res.Result == resource.FAIL && v.opts.Display != "ok": 103 table.AddRow(fail, "", res.ResourceType, res.ResourceId, res.SummaryLineCompact) 104 } 105 } 106 107 return should 108 } 109 110 func (v *ValidateCommand) renderTextResult(vr *scoutagent.GossValidateResponse, reqOk bool, sender string, statusMsg string) { 111 if !reqOk { 112 fmt.Printf("%s: %s\n\n", sender, v.fw.Colorize("red", statusMsg)) 113 return 114 } 115 116 if vr.Failures > 0 || vr.Tests == 0 { 117 fmt.Printf("%s: %s\n\n", sender, v.fw.Colorize("red", vr.Summary)) 118 } else { 119 fmt.Printf("%s: %s\n\n", sender, v.fw.Colorize("green", vr.Summary)) 120 } 121 122 sort.Slice(vr.Results, func(i, j int) bool { 123 return !vr.Results[i].Successful 124 }) 125 126 if v.opts.Display == "none" { 127 fmt.Println() 128 return 129 } 130 131 lb := false 132 for i, res := range vr.Results { 133 switch { 134 case res.Result == resource.SKIP && v.opts.Display != "ok": 135 if lb { 136 fmt.Println() 137 } 138 fmt.Printf(" %s %s\n", v.fw.Colorize("yellow", "?"), res.SummaryLineCompact) 139 lb = false 140 case res.Result == resource.FAIL && v.opts.Display != "ok": 141 if i != 0 { 142 fmt.Println() 143 } 144 lb = true 145 msg := fmt.Sprintf("%s %s", v.fw.Colorize("red", "X"), res.SummaryLine) 146 fmt.Printf("%s\n", iu.ParagraphPadding(msg, 3)) 147 case res.Result == resource.SUCCESS && v.opts.Display != "failed": 148 if lb { 149 fmt.Println() 150 } 151 152 fmt.Printf(" %s %s\n", v.fw.Colorize("green", "✓"), res.SummaryLineCompact) 153 154 lb = false 155 } 156 } 157 158 fmt.Println() 159 } 160 161 func (v *ValidateCommand) localValidate() error { 162 var err error 163 var out bytes.Buffer 164 var table *xtablewriter.Table 165 var shouldRenderTable bool 166 167 rules, err := os.CreateTemp("", "choria-gossfile-*.yaml") 168 if err != nil { 169 return err 170 } 171 defer os.Remove(rules.Name()) 172 defer rules.Close() 173 174 _, err = rules.Write(v.opts.Rules) 175 if err != nil { 176 return err 177 } 178 rules.Close() 179 180 opts := []gossutil.ConfigOption{ 181 gossutil.WithMaxConcurrency(1), 182 gossutil.WithResultWriter(&out), 183 gossutil.WithSpecFile(rules.Name()), 184 } 185 186 if len(v.opts.Variables) > 0 { 187 opts = append(opts, gossutil.WithVarsBytes(v.opts.Variables)) 188 } 189 190 cfg, err := gossutil.NewConfig(opts...) 191 if err != nil { 192 return err 193 } 194 195 _, err = goss.Validate(cfg) 196 if err != nil { 197 return err 198 } 199 200 res := &gossoutputs.StructuredOutput{} 201 err = json.Unmarshal(out.Bytes(), res) 202 if err != nil { 203 return err 204 } 205 206 resp := &scoutagent.GossValidateResponse{Results: []gossoutputs.StructuredTestResult{}} 207 208 var errors int 209 for _, r := range res.Results { 210 switch { 211 case r.Err != nil: 212 errors++ 213 case r.Result == resource.SKIP: 214 resp.Skipped++ 215 } 216 } 217 218 resp.Results = res.Results 219 resp.Summary = res.SummaryLine 220 resp.Failures = res.Summary.Failed + errors 221 resp.Runtime = res.Summary.TotalDuration.Seconds() 222 resp.Success = res.Summary.TestCount - res.Summary.Failed - resp.Skipped 223 resp.Tests = res.Summary.TestCount 224 225 if v.opts.Table { 226 table = iu.NewUTF8TableWithTitle("Goss check results", "", "Node", "Resource", "ID", "State") 227 } 228 229 if v.opts.Table { 230 shouldRenderTable = v.renderTableResult(table, resp, true, "localhost", "OK") 231 } else { 232 v.renderTextResult(resp, true, "localhost", "OK") 233 } 234 235 if v.opts.Table && shouldRenderTable { 236 fmt.Println(table.Render()) 237 } 238 239 return nil 240 } 241 242 func (v *ValidateCommand) Run(ctx context.Context, wg *sync.WaitGroup) error { 243 defer wg.Done() 244 245 if v.opts.NodeRulesFile == "" && len(v.opts.Rules) == 0 { 246 return fmt.Errorf("neither local validation rules nor a remote file were supplied") 247 } 248 if v.opts.NodeRulesFile != "" && len(v.opts.Rules) > 0 { 249 return fmt.Errorf("both local validation rules and a remote rules file were supplied") 250 } 251 if len(v.opts.Variables) > 0 && v.opts.NodeVarsFile != "" { 252 return fmt.Errorf("both local variables and a remote variables file were supplied") 253 } 254 255 if v.opts.Local { 256 return v.localValidate() 257 } 258 259 sc, err := scoutClient(v.fw, v.sopts, v.log) 260 if err != nil { 261 return err 262 } 263 264 action := sc.GossValidate() 265 if v.opts.NodeRulesFile != "" { 266 action.File(v.opts.NodeRulesFile) 267 } else if len(v.opts.Rules) > 0 { 268 action.YamlRules(string(v.opts.Rules)) 269 } else { 270 return fmt.Errorf("no rules or rules file specified") 271 } 272 273 if len(v.opts.Variables) > 0 { 274 action.YamlVars(string(v.opts.Variables)) 275 } else if v.opts.NodeVarsFile != "" { 276 action.Vars(v.opts.NodeVarsFile) 277 } 278 279 start := time.Now() 280 result, err := action.Do(ctx) 281 if err != nil { 282 return err 283 } 284 runTime := time.Since(start) 285 286 if v.opts.Json { 287 return result.RenderResults(os.Stdout, scoutclient.JSONFormat, scoutclient.DisplayDDL, v.opts.Verbose, false, v.opts.Color, v.log) 288 } 289 290 if result.Stats().ResponsesCount() == 0 { 291 return fmt.Errorf("no responses received") 292 } 293 294 count := 0 295 failed := 0 296 success := 0 297 skipped := 0 298 nodes := 0 299 shouldRenderTable := false 300 301 var table *xtablewriter.Table 302 if v.opts.Table { 303 table = iu.NewUTF8TableWithTitle("Goss check results", "", "Node", "Resource", "ID", "State") 304 } 305 306 result.EachOutput(func(r *scoutclient.GossValidateOutput) { 307 vr := &scoutagent.GossValidateResponse{} 308 err = r.ParseGossValidateOutput(vr) 309 if err != nil { 310 v.log.Errorf("Could not parse output from %s: %s", r.ResultDetails().Sender(), err) 311 return 312 } 313 314 nodes++ 315 count += vr.Tests 316 failed += vr.Failures 317 success += vr.Success 318 skipped += vr.Skipped 319 if !r.ResultDetails().OK() { 320 failed++ 321 } 322 323 switch v.opts.Display { 324 case "none": 325 return 326 case "all": 327 case "ok": 328 // skip on not ok 329 if !r.ResultDetails().OK() || vr.Tests == 0 || vr.Failures > 0 || vr.Skipped > 0 { 330 return 331 } 332 case "failed": 333 // skip all ok 334 if r.ResultDetails().OK() && vr.Tests > 0 && vr.Failures == 0 && vr.Skipped == 0 { 335 return 336 } 337 } 338 339 if v.opts.Table { 340 shouldRenderTable = v.renderTableResult(table, vr, r.ResultDetails().OK(), r.ResultDetails().Sender(), r.ResultDetails().StatusMessage()) 341 } else { 342 v.renderTextResult(vr, r.ResultDetails().OK(), r.ResultDetails().Sender(), r.ResultDetails().StatusMessage()) 343 } 344 }) 345 346 if v.opts.Table && shouldRenderTable { 347 fmt.Println(table.Render()) 348 } 349 350 parts := []string{ 351 fmt.Sprintf("Nodes: %d", nodes), 352 } 353 if failed > 0 { 354 parts = append(parts, v.fw.Colorize("red", fmt.Sprintf("Failed: %d", failed))) 355 } else { 356 parts = append(parts, v.fw.Colorize("green", fmt.Sprintf("Failed: %d", failed))) 357 } 358 if skipped > 0 { 359 parts = append(parts, v.fw.Colorize("yellow", fmt.Sprintf("Skipped: %d", skipped))) 360 } else { 361 parts = append(parts, v.fw.Colorize("green", fmt.Sprintf("Skipped: %d", skipped))) 362 } 363 if success > 0 { 364 parts = append(parts, v.fw.Colorize("green", fmt.Sprintf("Success: %d", success))) 365 } else { 366 parts = append(parts, v.fw.Colorize("red", fmt.Sprintf("Success: %d", success))) 367 } 368 parts = append(parts, fmt.Sprintf("Duration: %v", runTime.Round(time.Millisecond))) 369 370 fmt.Printf("%s\n", strings.Join(parts, ", ")) 371 372 if v.opts.Verbose { 373 return result.RenderResults(os.Stdout, scoutclient.TXTFooter, scoutclient.DisplayDDL, v.opts.Verbose, false, v.opts.Color, v.log) 374 } 375 376 return nil 377 }