Repository: zegl/kube-score
Pull request #516 (2023-02-22 12:12:00)

[#495] implement feature request: topologySpreadConstraints
<!--
    Optional: Add this change to the release notes by adding a RELNOTE comment
    If this shouldn't appear in the notes, simply remove this.
-->

```
RELNOTE: Add Pod Topology Spread Constraints checks
```


zegl: Really nice, thanks a lot! ⭐ 

Patch:
diff --git a/score/podtopologyspreadconstraints/podtopologyspreadconstraints.go b/score/podtopologyspreadconstraints/podtopologyspreadconstraints.go
new file mode 100644
index 0000000..f435174
--- /dev/null
+++ b/score/podtopologyspreadconstraints/podtopologyspreadconstraints.go
@@ -0,0 +1,57 @@
+package podtopologyspreadconstraints
+
+import (
+	ks "github.com/zegl/kube-score/domain"
+	"github.com/zegl/kube-score/score/checks"
+	"github.com/zegl/kube-score/scorecard"
+)
+
+func Register(allChecks *checks.Checks) {
+	allChecks.RegisterPodCheck("Pod Topology Spread Constraints", "Pod Topology Spread Constraints", podTopologySpreadConstraints)
+}
+
+func podTopologySpreadConstraints(pod ks.PodSpecer) (score scorecard.TestScore, err error) {
+	spreads := pod.GetPodTemplateSpec().Spec.TopologySpreadConstraints
+
+	if spreads == nil {
+		score.Grade = scorecard.GradeAllOK
+		score.AddComment("", "Pod Topology Spread Constraints", "No Pod Topology Spread Constraints set, kube-scheduler defaults assumed")
+		return
+	}
+
+	for _, spread := range spreads {
+		if spread.LabelSelector == nil {
+			score.Grade = scorecard.GradeCritical
+			score.AddComment("", "Pod Topology Spread Constraints", "No labelSelector detected. A label selector is needed determine the number of pods in a topology domain")
+			return
+		}
+
+		if spread.MaxSkew == 0 {
+			score.Grade = scorecard.GradeCritical
+			score.AddComment("", "Pod Topology Spread Constraints", "MaxSkew is set to zero. This is not allowed.")
+			return
+		}
+
+		if spread.MinDomains != nil && *spread.MinDomains == 0 {
+			score.Grade = scorecard.GradeCritical
+			score.AddComment("", "Pod Topology Spread Constraints", "MaxDomain set to zero. This is not allowed. Constraint behaves if minDomains is set to 1 if nil")
+			return
+		}
+
+		if spread.TopologyKey == "" {
+			score.Grade = scorecard.GradeCritical
+			score.AddComment("", "Pod Topology Spread Constraints", "TopologyKey is not set. This is the key of node labels used to bucket nodes into a domain")
+			return
+		}
+
+		if spread.WhenUnsatisfiable != "DoNotSchedule" && spread.WhenUnsatisfiable != "ScheduleAnyway" {
+			score.Grade = scorecard.GradeCritical
+			score.AddComment("", "Pod Topology Spread Constraints", "Invalid WhenUnsatisfiable setting detected")
+			return
+		}
+	}
+
+	score.Grade = scorecard.GradeAllOK
+	score.AddComment("", "Pod Topology Spread Constraints", "Pod Topology Spread Constraints")
+	return
+}
diff --git a/score/score.go b/score/score.go
index 8f538a4..56db5e8 100644
--- a/score/score.go
+++ b/score/score.go
@@ -1,9 +1,6 @@
 package score
 
 import (
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
 	"github.com/zegl/kube-score/config"
 	ks "github.com/zegl/kube-score/domain"
 	"github.com/zegl/kube-score/score/apps"
@@ -15,11 +12,14 @@ import (
 	"github.com/zegl/kube-score/score/ingress"
 	"github.com/zegl/kube-score/score/meta"
 	"github.com/zegl/kube-score/score/networkpolicy"
+	"github.com/zegl/kube-score/score/podtopologyspreadconstraints"
 	"github.com/zegl/kube-score/score/probes"
 	"github.com/zegl/kube-score/score/security"
 	"github.com/zegl/kube-score/score/service"
 	"github.com/zegl/kube-score/score/stable"
 	"github.com/zegl/kube-score/scorecard"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func RegisterAllChecks(allObjects ks.AllTypes, cnf config.Configuration) *checks.Checks {
@@ -37,6 +37,7 @@ func RegisterAllChecks(allObjects ks.AllTypes, cnf config.Configuration) *checks
 	apps.Register(allChecks, allObjects.HorizontalPodAutoscalers(), allObjects.Services())
 	meta.Register(allChecks)
 	hpa.Register(allChecks, allObjects.Metas())
+	podtopologyspreadconstraints.Register(allChecks)
 
 	return allChecks
 }


Test patch:
diff --git a/score/podtopologyspreadconstraints_test.go b/score/podtopologyspreadconstraints_test.go
new file mode 100644
index 0000000..26d66c4
--- /dev/null
+++ b/score/podtopologyspreadconstraints_test.go
@@ -0,0 +1,42 @@
+package score
+
+import (
+	"testing"
+
+	"github.com/zegl/kube-score/scorecard"
+)
+
+func TestPodTopologySpreadContraintsWithOneConstraint(t *testing.T) {
+	t.Parallel()
+	testExpectedScore(t, "pod-topology-spread-constraints-one-constraint.yaml", "Pod Topology Spread Constraints", scorecard.GradeAllOK)
+}
+
+func TestPodTopologySpreadContraintsWithTwoConstraints(t *testing.T) {
+	t.Parallel()
+	testExpectedScore(t, "pod-topology-spread-constraints-two-constraints.yaml", "Pod Topology Spread Constraints", scorecard.GradeAllOK)
+}
+
+func TestPodTopologySpreadContraintsNoLabelSelector(t *testing.T) {
+	t.Parallel()
+	testExpectedScore(t, "pod-topology-spread-constraints-no-labelselector.yaml", "Pod Topology Spread Constraints", scorecard.GradeCritical)
+}
+
+func TestPodTopologySpreadContraintsInvalidMaxSkew(t *testing.T) {
+	t.Parallel()
+	testExpectedScore(t, "pod-topology-spread-constraints-invalid-maxskew.yaml", "Pod Topology Spread Constraints", scorecard.GradeCritical)
+}
+
+func TestPodTopologySpreadContraintsInvalidMinDomains(t *testing.T) {
+	t.Parallel()
+	testExpectedScore(t, "pod-topology-spread-constraints-invalid-mindomains.yaml", "Pod Topology Spread Constraints", scorecard.GradeCritical)
+}
+
+func TestPodTopologySpreadContraintsNoTopologyKey(t *testing.T) {
+	t.Parallel()
+	testExpectedScore(t, "pod-topology-spread-constraints-no-topologykey.yaml", "Pod Topology Spread Constraints", scorecard.GradeCritical)
+}
+
+func TestPodTopologySpreadContraintsInvalidDirective(t *testing.T) {
+	t.Parallel()
+	testExpectedScore(t, "pod-topology-spread-constraints-invalid-whenunsatisfiable.yaml", "Pod Topology Spread Constraints", scorecard.GradeCritical)
+}
diff --git a/score/testdata/pod-topology-spread-constraints-invalid-maxskew.yaml b/score/testdata/pod-topology-spread-constraints-invalid-maxskew.yaml
new file mode 100644
index 0000000..3e1a1af
--- /dev/null
+++ b/score/testdata/pod-topology-spread-constraints-invalid-maxskew.yaml
@@ -0,0 +1,17 @@
+kind: Pod
+apiVersion: v1
+metadata:
+  name: mypod
+  labels:
+    foo: bar
+spec:
+  topologySpreadConstraints:
+  - maxSkew: 0
+    topologyKey: zone
+    whenUnsatisfiable: DoNotSchedule
+    labelSelector:
+      matchLabels:
+        foo: bar
+  containers:
+    - name: pause
+      image: registry.k8s.io/pause:3.1
diff --git a/score/testdata/pod-topology-spread-constraints-invalid-mindomains.yaml b/score/testdata/pod-topology-spread-constraints-invalid-mindomains.yaml
new file mode 100644
index 0000000..c593d46
--- /dev/null
+++ b/score/testdata/pod-topology-spread-constraints-invalid-mindomains.yaml
@@ -0,0 +1,18 @@
+kind: Pod
+apiVersion: v1
+metadata:
+  name: mypod
+  labels:
+    foo: bar
+spec:
+  topologySpreadConstraints:
+  - maxSkew: 1
+    minDomains: 0
+    topologyKey: zone
+    whenUnsatisfiable: DoNotSchedule
+    labelSelector:
+      matchLabels:
+        foo: bar
+  containers:
+    - name: pause
+      image: registry.k8s.io/pause:3.1
diff --git a/score/testdata/pod-topology-spread-constraints-invalid-whenunsatisfiable.yaml b/score/testdata/pod-topology-spread-constraints-invalid-whenunsatisfiable.yaml
new file mode 100644
index 0000000..e1238ce
--- /dev/null
+++ b/score/testdata/pod-topology-spread-constraints-invalid-whenunsatisfiable.yaml
@@ -0,0 +1,17 @@
+kind: Pod
+apiVersion: v1
+metadata:
+  name: mypod
+  labels:
+    foo: bar
+spec:
+  topologySpreadConstraints:
+  - maxSkew: 1
+    topologyKey: zone
+    whenUnsatisfiable: DoNotSCHEDULE 
+    labelSelector:
+      matchLabels:
+        foo: bar
+  containers:
+    - name: pause
+      image: registry.k8s.io/pause:3.1
diff --git a/score/testdata/pod-topology-spread-constraints-no-labelselector.yaml b/score/testdata/pod-topology-spread-constraints-no-labelselector.yaml
new file mode 100644
index 0000000..dce9bf1
--- /dev/null
+++ b/score/testdata/pod-topology-spread-constraints-no-labelselector.yaml
@@ -0,0 +1,14 @@
+kind: Pod
+apiVersion: v1
+metadata:
+  name: mypod
+  labels:
+    foo: bar
+spec:
+  topologySpreadConstraints:
+  - maxSkew: 1
+    topologyKey: zone
+    whenUnsatisfiable: DoNotSchedule
+  containers:
+    - name: pause
+      image: registry.k8s.io/pause:3.1
diff --git a/score/testdata/pod-topology-spread-constraints-no-topologykey.yaml b/score/testdata/pod-topology-spread-constraints-no-topologykey.yaml
new file mode 100644
index 0000000..2cb90c4
--- /dev/null
+++ b/score/testdata/pod-topology-spread-constraints-no-topologykey.yaml
@@ -0,0 +1,16 @@
+kind: Pod
+apiVersion: v1
+metadata:
+  name: mypod
+  labels:
+    foo: bar
+spec:
+  topologySpreadConstraints:
+  - maxSkew: 1
+    whenUnsatisfiable: DoNotSchedule
+    labelSelector:
+      matchLabels:
+        foo: bar
+  containers:
+    - name: pause
+      image: registry.k8s.io/pause:3.1
diff --git a/score/testdata/pod-topology-spread-constraints-one-constraint.yaml b/score/testdata/pod-topology-spread-constraints-one-constraint.yaml
new file mode 100644
index 0000000..5b26c57
--- /dev/null
+++ b/score/testdata/pod-topology-spread-constraints-one-constraint.yaml
@@ -0,0 +1,17 @@
+kind: Pod
+apiVersion: v1
+metadata:
+  name: mypod
+  labels:
+    foo: bar
+spec:
+  topologySpreadConstraints:
+  - maxSkew: 1
+    topologyKey: zone
+    whenUnsatisfiable: DoNotSchedule
+    labelSelector:
+      matchLabels:
+        foo: bar
+  containers:
+    - name: pause
+      image: registry.k8s.io/pause:3.1
diff --git a/score/testdata/pod-topology-spread-constraints-two-constraints.yaml b/score/testdata/pod-topology-spread-constraints-two-constraints.yaml
new file mode 100644
index 0000000..04aba40
--- /dev/null
+++ b/score/testdata/pod-topology-spread-constraints-two-constraints.yaml
@@ -0,0 +1,23 @@
+kind: Pod
+apiVersion: v1
+metadata:
+  name: mypod
+  labels:
+    foo: bar
+spec:
+  topologySpreadConstraints:
+  - maxSkew: 1
+    topologyKey: zone
+    whenUnsatisfiable: DoNotSchedule
+    labelSelector:
+      matchLabels:
+        foo: bar
+  - maxSkew: 1
+    topologyKey: node
+    whenUnsatisfiable: DoNotSchedule
+    labelSelector:
+      matchLabels:
+        foo: bar
+  containers:
+  - name: pause
+  image: registry.k8s.io/pause:3.1
\ No newline at end of file