Repository: aquasecurity/starboard
Pull request #352 (2021-01-18 16:13:38)

refactor(operator): Use predicates to filter out events added to a worker queue
Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/352?src=pr&el=h1) Report
> Merging [#352](https://codecov.io/gh/aquasecurity/starboard/pull/352?src=pr&el=desc) (750d58d) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/0d454621b8d1ced69a88644d47944fd02ec14808?el=desc) (0d45462) will **decrease** coverage by `35.07%`.
> The diff coverage is `46.15%`.

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

```diff
@@             Coverage Diff             @@
##             main     #352       +/-   ##
===========================================
- Coverage   66.24%   31.16%   -35.08%     
===========================================
  Files          56       53        -3     
  Lines        2844     2650      -194     
===========================================
- Hits         1884      826     -1058     
- Misses        718     1764     +1046     
+ Partials      242       60      -182     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/352?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/resources/resources.go](https://codecov.io/gh/aquasecurity/starboard/pull/352/diff?src=pr&el=tree#diff-cGtnL3Jlc291cmNlcy9yZXNvdXJjZXMuZ28=) | `31.25% <ø> (+2.67%)` | :arrow_up: |
| [pkg/operator/predicate/predicate.go](https://codecov.io/gh/aquasecurity/starboard/pull/352/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL3ByZWRpY2F0ZS9wcmVkaWNhdGUuZ28=) | `46.15% <46.15%> (ø)` | |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/352/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/352/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/352/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/352/diff?src=pr&el=tree#diff-cGtnL2NtZC9zY2FuLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/polaris.go](https://codecov.io/gh/aquasecurity/starboard/pull/352/diff?src=pr&el=tree#diff-cGtnL2NtZC9wb2xhcmlzLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/version.go](https://codecov.io/gh/aquasecurity/starboard/pull/352/diff?src=pr&el=tree#diff-cGtnL2NtZC92ZXJzaW9uLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/kube\_bench.go](https://codecov.io/gh/aquasecurity/starboard/pull/352/diff?src=pr&el=tree#diff-cGtnL2NtZC9rdWJlX2JlbmNoLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/kube\_hunter.go](https://codecov.io/gh/aquasecurity/starboard/pull/352/diff?src=pr&el=tree#diff-cGtnL2NtZC9rdWJlX2h1bnRlci5nbw==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [30 more](https://codecov.io/gh/aquasecurity/starboard/pull/352/diff?src=pr&el=tree-more) | |

------

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


Patch:
diff --git a/Makefile b/Makefile
index 8f10e40..4bfa934 100644
--- a/Makefile
+++ b/Makefile
@@ -84,6 +84,7 @@ itests-starboard-operator: check-env get-ginkgo
 	--v \
 	-coverprofile=coverage.txt \
 	-coverpkg=github.com/aquasecurity/starboard/pkg/operator,\
+	github.com/aquasecurity/starboard/pkg/operator/predicate,\
 	github.com/aquasecurity/starboard/pkg/operator/controller,\
 	github.com/aquasecurity/starboard/pkg/operator/controller/job,\
 	github.com/aquasecurity/starboard/pkg/operator/controller/pod,\
diff --git a/pkg/operator/controller/job/job_controller.go b/pkg/operator/controller/job/job_controller.go
index 8ef4eef..641cc0a 100644
--- a/pkg/operator/controller/job/job_controller.go
+++ b/pkg/operator/controller/job/job_controller.go
@@ -4,19 +4,17 @@ import (
 	"context"
 	"fmt"
 
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
-	"github.com/aquasecurity/starboard/pkg/operator/controller"
-
 	"github.com/aquasecurity/starboard/pkg/kube"
 	pods "github.com/aquasecurity/starboard/pkg/kube/pod"
+	"github.com/aquasecurity/starboard/pkg/operator/controller"
 	"github.com/aquasecurity/starboard/pkg/operator/etc"
+	"github.com/aquasecurity/starboard/pkg/operator/predicate"
 	"github.com/aquasecurity/starboard/pkg/resources"
-	"k8s.io/apimachinery/pkg/api/errors"
-
 	batchv1 "k8s.io/api/batch/v1"
-
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -34,11 +32,6 @@ type JobController struct {
 func (r *JobController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	log := log.WithValues("job", req.NamespacedName)
 
-	if req.Namespace != r.Namespace {
-		log.V(1).Info("Ignoring Job not managed by this operator")
-		return ctrl.Result{}, nil
-	}
-
 	job := &batchv1.Job{}
 	err := r.Client.Get(ctx, req.NamespacedName, job)
 	if err != nil {
@@ -49,11 +42,6 @@ func (r *JobController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
 		return ctrl.Result{}, fmt.Errorf("getting job from cache: %w", err)
 	}
 
-	if len(job.Status.Conditions) == 0 {
-		log.V(1).Info("Ignoring Job without status conditions")
-		return ctrl.Result{}, nil
-	}
-
 	switch jobCondition := job.Status.Conditions[0].Type; jobCondition {
 	case batchv1.JobComplete:
 		err = r.processCompleteScanJob(ctx, job)
@@ -125,6 +113,10 @@ func (r *JobController) processFailedScanJob(ctx context.Context, scanJob *batch
 
 func (r *JobController) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
-		For(&batchv1.Job{}).
+		For(&batchv1.Job{}, builder.WithPredicates(
+			predicate.InNamespace(r.Config.Namespace),
+			predicate.ManagedByStarboardOperator,
+			predicate.JobHasAnyCondition,
+		)).
 		Complete(r)
 }
diff --git a/pkg/operator/controller/pod/pod_controller.go b/pkg/operator/controller/pod/pod_controller.go
index 5bc8132..068287e 100644
--- a/pkg/operator/controller/pod/pod_controller.go
+++ b/pkg/operator/controller/pod/pod_controller.go
@@ -5,15 +5,14 @@ import (
 	"fmt"
 
 	"github.com/aquasecurity/starboard/pkg/operator/controller"
-
-	"k8s.io/apimachinery/pkg/types"
-
-	"github.com/aquasecurity/starboard/pkg/resources"
-
 	"github.com/aquasecurity/starboard/pkg/operator/etc"
+	. "github.com/aquasecurity/starboard/pkg/operator/predicate"
+	"github.com/aquasecurity/starboard/pkg/resources"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -38,6 +37,7 @@ func (r *PodController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
 		return ctrl.Result{}, fmt.Errorf("getting install mode: %w", err)
 	}
 
+	// TODO Consider using a predicate
 	if r.IgnorePodInOperatorNamespace(installMode, req.NamespacedName) {
 		log.V(1).Info("Ignoring Pod run in the operator namespace")
 		return ctrl.Result{}, nil
@@ -53,24 +53,6 @@ func (r *PodController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
 		return ctrl.Result{}, fmt.Errorf("getting pod from cache: %w", err)
 	}
 
-	// Check if the Pod is managed by the operator, i.e. is controlled by a scan Job created by the PodController.
-	if IsPodManagedByStarboardOperator(pod) {
-		log.V(1).Info("Ignoring Pod managed by this operator")
-		return ctrl.Result{}, nil
-	}
-
-	// Check if the Pod is being terminated.
-	if pod.DeletionTimestamp != nil {
-		log.V(1).Info("Ignoring Pod that is being terminated")
-		return ctrl.Result{}, nil
-	}
-
-	// Check if the Pod containers are ready.
-	if !resources.HasContainersReadyCondition(pod) {
-		log.V(1).Info("Ignoring Pod that is being scheduled")
-		return ctrl.Result{}, nil
-	}
-
 	owner := resources.GetImmediateOwnerReference(pod)
 	containerImages := resources.GetContainerImagesFromPodSpec(pod.Spec)
 	hash := resources.ComputeHash(pod.Spec)
@@ -143,19 +125,13 @@ func (r *PodController) IgnorePodInOperatorNamespace(installMode etc.InstallMode
 	return false
 }
 
-// IsPodManagedByStarboardOperator returns true if the specified Pod
-// is managed by the Starboard Operator, false otherwise.
-//
-// We define managed Pods as ones controlled by Jobs created by the Starboard Operator.
-// They're labeled with `app.kubernetes.io/managed-by=starboard-operator`.
-func IsPodManagedByStarboardOperator(pod *corev1.Pod) bool {
-	managedBy, exists := pod.Labels["app.kubernetes.io/managed-by"]
-	return exists && managedBy == "starboard-operator"
-}
-
 func (r *PodController) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
-		For(&corev1.Pod{}).
+		For(&corev1.Pod{}, builder.WithPredicates(
+			Not(ManagedByStarboardOperator),
+			Not(PodBeingTerminated),
+			PodHasContainersReadyCondition,
+		)).
 		Complete(r)
 }
 
diff --git a/pkg/operator/predicate/predicate.go b/pkg/operator/predicate/predicate.go
new file mode 100644
index 0000000..a17f458
--- /dev/null
+++ b/pkg/operator/predicate/predicate.go
@@ -0,0 +1,78 @@
+package predicate
+
+import (
+	batchv1 "k8s.io/api/batch/v1"
+	corev1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/event"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+)
+
+// InNamespace is a predicate.Predicate that returns true if the
+// specified client.Object is in the desired namespace.
+var InNamespace = func(namespace string) predicate.Predicate {
+	return predicate.NewPredicateFuncs(func(obj client.Object) bool {
+		return namespace == obj.GetNamespace()
+	})
+}
+
+// ManagedByStarboardOperator is a predicate.Predicate that returns true if the
+// specified client.Object is managed by the Starboard Operator.
+//
+// For example, pods controlled by jobs scheduled by Starboard Operator are
+// labeled with `app.kubernetes.io/managed-by=starboard-operator`.
+var ManagedByStarboardOperator = predicate.NewPredicateFuncs(func(obj client.Object) bool {
+	if managedBy, ok := obj.GetLabels()["app.kubernetes.io/managed-by"]; ok {
+		return managedBy == "starboard-operator"
+	}
+	return false
+})
+
+// PodHasContainersReadyCondition is a predicate.Predicate that returns true if the
+// specified client.Object is a corev1.Pod with corev1.ContainersReady condition.
+var PodHasContainersReadyCondition = predicate.NewPredicateFuncs(func(obj client.Object) bool {
+	if pod, ok := obj.(*corev1.Pod); ok {
+		for _, condition := range pod.Status.Conditions {
+			if condition.Type == corev1.ContainersReady {
+				return true
+			}
+		}
+	}
+	return false
+})
+
+// PodBeingTerminated is a predicate.Predicate that returns true if the specified
+// client.Object is a corev1.Pod that is being terminated, i.e. its
+// DeletionTimestamp property is set to non nil value.
+var PodBeingTerminated = predicate.NewPredicateFuncs(func(obj client.Object) bool {
+	if pod, ok := obj.(*corev1.Pod); ok {
+		return pod.DeletionTimestamp != nil
+	}
+	return false
+})
+
+// JobHasConditions is a predicate.Predicate that returns true if the
+// specified client.Object is a batchv1.Job with any batchv1.JobConditionType.
+var JobHasAnyCondition = predicate.NewPredicateFuncs(func(obj client.Object) bool {
+	if job, ok := obj.(*batchv1.Job); ok {
+		return len(job.Status.Conditions) > 0
+	}
+	return false
+})
+
+func Not(p predicate.Predicate) predicate.Predicate {
+	return predicate.Funcs{
+		CreateFunc: func(event event.CreateEvent) bool {
+			return !p.Create(event)
+		},
+		DeleteFunc: func(event event.DeleteEvent) bool {
+			return !p.Delete(event)
+		},
+		UpdateFunc: func(event event.UpdateEvent) bool {
+			return !p.Update(event)
+		},
+		GenericFunc: func(event event.GenericEvent) bool {
+			return !p.Generic(event)
+		},
+	}
+}
diff --git a/pkg/resources/resources.go b/pkg/resources/resources.go
index de7ec37..f7785d1 100644
--- a/pkg/resources/resources.go
+++ b/pkg/resources/resources.go
@@ -5,14 +5,12 @@ import (
 	"hash"
 	"hash/fnv"
 
-	"github.com/davecgh/go-spew/spew"
-	"k8s.io/apimachinery/pkg/util/rand"
-
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
 	"github.com/aquasecurity/starboard/pkg/kube"
+	"github.com/davecgh/go-spew/spew"
 	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/rand"
 )
 
 func GetContainerImagesFromPodSpec(spec corev1.PodSpec) kube.ContainerImages {
@@ -38,17 +36,6 @@ func GetContainerImagesFromJob(job *batchv1.Job) (kube.ContainerImages, error) {
 	return containerImages, nil
 }
 
-// HasContainersReadyCondition iterates conditions of the specified Pod to check
-// whether all containers in the Pod are ready.
-func HasContainersReadyCondition(pod *corev1.Pod) bool {
-	for _, condition := range pod.Status.Conditions {
-		if condition.Type == corev1.ContainersReady {
-			return true
-		}
-	}
-	return false
-}
-
 // GetImmediateOwnerReference returns the immediate owner of the specified Pod.
 // For example, for a Pod controlled by a Deployment it will return the active ReplicaSet object,
 // whereas for an unmanaged Pod the immediate owner is the Pod itself.


Test patch:
diff --git a/pkg/operator/predicate/predicate_suite_test.go b/pkg/operator/predicate/predicate_suite_test.go
new file mode 100644
index 0000000..445fc4f
--- /dev/null
+++ b/pkg/operator/predicate/predicate_suite_test.go
@@ -0,0 +1,20 @@
+package predicate_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+)
+
+func TestPredicate(t *testing.T) {
+	RegisterFailHandler(Fail)
+	suiteName := "Predicate Suite"
+	RunSpecs(t, suiteName)
+}
+
+var _ = BeforeSuite(func() {
+	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+})
diff --git a/pkg/operator/predicate/predicate_test.go b/pkg/operator/predicate/predicate_test.go
new file mode 100644
index 0000000..675345b
--- /dev/null
+++ b/pkg/operator/predicate/predicate_test.go
@@ -0,0 +1,243 @@
+package predicate_test
+
+import (
+	"time"
+
+	"github.com/aquasecurity/starboard/pkg/operator/predicate"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	batchv1 "k8s.io/api/batch/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/event"
+	predicatex "sigs.k8s.io/controller-runtime/pkg/predicate"
+)
+
+var _ = Describe("Predicate", func() {
+
+	Describe("When checking a InNamespace predicate", func() {
+
+		Context("When object is in desired namespace", func() {
+
+			It("Should return true", func() {
+				instance := predicate.InNamespace("starboard-operator")
+				obj := &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "starboard-operator",
+					},
+				}
+
+				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeTrue())
+				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeTrue())
+				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeTrue())
+				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeTrue())
+			})
+
+		})
+
+		Context("When object is not in desired namespace", func() {
+
+			It("Should return false", func() {
+				instance := predicate.InNamespace("starboard-operator")
+				obj := &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: corev1.NamespaceDefault,
+					},
+				}
+
+				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeFalse())
+				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeFalse())
+			})
+
+		})
+
+	})
+
+	Describe("When checking a ManagedByStarboardOperator predicate", func() {
+
+		instance := predicate.ManagedByStarboardOperator
+
+		Context("Where object is managed by Starboard operator", func() {
+			It("Should return true", func() {
+				obj := &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{
+							"app.kubernetes.io/managed-by": "starboard-operator",
+						},
+					},
+				}
+
+				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeTrue())
+				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeTrue())
+				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeTrue())
+				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeTrue())
+			})
+		})
+
+		Context("Where object is managed by foo app", func() {
+			It("Should return false", func() {
+				obj := &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{
+							"app.kubernetes.io/managed-by": "foo",
+						},
+					},
+				}
+
+				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeFalse())
+				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeFalse())
+			})
+		})
+
+		Context("Where object is not managed by any app", func() {
+			It("Should return false", func() {
+				obj := &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{},
+					},
+				}
+
+				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeFalse())
+				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeFalse())
+			})
+		})
+	})
+
+	Describe("When checking a PodBeingTerminated predicate", func() {
+
+		instance := predicate.PodBeingTerminated
+		deletionTimestamp := metav1.NewTime(time.Now())
+
+		Context("Where pod is being terminated", func() {
+			It("Should return true", func() {
+				obj := &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						DeletionTimestamp: &deletionTimestamp,
+					},
+				}
+
+				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeTrue())
+				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeTrue())
+				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeTrue())
+				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeTrue())
+			})
+		})
+
+		Context("Where pod is not being terminated", func() {
+			It("Should return false", func() {
+				obj := &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						DeletionTimestamp: nil,
+					},
+				}
+
+				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeFalse())
+				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeFalse())
+			})
+		})
+	})
+
+	Describe("When checking a PodHasContainersReadyCondition predicate", func() {
+
+		instance := predicate.PodHasContainersReadyCondition
+
+		Context("Where pod has ContainersReady condition", func() {
+			It("Should return true", func() {
+				obj := &corev1.Pod{
+					Status: corev1.PodStatus{
+						Conditions: []corev1.PodCondition{
+							{Type: corev1.PodInitialized},
+							{Type: corev1.PodReady},
+							{Type: corev1.ContainersReady},
+						},
+					},
+				}
+
+				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeTrue())
+				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeTrue())
+				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeTrue())
+				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeTrue())
+			})
+		})
+
+		Context("Where pod doesn't have ContainersReady condition", func() {
+			It("Should return false", func() {
+				obj := &corev1.Pod{
+					Status: corev1.PodStatus{
+						Conditions: []corev1.PodCondition{
+							{Type: corev1.PodInitialized},
+							{Type: corev1.PodReady},
+						},
+					},
+				}
+
+				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeFalse())
+				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeFalse())
+			})
+		})
+	})
+
+	Describe("When checking a JobHasAnyCondition predicate", func() {
+		instance := predicate.JobHasAnyCondition
+		Context("Where job has any condition", func() {
+			It("Should return true", func() {
+				obj := &batchv1.Job{
+					Status: batchv1.JobStatus{
+						Conditions: []batchv1.JobCondition{
+							{
+								Type: batchv1.JobComplete,
+							},
+						},
+					},
+				}
+
+				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeTrue())
+				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeTrue())
+				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeTrue())
+				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeTrue())
+			})
+		})
+		Context("Where job doesn't have condition", func() {
+			It("Should return false", func() {
+				obj := &batchv1.Job{
+					Status: batchv1.JobStatus{
+						Conditions: []batchv1.JobCondition{},
+					},
+				}
+
+				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeFalse())
+				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeFalse())
+				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeFalse())
+			})
+		})
+	})
+
+	Describe("When checking a Not predicate", func() {
+
+		Context("Where input predicate returns true", func() {
+			It("Should return false", func() {
+				instance := predicate.Not(predicatex.NewPredicateFuncs(func(_ client.Object) bool {
+					return true
+				}))
+
+				Expect(instance.Create(event.CreateEvent{})).To(BeFalse())
+				Expect(instance.Update(event.UpdateEvent{})).To(BeFalse())
+				Expect(instance.Delete(event.DeleteEvent{})).To(BeFalse())
+				Expect(instance.Generic(event.GenericEvent{})).To(BeFalse())
+			})
+		})
+
+	})
+})