Repository: zegl/kube-score
Pull request #288 (2020-08-05 19:44:25)

Support HPA v2beta1 and v2beta2
```
RELNOTE: Support HPA v2beta1 and v2beta2. All features supported by HPA v1 are supported.
```



Patch:
diff --git a/README.md b/README.md
index 22dceef..927c571 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,7 @@
 
 <p align="center"><img src="https://user-images.githubusercontent.com/47952/56085330-6c0a2480-5e41-11e9-89ba-0cfddd7714a8.png" height="200"></p>
 
-[![Go Report Card](https://goreportcard.com/badge/github.com/zegl/kube-score?)](https://goreportcard.com/report/github.com/zegl/kube-score)
-[![Build Status](https://circleci.com/gh/zegl/kube-score.svg?style=shield)](https://app.circleci.com/pipelines/github/zegl/kube-score?branch=master)
-[![Releases](https://img.shields.io/github/release-pre/zegl/kube-score.svg)](https://github.com/zegl/kube-score/releases)
-![GitHub stars](https://img.shields.io/github/stars/zegl/kube-score.svg?label=github%20stars)
-[![Downloads](https://img.shields.io/github/downloads/zegl/kube-score/total.svg)]()
-[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/zegl/kube-score/blob/master/LICENSE)
+[![Join #kube-score on the Kubernetes Slack](https://img.shields.io/badge/Slack-kubernetes%2Fkube--score-blue.svg)](https://slack.k8s.io/)
 
 ---
 
diff --git a/domain/kube-score.go b/domain/kube-score.go
index 77a5a17..f77d616 100644
--- a/domain/kube-score.go
+++ b/domain/kube-score.go
@@ -30,6 +30,12 @@ type PodSpecer interface {
 	GetPodTemplateSpec() corev1.PodTemplateSpec
 }
 
+type HpaTargeter interface {
+	GetTypeMeta() metav1.TypeMeta
+	GetObjectMeta() metav1.ObjectMeta
+	HpaTarget() autoscalingv1.CrossVersionObjectReference
+}
+
 type Metas interface {
 	Metas() []BothMeta
 }
@@ -71,7 +77,7 @@ type PodDisruptionBudgets interface {
 }
 
 type HorizontalPodAutoscalers interface {
-	HorizontalPodAutoscalers() []autoscalingv1.HorizontalPodAutoscaler
+	HorizontalPodAutoscalers() []HpaTargeter
 }
 
 type AllTypes interface {
diff --git a/parser/internal/hpa.go b/parser/internal/hpa.go
new file mode 100644
index 0000000..45bd673
--- /dev/null
+++ b/parser/internal/hpa.go
@@ -0,0 +1,56 @@
+package internal
+
+import (
+	autoscalingv1 "k8s.io/api/autoscaling/v1"
+	autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1"
+	autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type HPAv1 struct {
+	autoscalingv1.HorizontalPodAutoscaler
+}
+
+func (d HPAv1) GetTypeMeta() metav1.TypeMeta {
+	return d.TypeMeta
+}
+
+func (d HPAv1) GetObjectMeta() metav1.ObjectMeta {
+	return d.ObjectMeta
+}
+
+func (d HPAv1) HpaTarget() autoscalingv1.CrossVersionObjectReference {
+	return d.Spec.ScaleTargetRef
+}
+
+type HPAv2beta1 struct {
+	autoscalingv2beta1.HorizontalPodAutoscaler
+}
+
+func (d HPAv2beta1) GetTypeMeta() metav1.TypeMeta {
+	return d.TypeMeta
+}
+
+func (d HPAv2beta1) GetObjectMeta() metav1.ObjectMeta {
+	return d.ObjectMeta
+}
+
+func (d HPAv2beta1) HpaTarget() autoscalingv1.CrossVersionObjectReference {
+	return autoscalingv1.CrossVersionObjectReference(d.Spec.ScaleTargetRef)
+}
+
+type HPAv2beta2 struct {
+	autoscalingv2beta2.HorizontalPodAutoscaler
+}
+
+func (d HPAv2beta2) GetTypeMeta() metav1.TypeMeta {
+	return d.TypeMeta
+}
+
+func (d HPAv2beta2) GetObjectMeta() metav1.ObjectMeta {
+	return d.ObjectMeta
+}
+
+func (d HPAv2beta2) HpaTarget() autoscalingv1.CrossVersionObjectReference {
+	return autoscalingv1.CrossVersionObjectReference(d.Spec.ScaleTargetRef)
+}
diff --git a/parser/parse.go b/parser/parse.go
index 1139066..016740f 100644
--- a/parser/parse.go
+++ b/parser/parse.go
@@ -11,6 +11,8 @@ import (
 	appsv1beta1 "k8s.io/api/apps/v1beta1"
 	appsv1beta2 "k8s.io/api/apps/v1beta2"
 	autoscalingv1 "k8s.io/api/autoscaling/v1"
+	autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1"
+	autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2"
 	batchv1 "k8s.io/api/batch/v1"
 	batchv1beta1 "k8s.io/api/batch/v1beta1"
 	corev1 "k8s.io/api/core/v1"
@@ -51,17 +53,18 @@ type detectKind struct {
 }
 
 type parsedObjects struct {
-	bothMetas                []ks.BothMeta
-	pods                     []corev1.Pod
-	podspecers               []ks.PodSpecer
-	networkPolicies          []networkingv1.NetworkPolicy
-	services                 []corev1.Service
-	podDisruptionBudgets     []policyv1beta1.PodDisruptionBudget
-	deployments              []appsv1.Deployment
-	statefulsets             []appsv1.StatefulSet
-	ingresses                []extensionsv1beta1.Ingress
-	cronjobs                 []batchv1beta1.CronJob
-	horizontalPodAutoscalers []autoscalingv1.HorizontalPodAutoscaler
+	bothMetas            []ks.BothMeta
+	pods                 []corev1.Pod
+	podspecers           []ks.PodSpecer
+	networkPolicies      []networkingv1.NetworkPolicy
+	services             []corev1.Service
+	podDisruptionBudgets []policyv1beta1.PodDisruptionBudget
+	deployments          []appsv1.Deployment
+	statefulsets         []appsv1.StatefulSet
+	ingresses            []extensionsv1beta1.Ingress
+	cronjobs             []batchv1beta1.CronJob
+	// horizontalPodAutoscalers []autoscalingv1.HorizontalPodAutoscaler
+	hpaTargeters []ks.HpaTargeter // all versions of HPAs
 }
 
 func (p *parsedObjects) Services() []corev1.Service {
@@ -104,8 +107,8 @@ func (p *parsedObjects) NetworkPolicies() []networkingv1.NetworkPolicy {
 	return p.networkPolicies
 }
 
-func (p *parsedObjects) HorizontalPodAutoscalers() []autoscalingv1.HorizontalPodAutoscaler {
-	return p.horizontalPodAutoscalers
+func (p *parsedObjects) HorizontalPodAutoscalers() []ks.HpaTargeter {
+	return p.hpaTargeters
 }
 
 func Empty() ks.AllTypes {
@@ -279,7 +282,19 @@ func decodeItem(cnf config.Configuration, s *parsedObjects, detectedVersion sche
 	case autoscalingv1.SchemeGroupVersion.WithKind("HorizontalPodAutoscaler"):
 		var hpa autoscalingv1.HorizontalPodAutoscaler
 		errs.AddIfErr(decode(fileContents, &hpa))
-		s.horizontalPodAutoscalers = append(s.horizontalPodAutoscalers, hpa)
+		s.bothMetas = append(s.bothMetas, ks.BothMeta{hpa.TypeMeta, hpa.ObjectMeta})
+		s.hpaTargeters = append(s.hpaTargeters, internal.HPAv1{hpa})
+
+	case autoscalingv2beta1.SchemeGroupVersion.WithKind("HorizontalPodAutoscaler"):
+		var hpa autoscalingv2beta1.HorizontalPodAutoscaler
+		errs.AddIfErr(decode(fileContents, &hpa))
+		s.hpaTargeters = append(s.hpaTargeters, internal.HPAv2beta1{hpa})
+		s.bothMetas = append(s.bothMetas, ks.BothMeta{hpa.TypeMeta, hpa.ObjectMeta})
+
+	case autoscalingv2beta2.SchemeGroupVersion.WithKind("HorizontalPodAutoscaler"):
+		var hpa autoscalingv2beta2.HorizontalPodAutoscaler
+		errs.AddIfErr(decode(fileContents, &hpa))
+		s.hpaTargeters = append(s.hpaTargeters, internal.HPAv2beta2{hpa})
 		s.bothMetas = append(s.bothMetas, ks.BothMeta{hpa.TypeMeta, hpa.ObjectMeta})
 
 	default:
diff --git a/score/apps/apps.go b/score/apps/apps.go
index 6c9e437..b6b644b 100644
--- a/score/apps/apps.go
+++ b/score/apps/apps.go
@@ -2,28 +2,31 @@ package apps
 
 import (
 	appsv1 "k8s.io/api/apps/v1"
-	autoscalingv1 "k8s.io/api/autoscaling/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"strings"
 
+	ks "github.com/zegl/kube-score/domain"
 	"github.com/zegl/kube-score/score/checks"
 	"github.com/zegl/kube-score/score/internal"
 	"github.com/zegl/kube-score/scorecard"
 )
 
-func Register(allChecks *checks.Checks, allHPAs []autoscalingv1.HorizontalPodAutoscaler) {
+func Register(allChecks *checks.Checks, allHPAs []ks.HpaTargeter) {
 	allChecks.RegisterDeploymentCheck("Deployment has host PodAntiAffinity", "Makes sure that a podAntiAffinity has been set that prevents multiple pods from being scheduled on the same node. https://kubernetes.io/docs/concepts/configuration/assign-pod-node/", deploymentHasAntiAffinity)
 	allChecks.RegisterStatefulSetCheck("StatefulSet has host PodAntiAffinity", "Makes sure that a podAntiAffinity has been set that prevents multiple pods from being scheduled on the same node. https://kubernetes.io/docs/concepts/configuration/assign-pod-node/", statefulsetHasAntiAffinity)
 	allChecks.RegisterDeploymentCheck("Deployment targeted by HPA does not have replicas configured", "Hakes sure that Deployments using a HorizontalPodAutoscaler doesn't have a statically configured replica count set", hpaDeploymentNoReplicas(allHPAs))
 }
 
-func hpaDeploymentNoReplicas(allHPAs []autoscalingv1.HorizontalPodAutoscaler) func(deployment appsv1.Deployment) (scorecard.TestScore, error) {
+func hpaDeploymentNoReplicas(allHPAs []ks.HpaTargeter) func(deployment appsv1.Deployment) (scorecard.TestScore, error) {
 	return func(deployment appsv1.Deployment) (score scorecard.TestScore, err error) {
 		// If is targeted by a HPA
 		for _, hpa := range allHPAs {
-			if hpa.Namespace == deployment.Namespace &&
-				hpa.Spec.ScaleTargetRef.Kind == deployment.Kind &&
-				hpa.Spec.ScaleTargetRef.Name == deployment.Name {
+			target := hpa.HpaTarget()
+
+			if hpa.GetObjectMeta().Namespace == deployment.Namespace &&
+				strings.ToLower(target.Kind) == strings.ToLower(deployment.Kind) &&
+				target.Name == deployment.Name {
 
 				if deployment.Spec.Replicas == nil {
 					score.Grade = scorecard.GradeAllOK
diff --git a/score/checks/checks.go b/score/checks/checks.go
index a7febb4..68308aa 100644
--- a/score/checks/checks.go
+++ b/score/checks/checks.go
@@ -1,7 +1,6 @@
 package checks
 
 import (
-	autoscalingv1 "k8s.io/api/autoscaling/v1"
 	"strings"
 
 	appsv1 "k8s.io/api/apps/v1"
@@ -97,7 +96,7 @@ type CronJobCheck struct {
 	Fn CronJobCheckFn
 }
 
-type HorizontalPodAutoscalerCheckFn = func(autoscalingv1.HorizontalPodAutoscaler) scorecard.TestScore
+type HorizontalPodAutoscalerCheckFn = func(ks.HpaTargeter) scorecard.TestScore
 type HorizontalPodAutoscalerCheck struct {
 	ks.Check
 	Fn HorizontalPodAutoscalerCheckFn
diff --git a/score/hpa/hpa.go b/score/hpa/hpa.go
index 8e1bfc8..898dc94 100644
--- a/score/hpa/hpa.go
+++ b/score/hpa/hpa.go
@@ -1,8 +1,6 @@
 package hpa
 
 import (
-	v1 "k8s.io/api/autoscaling/v1"
-
 	"github.com/zegl/kube-score/domain"
 	"github.com/zegl/kube-score/score/checks"
 	"github.com/zegl/kube-score/scorecard"
@@ -12,15 +10,15 @@ func Register(allChecks *checks.Checks, allTargetableObjs []domain.BothMeta) {
 	allChecks.RegisterHorizontalPodAutoscalerCheck("HorizontalPodAutoscaler has target", `Makes sure that the HPA targets a valid object`, hpaHasTarget(allTargetableObjs))
 }
 
-func hpaHasTarget(allTargetableObjs []domain.BothMeta) func(v1.HorizontalPodAutoscaler) scorecard.TestScore {
-	return func(hpa v1.HorizontalPodAutoscaler) (score scorecard.TestScore) {
-		targetRef := hpa.Spec.ScaleTargetRef
+func hpaHasTarget(allTargetableObjs []domain.BothMeta) func(hpa domain.HpaTargeter) scorecard.TestScore {
+	return func(hpa domain.HpaTargeter) (score scorecard.TestScore) {
+		targetRef := hpa.HpaTarget()
 		var hasTarget bool
 		for _, t := range allTargetableObjs {
 			if t.TypeMeta.APIVersion == targetRef.APIVersion &&
 				t.TypeMeta.Kind == targetRef.Kind &&
 				t.ObjectMeta.Name == targetRef.Name &&
-				t.ObjectMeta.Namespace == hpa.Namespace {
+				t.ObjectMeta.Namespace == hpa.GetObjectMeta().Namespace {
 				hasTarget = true
 				break
 			}
diff --git a/score/score.go b/score/score.go
index a89dc9d..af7dbd3 100644
--- a/score/score.go
+++ b/score/score.go
@@ -128,7 +128,7 @@ func Score(allObjects ks.AllTypes, cnf config.Configuration) (*scorecard.Scoreca
 	}
 
 	for _, hpa := range allObjects.HorizontalPodAutoscalers() {
-		o := newObject(hpa.TypeMeta, hpa.ObjectMeta)
+		o := newObject(hpa.GetTypeMeta(), hpa.GetObjectMeta())
 		for _, test := range allChecks.HorizontalPodAutoscalers() {
 			o.Add(test.Fn(hpa), test.Check)
 		}


Test patch:
diff --git a/score/apps/apps_test.go b/score/apps/apps_test.go
index f612c55..dae192e 100644
--- a/score/apps/apps_test.go
+++ b/score/apps/apps_test.go
@@ -8,6 +8,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"testing"
 
+	ks "github.com/zegl/kube-score/domain"
 	"github.com/zegl/kube-score/scorecard"
 )
 
@@ -164,13 +165,15 @@ func TestDeploymentTargetedByHpaHasNoReplicasAllOK(t *testing.T) {
 		},
 	}
 
-	hpas := []autoscalingv1.HorizontalPodAutoscaler{
-		{
-			Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
-				ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
-					Kind:       "Deployment",
-					Name:       "foo",
-					APIVersion: "apps/v1",
+	hpas := []ks.HpaTargeter{
+		hpav1{
+			autoscalingv1.HorizontalPodAutoscaler{
+				Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
+					ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
+						Kind:       "Deployment",
+						Name:       "foo",
+						APIVersion: "apps/v1",
+					},
 				},
 			},
 		},
@@ -183,7 +186,7 @@ func TestDeploymentTargetedByHpaHasNoReplicasAllOK(t *testing.T) {
 	assert.False(t, score.Skipped)
 }
 
-func TestDeploymentTargetedByHpaHasSetReplicasAllOK(t *testing.T) {
+func TestDeploymentTargetedByHpaHasSetReplicasCritical(t *testing.T) {
 	t.Parallel()
 
 	deployment := appsv1.Deployment{
@@ -194,13 +197,15 @@ func TestDeploymentTargetedByHpaHasSetReplicasAllOK(t *testing.T) {
 		},
 	}
 
-	hpas := []autoscalingv1.HorizontalPodAutoscaler{
-		{
-			Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
-				ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
-					Kind:       "Deployment",
-					Name:       "foo",
-					APIVersion: "apps/v1",
+	hpas := []ks.HpaTargeter{
+		hpav1{
+			autoscalingv1.HorizontalPodAutoscaler{
+				Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
+					ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
+						Kind:       "Deployment",
+						Name:       "foo",
+						APIVersion: "apps/v1",
+					},
 				},
 			},
 		},
@@ -224,13 +229,15 @@ func TestDeploymentNotTargetedByHpaIsSkippedAllOKK(t *testing.T) {
 		},
 	}
 
-	hpas := []autoscalingv1.HorizontalPodAutoscaler{
-		{
-			Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
-				ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
-					Kind:       "Deployment",
-					Name:       "some-other-obj",
-					APIVersion: "apps/v1",
+	hpas := []ks.HpaTargeter{
+		hpav1{
+			autoscalingv1.HorizontalPodAutoscaler{
+				Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
+					ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
+						Kind:       "Deployment",
+						Name:       "some-other-foo",
+						APIVersion: "apps/v1",
+					},
 				},
 			},
 		},
@@ -242,3 +249,51 @@ func TestDeploymentNotTargetedByHpaIsSkippedAllOKK(t *testing.T) {
 	assert.Equal(t, scorecard.GradeAllOK, score.Grade)
 	assert.True(t, score.Skipped)
 }
+
+func TestDeploymentTargetedByHpaHasNoReplicasAllOKCaseInsensitiveKind(t *testing.T) {
+	t.Parallel()
+
+	deployment := appsv1.Deployment{
+		TypeMeta:   metav1.TypeMeta{Kind: "Deployment"},
+		ObjectMeta: metav1.ObjectMeta{Name: "foo"},
+		Spec: appsv1.DeploymentSpec{
+			Replicas: nil,
+		},
+	}
+
+	hpas := []ks.HpaTargeter{
+		hpav1{
+			autoscalingv1.HorizontalPodAutoscaler{
+				Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
+					ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
+						Kind:       "deployment",
+						Name:       "foo",
+						APIVersion: "apps/v1",
+					},
+				},
+			},
+		},
+	}
+
+	f := hpaDeploymentNoReplicas(hpas)
+	score, err := f(deployment)
+	assert.Nil(t, err)
+	assert.Equal(t, scorecard.GradeAllOK, score.Grade)
+	assert.False(t, score.Skipped)
+}
+
+type hpav1 struct {
+	autoscalingv1.HorizontalPodAutoscaler
+}
+
+func (d hpav1) GetTypeMeta() metav1.TypeMeta {
+	return d.TypeMeta
+}
+
+func (d hpav1) GetObjectMeta() metav1.ObjectMeta {
+	return d.ObjectMeta
+}
+
+func (d hpav1) HpaTarget() autoscalingv1.CrossVersionObjectReference {
+	return d.Spec.ScaleTargetRef
+}
diff --git a/score/hpa/hpa_test.go b/score/hpa/hpa_test.go
index ad87dac..76d7c24 100644
--- a/score/hpa/hpa_test.go
+++ b/score/hpa/hpa_test.go
@@ -159,7 +159,23 @@ func TestHpaHasTarget(t *testing.T) {
 
 	for _, tc := range testcases {
 		fn := hpaHasTarget(tc.allTargets)
-		score := fn(tc.hpa)
+		score := fn(hpav1{tc.hpa})
 		assert.Equal(t, tc.expectedGrade, score.Grade)
 	}
 }
+
+type hpav1 struct {
+	v1.HorizontalPodAutoscaler
+}
+
+func (d hpav1) GetTypeMeta() metav1.TypeMeta {
+	return d.TypeMeta
+}
+
+func (d hpav1) GetObjectMeta() metav1.ObjectMeta {
+	return d.ObjectMeta
+}
+
+func (d hpav1) HpaTarget() v1.CrossVersionObjectReference {
+	return d.Spec.ScaleTargetRef
+}