From b9ba7775d42d66097a5dc38222c9af716dd80ec8 Mon Sep 17 00:00:00 2001 From: kmarteaux Date: Sat, 5 Mar 2022 09:05:08 -0500 Subject: [PATCH] Implement feature request: a configuration file #384 --- cmd/kube-score/main.go | 31 ++++- cmd/kube-score/object-checks-config-file.go | 113 ++++++++++++++++++ .../object-checks-config-file_test.go | 55 +++++++++ cmd/kube-score/testdata/kube-score.yml | 40 +++++++ 4 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 cmd/kube-score/object-checks-config-file.go create mode 100644 cmd/kube-score/object-checks-config-file_test.go create mode 100644 cmd/kube-score/testdata/kube-score.yml diff --git a/cmd/kube-score/main.go b/cmd/kube-score/main.go index 8058e478..d081e5dc 100644 --- a/cmd/kube-score/main.go +++ b/cmd/kube-score/main.go @@ -12,8 +12,6 @@ import ( "path/filepath" flag "github.com/spf13/pflag" - "golang.org/x/crypto/ssh/terminal" - "github.com/zegl/kube-score/config" ks "github.com/zegl/kube-score/domain" "github.com/zegl/kube-score/parser" @@ -23,6 +21,7 @@ import ( "github.com/zegl/kube-score/renderer/sarif" "github.com/zegl/kube-score/score" "github.com/zegl/kube-score/scorecard" + "golang.org/x/crypto/ssh/terminal" ) func main() { @@ -43,6 +42,10 @@ func main() { listChecks(helpName, args) }, + "mkconfig": func(helpName string, args []string) { + mkConfigFile(helpName, args) + }, + "version": func(helpName string, args []string) { cmdVersion() }, @@ -73,10 +76,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 + version Print the version of kube-score + mkconfig Creates a .kube-score.yml configuration file from kube-score's registered checks in the current working directory + 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) @@ -107,6 +111,7 @@ func scoreFiles(binName string, args []string) error { ignoreTests := fs.StringSlice("ignore-test", []string{}, "Disable a test, can be set multiple times") disableIgnoreChecksAnnotation := fs.Bool("disable-ignore-checks-annotations", false, "Set to true to disable the effect of the 'kube-score/ignore' 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", ".kube-score.yml", "Optional kube-score configuration file") setDefault(fs, binName, "score", false) err := fs.Parse(args) @@ -153,9 +158,23 @@ Use "-" as filename to read from STDIN.`, execName(binName)) allFilePointers = append(allFilePointers, namedReader{Reader: fp, name: filename}) } + // load kube-score.yml configuration file (if present) + cfg := loadConfigFile(*configFile) + excludeChks := excludeChecks(&cfg) + includeChks := includeChecks(&cfg) + + fmt.Println("excludeChks <- ", excludeChks) + fmt.Println("includeChks <- ", includeChks) + + *ignoreTests = append(*ignoreTests, excludeChks...) + *optionalTests = append(*optionalTests, includeChks...) + ignoredTests := listToStructMap(ignoreTests) enabledOptionalTests := listToStructMap(optionalTests) + fmt.Println("ignoredTest <- ", ignoredTests) + fmt.Println("enabledOptionalTests <- ", optionalTests) + kubeVer, err := config.ParseSemver(*kubernetesVersion) if err != nil { return errors.New("Invalid --kubernetes-version. Use on format \"vN.NN\"") diff --git a/cmd/kube-score/object-checks-config-file.go b/cmd/kube-score/object-checks-config-file.go new file mode 100644 index 00000000..7ae52694 --- /dev/null +++ b/cmd/kube-score/object-checks-config-file.go @@ -0,0 +1,113 @@ +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" +) + +type kubescorechecks struct { + AddAllDefaultChecks bool `yaml:"addAllDefaultChecks"` + AddAllOptionalChecks bool `yaml:"addAllOptionalChecks"` + DisableIgnoreChecksAnnotations bool `yaml:"disableIgnoreChecksAnnotations"` + DefaultChecks []string `yaml:"defaultChecks"` + OptionalChecks []string `yaml:"optionalChecks"` + IncludeChecks []string `yaml:"include"` + ExcludeChecks []string `yaml:"exclude"` +} + +func mkConfigFile(binName string, args []string) { + 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 { + panic("Failed to parse mkconfig arguments") + } + + if *printHelp { + fs.Usage() + return + } + + if _, err := os.Stat(*cfgFile); err == nil { + if !*cfgForce { + errmsg := fmt.Errorf("File %s exists. Use --force flag to overwrite\n", *cfgFile) + fmt.Println(errmsg) + fs.Usage() + return + } + } + + allChecks := score.RegisterAllChecks(parser.Empty(), config.Configuration{}) + + var checks kubescorechecks + + checks.AddAllDefaultChecks = true + checks.AddAllOptionalChecks = false + checks.DisableIgnoreChecksAnnotations = false + + for _, c := range allChecks.All() { + if c.Optional { + checks.OptionalChecks = append(checks.OptionalChecks, c.ID) + } else { + checks.DefaultChecks = append(checks.DefaultChecks, c.ID) + } + } + + if o, err := yaml.Marshal(&checks); err != nil { + err := fmt.Errorf("Failed to marshal checks") + fmt.Println(err.Error()) + } else { + if err := os.WriteFile(*cfgFile, []byte(o), 0600); err != nil { + panic(err) + } + fmt.Println("Created kube-score configuration file ", *cfgFile) + } +} + +func loadConfigFile(fp string) (config kubescorechecks) { + + content, err := os.ReadFile(fp) + + // if the file does not exist, create it + if err != nil { + mkConfigFile("mkconfig", []string{fp}) + } + + err2 := yaml.Unmarshal(content, &config) + if err2 != nil { + panic(err2) + } + + return config +} + +func includeChecks(k *kubescorechecks) (checks []string) { + if k.AddAllOptionalChecks { + checks = append(checks, k.OptionalChecks...) + } + if len(k.IncludeChecks) > 0 { + checks = append(checks, k.IncludeChecks...) + } + return +} + +func excludeChecks(k *kubescorechecks) (checks []string) { + if !k.AddAllDefaultChecks { + checks = append(checks, k.DefaultChecks...) + } + if len(k.ExcludeChecks) > 0 { + checks = append(checks, k.ExcludeChecks...) + } + return +} diff --git a/cmd/kube-score/object-checks-config-file_test.go b/cmd/kube-score/object-checks-config-file_test.go new file mode 100644 index 00000000..3b7a76c0 --- /dev/null +++ b/cmd/kube-score/object-checks-config-file_test.go @@ -0,0 +1,55 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKubeScoreConfigExcludeAllDefaultChecks(t *testing.T) { + + cfg := loadConfigFile("testdata/kube-score.yml") + cfg.AddAllDefaultChecks = false + excludeThese := excludeChecks(&cfg) + + assert.Equal(t, len(excludeThese), len(cfg.DefaultChecks)) +} + +func TestKubeScoreConfigIncludeAllOptionalChecks(t *testing.T) { + + cfg := loadConfigFile("testdata/kube-score.yml") + cfg.AddAllOptionalChecks = true + includeThese := includeChecks(&cfg) + + assert.Equal(t, len(includeThese), len(cfg.OptionalChecks)) +} + +func TestKubeScoreConfigExcludeSelectDefaultChecks(t *testing.T) { + + cfg := loadConfigFile("testdata/kube-score.yml") + cfg.AddAllDefaultChecks = true + cfg.ExcludeChecks = append(cfg.ExcludeChecks, "pod-probes") + excludeThese := excludeChecks(&cfg) + + assert.Contains(t, cfg.ExcludeChecks, "pod-probes") + assert.Equal(t, len(excludeThese), 1) +} + +func TestKubeScoreConfigNoDefaultChecksIncludeSelectChecks(t *testing.T) { + + cfg := loadConfigFile("testdata/kube-score.yml") + cfg.AddAllDefaultChecks = false + + onlyThese := []string{"container-resources", "image-tag", "image-pull-policy"} + + cfg.IncludeChecks = append(cfg.IncludeChecks, onlyThese...) + includeThese := includeChecks(&cfg) + + for _, v := range onlyThese { + assert.Contains(t, cfg.IncludeChecks, v) + } + + assert.NotContains(t, cfg.IncludeChecks, "pod-networkpolicy") + + assert.Equal(t, len(includeThese), len(onlyThese)) +} diff --git a/cmd/kube-score/testdata/kube-score.yml b/cmd/kube-score/testdata/kube-score.yml new file mode 100644 index 00000000..0ffce690 --- /dev/null +++ b/cmd/kube-score/testdata/kube-score.yml @@ -0,0 +1,40 @@ +addAllDefaultChecks: true +addAllOptionalChecks: false +disableIgnoreChecksAnnotations: false +defaultChecks: + - ingress-targets-service + - cronjob-has-deadline + - container-resources + - container-image-tag + - container-image-pull-policy + - container-ephemeral-storage-request-and-limit + - statefulset-has-poddisruptionbudget + - deployment-has-poddisruptionbudget + - poddisruptionbudget-has-policy + - pod-networkpolicy + - networkpolicy-targets-pod + - pod-probes + - container-security-context-user-group-id + - container-security-context-privileged + - container-security-context-readonlyrootfilesystem + - service-targets-pod + - service-type + - stable-version + - deployment-has-host-podantiaffinity + - statefulset-has-host-podantiaffinity + - deployment-targeted-by-hpa-does-not-have-replicas-configured + - statefulset-has-servicename + - deployment-pod-selector-labels-match-template-metadata-labels + - statefulset-pod-selector-labels-match-template-metadata-labels + - label-values + - horizontalpodautoscaler-has-target + - container-ephemeral-storage-requests-and-limits +optionalChecks: + - container-resource-requests-equal-limits + - container-cpu-requests-equal-limits + - container-memory-requests-equal-limits + - container-ephemeral-storage-request-equals-limit + - container-ports-check + - container-seccomp-profile +include: [] +exclude: []