Repository: aquasecurity/starboard
Pull request #71 (2020-06-24 09:51:33)

refactor: Introduce kube.Object as more generic representation of kube.Workload
Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/71?src=pr&el=h1) Report
> Merging [#71](https://codecov.io/gh/aquasecurity/starboard/pull/71?src=pr&el=desc) into [master](https://codecov.io/gh/aquasecurity/starboard/commit/8474833bd94ccde8038d3f4e00bc3222211bcd4f&el=desc) will **not change** coverage.
> The diff coverage is `33.33%`.

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

```diff
@@           Coverage Diff           @@
##           master      #71   +/-   ##
=======================================
  Coverage   11.05%   11.05%           
=======================================
  Files          23       23           
  Lines         859      859           
=======================================
  Hits           95       95           
  Misses        759      759           
  Partials        5        5           
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/71?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/find/vulnerabilities/trivy/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/71/diff?src=pr&el=tree#diff-cGtnL2ZpbmQvdnVsbmVyYWJpbGl0aWVzL3RyaXZ5L3NjYW5uZXIuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/cmd/commands.go](https://codecov.io/gh/aquasecurity/starboard/pull/71/diff?src=pr&el=tree#diff-cGtnL2NtZC9jb21tYW5kcy5nbw==) | `56.41% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/71?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/71?src=pr&el=footer). Last update [8474833...ed49b4f](https://codecov.io/gh/aquasecurity/starboard/pull/71?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/pkg/cmd/commands.go b/pkg/cmd/commands.go
index d0e517e..e098cd5 100644
--- a/pkg/cmd/commands.go
+++ b/pkg/cmd/commands.go
@@ -17,7 +17,7 @@ func SetGlobalFlags(cf *genericclioptions.ConfigFlags, cmd *cobra.Command) {
 	}
 }
 
-func WorkloadFromArgs(namespace string, args []string) (workload kube.Workload, err error) {
+func WorkloadFromArgs(namespace string, args []string) (workload kube.Object, err error) {
 	if len(args) < 1 {
 		err = errors.New("required workload kind and name not specified")
 		return
@@ -25,14 +25,14 @@ func WorkloadFromArgs(namespace string, args []string) (workload kube.Workload,
 
 	parts := strings.SplitN(args[0], "/", 2)
 	if len(parts) == 1 {
-		workload = kube.Workload{
+		workload = kube.Object{
 			Namespace: namespace,
-			Kind:      kube.WorkloadKindPod,
+			Kind:      kube.KindPod,
 			Name:      parts[0],
 		}
 		return
 	}
-	kind, err := kube.WorkloadKindFromString(parts[0])
+	kind, err := kube.KindFromResource(parts[0])
 	if err != nil {
 		return
 	}
@@ -40,7 +40,7 @@ func WorkloadFromArgs(namespace string, args []string) (workload kube.Workload,
 		err = errors.New("required workload name is blank")
 		return
 	}
-	workload = kube.Workload{
+	workload = kube.Object{
 		Namespace: namespace,
 		Kind:      kind,
 		Name:      parts[1],
diff --git a/pkg/find/vulnerabilities/crd/writer.go b/pkg/find/vulnerabilities/crd/writer.go
index cf195bb..0a117fc 100644
--- a/pkg/find/vulnerabilities/crd/writer.go
+++ b/pkg/find/vulnerabilities/crd/writer.go
@@ -23,7 +23,7 @@ func NewWriter(client clientset.Interface) vulnerabilities.Writer {
 	}
 }
 
-func (s *writer) Write(ctx context.Context, workload kube.Workload, reports map[string]starboard.VulnerabilityReport) (err error) {
+func (s *writer) Write(ctx context.Context, workload kube.Object, reports map[string]starboard.VulnerabilityReport) (err error) {
 	for container, report := range reports {
 		err = s.createVulnerability(ctx, workload, container, report)
 		if err != nil {
@@ -33,14 +33,15 @@ func (s *writer) Write(ctx context.Context, workload kube.Workload, reports map[
 	return
 }
 
-func (s *writer) createVulnerability(ctx context.Context, workload kube.Workload, container string, report starboard.VulnerabilityReport) (err error) {
+func (s *writer) createVulnerability(ctx context.Context, workload kube.Object, container string, report starboard.VulnerabilityReport) (err error) {
 	_, err = s.client.AquasecurityV1alpha1().Vulnerabilities(workload.Namespace).Create(ctx, &starboard.Vulnerability{
 		ObjectMeta: meta.ObjectMeta{
 			Name: fmt.Sprintf(uuid.New().String()),
 			Labels: map[string]string{
-				kube.LabelResourceKind:  workload.Kind.String(),
-				kube.LabelResourceName:  workload.Name,
-				kube.LabelContainerName: container,
+				kube.LabelResourceKind:      string(workload.Kind),
+				kube.LabelResourceName:      workload.Name,
+				kube.LabelResourceNamespace: workload.Namespace,
+				kube.LabelContainerName:     container,
 			},
 		},
 		Report: report,
diff --git a/pkg/find/vulnerabilities/scanner.go b/pkg/find/vulnerabilities/scanner.go
index 82cc67a..4217f48 100644
--- a/pkg/find/vulnerabilities/scanner.go
+++ b/pkg/find/vulnerabilities/scanner.go
@@ -16,6 +16,6 @@ import (
 // ScanByPodSpec scans all container images of the specified Kubernetes workload with the given PodSpec.
 // Returns a map of container names to VulnerabilityReports.
 type Scanner interface {
-	Scan(ctx context.Context, workload kube.Workload) (reports map[string]starboard.VulnerabilityReport, err error)
-	ScanByPodSpec(ctx context.Context, workload kube.Workload, spec core.PodSpec) (reports map[string]starboard.VulnerabilityReport, err error)
+	Scan(ctx context.Context, workload kube.Object) (reports map[string]starboard.VulnerabilityReport, err error)
+	ScanByPodSpec(ctx context.Context, workload kube.Object, spec core.PodSpec) (reports map[string]starboard.VulnerabilityReport, err error)
 }
diff --git a/pkg/find/vulnerabilities/trivy/scanner.go b/pkg/find/vulnerabilities/trivy/scanner.go
index f2083d3..88b1d53 100644
--- a/pkg/find/vulnerabilities/trivy/scanner.go
+++ b/pkg/find/vulnerabilities/trivy/scanner.go
@@ -51,7 +51,7 @@ type scanner struct {
 	scanners.Base
 }
 
-func (s *scanner) Scan(ctx context.Context, workload kube.Workload) (reports map[string]sec.VulnerabilityReport, err error) {
+func (s *scanner) Scan(ctx context.Context, workload kube.Object) (reports map[string]sec.VulnerabilityReport, err error) {
 	klog.V(3).Infof("Getting Pod template for workload: %v", workload)
 	podSpec, err := s.pods.GetPodSpecByWorkload(ctx, workload)
 	if err != nil {
@@ -66,7 +66,7 @@ func (s *scanner) Scan(ctx context.Context, workload kube.Workload) (reports map
 	return
 }
 
-func (s *scanner) ScanByPodSpec(ctx context.Context, workload kube.Workload, spec core.PodSpec) (map[string]sec.VulnerabilityReport, error) {
+func (s *scanner) ScanByPodSpec(ctx context.Context, workload kube.Object, spec core.PodSpec) (map[string]sec.VulnerabilityReport, error) {
 	klog.V(3).Infof("Scanning with options: %+v", s.opts)
 	job, err := s.prepareJob(ctx, workload, spec)
 	if err != nil {
@@ -101,7 +101,7 @@ func (s *scanner) ScanByPodSpec(ctx context.Context, workload kube.Workload, spe
 	return s.getScanReportsFor(ctx, job)
 }
 
-func (s *scanner) prepareJob(ctx context.Context, workload kube.Workload, spec core.PodSpec) (*batch.Job, error) {
+func (s *scanner) prepareJob(ctx context.Context, workload kube.Object, spec core.PodSpec) (*batch.Job, error) {
 	credentials, err := s.secrets.GetImagesWithCredentials(ctx, workload.Namespace, spec)
 	if err != nil {
 		return nil, fmt.Errorf("getting docker configs: %w", err)
@@ -179,8 +179,9 @@ func (s *scanner) prepareJob(ctx context.Context, workload kube.Workload, spec c
 			Name:      jobName,
 			Namespace: kube.NamespaceStarboard,
 			Labels: map[string]string{
-				kube.LabelResourceKind: workload.Kind.String(),
-				kube.LabelResourceName: workload.Name,
+				kube.LabelResourceKind:      string(workload.Kind),
+				kube.LabelResourceName:      workload.Name,
+				kube.LabelResourceNamespace: workload.Namespace,
 			},
 		},
 		Spec: batch.JobSpec{
@@ -190,7 +191,7 @@ func (s *scanner) prepareJob(ctx context.Context, workload kube.Workload, spec c
 			Template: core.PodTemplateSpec{
 				ObjectMeta: meta.ObjectMeta{
 					Labels: map[string]string{
-						kube.LabelResourceKind: workload.Kind.String(),
+						kube.LabelResourceKind: string(workload.Kind),
 						kube.LabelResourceName: workload.Name,
 					},
 				},
diff --git a/pkg/find/vulnerabilities/writer.go b/pkg/find/vulnerabilities/writer.go
index cc88683..2e6d1fe 100644
--- a/pkg/find/vulnerabilities/writer.go
+++ b/pkg/find/vulnerabilities/writer.go
@@ -8,5 +8,5 @@ import (
 )
 
 type Writer interface {
-	Write(ctx context.Context, workload kube.Workload, reports map[string]starboard.VulnerabilityReport) error
+	Write(ctx context.Context, workload kube.Object, reports map[string]starboard.VulnerabilityReport) error
 }
diff --git a/pkg/kube/object.go b/pkg/kube/object.go
new file mode 100644
index 0000000..3906fb8
--- /dev/null
+++ b/pkg/kube/object.go
@@ -0,0 +1,70 @@
+package kube
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/labels"
+)
+
+// Object is a simplified representation a Kubernetes object.
+// Each object has kind, which designates the type of the entity it represents.
+// Objects have names and many of them live in namespaces.
+type Object struct {
+	Kind      Kind
+	Name      string
+	Namespace string
+}
+
+// Kind represents the type of a Kubernetes Object.
+type Kind string
+
+const (
+	KindUnknown Kind = "Unknown"
+
+	KindNode Kind = "Node"
+
+	KindPod                   Kind = "Pod"
+	KindReplicaSet            Kind = "ReplicaSet"
+	KindReplicationController Kind = "ReplicationController"
+	KindDeployment            Kind = "Deployment"
+	KindStatefulSet           Kind = "StatefulSet"
+	KindDaemonSet             Kind = "DaemonSet"
+	KindCronJob               Kind = "CronJob"
+	KindJob                   Kind = "Job"
+)
+
+func ObjectFromLabelsSet(set labels.Set) (Object, error) {
+	if !set.Has(LabelResourceKind) {
+		return Object{}, fmt.Errorf("required label does not exist: %s", LabelResourceKind)
+	}
+	if !set.Has(LabelResourceName) {
+		return Object{}, fmt.Errorf("required label does not exist: %s", LabelResourceName)
+	}
+	return Object{
+		Kind:      Kind(set.Get(LabelResourceKind)),
+		Name:      set.Get(LabelResourceName),
+		Namespace: set.Get(LabelResourceNamespace),
+	}, nil
+}
+
+func KindFromResource(resource string) (Kind, error) {
+	switch resource {
+	case "pods", "pod", "po":
+		return KindPod, nil
+	case "replicasets.apps", "replicasets", "replicaset", "rs":
+		return KindReplicaSet, nil
+	case "replicationcontrollers", "replicationcontroller", "rc":
+		return KindReplicationController, nil
+	case "deployments.apps", "deployments", "deployment", "deploy":
+		return KindDeployment, nil
+	case "statefulsets.apps", "statefulsets", "statefulset", "sts":
+		return KindStatefulSet, nil
+	case "daemonsets.apps", "daemonsets", "daemonset", "ds":
+		return KindDaemonSet, nil
+	case "cronjobs.batch", "cronjob.batch", "cronjobs", "cronjob", "cj":
+		return KindCronJob, nil
+	case "jobs.batch", "job.batch", "jobs", "job":
+		return KindJob, nil
+	}
+	return KindUnknown, fmt.Errorf("unrecognized resource: %s", resource)
+}
diff --git a/pkg/kube/pod/manager.go b/pkg/kube/pod/manager.go
index 2e7806c..27b7e5a 100644
--- a/pkg/kube/pod/manager.go
+++ b/pkg/kube/pod/manager.go
@@ -27,10 +27,10 @@ func NewPodManager(clientset kubernetes.Interface) *Manager {
 }
 
 // GetPodSpecByWorkload returns a PodSpec of the specified Workload.
-func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Workload) (spec core.PodSpec, err error) {
+func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Object) (spec core.PodSpec, err error) {
 	ns := workload.Namespace
 	switch workload.Kind {
-	case kube.WorkloadKindPod:
+	case kube.KindPod:
 		var pod *core.Pod
 		pod, err = pw.GetPodByName(ctx, ns, workload.Name)
 		if err != nil {
@@ -38,7 +38,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Workl
 		}
 		spec = pod.Spec
 		return
-	case kube.WorkloadKindReplicaSet:
+	case kube.KindReplicaSet:
 		var rs *apps.ReplicaSet
 		rs, err = pw.clientset.AppsV1().ReplicaSets(ns).Get(ctx, workload.Name, meta.GetOptions{})
 		if err != nil {
@@ -46,7 +46,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Workl
 		}
 		spec = rs.Spec.Template.Spec
 		return
-	case kube.WorkloadKindReplicationController:
+	case kube.KindReplicationController:
 		var rc *core.ReplicationController
 		rc, err = pw.clientset.CoreV1().ReplicationControllers(ns).Get(ctx, workload.Name, meta.GetOptions{})
 		if err != nil {
@@ -54,7 +54,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Workl
 		}
 		spec = rc.Spec.Template.Spec
 		return
-	case kube.WorkloadKindDeployment:
+	case kube.KindDeployment:
 		var deploy *apps.Deployment
 		deploy, err = pw.clientset.AppsV1().Deployments(ns).Get(ctx, workload.Name, meta.GetOptions{})
 		if err != nil {
@@ -62,7 +62,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Workl
 		}
 		spec = deploy.Spec.Template.Spec
 		return
-	case kube.WorkloadKindStatefulSet:
+	case kube.KindStatefulSet:
 		var sts *apps.StatefulSet
 		sts, err = pw.clientset.AppsV1().StatefulSets(ns).Get(ctx, workload.Name, meta.GetOptions{})
 		if err != nil {
@@ -70,7 +70,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Workl
 		}
 		spec = sts.Spec.Template.Spec
 		return
-	case kube.WorkloadKindDaemonSet:
+	case kube.KindDaemonSet:
 		var ds *apps.DaemonSet
 		ds, err = pw.clientset.AppsV1().DaemonSets(ns).Get(ctx, workload.Name, meta.GetOptions{})
 		if err != nil {
@@ -78,7 +78,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Workl
 		}
 		spec = ds.Spec.Template.Spec
 		return
-	case kube.WorkloadKindCronJob:
+	case kube.KindCronJob:
 		var cj *batchv1beta1.CronJob
 		cj, err = pw.clientset.BatchV1beta1().CronJobs(ns).Get(ctx, workload.Name, meta.GetOptions{})
 		if err != nil {
@@ -86,7 +86,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Workl
 		}
 		spec = cj.Spec.JobTemplate.Spec.Template.Spec
 		return
-	case kube.WorkloadKindJob:
+	case kube.KindJob:
 		var job *batch.Job
 		job, err = pw.clientset.BatchV1().Jobs(ns).Get(ctx, workload.Name, meta.GetOptions{})
 		if err != nil {
diff --git a/pkg/kube/starboard.go b/pkg/kube/starboard.go
new file mode 100644
index 0000000..12a2c01
--- /dev/null
+++ b/pkg/kube/starboard.go
@@ -0,0 +1,38 @@
+package kube
+
+import (
+	"time"
+)
+
+const (
+	NamespaceStarboard = "starboard"
+	// ServiceAccountPolaris the name of the ServiceAccount used to run Polaris scan Jobs.
+	ServiceAccountPolaris = "polaris"
+	ConfigMapPolaris      = "polaris"
+)
+
+const (
+	// TODO I'm wondering if we should rename starboard.resource.* labels to starboard.object.*
+	// TODO In Kubernetes API terminology a resource is usually lowercase, plural word (e.g. pods) identifying a set of
+	// TODO HTTP endpoints (paths) exposing the CRUD semantics of a certain object type in the system
+	LabelResourceKind      = "starboard.resource.kind"
+	LabelResourceName      = "starboard.resource.name"
+	LabelResourceNamespace = "starboard.resource.namespace"
+
+	LabelContainerName = "starboard.container.name"
+
+	LabelScannerName   = "starboard.scanner.name"
+	LabelScannerVendor = "starboard.scanner.vendor"
+
+	LabelHistoryLatest = "starboard.history.latest"
+)
+
+const (
+	AnnotationHistoryLimit = "starboard.history.limit"
+)
+
+// ScannerOpts holds configuration of the vulnerability Scanner.
+type ScannerOpts struct {
+	ScanJobTimeout time.Duration
+	DeleteScanJob  bool
+}
diff --git a/pkg/kube/workload.go b/pkg/kube/workload.go
deleted file mode 100644
index f6bc36c..0000000
--- a/pkg/kube/workload.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package kube
-
-import (
-	"fmt"
-	"time"
-)
-
-const (
-	NamespaceStarboard = "starboard"
-	// ServiceAccountPolaris the name of the ServiceAccount used to run Polaris scan Jobs.
-	ServiceAccountPolaris = "polaris"
-	ConfigMapPolaris      = "polaris"
-)
-
-const (
-	LabelResourceKind  = "starboard.resource.kind"
-	LabelResourceName  = "starboard.resource.name"
-	LabelContainerName = "starboard.container.name"
-
-	LabelScannerName   = "starboard.scanner.name"
-	LabelScannerVendor = "starboard.scanner.vendor"
-
-	LabelHistoryLatest = "starboard.history.latest"
-)
-
-const (
-	AnnotationHistoryLimit = "starboard.history.limit"
-)
-
-// WorkloadKind is an enum defining the different kinds of Kubernetes workloads.
-type WorkloadKind int
-
-const (
-	WorkloadKindUnknown WorkloadKind = iota
-	WorkloadKindPod
-	WorkloadKindReplicaSet
-	WorkloadKindReplicationController
-	WorkloadKindDeployment
-	WorkloadKindStatefulSet
-	WorkloadKindDaemonSet
-	WorkloadKindCronJob
-	WorkloadKindJob
-)
-
-var workloadKindToString = map[WorkloadKind]string{
-	WorkloadKindUnknown:               "Unknown",
-	WorkloadKindPod:                   "Pod",
-	WorkloadKindReplicaSet:            "ReplicaSet",
-	WorkloadKindReplicationController: "ReplicationController",
-	WorkloadKindDeployment:            "Deployment",
-	WorkloadKindStatefulSet:           "StatefulSet",
-	WorkloadKindDaemonSet:             "DaemonSet",
-	WorkloadKindCronJob:               "CronJob",
-	WorkloadKindJob:                   "Job",
-}
-
-func (t WorkloadKind) String() string {
-	if s, exists := workloadKindToString[t]; exists {
-		return s
-	}
-	return "Unknown"
-}
-
-type Workload struct {
-	Namespace string
-	Kind      WorkloadKind
-	Name      string
-}
-
-func (t Workload) String() string {
-	return fmt.Sprintf("%s/%s", t.Kind, t.Name)
-}
-
-func WorkloadKindFromString(s string) (WorkloadKind, error) {
-	switch s {
-	case "pods", "pod", "po":
-		return WorkloadKindPod, nil
-	case "replicasets.apps", "replicasets", "replicaset", "rs":
-		return WorkloadKindReplicaSet, nil
-	case "replicationcontrollers", "replicationcontroller", "rc":
-		return WorkloadKindReplicationController, nil
-	case "deployments.apps", "deployments", "deployment", "deploy":
-		return WorkloadKindDeployment, nil
-	case "statefulsets.apps", "statefulsets", "statefulset", "sts":
-		return WorkloadKindStatefulSet, nil
-	case "daemonsets.apps", "daemonsets", "daemonset", "ds":
-		return WorkloadKindDaemonSet, nil
-	case "cronjobs.batch", "cronjob.batch", "cronjobs", "cronjob", "cj":
-		return WorkloadKindCronJob, nil
-	case "jobs.batch", "job.batch", "jobs", "job":
-		return WorkloadKindJob, nil
-	}
-	return WorkloadKindUnknown, fmt.Errorf("unrecognized workload: %s", s)
-}
-
-// ScannerOpts holds configuration of the vulnerability Scanner.
-type ScannerOpts struct {
-	ScanJobTimeout time.Duration
-	DeleteScanJob  bool
-}


Test patch:
diff --git a/pkg/cmd/root_test.go b/pkg/cmd/root_test.go
index deab3cd..18a88c6 100644
--- a/pkg/cmd/root_test.go
+++ b/pkg/cmd/root_test.go
@@ -15,180 +15,180 @@ func TestWorkloadFromArgs(t *testing.T) {
 
 		givenArgs []string
 
-		expectedWorkload kube.Workload
+		expectedWorkload kube.Object
 		expectedError    error
 	}{
 		{
 			name:             "Should return Pod/my-pod when kind is not explicitly specified",
 			givenArgs:        []string{"my-pod"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindPod, Name: "my-pod"},
+			expectedWorkload: kube.Object{Kind: kube.KindPod, Name: "my-pod"},
 		},
 		{
 			name:             "Should return Pod/my-pod when kind is specified as pods",
 			givenArgs:        []string{"pods/my-pod"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindPod, Name: "my-pod"},
+			expectedWorkload: kube.Object{Kind: kube.KindPod, Name: "my-pod"},
 		},
 		{
 			name:             "Should return Pod/my-pod when kind is specified as pod",
 			givenArgs:        []string{"pod/my-pod"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindPod, Name: "my-pod"},
+			expectedWorkload: kube.Object{Kind: kube.KindPod, Name: "my-pod"},
 		},
 		{
 			name:             "Should return Pod/my-pod when kind is specified as po",
 			givenArgs:        []string{"po/my-pod"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindPod, Name: "my-pod"},
+			expectedWorkload: kube.Object{Kind: kube.KindPod, Name: "my-pod"},
 		},
 		{
 			name:             "Should return ReplicaSet/my-rs when kind is specified as replicasets.apps",
 			givenArgs:        []string{"replicasets.apps/my-rs"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindReplicaSet, Name: "my-rs"},
+			expectedWorkload: kube.Object{Kind: kube.KindReplicaSet, Name: "my-rs"},
 		},
 		{
 			name:             "Should return ReplicaSet/my-rs when kind is specified as replicasets",
 			givenArgs:        []string{"replicasets/my-rs"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindReplicaSet, Name: "my-rs"},
+			expectedWorkload: kube.Object{Kind: kube.KindReplicaSet, Name: "my-rs"},
 		},
 		{
 			name:             "Should return ReplicaSet/my-rs when kind is specified as replicaset",
 			givenArgs:        []string{"replicaset/my-rs"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindReplicaSet, Name: "my-rs"},
+			expectedWorkload: kube.Object{Kind: kube.KindReplicaSet, Name: "my-rs"},
 		},
 		{
 			name:             "Should return ReplicaSet/my-rs when kind is specified as rs",
 			givenArgs:        []string{"rs/my-rs"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindReplicaSet, Name: "my-rs"},
+			expectedWorkload: kube.Object{Kind: kube.KindReplicaSet, Name: "my-rs"},
 		},
 		{
 			name:             "Should return ReplicationController/my-rc when kind is specified as replicationcontrollers",
 			givenArgs:        []string{"replicationcontrollers/my-rc"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindReplicationController, Name: "my-rc"},
+			expectedWorkload: kube.Object{Kind: kube.KindReplicationController, Name: "my-rc"},
 		},
 		{
 			name:             "Should return ReplicationController/my-rc when kind is specified as replicationcontroller",
 			givenArgs:        []string{"replicationcontroller/my-rc"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindReplicationController, Name: "my-rc"},
+			expectedWorkload: kube.Object{Kind: kube.KindReplicationController, Name: "my-rc"},
 		},
 		{
 			name:             "Should return ReplicationController/my-rc when kind is specified as rc",
 			givenArgs:        []string{"rc/my-rc"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindReplicationController, Name: "my-rc"},
+			expectedWorkload: kube.Object{Kind: kube.KindReplicationController, Name: "my-rc"},
 		},
 		{
 			name:             "Should return Deployment/my-deployment when kind is specified as deployments.apps",
 			givenArgs:        []string{"deploy/my-deployment"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindDeployment, Name: "my-deployment"},
+			expectedWorkload: kube.Object{Kind: kube.KindDeployment, Name: "my-deployment"},
 		},
 		{
 			name:             "Should return Deployment/my-deployment when kind is specified as deployments",
 			givenArgs:        []string{"deployments/my-deployment"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindDeployment, Name: "my-deployment"},
+			expectedWorkload: kube.Object{Kind: kube.KindDeployment, Name: "my-deployment"},
 		},
 		{
 			name:             "Should return Deployment/my-deployment when kind is specified as deployment",
 			givenArgs:        []string{"deployment/my-deployment"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindDeployment, Name: "my-deployment"},
+			expectedWorkload: kube.Object{Kind: kube.KindDeployment, Name: "my-deployment"},
 		},
 		{
 			name:             "Should return Deployment/my-deployment when kind is specified as deploy",
 			givenArgs:        []string{"deploy/my-deployment"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindDeployment, Name: "my-deployment"},
+			expectedWorkload: kube.Object{Kind: kube.KindDeployment, Name: "my-deployment"},
 		},
 		{
 			name:             "Should return DaemonSet/my-ds when kind is specified as daemonsets.apps",
 			givenArgs:        []string{"daemonsets/my-ds"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindDaemonSet, Name: "my-ds"},
+			expectedWorkload: kube.Object{Kind: kube.KindDaemonSet, Name: "my-ds"},
 		},
 		{
 			name:             "Should return DaemonSet/my-ds when kind is specified as daemonsets",
 			givenArgs:        []string{"daemonsets/my-ds"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindDaemonSet, Name: "my-ds"},
+			expectedWorkload: kube.Object{Kind: kube.KindDaemonSet, Name: "my-ds"},
 		},
 		{
 			name:             "Should return DaemonSet/my-ds when kind is specified as daemonset",
 			givenArgs:        []string{"daemonsets/my-ds"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindDaemonSet, Name: "my-ds"},
+			expectedWorkload: kube.Object{Kind: kube.KindDaemonSet, Name: "my-ds"},
 		},
 		{
 			name:             "Should return DaemonSet/my-ds when kind is specified as ds",
 			givenArgs:        []string{"daemonsets/my-ds"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindDaemonSet, Name: "my-ds"},
+			expectedWorkload: kube.Object{Kind: kube.KindDaemonSet, Name: "my-ds"},
 		},
 		{
 			name:             "Should return StatefulSet/my-sts when kind is specified as statefulsets.apps",
 			givenArgs:        []string{"statefulsets.apps/my-sts"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindStatefulSet, Name: "my-sts"},
+			expectedWorkload: kube.Object{Kind: kube.KindStatefulSet, Name: "my-sts"},
 		},
 		{
 			name:             "Should return StatefulSet/my-sts when kind is specified as statefulsets",
 			givenArgs:        []string{"statefulsets/my-sts"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindStatefulSet, Name: "my-sts"},
+			expectedWorkload: kube.Object{Kind: kube.KindStatefulSet, Name: "my-sts"},
 		},
 		{
 			name:             "Should return StatefulSet/my-sts when kind is specified as statefulset",
 			givenArgs:        []string{"statefulset/my-sts"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindStatefulSet, Name: "my-sts"},
+			expectedWorkload: kube.Object{Kind: kube.KindStatefulSet, Name: "my-sts"},
 		},
 		{
 			name:             "Should return StatefulSet/my-sts when kind is specified as sts",
 			givenArgs:        []string{"sts/my-sts"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindStatefulSet, Name: "my-sts"},
+			expectedWorkload: kube.Object{Kind: kube.KindStatefulSet, Name: "my-sts"},
 		},
 		{
 			name:             "Should return CronJob/my-cj when kind is specified as cronjobs.batch",
 			givenArgs:        []string{"cronjobs.batch/my-cj"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindCronJob, Name: "my-cj"},
+			expectedWorkload: kube.Object{Kind: kube.KindCronJob, Name: "my-cj"},
 		},
 		{
 			name:             "Should return CronJob/my-cj when kind is specified as cronjob.batch",
 			givenArgs:        []string{"cronjob.batch/my-cj"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindCronJob, Name: "my-cj"},
+			expectedWorkload: kube.Object{Kind: kube.KindCronJob, Name: "my-cj"},
 		},
 		{
 			name:             "Should return CronJob/my-cj when kind is specified as cronjobs",
 			givenArgs:        []string{"cronjobs/my-cj"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindCronJob, Name: "my-cj"},
+			expectedWorkload: kube.Object{Kind: kube.KindCronJob, Name: "my-cj"},
 		},
 		{
 			name:             "Should return CronJob/my-cj when kind is specified as cronjob",
 			givenArgs:        []string{"cronjob/my-cj"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindCronJob, Name: "my-cj"},
+			expectedWorkload: kube.Object{Kind: kube.KindCronJob, Name: "my-cj"},
 		},
 		{
 			name:             "Should return CronJob/my-cj when kind is specified as cj",
 			givenArgs:        []string{"cj/my-cj"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindCronJob, Name: "my-cj"},
+			expectedWorkload: kube.Object{Kind: kube.KindCronJob, Name: "my-cj"},
 		},
 		{
 			name:             "Should return Job/my-job when kind is specified as jobs.batch",
 			givenArgs:        []string{"jobs.batch/my-job"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindJob, Name: "my-job"},
+			expectedWorkload: kube.Object{Kind: kube.KindJob, Name: "my-job"},
 		},
 		{
 			name:             "Should return Job/my-job when kind is specified as job.batch",
 			givenArgs:        []string{"job.batch/my-job"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindJob, Name: "my-job"},
+			expectedWorkload: kube.Object{Kind: kube.KindJob, Name: "my-job"},
 		},
 		{
 			name:             "Should return Job/my-job when kind is specified as jobs",
 			givenArgs:        []string{"jobs/my-job"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindJob, Name: "my-job"},
+			expectedWorkload: kube.Object{Kind: kube.KindJob, Name: "my-job"},
 		},
 		{
 			name:             "Should return Job/my-job when kind is specified as job",
 			givenArgs:        []string{"job/my-job"},
-			expectedWorkload: kube.Workload{Kind: kube.WorkloadKindJob, Name: "my-job"},
+			expectedWorkload: kube.Object{Kind: kube.KindJob, Name: "my-job"},
 		},
 		{
 			name:             "Should return error when neither workload kind nor name is specified",
 			givenArgs:        []string{},
-			expectedWorkload: kube.Workload{},
+			expectedWorkload: kube.Object{},
 			expectedError:    errors.New("required workload kind and name not specified"),
 		},
 		{
 			name:             "Should return error when kind is unrecognized",
 			givenArgs:        []string{"xpod/my-pod"},
-			expectedWorkload: kube.Workload{},
-			expectedError:    errors.New("unrecognized workload: xpod"),
+			expectedWorkload: kube.Object{},
+			expectedError:    errors.New("unrecognized resource: xpod"),
 		},
 		{
 			name:          "Should return error when workload name is blank",
diff --git a/pkg/kube/object_test.go b/pkg/kube/object_test.go
new file mode 100644
index 0000000..83b8e12
--- /dev/null
+++ b/pkg/kube/object_test.go
@@ -0,0 +1,73 @@
+package kube
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/labels"
+)
+
+func TestObjectFromLabelsSet(t *testing.T) {
+	testCases := []struct {
+		name           string
+		labelsSet      labels.Set
+		expectedObject Object
+		expectedError  error
+	}{
+		{
+			name: "Should return object for namespaced object",
+			labelsSet: labels.Set{
+				LabelResourceKind:      "Deployment",
+				LabelResourceName:      "my-deployment",
+				LabelResourceNamespace: "my-namespace",
+			},
+			expectedObject: Object{
+				Kind:      KindDeployment,
+				Name:      "my-deployment",
+				Namespace: "my-namespace",
+			},
+		},
+		{
+			name: "Should return object for cluster-scoped object",
+			labelsSet: labels.Set{
+				LabelResourceKind: "Node",
+				LabelResourceName: "my-node",
+			},
+			expectedObject: Object{
+				Kind:      KindNode,
+				Name:      "my-node",
+				Namespace: "",
+			},
+		},
+		{
+			name: "Should return error when object kind is not specified as label",
+			labelsSet: labels.Set{
+				LabelResourceName:      "my-deployment",
+				LabelResourceNamespace: "my-namespace",
+			},
+			expectedError: errors.New("required label does not exist: starboard.resource.kind"),
+		},
+		{
+			name: "Should return error when object name is not specified as label",
+			labelsSet: labels.Set{
+				LabelResourceKind: "Deployment",
+			},
+			expectedError: errors.New("required label does not exist: starboard.resource.name"),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			obj, err := ObjectFromLabelsSet(tc.labelsSet)
+			switch {
+			case tc.expectedError == nil:
+				require.NoError(t, err)
+				assert.Equal(t, tc.expectedObject, obj)
+			default:
+				assert.EqualError(t, err, tc.expectedError.Error())
+			}
+		})
+	}
+}