Repository: zegl/kube-score
Pull request #93 (2019-01-16 21:30:17)

Add List support


Patch:
diff --git a/README.md b/README.md
index ee690f6..097a1d1 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Pre-built releases can be downloaded from the [Github Releases page](https://git
 
 ### Building from source
 
-`kube-score` requires [go](https://golang.org/) in version `1.11.+` with [go modules](https://github.com/golang/go/wiki/Modules). To install `kube-score` into your local gobin path run the following commands:
+`kube-score` requires [Go](https://golang.org/) `1.11` or later. To install `kube-score` into your local gobin, run the following commands:
 
 ```bash
 go get github.com/zegl/kube-score
diff --git a/parser/parse.go b/parser/parse.go
index ebbd102..d80c396 100644
--- a/parser/parse.go
+++ b/parser/parse.go
@@ -106,11 +106,6 @@ func Empty() ks.AllTypes {
 func ParseFiles(cnf config.Configuration) (ks.AllTypes, error) {
 	s := &parsedObjects{}
 
-	addPodSpeccer := func(ps ks.PodSpecer) {
-		s.podspecers = append(s.podspecers, ps)
-		s.bothMetas = append(s.bothMetas, ks.BothMeta{ps.GetTypeMeta(), ps.GetObjectMeta()})
-	}
-
 	for _, file := range cnf.AllFiles {
 		fullFile, err := ioutil.ReadAll(file)
 		if err != nil {
@@ -121,119 +116,150 @@ func ParseFiles(cnf config.Configuration) (ks.AllTypes, error) {
 		fullFile = bytes.Replace(fullFile, []byte("\r\n"), []byte("\n"), -1)
 
 		for _, fileContents := range bytes.Split(fullFile, []byte("\n---\n")) {
-			var detect detectKind
-			err = yaml.Unmarshal(fileContents, &detect)
+			err := detectAndDecode(cnf, s, fileContents)
 			if err != nil {
 				return nil, err
 			}
+		}
+	}
 
-			decode := func(data []byte, object runtime.Object) {
-				deserializer := codecs.UniversalDeserializer()
-				if _, _, err := deserializer.Decode(data, nil, object); err != nil {
-					panic(err)
-				}
-			}
+	return s, nil
+}
 
-			detectedVersion := schema.FromAPIVersionAndKind(detect.ApiVersion, detect.Kind)
-
-			switch detectedVersion {
-			case corev1.SchemeGroupVersion.WithKind("Pod"):
-				var pod corev1.Pod
-				decode(fileContents, &pod)
-				s.pods = append(s.pods, pod)
-				s.bothMetas = append(s.bothMetas, ks.BothMeta{pod.TypeMeta, pod.ObjectMeta})
-
-			case batchv1.SchemeGroupVersion.WithKind("Job"):
-				var job batchv1.Job
-				decode(fileContents, &job)
-				addPodSpeccer(internal.Batchv1Job{job})
-
-			case batchv1beta1.SchemeGroupVersion.WithKind("CronJob"):
-				var cronjob batchv1beta1.CronJob
-				decode(fileContents, &cronjob)
-				addPodSpeccer(internal.Batchv1beta1CronJob{cronjob})
-				s.cronjobs = append(s.cronjobs, cronjob)
-
-			case appsv1.SchemeGroupVersion.WithKind("Deployment"):
-				var deployment appsv1.Deployment
-				decode(fileContents, &deployment)
-				addPodSpeccer(internal.Appsv1Deployment{deployment})
-
-				// TODO: Support older versions of Deployment as well?
-				s.deployments = append(s.deployments, deployment)
-			case appsv1beta1.SchemeGroupVersion.WithKind("Deployment"):
-				var deployment appsv1beta1.Deployment
-				decode(fileContents, &deployment)
-				addPodSpeccer(internal.Appsv1beta1Deployment{deployment})
-			case appsv1beta2.SchemeGroupVersion.WithKind("Deployment"):
-				var deployment appsv1beta2.Deployment
-				decode(fileContents, &deployment)
-				addPodSpeccer(internal.Appsv1beta2Deployment{deployment})
-			case extensionsv1beta1.SchemeGroupVersion.WithKind("Deployment"):
-				var deployment extensionsv1beta1.Deployment
-				decode(fileContents, &deployment)
-				addPodSpeccer(internal.Extensionsv1beta1Deployment{deployment})
-
-			case appsv1.SchemeGroupVersion.WithKind("StatefulSet"):
-				var statefulSet appsv1.StatefulSet
-				decode(fileContents, &statefulSet)
-				addPodSpeccer(internal.Appsv1StatefulSet{statefulSet})
-
-				// TODO: Support older versions of StatefulSet as well?
-				s.statefulsets = append(s.statefulsets, statefulSet)
-			case appsv1beta1.SchemeGroupVersion.WithKind("StatefulSet"):
-				var statefulSet appsv1beta1.StatefulSet
-				decode(fileContents, &statefulSet)
-				addPodSpeccer(internal.Appsv1beta1StatefulSet{statefulSet})
-			case appsv1beta2.SchemeGroupVersion.WithKind("StatefulSet"):
-				var statefulSet appsv1beta2.StatefulSet
-				decode(fileContents, &statefulSet)
-				addPodSpeccer(internal.Appsv1beta2StatefulSet{statefulSet})
-
-			case appsv1.SchemeGroupVersion.WithKind("DaemonSet"):
-				var daemonset appsv1.DaemonSet
-				decode(fileContents, &daemonset)
-				addPodSpeccer(internal.Appsv1DaemonSet{daemonset})
-			case appsv1beta2.SchemeGroupVersion.WithKind("DaemonSet"):
-				var daemonset appsv1beta2.DaemonSet
-				decode(fileContents, &daemonset)
-				addPodSpeccer(internal.Appsv1beta2DaemonSet{daemonset})
-			case extensionsv1beta1.SchemeGroupVersion.WithKind("DaemonSet"):
-				var daemonset extensionsv1beta1.DaemonSet
-				decode(fileContents, &daemonset)
-				addPodSpeccer(internal.Extensionsv1beta1DaemonSet{daemonset})
-
-			case networkingv1.SchemeGroupVersion.WithKind("NetworkPolicy"):
-				var netpol networkingv1.NetworkPolicy
-				decode(fileContents, &netpol)
-				s.networkPolicies = append(s.networkPolicies, netpol)
-				s.bothMetas = append(s.bothMetas, ks.BothMeta{netpol.TypeMeta, netpol.ObjectMeta})
-
-			case corev1.SchemeGroupVersion.WithKind("Service"):
-				var service corev1.Service
-				decode(fileContents, &service)
-				s.services = append(s.services, service)
-				s.bothMetas = append(s.bothMetas, ks.BothMeta{service.TypeMeta, service.ObjectMeta})
-
-			case policyv1beta1.SchemeGroupVersion.WithKind("PodDisruptionBudget"):
-				var disruptBudget policyv1beta1.PodDisruptionBudget
-				decode(fileContents, &disruptBudget)
-				s.podDisruptionBudgets = append(s.podDisruptionBudgets, disruptBudget)
-				s.bothMetas = append(s.bothMetas, ks.BothMeta{disruptBudget.TypeMeta, disruptBudget.ObjectMeta})
-
-			case extensionsv1beta1.SchemeGroupVersion.WithKind("Ingress"):
-				var ingress extensionsv1beta1.Ingress
-				decode(fileContents, &ingress)
-				s.ingresses = append(s.ingresses, ingress)
-				s.bothMetas = append(s.bothMetas, ks.BothMeta{ingress.TypeMeta, ingress.ObjectMeta})
-
-			default:
-				if cnf.VerboseOutput {
-					log.Printf("Unknown datatype: %s", detect.Kind)
-				}
+func detectAndDecode(cnf config.Configuration, s *parsedObjects, raw []byte) error {
+	var detect detectKind
+	err := yaml.Unmarshal(raw, &detect)
+	if err != nil {
+		return err
+	}
+
+	detectedVersion := schema.FromAPIVersionAndKind(detect.ApiVersion, detect.Kind)
+
+	// Parse lists and their items recursively
+	if detectedVersion == corev1.SchemeGroupVersion.WithKind("List") {
+		var list corev1.List
+		decode(raw, &list)
+		for _, listItem := range list.Items {
+			err := detectAndDecode(cnf, s, listItem.Raw)
+			if err != nil {
+				return err
 			}
 		}
+		return nil
 	}
 
-	return s, nil
+	decodeItem(cnf, s, detectedVersion, raw)
+
+	return nil
+}
+
+func decode(data []byte, object runtime.Object) {
+	deserializer := codecs.UniversalDeserializer()
+	if _, _, err := deserializer.Decode(data, nil, object); err != nil {
+		panic(err)
+	}
+}
+
+func decodeItem(cnf config.Configuration, s *parsedObjects, detectedVersion schema.GroupVersionKind, fileContents []byte) {
+	addPodSpeccer := func(ps ks.PodSpecer) {
+		s.podspecers = append(s.podspecers, ps)
+		s.bothMetas = append(s.bothMetas, ks.BothMeta{ps.GetTypeMeta(), ps.GetObjectMeta()})
+	}
+
+	switch detectedVersion {
+	case corev1.SchemeGroupVersion.WithKind("Pod"):
+		var pod corev1.Pod
+		decode(fileContents, &pod)
+		s.pods = append(s.pods, pod)
+		s.bothMetas = append(s.bothMetas, ks.BothMeta{pod.TypeMeta, pod.ObjectMeta})
+
+	case batchv1.SchemeGroupVersion.WithKind("Job"):
+		var job batchv1.Job
+		decode(fileContents, &job)
+		addPodSpeccer(internal.Batchv1Job{job})
+
+	case batchv1beta1.SchemeGroupVersion.WithKind("CronJob"):
+		var cronjob batchv1beta1.CronJob
+		decode(fileContents, &cronjob)
+		addPodSpeccer(internal.Batchv1beta1CronJob{cronjob})
+		s.cronjobs = append(s.cronjobs, cronjob)
+
+	case appsv1.SchemeGroupVersion.WithKind("Deployment"):
+		var deployment appsv1.Deployment
+		decode(fileContents, &deployment)
+		addPodSpeccer(internal.Appsv1Deployment{deployment})
+
+		// TODO: Support older versions of Deployment as well?
+		s.deployments = append(s.deployments, deployment)
+	case appsv1beta1.SchemeGroupVersion.WithKind("Deployment"):
+		var deployment appsv1beta1.Deployment
+		decode(fileContents, &deployment)
+		addPodSpeccer(internal.Appsv1beta1Deployment{deployment})
+	case appsv1beta2.SchemeGroupVersion.WithKind("Deployment"):
+		var deployment appsv1beta2.Deployment
+		decode(fileContents, &deployment)
+		addPodSpeccer(internal.Appsv1beta2Deployment{deployment})
+	case extensionsv1beta1.SchemeGroupVersion.WithKind("Deployment"):
+		var deployment extensionsv1beta1.Deployment
+		decode(fileContents, &deployment)
+		addPodSpeccer(internal.Extensionsv1beta1Deployment{deployment})
+
+	case appsv1.SchemeGroupVersion.WithKind("StatefulSet"):
+		var statefulSet appsv1.StatefulSet
+		decode(fileContents, &statefulSet)
+		addPodSpeccer(internal.Appsv1StatefulSet{statefulSet})
+
+		// TODO: Support older versions of StatefulSet as well?
+		s.statefulsets = append(s.statefulsets, statefulSet)
+	case appsv1beta1.SchemeGroupVersion.WithKind("StatefulSet"):
+		var statefulSet appsv1beta1.StatefulSet
+		decode(fileContents, &statefulSet)
+		addPodSpeccer(internal.Appsv1beta1StatefulSet{statefulSet})
+	case appsv1beta2.SchemeGroupVersion.WithKind("StatefulSet"):
+		var statefulSet appsv1beta2.StatefulSet
+		decode(fileContents, &statefulSet)
+		addPodSpeccer(internal.Appsv1beta2StatefulSet{statefulSet})
+
+	case appsv1.SchemeGroupVersion.WithKind("DaemonSet"):
+		var daemonset appsv1.DaemonSet
+		decode(fileContents, &daemonset)
+		addPodSpeccer(internal.Appsv1DaemonSet{daemonset})
+	case appsv1beta2.SchemeGroupVersion.WithKind("DaemonSet"):
+		var daemonset appsv1beta2.DaemonSet
+		decode(fileContents, &daemonset)
+		addPodSpeccer(internal.Appsv1beta2DaemonSet{daemonset})
+	case extensionsv1beta1.SchemeGroupVersion.WithKind("DaemonSet"):
+		var daemonset extensionsv1beta1.DaemonSet
+		decode(fileContents, &daemonset)
+		addPodSpeccer(internal.Extensionsv1beta1DaemonSet{daemonset})
+
+	case networkingv1.SchemeGroupVersion.WithKind("NetworkPolicy"):
+		var netpol networkingv1.NetworkPolicy
+		decode(fileContents, &netpol)
+		s.networkPolicies = append(s.networkPolicies, netpol)
+		s.bothMetas = append(s.bothMetas, ks.BothMeta{netpol.TypeMeta, netpol.ObjectMeta})
+
+	case corev1.SchemeGroupVersion.WithKind("Service"):
+		var service corev1.Service
+		decode(fileContents, &service)
+		s.services = append(s.services, service)
+		s.bothMetas = append(s.bothMetas, ks.BothMeta{service.TypeMeta, service.ObjectMeta})
+
+	case policyv1beta1.SchemeGroupVersion.WithKind("PodDisruptionBudget"):
+		var disruptBudget policyv1beta1.PodDisruptionBudget
+		decode(fileContents, &disruptBudget)
+		s.podDisruptionBudgets = append(s.podDisruptionBudgets, disruptBudget)
+		s.bothMetas = append(s.bothMetas, ks.BothMeta{disruptBudget.TypeMeta, disruptBudget.ObjectMeta})
+
+	case extensionsv1beta1.SchemeGroupVersion.WithKind("Ingress"):
+		var ingress extensionsv1beta1.Ingress
+		decode(fileContents, &ingress)
+		s.ingresses = append(s.ingresses, ingress)
+		s.bothMetas = append(s.bothMetas, ks.BothMeta{ingress.TypeMeta, ingress.ObjectMeta})
+
+	default:
+		if cnf.VerboseOutput {
+			log.Printf("Unknown datatype: %s", detectedVersion.String())
+		}
+	}
 }


Test patch:
diff --git a/score/score_test.go b/score/score_test.go
index d667597..55a0d33 100644
--- a/score/score_test.go
+++ b/score/score_test.go
@@ -148,3 +148,25 @@ func TestAnnotationIgnore(t *testing.T) {
 		assert.Equal(t, "node-port-service-with-ignore", o.ObjectMeta.Name)
 	}
 }
+
+func TestList(t *testing.T) {
+	s, err := testScore(config.Configuration{}, "list.yaml")
+	assert.Nil(t, err)
+	assert.Len(t, s.Objects, 2)
+
+	hasService := false
+	hasDeployment := false
+
+	for _, obj := range s.Objects {
+		if obj.ObjectMeta.Name == "list-service-test" {
+			hasService = true
+		}
+		if obj.ObjectMeta.Name == "list-deployment-test" {
+			hasDeployment = true
+		}
+		assert.Condition(t, func() bool { return len(obj.Checks) > 2 })
+	}
+
+	assert.True(t, hasService)
+	assert.True(t, hasDeployment)
+}
diff --git a/score/testdata/list.yaml b/score/testdata/list.yaml
new file mode 100644
index 0000000..63547d3
--- /dev/null
+++ b/score/testdata/list.yaml
@@ -0,0 +1,32 @@
+apiVersion: v1
+kind: List
+items:
+  - apiVersion: v1
+    kind: Service
+    metadata:
+      name: list-service-test
+    spec:
+      ports:
+        - protocol: TCP
+          port: 80
+      selector:
+        app: list-deployment-test
+  - apiVersion: apps/v1
+    kind: Deployment
+    metadata:
+      name: list-deployment-test
+      labels:
+        app: list-deployment-test
+    spec:
+      replicas: 1
+      selector:
+        matchLabels:
+          app: list-deployment-test
+      template:
+        metadata:
+          labels:
+            app: list-deployment-test
+        spec:
+          containers:
+            - name: nginx
+              image: nginx