Repository: yonahd/kor
Pull request #77 (2023-09-12 20:02:12)

feat: add send to slack support
As discussed in #73, this is a separate PR to add send to Slack support.


yonahd: As a general direction here we should pass a client set to the functions instead of a kubeconfig.
That way we can check if we are in cluster before and either create the clientset from the service account or from the kubeconfig 
patricktalmeida: > As a general direction here we should pass a client set to the functions instead of a kubeconfig. That way we can check if we are in cluster before and either create the clientset from the service account or from the kubeconfig

@yonahd I didn't quite understand your idea. I created 2 separate handlers for InCluster and KubeConfig configurations.
I create the clientset from those handlers and I let `GetKubeClient` handle which one to use. And kubeconfig file is only sent to `KubeConfigClient` which needs it to create the client.

Could you explain your idea when looking into those 3 functions? `KubeConfigClient`, `InClusterConfigClient` and `GetKubeClient`

yonahd: > > As a general direction here we should pass a client set to the functions instead of a kubeconfig. That way we can check if we are in cluster before and either create the clientset from the service account or from the kubeconfig
> 
> @yonahd I didn't quite understand your idea. I created 2 separate handlers for InCluster and KubeConfig configurations. I create the clientset from those handlers and I let `GetKubeClient` handle which one to use. And kubeconfig file is only sent to `KubeConfigClient` which needs it to create the client.
> 
> Could you explain your idea when looking into those 3 functions? `KubeConfigClient`, `InClusterConfigClient` and `GetKubeClient`

I don't want to limit any of the functions to only be in cluster or only out of cluster. That's why I want to create the separation of kubeconfig vs service account earlier and regardless pass a clientset to the functions. 
patricktalmeida: > > > As a general direction here we should pass a client set to the functions instead of a kubeconfig. That way we can check if we are in cluster before and either create the clientset from the service account or from the kubeconfig
> > 
> > 
> > @yonahd I didn't quite understand your idea. I created 2 separate handlers for InCluster and KubeConfig configurations. I create the clientset from those handlers and I let `GetKubeClient` handle which one to use. And kubeconfig file is only sent to `KubeConfigClient` which needs it to create the client.
> > Could you explain your idea when looking into those 3 functions? `KubeConfigClient`, `InClusterConfigClient` and `GetKubeClient`
> 
> I don't want to limit any of the functions to only be in cluster or only out of cluster. That's why I want to create the separation of kubeconfig vs service account earlier and regardless pass a clientset to the functions.

I think I got your idea. But still thinking on how to control that. I mean, when it should authenticate with kubeconfig or when to use `rest.InClusterConfig()`.
What do you think of sending a flag when it should authenticate from inside the cluster, e.g. `kor all --in-cluster-auth`?
With that it could be could be hard coded inside the helm template for CronJob and also in the future for Deployments. e.g. `args: ["{{ .Values.cronJob.korCommand }} --in-cluster-auth --slack-channel {{ .Values.cronJob.slackChannel }} --slack-auth-token {{ .Values.cronJob.slackAuthToken }}"]`
yonahd: We can try something like this 
```
func GetKubeClient(kubeconfig string) *kubernetes.Clientset {
	if _, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token"); err == nil {
		config, err := rest.InClusterConfig()
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err)
			os.Exit(1)
		}

		clientset, err := kubernetes.NewForConfig(config)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err)
			os.Exit(1)
		}
		return clientset
	}
	if kubeconfig == "" {
		if configEnv := os.Getenv("KUBECONFIG"); configEnv != "" {
			kubeconfig = configEnv
		} else {
			kubeconfig = GetKubeConfigPath()
		}
	}
	config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err)
		os.Exit(1)
	}

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err)
		os.Exit(1)
	}
	return clientset
}
```

and in the command we can do e.g.
```
var configmapCmd = &cobra.Command{
	Use:     "configmap",
	Aliases: []string{"cm"},
	Short:   "Gets unused configmaps",
	Args:    cobra.ExactArgs(0),
	Run: func(cmd *cobra.Command, args []string) {
		clientset := kor.GetKubeClient(kubeconfig)
		if outputFormat == "json" || outputFormat == "yaml" {
			if response, err := kor.GetUnusedConfigmapsStructured(includeExcludeLists, clientset, outputFormat); err != nil {
				fmt.Println(err)
			} else {
				fmt.Println(response)
			}
		} else {
			kor.GetUnusedConfigmaps(includeExcludeLists, clientset)
		}

	},
}

```
patricktalmeida: > We can try something like this
> 
> ```
> func GetKubeClient(kubeconfig string) *kubernetes.Clientset {
> 	if _, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token"); err == nil {
> 		config, err := rest.InClusterConfig()
> 		if err != nil {
> 			fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err)
> 			os.Exit(1)
> 		}
> 
> 		clientset, err := kubernetes.NewForConfig(config)
> 		if err != nil {
> 			fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err)
> 			os.Exit(1)
> 		}
> 		return clientset
> 	}
> 	if kubeconfig == "" {
> 		if configEnv := os.Getenv("KUBECONFIG"); configEnv != "" {
> 			kubeconfig = configEnv
> 		} else {
> 			kubeconfig = GetKubeConfigPath()
> 		}
> 	}
> 	config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
> 	if err != nil {
> 		fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err)
> 		os.Exit(1)
> 	}
> 
> 	clientset, err := kubernetes.NewForConfig(config)
> 	if err != nil {
> 		fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err)
> 		os.Exit(1)
> 	}
> 	return clientset
> }
> ```
> 
> and in the command we can do e.g.
> 
> ```
> var configmapCmd = &cobra.Command{
> 	Use:     "configmap",
> 	Aliases: []string{"cm"},
> 	Short:   "Gets unused configmaps",
> 	Args:    cobra.ExactArgs(0),
> 	Run: func(cmd *cobra.Command, args []string) {
> 		clientset := kor.GetKubeClient(kubeconfig)
> 		if outputFormat == "json" || outputFormat == "yaml" {
> 			if response, err := kor.GetUnusedConfigmapsStructured(includeExcludeLists, clientset, outputFormat); err != nil {
> 				fmt.Println(err)
> 			} else {
> 				fmt.Println(response)
> 			}
> 		} else {
> 			kor.GetUnusedConfigmaps(includeExcludeLists, clientset)
> 		}
> 
> 	},
> }
> ```

Agreed and done. Let me know if you need to change something.
yonahd: Perfect!
Don't want to inconvenience you too much, but can we separate the slack part to a different MR?

patricktalmeida: > Perfect! Don't want to inconvenience you too much, but can we separate the slack part to a different MR? This MR would close #75 And I'll open a different ticket for Slack messages(I think we can combine some of the functions). In addition We can have the slack reports also out of the cluster

Sure. I can split it in two.
What do you mean by having slack report out of the cluster? I mean it already works out of the cluster also. If you run it from your local machine `kor all --slack-channel <your-channel> --slack-auth-token <your-token> --include-namespaces default,some-ns` it's gonna work just fine.
yonahd: > > Perfect! Don't want to inconvenience you too much, but can we separate the slack part to a different MR? This MR would close #75 And I'll open a different ticket for Slack messages(I think we can combine some of the functions). In addition We can have the slack reports also out of the cluster
> 
> Sure. I can split it in two. What do you mean by having slack report out of the cluster? I mean it already works out of the cluster also. If you run it from your local machine `kor all --slack-channel <your-channel> --slack-auth-token <your-token> --include-namespaces default,some-ns` it's gonna work just fine.

My mistake.
Now though it can be a condition in the regular function.
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) Report
Patch coverage: **`6.35%`** and project coverage change: **`-11.35%`** :warning:
> Comparison is base [(`f3235b0`)](https://app.codecov.io/gh/yonahd/kor/commit/f3235b03eba8879724455760390821060179c28c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) 49.36% compared to head [(`a217e51`)](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) 38.01%.

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##             main      #77       +/-   ##
===========================================
- Coverage   49.36%   38.01%   -11.35%     
===========================================
  Files          15       16        +1     
  Lines        1493     2028      +535     
===========================================
+ Hits          737      771       +34     
- Misses        657     1147      +490     
- Partials       99      110       +11     
```


| [Files Changed](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) | Coverage Δ | |
|---|---|---|
| [pkg/kor/all.go](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen#diff-cGtnL2tvci9hbGwuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/kor/confimgmaps.go](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen#diff-cGtnL2tvci9jb25maW1nbWFwcy5nbw==) | `47.43% <0.00%> (-14.24%)` | :arrow_down: |
| [pkg/kor/deployments.go](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen#diff-cGtnL2tvci9kZXBsb3ltZW50cy5nbw==) | `25.58% <0.00%> (-18.42%)` | :arrow_down: |
| [pkg/kor/hpas.go](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen#diff-cGtnL2tvci9ocGFzLmdv) | `32.28% <0.00%> (-12.78%)` | :arrow_down: |
| [pkg/kor/ingresses.go](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen#diff-cGtnL2tvci9pbmdyZXNzZXMuZ28=) | `40.62% <0.00%> (-15.90%)` | :arrow_down: |
| [pkg/kor/pdbs.go](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen#diff-cGtnL2tvci9wZGJzLmdv) | `29.12% <0.00%> (-15.65%)` | :arrow_down: |
| [pkg/kor/pvc.go](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen#diff-cGtnL2tvci9wdmMuZ28=) | `31.73% <0.00%> (-16.80%)` | :arrow_down: |
| [pkg/kor/roles.go](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen#diff-cGtnL2tvci9yb2xlcy5nbw==) | `34.23% <0.00%> (-16.44%)` | :arrow_down: |
| [pkg/kor/secrets.go](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen#diff-cGtnL2tvci9zZWNyZXRzLmdv) | `50.62% <0.00%> (-14.70%)` | :arrow_down: |
| [pkg/kor/serviceaccounts.go](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen#diff-cGtnL2tvci9zZXJ2aWNlYWNjb3VudHMuZ28=) | `44.37% <0.00%> (-12.89%)` | :arrow_down: |
| ... and [3 more](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/yonahd/kor/pull/77?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen).

yonahd: I'd like to condense the slack functions to be part of the `GetUnusedHpas` function.
Can we make the slack parameters optional and based on the users sent parameters decide if we are sending to slack(and whether it's a file or webhook)?
patricktalmeida: > I'd like to condense the slack functions to be part of the `GetUnusedHpas` function. Can we make the slack parameters optional and based on the users sent parameters decide if we are sending to slack(and whether it's a file or webhook)?



> I'd like to condense the slack functions to be part of the `GetUnusedHpas` function. Can we make the slack parameters optional and based on the users sent parameters decide if we are sending to slack(and whether it's a file or webhook)?

alright, give me some time to take a look at that. 

Patch:
diff --git a/README.md b/README.md
index 295648a..a31c0d4 100644
--- a/README.md
+++ b/README.md
@@ -23,10 +23,24 @@ Kor is a tool to discover unused Kubernetes resources. Currently, Kor can identi
 
 Download the binary for your operating system from the [releases page](https://github.com/yonahd/kor/releases) and add it to your system's PATH.
 
+### Homebrew
 For macOS users, you can install Kor using Homebrew:
 ```sh
 brew install kor
 ```
+### Build from source
+Install the binary to your `$GOBIN` or `$GOPATH/bin`:
+```sh
+go install github.com/yonahd/kor@latest
+```
+
+### Docker
+Run a container with your kubeconfig mounted:
+```sh
+docker run --rm -i yonahdissen/kor
+
+docker run --rm -i -v "/path/to/.kube/config:/root/.kube/config" yonahdissen/kor all
+```
 
 ## Usage
 
@@ -47,11 +61,14 @@ Kor provides various subcommands to identify and list unused resources. The avai
 
 ### Supported Flags
 ```
--e, --exclude-namespaces string   Namespaces to be excluded, splited by comma. Example: --exclude-namespace ns1,ns2,ns3. If --include-namespace is set, --exclude-namespaces will be ignored.
+-e, --exclude-namespaces string   Namespaces to be excluded, split by comma. Example: --exclude-namespace ns1,ns2,ns3. If --include-namespace is set, --exclude-namespaces will be ignored.
 -h, --help                        help for kor
--n, --include-namespaces string   Namespaces to run on, split by comma. Example: --include-namespace ns1,ns2,ns3. 
+-n, --include-namespaces string   Namespaces to run on, split by comma. Example: --include-namespace ns1,ns2,ns3.
 -k, --kubeconfig string           Path to kubeconfig file (optional)
-    --output string               Output format ("table" or "json") (default "table")
+    --output string               Output format (table or json) (default "table")
+    --slack-auth-token string     Slack auth token to send notifications to. --slack-auth-token requires --slack-channel to be set.
+    --slack-channel string        Slack channel to send notifications to. --slack-channel requires --slack-auth-token to be set.
+    --slack-webhook-url string    Slack webhook URL to send notifications to
 ```
 
 To use a specific subcommand, run `kor [subcommand] [flags]`.
@@ -135,6 +152,7 @@ helm upgrade -i kor \
     --set cronJob.slackToken=<slack-token> \
     ./charts/kor
 ```
+> Note: To send it to Slack as a file it's required to set the `slackToken` and `slackChannel` values.
 
 It's set to run every Monday at 1 a.m. by default. You can change the schedule by setting the `cronJob.schedule` value.
 
diff --git a/charts/kor/templates/cronjob.yaml b/charts/kor/templates/cronjob.yaml
index 0a0100e..4f0454b 100644
--- a/charts/kor/templates/cronjob.yaml
+++ b/charts/kor/templates/cronjob.yaml
@@ -19,9 +19,21 @@ spec:
               image: {{ .Values.cronJob.image.repository }}:{{ .Values.cronJob.image.tag }}
               command: ["/bin/sh", "-c"]
               {{- if .Values.cronJob.slackWebhookUrl }}
-              args: ["{{ .Values.cronJob.command }} --slack-webhook-url {{ .Values.cronJob.slackWebhookUrl }}"]
+              args: ["{{ .Values.cronJob.command }} --slack-webhook-url $SLACK_WEBHOOK_URL"]
+              env:
+                - name: SLACK_WEBHOOK_URL
+                  valueFrom:
+                    secretKeyRef:
+                      name: {{ .Release.Name }}-slack-webhook-url-secret
+                      key: slack-webhook-url
               {{- else if and .Values.cronJob.slackChannel .Values.cronJob.slackAuthToken }}
-              args: ["{{ .Values.cronJob.command }} --slack-channel {{ .Values.cronJob.slackChannel }} --slack-auth-token {{ .Values.cronJob.slackAuthToken }}"]
+              args: ["{{ .Values.cronJob.command }} --slack-channel {{ .Values.cronJob.slackChannel }} --slack-auth-token $SLACK_AUTH_TOKEN"]
+              env:
+                - name: SLACK_AUTH_TOKEN
+                  valueFrom:
+                    secretKeyRef:
+                      name: {{ .Release.Name }}-slack-auth-token-secret
+                      key: slack-auth-token
               {{- else }}
               args: ["{{ .Values.cronJob.command }}"]
               {{- end }}
diff --git a/charts/kor/templates/secret.yaml b/charts/kor/templates/secret.yaml
new file mode 100644
index 0000000..9e27bdd
--- /dev/null
+++ b/charts/kor/templates/secret.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ .Release.Name }}-slack-auth-token-secret
+type: Opaque
+data:
+  slack-auth-token: {{ .Values.cronJob.slackAuthToken | b64enc | quote }}
+
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ .Release.Name }}-slack-webhook-url-secret
+type: Opaque
+data:
+  slack-webhook-url: {{ .Values.cronJob.slackWebhookUrl | b64enc | quote }}
diff --git a/cmd/kor/all.go b/cmd/kor/all.go
index 5b3490f..3aaf7e5 100644
--- a/cmd/kor/all.go
+++ b/cmd/kor/all.go
@@ -20,7 +20,7 @@ var allCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedAll(includeExcludeLists, clientset)
+			kor.GetUnusedAll(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/configmaps.go b/cmd/kor/configmaps.go
index 49ed5ce..4d52e88 100644
--- a/cmd/kor/configmaps.go
+++ b/cmd/kor/configmaps.go
@@ -21,7 +21,7 @@ var configmapCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedConfigmaps(includeExcludeLists, clientset)
+			kor.GetUnusedConfigmaps(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/deployments.go b/cmd/kor/deployments.go
index 61074f9..e56c01b 100644
--- a/cmd/kor/deployments.go
+++ b/cmd/kor/deployments.go
@@ -21,7 +21,7 @@ var deployCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedDeployments(includeExcludeLists, clientset)
+			kor.GetUnusedDeployments(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/hpas.go b/cmd/kor/hpas.go
index 1af3a3a..35f074e 100644
--- a/cmd/kor/hpas.go
+++ b/cmd/kor/hpas.go
@@ -21,7 +21,7 @@ var hpaCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedHpas(includeExcludeLists, clientset)
+			kor.GetUnusedHpas(includeExcludeLists, clientset, slackOpts)
 		}
 	},
 }
diff --git a/cmd/kor/ingresses.go b/cmd/kor/ingresses.go
index 97f0c61..f48c2cc 100644
--- a/cmd/kor/ingresses.go
+++ b/cmd/kor/ingresses.go
@@ -21,7 +21,7 @@ var ingressCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedIngresses(includeExcludeLists, clientset)
+			kor.GetUnusedIngresses(includeExcludeLists, clientset, slackOpts)
 		}
 	},
 }
diff --git a/cmd/kor/pdbs.go b/cmd/kor/pdbs.go
index 0cf774f..2b06049 100644
--- a/cmd/kor/pdbs.go
+++ b/cmd/kor/pdbs.go
@@ -21,7 +21,7 @@ var pdbCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedPdbs(includeExcludeLists, clientset)
+			kor.GetUnusedPdbs(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/pvc.go b/cmd/kor/pvc.go
index c0a02e0..2b4cfba 100644
--- a/cmd/kor/pvc.go
+++ b/cmd/kor/pvc.go
@@ -21,7 +21,7 @@ var pvcCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedPvcs(includeExcludeLists, clientset)
+			kor.GetUnusedPvcs(includeExcludeLists, clientset, slackOpts)
 		}
 	},
 }
diff --git a/cmd/kor/roles.go b/cmd/kor/roles.go
index 44e9e22..65087fe 100644
--- a/cmd/kor/roles.go
+++ b/cmd/kor/roles.go
@@ -21,7 +21,7 @@ var roleCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedRoles(includeExcludeLists, clientset)
+			kor.GetUnusedRoles(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/root.go b/cmd/kor/root.go
index 6c5c162..418a415 100644
--- a/cmd/kor/root.go
+++ b/cmd/kor/root.go
@@ -27,7 +27,7 @@ var rootCmd = &cobra.Command{
 					fmt.Println(response)
 				}
 			} else {
-				kor.GetUnusedMulti(includeExcludeLists, kubeconfig, resourceNames)
+				kor.GetUnusedMulti(includeExcludeLists, kubeconfig, resourceNames, slackOpts)
 			}
 		} else {
 			fmt.Printf("Subcommand %q was not found, try using 'kor --help' for available subcommands", args[0])
@@ -39,6 +39,7 @@ var (
 	outputFormat        string
 	kubeconfig          string
 	includeExcludeLists kor.IncludeExcludeLists
+	slackOpts           kor.SlackOpts
 )
 
 func Execute() {
@@ -46,6 +47,9 @@ func Execute() {
 	rootCmd.PersistentFlags().StringVarP(&includeExcludeLists.IncludeListStr, "include-namespaces", "n", "", "Namespaces to run on, splited by comma. Example: --include-namespace ns1,ns2,ns3. ")
 	rootCmd.PersistentFlags().StringVarP(&includeExcludeLists.ExcludeListStr, "exclude-namespaces", "e", "", "Namespaces to be excluded, splited by comma. Example: --exclude-namespace ns1,ns2,ns3. If --include-namespace is set, --exclude-namespaces will be ignored.")
 	rootCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table or json)")
+	rootCmd.PersistentFlags().StringVar(&slackOpts.WebhookURL, "slack-webhook-url", "", "Slack webhook URL to send notifications to")
+	rootCmd.PersistentFlags().StringVar(&slackOpts.Channel, "slack-channel", "", "Slack channel to send notifications to. --slack-channel requires --slack-auth-token to be set.")
+	rootCmd.PersistentFlags().StringVar(&slackOpts.Token, "slack-auth-token", "", "Slack auth token to send notifications to. --slack-auth-token requires --slack-channel to be set.")
 	if err := rootCmd.Execute(); err != nil {
 		fmt.Fprintf(os.Stderr, "Error while executing your CLI '%s'", err)
 		os.Exit(1)
diff --git a/cmd/kor/secrets.go b/cmd/kor/secrets.go
index fae67f7..7139728 100644
--- a/cmd/kor/secrets.go
+++ b/cmd/kor/secrets.go
@@ -21,7 +21,7 @@ var secretCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedSecrets(includeExcludeLists, clientset)
+			kor.GetUnusedSecrets(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/serviceaccounts.go b/cmd/kor/serviceaccounts.go
index 3b2472d..bdeca3d 100644
--- a/cmd/kor/serviceaccounts.go
+++ b/cmd/kor/serviceaccounts.go
@@ -21,7 +21,7 @@ var serviceAccountCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedServiceAccounts(includeExcludeLists, clientset)
+			kor.GetUnusedServiceAccounts(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/services.go b/cmd/kor/services.go
index d027495..d04ba6b 100644
--- a/cmd/kor/services.go
+++ b/cmd/kor/services.go
@@ -21,7 +21,7 @@ var serviceCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedServices(includeExcludeLists, clientset)
+			kor.GetUnusedServices(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/statefulsets.go b/cmd/kor/statefulsets.go
index 130da02..e8ffb14 100644
--- a/cmd/kor/statefulsets.go
+++ b/cmd/kor/statefulsets.go
@@ -21,7 +21,7 @@ var stsCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedStatefulSets(includeExcludeLists, clientset)
+			kor.GetUnusedStatefulSets(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/pkg/kor/all.go b/pkg/kor/all.go
index 5952b20..abbd084 100644
--- a/pkg/kor/all.go
+++ b/pkg/kor/all.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"os"
@@ -118,8 +119,11 @@ func getUnusedPdbs(clientset kubernetes.Interface, namespace string) ResourceDif
 	return namespacePdbDiff
 }
 
-func GetUnusedAll(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedAll(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
+
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		var allDiffs []ResourceDiff
 		namespaceCMDiff := getUnusedCMs(clientset, namespace)
@@ -144,9 +148,20 @@ func GetUnusedAll(includeExcludeLists IncludeExcludeLists, clientset kubernetes.
 		allDiffs = append(allDiffs, namespaceIngressDiff)
 		namespacePdbDiff := getUnusedPdbs(clientset, namespace)
 		allDiffs = append(allDiffs, namespacePdbDiff)
+
 		output := FormatOutputAll(namespace, allDiffs)
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/confimgmaps.go b/pkg/kor/confimgmaps.go
index 64ca796..8125e3c 100644
--- a/pkg/kor/confimgmaps.go
+++ b/pkg/kor/confimgmaps.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -128,9 +129,11 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string) ([]str
 
 }
 
-func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceCM(clientset, namespace)
 		if err != nil {
@@ -138,8 +141,18 @@ func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset kube
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Config Maps")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/deployments.go b/pkg/kor/deployments.go
index 3285ea7..c5d84f2 100644
--- a/pkg/kor/deployments.go
+++ b/pkg/kor/deployments.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -32,9 +33,11 @@ func ProcessNamespaceDeployments(clientset kubernetes.Interface, namespace strin
 	return deploymentsWithoutReplicas, nil
 }
 
-func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := ProcessNamespaceDeployments(clientset, namespace)
 		if err != nil {
@@ -42,8 +45,18 @@ func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset kub
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Deployments")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/hpas.go b/pkg/kor/hpas.go
index 635fb7e..7fc4bf1 100644
--- a/pkg/kor/hpas.go
+++ b/pkg/kor/hpas.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -79,9 +80,11 @@ func processNamespaceHpas(clientset kubernetes.Interface, namespace string) ([]s
 	return unusedHpas, nil
 }
 
-func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceHpas(clientset, namespace)
 		if err != nil {
@@ -89,10 +92,19 @@ func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset kubernetes
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Hpas")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
 	}
 
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
+	}
 }
 
 func GetUnusedHpasStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) {
diff --git a/pkg/kor/ingresses.go b/pkg/kor/ingresses.go
index b657dc4..22dbf8a 100644
--- a/pkg/kor/ingresses.go
+++ b/pkg/kor/ingresses.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -88,9 +89,11 @@ func processNamespaceIngresses(clientset kubernetes.Interface, namespace string)
 
 }
 
-func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceIngresses(clientset, namespace)
 		if err != nil {
@@ -98,8 +101,18 @@ func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset kuber
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Ingresses")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/multi.go b/pkg/kor/multi.go
index 9caee0e..cb85f07 100644
--- a/pkg/kor/multi.go
+++ b/pkg/kor/multi.go
@@ -1,8 +1,10 @@
 package kor
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
+	"os"
 	"strings"
 
 	"k8s.io/client-go/kubernetes"
@@ -53,10 +55,12 @@ func retrieveNamespaceDiffs(clientset kubernetes.Interface, namespace string, re
 	return allDiffs
 }
 
-func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourceNames string) {
+func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourceNames string, slackOpts SlackOpts) {
 	var clientset kubernetes.Interface
 	var namespaces []string
 
+	var outputBuffer bytes.Buffer
+
 	clientset = GetKubeClient(kubeconfig)
 
 	resourceList := strings.Split(resourceNames, ",")
@@ -65,8 +69,18 @@ func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourc
 	for _, namespace := range namespaces {
 		allDiffs := retrieveNamespaceDiffs(clientset, namespace, resourceList)
 		output := FormatOutputAll(namespace, allDiffs)
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/pdbs.go b/pkg/kor/pdbs.go
index b0458a0..9407503 100644
--- a/pkg/kor/pdbs.go
+++ b/pkg/kor/pdbs.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -48,9 +49,11 @@ func processNamespacePdbs(clientset kubernetes.Interface, namespace string) ([]s
 	return unusedPdbs, nil
 }
 
-func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespacePdbs(clientset, namespace)
 		if err != nil {
@@ -58,8 +61,18 @@ func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, clientset kubernetes
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Pdbs")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/pvc.go b/pkg/kor/pvc.go
index 4303566..b5a9b13 100644
--- a/pkg/kor/pvc.go
+++ b/pkg/kor/pvc.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -53,9 +54,11 @@ func processNamespacePvcs(clientset kubernetes.Interface, namespace string) ([]s
 	return diff, nil
 }
 
-func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespacePvcs(clientset, namespace)
 		if err != nil {
@@ -63,10 +66,19 @@ func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, clientset kubernetes
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Pvcs")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
 	}
 
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
+	}
 }
 
 func GetUnusedPvcsStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) {
diff --git a/pkg/kor/roles.go b/pkg/kor/roles.go
index 93ddc55..7bfb26d 100644
--- a/pkg/kor/roles.go
+++ b/pkg/kor/roles.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -66,9 +67,11 @@ func processNamespaceRoles(clientset kubernetes.Interface, namespace string) ([]
 
 }
 
-func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceRoles(clientset, namespace)
 		if err != nil {
@@ -76,8 +79,18 @@ func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, clientset kubernete
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Roles")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/secrets.go b/pkg/kor/secrets.go
index 9a42133..e41a748 100644
--- a/pkg/kor/secrets.go
+++ b/pkg/kor/secrets.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -140,9 +141,11 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string) ([
 
 }
 
-func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceSecret(clientset, namespace)
 		if err != nil {
@@ -150,8 +153,18 @@ func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, clientset kuberne
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Secrets")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/serviceaccounts.go b/pkg/kor/serviceaccounts.go
index b7e90a6..e031025 100644
--- a/pkg/kor/serviceaccounts.go
+++ b/pkg/kor/serviceaccounts.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -139,9 +140,11 @@ func processNamespaceSA(clientset kubernetes.Interface, namespace string) ([]str
 
 }
 
-func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceSA(clientset, namespace)
 		if err != nil {
@@ -149,8 +152,18 @@ func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset
 			continue
 		}
 		output := FormatOutput(namespace, diff, "ServiceAccount")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/services.go b/pkg/kor/services.go
index 14a2cb8..ca0059a 100644
--- a/pkg/kor/services.go
+++ b/pkg/kor/services.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -32,9 +33,11 @@ func ProcessNamespaceServices(clientset kubernetes.Interface, namespace string)
 	return endpointsWithoutSubsets, nil
 }
 
-func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := ProcessNamespaceServices(clientset, namespace)
 		if err != nil {
@@ -42,8 +45,18 @@ func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset kubern
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Services")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/slack.go b/pkg/kor/slack.go
new file mode 100644
index 0000000..2eb8c92
--- /dev/null
+++ b/pkg/kor/slack.go
@@ -0,0 +1,112 @@
+package kor
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"os"
+	"path/filepath"
+)
+
+type SendMessageToSlack interface {
+	SendToSlack(slackOpts SlackOpts, outputBuffer string) error
+}
+
+type SlackOpts struct {
+	WebhookURL string
+	Channel    string
+	Token      string
+}
+
+type SlackMessage struct {
+}
+
+func SendToSlack(sm SendMessageToSlack, slackOpts SlackOpts, outputBuffer string) error {
+	return sm.SendToSlack(slackOpts, outputBuffer)
+}
+
+func (sm SlackMessage) SendToSlack(slackOpts SlackOpts, outputBuffer string) error {
+	if slackOpts.WebhookURL != "" {
+		payload := []byte(`{"text": "` + outputBuffer + `"}`)
+		_, err := http.Post(slackOpts.WebhookURL, "application/json", bytes.NewBuffer(payload))
+
+		if err != nil {
+			return err
+		}
+		return nil
+	} else if slackOpts.Channel != "" && slackOpts.Token != "" {
+		fmt.Printf("Sending message to Slack channel %s...", slackOpts.Channel)
+		outputFilePath, _ := writeOutputToFile(outputBuffer)
+
+		var formData bytes.Buffer
+		writer := multipart.NewWriter(&formData)
+
+		fileWriter, err := writer.CreateFormFile("file", outputFilePath)
+		if err != nil {
+			return err
+		}
+		file, err := os.Open(outputFilePath)
+		if err != nil {
+			return err
+		}
+		defer file.Close()
+		_, err = io.Copy(fileWriter, file)
+		if err != nil {
+			return err
+		}
+
+		if err := writer.WriteField("channels", slackOpts.Channel); err != nil {
+			return err
+		}
+
+		writer.Close()
+
+		req, err := http.NewRequest("POST", "https://slack.com/api/files.upload", &formData)
+		if err != nil {
+			return err
+		}
+		req.Header.Set("Authorization", "Bearer "+slackOpts.Token)
+		req.Header.Set("Content-Type", writer.FormDataContentType())
+
+		client := &http.Client{}
+		resp, err := client.Do(req)
+		if err != nil {
+			return err
+		}
+		defer resp.Body.Close()
+
+		if resp.StatusCode != http.StatusOK {
+			return fmt.Errorf("slack API returned non-OK status code: %d", resp.StatusCode)
+		}
+
+		return nil
+	} else {
+		return errors.New("SlackOpts must contain either WebhookURL or Channel and Token")
+	}
+}
+
+func writeOutputToFile(outputBuffer string) (string, error) {
+	homeDir, err := os.UserHomeDir()
+	if err != nil {
+		return "", fmt.Errorf("failed to get user's home directory: %v", err)
+	}
+
+	outputFileName := "kor-scan-results.txt"
+	outputFilePath := filepath.Join(homeDir, outputFileName)
+
+	file, err := os.Create(outputFilePath)
+	if err != nil {
+		return "", fmt.Errorf("failed to create output file: %v", err)
+	}
+	defer file.Close()
+
+	_, err = file.WriteString(outputBuffer)
+	if err != nil {
+		return "", fmt.Errorf("failed to write output to file: %v", err)
+	}
+
+	return outputFilePath, nil
+}
diff --git a/pkg/kor/statefulsets.go b/pkg/kor/statefulsets.go
index 6c7102e..ead07c4 100644
--- a/pkg/kor/statefulsets.go
+++ b/pkg/kor/statefulsets.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -28,18 +29,30 @@ func ProcessNamespaceStatefulSets(clientset kubernetes.Interface, namespace stri
 	return statefulSetsWithoutReplicas, nil
 }
 
-func GetUnusedStatefulSets(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedStatefulSets(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := ProcessNamespaceStatefulSets(clientset, namespace)
 		if err != nil {
 			fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
 			continue
 		}
-		output := FormatOutput(namespace, diff, "StatefulSets")
-		fmt.Println(output)
-		fmt.Println()
+		output := FormatOutput(namespace, diff, "Statefulsets")
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 


Test patch:
diff --git a/pkg/kor/slack_test.go b/pkg/kor/slack_test.go
new file mode 100644
index 0000000..1875b92
--- /dev/null
+++ b/pkg/kor/slack_test.go
@@ -0,0 +1,74 @@
+package kor
+
+import (
+	"bytes"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+)
+
+type SendToSlackTestCase struct {
+	Name         string
+	SlackOpts    SlackOpts
+	OutputBuffer string
+}
+
+var testCases = []SendToSlackTestCase{
+	{
+		Name: "Test using WebhookURL",
+		SlackOpts: SlackOpts{
+			WebhookURL: "slack.webhookurl.com",
+		},
+		OutputBuffer: "Test message",
+	},
+	{
+		Name: "Test using Channel and Token",
+		SlackOpts: SlackOpts{
+			Channel: "your_channel",
+			Token:   "your_token",
+		},
+		OutputBuffer: "Test message",
+	},
+	{
+		Name:         "Test with empty SlackOpts",
+		SlackOpts:    SlackOpts{},
+		OutputBuffer: "Test message",
+	},
+}
+
+func TestSendToSlack(t *testing.T) {
+	for _, tc := range testCases {
+		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			if err := SendToSlack(SlackMessage{}, tc.SlackOpts, tc.OutputBuffer); err != nil {
+				t.Errorf("Expected no error, got %v", err)
+			}
+		}))
+
+		defer server.Close()
+	}
+}
+
+func TestWriteOutputToFile(t *testing.T) {
+	outputBuffer := bytes.Buffer{}
+	outputBuffer.WriteString("This is a test output.\n")
+	expectedOutput := outputBuffer.String()
+
+	outputFilePath, err := writeOutputToFile(expectedOutput)
+	if err != nil {
+		t.Errorf("Expected no error, got %v", err)
+	}
+
+	if _, err := os.Stat(outputFilePath); os.IsNotExist(err) {
+		t.Errorf("Expected output file to exist, got error: %v", err)
+	}
+
+	fileContent, err := os.ReadFile(outputFilePath)
+	if err != nil {
+		t.Errorf("Failed to read output file: %v", err)
+	}
+
+	if string(fileContent) != expectedOutput {
+		t.Errorf("Expected file content:\n%s\nGot:\n%s", expectedOutput, string(fileContent))
+	}
+}