Repository: abahmed/kwatch
Pull request #280 (2024-02-29 10:31:33)

🚀 Refactor controller
Changes proposed in this pull request:
- Refactor controller and use watcher
- Introduce new mechanism for detecting crashes
- Update memory module




Patch:
diff --git a/.gitignore b/.gitignore
index f2ae5d0..1648f2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,4 +29,4 @@ config.yaml
 *.DS_Store
 
 # debug
-__debug_bin
\ No newline at end of file
+__debug_bin*
\ No newline at end of file
diff --git a/README.md b/README.md
index 4a3167d..ead3a99 100644
--- a/README.md
+++ b/README.md
@@ -28,11 +28,6 @@
 
 **kwatch** helps you monitor all changes in your Kubernetes(K8s) cluster, detects crashes in your running apps in realtime, and publishes notifications to your channels (Slack, Discord, etc.) instantly
 
-## ⭐️ Join Waitlist
-
-We're working on SAAS version of kwatch that provides User interface, optimized notifications, more details about crashes, and more. You can join <a href="https://join.kwatch.dev">the waitlist</a> to get early access!
-
-
 ## ⚡️ Getting Started
 
 ### Install
diff --git a/config/config.go b/config/config.go
index 174e76a..02521b7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -46,12 +46,12 @@ type Config struct {
 	Alert map[string]map[string]interface{} `yaml:"alert"`
 
 	// AllowedNamespaces, ForbiddenNamespaces are calculated internally
-	// after loading Namespaces configuration
+	// after populating Namespaces configuration
 	AllowedNamespaces   []string
 	ForbiddenNamespaces []string
 
-	// AllowedReasons, ForbiddenReasons are calculated internally after loading
-	// Reasons configuration
+	// AllowedReasons, ForbiddenReasons are calculated internally after
+	// populating Reasons configuration
 	AllowedReasons   []string
 	ForbiddenReasons []string
 
diff --git a/config/defaultConfig.go b/config/defaultConfig.go
index 07de601..0957a3d 100644
--- a/config/defaultConfig.go
+++ b/config/defaultConfig.go
@@ -2,6 +2,7 @@ package config
 
 func DefaultConfig() *Config {
 	return &Config{
+		IgnoreFailedGracefulShutdown: true,
 		PvcMonitor: PvcMonitor{
 			Enabled:   true,
 			Interval:  5,
diff --git a/constant/constant.go b/constant/constant.go
index 9f7c871..df1a203 100644
--- a/constant/constant.go
+++ b/constant/constant.go
@@ -9,12 +9,6 @@ const KwatchUpdateMsg = ":tada: A newer version " +
 	"<https://github.com/abahmed/kwatch/releases/tag/%[1]s|%[1]s> of Kwatch " +
 	"is available! Please update to the latest version."
 
-// NumRequeues indicates number of retries when worker fails to handle item
-const NumRequeues = 5
-
-// NumWorkers is the number concurrent workers that consume items for the queue
-const NumWorkers = 4
-
 const (
 	Footer        = "<https://github.com/abahmed/kwatch|kwatch>"
 	DefaultTitle  = ":red_circle: kwatch detected a crash in pod"
diff --git a/controller/controller.go b/controller/controller.go
deleted file mode 100644
index 8fbc0b1..0000000
--- a/controller/controller.go
+++ /dev/null
@@ -1,269 +0,0 @@
-package controller
-
-import (
-	"errors"
-	"time"
-
-	"github.com/abahmed/kwatch/alertmanager"
-	"github.com/abahmed/kwatch/config"
-	"github.com/abahmed/kwatch/constant"
-	"github.com/abahmed/kwatch/event"
-	"github.com/abahmed/kwatch/storage"
-	"github.com/abahmed/kwatch/util"
-	"github.com/sirupsen/logrus"
-	v1 "k8s.io/api/core/v1"
-	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
-	"k8s.io/apimachinery/pkg/util/wait"
-	"k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/tools/cache"
-	"k8s.io/client-go/util/workqueue"
-	"k8s.io/utils/strings/slices"
-)
-
-// Controller holds necessary
-type Controller struct {
-	name         string
-	informer     cache.Controller
-	indexer      cache.Indexer
-	kclient      kubernetes.Interface
-	queue        workqueue.RateLimitingInterface
-	alertManager *alertmanager.AlertManager
-	store        storage.Storage
-	config       *config.Config
-}
-
-// run starts the controller
-func (c *Controller) run(workers int, stopCh chan struct{}) {
-	defer utilruntime.HandleCrash()
-	defer c.queue.ShutDown()
-
-	logrus.Infof("starting %s controller", c.name)
-
-	go c.informer.Run(stopCh)
-
-	if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {
-		utilruntime.HandleError(
-			errors.New("timed out waiting for caches to sync"))
-		return
-	}
-
-	logrus.Infof("%s controller synced and ready", c.name)
-
-	// start workers
-	for i := 0; i < workers; i++ {
-		go wait.Until(c.runWorker, time.Second, stopCh)
-	}
-
-	<-stopCh
-}
-
-func (c *Controller) runWorker() {
-	for c.processNextItem() {
-		// continue looping
-	}
-}
-
-func (c *Controller) processNextItem() bool {
-	newEvent, quit := c.queue.Get()
-
-	if quit {
-		return false
-	}
-
-	defer c.queue.Done(newEvent)
-
-	err := c.processItem(newEvent.(string))
-	if err == nil {
-		// No error, reset the ratelimit counters
-		c.queue.Forget(newEvent)
-	} else if c.queue.NumRequeues(newEvent) < constant.NumRequeues {
-		logrus.Errorf("failed to process %v (will retry): %v", newEvent, err)
-		c.queue.AddRateLimited(newEvent)
-	} else {
-		// err != nil and too many retries
-		logrus.Errorf("failed to process %v (giving up): %v", newEvent, err)
-		c.queue.Forget(newEvent)
-		utilruntime.HandleError(err)
-	}
-
-	return true
-}
-
-func (c *Controller) processItem(key string) error {
-	obj, exists, err := c.indexer.GetByKey(key)
-	if err != nil {
-		logrus.Errorf(
-			"failed to fetch object %s from store: %s",
-			key,
-			err.Error())
-		return err
-	}
-
-	if !exists {
-		// Below we will warm up our cache with a Pod, so that we will see a
-		// delete for one pod
-		logrus.Infof("pod %s does not exist anymore\n", key)
-
-		c.store.DelPod(key)
-
-		// Clean up intervals if possible
-		return nil
-	}
-
-	pod, ok := obj.(*v1.Pod)
-	if !ok {
-		logrus.Warnf("failed to cast to pod object: %v", obj)
-
-		// to avoid re-queuing it
-		return nil
-	}
-
-	// filter by namespaces in config if specified
-	if len(c.config.AllowedNamespaces) > 0 &&
-		!slices.Contains(c.config.AllowedNamespaces, pod.Namespace) {
-		logrus.Infof(
-			"skip namespace %s as not in namespace allow list",
-			pod.Namespace)
-		return nil
-	}
-	if len(c.config.ForbiddenNamespaces) > 0 &&
-		slices.Contains(c.config.ForbiddenNamespaces, pod.Namespace) {
-		logrus.Infof(
-			"skip namespace %s as in namespace forbid list",
-			pod.Namespace)
-		return nil
-	}
-
-	c.processPod(key, pod)
-
-	return nil
-}
-
-// processPod checks status of pod and notify in abnormal cases
-func (c *Controller) processPod(key string, pod *v1.Pod) {
-	for _, container := range pod.Status.ContainerStatuses {
-		// filter running containers
-		if container.Ready ||
-			(container.State.Waiting == nil &&
-				container.State.Terminated == nil) {
-			c.store.DelPodContainer(key, container.Name)
-			continue
-		}
-
-		if container.State.Waiting != nil {
-			switch {
-			case container.State.Waiting.Reason == "ContainerCreating":
-				continue
-			case container.State.Waiting.Reason == "PodInitializing":
-				continue
-			}
-		} else if container.State.Terminated != nil {
-			switch {
-			case container.State.Terminated.Reason == "Completed":
-				continue
-			case container.State.Terminated.ExitCode == 143:
-				// 143 is the exit code for graceful termination
-				continue
-			case container.State.Terminated.ExitCode == 0:
-				// 0 is the exit code for purpose stop
-				continue
-			}
-		}
-
-		// if reported, continue
-		if c.store.HasPodContainer(key, container.Name) {
-			continue
-		}
-
-		if c.config.IgnoreFailedGracefulShutdown &&
-			util.ContainsKillingStoppingContainerEvents(
-				c.kclient,
-				pod.Name,
-				pod.Namespace) {
-			// Graceful shutdown did not work and container was killed during
-			// shutdown.
-			//  Not really an error
-			continue
-		}
-
-		logrus.Debugf(
-			"processing container %s in pod %s@%s",
-			container.Name,
-			pod.Name,
-			pod.Namespace)
-
-		// get reason according to state
-		reason := "Unknown"
-		if container.State.Waiting != nil {
-			reason = container.State.Waiting.Reason
-		} else if container.State.Terminated != nil {
-			reason = container.State.Terminated.Reason
-		}
-
-		if len(c.config.AllowedReasons) > 0 &&
-			!slices.Contains(c.config.AllowedReasons, reason) {
-			logrus.Infof("skip reason %s as not in reason allow list", reason)
-			return
-		}
-		if len(c.config.ForbiddenReasons) > 0 &&
-			slices.Contains(c.config.ForbiddenReasons, reason) {
-			logrus.Infof("skip reason %s as in reason forbid list", reason)
-			return
-		}
-
-		if len(c.config.IgnoreContainerNames) > 0 &&
-			slices.Contains(c.config.IgnoreContainerNames, container.Name) {
-			logrus.Infof(
-				"skip container %s as in container ignore list",
-				container.Name)
-			return
-		}
-
-		if len(c.config.IgnorePodNames) > 0 {
-			for _, pattern := range c.config.IgnorePodNamePatterns {
-				if pattern.MatchString(pod.Name) {
-					logrus.Infof(
-						"skip pod %s as in pod name patterns ignore list",
-						container.Name)
-					return
-				}
-			}
-		}
-
-		// get logs for this container
-		previous := true
-		if reason == "Error" {
-			previous = false
-		} else if container.RestartCount > 0 {
-			previous = true
-		}
-
-		logs := util.GetPodContainerLogs(
-			c.kclient,
-			pod.Name,
-			container.Name,
-			pod.Namespace,
-			previous,
-			c.config.MaxRecentLogLines)
-
-		// get events for this pod
-		eventsString :=
-			util.GetPodEventsStr(c.kclient, pod.Name, pod.Namespace)
-
-		evnt := event.Event{
-			Name:      pod.Name,
-			Container: container.Name,
-			Namespace: pod.Namespace,
-			Reason:    reason,
-			Logs:      logs,
-			Events:    eventsString,
-			Labels:    pod.Labels,
-		}
-
-		// save container as it's reported to avoid duplication
-		c.store.AddPodContainer(key, container.Name)
-
-		// send event to providers
-		c.alertManager.NotifyEvent(evnt)
-	}
-}
diff --git a/controller/start.go b/controller/start.go
deleted file mode 100644
index c8b2273..0000000
--- a/controller/start.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package controller
-
-import (
-	"context"
-
-	"github.com/abahmed/kwatch/alertmanager"
-	"github.com/abahmed/kwatch/config"
-	"github.com/abahmed/kwatch/constant"
-	memory "github.com/abahmed/kwatch/storage/memory"
-	"github.com/sirupsen/logrus"
-	v1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/watch"
-	"k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/tools/cache"
-	"k8s.io/client-go/util/workqueue"
-)
-
-// Start creates an instance of controller after initialization and runs it
-func Start(
-	client kubernetes.Interface,
-	alertManager *alertmanager.AlertManager,
-	config *config.Config) {
-	// create rate limiting queue
-	queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
-
-	// Namespace to watch, if all is selected it will watch all namespaces
-	// in a cluster scope, if not then it will watch only in the namespace
-	var namespaceToWatch = v1.NamespaceAll
-
-	// if there is exactly 1 namespace listen only to that namespace for events
-	if len(config.AllowedNamespaces) == 1 {
-		namespaceToWatch = config.AllowedNamespaces[0]
-	}
-
-	indexer, informer := cache.NewIndexerInformer(
-		&cache.ListWatch{
-			ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
-				return client.CoreV1().
-					Pods(namespaceToWatch).
-					List(context.TODO(), opts)
-			},
-			WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) {
-				return client.CoreV1().
-					Pods(namespaceToWatch).
-					Watch(context.TODO(), opts)
-			},
-		},
-		&v1.Pod{},
-		0,
-		cache.ResourceEventHandlerFuncs{
-			AddFunc: func(obj interface{}) {
-				key, err := cache.MetaNamespaceKeyFunc(obj)
-				if err == nil {
-					logrus.Debugf("received create for Pod %s\n", key)
-					queue.Add(key)
-				}
-			},
-			UpdateFunc: func(_, new interface{}) {
-				key, err := cache.MetaNamespaceKeyFunc(new)
-				if err == nil {
-					logrus.Debugf("received update for Pod %s\n", key)
-					queue.Add(key)
-				}
-			},
-			DeleteFunc: func(obj interface{}) {
-				// IndexerInformer uses a delta queue, therefore for deletes
-				// we have to use this key function.
-				key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
-				if err == nil {
-					logrus.Debugf("received delete for Pod %s\n", key)
-					queue.Add(key)
-				}
-			},
-		}, cache.Indexers{})
-
-	controller := Controller{
-		name:         "pod-crash",
-		informer:     informer,
-		indexer:      indexer,
-		queue:        queue,
-		kclient:      client,
-		alertManager: alertManager,
-		store:        memory.NewMemory(),
-		config:       config,
-	}
-
-	stopCh := make(chan struct{})
-	defer close(stopCh)
-
-	controller.run(constant.NumWorkers, stopCh)
-}
diff --git a/filter/containerKillingFilter.go b/filter/containerKillingFilter.go
new file mode 100644
index 0000000..13ff9c9
--- /dev/null
+++ b/filter/containerKillingFilter.go
@@ -0,0 +1,32 @@
+package filter
+
+import (
+	"strings"
+)
+
+type ContainerKillingFilter struct{}
+
+func (f ContainerKillingFilter) Execute(ctx *Context) bool {
+	if !ctx.Config.IgnoreFailedGracefulShutdown || ctx.Events == nil {
+		return false
+	}
+	container := ctx.Container.Container
+
+	isOk := false
+	if container.State.Waiting != nil {
+		return isOk
+	}
+
+	for _, ev := range *ctx.Events {
+		// Graceful shutdown did not work and container was killed during
+		// shutdown. Not really an error
+		if ev.Reason == "Killing" &&
+			strings.Contains(
+				ev.Message,
+				"Stopping container "+container.Name) {
+			isOk = true
+		}
+	}
+
+	return isOk
+}
diff --git a/filter/containerLogsFilter.go b/filter/containerLogsFilter.go
new file mode 100644
index 0000000..420db3a
--- /dev/null
+++ b/filter/containerLogsFilter.go
@@ -0,0 +1,31 @@
+package filter
+
+import (
+	"github.com/abahmed/kwatch/util"
+)
+
+type ContainerLogsFilter struct{}
+
+func (f ContainerLogsFilter) Execute(ctx *Context) bool {
+	container := ctx.Container.Container
+
+	if container.RestartCount == 0 && container.State.Waiting != nil {
+		return false
+	}
+
+	previousLogs := false
+	if ctx.Container.HasRestarts && container.State.Running != nil {
+		previousLogs = true
+	}
+
+	logs := util.GetPodContainerLogs(
+		ctx.Client,
+		ctx.Pod.Name,
+		container.Name,
+		ctx.Pod.Namespace,
+		previousLogs,
+		ctx.Config.MaxRecentLogLines)
+
+	ctx.Container.Logs = logs
+	return false
+}
diff --git a/filter/containerNameFilter.go b/filter/containerNameFilter.go
new file mode 100644
index 0000000..0046da5
--- /dev/null
+++ b/filter/containerNameFilter.go
@@ -0,0 +1,21 @@
+package filter
+
+import (
+	"github.com/sirupsen/logrus"
+	"golang.org/x/exp/slices"
+)
+
+type ContainerNameFilter struct{}
+
+func (f ContainerNameFilter) Execute(ctx *Context) bool {
+	container := ctx.Container.Container
+	if len(ctx.Config.IgnoreContainerNames) > 0 &&
+		slices.Contains(ctx.Config.IgnoreContainerNames, container.Name) {
+		logrus.Infof(
+			"skip pod %s as in container ignore list",
+			container.Name)
+		return true
+	}
+
+	return false
+}
diff --git a/filter/containerReasonsFilter.go b/filter/containerReasonsFilter.go
new file mode 100644
index 0000000..b6b377a
--- /dev/null
+++ b/filter/containerReasonsFilter.go
@@ -0,0 +1,68 @@
+package filter
+
+import (
+	"github.com/sirupsen/logrus"
+	"golang.org/x/exp/slices"
+)
+
+type ContainerReasonsFilter struct{}
+
+func (f ContainerReasonsFilter) Execute(ctx *Context) bool {
+	container := ctx.Container.Container
+
+	if container.State.Waiting != nil {
+		ctx.Container.Reason = container.State.Waiting.Reason
+		ctx.Container.Msg = container.State.Waiting.Message
+	} else if container.State.Terminated != nil {
+		ctx.Container.Reason = container.State.Terminated.Reason
+		ctx.Container.Msg = container.State.Terminated.Message
+		ctx.Container.ExitCode = container.State.Terminated.ExitCode
+		ctx.Container.LastTerminatedOn = container.State.Terminated.StartedAt.Time
+	}
+
+	if len(ctx.Config.AllowedReasons) > 0 &&
+		!slices.Contains(ctx.Config.AllowedReasons, ctx.Container.Reason) {
+		logrus.Infof(
+			"skip reason %s as not in reason allow list",
+			ctx.Container.Reason)
+		return true
+	}
+
+	if len(ctx.Config.ForbiddenReasons) > 0 &&
+		slices.Contains(ctx.Config.ForbiddenReasons, ctx.Container.Reason) {
+		logrus.Infof(
+			"skip reason %s as in reason forbid list",
+			ctx.Container.Reason)
+		return true
+	}
+	if (ctx.Container.Reason == "CrashLoopBackOff" ||
+		ctx.Container.HasRestarts) &&
+		container.LastTerminationState.Terminated != nil {
+		ctx.Container.Reason =
+			container.LastTerminationState.Terminated.Reason
+		ctx.Container.Msg =
+			container.LastTerminationState.Terminated.Message
+		ctx.Container.ExitCode =
+			container.LastTerminationState.Terminated.ExitCode
+		ctx.Container.LastTerminatedOn =
+			container.LastTerminationState.Terminated.StartedAt.Time
+	}
+
+	lastState := ctx.Memory.GetPodContainer(ctx.Pod.Namespace,
+		ctx.Pod.Name,
+		container.Name)
+
+	if lastState != nil {
+		if lastState.LastTerminatedOn == ctx.Container.LastTerminatedOn {
+			return true
+		}
+
+		if lastState.Reason == ctx.Container.Reason &&
+			lastState.Msg == ctx.Container.Msg &&
+			lastState.ExitCode == ctx.Container.ExitCode {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/filter/containerRestartsFilter.go b/filter/containerRestartsFilter.go
new file mode 100644
index 0000000..95f7645
--- /dev/null
+++ b/filter/containerRestartsFilter.go
@@ -0,0 +1,22 @@
+package filter
+
+type ContainerRestartsFilter struct{}
+
+func (f ContainerRestartsFilter) Execute(ctx *Context) bool {
+	container := ctx.Container.Container
+
+	lastState := ctx.Memory.GetPodContainer(ctx.Pod.Namespace,
+		ctx.Pod.Name,
+		container.Name)
+
+	ctx.Container.HasRestarts = false
+	if lastState == nil {
+		return false
+	}
+
+	if container.RestartCount > lastState.RestartCount {
+		ctx.Container.HasRestarts = true
+	}
+
+	return false
+}
diff --git a/filter/containerStateFilter.go b/filter/containerStateFilter.go
new file mode 100644
index 0000000..92b8960
--- /dev/null
+++ b/filter/containerStateFilter.go
@@ -0,0 +1,36 @@
+package filter
+
+type ContainerStateFilter struct{}
+
+func (f ContainerStateFilter) Execute(ctx *Context) bool {
+	container := ctx.Container.Container
+
+	if container.State.Running != nil {
+		ctx.Container.Status = "running"
+	} else if container.State.Waiting != nil {
+		ctx.Container.Status = "waiting"
+	} else if container.State.Terminated != nil {
+		ctx.Container.Status = "terminated"
+	}
+
+	if !ctx.Container.HasRestarts && container.State.Running != nil {
+		return true
+	}
+
+	if container.State.Waiting != nil &&
+		(container.State.Waiting.Reason == "ContainerCreating" ||
+			container.State.Waiting.Reason == "PodInitializing") {
+		return true
+	}
+
+	if container.State.Terminated != nil &&
+		(container.State.Terminated.Reason == "Completed" ||
+			// 143 is the exit code for graceful termination
+			container.State.Terminated.ExitCode == 143 ||
+			// 0 is the exit code for purpose stop
+			container.State.Terminated.ExitCode == 0) {
+		return true
+	}
+
+	return false
+}
diff --git a/filter/eventFilter.go b/filter/eventFilter.go
new file mode 100644
index 0000000..08e853b
--- /dev/null
+++ b/filter/eventFilter.go
@@ -0,0 +1,12 @@
+package filter
+
+type EventFilter struct{}
+
+func (f EventFilter) Execute(ctx *Context) bool {
+	if ctx.EvType == "DELETED" {
+		ctx.Memory.DelPod(ctx.Pod.Namespace, ctx.Pod.Name)
+		return true
+	}
+
+	return false
+}
diff --git a/filter/filter.go b/filter/filter.go
new file mode 100644
index 0000000..3c93d13
--- /dev/null
+++ b/filter/filter.go
@@ -0,0 +1,51 @@
+package filter
+
+import (
+	"time"
+
+	"github.com/abahmed/kwatch/config"
+	"github.com/abahmed/kwatch/storage"
+	corev1 "k8s.io/api/core/v1"
+	apiv1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+type Filter interface {
+	Execute(ctx *Context) (ShouldStop bool)
+}
+
+type FilterResult struct {
+	ShouldStop bool
+}
+
+type Context struct {
+	Client kubernetes.Interface
+	Config *config.Config
+	Memory storage.Storage
+
+	Pod    *corev1.Pod
+	EvType string
+
+	Owner  *apiv1.OwnerReference
+	Events *[]corev1.Event
+
+	PodHasIssues        bool
+	ContainersHasIssues bool
+	PodReason           string
+	PodMsg              string
+
+	// Container
+	Container *ContainerContext
+}
+
+type ContainerContext struct {
+	Container        *corev1.ContainerStatus
+	Reason           string
+	Msg              string
+	ExitCode         int32
+	Logs             string
+	HasRestarts      bool
+	LastTerminatedOn time.Time
+	State            string
+	Status           string
+}
diff --git a/filter/nsFilter.go b/filter/nsFilter.go
new file mode 100644
index 0000000..dcbc609
--- /dev/null
+++ b/filter/nsFilter.go
@@ -0,0 +1,29 @@
+package filter
+
+import (
+	"github.com/sirupsen/logrus"
+	"golang.org/x/exp/slices"
+)
+
+type NsFilter struct{}
+
+func (f NsFilter) Execute(ctx *Context) bool {
+	// filter by namespaces in config if specified
+	if len(ctx.Config.AllowedNamespaces) > 0 &&
+		!slices.Contains(ctx.Config.AllowedNamespaces, ctx.Pod.Namespace) {
+		logrus.Infof(
+			"skip namespace %s as not in namespace allow list",
+			ctx.Pod.Namespace)
+		return true
+	}
+
+	if len(ctx.Config.ForbiddenNamespaces) > 0 &&
+		slices.Contains(ctx.Config.ForbiddenNamespaces, ctx.Pod.Namespace) {
+		logrus.Infof(
+			"skip namespace %s as in namespace forbid list",
+			ctx.Pod.Namespace)
+		return true
+	}
+
+	return false
+}
diff --git a/filter/podEventsFilter.go b/filter/podEventsFilter.go
new file mode 100644
index 0000000..d7cf7f7
--- /dev/null
+++ b/filter/podEventsFilter.go
@@ -0,0 +1,42 @@
+package filter
+
+import (
+	"strings"
+
+	"github.com/abahmed/kwatch/util"
+	corev1 "k8s.io/api/core/v1"
+)
+
+type PodEventsFilter struct{}
+
+func (f PodEventsFilter) Execute(ctx *Context) bool {
+	if !ctx.PodHasIssues {
+		return false
+	}
+	events, _ := util.GetPodEvents(ctx.Client, ctx.Pod.Name, ctx.Pod.Namespace)
+	ctx.Events = &events.Items
+
+	if ctx.Events == nil {
+		return false
+	}
+
+	for _, ev := range *ctx.Events {
+		if ev.Type == corev1.EventTypeWarning {
+			if strings.Contains(ev.Message, "deleting pod") {
+				ctx.PodHasIssues = false
+				ctx.ContainersHasIssues = false
+				return true
+			}
+
+			/*
+				if ev.Reason == "FailedScheduling" ||
+					ev.Reason == "NetworkNotReady" ||
+					ev.Reason == "FailedMount" {
+					ctx.PodHasIssues = true
+					ctx.ContainersHasIssues = false
+					return false
+				}*/
+		}
+	}
+	return false
+}
diff --git a/filter/podOwnersFilter.go b/filter/podOwnersFilter.go
new file mode 100644
index 0000000..0170f4e
--- /dev/null
+++ b/filter/podOwnersFilter.go
@@ -0,0 +1,54 @@
+package filter
+
+import (
+	"context"
+
+	apiv1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type PodOwnersFilter struct{}
+
+func (f PodOwnersFilter) Execute(ctx *Context) bool {
+	if ctx.Owner != nil {
+		return false
+	}
+
+	if len(ctx.Pod.OwnerReferences) == 0 {
+		return false
+	}
+
+	owner := ctx.Pod.OwnerReferences[0]
+	if owner.Kind == "ReplicaSet" {
+		rs, _ :=
+			ctx.Client.AppsV1().ReplicaSets(ctx.Pod.Namespace).Get(
+				context.TODO(),
+				owner.Name,
+				apiv1.GetOptions{})
+
+		if rs != nil && len(rs.ObjectMeta.OwnerReferences) > 0 {
+			owner = rs.ObjectMeta.OwnerReferences[0]
+		}
+	} else if owner.Kind == "DaemonSet" {
+		ds, _ :=
+			ctx.Client.AppsV1().DaemonSets(ctx.Pod.Namespace).Get(
+				context.TODO(),
+				owner.Name,
+				apiv1.GetOptions{})
+		if ds != nil && len(ds.ObjectMeta.OwnerReferences) > 0 {
+			owner = ds.ObjectMeta.OwnerReferences[0]
+		}
+	} else if owner.Kind == "StatefulSet" {
+		ss, _ :=
+			ctx.Client.AppsV1().StatefulSets(ctx.Pod.Namespace).Get(
+				context.TODO(),
+				owner.Name,
+				apiv1.GetOptions{})
+		if ss != nil && len(ss.ObjectMeta.OwnerReferences) > 0 {
+			owner = ss.ObjectMeta.OwnerReferences[0]
+		}
+	}
+
+	ctx.Owner = &owner
+
+	return false
+}
diff --git a/filter/podStatusFilter.go b/filter/podStatusFilter.go
new file mode 100644
index 0000000..39ad2f4
--- /dev/null
+++ b/filter/podStatusFilter.go
@@ -0,0 +1,60 @@
+package filter
+
+import (
+	corev1 "k8s.io/api/core/v1"
+)
+
+type PodStatusFilter struct{}
+
+func (f PodStatusFilter) Execute(ctx *Context) bool {
+	if ctx.Pod.Status.Phase == corev1.PodSucceeded {
+		ctx.PodHasIssues = false
+		ctx.ContainersHasIssues = false
+		return true
+	}
+
+	if ctx.EvType == "Added" && len(ctx.Pod.Status.Conditions) == 0 {
+		ctx.PodHasIssues = false
+		ctx.ContainersHasIssues = false
+		return true
+	}
+
+	issueInContainers := true
+	issueInPod := true
+	for _, c := range ctx.Pod.Status.Conditions {
+		if c.Type == corev1.PodReady {
+			if c.Status == corev1.ConditionFalse && c.Reason == "PodCompleted" {
+				ctx.PodHasIssues = false
+				ctx.ContainersHasIssues = false
+				return true
+			}
+
+			issueInPod = false
+			issueInContainers = false
+			if c.Status != corev1.ConditionTrue {
+				issueInContainers = true
+			}
+		} else if c.Type == corev1.PodScheduled && c.Status == corev1.ConditionFalse {
+			issueInPod = true
+			issueInContainers = false
+			ctx.PodReason = c.Reason
+			ctx.PodMsg = c.Message
+		} else if c.Type == corev1.ContainersReady && c.Status == corev1.ConditionFalse {
+			issueInContainers = true
+			issueInPod = false
+		}
+	}
+
+	ctx.PodHasIssues = issueInPod
+	ctx.ContainersHasIssues = issueInContainers
+
+	lastState := ctx.Memory.GetPodContainer(ctx.Pod.Namespace,
+		ctx.Pod.Name,
+		".")
+
+	if ctx.PodHasIssues && lastState != nil {
+		return true
+	}
+
+	return false
+}
diff --git a/go.mod b/go.mod
index 90af136..a9c1752 100644
--- a/go.mod
+++ b/go.mod
@@ -1,36 +1,36 @@
 module github.com/abahmed/kwatch
 
-go 1.22
+go 1.22.3
 
 require (
 	github.com/bwmarrin/discordgo v0.28.1
 	github.com/google/go-github/v41 v41.0.1-0.20211227215900-a899e0fadbec
 	github.com/sirupsen/logrus v1.9.3
-	github.com/slack-go/slack v0.12.5
+	github.com/slack-go/slack v0.13.0
 	github.com/stretchr/testify v1.9.0
 	gopkg.in/mail.v2 v2.3.1
 	gopkg.in/yaml.v3 v3.0.1
-	k8s.io/api v0.29.4
-	k8s.io/apimachinery v0.29.4
-	k8s.io/client-go v0.29.4
+	k8s.io/api v0.30.1
+	k8s.io/apimachinery v0.30.1
+	k8s.io/client-go v0.30.1
 )
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
-	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
-	github.com/go-logr/logr v1.3.0 // indirect
-	github.com/go-openapi/jsonpointer v0.19.6 // indirect
-	github.com/go-openapi/jsonreference v0.20.2 // indirect
-	github.com/go-openapi/swag v0.22.3 // indirect
+	github.com/emicklei/go-restful/v3 v3.12.0 // indirect
+	github.com/evanphx/json-patch v5.9.0+incompatible // indirect
+	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-openapi/jsonpointer v0.21.0 // indirect
+	github.com/go-openapi/jsonreference v0.21.0 // indirect
+	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/uuid v1.3.0 // indirect
-	github.com/gorilla/websocket v1.5.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/gorilla/websocket v1.5.1 // indirect
 	github.com/imdario/mergo v0.3.13 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
@@ -41,22 +41,22 @@ require (
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	golang.org/x/crypto v0.21.0 // indirect
-	golang.org/x/net v0.23.0 // indirect
-	golang.org/x/oauth2 v0.10.0 // indirect
-	golang.org/x/sys v0.18.0 // indirect
-	golang.org/x/term v0.18.0 // indirect
-	golang.org/x/text v0.14.0 // indirect
-	golang.org/x/time v0.3.0 // indirect
-	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/protobuf v1.33.0 // indirect
+	golang.org/x/crypto v0.23.0 // indirect
+	golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d
+	golang.org/x/net v0.25.0 // indirect
+	golang.org/x/oauth2 v0.20.0 // indirect
+	golang.org/x/sys v0.20.0 // indirect
+	golang.org/x/term v0.20.0 // indirect
+	golang.org/x/text v0.15.0 // indirect
+	golang.org/x/time v0.5.0 // indirect
+	google.golang.org/protobuf v1.34.1 // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	k8s.io/klog/v2 v2.110.1 // indirect
-	k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
-	k8s.io/utils v0.0.0-20230726121419-3b25d923346b
+	k8s.io/klog/v2 v2.120.1 // indirect
+	k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect
+	k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
-	sigs.k8s.io/yaml v1.3.0 // indirect
+	sigs.k8s.io/yaml v1.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 8d979b7..1121ea7 100644
--- a/go.sum
+++ b/go.sum
@@ -1,28 +1,27 @@
 github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
 github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
-github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
-github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
-github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
-github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
-github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
-github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
-github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
-github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
-github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
+github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
+github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
 github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
@@ -39,13 +38,13 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
-github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
 github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
 github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -54,11 +53,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -70,31 +66,26 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
-github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
-github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
-github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
+github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g=
+github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
-github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/slack-go/slack v0.12.5 h1:ddZ6uz6XVaB+3MTDhoW04gG+Vc/M/X1ctC+wssy2cqs=
-github.com/slack-go/slack v0.12.5/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
+github.com/slack-go/slack v0.13.0 h1:7my/pR2ubZJ9912p9FtvALYpbt0cQPAqkRy2jaSI1PQ=
+github.com/slack-go/slack v0.13.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -103,20 +94,23 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
-golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
+golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
+golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
+golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
-golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
-golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
+golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -125,32 +119,29 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
-golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
-golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
-golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
+golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
+golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -167,21 +158,21 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.29.4 h1:WEnF/XdxuCxdG3ayHNRR8yH3cI1B/llkWBma6bq4R3w=
-k8s.io/api v0.29.4/go.mod h1:DetSv0t4FBTcEpfA84NJV3g9a7+rSzlUHk5ADAYHUv0=
-k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q=
-k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y=
-k8s.io/client-go v0.29.4 h1:79ytIedxVfyXV8rpH3jCBW0u+un0fxHDwX5F9K8dPR8=
-k8s.io/client-go v0.29.4/go.mod h1:kC1thZQ4zQWYwldsfI088BbK6RkxK+aF5ebV8y9Q4tk=
-k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
-k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
-k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
-k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
-k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
-k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY=
+k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM=
+k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U=
+k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
+k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q=
+k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc=
+k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
+k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5TxDkI7OGJjBNBzq5fJtLA=
+k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc=
+k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak=
+k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
 sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
 sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
-sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
-sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/handler/executeContainersFilters.go b/handler/executeContainersFilters.go
new file mode 100644
index 0000000..e9d08c0
--- /dev/null
+++ b/handler/executeContainersFilters.go
@@ -0,0 +1,56 @@
+package handler
+
+import (
+	"time"
+
+	"github.com/abahmed/kwatch/filter"
+	"github.com/abahmed/kwatch/storage"
+	"github.com/sirupsen/logrus"
+)
+
+func (h *handler) executeContainersFilters(ctx *filter.Context) {
+	for cIdx := range ctx.Pod.Status.ContainerStatuses {
+		ctx.Container = &filter.ContainerContext{
+			Container:        &ctx.Pod.Status.ContainerStatuses[cIdx],
+			HasRestarts:      false,
+			LastTerminatedOn: time.Time{},
+		}
+
+		isContainerOk := false
+		for i := range h.containerFilters {
+			if shouldStop := h.containerFilters[i].Execute(ctx); shouldStop {
+				isContainerOk = true
+				break
+			}
+		}
+
+		ctx.Memory.AddPodContainer(
+			ctx.Pod.Namespace,
+			ctx.Pod.Name,
+			ctx.Container.Container.Name,
+			&storage.ContainerState{
+				RestartCount:     ctx.Container.Container.RestartCount,
+				LastTerminatedOn: ctx.Container.LastTerminatedOn,
+				Reason:           ctx.Container.Reason,
+				Msg:              ctx.Container.Msg,
+				ExitCode:         ctx.Container.ExitCode,
+				Status:           ctx.Container.Status,
+			})
+
+		if !isContainerOk {
+			ownerName := ""
+			if ctx.Owner != nil {
+				ownerName = ctx.Owner.Name
+			}
+
+			logrus.Printf(
+				"container only issue %s %s %s %s %s %d",
+				ctx.Container.Container.Name,
+				ctx.Pod.Name,
+				ownerName,
+				ctx.Container.Reason,
+				ctx.Container.Msg,
+				ctx.Container.ExitCode)
+		}
+	}
+}
diff --git a/handler/executePodFilters.go b/handler/executePodFilters.go
new file mode 100644
index 0000000..9d0afad
--- /dev/null
+++ b/handler/executePodFilters.go
@@ -0,0 +1,41 @@
+package handler
+
+import (
+	"github.com/abahmed/kwatch/filter"
+	"github.com/abahmed/kwatch/storage"
+	"github.com/sirupsen/logrus"
+)
+
+func (h *handler) executePodFilters(ctx *filter.Context) {
+	isPodOk := false
+	for i := range h.podFilters {
+		if shouldStop := h.podFilters[i].Execute(ctx); shouldStop {
+			isPodOk = true
+			break
+		}
+	}
+
+	if isPodOk ||
+		ctx.ContainersHasIssues ||
+		!ctx.PodHasIssues {
+		return
+	}
+
+	ownerName := ""
+	if ctx.Owner != nil {
+		ownerName = ctx.Owner.Name
+	}
+
+	ctx.Memory.AddPodContainer(
+		ctx.Pod.Namespace,
+		ctx.Pod.Name,
+		".",
+		&storage.ContainerState{
+			Reason: ctx.PodReason,
+			Msg:    ctx.PodMsg,
+			Status: "",
+		},
+	)
+
+	logrus.Printf("pod only issue %s %s %s %s", ctx.Pod.Name, ownerName, ctx.PodReason, ctx.PodMsg)
+}
diff --git a/handler/handler.go b/handler/handler.go
new file mode 100644
index 0000000..c02b6ae
--- /dev/null
+++ b/handler/handler.go
@@ -0,0 +1,56 @@
+package handler
+
+import (
+	"github.com/abahmed/kwatch/alertmanager"
+	"github.com/abahmed/kwatch/config"
+	"github.com/abahmed/kwatch/filter"
+	"github.com/abahmed/kwatch/storage"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+type Handler interface {
+	ProcessPod(evType string, pod *corev1.Pod)
+}
+
+type handler struct {
+	kclient          kubernetes.Interface
+	config           *config.Config
+	memory           storage.Storage
+	podFilters       []filter.Filter
+	containerFilters []filter.Filter
+	alertManager     *alertmanager.AlertManager
+}
+
+func NewHandler(
+	cli kubernetes.Interface,
+	cfg *config.Config,
+	mem storage.Storage,
+	alertManager *alertmanager.AlertManager) Handler {
+	// Order is important
+	podFilters := []filter.Filter{
+		filter.NsFilter{},
+		filter.PodStatusFilter{},
+		filter.PodEventsFilter{},
+		//filter.PodOwnersFilter{},
+	}
+
+	containersFilters := []filter.Filter{
+		filter.ContainerNameFilter{},
+		filter.ContainerRestartsFilter{},
+		filter.ContainerStateFilter{},
+		filter.ContainerKillingFilter{},
+		filter.ContainerReasonsFilter{},
+		filter.ContainerLogsFilter{},
+		filter.PodOwnersFilter{},
+	}
+
+	return &handler{
+		kclient:          cli,
+		config:           cfg,
+		podFilters:       podFilters,
+		containerFilters: containersFilters,
+		memory:           mem,
+		alertManager:     alertManager,
+	}
+}
diff --git a/handler/processPod.go b/handler/processPod.go
new file mode 100644
index 0000000..487b3e9
--- /dev/null
+++ b/handler/processPod.go
@@ -0,0 +1,28 @@
+package handler
+
+import (
+	"github.com/abahmed/kwatch/filter"
+	corev1 "k8s.io/api/core/v1"
+)
+
+func (h *handler) ProcessPod(eventType string, pod *corev1.Pod) {
+	if pod == nil {
+		return
+	}
+
+	if eventType == "DELETED" {
+		h.memory.DelPod(pod.Namespace, pod.Name)
+		return
+	}
+
+	ctx := filter.Context{
+		Client: h.kclient,
+		Config: h.config,
+		Memory: h.memory,
+		Pod:    pod,
+		EvType: eventType,
+	}
+
+	h.executePodFilters(&ctx)
+	h.executeContainersFilters(&ctx)
+}
diff --git a/main.go b/main.go
index c962b50..15dc82b 100644
--- a/main.go
+++ b/main.go
@@ -7,11 +7,14 @@ import (
 	"github.com/abahmed/kwatch/client"
 	"github.com/abahmed/kwatch/config"
 	"github.com/abahmed/kwatch/constant"
-	"github.com/abahmed/kwatch/controller"
+	"github.com/abahmed/kwatch/handler"
 	"github.com/abahmed/kwatch/pvcmonitor"
+	"github.com/abahmed/kwatch/storage/memory"
 	"github.com/abahmed/kwatch/upgrader"
 	"github.com/abahmed/kwatch/version"
+	"github.com/abahmed/kwatch/watcher"
 	"github.com/sirupsen/logrus"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func main() {
@@ -42,10 +45,19 @@ func main() {
 		pvcmonitor.NewPvcMonitor(client, &config.PvcMonitor, &alertManager)
 	go pvcMonitor.Start()
 
-	// start controller
-	controller.Start(
+	// Create handler
+	h := handler.NewHandler(
 		client,
-		&alertManager,
 		config,
+		memory.NewMemory(),
+		&alertManager,
 	)
+
+	namespace := metav1.NamespaceAll
+	if len(config.AllowedNamespaces) == 1 {
+		namespace = config.AllowedNamespaces[0]
+	}
+
+	// start watcher
+	watcher.Start(client, namespace, h.ProcessPod)
 }
diff --git a/storage/memory/memory.go b/storage/memory/memory.go
index e4189b3..a0bef40 100644
--- a/storage/memory/memory.go
+++ b/storage/memory/memory.go
@@ -18,46 +18,72 @@ func NewMemory() storage.Storage {
 }
 
 // AddPodContainer attaches container to pod to mark it has an error
-func (m *memory) AddPodContainer(podKey, containerKey string) {
-	if v, ok := m.smap.Load(podKey); ok {
-		containers := v.(map[string]bool)
-		containers[containerKey] = true
+func (m *memory) AddPodContainer(namespace, podKey, containerKey string, state *storage.ContainerState) {
+	key := m.getKey(namespace, podKey)
+	if v, ok := m.smap.Load(key); ok {
+		containers := v.(map[string]*storage.ContainerState)
+		containers[containerKey] = state
 
-		m.smap.Store(podKey, containers)
+		m.smap.Store(key, containers)
 		return
 	}
-	m.smap.Store(podKey, map[string]bool{containerKey: true})
+	m.smap.Store(key, map[string]*storage.ContainerState{containerKey: state})
 }
 
 // Delete deletes pod with all its containers
-func (m *memory) DelPod(key string) {
+func (m *memory) DelPod(namespace, podKey string) {
+	key := m.getKey(namespace, podKey)
 	m.smap.Delete(key)
 }
 
 // DelPodContainer detaches container from pod to mark error is resolved
-func (m *memory) DelPodContainer(podKey, containerKey string) {
-	v, ok := m.smap.Load(podKey)
+func (m *memory) DelPodContainer(namespace, podKey, containerKey string) {
+	key := m.getKey(namespace, podKey)
+
+	v, ok := m.smap.Load(key)
 	if !ok {
 		return
 	}
 
-	containers := v.(map[string]bool)
+	containers := v.(map[string]*storage.ContainerState)
 	delete(containers, containerKey)
 
-	m.smap.Store(podKey, containers)
+	m.smap.Store(key, containers)
 }
 
 // HasPodContainer checks if container is attached to given pod or not
-func (m *memory) HasPodContainer(podKey, containerKey string) bool {
-	v, ok := m.smap.Load(podKey)
+func (m *memory) HasPodContainer(namespace, podKey, containerKey string) bool {
+	key := m.getKey(namespace, podKey)
+
+	v, ok := m.smap.Load(key)
 	if !ok {
 		return false
 	}
 
-	containers := v.(map[string]bool)
+	containers := v.(map[string]*storage.ContainerState)
 	if _, ok := containers[containerKey]; ok {
 		return true
 	}
 
 	return false
 }
+
+func (m *memory) GetPodContainer(namespace, podKey, containerKey string) *storage.ContainerState {
+	key := m.getKey(namespace, podKey)
+
+	v, ok := m.smap.Load(key)
+	if !ok {
+		return nil
+	}
+
+	containers := v.(map[string]*storage.ContainerState)
+	if val, ok := containers[containerKey]; ok {
+		return val
+	}
+
+	return nil
+}
+
+func (*memory) getKey(namespace, pod string) string {
+	return namespace + "/" + pod
+}
diff --git a/storage/storage.go b/storage/storage.go
index 82697a1..c09db91 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -1,9 +1,22 @@
 package storage
 
+import "time"
+
+type ContainerState struct {
+	RestartCount     int32
+	LastTerminatedOn time.Time
+	Reason           string
+	Msg              string
+	ExitCode         int32
+	Status           string
+	Reported         bool
+}
+
 // Storage interface
 type Storage interface {
-	AddPodContainer(podKey, containerKey string)
-	DelPodContainer(podKey, containerKey string)
-	DelPod(podKey string)
-	HasPodContainer(podKey, containerKey string) bool
+	AddPodContainer(namespace, podKey, containerKey string, state *ContainerState)
+	DelPodContainer(namespace, podKey, containerKey string)
+	DelPod(namespace, podKey string)
+	HasPodContainer(namespace, podKey, containerKey string) bool
+	GetPodContainer(namespace, podKey, containerKey string) *ContainerState
 }
diff --git a/upgrader/upgrader.go b/upgrader/upgrader.go
index a9bbc79..36f0fb8 100644
--- a/upgrader/upgrader.go
+++ b/upgrader/upgrader.go
@@ -29,7 +29,8 @@ func NewUpgrader(config *config.Upgrader,
 
 // CheckUpdates checks every 24 hours if a newer version of Kwatch is available
 func (u *Upgrader) CheckUpdates() {
-	if u.config.DisableUpdateCheck {
+	if u.config.DisableUpdateCheck ||
+		version.Short() == "dev" {
 		return
 	}
 
diff --git a/util/util.go b/util/util.go
index fc3a19b..2355c86 100644
--- a/util/util.go
+++ b/util/util.go
@@ -18,7 +18,7 @@ import (
 
 // GetPodEventsStr returns formatted events as a string for specified pod
 func GetPodEventsStr(c kubernetes.Interface, name, namespace string) string {
-	events, err := getPodEvents(c, name, namespace)
+	events, err := GetPodEvents(c, name, namespace)
 
 	eventsString := ""
 	if err != nil {
@@ -49,7 +49,7 @@ func ContainsKillingStoppingContainerEvents(
 	c kubernetes.Interface,
 	name,
 	namespace string) bool {
-	events, err := getPodEvents(c, name, namespace)
+	events, err := GetPodEvents(c, name, namespace)
 	if err != nil {
 		return false
 	}
@@ -120,7 +120,7 @@ func getContainerLogs(
 		DoRaw(context.TODO())
 }
 
-func getPodEvents(
+func GetPodEvents(
 	c kubernetes.Interface,
 	name,
 	namespace string) (*v1.EventList, error) {
@@ -180,9 +180,9 @@ func RandomString(n int) string {
 		"NOPQRSTUVWXYZ0123456789"
 
 	b := make([]byte, n)
-	rand.Seed(time.Now().UnixNano())
+	r := rand.New(rand.NewSource(time.Now().UnixNano()))
 	for i := range b {
-		b[i] = availableCharacterBytes[rand.Intn(len(availableCharacterBytes))]
+		b[i] = availableCharacterBytes[r.Intn(len(availableCharacterBytes))]
 	}
 
 	return string(b)
diff --git a/watcher/start.go b/watcher/start.go
new file mode 100644
index 0000000..147cb77
--- /dev/null
+++ b/watcher/start.go
@@ -0,0 +1,45 @@
+package watcher
+
+import (
+	"context"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/tools/cache"
+	toolsWatch "k8s.io/client-go/tools/watch"
+	"k8s.io/client-go/util/workqueue"
+)
+
+// Start creates an instance of watcher after initialization and runs it
+func Start(
+	client kubernetes.Interface,
+	namespace string,
+	handleFunc func(string, *corev1.Pod)) {
+	watchFunc :=
+		func(options metav1.ListOptions) (watch.Interface, error) {
+			return client.CoreV1().Pods(namespace).Watch(
+				context.Background(),
+				metav1.ListOptions{},
+			)
+		}
+
+	watcher, _ :=
+		toolsWatch.NewRetryWatcher(
+			"1",
+			&cache.ListWatch{WatchFunc: watchFunc},
+		)
+
+	w := &Watcher{
+		watcher:     watcher,
+		queue:       workqueue.New(),
+		handlerFunc: handleFunc,
+	}
+
+	stopCh := make(chan struct{})
+	defer close(stopCh)
+
+	w.run(stopCh)
+}
diff --git a/watcher/watcher.go b/watcher/watcher.go
new file mode 100644
index 0000000..6e72c99
--- /dev/null
+++ b/watcher/watcher.go
@@ -0,0 +1,76 @@
+package watcher
+
+import (
+	"time"
+
+	"github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/apimachinery/pkg/util/wait"
+	toolsWatch "k8s.io/client-go/tools/watch"
+	"k8s.io/client-go/util/workqueue"
+)
+
+type watcherEvent struct {
+	eventType string
+	pod       *corev1.Pod
+}
+
+type Watcher struct {
+	watcher     *toolsWatch.RetryWatcher
+	queue       *workqueue.Type
+	handlerFunc func(string, *corev1.Pod)
+}
+
+// run starts the watcher
+func (w *Watcher) run(stopCh chan struct{}) {
+	defer utilruntime.HandleCrash()
+	defer w.queue.ShutDown()
+
+	logrus.Info("starting pod watcher")
+
+	go wait.Until(w.processEvents, time.Second, stopCh)
+	go wait.Until(w.runWorker, time.Second, stopCh)
+
+	<-stopCh
+}
+
+func (w *Watcher) processEvents() {
+	if w.watcher == nil {
+		return
+	}
+
+	for event := range w.watcher.ResultChan() {
+		pod := event.Object.(*corev1.Pod)
+		w.queue.Add(watcherEvent{
+			eventType: string(event.Type),
+			pod:       pod.DeepCopy(),
+		})
+	}
+}
+
+func (w *Watcher) runWorker() {
+	for w.processNextItem() {
+		// continue looping
+	}
+}
+
+func (w *Watcher) processNextItem() bool {
+	newEvent, quit := w.queue.Get()
+
+	if quit {
+		return false
+	}
+
+	defer w.queue.Done(newEvent)
+
+	ev, ok := newEvent.(watcherEvent)
+	if !ok {
+		logrus.Errorf("failed to cast watcher event: %v", ev)
+		return true
+	}
+
+	w.handlerFunc(ev.eventType, ev.pod)
+
+	return true
+}


Test patch:
diff --git a/storage/memory/memory_test.go b/storage/memory/memory_test.go
index 49221ab..3db9652 100644
--- a/storage/memory/memory_test.go
+++ b/storage/memory/memory_test.go
@@ -20,19 +20,19 @@ func TestAddPodContainer(t *testing.T) {
 		smap: sync.Map{},
 	}
 
-	mem.AddPodContainer("test", "test")
-	mem.AddPodContainer("test", "test2")
+	mem.AddPodContainer("default", "test", "container1", &storage.ContainerState{})
+	mem.AddPodContainer("default", "test", "container2", &storage.ContainerState{})
 
-	if v, ok := mem.smap.Load("test"); !ok {
+	if v, ok := mem.smap.Load(mem.getKey("default", "test")); !ok {
 		t.Errorf("expected to find value in pod test")
 	} else {
-		containers := v.(map[string]bool)
-		if _, ok = containers["test"]; !ok {
-			t.Errorf("expected to find container test in pod test")
+		containers := v.(map[string]*storage.ContainerState)
+		if _, ok = containers["container1"]; !ok {
+			t.Errorf("expected to find container container1 in pod test")
 		}
 
-		if _, ok = containers["test2"]; !ok {
-			t.Errorf("expected to find container test2 in pod test")
+		if _, ok = containers["container2"]; !ok {
+			t.Errorf("expected to find container container2 in pod test")
 		}
 	}
 }
@@ -42,25 +42,25 @@ func TestHasPodContainer(t *testing.T) {
 		smap: sync.Map{},
 	}
 
-	mem.AddPodContainer("test", "test")
-	mem.AddPodContainer("test", "test2")
+	mem.AddPodContainer("default", "test", "test", &storage.ContainerState{})
+	mem.AddPodContainer("default", "test", "test2", &storage.ContainerState{})
 
-	mem.DelPodContainer("test", "test")
-	mem.DelPodContainer("test3", "test")
+	mem.DelPodContainer("default", "test", "test")
+	mem.DelPodContainer("default", "test3", "test")
 
-	if !mem.HasPodContainer("test", "test2") {
+	if !mem.HasPodContainer("default", "test", "test2") {
 		t.Errorf("expected to find container test2 in pod test")
 	}
 
-	if mem.HasPodContainer("test", "test") {
+	if mem.HasPodContainer("default", "test", "test") {
 		t.Errorf("expected not to find container test in pod test")
 	}
 
-	if mem.HasPodContainer("test", "test6") {
+	if mem.HasPodContainer("default", "test", "test6") {
 		t.Errorf("expected not to find container test6 in pod test")
 	}
 
-	if mem.HasPodContainer("test4", "test") {
+	if mem.HasPodContainer("default", "test4", "test") {
 		t.Errorf("expected to not find container test in pod test4")
 	}
 }
@@ -70,34 +70,58 @@ func TestDelPodContainer(t *testing.T) {
 		smap: sync.Map{},
 	}
 
-	mem.AddPodContainer("test", "test")
-	mem.AddPodContainer("test", "test2")
+	mem.AddPodContainer("default", "test", "test", &storage.ContainerState{})
+	mem.AddPodContainer("default", "test", "test2", &storage.ContainerState{})
 
-	mem.DelPodContainer("test", "test")
-	mem.DelPodContainer("test3", "test")
+	mem.DelPodContainer("default", "test", "test")
+	mem.DelPodContainer("default", "test3", "test")
 
-	if v, ok := mem.smap.Load("test"); !ok {
+	if v, ok := mem.smap.Load(mem.getKey("default", "test")); !ok {
 		t.Errorf("expected to find value in pod test")
 	} else {
-		containers := v.(map[string]bool)
+		containers := v.(map[string]*storage.ContainerState)
 		if _, ok = containers["test"]; ok {
 			t.Errorf("expected not to find container test in pod test")
 		}
 	}
 }
 
+func TestGetPodContainer(t *testing.T) {
+	mem := &memory{
+		smap: sync.Map{},
+	}
+
+	mem.AddPodContainer("default", "test", "test1", &storage.ContainerState{})
+	mem.AddPodContainer("default", "test", "test2", &storage.ContainerState{})
+
+	state := mem.GetPodContainer("default", "test", "test1")
+	if state == nil {
+		t.Errorf("expected to find value in pod test")
+	}
+
+	state2 := mem.GetPodContainer("default", "test", "test3")
+	if state2 != nil {
+		t.Errorf("expected to be nil as container does not exist")
+	}
+
+	state3 := mem.GetPodContainer("default", "test3", "test1")
+	if state3 != nil {
+		t.Errorf("expected to be nil as pod does not exist")
+	}
+}
+
 func TestDelPod(t *testing.T) {
 	mem := &memory{
 		smap: sync.Map{},
 	}
 
-	mem.AddPodContainer("test", "test")
-	mem.AddPodContainer("test", "test2")
+	mem.AddPodContainer("default", "test", "test1", &storage.ContainerState{})
+	mem.AddPodContainer("default", "test", "test2", &storage.ContainerState{})
 
-	mem.DelPod("test")
-	mem.DelPod("test3")
+	mem.DelPod("default", "test")
+	mem.DelPod("default", "test3")
 
-	if _, ok := mem.smap.Load("test"); ok {
+	if _, ok := mem.smap.Load(mem.getKey("default", "test")); ok {
 		t.Errorf("expected not to find pod test")
 	}
 }