Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kube score configuration file #468

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 48 additions & 6 deletions cmd/kube-score/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import (
"os"
"path/filepath"

"github.com/spf13/pflag"
flag "github.com/spf13/pflag"
"golang.org/x/term"

"github.com/zegl/kube-score/config"
ks "github.com/zegl/kube-score/domain"
"github.com/zegl/kube-score/parser"
Expand All @@ -23,6 +22,7 @@ import (
"github.com/zegl/kube-score/renderer/sarif"
"github.com/zegl/kube-score/score"
"github.com/zegl/kube-score/scorecard"
"golang.org/x/term"
)

func main() {
Expand All @@ -46,6 +46,13 @@ func main() {
}
},

"mkconfig": func(helpName string, args []string) {
if err := mkConfigFile(helpName, args); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Failed to make configuration file: %v\n", err)
os.Exit(1)
}
},

"version": func(helpName string, args []string) {
cmdVersion()
},
Expand Down Expand Up @@ -76,10 +83,11 @@ func setDefault(fs *flag.FlagSet, binName, actionName string, displayForMoreInfo
%s [action] --flags

Actions:
score Checks all files in the input, and gives them a score and recommendations
list Prints a CSV list of all available score checks
version Print the version of kube-score
help Print this message`+"\n\n", binName, binName)
score Checks all files in the input, and gives them a score and recommendations
list Prints a CSV list of all available score checks
mkconfig Create a configuration file from kube-score's registered checks
version Print the version of kube-score
help Print this message`+"\n\n", binName, binName)

if displayForMoreInfo {
usage += fmt.Sprintf(`Run "%s [action] --help" for more information about a particular command`, binName)
Expand All @@ -97,6 +105,22 @@ Actions:
}
}

func aliasFlagNames(f *pflag.FlagSet, name string) pflag.NormalizedName {

var aliasFlag = map[string]string{
"enable-optional-test": "enable",
"ignore-test": "disable",
}

normalized, exists := aliasFlag[name]

if exists {
name = normalized
}

return pflag.NormalizedName(name)
}

func scoreFiles(binName string, args []string) error {
fs := flag.NewFlagSet(binName, flag.ExitOnError)
exitOneOnWarning := fs.Bool("exit-one-on-warning", false, "Exit with code 1 in case of warnings")
Expand All @@ -111,7 +135,9 @@ func scoreFiles(binName string, args []string) error {
disableIgnoreChecksAnnotation := fs.Bool("disable-ignore-checks-annotations", false, "Set to true to disable the effect of the 'kube-score/ignore' annotations")
disableOptionalChecksAnnotation := fs.Bool("disable-optional-checks-annotations", false, "Set to true to disable the effect of the 'kube-score/enable' annotations")
kubernetesVersion := fs.String("kubernetes-version", "v1.18", "Setting the kubernetes-version will affect the checks ran against the manifests. Set this to the version of Kubernetes that you're using in production for the best results.")
configFile := fs.String("config", "", "Optional kube-score configuration file")
setDefault(fs, binName, "score", false)
fs.SetNormalizeFunc(aliasFlagNames)

err := fs.Parse(args)
if err != nil {
Expand Down Expand Up @@ -157,6 +183,22 @@ Use "-" as filename to read from STDIN.`, execName(binName))
allFilePointers = append(allFilePointers, namedReader{Reader: fp, name: filename})
}

// load configuration file

if len(*configFile) > 0 {
cfg, err := loadConfigFile(*configFile)

if err != nil {
return err
}

excludeChks := excludeChecks(&cfg)
includeChks := includeChecks(&cfg)

*ignoreTests = append(*ignoreTests, excludeChks...)
*optionalTests = append(*optionalTests, includeChks...)
}

ignoredTests := listToStructMap(ignoreTests)
enabledOptionalTests := listToStructMap(optionalTests)

Expand Down
145 changes: 145 additions & 0 deletions cmd/kube-score/object-checks-config-file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package main

import (
"fmt"
"os"

flag "github.com/spf13/pflag"
"github.com/zegl/kube-score/config"
"github.com/zegl/kube-score/parser"
"github.com/zegl/kube-score/score"
"gopkg.in/yaml.v3"
)

// Start with an empty enable and disable list
// If enable-all is true, add all tests to the list. If it's false, add only the default tests to the list.
// If disable-all is true, add all default tests to the disable list. If it's false, do nothing.
// If enable is set, use it as the enable list.
// If disable is set, use it as the disable list.
// If --enable-optional-test is set, add the test(s) to the enable list
// If --ignore-test is set, add the test(s) to the disable list
type configuration struct {
DisableAll bool `yaml:"disable-all"`
EnableChecks []string `yaml:"enable"`
EnableAll bool `yaml:"enable-all"`
DisableChecks []string `yaml:"disable"`
}

// Start with an empty enable and disable list
func mkConfigFile(binName string, args []string) error {
fs := flag.NewFlagSet(binName, flag.ExitOnError)
printHelp := fs.Bool("help", false, "Print help")
setDefault(fs, binName, "mkconfig", false)
cfgFile := fs.String("config", ".kube-score.yml", "Optional kube-score configuration file")
cfgForce := fs.Bool("force", false, "Force overwrite of existing .kube-score.yml file")

err := fs.Parse(args)

if err != nil {
return fmt.Errorf("Failed to parse mkconfig arguments: %w", err)
}

if *printHelp {
fs.Usage()
return nil
}

if _, err := os.Stat(*cfgFile); err == nil {
if !*cfgForce {
return fmt.Errorf("File %s exists. Use --force flag to overwrite\n", *cfgFile)
}
}

var checks configuration

checks.DisableAll = false
checks.EnableAll = false

if o, err := yaml.Marshal(&checks); err != nil {
return fmt.Errorf("Failed to marshal checks %w", err)
} else {
if err := os.WriteFile(*cfgFile, []byte(o), 0600); err != nil {
return fmt.Errorf("Failed to write file %w,", err)
}
fmt.Println("Created kube-score configuration file ", *cfgFile)
return nil
}
}

func loadConfigFile(fileName string) (config configuration, err error) {

content, err := os.ReadFile(fileName)

if err != nil {
return config, fmt.Errorf("Failed to read file %s, error %w", fileName, err)
}

if err := yaml.Unmarshal(content, &config); err != nil {
return config, fmt.Errorf("Failed to unmarshal yaml %w", err)
}

return config, nil
}

func registeredChecks() (requiredChecks []string, optionalChecks []string) {

allChecks := score.RegisterAllChecks(parser.Empty(), config.Configuration{})

for _, c := range allChecks.All() {
if c.Optional {
optionalChecks = append(optionalChecks, c.ID)
} else {
requiredChecks = append(requiredChecks, c.ID)
}
}

return
}

func allRegisteredChecks() (allChecks []string) {

registeredChecks := score.RegisterAllChecks(parser.Empty(), config.Configuration{})

for _, c := range registeredChecks.All() {
allChecks = append(allChecks, c.ID)
}

return
}

// If enable is set, use it as the enable list.
// If enable-all is true, add all tests to the list.
// By default add only the non-optional tests to the list.
func includeChecks(k *configuration) (checks []string) {

switch {
case len(k.EnableChecks) > 0:
checks = append(checks, k.EnableChecks...)
return
case k.EnableAll:
checks = append(checks, allRegisteredChecks()...)
return
}

// default case
defaultChecks, _ := registeredChecks()
checks = append(checks, defaultChecks...)

return
}

// If disable is set, use it as the disable list.
// If disable-all is true, add all tests to the disable list.
func excludeChecks(k *configuration) (checks []string) {

switch {
case len(k.DisableChecks) > 0:
checks = append(checks, k.DisableChecks...)
return
case k.DisableAll:
checks = append(checks, allRegisteredChecks()...)
return
}

return
}
79 changes: 79 additions & 0 deletions cmd/kube-score/object-checks-config-file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package main

import (
"testing"

"github.com/stretchr/testify/assert"
)

// functionally the same as having no configuration file
func TestKubeScoreConfigDefaultChecks(t *testing.T) {

if cfg, err := loadConfigFile("testdata/kube-score-default.yml"); err == nil {
assert.Equal(t, cfg.DisableAll, false)
assert.Equal(t, cfg.EnableAll, false)
assert.Equal(t, len(cfg.DisableChecks), 0)
assert.Equal(t, len(cfg.EnableChecks), 0)

include := includeChecks(&cfg)
exclude := excludeChecks(&cfg)
dflt, _ := registeredChecks()
assert.Equal(t, len(include), len(dflt))
assert.Equal(t, len(exclude), 0)
}
}

// functionally means include all optional tests
func TestKubeScoreConfigIncludeAllOptionalChecks(t *testing.T) {

if cfg, err := loadConfigFile("testdata/kube-score-enable-all.yml"); err == nil {
allChecks := allRegisteredChecks()
include := includeChecks(&cfg)
assert.Equal(t, cfg.EnableAll, true)
assert.Equal(t, len(include), len(allChecks))
}
}

// enable most tests, but disable a select few
func TestKubeScoreConfigExcludeSelectDefaultChecks(t *testing.T) {

if cfg, err := loadConfigFile("testdata/kube-score-enable-all-select-disable.yml"); err == nil {
assert.Equal(t, cfg.EnableAll, true)
assert.True(t, len(cfg.DisableChecks) > 0)
excludeChecks := excludeChecks(&cfg)
idx := len(excludeChecks) - 1
assert.Contains(t, cfg.DisableChecks, excludeChecks[idx])
}
}

func TestKubeScoreConfigNoDefaultChecksIncludeSelectChecks(t *testing.T) {

if cfg, err := loadConfigFile("testdata/kube-score-disable-all-select-enable.yml"); err == nil {
allChecks := allRegisteredChecks()
includeChecks := includeChecks(&cfg)
excludeChecks := excludeChecks(&cfg)
assert.Equal(t, cfg.DisableAll, true)
assert.Equal(t, len(excludeChecks), len(allChecks))
idx := len(includeChecks) - 1
assert.Contains(t, cfg.EnableChecks, includeChecks[idx])
}
}

func TestKubeScoreConfigOnlyEnableAndDisableSelectChecks(t *testing.T) {

if cfg, err := loadConfigFile("testdata/kube-score-only-enable-disable.yml"); err == nil {
includeChecks := includeChecks(&cfg)
excludeChecks := excludeChecks(&cfg)
assert.Equal(t, cfg.DisableAll, true)
assert.Equal(t, len(excludeChecks), len(cfg.DisableChecks))
assert.Equal(t, len(includeChecks), len(cfg.EnableChecks))
}
}

func TestKubeScoreConfigBadFileNameReturnsError(t *testing.T) {

// badfilename.yml does not exist
if _, err := loadConfigFile("testdata/badfilename.yml"); err != nil {
assert.ErrorContains(t, err, "Failed to read file testdata/badfilename.yml")
}
}
4 changes: 4 additions & 0 deletions cmd/kube-score/testdata/kube-score-default.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
disable-all: false
enable: []
enable-all: false
disable: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
disable-all: true
enable:
- ingress-targets-service
- cronjab-has-deadline
- container-resources
- container-image-tag
enable-all: false
disable: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
disable-all: false
enable: []
enable-all: true
disable:
- statefulset-has-poddisruptionbudget
- deployment-has-poddisruptionbudget
- poddisruptionbudget-has-policy
- pod-networkpolicy
4 changes: 4 additions & 0 deletions cmd/kube-score/testdata/kube-score-enable-all.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
disable-all: false
enable: []
enable-all: true
disable: []
12 changes: 12 additions & 0 deletions cmd/kube-score/testdata/kube-score-only-enable-disable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
disable-all: true # defaults to false
enable:
- ingress-targets-service
- cronjob-has-deadline
- container-resources
- container-image-tag
enable-all: true # defaults to false
disable:
- statefulset-has-poddisruptionbudget
- deployment-has-poddisruptionbudget
- poddisruptionbudget-has-policy
- pod-networkpolicy