Repository: yonahd/kor
Pull request #90 (2023-09-21 19:30:38)

Update helm charts with deployment support
for #40 and #87.

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/yonahd/kor/pull/90?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) Report
Patch and project coverage have no change.
> Comparison is base [(`bed9ba5`)](https://app.codecov.io/gh/yonahd/kor/commit/bed9ba5114691da2455f03e5dacec1d5e3c087c2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) 49.36% compared to head [(`539b2d1`)](https://app.codecov.io/gh/yonahd/kor/pull/90?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) 49.36%.

: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      #90   +/-   ##
=======================================
  Coverage   49.36%   49.36%           
=======================================
  Files          15       15           
  Lines        1493     1493           
=======================================
  Hits          737      737           
  Misses        657      657           
  Partials       99       99           
```




</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/yonahd/kor/pull/90?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: Can you add a ServiceMonitor to the chart 

Patch:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..3977912
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: 'Feature: Add this super cool feature '
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/README.md b/README.md
index 3b86aa7..b1ba0fd 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,17 @@ docker run --rm -i yonahdissen/kor
 docker run --rm -i -v "/path/to/.kube/config:/root/.kube/config" yonahdissen/kor all
 ```
 
+### Helm
+Run as a cronjob in your Cluster
+```sh
+helm upgrade -i kor \
+    --namespace kor \
+    --create-namespace \
+    ./charts/kor
+```
+
+For more information see [in cluster usage](#in-cluster-usage) 
+
 ## Usage
 
 Kor provides various subcommands to identify and list unused resources. The available commands are:
@@ -58,14 +69,18 @@ Kor provides various subcommands to identify and list unused resources. The avai
 - `pvc` - Gets unused PVCs for the specified namespace or all namespaces.
 - `ingress` - Gets unused Ingresses for the specified namespace or all namespaces.
 - `pdb` - Gets unused PDBs for the specified namespace or all namespaces.
+- `exporter` - Export Prometheus metrics.
 
 ### 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]`.
@@ -149,6 +164,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/deployment.yaml b/charts/kor/templates/deployment.yaml
new file mode 100644
index 0000000..3abf505
--- /dev/null
+++ b/charts/kor/templates/deployment.yaml
@@ -0,0 +1,66 @@
+{{- if .Values.prometheusExporter.enabled -}}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ .Values.prometheusExporter.name }}
+  labels:
+    app: {{ .Values.prometheusExporter.name }}
+spec:
+  {{- with .Values.prometheusExporter.deployment.imagePullSecrets }}
+  imagePullSecrets:
+    {{- toYaml . | nindent }}
+  {{- end }}
+  replicas: {{ .Values.prometheusExporter.deployment.replicaCount | default 1 }}
+  selector:
+    matchLabels:
+      app: {{ .Values.prometheusExporter.name }}
+  template:
+    metadata:
+      labels:
+        app: {{ .Values.prometheusExporter.name }}
+    spec: 
+      serviceAccountName: {{ include "kor.serviceAccountName" . }}
+      containers:
+        - name: "{{ .Values.prometheusExporter.name }}-container"
+          {{- with .Values.prometheusExporter.deployment.securityContext }}
+          securityContext:
+            {{ toYaml . | nindent 12 }}
+          {{- end }}
+          image: "{{ .Values.prometheusExporter.deployment.image.repository }}:{{ .Values.prometheusExporter.deployment.image.tag }}"
+          command: ["/bin/sh", "-c"]
+          args: ["{{ .Values.prometheusExporter.deployment.command }}"]
+          ports:
+          - containerPort: 8080
+            name: http
+            protocol: TCP
+          {{- with .Values.prometheusExporter.deployment.resources }}
+          resources:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+          {{- if .Values.prometheusExporter.exporterInterval }}
+          env:
+          - name: EXPORTER_INTERVAL
+            value: {{ .Values.prometheusExporter.exporterInterval }}
+          {{- end}}
+      {{- with .Values.prometheusExporter.deployment.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.prometheusExporter.deployment.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.prometheusExporter.deployment.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+          terminationMessagePath: "/dev/termination-log"
+          imagePullPolicy: {{ .Values.prometheusExporter.deployment.imagePullPolicy }}
+      restartPolicy: {{ .Values.prometheusExporter.deployment.restartPolicy }}
+      terminationGracePeriodSeconds: 30
+      dnsPolicy: ClusterFirst
+      {{- with .Values.prometheusExporter.deployment.podSecurityContext }}
+      securityContext: 
+        {{- toYaml . | nindent 8}}
+      {{- end }}
+{{- end }}
\ No newline at end of file
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/charts/kor/templates/service.yaml b/charts/kor/templates/service.yaml
new file mode 100644
index 0000000..d8b5827
--- /dev/null
+++ b/charts/kor/templates/service.yaml
@@ -0,0 +1,20 @@
+{{- if .Values.prometheusExporter.enabled -}}
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ .Values.prometheusExporter.name }}
+  annotations:
+    prometheus.io/scrape: "true"
+    prometheus.io/path: /metrics
+    prometheus.io/port: "8080"
+  labels:
+    app: {{ .Values.prometheusExporter.name }}
+spec:
+  type: {{ .Values.prometheusExporter.service.type }}
+  selector:
+    app: {{ .Values.prometheusExporter.name }}
+  ports:
+    - name: metrics
+      port: {{ .Values.prometheusExporter.service.port }}
+      targetPort: http
+{{- end }}
\ No newline at end of file
diff --git a/charts/kor/templates/servicemonitor.yaml b/charts/kor/templates/servicemonitor.yaml
new file mode 100644
index 0000000..28247bf
--- /dev/null
+++ b/charts/kor/templates/servicemonitor.yaml
@@ -0,0 +1,44 @@
+{{- if and ( .Capabilities.APIVersions.Has "monitoring.coreos.com/v1" ) ( .Values.prometheusExporter.serviceMonitor.enabled ) }}
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  labels:
+    app: {{ .Values.prometheusExporter.name }}
+  name: {{ .Values.prometheusExporter.name }}
+{{- if .Values.prometheusExporter.serviceMonitor.namespace }}
+  namespace: {{ .Values.prometheusExporter.serviceMonitor.namespace }}
+{{- end }}
+spec:
+  endpoints:
+  - targetPort: 8080
+{{- if .Values.prometheusExporter.serviceMonitor.interval }}
+    interval: {{ .Values.prometheusExporter.serviceMonitor.interval }}
+{{- end }}
+{{- if .Values.prometheusExporter.serviceMonitor.telemetryPath }}
+    path: {{ .Values.prometheusExporter.serviceMonitor.telemetryPath }}
+{{- end }}
+{{- if .Values.prometheusExporter.serviceMonitor.timeout }}
+    scrapeTimeout: {{ .Values.prometheusExporter.serviceMonitor.timeout }}
+{{- end }}
+{{- if .Values.prometheusExporter.serviceMonitor.metricRelabelings }}
+    metricRelabelings:
+{{ toYaml .Values.prometheusExporter.serviceMonitor.metricRelabelings | indent 4 }}
+{{- end }}
+{{- if .Values.prometheusExporter.serviceMonitor.relabelings }}
+    relabelings:
+{{ toYaml .Values.prometheusExporter.serviceMonitor.relabelings | indent 4 }}
+{{- end }}
+  jobLabel: {{ .Values.prometheusExporter.name }}
+  namespaceSelector:
+    matchNames:
+    - {{ .Release.Namespace }}
+  selector:
+    matchLabels:
+      app: {{ .Values.prometheusExporter.name }}
+{{- if .Values.prometheusExporter.serviceMonitor.targetLabels }}
+  targetLabels:
+{{- range .Values.prometheusExporter.serviceMonitor.targetLabels }}
+    - {{ . }}
+{{- end }}
+{{- end }}
+{{- end }}
diff --git a/charts/kor/values.yaml b/charts/kor/values.yaml
index d3077b2..771638d 100644
--- a/charts/kor/values.yaml
+++ b/charts/kor/values.yaml
@@ -1,5 +1,5 @@
 cronJob:
-  enabled: true
+  enabled: false
   name: kor
   schedule: "0 1 * * 1"
   image:
@@ -14,6 +14,61 @@ cronJob:
   successfulJobsHistoryLimit: 3
   failedJobsHistoryLimit: 2
 
+prometheusExporter:
+  enabled: true
+  name: kor-exporter
+  # time in minutes, default is 10 minutes
+  exporterInterval: ""
+  deployment:
+    image:
+      repository: yonahdissen/kor
+      tag: latest
+    command: kor exporter
+    restartPolicy: Always
+    imagePullPolicy: Always
+    imagePullSecrets: []
+    podSecurityContext: {}
+    replicaCount: 1
+    resources: {}
+      # limits:
+      #   cpu: 100m
+      #   memory: 128Mi
+      # requests:
+      #   cpu: 100m
+      #   memory: 128Mi
+
+    securityContext: {}
+      # capabilities:
+      #   drop:
+      #   - ALL
+      # readOnlyRootFilesystem: true
+      # runAsNonRoot: true
+      # runAsUser: 1000
+
+    nodeSelector: {}
+    tolerations: []
+    affinity: {}
+
+  service:
+    type: ClusterIP
+    port: 8080
+
+  serviceMonitor:
+    enabled: true
+    # Set the namespace the ServiceMonitor should be deployed, if empty namespace will be .Release.Namespace
+    namespace: ""
+    # Service monitor labels
+    labels: {}
+    # Set how frequently Prometheus should scrape
+    interval: 30s
+    telemetryPath: /metrics
+    # Set timeout for scrape
+    timeout: 10s
+    relabelings: []
+    # Set of labels to transfer on the Kubernetes Service onto the target.
+    targetLabels: []
+    metricRelabelings: []
+
 serviceAccount:
   # Specifies whether a service account should be created
   create: true
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/exporter.go b/cmd/kor/exporter.go
new file mode 100644
index 0000000..5f8a800
--- /dev/null
+++ b/cmd/kor/exporter.go
@@ -0,0 +1,21 @@
+package kor
+
+import (
+	"github.com/spf13/cobra"
+	"github.com/yonahd/kor/pkg/kor"
+)
+
+var exporterCmd = &cobra.Command{
+	Use:   "exporter",
+	Short: "start prometheus exporter",
+	Args:  cobra.ExactArgs(0),
+	Run: func(cmd *cobra.Command, args []string) {
+		clientset := kor.GetKubeClient(kubeconfig)
+		kor.Exporter(includeExcludeLists, clientset, "json")
+
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(exporterCmd)
+}
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/go.mod b/go.mod
index 3a4d873..18dd26b 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.20
 
 require (
 	github.com/olekukonko/tablewriter v0.0.5
+	github.com/prometheus/client_golang v1.16.0
 	github.com/spf13/cobra v1.7.0
 	k8s.io/api v0.28.2
 	k8s.io/apimachinery v0.28.2
@@ -13,6 +14,8 @@ require (
 )
 
 require (
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/emicklei/go-restful/v3 v3.10.1 // indirect
 	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
@@ -32,12 +35,16 @@ require (
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-runewidth v0.0.14 // indirect
+	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/onsi/ginkgo/v2 v2.11.0 // indirect
 	github.com/onsi/gomega v1.27.10 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
+	github.com/prometheus/client_model v0.3.0 // indirect
+	github.com/prometheus/common v0.42.0 // indirect
+	github.com/prometheus/procfs v0.10.1 // indirect
 	github.com/rivo/uniseg v0.4.4 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	golang.org/x/net v0.13.0 // indirect
diff --git a/go.sum b/go.sum
index 423cae2..4f4e8cb 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,7 @@
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 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=
@@ -21,7 +25,9 @@ github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
 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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@@ -57,6 +63,8 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
 github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -74,6 +82,14 @@ 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/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
+github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
+github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
+github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
+github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
+github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
+github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
+github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
 github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -107,6 +123,7 @@ golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
 golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
 golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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=
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/exporter.go b/pkg/kor/exporter.go
new file mode 100644
index 0000000..e218068
--- /dev/null
+++ b/pkg/kor/exporter.go
@@ -0,0 +1,72 @@
+package kor
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"os"
+	"strconv"
+	"time"
+
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+	"k8s.io/client-go/kubernetes"
+)
+
+var (
+	orphanedResourcesCounter = prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "kubernetes_orphaned_resources",
+			Help: "Orphaned resources in Kubernetes",
+		},
+		[]string{"kind", "namespace", "resourceName"},
+	)
+)
+
+func init() {
+	prometheus.MustRegister(orphanedResourcesCounter)
+}
+
+// TODO: add option to change port / url !?
+func Exporter(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) {
+	http.Handle("/metrics", promhttp.Handler())
+	fmt.Println("Server listening on :8080")
+	go exportMetrics(includeExcludeLists, clientset, outputFormat) // Start exporting metrics in the background
+	if err := http.ListenAndServe(":8080", nil); err != nil {
+		fmt.Println(err)
+	}
+}
+
+func exportMetrics(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) {
+	exporterInterval := os.Getenv("EXPORTER_INTERVAL")
+	if exporterInterval == "" {
+		exporterInterval = "10"
+	}
+	exporterIntervalValue, err := strconv.Atoi(exporterInterval)
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+
+	for {
+		if korOutput, err := GetUnusedAllStructured(includeExcludeLists, clientset, outputFormat); err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		} else {
+			var data map[string]map[string][]string
+			if err := json.Unmarshal([]byte(korOutput), &data); err != nil {
+				fmt.Println("Error parsing JSON:", err)
+				return
+			}
+
+			for namespace, resources := range data {
+				for kind, resourceList := range resources {
+					for _, resourceName := range resourceList {
+						orphanedResourcesCounter.WithLabelValues(kind, namespace, resourceName).Set(1)
+					}
+				}
+			}
+			time.Sleep(time.Duration(exporterIntervalValue) * time.Minute)
+		}
+	}
+}
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))
+	}
+}