Expose CI validation options to the CLI (#212)
* expose ci config to cli switches; addresses #198 * added/updated tests for ci configuration
This commit is contained in:
parent
efc67354ac
commit
e4ddfb411c
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,3 +21,6 @@
|
|||||||
/dist
|
/dist
|
||||||
.cover
|
.cover
|
||||||
coverage.txt
|
coverage.txt
|
||||||
|
|
||||||
|
# ignore the binary
|
||||||
|
dive
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
// image analysis to the screen
|
// image analysis to the screen
|
||||||
func doAnalyzeCmd(cmd *cobra.Command, args []string) {
|
func doAnalyzeCmd(cmd *cobra.Command, args []string) {
|
||||||
defer utils.Cleanup()
|
defer utils.Cleanup()
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
printVersionFlag, err := cmd.PersistentFlags().GetBool("version")
|
printVersionFlag, err := cmd.PersistentFlags().GetBool("version")
|
||||||
if err == nil && printVersionFlag {
|
if err == nil && printVersionFlag {
|
||||||
@ -20,22 +21,28 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("No image argument given")
|
fmt.Println("No image argument given")
|
||||||
_ = cmd.Help()
|
|
||||||
utils.Exit(1)
|
utils.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
userImage := args[0]
|
userImage := args[0]
|
||||||
if userImage == "" {
|
if userImage == "" {
|
||||||
fmt.Println("No image argument given")
|
fmt.Println("No image argument given")
|
||||||
_ = cmd.Help()
|
|
||||||
utils.Exit(1)
|
utils.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
initLogging()
|
initLogging()
|
||||||
|
|
||||||
|
isCi, ciConfig, err := configureCi()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("ci configuration error: %v\n", err)
|
||||||
|
utils.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
runtime.Run(runtime.Options{
|
runtime.Run(runtime.Options{
|
||||||
ImageId: userImage,
|
Ci: isCi,
|
||||||
ExportFile: exportFile,
|
ImageId: userImage,
|
||||||
CiConfigFile: ciConfigFile,
|
ExportFile: exportFile,
|
||||||
|
CiConfig: ciConfig,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,9 @@ func doBuildCmd(cmd *cobra.Command, args []string) {
|
|||||||
initLogging()
|
initLogging()
|
||||||
|
|
||||||
runtime.Run(runtime.Options{
|
runtime.Run(runtime.Options{
|
||||||
|
Ci: isCi,
|
||||||
BuildArgs: args,
|
BuildArgs: args,
|
||||||
ExportFile: exportFile,
|
ExportFile: exportFile,
|
||||||
|
CiConfig: ciConfig,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
39
cmd/ci.go
Normal file
39
cmd/ci.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func configureCi() (bool, *viper.Viper, error) {
|
||||||
|
|
||||||
|
isCiFromEnv, _ := strconv.ParseBool(os.Getenv("CI"))
|
||||||
|
isCi = isCi || isCiFromEnv
|
||||||
|
|
||||||
|
if isCi {
|
||||||
|
ciConfig.SetConfigType("yaml")
|
||||||
|
|
||||||
|
if _, err := os.Stat(ciConfigFile); !os.IsNotExist(err) {
|
||||||
|
fmt.Printf(" Using CI config: %s\n", ciConfigFile)
|
||||||
|
|
||||||
|
fileBytes, err := ioutil.ReadFile(ciConfigFile)
|
||||||
|
if err != nil {
|
||||||
|
return isCi, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ciConfig.ReadConfig(bytes.NewBuffer(fileBytes))
|
||||||
|
if err != nil {
|
||||||
|
return isCi, nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println(" Using default CI config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isCi, ciConfig, nil
|
||||||
|
}
|
19
cmd/root.go
19
cmd/root.go
@ -18,6 +18,8 @@ import (
|
|||||||
var cfgFile string
|
var cfgFile string
|
||||||
var exportFile string
|
var exportFile string
|
||||||
var ciConfigFile string
|
var ciConfigFile string
|
||||||
|
var ciConfig = viper.New()
|
||||||
|
var isCi bool
|
||||||
|
|
||||||
// rootCmd represents the base command when called without any subcommands
|
// rootCmd represents the base command when called without any subcommands
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@ -39,13 +41,26 @@ func Execute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
initCli()
|
||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initCli() {
|
||||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml, ~/.config/dive/*.yaml, or $XDG_CONFIG_HOME/dive.yaml)")
|
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml, ~/.config/dive/*.yaml, or $XDG_CONFIG_HOME/dive.yaml)")
|
||||||
rootCmd.PersistentFlags().BoolP("version", "v", false, "display version number")
|
rootCmd.PersistentFlags().BoolP("version", "v", false, "display version number")
|
||||||
|
rootCmd.Flags().BoolVar(&isCi, "ci", false, "Skip the interactive TUI and validate against CI rules (same as env var CI=true)")
|
||||||
rootCmd.Flags().StringVarP(&exportFile, "json", "j", "", "Skip the interactive TUI and write the layer analysis statistics to a given file.")
|
rootCmd.Flags().StringVarP(&exportFile, "json", "j", "", "Skip the interactive TUI and write the layer analysis statistics to a given file.")
|
||||||
rootCmd.Flags().StringVar(&ciConfigFile, "ci-config", ".dive-ci", "If CI=true in the environment, use the given yaml to drive validation rules.")
|
rootCmd.Flags().StringVar(&ciConfigFile, "ci-config", ".dive-ci", "If CI=true in the environment, use the given yaml to drive validation rules.")
|
||||||
|
|
||||||
|
rootCmd.Flags().String("lowestEfficiency", "0.9", "(only valid with --ci given) lowest allowable image efficiency, otherwise CI validation will fail.")
|
||||||
|
rootCmd.Flags().String("highestWastedBytes", "disabled", "(only valid with --ci given) highest allowable bytes wasted, otherwise CI validation will fail.")
|
||||||
|
rootCmd.Flags().String("highestUserWastedPercent", "0.1", "(only valid with --ci given) highest allowable percentage of bytes wasted, otherwise CI validation will fail.")
|
||||||
|
|
||||||
|
for _, key := range []string{"lowestEfficiency", "highestWastedBytes", "highestUserWastedPercent"} {
|
||||||
|
if err := ciConfig.BindPFlag(fmt.Sprintf("rules.%s", key), rootCmd.Flags().Lookup(key)); err != nil {
|
||||||
|
log.Fatal("Unable to bind flag:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// initConfig reads in config file and ENV variables if set.
|
// initConfig reads in config file and ENV variables if set.
|
||||||
@ -55,7 +70,7 @@ func initConfig() {
|
|||||||
|
|
||||||
viper.SetDefault("log.level", log.InfoLevel.String())
|
viper.SetDefault("log.level", log.InfoLevel.String())
|
||||||
viper.SetDefault("log.path", "./dive.log")
|
viper.SetDefault("log.path", "./dive.log")
|
||||||
viper.SetDefault("log.enabled", true)
|
viper.SetDefault("log.enabled", false)
|
||||||
// keybindings: status view / global
|
// keybindings: status view / global
|
||||||
viper.SetDefault("keybinding.quit", "ctrl+c")
|
viper.SetDefault("keybinding.quit", "ctrl+c")
|
||||||
viper.SetDefault("keybinding.toggle-view", "tab")
|
viper.SetDefault("keybinding.toggle-view", "tab")
|
||||||
|
@ -1,76 +1,101 @@
|
|||||||
package ci
|
package ci
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/logrusorgru/aurora"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"github.com/logrusorgru/aurora"
|
||||||
"github.com/wagoodman/dive/image"
|
"github.com/wagoodman/dive/image"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewEvaluator() *Evaluator {
|
type Evaluator struct {
|
||||||
ciConfig := viper.New()
|
Rules []Rule
|
||||||
ciConfig.SetConfigType("yaml")
|
Results map[string]RuleResult
|
||||||
|
Tally ResultTally
|
||||||
|
Pass bool
|
||||||
|
Misconfigured bool
|
||||||
|
}
|
||||||
|
|
||||||
ciConfig.SetDefault("rules.lowestEfficiency", 0.9)
|
type ResultTally struct {
|
||||||
ciConfig.SetDefault("rules.highestWastedBytes", "disabled")
|
Pass int
|
||||||
ciConfig.SetDefault("rules.highestUserWastedPercent", 0.1)
|
Fail int
|
||||||
|
Skip int
|
||||||
|
Warn int
|
||||||
|
Total int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEvaluator(config *viper.Viper) *Evaluator {
|
||||||
return &Evaluator{
|
return &Evaluator{
|
||||||
Config: ciConfig,
|
Rules: loadCiRules(config),
|
||||||
Rules: loadCiRules(),
|
|
||||||
Results: make(map[string]RuleResult),
|
Results: make(map[string]RuleResult),
|
||||||
Pass: true,
|
Pass: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ci *Evaluator) LoadConfig(configFile string) error {
|
|
||||||
fileBytes, err := ioutil.ReadFile(configFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ci.Config.ReadConfig(bytes.NewBuffer(fileBytes))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *Evaluator) isRuleEnabled(rule Rule) bool {
|
func (ci *Evaluator) isRuleEnabled(rule Rule) bool {
|
||||||
value := ci.Config.GetString(rule.Key())
|
return rule.Configuration() != "disabled"
|
||||||
return value != "disabled"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ci *Evaluator) Evaluate(analysis *image.AnalysisResult) bool {
|
func (ci *Evaluator) Evaluate(analysis *image.AnalysisResult) bool {
|
||||||
|
canEvaluate := true
|
||||||
for _, rule := range ci.Rules {
|
for _, rule := range ci.Rules {
|
||||||
if ci.isRuleEnabled(rule) {
|
if !ci.isRuleEnabled(rule) {
|
||||||
|
|
||||||
value := ci.Config.GetString(rule.Key())
|
|
||||||
status, message := rule.Evaluate(analysis, value)
|
|
||||||
|
|
||||||
if _, exists := ci.Results[rule.Key()]; exists {
|
|
||||||
panic(fmt.Errorf("CI rule result recorded twice: %s", rule.Key()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if status == RuleFailed {
|
|
||||||
ci.Pass = false
|
|
||||||
}
|
|
||||||
|
|
||||||
ci.Results[rule.Key()] = RuleResult{
|
ci.Results[rule.Key()] = RuleResult{
|
||||||
status: status,
|
status: RuleConfigured,
|
||||||
message: message,
|
message: "rule disabled",
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := rule.Validate()
|
||||||
|
if err != nil {
|
||||||
|
ci.Results[rule.Key()] = RuleResult{
|
||||||
|
status: RuleMisconfigured,
|
||||||
|
message: err.Error(),
|
||||||
|
}
|
||||||
|
canEvaluate = false
|
||||||
} else {
|
} else {
|
||||||
|
ci.Results[rule.Key()] = RuleResult{
|
||||||
|
status: RuleConfigured,
|
||||||
|
message: "test",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canEvaluate {
|
||||||
|
ci.Pass = false
|
||||||
|
ci.Misconfigured = true
|
||||||
|
return ci.Pass
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range ci.Rules {
|
||||||
|
if !ci.isRuleEnabled(rule) {
|
||||||
ci.Results[rule.Key()] = RuleResult{
|
ci.Results[rule.Key()] = RuleResult{
|
||||||
status: RuleDisabled,
|
status: RuleDisabled,
|
||||||
message: "rule disabled",
|
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)
|
ci.Tally.Total = len(ci.Results)
|
||||||
@ -115,6 +140,11 @@ func (ci *Evaluator) Report() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ci.Misconfigured {
|
||||||
|
fmt.Println(aurora.Red("CI Misconfigured"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
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 {
|
if ci.Pass {
|
||||||
fmt.Println(aurora.Green(summary))
|
fmt.Println(aurora.Green(summary))
|
||||||
|
@ -22,34 +22,37 @@ func Test_Evaluator(t *testing.T) {
|
|||||||
expectedPass bool
|
expectedPass bool
|
||||||
expectedResult map[string]RuleStatus
|
expectedResult map[string]RuleStatus
|
||||||
}{
|
}{
|
||||||
"allFail": {"0.99", "1B", "0.01", false, map[string]RuleStatus{"lowestEfficiency": RuleFailed, "highestWastedBytes": RuleFailed, "highestUserWastedPercent": RuleFailed}},
|
"allFail": {"0.99", "1B", "0.01", false, map[string]RuleStatus{"lowestEfficiency": RuleFailed, "highestWastedBytes": RuleFailed, "highestUserWastedPercent": RuleFailed}},
|
||||||
"allPass": {"0.9", "50kB", "0.1", true, map[string]RuleStatus{"lowestEfficiency": RulePassed, "highestWastedBytes": RulePassed, "highestUserWastedPercent": RulePassed}},
|
"allPass": {"0.9", "50kB", "0.1", true, map[string]RuleStatus{"lowestEfficiency": RulePassed, "highestWastedBytes": RulePassed, "highestUserWastedPercent": RulePassed}},
|
||||||
"allDisabled": {"disabled", "disabled", "disabled", true, map[string]RuleStatus{"lowestEfficiency": RuleDisabled, "highestWastedBytes": RuleDisabled, "highestUserWastedPercent": RuleDisabled}},
|
"allDisabled": {"disabled", "disabled", "disabled", true, map[string]RuleStatus{"lowestEfficiency": RuleDisabled, "highestWastedBytes": RuleDisabled, "highestUserWastedPercent": RuleDisabled}},
|
||||||
|
"misconfiguredHigh": {"1.1", "1BB", "10", false, map[string]RuleStatus{"lowestEfficiency": RuleMisconfigured, "highestWastedBytes": RuleMisconfigured, "highestUserWastedPercent": RuleMisconfigured}},
|
||||||
|
"misconfiguredLow": {"-9", "-1BB", "-0.1", false, map[string]RuleStatus{"lowestEfficiency": RuleMisconfigured, "highestWastedBytes": RuleMisconfigured, "highestUserWastedPercent": RuleMisconfigured}},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range table {
|
for name, test := range table {
|
||||||
evaluator := NewEvaluator()
|
|
||||||
|
|
||||||
ciConfig := viper.New()
|
ciConfig := viper.New()
|
||||||
ciConfig.SetDefault("rules.lowestEfficiency", test.efficiency)
|
ciConfig.SetDefault("rules.lowestEfficiency", test.efficiency)
|
||||||
ciConfig.SetDefault("rules.highestWastedBytes", test.wastedBytes)
|
ciConfig.SetDefault("rules.highestWastedBytes", test.wastedBytes)
|
||||||
ciConfig.SetDefault("rules.highestUserWastedPercent", test.wastedPercent)
|
ciConfig.SetDefault("rules.highestUserWastedPercent", test.wastedPercent)
|
||||||
evaluator.Config = ciConfig
|
|
||||||
|
evaluator := NewEvaluator(ciConfig)
|
||||||
|
|
||||||
pass := evaluator.Evaluate(result)
|
pass := evaluator.Evaluate(result)
|
||||||
|
|
||||||
if test.expectedPass != pass {
|
if test.expectedPass != pass {
|
||||||
|
t.Logf("Test: %s", name)
|
||||||
t.Errorf("Test_Evaluator: expected pass=%v, got %v", test.expectedPass, pass)
|
t.Errorf("Test_Evaluator: expected pass=%v, got %v", test.expectedPass, pass)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(test.expectedResult) != len(evaluator.Results) {
|
if len(test.expectedResult) != len(evaluator.Results) {
|
||||||
|
t.Logf("Test: %s", name)
|
||||||
t.Errorf("Test_Evaluator: expected %v results, got %v", len(test.expectedResult), len(evaluator.Results))
|
t.Errorf("Test_Evaluator: expected %v results, got %v", len(test.expectedResult), len(evaluator.Results))
|
||||||
}
|
}
|
||||||
|
|
||||||
for rule, actualResult := range evaluator.Results {
|
for rule, actualResult := range evaluator.Results {
|
||||||
expectedStatus := test.expectedResult[strings.TrimPrefix(rule, "rules.")]
|
expectedStatus := test.expectedResult[strings.TrimPrefix(rule, "rules.")]
|
||||||
if expectedStatus != actualResult.status {
|
if expectedStatus != actualResult.status {
|
||||||
t.Errorf(" %v: expected %v rule failures, got %v", rule, expectedStatus, actualResult.status)
|
t.Errorf(" %v: expected %v rule failures, got %v: %v", rule, expectedStatus, actualResult.status, actualResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,15 +4,50 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/logrusorgru/aurora"
|
"github.com/logrusorgru/aurora"
|
||||||
"github.com/wagoodman/dive/image"
|
"github.com/wagoodman/dive/image"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newGenericCiRule(key string, evaluator func(*image.AnalysisResult, string) (RuleStatus, string)) *GenericCiRule {
|
const (
|
||||||
|
RuleUnknown = iota
|
||||||
|
RulePassed
|
||||||
|
RuleFailed
|
||||||
|
RuleWarning
|
||||||
|
RuleDisabled
|
||||||
|
RuleMisconfigured
|
||||||
|
RuleConfigured
|
||||||
|
)
|
||||||
|
|
||||||
|
type Rule interface {
|
||||||
|
Key() string
|
||||||
|
Configuration() string
|
||||||
|
Validate() error
|
||||||
|
Evaluate(*image.AnalysisResult) (RuleStatus, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenericCiRule struct {
|
||||||
|
key string
|
||||||
|
configValue string
|
||||||
|
configValidator func(string) error
|
||||||
|
evaluator func(*image.AnalysisResult, string) (RuleStatus, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleStatus int
|
||||||
|
|
||||||
|
type RuleResult struct {
|
||||||
|
status RuleStatus
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGenericCiRule(key string, configValue string, validator func(string) error, evaluator func(*image.AnalysisResult, string) (RuleStatus, string)) *GenericCiRule {
|
||||||
return &GenericCiRule{
|
return &GenericCiRule{
|
||||||
key: key,
|
key: key,
|
||||||
evaluator: evaluator,
|
configValue: configValue,
|
||||||
|
configValidator: validator,
|
||||||
|
evaluator: evaluator,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,8 +55,16 @@ func (rule *GenericCiRule) Key() string {
|
|||||||
return rule.key
|
return rule.key
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *GenericCiRule) Evaluate(result *image.AnalysisResult, value string) (RuleStatus, string) {
|
func (rule *GenericCiRule) Configuration() string {
|
||||||
return rule.evaluator(result, value)
|
return rule.configValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rule *GenericCiRule) Validate() error {
|
||||||
|
return rule.configValidator(rule.configValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rule *GenericCiRule) Evaluate(result *image.AnalysisResult) (RuleStatus, string) {
|
||||||
|
return rule.evaluator(result, rule.configValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (status RuleStatus) String() string {
|
func (status RuleStatus) String() string {
|
||||||
@ -34,16 +77,31 @@ func (status RuleStatus) String() string {
|
|||||||
return aurora.Blue("WARN").String()
|
return aurora.Blue("WARN").String()
|
||||||
case RuleDisabled:
|
case RuleDisabled:
|
||||||
return aurora.Blue("SKIP").String()
|
return aurora.Blue("SKIP").String()
|
||||||
|
case RuleMisconfigured:
|
||||||
|
return aurora.Bold(aurora.Inverse(aurora.Red("MISCONFIGURED"))).String()
|
||||||
|
case RuleConfigured:
|
||||||
|
return "CONFIGURED "
|
||||||
default:
|
default:
|
||||||
return aurora.Inverse("Unknown").String()
|
return aurora.Inverse("Unknown").String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCiRules() []Rule {
|
func loadCiRules(config *viper.Viper) []Rule {
|
||||||
var rules = make([]Rule, 0)
|
var rules = make([]Rule, 0)
|
||||||
|
var ruleKey = "lowestEfficiency"
|
||||||
rules = append(rules, newGenericCiRule(
|
rules = append(rules, newGenericCiRule(
|
||||||
"rules.lowestEfficiency",
|
ruleKey,
|
||||||
|
config.GetString(fmt.Sprintf("rules.%s", ruleKey)),
|
||||||
|
func(value string) error {
|
||||||
|
lowestEfficiency, err := strconv.ParseFloat(value, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid config value ('%v'): %v", value, err)
|
||||||
|
}
|
||||||
|
if lowestEfficiency < 0 || lowestEfficiency > 1 {
|
||||||
|
return fmt.Errorf("lowestEfficiency config value is outside allowed range (0-1), given '%s'", value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
func(analysis *image.AnalysisResult, value string) (RuleStatus, string) {
|
func(analysis *image.AnalysisResult, value string) (RuleStatus, string) {
|
||||||
lowestEfficiency, err := strconv.ParseFloat(value, 64)
|
lowestEfficiency, err := strconv.ParseFloat(value, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -56,8 +114,17 @@ func loadCiRules() []Rule {
|
|||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
|
||||||
|
ruleKey = "highestWastedBytes"
|
||||||
rules = append(rules, newGenericCiRule(
|
rules = append(rules, newGenericCiRule(
|
||||||
"rules.highestWastedBytes",
|
ruleKey,
|
||||||
|
config.GetString(fmt.Sprintf("rules.%s", ruleKey)),
|
||||||
|
func(value string) error {
|
||||||
|
_, err := humanize.ParseBytes(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid config value ('%v'): %v", value, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
func(analysis *image.AnalysisResult, value string) (RuleStatus, string) {
|
func(analysis *image.AnalysisResult, value string) (RuleStatus, string) {
|
||||||
highestWastedBytes, err := humanize.ParseBytes(value)
|
highestWastedBytes, err := humanize.ParseBytes(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -70,8 +137,20 @@ func loadCiRules() []Rule {
|
|||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
|
||||||
|
ruleKey = "highestUserWastedPercent"
|
||||||
rules = append(rules, newGenericCiRule(
|
rules = append(rules, newGenericCiRule(
|
||||||
"rules.highestUserWastedPercent",
|
ruleKey,
|
||||||
|
config.GetString(fmt.Sprintf("rules.%s", ruleKey)),
|
||||||
|
func(value string) error {
|
||||||
|
highestUserWastedPercent, err := strconv.ParseFloat(value, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid config value ('%v'): %v", value, err)
|
||||||
|
}
|
||||||
|
if highestUserWastedPercent < 0 || highestUserWastedPercent > 1 {
|
||||||
|
return fmt.Errorf("highestUserWastedPercent config value is outside allowed range (0-1), given '%s'", value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
func(analysis *image.AnalysisResult, value string) (RuleStatus, string) {
|
func(analysis *image.AnalysisResult, value string) (RuleStatus, string) {
|
||||||
highestUserWastedPercent, err := strconv.ParseFloat(value, 64)
|
highestUserWastedPercent, err := strconv.ParseFloat(value, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
package ci
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/wagoodman/dive/image"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RuleStatus int
|
|
||||||
|
|
||||||
type RuleResult struct {
|
|
||||||
status RuleStatus
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
RuleUnknown = iota
|
|
||||||
RulePassed
|
|
||||||
RuleFailed
|
|
||||||
RuleWarning
|
|
||||||
RuleDisabled
|
|
||||||
)
|
|
||||||
|
|
||||||
type Rule interface {
|
|
||||||
Key() string
|
|
||||||
Evaluate(*image.AnalysisResult, string) (RuleStatus, string)
|
|
||||||
}
|
|
||||||
|
|
||||||
type GenericCiRule struct {
|
|
||||||
key string
|
|
||||||
evaluator func(*image.AnalysisResult, string) (RuleStatus, string)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Evaluator struct {
|
|
||||||
Config *viper.Viper
|
|
||||||
Rules []Rule
|
|
||||||
Results map[string]RuleResult
|
|
||||||
Tally ResultTally
|
|
||||||
Pass bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResultTally struct {
|
|
||||||
Pass int
|
|
||||||
Fail int
|
|
||||||
Skip int
|
|
||||||
Warn int
|
|
||||||
Total int
|
|
||||||
}
|
|
@ -5,7 +5,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/logrusorgru/aurora"
|
"github.com/logrusorgru/aurora"
|
||||||
@ -25,15 +24,9 @@ func runCi(analysis *image.AnalysisResult, options Options) {
|
|||||||
fmt.Printf(" wastedBytes: %d bytes (%s)\n", analysis.WastedBytes, humanize.Bytes(analysis.WastedBytes))
|
fmt.Printf(" wastedBytes: %d bytes (%s)\n", analysis.WastedBytes, humanize.Bytes(analysis.WastedBytes))
|
||||||
fmt.Printf(" userWastedPercent: %2.4f %%\n", analysis.WastedUserPercent*100)
|
fmt.Printf(" userWastedPercent: %2.4f %%\n", analysis.WastedUserPercent*100)
|
||||||
|
|
||||||
fmt.Println(title("Run CI Validations..."))
|
evaluator := ci.NewEvaluator(options.CiConfig)
|
||||||
evaluator := ci.NewEvaluator()
|
|
||||||
|
|
||||||
err := evaluator.LoadConfig(options.CiConfigFile)
|
fmt.Println(title("Run CI Validations..."))
|
||||||
if err != nil {
|
|
||||||
fmt.Println(" Using default CI config")
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" Using CI config: %s\n", options.CiConfigFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
pass := evaluator.Evaluate(analysis)
|
pass := evaluator.Evaluate(analysis)
|
||||||
evaluator.Report()
|
evaluator.Report()
|
||||||
@ -71,7 +64,6 @@ func runBuild(buildArgs []string) string {
|
|||||||
func Run(options Options) {
|
func Run(options Options) {
|
||||||
doExport := options.ExportFile != ""
|
doExport := options.ExportFile != ""
|
||||||
doBuild := len(options.BuildArgs) > 0
|
doBuild := len(options.BuildArgs) > 0
|
||||||
isCi, _ := strconv.ParseBool(os.Getenv("CI"))
|
|
||||||
|
|
||||||
if doBuild {
|
if doBuild {
|
||||||
fmt.Println(title("Building image..."))
|
fmt.Println(title("Building image..."))
|
||||||
@ -115,7 +107,7 @@ func Run(options Options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isCi {
|
if options.Ci {
|
||||||
runCi(result, options)
|
runCi(result, options)
|
||||||
} else {
|
} else {
|
||||||
if doExport {
|
if doExport {
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
ImageId string
|
Ci bool
|
||||||
ExportFile string
|
ImageId string
|
||||||
CiConfigFile string
|
ExportFile string
|
||||||
BuildArgs []string
|
CiConfig *viper.Viper
|
||||||
|
BuildArgs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type export struct {
|
type export struct {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user