Repository: abahmed/kwatch (go)

Issue:
load configuration into struct
**Is your feature request related to a problem? Please describe.**
Configuration is accessed as string keys and this may lead to issues while accessing it if there's a typo in key

**Describe the solution you'd like**
Load config into shared struct across the app

Pull request:
🚀 Load configuration into struct
Fixes #146 

Changes proposed in this pull request:
- Load configuration into struct 

Patch:
diff --git a/.gitignore b/.gitignore
index 8cf5612..9491ca1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,9 @@ kwatch
 # Dependency directories (remove the comment below to include it)
 # vendor/
 
+# Config files
+config.yaml
+
 ### Go Patch ###
 /vendor/
 /Godeps/
diff --git a/config.yaml b/config.yaml
deleted file mode 100644
index 6082fd3..0000000
--- a/config.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-maxRecentLogLines: 0
-alert:
-  slack:
-    webhook: ""
-  pagerduty:
-    integrationKey: ""
-  discord:
-    webhook: ""
-  telegram:
-    token: ""
-    chatId: ""
-  email:
-    form: ""
-    password: ""
-    host: ""
-    port: ""
-    to: ""
-  teams:
-    webhook: ""
-  rocketchat:
-    webhook: ""
-  mattermost:
-    webhook: ""
-  opsgenie:
-    apiKey: ""
-namespaces:
-  - default
\ No newline at end of file
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..7f6021c
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,112 @@
+package config
+
+import (
+	"os"
+	"strings"
+
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/viper"
+)
+
+type Config struct {
+	// MaxRecentLogLines optional max tail log lines in messages,
+	// if it's not provided it will get all log lines
+	MaxRecentLogLines int `mapstructure:"maxRecentLogLines"`
+
+	// IgnoreFailedGracefulShutdown if set to true, containers which are
+	// forcefully killed during shutdown (as their graceful shutdown failed)
+	// are not reported as error
+	IgnoreFailedGracefulShutdown bool `mapstructure:"ignoreFailedGracefulShutdown"`
+
+	// DisableUpdateCheck if set to true, does not check for and
+	// notify about kwatch updates
+	DisableUpdateCheck bool `mapstructure:"DisableUpdateCheck"`
+
+	// Namespaces is an optional list of namespaces that you want to watch or
+	// forbid, if it's not provided it will watch all namespaces.
+	// If you want to forbid a namespace, configure it with !<namespace name>
+	// You can either set forbidden namespaces or allowed, not both
+	Namespaces []string `mapstructure:"namespaces"`
+
+	// Reasons is an  optional list of reasons that you want to watch or forbid,
+	// if it's not provided it will watch all reasons.
+	// If you want to forbid a reason, configure it with !<reason>
+	// You can either set forbidden reasons or allowed, not both
+	Reasons []string `mapstructure:"reasons"`
+
+	// IgnoreContainerNames optional list of container names to ignore
+	IgnoreContainerNames []string `mapstructure:"ignoreContainerNames"`
+
+	// Alert is a map contains a map of each provider configuration
+	// e.g. {"slack": {"webhook": "URL"}}
+	Alert map[string]map[string]string `mapstructure:"alert"`
+
+	// AllowedNamespaces, ForbiddenNamespaces are calculated internally
+	// after loading Namespaces configuration
+	AllowedNamespaces   []string
+	ForbiddenNamespaces []string
+
+	// AllowedReasons, ForbiddenReasons are calculated internally after loading
+	// Reasons configuration
+	AllowedReasons   []string
+	ForbiddenReasons []string
+}
+
+// LoadConfig loads yaml configuration from file if provided, otherwise
+// loads default configuration
+func LoadConfig() (*Config, error) {
+	// initialize configuration
+	configFile := os.Getenv("CONFIG_FILE")
+	if len(configFile) != 0 {
+		viper.SetConfigFile(configFile)
+	}
+	viper.AutomaticEnv()
+
+	// if a config file is found, read it in.
+	if err := viper.ReadInConfig(); err == nil {
+		logrus.Infof("using config file: %s", viper.ConfigFileUsed())
+	} else {
+		logrus.Warnf("unable to load config file: %s", err.Error())
+	}
+
+	// Load config
+	var config Config
+	if err := viper.Unmarshal(&config); err != nil {
+		return nil, err
+	}
+
+	// Parse namespace allow/forbid lists
+	config.AllowedNamespaces, config.ForbiddenNamespaces =
+		getAllowForbidSlices(config.Namespaces)
+	if len(config.AllowedNamespaces) > 0 &&
+		len(config.ForbiddenNamespaces) > 0 {
+		logrus.Fatal(
+			"Either allowed or forbidden namespaces must be set. " +
+				"Can't set both")
+	}
+
+	// Parse reason allow/forbid lists
+	config.AllowedReasons, config.ForbiddenReasons =
+		getAllowForbidSlices(config.Reasons)
+	if len(config.AllowedReasons) > 0 &&
+		len(config.ForbiddenReasons) > 0 {
+		logrus.Fatal("Either allowed or forbidden reasons must be set. " +
+			"Can't set both")
+	}
+
+	return &config, nil
+}
+
+// getAllowForbidSlices split input slice into two slices by items start with !
+func getAllowForbidSlices(items []string) (allow []string, forbid []string) {
+	allow = make([]string, 0)
+	forbid = make([]string, 0)
+	for _, item := range items {
+		if clean := strings.TrimPrefix(item, "!"); item != clean {
+			forbid = append(forbid, clean)
+			continue
+		}
+		allow = append(allow, item)
+	}
+	return allow, forbid
+}
diff --git a/controller/controller.go b/controller/controller.go
index 2a14569..f0be447 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/abahmed/kwatch/config"
 	"github.com/abahmed/kwatch/constant"
 	"github.com/abahmed/kwatch/event"
 	"github.com/abahmed/kwatch/provider"
@@ -21,19 +22,14 @@ import (
 
 // Controller holds necessary
 type Controller struct {
-	name                         string
-	informer                     cache.Controller
-	indexer                      cache.Indexer
-	kclient                      kubernetes.Interface
-	queue                        workqueue.RateLimitingInterface
-	providers                    []provider.Provider
-	store                        storage.Storage
-	ignoreFailedGracefulShutdown bool
-	namespaceAllowList           []string
-	namespaceForbidList          []string
-	reasonAllowList              []string
-	reasonForbidList             []string
-	ignoreContainerList          []string
+	name      string
+	informer  cache.Controller
+	indexer   cache.Indexer
+	kclient   kubernetes.Interface
+	queue     workqueue.RateLimitingInterface
+	providers []provider.Provider
+	store     storage.Storage
+	config    *config.Config
 }
 
 // run starts the controller
@@ -54,7 +50,7 @@ func (c *Controller) run(workers int, stopCh chan struct{}) {
 	logrus.Infof("%s controller synced and ready", c.name)
 
 	// send notification to providers
-	util.SendProvidersMsg(
+	provider.SendProvidersMsg(
 		c.providers,
 		fmt.Sprintf(constant.WelcomeMsg, constant.Version),
 	)
@@ -129,15 +125,15 @@ func (c *Controller) processItem(key string) error {
 	}
 
 	// filter by namespaces in config if specified
-	if len(c.namespaceAllowList) > 0 &&
-		!util.IsStrInSlice(pod.Namespace, c.namespaceAllowList) {
+	if len(c.config.AllowedNamespaces) > 0 &&
+		!util.IsStrInSlice(pod.Namespace, c.config.AllowedNamespaces) {
 		logrus.Infof(
 			"skip namespace %s as not in namespace allow list",
 			pod.Namespace)
 		return nil
 	}
-	if len(c.namespaceForbidList) > 0 &&
-		util.IsStrInSlice(pod.Namespace, c.namespaceForbidList) {
+	if len(c.config.ForbiddenNamespaces) > 0 &&
+		util.IsStrInSlice(pod.Namespace, c.config.ForbiddenNamespaces) {
 		logrus.Infof(
 			"skip namespace %s as in namespace forbid list",
 			pod.Namespace)
@@ -185,7 +181,7 @@ func (c *Controller) processPod(key string, pod *v1.Pod) {
 			continue
 		}
 
-		if c.ignoreFailedGracefulShutdown &&
+		if c.config.IgnoreFailedGracefulShutdown &&
 			util.ContainsKillingStoppingContainerEvents(
 				c.kclient,
 				pod.Name,
@@ -210,19 +206,19 @@ func (c *Controller) processPod(key string, pod *v1.Pod) {
 			reason = container.State.Terminated.Reason
 		}
 
-		if len(c.reasonAllowList) > 0 &&
-			!util.IsStrInSlice(reason, c.reasonAllowList) {
+		if len(c.config.AllowedReasons) > 0 &&
+			!util.IsStrInSlice(reason, c.config.AllowedReasons) {
 			logrus.Infof("skip reason %s as not in reason allow list", reason)
 			return
 		}
-		if len(c.reasonForbidList) > 0 &&
-			util.IsStrInSlice(reason, c.reasonForbidList) {
+		if len(c.config.ForbiddenReasons) > 0 &&
+			util.IsStrInSlice(reason, c.config.ForbiddenReasons) {
 			logrus.Infof("skip reason %s as in reason forbid list", reason)
 			return
 		}
 
-		if len(c.ignoreContainerList) > 0 &&
-			util.IsStrInSlice(container.Name, c.ignoreContainerList) {
+		if len(c.config.IgnoreContainerNames) > 0 &&
+			util.IsStrInSlice(container.Name, c.config.IgnoreContainerNames) {
 			logrus.Infof(
 				"skip pod %s as in container ignore list",
 				container.Name)
@@ -261,6 +257,6 @@ func (c *Controller) processPod(key string, pod *v1.Pod) {
 		c.store.AddPodContainer(key, container.Name)
 
 		// send event to providers
-		util.SendProvidersEvent(c.providers, evnt)
+		provider.SendProvidersEvent(c.providers, evnt)
 	}
 }
diff --git a/controller/start.go b/controller/start.go
index 22a337c..f37732f 100644
--- a/controller/start.go
+++ b/controller/start.go
@@ -4,6 +4,7 @@ import (
 	"context"
 
 	"github.com/abahmed/kwatch/client"
+	"github.com/abahmed/kwatch/config"
 	"github.com/abahmed/kwatch/constant"
 	"github.com/abahmed/kwatch/provider"
 	memory "github.com/abahmed/kwatch/storage/memory"
@@ -17,9 +18,9 @@ import (
 )
 
 // Start creates an instance of controller after initialization and runs it
-func Start(providers []provider.Provider, ignoreFailedGracefulShutdown bool,
-	namespaceAllowList, namespaceForbidList,
-	reasonAllowList, reasonForbidList []string, ignoreContainerList []string) {
+func Start(
+	providers []provider.Provider,
+	config *config.Config) {
 	// create kubernetes client
 	kclient := client.Create()
 
@@ -31,8 +32,8 @@ func Start(providers []provider.Provider, ignoreFailedGracefulShutdown bool,
 	var namespaceToWatch = v1.NamespaceAll
 
 	// if there is exactly 1 namespace listen only to that namespace for events
-	if len(namespaceAllowList) == 1 {
-		namespaceToWatch = namespaceAllowList[0]
+	if len(config.AllowedNamespaces) == 1 {
+		namespaceToWatch = config.AllowedNamespaces[0]
 	}
 
 	indexer, informer := cache.NewIndexerInformer(
@@ -77,20 +78,14 @@ func Start(providers []provider.Provider, ignoreFailedGracefulShutdown bool,
 		}, cache.Indexers{})
 
 	controller := Controller{
-		name:                         "pod-crash",
-		informer:                     informer,
-		indexer:                      indexer,
-		queue:                        queue,
-		kclient:                      kclient,
-		providers:                    providers,
-		store:                        memory.NewMemory(),
-		ignoreFailedGracefulShutdown: ignoreFailedGracefulShutdown,
-
-		namespaceAllowList:  namespaceAllowList,
-		namespaceForbidList: namespaceForbidList,
-		reasonAllowList:     reasonAllowList,
-		reasonForbidList:    reasonForbidList,
-		ignoreContainerList: ignoreContainerList,
+		name:      "pod-crash",
+		informer:  informer,
+		indexer:   indexer,
+		queue:     queue,
+		kclient:   kclient,
+		providers: providers,
+		store:     memory.NewMemory(),
+		config:    config,
 	}
 
 	stopCh := make(chan struct{})
diff --git a/deploy/config.yaml b/deploy/config.yaml
index 6baa96c..b3dc7b8 100644
--- a/deploy/config.yaml
+++ b/deploy/config.yaml
@@ -24,10 +24,10 @@ data:
           chatId: <chat_id>
       email:
         form: <from_mail>
+        to: <to_mail>
         password: <password>
         host: <host>
         port: <port_number>
-        to: <to_mail>
       teams:
           webhook: <webhook_url>
       rocketchat:
@@ -38,4 +38,8 @@ data:
         apiKey: <api_key>
     namespaces:
       - <optional_namespace>
+    reasons:
+      - <optional_reason>
+    ignoreContainerNames:
+      - <optional_container_name>
 
diff --git a/main.go b/main.go
index a0babb6..97a2338 100644
--- a/main.go
+++ b/main.go
@@ -2,87 +2,34 @@ package main
 
 import (
 	"fmt"
-	"os"
-	"path/filepath"
-	"strings"
 
+	"github.com/abahmed/kwatch/config"
 	"github.com/abahmed/kwatch/constant"
 	"github.com/abahmed/kwatch/controller"
+	"github.com/abahmed/kwatch/provider"
 	"github.com/abahmed/kwatch/upgrader"
-	"github.com/abahmed/kwatch/util"
 	"github.com/sirupsen/logrus"
-	"github.com/spf13/viper"
 )
 
 func main() {
 	logrus.Infof(fmt.Sprintf(constant.WelcomeMsg, constant.Version))
 
-	// initialize configuration
-	configFile := os.Getenv("CONFIG_FILE")
-	if len(configFile) == 0 {
-		configFile = filepath.Join("config.yaml")
-	}
-	viper.SetConfigFile(configFile)
-	viper.AutomaticEnv()
-
-	// if a config file is found, read it in.
-	if err := viper.ReadInConfig(); err == nil {
-		logrus.Infof("using config file: %s", viper.ConfigFileUsed())
-	} else {
-		logrus.Warnf("unable to load config file: %s", err.Error())
+	config, err := config.LoadConfig()
+	if err != nil {
+		logrus.Fatalf("failed to load config: %s", err.Error())
 	}
 
 	// get providers
-	providers := util.GetProviders()
+	providers := provider.GetProviders(config)
 
 	// check and notify if newer versions are available
-	if !viper.GetBool("disableUpdateCheck") {
+	if !config.DisableUpdateCheck {
 		go upgrader.CheckUpdates(providers)
 	}
 
-	// Parse namespace allow/forbid lists
-	namespaceAllowList := make([]string, 0)
-	namespaceForbidList := make([]string, 0)
-	for _, namespace := range viper.GetStringSlice("namespaces") {
-		if clean := strings.TrimPrefix(namespace, "!"); namespace != clean {
-			namespaceForbidList = append(namespaceForbidList, clean)
-			continue
-		}
-		namespaceAllowList = append(namespaceAllowList, namespace)
-	}
-	if len(namespaceAllowList) > 0 && len(namespaceForbidList) > 0 {
-		logrus.Fatal(
-			"Either allowed or forbidden namespaces must be set. " +
-				"Can't set both")
-	}
-
-	// Parse reason allow/forbid lists
-	reasonAllowList := make([]string, 0)
-	reasonForbidList := make([]string, 0)
-	for _, namespace := range viper.GetStringSlice("reasons") {
-		if clean := strings.TrimPrefix(namespace, "!"); namespace != clean {
-			reasonForbidList = append(reasonForbidList, clean)
-			continue
-		}
-		reasonAllowList = append(reasonAllowList, namespace)
-	}
-	if len(reasonAllowList) > 0 && len(reasonForbidList) > 0 {
-		logrus.Fatal("Either allowed or forbidden reasons must be set. " +
-			"Can't set both")
-	}
-
-	ignoreContainerList := make([]string, 0)
-	ignoreContainerList = append(
-		ignoreContainerList,
-		viper.GetStringSlice("ignoreContainerNames")...)
-
 	// start controller
 	controller.Start(
 		providers,
-		viper.GetBool("ignoreFailedGracefulShutdown"),
-		namespaceAllowList,
-		namespaceForbidList,
-		reasonAllowList,
-		reasonForbidList,
-		ignoreContainerList)
+		config,
+	)
 }
diff --git a/provider/discord.go b/provider/discord.go
index 9442e10..f53a25c 100644
--- a/provider/discord.go
+++ b/provider/discord.go
@@ -16,24 +16,24 @@ type discord struct {
 }
 
 // NewDiscord returns new Discord instance
-func NewDiscord(url string) Provider {
-	if len(url) == 0 {
+func NewDiscord(config map[string]string) Provider {
+	webhook, ok := config["webhook"]
+	if !ok || len(webhook) == 0 {
 		logrus.Warnf("initializing discord with empty webhook url")
-	} else {
-		logrus.Infof("initializing discord with webhook url: %s", url)
+		return nil
 	}
 
-	webhookToken := ""
-	webhookID := ""
+	logrus.Infof("initializing discord with webhook url: %s", webhook)
 
-	webhookList := strings.Split(url, "/")
-	if len(webhookList) > 1 {
-		webhookToken = webhookList[len(webhookList)-1]
-		webhookID = webhookList[len(webhookList)-2]
-	} else {
+	webhookList := strings.Split(webhook, "/")
+	if len(webhookList) <= 1 {
 		logrus.Warnf("initializing discord with missing id or token")
+		return nil
 	}
 
+	webhookToken := webhookList[len(webhookList)-1]
+	webhookID := webhookList[len(webhookList)-2]
+
 	return &discord{
 		id:    webhookID,
 		token: webhookToken,
diff --git a/provider/email.go b/provider/email.go
index 3c6e717..a66e49e 100644
--- a/provider/email.go
+++ b/provider/email.go
@@ -3,6 +3,7 @@ package provider
 import (
 	"fmt"
 	"math"
+	"strconv"
 	"strings"
 
 	"github.com/abahmed/kwatch/event"
@@ -19,34 +20,52 @@ type email struct {
 }
 
 // NewEmail returns new email instance
-func NewEmail(
-	from string,
-	password string,
-	host string,
-	port int,
-	to string) Provider {
-	if len(from) == 0 {
+func NewEmail(config map[string]string) Provider {
+	from, ok := config["from"]
+	if !ok || len(from) == 0 {
 		logrus.Warnf("initializing email with an empty from")
+		return nil
 	}
-	if len(password) == 0 {
+
+	to, ok := config["to"]
+	if !ok || len(to) == 0 {
+		logrus.Warnf("initializing email with an empty to")
+		return nil
+	}
+
+	password, ok := config["password"]
+	if !ok || len(password) == 0 {
 		logrus.Warnf("initializing email with an empty password")
+		return nil
 	}
-	if len(host) == 0 {
+
+	host, ok := config["host"]
+	if !ok || len(host) == 0 {
 		logrus.Warnf("initializing email with an empty host")
+		return nil
 	}
-	if port > math.MaxUint16 {
-		logrus.Warnf("initializing email with an invalid port number")
+
+	port, ok := config["port"]
+	if !ok || len(port) == 0 {
+		logrus.Warnf("initializing email with an empty port number")
+		return nil
 	}
-	if len(to) == 0 {
-		logrus.Warnf("initializing email with an empty to")
+	portNumber, err := strconv.Atoi(port)
+	if err != nil {
+		logrus.Warnf("initializing email with an invalid port number: %s", err)
+		return nil
+	}
+
+	if portNumber > math.MaxUint16 {
+		logrus.Warnf("initializing email with an invalid range for port number")
 	}
 
 	return &email{
 		from:     from,
+		to:       to,
 		password: password,
 		host:     host,
-		port:     port,
-		to:       to,
+		port:     portNumber,
 	}
 }
 
diff --git a/provider/mattermost.go b/provider/mattermost.go
index 8c489bf..f7958f0 100644
--- a/provider/mattermost.go
+++ b/provider/mattermost.go
@@ -40,15 +40,17 @@ type mmPayload struct {
 }
 
 // NewMattermost returns new mattermost instance
-func NewMattermost(url string) Provider {
-	if len(url) == 0 {
+func NewMattermost(config map[string]string) Provider {
+	webhook, ok := config["webhook"]
+	if !ok || len(webhook) == 0 {
 		logrus.Warnf("initializing mattermost with empty webhook url")
-	} else {
-		logrus.Infof("initializing mattermost with webhook url: %s", url)
+		return nil
 	}
 
+	logrus.Infof("initializing mattermost with webhook url: %s", webhook)
+
 	return &mattermost{
-		webhook: url,
+		webhook: webhook,
 	}
 }
 
diff --git a/provider/opsgenie.go b/provider/opsgenie.go
index 7d6ebc3..4089d77 100644
--- a/provider/opsgenie.go
+++ b/provider/opsgenie.go
@@ -31,15 +31,17 @@ type ogPayload struct {
 }
 
 // NewOpsgenie returns new opsgenie instance
-func NewOpsgenie(apikey string) Provider {
-	if len(apikey) == 0 {
+func NewOpsgenie(config map[string]string) Provider {
+	apiKey, ok := config["apiKey"]
+	if !ok || len(apiKey) == 0 {
 		logrus.Warnf("initializing opsgenie with empty webhook url")
-	} else {
-		logrus.Infof("initializing opsgenie with secret apikey")
+		return nil
 	}
 
+	logrus.Infof("initializing opsgenie with secret apiKey")
+
 	return &opsgenie{
-		apikey: apikey,
+		apikey: apiKey,
 	}
 }
 
diff --git a/provider/pagerduty.go b/provider/pagerduty.go
index 4b09934..f77b823 100644
--- a/provider/pagerduty.go
+++ b/provider/pagerduty.go
@@ -22,13 +22,15 @@ type pagerduty struct {
 }
 
 // NewPagerDuty returns new PagerDuty instance
-func NewPagerDuty(integrationKey string) Provider {
-	if len(integrationKey) == 0 {
+func NewPagerDuty(config map[string]string) Provider {
+	integrationKey, ok := config["integrationKey"]
+	if !ok || len(integrationKey) == 0 {
 		logrus.Warnf("initializing pagerduty with an empty integration key")
-	} else {
-		logrus.Infof("initializing pagerduty with the provided integration key")
+		return nil
 	}
 
+	logrus.Infof("initializing pagerduty with the provided integration key")
+
 	return &pagerduty{
 		integrationKey: integrationKey,
 	}
diff --git a/provider/provider.go b/provider/provider.go
index fc1d6f7..55dfdeb 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -1,6 +1,12 @@
 package provider
 
-import "github.com/abahmed/kwatch/event"
+import (
+	"strings"
+
+	"github.com/abahmed/kwatch/config"
+	"github.com/abahmed/kwatch/event"
+	"github.com/sirupsen/logrus"
+)
 
 const (
 	footer       = "<https://github.com/abahmed/kwatch|kwatch>"
@@ -14,3 +20,75 @@ type Provider interface {
 	SendEvent(*event.Event) error
 	SendMessage(string) error
 }
+
+// GetProviders returns slice of provider objects after parsing config
+func GetProviders(config *config.Config) []Provider {
+	providers := make([]Provider, 0)
+
+	if config.Alert == nil {
+		return providers
+	}
+
+	for k, v := range config.Alert {
+		lowerCaseKey := strings.ToLower(k)
+		var pvdr func(map[string]string) Provider
+		if lowerCaseKey == "slack" {
+			pvdr = NewSlack
+		} else if lowerCaseKey == "pagerduty" {
+			pvdr = NewPagerDuty
+		} else if lowerCaseKey == "discord" {
+			pvdr = NewDiscord
+		} else if lowerCaseKey == "telegram" {
+			pvdr = NewTelegram
+		} else if lowerCaseKey == "teams" {
+			pvdr = NewTeams
+		} else if lowerCaseKey == "email" {
+			pvdr = NewEmail
+		} else if lowerCaseKey == "rocketchat" {
+			pvdr = NewRocketChat
+		} else if lowerCaseKey == "mattermost" {
+			pvdr = NewMattermost
+		} else if lowerCaseKey == "opsgenie" {
+			pvdr = NewOpsgenie
+		}
+
+		if pvdr != nil {
+			instance := pvdr(v)
+			if instance != nil {
+				providers = append(
+					providers, instance)
+			}
+		}
+	}
+
+	return providers
+}
+
+// SendProvidersMsg sends string msg to all providers
+func SendProvidersMsg(p []Provider, msg string) {
+	logrus.Infof("sending message: %s", msg)
+	for _, prv := range p {
+		err :=
+			prv.SendMessage(msg)
+		if err != nil {
+			logrus.Errorf(
+				"failed to send msg with %s: %s",
+				prv.Name(),
+				err.Error())
+		}
+	}
+}
+
+// SendProvidersEvent sends event to all providers
+func SendProvidersEvent(p []Provider, event event.Event) {
+	logrus.Infof("sending event: %+v", event)
+	for _, prv := range p {
+		if err := prv.SendEvent(&event); err != nil {
+			logrus.Errorf(
+				"failed to send event with %s: %s",
+				prv.Name(),
+				err.Error(),
+			)
+		}
+	}
+}
diff --git a/provider/rocketchat.go b/provider/rocketchat.go
index 6a75d2d..84f8b5d 100644
--- a/provider/rocketchat.go
+++ b/provider/rocketchat.go
@@ -28,15 +28,17 @@ type rocketChatWebhookPayload struct {
 }
 
 // NewRocketChat returns new rocket chat instance
-func NewRocketChat(url string) Provider {
-	if len(url) == 0 {
+func NewRocketChat(config map[string]string) Provider {
+	webhook, ok := config["webhook"]
+	if !ok || len(webhook) == 0 {
 		logrus.Warnf("initializing Rocket Chat with empty webhook url")
-	} else {
-		logrus.Infof("initializing Rocket Chat with webhook url: %s", url)
+		return nil
 	}
 
+	logrus.Infof("initializing Rocket Chat with webhook url: %s", webhook)
+
 	return &rocketChat{
-		webhook: url,
+		webhook: webhook,
 	}
 }
 
diff --git a/provider/slack.go b/provider/slack.go
index 2f315f9..8e32294 100644
--- a/provider/slack.go
+++ b/provider/slack.go
@@ -16,15 +16,17 @@ type slack struct {
 }
 
 // NewSlack returns new Slack instance
-func NewSlack(url string) Provider {
-	if len(url) == 0 {
+func NewSlack(config map[string]string) Provider {
+	webhook, ok := config["webhook"]
+	if !ok || len(webhook) == 0 {
 		logrus.Warnf("initializing slack with empty webhook url")
-	} else {
-		logrus.Infof("initializing slack with webhook url: %s", url)
+		return nil
 	}
 
+	logrus.Infof("initializing slack with webhook url: %s", webhook)
+
 	return &slack{
-		webhook: url,
+		webhook: webhook,
 	}
 }
 
diff --git a/provider/teams.go b/provider/teams.go
index f89ced2..c924643 100644
--- a/provider/teams.go
+++ b/provider/teams.go
@@ -30,15 +30,18 @@ type teamsWebhookPayload struct {
 }
 
 // NewTeams returns new team instance
-func NewTeams(url string) Provider {
-	if len(url) == 0 {
+func NewTeams(config map[string]string) Provider {
+	webhook, ok := config["webhook"]
+
+	if !ok || len(webhook) == 0 {
 		logrus.Warnf("initializing Teams with empty webhook url")
-	} else {
-		logrus.Infof("initializing Teams with webhook url: %s", url)
+		return nil
 	}
 
+	logrus.Infof("initializing Teams with webhook url: %s", webhook)
+
 	return &teams{
-		webhook: url,
+		webhook: webhook,
 	}
 }
 
diff --git a/provider/telegram.go b/provider/telegram.go
index 72224c0..2a187cd 100644
--- a/provider/telegram.go
+++ b/provider/telegram.go
@@ -21,19 +21,24 @@ type telegram struct {
 }
 
 // NewTelegram returns a new Telegram object
-func NewTelegram(token string, chatId string) Provider {
-	// object validation
-	if len(token) == 0 {
+func NewTelegram(config map[string]string) Provider {
+	token, ok := config["token"]
+	if !ok || len(token) == 0 {
 		logrus.Warnf("initializing telegram with empty token")
-	} else if len(chatId) == 0 {
+		return nil
+	}
+
+	chatId, ok := config["chatId"]
+	if !ok || len(token) == 0 {
 		logrus.Warnf("initializing telegram with empty chat_id")
-	} else {
-		logrus.Infof(
-			"initializing telegram with token  %s and chat_id %s",
-			token,
-			chatId)
+		return nil
 	}
 
+	logrus.Infof(
+		"initializing telegram with token  %s and chat_id %s",
+		token,
+		chatId)
+
 	// returns a new telegram object
 	return &telegram{
 		token:  token,
diff --git a/upgrader/upgrader.go b/upgrader/upgrader.go
index b2749c6..670b644 100644
--- a/upgrader/upgrader.go
+++ b/upgrader/upgrader.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/abahmed/kwatch/constant"
 	"github.com/abahmed/kwatch/provider"
-	"github.com/abahmed/kwatch/util"
 	"github.com/google/go-github/v41/github"
 	"github.com/sirupsen/logrus"
 )
@@ -46,7 +45,7 @@ func checkRelease(providers []provider.Provider) {
 		return
 	}
 
-	util.SendProvidersMsg(
+	provider.SendProvidersMsg(
 		providers,
 		fmt.Sprintf(constant.KwatchUpdateMsg, *r.TagName),
 	)
diff --git a/util/util.go b/util/util.go
index fb8930c..e3d4037 100644
--- a/util/util.go
+++ b/util/util.go
@@ -6,9 +6,6 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/abahmed/kwatch/event"
-
-	"github.com/abahmed/kwatch/provider"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/viper"
 	v1 "k8s.io/api/core/v1"
@@ -125,157 +122,6 @@ func getPodEvents(
 		})
 }
 
-// GetProviders returns slice of provider objects after parsing config
-func GetProviders() []provider.Provider {
-	var providers []provider.Provider
-	const isPresent = false
-	telegram := []bool{isPresent, isPresent}
-	email := []bool{isPresent, isPresent, isPresent, isPresent, isPresent}
-
-	for key, value := range viper.Get("alert").(map[string]interface{}) {
-		for c, v := range value.(map[string]interface{}) {
-			if key == "slack" &&
-				c == "webhook" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				providers = append(
-					providers,
-					provider.NewSlack(viper.GetString("alert.slack.webhook")))
-			}
-			if key == "pagerduty" &&
-				c == "integrationkey" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				providers = append(
-					providers,
-					provider.NewPagerDuty(
-						viper.GetString("alert.pagerduty.integrationKey")))
-			}
-			if key == "discord" &&
-				c == "webhook" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				providers = append(
-					providers,
-					provider.NewDiscord(
-						viper.GetString("alert.discord.webhook")))
-			}
-			if key == "telegram" &&
-				c == "token" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				telegram[0] = true
-			}
-			if key == "telegram" &&
-				c == "chatid" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				telegram[1] = true
-			}
-			if key == "teams" &&
-				c == "webhook" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				providers = append(
-					providers,
-					provider.NewTeams(viper.GetString("alert.teams.webhook")))
-			}
-			if key == "email" &&
-				c == "from" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				email[0] = true
-			}
-			if key == "email" &&
-				c == "to" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				email[1] = true
-			}
-			if key == "email" &&
-				c == "port" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				email[2] = true
-			}
-			if key == "email" &&
-				c == "host" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				email[3] = true
-			}
-			if key == "email" &&
-				c == "password" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				email[4] = true
-			}
-			if key == "rocketchat" &&
-				c == "webhook" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				providers = append(
-					providers,
-					provider.NewRocketChat(
-						viper.GetString("alert.rocketchat.webhook")))
-			}
-			if key == "mattermost" &&
-				c == "webhook" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				providers = append(
-					providers,
-					provider.NewMattermost(
-						viper.GetString("alert.mattermost.webhook")))
-			}
-			if key == "opsgenie" &&
-				c == "apikey" &&
-				len(strings.TrimSpace(v.(string))) > 0 {
-				providers = append(
-					providers,
-					provider.NewOpsgenie(
-						viper.GetString("alert.opsgenie.apikey")))
-			}
-			if key == "telegram" &&
-				IsListAllBool(true, telegram) {
-				providers = append(
-					providers,
-					provider.NewTelegram(
-						viper.GetString("alert.telegram.token"),
-						viper.GetString("alert.telegram.chatId")))
-			}
-			if key == "email" &&
-				IsListAllBool(true, email) {
-				providers = append(
-					providers,
-					provider.NewEmail(viper.GetString("alert.email.from"),
-						viper.GetString("alert.email.password"),
-						viper.GetString("alert.email.host"),
-						viper.GetInt("alert.email.port"),
-						viper.GetString("alert.email.to")))
-			}
-		}
-	}
-
-	return providers
-}
-
-// SendProvidersMsg sends string msg to all providers
-func SendProvidersMsg(p []provider.Provider, msg string) {
-	logrus.Infof("sending message: %s", msg)
-	for _, prv := range p {
-		err :=
-			prv.SendMessage(msg)
-		if err != nil {
-			logrus.Errorf(
-				"failed to send msg with %s: %s",
-				prv.Name(),
-				err.Error())
-		}
-	}
-}
-
-// SendProvidersEvent sends event to all providers
-func SendProvidersEvent(p []provider.Provider, event event.Event) {
-	logrus.Infof("sending event: %+v", event)
-	for _, prv := range p {
-		if err := prv.SendEvent(&event); err != nil {
-			logrus.Errorf(
-				"failed to send event with %s: %s",
-				prv.Name(),
-				err.Error(),
-			)
-		}
-	}
-}
-
 // IsStrInSlice checks if string is existing in a slice of string
 func IsStrInSlice(str string, strList []string) bool {
 	if len(strList) == 0 {
@@ -290,13 +136,3 @@ func IsStrInSlice(str string, strList []string) bool {
 
 	return false
 }
-
-// IsListAllBool checks if all elements in a boolean list have the same value
-func IsListAllBool(v bool, l []bool) bool {
-	for _, x := range l {
-		if x != v {
-			return false
-		}
-	}
-	return true
-}


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
new file mode 100644
index 0000000..aa2e0c2
--- /dev/null
+++ b/config/config_test.go
@@ -0,0 +1,48 @@
+package config
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestGetAllowForbidSlices(t *testing.T) {
+	testCases := []map[string][]string{
+		{
+			"input":  {},
+			"allow":  {},
+			"forbid": {},
+		},
+		{
+			"input":  {"hello", "!world"},
+			"allow":  {"hello"},
+			"forbid": {"world"},
+		},
+		{
+			"input":  {"hello"},
+			"allow":  {"hello"},
+			"forbid": {},
+		},
+		{
+			"input":  {"!hello"},
+			"allow":  {},
+			"forbid": {"hello"},
+		},
+	}
+
+	for _, tc := range testCases {
+		actualAllow, actualForbid := getAllowForbidSlices(tc["input"])
+		if !reflect.DeepEqual(actualAllow, tc["allow"]) {
+			t.Fatalf(
+				"getAllowForbidSlices allow returned %v expected %v",
+				actualAllow,
+				tc["allow"])
+		}
+
+		if !reflect.DeepEqual(actualForbid, tc["forbid"]) {
+			t.Fatalf(
+				"getAllowForbidSlices forbid returned %v expected %v",
+				actualForbid,
+				tc["forbid"])
+		}
+	}
+}
diff --git a/provider/provider_test.go b/provider/provider_test.go
new file mode 100644
index 0000000..d260075
--- /dev/null
+++ b/provider/provider_test.go
@@ -0,0 +1,109 @@
+package provider
+
+import (
+	"testing"
+
+	"github.com/abahmed/kwatch/config"
+	"github.com/abahmed/kwatch/event"
+)
+
+func TestGetProviders(t *testing.T) {
+	alertMap := map[string]map[string]string{
+		"slack": {
+			"webhook": "test",
+		},
+		"pagerduty": {
+			"integrationKey": "test",
+		},
+		"discord": {
+			"webhook": "test/id",
+		},
+		"telegram": {
+			"token":  "test",
+			"chatId": "test",
+		},
+		"teams": {
+			"webhook": "test",
+		},
+		"mattermost": {
+			"webhook": "test",
+		},
+		"opsgenie": {
+			"apiKey": "test",
+		},
+	}
+
+	providers := GetProviders(&config.Config{Alert: alertMap})
+	if len(providers) != len(alertMap) {
+		t.Fatalf(
+			"get providers returned %d expected %d",
+			len(providers),
+			len(alertMap))
+	}
+}
+
+func TestSendProvidersEvent(t *testing.T) {
+	alertMap := map[string]map[string]string{
+		"slack": {
+			"webhook": "test",
+		},
+		"pagerduty": {
+			"integrationkey": "test",
+		},
+		"discord": {
+			"webhook": "test",
+		},
+		"telegram": {
+			"token":  "test",
+			"chatid": "test",
+		},
+		"teams": {
+			"webhook": "test",
+		},
+		"rocketchat": {
+			"webhook": "test",
+		},
+		"mattermost": {
+			"webhook": "test",
+		},
+		"opsgenie": {
+			"apiKey": "test",
+		},
+	}
+	providers := GetProviders(&config.Config{Alert: alertMap})
+
+	SendProvidersEvent(providers, event.Event{})
+}
+
+func TestSendProvidersMsg(t *testing.T) {
+	alertMap := map[string]map[string]string{
+		"slack": {
+			"webhook": "test",
+		},
+		"pagerduty": {
+			"integrationkey": "test",
+		},
+		"discord": {
+			"webhook": "test",
+		},
+		"telegram": {
+			"token":  "test",
+			"chatid": "test",
+		},
+		"teams": {
+			"webhook": "test",
+		},
+		"rocketchat": {
+			"webhook": "test",
+		},
+		"mattermost": {
+			"webhook": "test",
+		},
+		"opsgenie": {
+			"apiKey": "test",
+		},
+	}
+	providers := GetProviders(&config.Config{Alert: alertMap})
+
+	SendProvidersMsg(providers, "hello world!")
+}
diff --git a/util/util_test.go b/util/util_test.go
index 2462a34..461584f 100644
--- a/util/util_test.go
+++ b/util/util_test.go
@@ -1,10 +1,8 @@
 package util
 
 import (
-	"fmt"
 	"testing"
 
-	"github.com/abahmed/kwatch/event"
 	"github.com/spf13/viper"
 	testclient "k8s.io/client-go/kubernetes/fake"
 )
@@ -79,149 +77,3 @@ func TestGetPodContainerLogs(t *testing.T) {
 			"")
 	}
 }
-
-func TestIsListAllBool(t *testing.T) {
-	testCases := []struct {
-		boolean bool
-		list    []bool
-		output  bool
-	}{
-		{
-			boolean: true,
-			list:    []bool{true, true},
-			output:  true,
-		},
-		{
-			boolean: false,
-			list:    []bool{false, false},
-			output:  true,
-		},
-		{
-			boolean: true,
-			list:    []bool{false, true},
-			output:  false,
-		},
-		{
-			boolean: true,
-			list:    []bool{},
-			output:  true,
-		},
-	}
-
-	for _, tc := range testCases {
-		out := IsListAllBool(tc.boolean, tc.list)
-		if out != tc.output {
-			t.Fatalf(
-				"IsListAllBool %t in %v: returned %t expected %t",
-				tc.boolean,
-				tc.list,
-				out,
-				tc.output)
-		}
-	}
-}
-
-func TestGetProviders(t *testing.T) {
-	alertMap := map[string]interface{}{
-		"slack": map[string]interface{}{
-			"webhook": "test",
-		},
-		"pagerduty": map[string]interface{}{
-			"integrationkey": "test",
-		},
-		"discord": map[string]interface{}{
-			"webhook": "test",
-		},
-		"telegram": map[string]interface{}{
-			"token":  "test",
-			"chatId": "test",
-		},
-		"teams": map[string]interface{}{
-			"webhook": "test",
-		},
-		"mattermost": map[string]interface{}{
-			"webhook": "test",
-		},
-		"opsgenie": map[string]interface{}{
-			"apiKey": "test",
-		},
-	}
-	viper.SetDefault("alert", alertMap)
-
-	providers := GetProviders()
-	fmt.Printf("t: %v\n", providers)
-	if len(providers) != len(alertMap) {
-		t.Fatalf(
-			"get providers returned %d expected %d",
-			len(providers),
-			len(alertMap))
-	}
-}
-
-func TestSendProvidersEvent(t *testing.T) {
-	alertMap := map[string]interface{}{
-		"slack": map[string]interface{}{
-			"webhook": "test",
-		},
-		"pagerduty": map[string]interface{}{
-			"integrationkey": "test",
-		},
-		"discord": map[string]interface{}{
-			"webhook": "test",
-		},
-		"telegram": map[string]interface{}{
-			"token":  "test",
-			"chatid": "test",
-		},
-		"teams": map[string]interface{}{
-			"webhook": "test",
-		},
-		"rocketchat": map[string]interface{}{
-			"webhook": "test",
-		},
-		"mattermost": map[string]interface{}{
-			"webhook": "test",
-		},
-		"opsgenie": map[string]interface{}{
-			"apiKey": "test",
-		},
-	}
-	viper.SetDefault("alert", alertMap)
-	providers := GetProviders()
-
-	SendProvidersEvent(providers, event.Event{})
-}
-
-func TestSendProvidersMsg(t *testing.T) {
-	alertMap := map[string]interface{}{
-		"slack": map[string]interface{}{
-			"webhook": "test",
-		},
-		"pagerduty": map[string]interface{}{
-			"integrationkey": "test",
-		},
-		"discord": map[string]interface{}{
-			"webhook": "test",
-		},
-		"telegram": map[string]interface{}{
-			"token":  "test",
-			"chatid": "test",
-		},
-		"teams": map[string]interface{}{
-			"webhook": "test",
-		},
-		"rocketchat": map[string]interface{}{
-			"webhook": "test",
-		},
-		"mattermost": map[string]interface{}{
-			"webhook": "test",
-		},
-		"opsgenie": map[string]interface{}{
-			"apiKey": "test",
-		},
-	}
-	viper.SetDefault("alert", alertMap)
-	providers := GetProviders()
-
-	SendProvidersMsg(providers, "hello world!")
-}