Repository: aquasecurity/starboard
Pull request #430 (2021-03-13 21:11:24)

refactor: Merge resources package with kube package
Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/430?src=pr&el=h1) Report
> Merging [#430](https://codecov.io/gh/aquasecurity/starboard/pull/430?src=pr&el=desc) (dab6064) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/d066379a0462a168a346f4be905d6e2b0c9d0478?el=desc) (d066379) will **decrease** coverage by `37.02%`.
> The diff coverage is `48.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aquasecurity/starboard/pull/430/graphs/tree.svg?width=650&height=150&src=pr&token=qqTGKAJ0Wb)](https://codecov.io/gh/aquasecurity/starboard/pull/430?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##             main     #430       +/-   ##
===========================================
- Coverage   68.15%   31.12%   -37.03%     
===========================================
  Files          57       56        -1     
  Lines        3344     3245       -99     
===========================================
- Hits         2279     1010     -1269     
- Misses        790     2174     +1384     
+ Partials      275       61      -214     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/430?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/operator/controller/configauditreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/430/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvY29uZmlnYXVkaXRyZXBvcnQuZ28=) | `0.00% <0.00%> (-66.83%)` | :arrow_down: |
| [pkg/operator/controller/vulnerabilityreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/430/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvdnVsbmVyYWJpbGl0eXJlcG9ydC5nbw==) | `0.00% <0.00%> (-63.21%)` | :arrow_down: |
| [pkg/plugin/trivy/plugin.go](https://codecov.io/gh/aquasecurity/starboard/pull/430/diff?src=pr&el=tree#diff-cGtnL3BsdWdpbi90cml2eS9wbHVnaW4uZ28=) | `73.77% <0.00%> (-0.70%)` | :arrow_down: |
| [pkg/vulnerabilityreport/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/430/diff?src=pr&el=tree#diff-cGtnL3Z1bG5lcmFiaWxpdHlyZXBvcnQvc2Nhbm5lci5nbw==) | `0.00% <0.00%> (-74.23%)` | :arrow_down: |
| [pkg/kube/object.go](https://codecov.io/gh/aquasecurity/starboard/pull/430/diff?src=pr&el=tree#diff-cGtnL2t1YmUvb2JqZWN0Lmdv) | `44.30% <100.00%> (-8.95%)` | :arrow_down: |
| [pkg/kube/resources.go](https://codecov.io/gh/aquasecurity/starboard/pull/430/diff?src=pr&el=tree#diff-cGtnL2t1YmUvcmVzb3VyY2VzLmdv) | `100.00% <100.00%> (ø)` | |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/430/diff?src=pr&el=tree#diff-cGtnL2NtZC9nZXQuZ28=) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/find.go](https://codecov.io/gh/aquasecurity/starboard/pull/430/diff?src=pr&el=tree#diff-cGtnL2NtZC9maW5kLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/root.go](https://codecov.io/gh/aquasecurity/starboard/pull/430/diff?src=pr&el=tree#diff-cGtnL2NtZC9yb290Lmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/scan.go](https://codecov.io/gh/aquasecurity/starboard/pull/430/diff?src=pr&el=tree#diff-cGtnL2NtZC9zY2FuLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [37 more](https://codecov.io/gh/aquasecurity/starboard/pull/430/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/430?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/430?src=pr&el=footer). Last update [d066379...dab6064](https://codecov.io/gh/aquasecurity/starboard/pull/430?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/Makefile b/Makefile
index 16be3e4..96e3ae4 100644
--- a/Makefile
+++ b/Makefile
@@ -67,9 +67,7 @@ itests-starboard: check-env get-ginkgo
 	-coverprofile=coverage.txt \
 	-coverpkg=github.com/aquasecurity/starboard/pkg/cmd,\
 	github.com/aquasecurity/starboard/pkg/plugin,\
-	github.com/aquasecurity/starboard/pkg/resources,\
 	github.com/aquasecurity/starboard/pkg/kube,\
-	github.com/aquasecurity/starboard/pkg/kube/pod,\
 	github.com/aquasecurity/starboard/pkg/kubebench,\
 	github.com/aquasecurity/starboard/pkg/kubehunter,\
 	github.com/aquasecurity/starboard/pkg/plugin/trivy,\
@@ -89,7 +87,6 @@ itests-starboard-operator: check-env get-ginkgo
 	github.com/aquasecurity/starboard/pkg/operator/predicate,\
 	github.com/aquasecurity/starboard/pkg/operator/controller,\
 	github.com/aquasecurity/starboard/pkg/plugin,\
-	github.com/aquasecurity/starboard/pkg/resources,\
 	github.com/aquasecurity/starboard/pkg/plugin/trivy,\
 	github.com/aquasecurity/starboard/pkg/plugin/polaris,\
 	github.com/aquasecurity/starboard/pkg/configauditreport,\
diff --git a/pkg/kube/doc.go b/pkg/kube/doc.go
new file mode 100644
index 0000000..ff557e4
--- /dev/null
+++ b/pkg/kube/doc.go
@@ -0,0 +1,2 @@
+// The kube package provides primitives for working with Kubernetes objects.
+package kube
diff --git a/pkg/kube/object.go b/pkg/kube/object.go
index 3054622..b70f873 100644
--- a/pkg/kube/object.go
+++ b/pkg/kube/object.go
@@ -116,6 +116,9 @@ func GetPartialObjectFromKindAndNamespacedName(kind Kind, name types.NamespacedN
 	}
 }
 
+// GetPodSpec returns v1.PodSpec from the specified Kubernetes
+// client.Object. Returns error if the given client.Object
+// is not a Kubernetes workload.
 func GetPodSpec(obj client.Object) (corev1.PodSpec, error) {
 	switch t := obj.(type) {
 	case *corev1.Pod:
@@ -132,8 +135,10 @@ func GetPodSpec(obj client.Object) (corev1.PodSpec, error) {
 		return (obj.(*appsv1.DaemonSet)).Spec.Template.Spec, nil
 	case *batchv1beta1.CronJob:
 		return (obj.(*batchv1beta1.CronJob)).Spec.JobTemplate.Spec.Template.Spec, nil
+	case *batchv1.Job:
+		return (obj.(*batchv1.Job)).Spec.Template.Spec, nil
 	default:
-		return corev1.PodSpec{}, fmt.Errorf("unsupported workload %T", t)
+		return corev1.PodSpec{}, fmt.Errorf("unsupported workload: %T", t)
 	}
 }
 
diff --git a/pkg/resources/resources.go b/pkg/kube/resources.go
similarity index 68%
rename from pkg/resources/resources.go
rename to pkg/kube/resources.go
index 8e362c2..49194d0 100644
--- a/pkg/resources/resources.go
+++ b/pkg/kube/resources.go
@@ -1,11 +1,10 @@
-package resources
+package kube
 
 import (
 	"fmt"
 	"hash"
 	"hash/fnv"
 
-	"github.com/aquasecurity/starboard/pkg/kube"
 	"github.com/davecgh/go-spew/spew"
 	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -13,25 +12,31 @@ import (
 	"k8s.io/apimachinery/pkg/util/rand"
 )
 
-func GetContainerImagesFromPodSpec(spec corev1.PodSpec) kube.ContainerImages {
-	images := kube.ContainerImages{}
+// GetContainerImagesFromPodSpec returns a map of container names
+// to container images from the specified v1.PodSpec.
+func GetContainerImagesFromPodSpec(spec corev1.PodSpec) ContainerImages {
+	images := ContainerImages{}
 	for _, container := range spec.Containers {
 		images[container.Name] = container.Image
 	}
 	return images
 }
 
-func GetContainerImagesFromJob(job *batchv1.Job) (kube.ContainerImages, error) {
+// GetContainerImagesFromJob returns a map of container names
+// to container images from the specified v1.Job.
+// The mapping is encoded as JSON value of the AnnotationContainerImages
+// annotation.
+func GetContainerImagesFromJob(job *batchv1.Job) (ContainerImages, error) {
 	var containerImagesAsJSON string
 	var ok bool
 
-	if containerImagesAsJSON, ok = job.Annotations[kube.AnnotationContainerImages]; !ok {
-		return nil, fmt.Errorf("job does not have required annotation: %s", kube.AnnotationContainerImages)
+	if containerImagesAsJSON, ok = job.Annotations[AnnotationContainerImages]; !ok {
+		return nil, fmt.Errorf("required annotation not set: %s", AnnotationContainerImages)
 	}
-	containerImages := kube.ContainerImages{}
+	containerImages := ContainerImages{}
 	err := containerImages.FromJSON(containerImagesAsJSON)
 	if err != nil {
-		return nil, fmt.Errorf("parsing job annotation: %s: %w", kube.AnnotationContainerImages, err)
+		return nil, fmt.Errorf("parsing annotation: %s: %w", AnnotationContainerImages, err)
 	}
 	return containerImages, nil
 }
@@ -51,30 +56,30 @@ func GetContainerImagesFromJob(job *batchv1.Job) (kube.ContainerImages, error) {
 // Pods created and controlled by third party frameworks, such as Argo workflow
 // engine, are considered as unmanaged. Otherwise we'd need to maintain and
 // extend the list of RBAC permissions over time.
-// TODO Merge this method with OwnerResolver, which accepts kube.Object and resolves client.Object.
-func GetImmediateOwnerReference(pod *corev1.Pod) kube.Object {
+// TODO Merge this method with ObjectResolver, which accepts kube.Object and resolves client.Object.
+func GetImmediateOwnerReference(pod *corev1.Pod) Object {
 	ownerRef := metav1.GetControllerOf(pod)
 
 	if ownerRef != nil {
 		switch ownerRef.Kind {
 		case "Pod", "ReplicaSet", "ReplicationController", "Deployment", "StatefulSet", "DaemonSet", "CronJob", "Job":
-			return kube.Object{
+			return Object{
 				Namespace: pod.Namespace,
-				Kind:      kube.Kind(ownerRef.Kind),
+				Kind:      Kind(ownerRef.Kind),
 				Name:      ownerRef.Name,
 			}
 		}
 	}
 
 	// Pod owned by anything else is treated the same as an unmanaged pod
-	return kube.Object{
-		Kind:      kube.KindPod,
+	return Object{
+		Kind:      KindPod,
 		Namespace: pod.Namespace,
 		Name:      pod.Name,
 	}
 }
 
-// ComputeHash returns a hash value calculated from pod spec.
+// ComputeHash returns a hash value calculated from a given object.
 // The hash will be safe encoded to avoid bad words.
 func ComputeHash(obj interface{}) string {
 	podSpecHasher := fnv.New32a()
diff --git a/pkg/operator/controller/configauditreport.go b/pkg/operator/controller/configauditreport.go
index f5964b2..b5ab2a2 100644
--- a/pkg/operator/controller/configauditreport.go
+++ b/pkg/operator/controller/configauditreport.go
@@ -9,7 +9,6 @@ import (
 	"github.com/aquasecurity/starboard/pkg/kube"
 	"github.com/aquasecurity/starboard/pkg/operator/etc"
 	. "github.com/aquasecurity/starboard/pkg/operator/predicate"
-	"github.com/aquasecurity/starboard/pkg/resources"
 	"github.com/go-logr/logr"
 	appsv1 "k8s.io/api/apps/v1"
 	batchv1 "k8s.io/api/batch/v1"
@@ -118,7 +117,7 @@ func (r *ConfigAuditReportReconciler) reconcileWorkload(workloadKind kube.Kind)
 		if err != nil {
 			return ctrl.Result{}, err
 		}
-		podSpecHash := resources.ComputeHash(podSpec)
+		podSpecHash := kube.ComputeHash(podSpec)
 
 		log = log.WithValues("podSpecHash", podSpecHash)
 
@@ -219,7 +218,7 @@ func (r *ConfigAuditReportReconciler) hasActiveScanJob(ctx context.Context, owne
 }
 
 func (r *ConfigAuditReportReconciler) getScanJobName(workload kube.Object) string {
-	return fmt.Sprintf("scan-configauditreport-%s", resources.ComputeHash(workload))
+	return fmt.Sprintf("scan-configauditreport-%s", kube.ComputeHash(workload))
 }
 
 func (r *ConfigAuditReportReconciler) getScanJob(workload kube.Object, obj client.Object, hash string) (*batchv1.Job, []*corev1.Secret, error) {
diff --git a/pkg/operator/controller/vulnerabilityreport.go b/pkg/operator/controller/vulnerabilityreport.go
index 2731e78..3a14886 100644
--- a/pkg/operator/controller/vulnerabilityreport.go
+++ b/pkg/operator/controller/vulnerabilityreport.go
@@ -10,7 +10,6 @@ import (
 	"github.com/aquasecurity/starboard/pkg/kube"
 	"github.com/aquasecurity/starboard/pkg/operator/etc"
 	. "github.com/aquasecurity/starboard/pkg/operator/predicate"
-	"github.com/aquasecurity/starboard/pkg/resources"
 	"github.com/aquasecurity/starboard/pkg/vulnerabilityreport"
 	"github.com/go-logr/logr"
 	batchv1 "k8s.io/api/batch/v1"
@@ -79,9 +78,9 @@ func (r *VulnerabilityReportReconciler) reconcilePods() reconcile.Func {
 			return ctrl.Result{}, fmt.Errorf("getting pod from cache: %w", err)
 		}
 
-		owner := resources.GetImmediateOwnerReference(pod)
-		containerImages := resources.GetContainerImagesFromPodSpec(pod.Spec)
-		hash := resources.ComputeHash(pod.Spec)
+		owner := kube.GetImmediateOwnerReference(pod)
+		containerImages := kube.GetContainerImagesFromPodSpec(pod.Spec)
+		hash := kube.ComputeHash(pod.Spec)
 
 		log.V(1).Info("Resolving workload properties",
 			"owner", owner, "hash", hash, "containerImages", containerImages)
@@ -148,7 +147,7 @@ func (r *VulnerabilityReportReconciler) hasVulnerabilityReports(ctx context.Cont
 }
 
 func (r *VulnerabilityReportReconciler) hasActiveScanJob(ctx context.Context, owner kube.Object, hash string) (bool, *batchv1.Job, error) {
-	jobName := fmt.Sprintf("scan-vulnerabilityreport-%s", resources.ComputeHash(owner))
+	jobName := fmt.Sprintf("scan-vulnerabilityreport-%s", kube.ComputeHash(owner))
 	job := &batchv1.Job{}
 	err := r.Get(ctx, client.ObjectKey{Namespace: r.Config.Namespace, Name: jobName}, job)
 	if err != nil {
@@ -189,7 +188,7 @@ func (r *VulnerabilityReportReconciler) submitScanJob(ctx context.Context, spec
 		}
 	}
 
-	jobName := fmt.Sprintf("scan-vulnerabilityreport-%s", resources.ComputeHash(owner))
+	jobName := fmt.Sprintf("scan-vulnerabilityreport-%s", kube.ComputeHash(owner))
 
 	scanJob := &batchv1.Job{
 		ObjectMeta: metav1.ObjectMeta{
@@ -251,7 +250,7 @@ func (r *VulnerabilityReportReconciler) getCredentials(ctx context.Context, spec
 	if err != nil {
 		return nil, err
 	}
-	return kube.MapContainerNamesToDockerAuths(resources.GetContainerImagesFromPodSpec(spec), imagePullSecrets)
+	return kube.MapContainerNamesToDockerAuths(kube.GetContainerImagesFromPodSpec(spec), imagePullSecrets)
 }
 
 func (r *VulnerabilityReportReconciler) reconcileJobs() reconcile.Func {
@@ -299,7 +298,7 @@ func (r *VulnerabilityReportReconciler) processCompleteScanJob(ctx context.Conte
 		return err
 	}
 
-	containerImages, err := resources.GetContainerImagesFromJob(job)
+	containerImages, err := kube.GetContainerImagesFromJob(job)
 	if err != nil {
 		return fmt.Errorf("getting container images: %w", err)
 	}
diff --git a/pkg/plugin/trivy/plugin.go b/pkg/plugin/trivy/plugin.go
index 7186204..47c8c17 100644
--- a/pkg/plugin/trivy/plugin.go
+++ b/pkg/plugin/trivy/plugin.go
@@ -8,7 +8,6 @@ import (
 	"github.com/aquasecurity/starboard/pkg/docker"
 	"github.com/aquasecurity/starboard/pkg/ext"
 	"github.com/aquasecurity/starboard/pkg/kube"
-	"github.com/aquasecurity/starboard/pkg/resources"
 	"github.com/aquasecurity/starboard/pkg/starboard"
 	"github.com/aquasecurity/starboard/pkg/vulnerabilityreport"
 	corev1 "k8s.io/api/core/v1"
@@ -75,7 +74,7 @@ func (s *scanner) GetScanJobSpec(spec corev1.PodSpec, credentials map[string]doc
 }
 
 func (s *scanner) newSecretWithAggregateImagePullCredentials(spec corev1.PodSpec, credentials map[string]docker.Auth) *corev1.Secret {
-	containerImages := resources.GetContainerImagesFromPodSpec(spec)
+	containerImages := kube.GetContainerImagesFromPodSpec(spec)
 	secretData := kube.AggregateImagePullSecretsData(containerImages, credentials)
 
 	return &corev1.Secret{
diff --git a/pkg/resources/doc.go b/pkg/resources/doc.go
deleted file mode 100644
index a9009e6..0000000
--- a/pkg/resources/doc.go
+++ /dev/null
@@ -1,3 +0,0 @@
-// This package provides primitives for working with Kubernetes objects.
-// TODO We should merge this package with the kube package.
-package resources
diff --git a/pkg/vulnerabilityreport/scanner.go b/pkg/vulnerabilityreport/scanner.go
index 7e1c782..d4f2752 100644
--- a/pkg/vulnerabilityreport/scanner.go
+++ b/pkg/vulnerabilityreport/scanner.go
@@ -8,7 +8,6 @@ import (
 	"github.com/aquasecurity/starboard/pkg/docker"
 	"github.com/aquasecurity/starboard/pkg/ext"
 	"github.com/aquasecurity/starboard/pkg/kube"
-	"github.com/aquasecurity/starboard/pkg/resources"
 	"github.com/aquasecurity/starboard/pkg/runner"
 	"github.com/aquasecurity/starboard/pkg/starboard"
 	batchv1 "k8s.io/api/batch/v1"
@@ -113,7 +112,7 @@ func (s *Scanner) getCredentials(ctx context.Context, spec corev1.PodSpec, ns st
 	if err != nil {
 		return nil, err
 	}
-	return kube.MapContainerNamesToDockerAuths(resources.GetContainerImagesFromPodSpec(spec), imagePullSecrets)
+	return kube.MapContainerNamesToDockerAuths(kube.GetContainerImagesFromPodSpec(spec), imagePullSecrets)
 }
 
 func (s *Scanner) prepareScanJob(workload kube.Object, spec corev1.PodSpec, credentials map[string]docker.Auth) (*batchv1.Job, []*corev1.Secret, error) {
@@ -124,7 +123,7 @@ func (s *Scanner) prepareScanJob(workload kube.Object, spec corev1.PodSpec, cred
 
 	templateSpec.ServiceAccountName = starboard.ServiceAccountName
 
-	containerImagesAsJSON, err := resources.GetContainerImagesFromPodSpec(spec).AsJSON()
+	containerImagesAsJSON, err := kube.GetContainerImagesFromPodSpec(spec).AsJSON()
 	if err != nil {
 		return nil, nil, err
 	}
@@ -168,7 +167,7 @@ func (s *Scanner) prepareScanJob(workload kube.Object, spec corev1.PodSpec, cred
 func (s *Scanner) getVulnerabilityReportsByScanJob(ctx context.Context, job *batchv1.Job, owner metav1.Object) ([]v1alpha1.VulnerabilityReport, error) {
 	var reports []v1alpha1.VulnerabilityReport
 
-	containerImages, err := resources.GetContainerImagesFromJob(job)
+	containerImages, err := kube.GetContainerImagesFromJob(job)
 
 	if err != nil {
 		return nil, fmt.Errorf("getting container images: %w", err)


Test patch:
diff --git a/pkg/kube/object_test.go b/pkg/kube/object_test.go
index a0fcef0..4012889 100644
--- a/pkg/kube/object_test.go
+++ b/pkg/kube/object_test.go
@@ -7,7 +7,13 @@ import (
 	"github.com/aquasecurity/starboard/pkg/kube"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	appsv1 "k8s.io/api/apps/v1"
+	batchv1 "k8s.io/api/batch/v1"
+	batchv1beta1 "k8s.io/api/batch/v1beta1"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
 func TestObjectFromLabelsSet(t *testing.T) {
@@ -86,3 +92,243 @@ func TestContainerImages_AsJSON_And_FromJSON(t *testing.T) {
 	require.NoError(t, err)
 	assert.Equal(t, containerImages, newContainerImages)
 }
+
+func TestGetPartialObjectFromKindAndNamespacedName(t *testing.T) {
+	partial := kube.GetPartialObjectFromKindAndNamespacedName(kube.KindReplicaSet, types.NamespacedName{
+		Namespace: "prod",
+		Name:      "wordpress",
+	})
+	assert.Equal(t, kube.Object{
+		Kind:      kube.KindReplicaSet,
+		Name:      "wordpress",
+		Namespace: "prod",
+	}, partial)
+}
+
+func TestGetPodSpec(t *testing.T) {
+	testCases := []struct {
+		name            string
+		object          client.Object
+		expectedPodSpec corev1.PodSpec
+		expectedError   string
+	}{
+		{
+			name: "Should return PodSpec for Pod",
+			object: &corev1.Pod{
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "pod",
+							Image: "pod:1.3",
+						},
+					},
+				},
+			},
+			expectedPodSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "pod",
+						Image: "pod:1.3",
+					},
+				},
+			},
+		},
+		{
+			name: "Should return PodSpec for Deployment",
+			object: &appsv1.Deployment{
+				Spec: appsv1.DeploymentSpec{
+					Template: corev1.PodTemplateSpec{
+						Spec: corev1.PodSpec{
+							Containers: []corev1.Container{
+								{
+									Name:  "deployment",
+									Image: "deployment:2.4",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedPodSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "deployment",
+						Image: "deployment:2.4",
+					},
+				},
+			},
+		},
+		{
+			name: "Should return PodSpec for ReplicaSet",
+			object: &appsv1.ReplicaSet{
+				Spec: appsv1.ReplicaSetSpec{
+					Template: corev1.PodTemplateSpec{
+						Spec: corev1.PodSpec{
+							Containers: []corev1.Container{
+								{
+									Name:  "replicaset",
+									Image: "replicaset:3.17",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedPodSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "replicaset",
+						Image: "replicaset:3.17",
+					},
+				},
+			},
+		},
+		{
+			name: "Should return PodSpec for ReplicationController",
+			object: &corev1.ReplicationController{
+				Spec: corev1.ReplicationControllerSpec{
+					Template: &corev1.PodTemplateSpec{
+						Spec: corev1.PodSpec{
+							Containers: []corev1.Container{
+								{
+									Name:  "replicationcontroller",
+									Image: "replicationcontroller:latest",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedPodSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "replicationcontroller",
+						Image: "replicationcontroller:latest",
+					},
+				},
+			},
+		},
+		{
+			name: "Should return PodSpec for StatefulSet",
+			object: &appsv1.StatefulSet{
+				Spec: appsv1.StatefulSetSpec{
+					Template: corev1.PodTemplateSpec{
+						Spec: corev1.PodSpec{
+							Containers: []corev1.Container{
+								{
+									Name:  "statefulset",
+									Image: "statefulset:8",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedPodSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "statefulset",
+						Image: "statefulset:8",
+					},
+				},
+			},
+		},
+		{
+			name: "Should return PodSpec for DaemonSet",
+			object: &appsv1.DaemonSet{
+				Spec: appsv1.DaemonSetSpec{
+					Template: corev1.PodTemplateSpec{
+						Spec: corev1.PodSpec{
+							Containers: []corev1.Container{
+								{
+									Name:  "daemonset",
+									Image: "daemonset:1.1",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedPodSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "daemonset",
+						Image: "daemonset:1.1",
+					},
+				},
+			},
+		},
+		{
+			name: "Should return PodSpec for CronJob",
+			object: &batchv1beta1.CronJob{
+				Spec: batchv1beta1.CronJobSpec{
+					JobTemplate: batchv1beta1.JobTemplateSpec{
+						Spec: batchv1.JobSpec{
+							Template: corev1.PodTemplateSpec{
+								Spec: corev1.PodSpec{
+									Containers: []corev1.Container{
+										{
+											Name:  "cronjob",
+											Image: "cronjob:5",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedPodSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "cronjob",
+						Image: "cronjob:5",
+					},
+				},
+			},
+		},
+		{
+			name: "Should return PodSpec for Job",
+			object: &batchv1.Job{
+				Spec: batchv1.JobSpec{
+					Template: corev1.PodTemplateSpec{
+						Spec: corev1.PodSpec{
+							Containers: []corev1.Container{
+								{
+									Name:  "job",
+									Image: "job:2.8.2",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedPodSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "job",
+						Image: "job:2.8.2",
+					},
+				},
+			},
+		},
+		{
+			name:          "Should return error for Service",
+			object:        &corev1.Service{},
+			expectedError: "unsupported workload: *v1.Service",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			podSpec, err := kube.GetPodSpec(tc.object)
+			if tc.expectedError != "" {
+				require.EqualError(t, err, tc.expectedError)
+			} else {
+				require.NoError(t, err)
+				assert.Equal(t, tc.expectedPodSpec, podSpec)
+			}
+
+		})
+	}
+}
diff --git a/pkg/resources/resources_test.go b/pkg/kube/resources_test.go
similarity index 70%
rename from pkg/resources/resources_test.go
rename to pkg/kube/resources_test.go
index 26e0fe1..462a513 100644
--- a/pkg/resources/resources_test.go
+++ b/pkg/kube/resources_test.go
@@ -1,17 +1,71 @@
-package resources_test
+package kube_test
 
 import (
 	"fmt"
 	"testing"
 
 	"github.com/aquasecurity/starboard/pkg/kube"
-	"github.com/aquasecurity/starboard/pkg/resources"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/pointer"
 )
 
+func TestGetContainerImagesFromPodSpec(t *testing.T) {
+	images := kube.GetContainerImagesFromPodSpec(corev1.PodSpec{
+		Containers: []corev1.Container{
+			{
+				Name:  "nginx",
+				Image: "nginx:1.16",
+			},
+			{
+				Name:  "sidecar",
+				Image: "sidecar:1.32.7",
+			},
+		},
+	})
+	assert.Equal(t, kube.ContainerImages{
+		"nginx":   "nginx:1.16",
+		"sidecar": "sidecar:1.32.7",
+	}, images)
+}
+
+func TestGetContainerImagesFromJob(t *testing.T) {
+
+	t.Run("Should return error when annotation is not set", func(t *testing.T) {
+		_, err := kube.GetContainerImagesFromJob(&batchv1.Job{})
+		require.EqualError(t, err, "required annotation not set: starboard.container-images")
+	})
+
+	t.Run("Should return error when annotation is set but has invalid value", func(t *testing.T) {
+		_, err := kube.GetContainerImagesFromJob(&batchv1.Job{
+			ObjectMeta: metav1.ObjectMeta{
+				Annotations: map[string]string{
+					"starboard.container-images": ``,
+				},
+			},
+		})
+		require.EqualError(t, err, "parsing annotation: starboard.container-images: unexpected end of JSON input")
+	})
+
+	t.Run("Should return ContainerImages when annotation is set", func(t *testing.T) {
+		images, err := kube.GetContainerImagesFromJob(&batchv1.Job{
+			ObjectMeta: metav1.ObjectMeta{
+				Annotations: map[string]string{
+					"starboard.container-images": `{"nginx":"nginx:1.16","sidecar":"sidecar:1.32.7"}`,
+				},
+			},
+		})
+		require.NoError(t, err)
+		assert.Equal(t, kube.ContainerImages{
+			"nginx":   "nginx:1.16",
+			"sidecar": "sidecar:1.32.7",
+		}, images)
+	})
+}
+
 func TestGetImmediateOwnerReference(t *testing.T) {
 
 	testCases := []struct {
@@ -103,7 +157,7 @@ func TestGetImmediateOwnerReference(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			owner := resources.GetImmediateOwnerReference(tc.pod)
+			owner := kube.GetImmediateOwnerReference(tc.pod)
 			assert.Equal(t, tc.expectedOwner, owner)
 		})
 	}
@@ -146,7 +200,7 @@ func TestComputeHash(t *testing.T) {
 
 		assert.NotSame(t, booleanValue1Ptr, booleanValue2Ptr)
 		assert.Equal(t, booleanValue1, booleanValue2)
-		assert.Equal(t, resources.ComputeHash(spec1), resources.ComputeHash(spec2))
+		assert.Equal(t, kube.ComputeHash(spec1), kube.ComputeHash(spec2))
 	})
 
 	t.Run("Should return different hash when pointers point to different values", func(t *testing.T) {
@@ -176,13 +230,13 @@ func TestComputeHash(t *testing.T) {
 
 		assert.NotSame(t, booleanValue1Ptr, booleanValue3Ptr)
 		assert.NotEqual(t, booleanValue1, booleanValue3)
-		assert.Equal(t, resources.ComputeHash(spec1), resources.ComputeHash(spec2))
+		assert.Equal(t, kube.ComputeHash(spec1), kube.ComputeHash(spec2))
 	})
 
 	t.Run("Should return unique hashes", func(t *testing.T) {
 		hashes := make(map[string]bool)
 		for tag := 0; tag < 100; tag++ {
-			hash := resources.ComputeHash(corev1.PodSpec{
+			hash := kube.ComputeHash(corev1.PodSpec{
 				Containers: []corev1.Container{
 					{
 						Name:  "nginx",