Repository: zegl/kube-score
Pull request #42 (2018-10-15 19:45:47)

Configurable threshold


Patch:
diff --git a/README.md b/README.md
index 8ee6461..07f0ae7 100644
--- a/README.md
+++ b/README.md
@@ -70,5 +70,9 @@ Usage of ./kube-score:
     	Print help
   -ignore-container-cpu-limit
     	Disables the requirement of setting a container CPU limit
+  -threshold-ok int
+    	The score threshold for treating an score as OK. Must be between 1 and 10 (inclusive). Scores graded below this threshold are WARNING or CRITICAL. (default 10)
+  -threshold-warning int
+    	The score threshold for treating a score as WARNING. Grades below this threshold are CRITICAL. Must be between 1 and 10 (inclusive). (default 5)
   -v	Verbose output
 ```
diff --git a/cmd/kube-score/main.go b/cmd/kube-score/main.go
index 8326f9a..dd8900d 100644
--- a/cmd/kube-score/main.go
+++ b/cmd/kube-score/main.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"github.com/fatih/color"
 	"github.com/zegl/kube-score/score"
+	"github.com/zegl/kube-score/scorecard"
 	"io"
 	"os"
 )
@@ -13,6 +14,8 @@ func main() {
 	fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
 	exitOneOnWarning := fs.Bool("exit-one-on-warning", false, "Exit with code 1 in case of warnings")
 	ignoreContainerCpuLimit := fs.Bool("ignore-container-cpu-limit", false, "Disables the requirement of setting a container CPU limit")
+	okThreshold := fs.Int("threshold-ok", 10, "The score threshold for treating an score as OK. Must be between 1 and 10 (inclusive). Scores graded below this threshold are WARNING or CRITICAL.")
+	warningThreshold := fs.Int("threshold-warning", 5, "The score threshold for treating a score as WARNING. Grades below this threshold are CRITICAL. Must be between 1 and 10 (inclusive).")
 	verboseOutput := fs.Bool("v", false, "Verbose output")
 	printHelp := fs.Bool("help", false, "Print help")
 	fs.Parse(os.Args[1:])
@@ -22,6 +25,13 @@ func main() {
 		return
 	}
 
+	if *okThreshold < 1 || *okThreshold > 10 ||
+		*warningThreshold < 1 || *warningThreshold > 10 {
+		fmt.Println("Error: --threshold-ok and --threshold-warning must be set to a value between 1 and 10 inclusive.")
+		fs.Usage()
+		os.Exit(1)
+	}
+
 	filesToRead := fs.Args()
 	if len(filesToRead) == 0 {
 		fmt.Println(`Error: No files given as arguments.
@@ -61,8 +71,6 @@ Use "-" as filename to read from STDIN.`)
 		panic(err)
 	}
 
-	sumGrade := 0
-
 	hasWarning := false
 	hasCritical := false
 
@@ -80,17 +88,24 @@ Use "-" as filename to read from STDIN.`)
 		}
 
 		for _, card := range resourceScores {
-			col := color.FgGreen
-			status := "OK"
 
-			if card.Grade == 0 {
-				col = color.FgRed
-				status = "CRITICAL"
-				hasCritical = true
-			} else if card.Grade < 10 {
+			var col color.Attribute
+			var status string
+
+			if card.Grade >= scorecard.Grade(*okThreshold) {
+				// Higher than or equal to --threshold-ok
+				col = color.FgGreen
+				status = "OK"
+			} else if card.Grade >= scorecard.Grade(*warningThreshold) {
+				// Higher than or equal to --threshold-warning
 				col = color.FgYellow
 				status = "WARNING"
 				hasWarning = true
+			} else {
+				// All lower than both --threshold-ok and --threshold-warning are critical
+				col = color.FgRed
+				status = "CRITICAL"
+				hasCritical = true
 			}
 
 			color.New(col).Printf("    [%s] %s\n", status, card.Name)
@@ -110,8 +125,6 @@ Use "-" as filename to read from STDIN.`)
 
 				fmt.Println()
 			}
-
-			sumGrade += card.Grade
 		}
 	}
 
diff --git a/score/container/container.go b/score/container/container.go
index 3e35865..78c5a50 100644
--- a/score/container/container.go
+++ b/score/container/container.go
@@ -42,14 +42,14 @@ func ScoreContainerLimits(requireCpuLimit bool) func(corev1.PodTemplateSpec) sco
 		}
 
 		if len(allContainers) == 0 {
-			score.Grade = 0
+			score.Grade = scorecard.GradeCritical
 			score.AddComment("", "No containers defined", "")
 		} else if hasMissingLimit {
-			score.Grade = 0
+			score.Grade = scorecard.GradeCritical
 		} else if hasMissingRequest {
-			score.Grade = 5
+			score.Grade = scorecard.GradeWarning
 		} else {
-			score.Grade = 10
+			score.Grade = scorecard.GradeAllOK
 		}
 
 		return
@@ -76,9 +76,9 @@ func ScoreContainerImageTag(podTemplate corev1.PodTemplateSpec) (score scorecard
 	}
 
 	if hasTagLatest {
-		score.Grade = 0
+		score.Grade = scorecard.GradeCritical
 	} else {
-		score.Grade = 10
+		score.Grade = scorecard.GradeAllOK
 	}
 
 	return
@@ -112,9 +112,9 @@ func ScoreContainerImagePullPolicy(podTemplate corev1.PodTemplateSpec) (score sc
 	}
 
 	if hasNonAlways {
-		score.Grade = 0
+		score.Grade = scorecard.GradeCritical
 	} else {
-		score.Grade = 10
+		score.Grade = scorecard.GradeAllOK
 	}
 
 	return
diff --git a/score/networkpolicy/networkpolicy.go b/score/networkpolicy/networkpolicy.go
index 7047575..df6018a 100644
--- a/score/networkpolicy/networkpolicy.go
+++ b/score/networkpolicy/networkpolicy.go
@@ -34,15 +34,15 @@ func ScorePodHasNetworkPolicy(allNetpols []networkingv1.NetworkPolicy) func(spec
 		}
 
 		if hasMatchingEgressNetpol && hasMatchingIngressNetpol {
-			score.Grade = 10
+			score.Grade = scorecard.GradeAllOK
 		} else if hasMatchingEgressNetpol && !hasMatchingIngressNetpol {
-			score.Grade = 5
+			score.Grade = scorecard.GradeWarning
 			score.AddComment("", "The pod does not have a matching ingress network policy", "Add a egress policy to the pods NetworkPolicy")
 		} else if hasMatchingIngressNetpol && !hasMatchingEgressNetpol {
-			score.Grade = 5
+			score.Grade = scorecard.GradeWarning
 			score.AddComment("", "The pod does not have a matching egress network policy", "Add a ingress policy to the pods NetworkPolicy")
 		} else {
-			score.Grade = 0
+			score.Grade = scorecard.GradeCritical
 			score.AddComment("", "The pod does not have a matching network policy", "Create a NetworkPolicy that targets this pod")
 		}
 
diff --git a/score/probes/probes.go b/score/probes/probes.go
index 36a3dd2..2d38e7a 100644
--- a/score/probes/probes.go
+++ b/score/probes/probes.go
@@ -80,20 +80,20 @@ func ScoreContainerProbes(allServices []corev1.Service) func(corev1.PodTemplateS
 
 		if hasLivenessProbe && (hasReadinessProbe || !isTargetedByService) {
 			if !probesAreIdentical {
-				score.Grade = 10
+				score.Grade = scorecard.GradeAllOK
 			} else {
-				score.Grade = 7
+				score.Grade = scorecard.GradeAlmostOK
 				score.AddComment("", "Pod has the same readiness and liveness probe", "It's recommended to have different probes for the two different purposes.")
 			}
 		} else if !hasReadinessProbe && !hasLivenessProbe {
-			score.Grade = 0
+			score.Grade = scorecard.GradeCritical
 			score.AddComment("", "Container is missing a readinessProbe", "Without a readinessProbe Services will start sending traffic to this pod before it's ready")
 			score.AddComment("", "Container is missing a livenessProbe", "Without a livenessProbe kubelet can not restart the Pod if it has crashed")
 		} else if isTargetedByService && !hasReadinessProbe {
-			score.Grade = 0
+			score.Grade = scorecard.GradeCritical
 			score.AddComment("", "Container is missing a readinessProbe", "Without a readinessProbe Services will start sending traffic to this pod before it's ready")
 		} else if !hasLivenessProbe {
-			score.Grade = 5
+			score.Grade = scorecard.GradeWarning
 			score.AddComment("", "Pod is missing a livenessProbe", "Without a livenessProbe kubelet can not restart the Pod if it has crashed")
 		}
 
diff --git a/score/security/security.go b/score/security/security.go
index 71beb60..c798bca 100644
--- a/score/security/security.go
+++ b/score/security/security.go
@@ -47,9 +47,9 @@ func ScoreContainerSecurityContext(podTemplate corev1.PodTemplateSpec) (score sc
 	}
 
 	if hasPrivileged || hasWritableRootFS || hasLowUserID || hasLowGroupID {
-		score.Grade = 0
+		score.Grade = scorecard.GradeCritical
 	} else {
-		score.Grade = 10
+		score.Grade = scorecard.GradeAllOK
 	}
 
 	return
diff --git a/score/service/service.go b/score/service/service.go
index f561224..ea97818 100644
--- a/score/service/service.go
+++ b/score/service/service.go
@@ -43,8 +43,9 @@ func ScoreServiceTargetsPod(pods []corev1.Pod, podspecers []ks.PodSpecer) func(c
 		}
 
 		if hasMatch {
-			score.Grade = 10
+			score.Grade = scorecard.GradeAllOK
 		} else {
+			score.Grade = scorecard.GradeCritical
 			score.AddComment("", "The services selector does not match any pods", "")
 		}
 
diff --git a/score/stable/stable_version.go b/score/stable/stable_version.go
index b74c846..3e62ef8 100644
--- a/score/stable/stable_version.go
+++ b/score/stable/stable_version.go
@@ -28,7 +28,7 @@ func ScoreMetaStableAvailable(meta metav1.TypeMeta) (score scorecard.TestScore)
 
 	if inVersion, ok := withStable[meta.APIVersion]; ok {
 		if recommendedVersion, ok := inVersion[meta.Kind]; ok {
-			score.Grade = 5
+			score.Grade = scorecard.GradeWarning
 			score.AddComment("",
 				fmt.Sprintf("The apiVersion and kind %s/%s is deprecated", meta.APIVersion, meta.Kind),
 				fmt.Sprintf("It's recommended to use %s instead", recommendedVersion),
@@ -37,6 +37,6 @@ func ScoreMetaStableAvailable(meta metav1.TypeMeta) (score scorecard.TestScore)
 		}
 	}
 
-	score.Grade = 10
+	score.Grade = scorecard.GradeAllOK
 	return
 }
diff --git a/scorecard/scorecard.go b/scorecard/scorecard.go
index 91339ad..7d0607d 100644
--- a/scorecard/scorecard.go
+++ b/scorecard/scorecard.go
@@ -35,10 +35,19 @@ type TestScore struct {
 		Version   string
 	}
 
-	Grade    int
+	Grade    Grade
 	Comments []TestScoreComment
 }
 
+type Grade int
+
+const (
+	GradeCritical = 1
+	GradeWarning  = 5
+	GradeAlmostOK = 7
+	GradeAllOK    = 10
+)
+
 type TestScoreComment struct {
 	Path        string
 	Summary     string


Test patch:
diff --git a/score/networkpolicy_test.go b/score/networkpolicy_test.go
index 2564d28..568a5e2 100644
--- a/score/networkpolicy_test.go
+++ b/score/networkpolicy_test.go
@@ -5,7 +5,7 @@ import (
 )
 
 func TestPodHasNoMatchingNetworkPolicy(t *testing.T) {
-	testExpectedScore(t, "networkpolicy-not-matching.yaml", "Pod NetworkPolicy", 0)
+	testExpectedScore(t, "networkpolicy-not-matching.yaml", "Pod NetworkPolicy", 1)
 }
 
 func TestPodHasMatchingNetworkPolicy(t *testing.T) {
diff --git a/score/probe_test.go b/score/probe_test.go
index bbfeca2..8c0d6ee 100644
--- a/score/probe_test.go
+++ b/score/probe_test.go
@@ -6,7 +6,7 @@ import (
 )
 
 func TestProbesPodAllMissing(t *testing.T) {
-	comments := testExpectedScore(t, "pod-probes-all-missing.yaml", "Pod Probes", 0)
+	comments := testExpectedScore(t, "pod-probes-all-missing.yaml", "Pod Probes", 1)
 	assert.Len(t, comments, 2)
 	assert.Equal(t, "Container is missing a readinessProbe", comments[0].Summary)
 	assert.Equal(t, "Container is missing a livenessProbe", comments[1].Summary)
@@ -37,13 +37,13 @@ func TestProbesPodIdenticalExec(t *testing.T) {
 }
 
 func TestProbesTargetedByService(t *testing.T) {
-	comments := testExpectedScore(t, "pod-probes-targeted-by-service.yaml", "Pod Probes", 0)
+	comments := testExpectedScore(t, "pod-probes-targeted-by-service.yaml", "Pod Probes", 1)
 	assert.Len(t, comments, 1)
 	assert.Equal(t, "Container is missing a readinessProbe", comments[0].Summary)
 }
 
 func TestProbesTargetedByServiceSameNamespace(t *testing.T) {
-	comments := testExpectedScore(t, "pod-probes-targeted-by-service-same-namespace.yaml", "Pod Probes", 0)
+	comments := testExpectedScore(t, "pod-probes-targeted-by-service-same-namespace.yaml", "Pod Probes", 1)
 	assert.Len(t, comments, 1)
 	assert.Equal(t, "Container is missing a readinessProbe", comments[0].Summary)
 }
diff --git a/score/score_test.go b/score/score_test.go
index b1a4e16..5614a90 100644
--- a/score/score_test.go
+++ b/score/score_test.go
@@ -19,7 +19,7 @@ func testFile(name string) *os.File {
 
 // testExpectedScoreWithConfig runs all tests, but makes sure that the test for "testcase" was executed, and that
 // the grade is set to expectedScore. The function returns the comments of "testcase".
-func testExpectedScoreWithConfig(t *testing.T, config Configuration, filename string, testcase string, expectedScore int) []scorecard.TestScoreComment {
+func testExpectedScoreWithConfig(t *testing.T, config Configuration, filename string, testcase string, expectedScore scorecard.Grade) []scorecard.TestScoreComment {
 	config.AllFiles = []io.Reader{testFile(filename)}
 	sc, err := Score(config)
 	assert.NoError(t, err)
@@ -37,12 +37,12 @@ func testExpectedScoreWithConfig(t *testing.T, config Configuration, filename st
 	return nil
 }
 
-func testExpectedScore(t *testing.T, filename string, testcase string, expectedScore int) []scorecard.TestScoreComment {
+func testExpectedScore(t *testing.T, filename string, testcase string, expectedScore scorecard.Grade) []scorecard.TestScoreComment {
 	return testExpectedScoreWithConfig(t, Configuration{}, filename, testcase, expectedScore)
 }
 
 func TestPodContainerNoResources(t *testing.T) {
-	testExpectedScore(t, "pod-test-resources-none.yaml", "Container Resources", 0)
+	testExpectedScore(t, "pod-test-resources-none.yaml", "Container Resources", 1)
 }
 
 func TestPodContainerResourceLimits(t *testing.T) {
@@ -58,7 +58,7 @@ func TestPodContainerResourceLimitCpuNotRequired(t *testing.T) {
 }
 
 func TestPodContainerResourceLimitCpuRequired(t *testing.T) {
-	testExpectedScoreWithConfig(t, Configuration{IgnoreContainerCpuLimitRequirement: false}, "pod-test-resources-limits-and-requests-no-cpu-limit.yaml", "Container Resources", 0)
+	testExpectedScoreWithConfig(t, Configuration{IgnoreContainerCpuLimitRequirement: false}, "pod-test-resources-limits-and-requests-no-cpu-limit.yaml", "Container Resources", 1)
 }
 
 func TestDeploymentResources(t *testing.T) {
@@ -70,7 +70,7 @@ func TestStatefulSetResources(t *testing.T) {
 }
 
 func TestPodContainerTagLatest(t *testing.T) {
-	testExpectedScore(t, "pod-image-tag-latest.yaml", "Container Image Tag", 0)
+	testExpectedScore(t, "pod-image-tag-latest.yaml", "Container Image Tag", 1)
 }
 
 func TestPodContainerTagFixed(t *testing.T) {
@@ -78,7 +78,7 @@ func TestPodContainerTagFixed(t *testing.T) {
 }
 
 func TestPodContainerPullPolicyUndefined(t *testing.T) {
-	testExpectedScore(t, "pod-image-pullpolicy-undefined.yaml", "Container Image Pull Policy", 0)
+	testExpectedScore(t, "pod-image-pullpolicy-undefined.yaml", "Container Image Pull Policy", 1)
 }
 
 func TestPodContainerPullPolicyUndefinedLatestTag(t *testing.T) {
@@ -90,7 +90,7 @@ func TestPodContainerPullPolicyUndefinedNoTag(t *testing.T) {
 }
 
 func TestPodContainerPullPolicyNever(t *testing.T) {
-	testExpectedScore(t, "pod-image-pullpolicy-never.yaml", "Container Image Pull Policy", 0)
+	testExpectedScore(t, "pod-image-pullpolicy-never.yaml", "Container Image Pull Policy", 1)
 }
 
 func TestPodContainerPullPolicyAlways(t *testing.T) {
@@ -98,7 +98,7 @@ func TestPodContainerPullPolicyAlways(t *testing.T) {
 }
 
 func TestContainerSecurityContextPrivilegied(t *testing.T) {
-	testExpectedScore(t, "pod-security-context-privilegied.yaml", "Container Security Context", 0)
+	testExpectedScore(t, "pod-security-context-privilegied.yaml", "Container Security Context", 1)
 }
 
 func TestContainerSecurityContextNonPrivilegied(t *testing.T) {
@@ -106,11 +106,11 @@ func TestContainerSecurityContextNonPrivilegied(t *testing.T) {
 }
 
 func TestContainerSecurityContextLowUser(t *testing.T) {
-	testExpectedScore(t, "pod-security-context-low-user-id.yaml", "Container Security Context", 0)
+	testExpectedScore(t, "pod-security-context-low-user-id.yaml", "Container Security Context", 1)
 }
 
 func TestContainerSecurityContextLowGroup(t *testing.T) {
-	testExpectedScore(t, "pod-security-context-low-group-id.yaml", "Container Security Context", 0)
+	testExpectedScore(t, "pod-security-context-low-group-id.yaml", "Container Security Context", 1)
 }
 
 func TestContainerSecurityContextHighIds(t *testing.T) {
diff --git a/score/service_test.go b/score/service_test.go
index 53fd1ac..5398fba 100644
--- a/score/service_test.go
+++ b/score/service_test.go
@@ -7,7 +7,7 @@ func TestServiceTargetsPodDeployment(t *testing.T) {
 }
 
 func TestServiceNotTargetsPodDeployment(t *testing.T) {
-	testExpectedScore(t, "service-not-target-deployment.yaml", "Service Targets Pod", 0)
+	testExpectedScore(t, "service-not-target-deployment.yaml", "Service Targets Pod", 1)
 }
 
 func TestServiceTargetsPodRaw(t *testing.T) {
@@ -15,7 +15,7 @@ func TestServiceTargetsPodRaw(t *testing.T) {
 }
 
 func TestServiceNotTargetsPodRaw(t *testing.T) {
-	testExpectedScore(t, "service-not-target-pod.yaml", "Service Targets Pod", 0)
+	testExpectedScore(t, "service-not-target-pod.yaml", "Service Targets Pod", 1)
 }
 
 func TestServiceTargetsPodRawMultiLabel(t *testing.T) {
@@ -23,7 +23,7 @@ func TestServiceTargetsPodRawMultiLabel(t *testing.T) {
 }
 
 func TestServiceNotTargetsPodRawMultiLabel(t *testing.T) {
-	testExpectedScore(t, "service-not-target-pod-multi-label.yaml", "Service Targets Pod", 0)
+	testExpectedScore(t, "service-not-target-pod-multi-label.yaml", "Service Targets Pod", 1)
 }
 
 func TestServiceTargetsPodRawSameNamespace(t *testing.T) {
@@ -31,7 +31,7 @@ func TestServiceTargetsPodRawSameNamespace(t *testing.T) {
 }
 
 func TestServiceTargetsPodRawDifferentNamespace(t *testing.T) {
-	testExpectedScore(t, "service-target-pod-different-namespace.yaml", "Service Targets Pod", 0)
+	testExpectedScore(t, "service-target-pod-different-namespace.yaml", "Service Targets Pod", 1)
 }
 
 func TestServiceTargetsPodDeploymentSameNamespace(t *testing.T) {
@@ -39,5 +39,5 @@ func TestServiceTargetsPodDeploymentSameNamespace(t *testing.T) {
 }
 
 func TestServiceTargetsPodDeploymentDifferentNamespace(t *testing.T) {
-	testExpectedScore(t, "service-target-deployment-different-namespace.yaml", "Service Targets Pod", 0)
+	testExpectedScore(t, "service-target-deployment-different-namespace.yaml", "Service Targets Pod", 1)
 }