dive/runtime/ci/evaluator.go
Alex Goodman d5e8a92968
Rework CI validation workflow and makefile (#460)
* rework CI validation workflow and makefile

* enable push

* fix job names

* fix license check

* fix snapshot builds

* fix acceptance tests

* fix linting

* disable pull request event

* rework windows runner caching

* disable release pipeline and add issue templates
2023-07-06 22:01:46 -04:00

187 lines
4.2 KiB
Go

package ci
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/dustin/go-humanize"
"github.com/logrusorgru/aurora"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/utils"
)
type CiEvaluator struct {
Rules []CiRule
Results map[string]RuleResult
Tally ResultTally
Pass bool
Misconfigured bool
InefficientFiles []ReferenceFile
}
type ResultTally struct {
Pass int
Fail int
Skip int
Warn int
Total int
}
func NewCiEvaluator(config *viper.Viper) *CiEvaluator {
return &CiEvaluator{
Rules: loadCiRules(config),
Results: make(map[string]RuleResult),
Pass: true,
}
}
func (ci *CiEvaluator) isRuleEnabled(rule CiRule) bool {
return rule.Configuration() != "disabled"
}
func (ci *CiEvaluator) Evaluate(analysis *image.AnalysisResult) bool {
canEvaluate := true
for _, rule := range ci.Rules {
if !ci.isRuleEnabled(rule) {
ci.Results[rule.Key()] = RuleResult{
status: RuleConfigured,
message: "rule disabled",
}
continue
}
err := rule.Validate()
if err != nil {
ci.Results[rule.Key()] = RuleResult{
status: RuleMisconfigured,
message: err.Error(),
}
canEvaluate = false
} else {
ci.Results[rule.Key()] = RuleResult{
status: RuleConfigured,
message: "test",
}
}
}
if !canEvaluate {
ci.Pass = false
ci.Misconfigured = true
return ci.Pass
}
// capture inefficient files
for idx := 0; idx < len(analysis.Inefficiencies); idx++ {
fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx]
ci.InefficientFiles = append(ci.InefficientFiles, ReferenceFile{
References: len(fileData.Nodes),
SizeBytes: uint64(fileData.CumulativeSize),
Path: fileData.Path,
})
}
// evaluate results against the configured CI rules
for _, rule := range ci.Rules {
if !ci.isRuleEnabled(rule) {
ci.Results[rule.Key()] = RuleResult{
status: RuleDisabled,
message: "rule disabled",
}
continue
}
status, message := rule.Evaluate(analysis)
if value, exists := ci.Results[rule.Key()]; exists && value.status != RuleConfigured && value.status != RuleMisconfigured {
panic(fmt.Errorf("CI rule result recorded twice: %s", rule.Key()))
}
if status == RuleFailed {
ci.Pass = false
}
ci.Results[rule.Key()] = RuleResult{
status: status,
message: message,
}
}
ci.Tally.Total = len(ci.Results)
for rule, result := range ci.Results {
switch result.status {
case RulePassed:
ci.Tally.Pass++
case RuleFailed:
ci.Tally.Fail++
case RuleWarning:
ci.Tally.Warn++
case RuleDisabled:
ci.Tally.Skip++
default:
panic(fmt.Errorf("unknown test status (rule='%v'): %v", rule, result.status))
}
}
return ci.Pass
}
func (ci *CiEvaluator) Report() string {
var sb strings.Builder
fmt.Fprintln(&sb, utils.TitleFormat("Inefficient Files:"))
template := "%5s %12s %-s\n"
fmt.Fprintf(&sb, template, "Count", "Wasted Space", "File Path")
if len(ci.InefficientFiles) == 0 {
fmt.Fprintln(&sb, "None")
} else {
for _, file := range ci.InefficientFiles {
fmt.Fprintf(&sb, template, strconv.Itoa(file.References), humanize.Bytes(file.SizeBytes), file.Path)
}
}
fmt.Fprintln(&sb, utils.TitleFormat("Results:"))
status := "PASS"
rules := make([]string, 0, len(ci.Results))
for name := range ci.Results {
rules = append(rules, name)
}
sort.Strings(rules)
if ci.Tally.Fail > 0 {
status = "FAIL"
}
for _, rule := range rules {
result := ci.Results[rule]
name := strings.TrimPrefix(rule, "rules.")
if result.message != "" {
fmt.Fprintf(&sb, " %s: %s: %s\n", result.status.String(), name, result.message)
} else {
fmt.Fprintf(&sb, " %s: %s\n", result.status.String(), name)
}
}
if ci.Misconfigured {
fmt.Fprintln(&sb, aurora.Red("CI Misconfigured"))
} else {
summary := fmt.Sprintf("Result:%s [Total:%d] [Passed:%d] [Failed:%d] [Warn:%d] [Skipped:%d]", status, ci.Tally.Total, ci.Tally.Pass, ci.Tally.Fail, ci.Tally.Warn, ci.Tally.Skip)
if ci.Pass {
fmt.Fprintln(&sb, aurora.Green(summary))
} else if ci.Pass && ci.Tally.Warn > 0 {
fmt.Fprintln(&sb, aurora.Blue(summary))
} else {
fmt.Fprintln(&sb, aurora.Red(summary))
}
}
return sb.String()
}