Skip to content

Commit

Permalink
feat: add send to slack support (#77)
Browse files Browse the repository at this point in the history
* feat: add send to slack support

* feat: add send to slack support

* fix: add back logic to load kubeconfig from env

* fix: ci lint error

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: remove leftover func

* fix: remove leftover

* chore: send clientset instead of kubeconfig

* fix: lint errors

* test: add slack helper tests

* refactor: condense GetUnsedSlack functions into GetUnused functions

* refactor: condense GetUnsedSlack functions into GetUnused functions

* refactor: consolidate logic on a single SendToSlack function

* fix: address pull request comments

* fix: support secret for helm chart slack secrets
  • Loading branch information
patricktalmeida authored Sep 27, 2023
1 parent bed9ba5 commit 66f65b3
Show file tree
Hide file tree
Showing 31 changed files with 450 additions and 58 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,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]`.
Expand Down Expand Up @@ -149,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.

Expand Down
16 changes: 14 additions & 2 deletions charts/kor/templates/cronjob.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
16 changes: 16 additions & 0 deletions charts/kor/templates/secret.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
2 changes: 1 addition & 1 deletion cmd/kor/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var allCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedAll(includeExcludeLists, clientset)
kor.GetUnusedAll(includeExcludeLists, clientset, slackOpts)
}

},
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/configmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var configmapCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedConfigmaps(includeExcludeLists, clientset)
kor.GetUnusedConfigmaps(includeExcludeLists, clientset, slackOpts)
}

},
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var deployCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedDeployments(includeExcludeLists, clientset)
kor.GetUnusedDeployments(includeExcludeLists, clientset, slackOpts)
}

},
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/hpas.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var hpaCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedHpas(includeExcludeLists, clientset)
kor.GetUnusedHpas(includeExcludeLists, clientset, slackOpts)
}
},
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/ingresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var ingressCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedIngresses(includeExcludeLists, clientset)
kor.GetUnusedIngresses(includeExcludeLists, clientset, slackOpts)
}
},
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/pdbs.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var pdbCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedPdbs(includeExcludeLists, clientset)
kor.GetUnusedPdbs(includeExcludeLists, clientset, slackOpts)
}

},
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/pvc.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var pvcCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedPvcs(includeExcludeLists, clientset)
kor.GetUnusedPvcs(includeExcludeLists, clientset, slackOpts)
}
},
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var roleCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedRoles(includeExcludeLists, clientset)
kor.GetUnusedRoles(includeExcludeLists, clientset, slackOpts)
}

},
Expand Down
6 changes: 5 additions & 1 deletion cmd/kor/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -39,13 +39,17 @@ var (
outputFormat string
kubeconfig string
includeExcludeLists kor.IncludeExcludeLists
slackOpts kor.SlackOpts
)

func Execute() {
rootCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "Path to kubeconfig file (optional)")
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)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var secretCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedSecrets(includeExcludeLists, clientset)
kor.GetUnusedSecrets(includeExcludeLists, clientset, slackOpts)
}

},
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/serviceaccounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var serviceAccountCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedServiceAccounts(includeExcludeLists, clientset)
kor.GetUnusedServiceAccounts(includeExcludeLists, clientset, slackOpts)
}

},
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var serviceCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedServices(includeExcludeLists, clientset)
kor.GetUnusedServices(includeExcludeLists, clientset, slackOpts)
}

},
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/statefulsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var stsCmd = &cobra.Command{
fmt.Println(response)
}
} else {
kor.GetUnusedStatefulSets(includeExcludeLists, clientset)
kor.GetUnusedStatefulSets(includeExcludeLists, clientset, slackOpts)
}

},
Expand Down
21 changes: 18 additions & 3 deletions pkg/kor/all.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kor

import (
"bytes"
"encoding/json"
"fmt"
"os"
Expand Down Expand Up @@ -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)
Expand All @@ -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())
}
}

Expand Down
19 changes: 16 additions & 3 deletions pkg/kor/confimgmaps.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kor

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -128,18 +129,30 @@ 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 {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
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())
}
}

Expand Down
19 changes: 16 additions & 3 deletions pkg/kor/deployments.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kor

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -32,18 +33,30 @@ 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 {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
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())
}
}

Expand Down
18 changes: 15 additions & 3 deletions pkg/kor/hpas.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kor

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -79,20 +80,31 @@ 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 {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
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) {
Expand Down
Loading

0 comments on commit 66f65b3

Please sign in to comment.