* 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
187 lines
4.2 KiB
Go
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()
|
|
}
|