diff --git a/cmd/kor/all.go b/cmd/kor/all.go index d4d7d54d..4126bb33 100644 --- a/cmd/kor/all.go +++ b/cmd/kor/all.go @@ -14,7 +14,7 @@ var allCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedAll(includeExcludeLists,filterOptions, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedAll(includeExcludeLists, filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/cmd/kor/configmaps.go b/cmd/kor/configmaps.go index 88fece47..da2431b7 100644 --- a/cmd/kor/configmaps.go +++ b/cmd/kor/configmaps.go @@ -14,7 +14,7 @@ var configmapCmd = &cobra.Command{ Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedConfigmaps(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedConfigmaps(includeExcludeLists, filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/cmd/kor/deployments.go b/cmd/kor/deployments.go index c0b19e95..ba548a5f 100644 --- a/cmd/kor/deployments.go +++ b/cmd/kor/deployments.go @@ -14,7 +14,7 @@ var deployCmd = &cobra.Command{ Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedDeployments(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedDeployments(includeExcludeLists, filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/cmd/kor/exporter.go b/cmd/kor/exporter.go index cd492263..0f80f946 100644 --- a/cmd/kor/exporter.go +++ b/cmd/kor/exporter.go @@ -11,7 +11,7 @@ var exporterCmd = &cobra.Command{ Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - kor.Exporter(includeExcludeLists, clientset, "json", slackOpts) + kor.Exporter(includeExcludeLists, clientset, "json", opts) }, } diff --git a/cmd/kor/hpas.go b/cmd/kor/hpas.go index 86d422c7..1a0ccdcc 100644 --- a/cmd/kor/hpas.go +++ b/cmd/kor/hpas.go @@ -15,7 +15,7 @@ var hpaCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedHpas(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedHpas(includeExcludeLists, filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/cmd/kor/ingresses.go b/cmd/kor/ingresses.go index 3a798659..6edffe64 100644 --- a/cmd/kor/ingresses.go +++ b/cmd/kor/ingresses.go @@ -15,7 +15,7 @@ var ingressCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedIngresses(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedIngresses(includeExcludeLists, filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/cmd/kor/pdbs.go b/cmd/kor/pdbs.go index c2f1f92d..b2d0acb3 100644 --- a/cmd/kor/pdbs.go +++ b/cmd/kor/pdbs.go @@ -15,7 +15,7 @@ var pdbCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedPdbs(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedPdbs(includeExcludeLists, filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/cmd/kor/pvc.go b/cmd/kor/pvc.go index d2d10688..22457c29 100644 --- a/cmd/kor/pvc.go +++ b/cmd/kor/pvc.go @@ -15,7 +15,7 @@ var pvcCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedPvcs(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedPvcs(includeExcludeLists, filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/cmd/kor/roles.go b/cmd/kor/roles.go index 8a3aa75f..622c65ef 100644 --- a/cmd/kor/roles.go +++ b/cmd/kor/roles.go @@ -15,7 +15,7 @@ var roleCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedRoles(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedRoles(includeExcludeLists, filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/cmd/kor/root.go b/cmd/kor/root.go index b47161a0..58860b90 100644 --- a/cmd/kor/root.go +++ b/cmd/kor/root.go @@ -28,7 +28,7 @@ var rootCmd = &cobra.Command{ fmt.Println(response) } } else { - kor.GetUnusedMulti(includeExcludeLists, kubeconfig, resourceNames, slackOpts) + kor.GetUnusedMulti(includeExcludeLists, kubeconfig, resourceNames, opts) } } else { fmt.Printf("Subcommand %q was not found, try using 'kor --help' for available subcommands", args[0]) @@ -40,7 +40,7 @@ var ( outputFormat string kubeconfig string includeExcludeLists kor.IncludeExcludeLists - slackOpts kor.SlackOpts + opts kor.Opts filterOptions = kor.NewFilterOptions() ) @@ -50,9 +50,14 @@ 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, json or yaml)") - 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.") + rootCmd.PersistentFlags().StringVar(&opts.WebhookURL, "slack-webhook-url", "", "Slack webhook URL to send notifications to") + rootCmd.PersistentFlags().StringVar(&opts.Channel, "slack-channel", "", "Slack channel to send notifications to. --slack-channel requires --slack-auth-token to be set.") + rootCmd.PersistentFlags().StringVar(&opts.Token, "slack-auth-token", "", "Slack auth token to send notifications to. --slack-auth-token requires --slack-channel to be set.") + rootCmd.PersistentFlags().BoolVar(&opts.DeleteFlag, "delete", false, "Delete unused resources") + rootCmd.PersistentFlags().BoolVar(&opts.NoInteractive, "no-interactive", false, "Do not prompt for confirmation when deleting resources. Be careful using this flag!") + rootCmd.PersistentFlags().StringVar(&opts.WebhookURL, "slack-webhook-url", "", "Slack webhook URL to send notifications to") + rootCmd.PersistentFlags().StringVar(&opts.Channel, "slack-channel", "", "Slack channel to send notifications to. --slack-channel requires --slack-auth-token to be set.") + rootCmd.PersistentFlags().StringVar(&opts.Token, "slack-auth-token", "", "Slack auth token to send notifications to. --slack-auth-token requires --slack-channel to be set.") addFilterOptionsFlag(rootCmd, filterOptions) if err := filterOptions.Validate(); err != nil { diff --git a/cmd/kor/secrets.go b/cmd/kor/secrets.go index 04397464..89aaac0d 100644 --- a/cmd/kor/secrets.go +++ b/cmd/kor/secrets.go @@ -15,7 +15,7 @@ var secretCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedSecrets(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedSecrets(includeExcludeLists, filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/cmd/kor/serviceaccounts.go b/cmd/kor/serviceaccounts.go index 4d706746..7f6b5164 100644 --- a/cmd/kor/serviceaccounts.go +++ b/cmd/kor/serviceaccounts.go @@ -15,7 +15,7 @@ var serviceAccountCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedServiceAccounts(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedServiceAccounts(includeExcludeLists, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/cmd/kor/services.go b/cmd/kor/services.go index b17e27d2..d5a3f7ff 100644 --- a/cmd/kor/services.go +++ b/cmd/kor/services.go @@ -15,7 +15,7 @@ var serviceCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedServices(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedServices(includeExcludeLists, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/cmd/kor/statefulsets.go b/cmd/kor/statefulsets.go index f01e5842..8e7b39c7 100644 --- a/cmd/kor/statefulsets.go +++ b/cmd/kor/statefulsets.go @@ -15,7 +15,7 @@ var stsCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedStatefulSets(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil { + if response, err := kor.GetUnusedStatefulSets(includeExcludeLists, filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { fmt.Println(response) diff --git a/pkg/kor/all.go b/pkg/kor/all.go index 1d5b1065..a5556c22 100644 --- a/pkg/kor/all.go +++ b/pkg/kor/all.go @@ -19,8 +19,8 @@ type ResourceDiff struct { diff []string } -func getUnusedCMs(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff { - cmDiff, err := processNamespaceCM(clientset, namespace, opts) +func getUnusedCMs(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ResourceDiff { + cmDiff, err := processNamespaceCM(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "configmaps", namespace, err) } @@ -37,8 +37,8 @@ func getUnusedSVCs(clientset kubernetes.Interface, namespace string) ResourceDif return namespaceSVCDiff } -func getUnusedSecrets(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff { - secretDiff, err := processNamespaceSecret(clientset, namespace, opts) +func getUnusedSecrets(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ResourceDiff { + secretDiff, err := processNamespaceSecret(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "secrets", namespace, err) } @@ -55,8 +55,8 @@ func getUnusedServiceAccounts(clientset kubernetes.Interface, namespace string) return namespaceSADiff } -func getUnusedDeployments(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff { - deployDiff, err := ProcessNamespaceDeployments(clientset, namespace, opts) +func getUnusedDeployments(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ResourceDiff { + deployDiff, err := ProcessNamespaceDeployments(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "deployments", namespace, err) } @@ -64,8 +64,8 @@ func getUnusedDeployments(clientset kubernetes.Interface, namespace string, opts return namespaceSADiff } -func getUnusedStatefulSets(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff { - stsDiff, err := ProcessNamespaceStatefulSets(clientset, namespace, opts) +func getUnusedStatefulSets(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ResourceDiff { + stsDiff, err := ProcessNamespaceStatefulSets(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "statefulSets", namespace, err) } @@ -73,8 +73,8 @@ func getUnusedStatefulSets(clientset kubernetes.Interface, namespace string, opt return namespaceSADiff } -func getUnusedRoles(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff { - roleDiff, err := processNamespaceRoles(clientset, namespace, opts) +func getUnusedRoles(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ResourceDiff { + roleDiff, err := processNamespaceRoles(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "roles", namespace, err) } @@ -82,8 +82,8 @@ func getUnusedRoles(clientset kubernetes.Interface, namespace string, opts *Filt return namespaceSADiff } -func getUnusedHpas(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff { - hpaDiff, err := processNamespaceHpas(clientset, namespace, opts) +func getUnusedHpas(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ResourceDiff { + hpaDiff, err := processNamespaceHpas(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "hpas", namespace, err) } @@ -91,8 +91,8 @@ func getUnusedHpas(clientset kubernetes.Interface, namespace string, opts *Filte return namespaceHpaDiff } -func getUnusedPvcs(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff { - pvcDiff, err := processNamespacePvcs(clientset, namespace, opts) +func getUnusedPvcs(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ResourceDiff { + pvcDiff, err := processNamespacePvcs(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "pvcs", namespace, err) } @@ -100,8 +100,8 @@ func getUnusedPvcs(clientset kubernetes.Interface, namespace string, opts *Filte return namespacePvcDiff } -func getUnusedIngresses(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff { - ingressDiff, err := processNamespaceIngresses(clientset, namespace, opts) +func getUnusedIngresses(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ResourceDiff { + ingressDiff, err := processNamespaceIngresses(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "ingresses", namespace, err) } @@ -109,8 +109,8 @@ func getUnusedIngresses(clientset kubernetes.Interface, namespace string, opts * return namespaceIngressDiff } -func getUnusedPdbs(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff { - pdbDiff, err := processNamespacePdbs(clientset, namespace, opts) +func getUnusedPdbs(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ResourceDiff { + pdbDiff, err := processNamespacePdbs(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "pdbs", namespace, err) } @@ -118,7 +118,7 @@ func getUnusedPdbs(clientset kubernetes.Interface, namespace string, opts *Filte return namespacePdbDiff } -func GetUnusedAll(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedAll(includeExcludeLists IncludeExcludeLists, filterOpts *FilterOptions, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) @@ -126,27 +126,27 @@ func GetUnusedAll(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, for _, namespace := range namespaces { var allDiffs []ResourceDiff - namespaceCMDiff := getUnusedCMs(clientset, namespace, opts) + namespaceCMDiff := getUnusedCMs(clientset, namespace, filterOpts) allDiffs = append(allDiffs, namespaceCMDiff) namespaceSVCDiff := getUnusedSVCs(clientset, namespace) allDiffs = append(allDiffs, namespaceSVCDiff) - namespaceSecretDiff := getUnusedSecrets(clientset, namespace, opts) + namespaceSecretDiff := getUnusedSecrets(clientset, namespace, filterOpts) allDiffs = append(allDiffs, namespaceSecretDiff) namespaceSADiff := getUnusedServiceAccounts(clientset, namespace) allDiffs = append(allDiffs, namespaceSADiff) - namespaceDeploymentDiff := getUnusedDeployments(clientset, namespace, opts) + namespaceDeploymentDiff := getUnusedDeployments(clientset, namespace, filterOpts) allDiffs = append(allDiffs, namespaceDeploymentDiff) - namespaceStatefulsetDiff := getUnusedStatefulSets(clientset, namespace, opts) + namespaceStatefulsetDiff := getUnusedStatefulSets(clientset, namespace, filterOpts) allDiffs = append(allDiffs, namespaceStatefulsetDiff) - namespaceRoleDiff := getUnusedRoles(clientset, namespace, opts) + namespaceRoleDiff := getUnusedRoles(clientset, namespace, filterOpts) allDiffs = append(allDiffs, namespaceRoleDiff) - namespaceHpaDiff := getUnusedHpas(clientset, namespace, opts) + namespaceHpaDiff := getUnusedHpas(clientset, namespace, filterOpts) allDiffs = append(allDiffs, namespaceHpaDiff) - namespacePvcDiff := getUnusedPvcs(clientset, namespace, opts) + namespacePvcDiff := getUnusedPvcs(clientset, namespace, filterOpts) allDiffs = append(allDiffs, namespacePvcDiff) - namespaceIngressDiff := getUnusedIngresses(clientset, namespace, opts) + namespaceIngressDiff := getUnusedIngresses(clientset, namespace, filterOpts) allDiffs = append(allDiffs, namespaceIngressDiff) - namespacePdbDiff := getUnusedPdbs(clientset, namespace, opts) + namespacePdbDiff := getUnusedPdbs(clientset, namespace, filterOpts) allDiffs = append(allDiffs, namespacePdbDiff) output := FormatOutputAll(namespace, allDiffs) @@ -166,7 +166,7 @@ func GetUnusedAll(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, return "", err } - unusedAll, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedAll, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/configmaps_test.go b/pkg/kor/configmaps_test.go index b1633754..e0bd2458 100644 --- a/pkg/kor/configmaps_test.go +++ b/pkg/kor/configmaps_test.go @@ -176,13 +176,15 @@ func TestGetUnusedConfigmapsStructured(t *testing.T) { ExcludeListStr: "", } - slackopts := SlackOpts{ - WebhookURL: "", - Channel: "", - Token: "", + opts := Opts{ + WebhookURL: "", + Channel: "", + Token: "", + DeleteFlag: false, + NoInteractive: true, } - output, err := GetUnusedConfigmaps(includeExcludeLists, &FilterOptions{}, clientset, "json", slackopts) + output, err := GetUnusedConfigmaps(includeExcludeLists, &FilterOptions{}, clientset, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedConfigmapsStructured: %v", err) } diff --git a/pkg/kor/confimgmaps.go b/pkg/kor/confimgmaps.go index a8e353a3..efa87f88 100644 --- a/pkg/kor/confimgmaps.go +++ b/pkg/kor/confimgmaps.go @@ -83,7 +83,7 @@ func retrieveUsedCM(clientset kubernetes.Interface, namespace string) ([]string, return volumesCM, volumesProjectedCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, nil } -func retrieveConfigMapNames(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { +func retrieveConfigMapNames(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { configmaps, err := clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err @@ -92,12 +92,12 @@ func retrieveConfigMapNames(clientset kubernetes.Interface, namespace string, op for _, configmap := range configmaps.Items { // checks if the resource has any labels that match the excluded selector specified in opts.ExcludeLabels. // If it does, the resource is skipped. - if excluded, _ := HasExcludedLabel(configmap.Labels, opts.ExcludeLabels); excluded { + if excluded, _ := HasExcludedLabel(configmap.Labels, filterOpts.ExcludeLabels); excluded { continue } // checks if the resource's age (measured from its last modified time) matches the included criteria // specified by the filter options. - if included, _ := HasIncludedAge(configmap.CreationTimestamp, opts); !included { + if included, _ := HasIncludedAge(configmap.CreationTimestamp, filterOpts); !included { continue } @@ -110,7 +110,7 @@ func retrieveConfigMapNames(clientset kubernetes.Interface, namespace string, op return names, nil } -func processNamespaceCM(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { +func processNamespaceCM(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { volumesCM, volumesProjectedCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, err := retrieveUsedCM(clientset, namespace) if err != nil { return nil, err @@ -123,7 +123,7 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, opts * envFromContainerCM = RemoveDuplicatesAndSort(envFromContainerCM) envFromInitContainerCM = RemoveDuplicatesAndSort(envFromInitContainerCM) - configMapNames, err := retrieveConfigMapNames(clientset, namespace, opts) + configMapNames, err := retrieveConfigMapNames(clientset, namespace, filterOpts) if err != nil { return nil, err } @@ -139,19 +139,24 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, opts * } -func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, filterOpts *FilterOptions, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) for _, namespace := range namespaces { - diff, err := processNamespaceCM(clientset, namespace, opts) + diff, err := processNamespaceCM(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } - output := FormatOutput(namespace, diff, "Config Maps") + if opts.DeleteFlag { + if diff, err = DeleteResource(diff, clientset, namespace, "ConfigMap", opts.NoInteractive); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete ConfigMap %s in namespace %s: %v\n", diff, namespace, err) + } + } + output := FormatOutput(namespace, diff, "Configmaps") outputBuffer.WriteString(output) outputBuffer.WriteString("\n") @@ -165,7 +170,7 @@ func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, opts *FilterOp return "", err } - unusedCMs, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedCMs, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/delete.go b/pkg/kor/delete.go new file mode 100644 index 00000000..5242191d --- /dev/null +++ b/pkg/kor/delete.go @@ -0,0 +1,87 @@ +package kor + +import ( + "context" + "fmt" + "os" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +func DeleteResourceCmd() map[string]func(clientset kubernetes.Interface, namespace, name string) error { + var deleteResourceApiMap = map[string]func(clientset kubernetes.Interface, namespace, name string) error{ + "ConfigMap": func(clientset kubernetes.Interface, namespace, name string) error { + return clientset.CoreV1().ConfigMaps(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + }, + "Secret": func(clientset kubernetes.Interface, namespace, name string) error { + return clientset.CoreV1().Secrets(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + }, + "Service": func(clientset kubernetes.Interface, namespace, name string) error { + return clientset.CoreV1().Services(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + }, + "Deployment": func(clientset kubernetes.Interface, namespace, name string) error { + return clientset.AppsV1().Deployments(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + }, + "HPA": func(clientset kubernetes.Interface, namespace, name string) error { + return clientset.AutoscalingV1().HorizontalPodAutoscalers(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + }, + "Ingress": func(clientset kubernetes.Interface, namespace, name string) error { + return clientset.NetworkingV1beta1().Ingresses(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + }, + "PDB": func(clientset kubernetes.Interface, namespace, name string) error { + return clientset.PolicyV1beta1().PodDisruptionBudgets(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + }, + "Roles": func(clientset kubernetes.Interface, namespace, name string) error { + return clientset.RbacV1().Roles(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + }, + "PVC": func(clientset kubernetes.Interface, namespace, name string) error { + return clientset.CoreV1().PersistentVolumeClaims(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + }, + "StatefulSet": func(clientset kubernetes.Interface, namespace, name string) error { + return clientset.AppsV1().StatefulSets(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + }, + "ServiceAccount": func(clientset kubernetes.Interface, namespace, name string) error { + return clientset.CoreV1().ServiceAccounts(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + }, + } + + return deleteResourceApiMap +} + +func DeleteResource(diff []string, clientset kubernetes.Interface, namespace, resourceType string, noInteractive bool) ([]string, error) { + deletedDiff := []string{} + + for _, resourceName := range diff { + deleteFunc, exists := DeleteResourceCmd()[resourceType] + if !exists { + fmt.Printf("Resource type '%s' is not supported\n", resourceName) + continue + } + + if !noInteractive { + fmt.Printf("Do you want to delete %s %s in namespace %s? (Y/N): ", resourceType, resourceName, namespace) + var confirmation string + _, err := fmt.Scanf("%s", &confirmation) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to read input: %v\n", err) + continue + } + + if strings.ToLower(confirmation) != "y" && strings.ToLower(confirmation) != "yes" { + deletedDiff = append(deletedDiff, resourceName) + continue + } + } + + fmt.Printf("Deleting %s %s in namespace %s\n", resourceType, resourceName, namespace) + if err := deleteFunc(clientset, namespace, resourceName); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete %s %s in namespace %s: %v\n", resourceType, resourceName, namespace, err) + continue + } + deletedDiff = append(deletedDiff, resourceName+"-DELETED") + } + + return deletedDiff, nil +} diff --git a/pkg/kor/delete_test.go b/pkg/kor/delete_test.go new file mode 100644 index 00000000..2d7ed959 --- /dev/null +++ b/pkg/kor/delete_test.go @@ -0,0 +1,39 @@ +package kor + +import ( + "testing" + + "k8s.io/client-go/kubernetes/fake" +) + +func TestDeleteResource(t *testing.T) { + clientset := fake.NewSimpleClientset() + + tests := []struct { + name string + diff []string + resourceType string + expectedDiff []string + expectedError bool + }{ + { + name: "Test deletion confirmation", + diff: []string{"resource1", "resource2"}, + resourceType: "ConfigMap", + expectedDiff: []string{"resource1-DELETED", "resource2"}, + expectedError: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + deletedDiff, _ := DeleteResource(test.diff, clientset, "namespace", test.resourceType, true) + + for i, deleted := range deletedDiff { + if deleted != test.expectedDiff[i] { + t.Errorf("Expected: %s, Got: %s", test.expectedDiff[i], deleted) + } + } + }) + } +} diff --git a/pkg/kor/deployments.go b/pkg/kor/deployments.go index 92aa1e2d..717151f1 100644 --- a/pkg/kor/deployments.go +++ b/pkg/kor/deployments.go @@ -11,7 +11,7 @@ import ( "k8s.io/client-go/kubernetes" ) -func ProcessNamespaceDeployments(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { +func ProcessNamespaceDeployments(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { deploymentsList, err := clientset.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err @@ -26,12 +26,12 @@ func ProcessNamespaceDeployments(clientset kubernetes.Interface, namespace strin // checks if the resource has any labels that match the excluded selector specified in opts.ExcludeLabels. // If it does, the resource is skipped. - if excluded, _ := HasExcludedLabel(deployment.Labels, opts.ExcludeLabels); excluded { + if excluded, _ := HasExcludedLabel(deployment.Labels, filterOpts.ExcludeLabels); excluded { continue } // checks if the resource's age (measured from its last modified time) matches the included criteria // specified by the filter options. - if included, _ := HasIncludedAge(deployment.CreationTimestamp, opts); !included { + if included, _ := HasIncludedAge(deployment.CreationTimestamp, filterOpts); !included { continue } @@ -43,19 +43,24 @@ func ProcessNamespaceDeployments(clientset kubernetes.Interface, namespace strin return deploymentsWithoutReplicas, nil } -func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, filterOpts *FilterOptions, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) for _, namespace := range namespaces { - diff, err := ProcessNamespaceDeployments(clientset, namespace, opts) + diff, err := ProcessNamespaceDeployments(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } - output := FormatOutput(namespace, diff, "Deployments") + if opts.DeleteFlag { + if diff, err = DeleteResource(diff, clientset, namespace, "Deployment", opts.NoInteractive); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete Deployment %s in namespace %s: %v\n", diff, namespace, err) + } + } + output := FormatOutput(namespace, diff, "Deployments") outputBuffer.WriteString(output) outputBuffer.WriteString("\n") @@ -69,7 +74,7 @@ func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, opts *FilterO return "", err } - unusedDeployments, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedDeployments, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/deployments_test.go b/pkg/kor/deployments_test.go index b6e8377b..fae808fa 100644 --- a/pkg/kor/deployments_test.go +++ b/pkg/kor/deployments_test.go @@ -67,13 +67,15 @@ func TestGetUnusedDeploymentsStructured(t *testing.T) { ExcludeListStr: "", } - slackopts := SlackOpts{ - WebhookURL: "", - Channel: "", - Token: "", + opts := Opts{ + WebhookURL: "", + Channel: "", + Token: "", + DeleteFlag: false, + NoInteractive: true, } - output, err := GetUnusedDeployments(includeExcludeLists, &FilterOptions{}, clientset, "json", slackopts) + output, err := GetUnusedDeployments(includeExcludeLists, &FilterOptions{}, clientset, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedDeploymentsStructured: %v", err) } diff --git a/pkg/kor/exporter.go b/pkg/kor/exporter.go index 555ab457..42a2bc9e 100644 --- a/pkg/kor/exporter.go +++ b/pkg/kor/exporter.go @@ -28,16 +28,16 @@ func init() { } // TODO: add option to change port / url !? -func Exporter(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) { +func Exporter(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, opts Opts) { http.Handle("/metrics", promhttp.Handler()) fmt.Println("Server listening on :8080") - go exportMetrics(includeExcludeLists, clientset, outputFormat, slackOpts) // Start exporting metrics in the background + go exportMetrics(includeExcludeLists, clientset, outputFormat, opts) // 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, slackOpts SlackOpts) { +func exportMetrics(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, opts Opts) { exporterInterval := os.Getenv("EXPORTER_INTERVAL") if exporterInterval == "" { exporterInterval = "10" @@ -49,7 +49,7 @@ func exportMetrics(includeExcludeLists IncludeExcludeLists, clientset kubernetes } for { - if korOutput, err := GetUnusedAll(includeExcludeLists, nil, clientset, outputFormat, slackOpts); err != nil { + if korOutput, err := GetUnusedAll(includeExcludeLists, nil, clientset, outputFormat, opts); err != nil { fmt.Println(err) os.Exit(1) } else { diff --git a/pkg/kor/filter_options.go b/pkg/kor/filter_options.go index 2d00314f..75de5833 100644 --- a/pkg/kor/filter_options.go +++ b/pkg/kor/filter_options.go @@ -85,21 +85,21 @@ func HasExcludedLabel(resourcelabels map[string]string, excludeSelector string) // range specified by older-than and newer-than flags. // If older-than or newer-than is zero, no age limit is applied. // If both flags are set, an error is returned. -func HasIncludedAge(creationTime metav1.Time, opts *FilterOptions) (bool, error) { - if opts.OlderThan == "" && opts.NewerThan == "" { +func HasIncludedAge(creationTime metav1.Time, filterOpts *FilterOptions) (bool, error) { + if filterOpts.OlderThan == "" && filterOpts.NewerThan == "" { return true, nil } // The function returns an error if both flags are set is because it does not make sense to // query for resources that are both older than and newer than a certain duration. // For example, if you set --older-than=1h and --newer-than=30m, you are asking for resources // that are older than 1 hour and newer than 30 minutes, which is impossible! - if opts.OlderThan != "" && opts.NewerThan != "" { + if filterOpts.OlderThan != "" && filterOpts.NewerThan != "" { return false, errors.New("invalid flags: older-than and newer-than cannot be used together") } // Parse the older-than flag value into a time.Duration value - if opts.OlderThan != "" { - olderThan, err := time.ParseDuration(opts.OlderThan) + if filterOpts.OlderThan != "" { + olderThan, err := time.ParseDuration(filterOpts.OlderThan) if err != nil { return false, err } @@ -107,8 +107,8 @@ func HasIncludedAge(creationTime metav1.Time, opts *FilterOptions) (bool, error) } // Parse the newer-than flag value into a time.Duration value - if opts.NewerThan != "" { - newerThan, err := time.ParseDuration(opts.NewerThan) + if filterOpts.NewerThan != "" { + newerThan, err := time.ParseDuration(filterOpts.NewerThan) if err != nil { return false, err } diff --git a/pkg/kor/hpas.go b/pkg/kor/hpas.go index f74af2ac..0e7c568d 100644 --- a/pkg/kor/hpas.go +++ b/pkg/kor/hpas.go @@ -37,7 +37,7 @@ func getStatefulSetNames(clientset kubernetes.Interface, namespace string) ([]st return names, nil } -func extractUnusedHpas(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { +func extractUnusedHpas(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { deploymentNames, err := getDeploymentNames(clientset, namespace) if err != nil { return nil, err @@ -59,12 +59,12 @@ func extractUnusedHpas(clientset kubernetes.Interface, namespace string, opts *F // checks if the resource has any labels that match the excluded selector specified in opts.ExcludeLabels. // If it does, the resource is skipped. - if excluded, _ := HasExcludedLabel(hpa.Labels, opts.ExcludeLabels); excluded { + if excluded, _ := HasExcludedLabel(hpa.Labels, filterOpts.ExcludeLabels); excluded { continue } // checks if the resource's age (measured from its last modified time) matches the included criteria // specified by the filter options. - if included, _ := HasIncludedAge(hpa.CreationTimestamp, opts); !included { + if included, _ := HasIncludedAge(hpa.CreationTimestamp, filterOpts); !included { continue } @@ -82,27 +82,32 @@ func extractUnusedHpas(clientset kubernetes.Interface, namespace string, opts *F return diff, nil } -func processNamespaceHpas(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { - unusedHpas, err := extractUnusedHpas(clientset, namespace, opts) +func processNamespaceHpas(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { + unusedHpas, err := extractUnusedHpas(clientset, namespace, filterOpts) if err != nil { return nil, err } return unusedHpas, nil } -func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, filterOpts *FilterOptions, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) for _, namespace := range namespaces { - diff, err := processNamespaceHpas(clientset, namespace, opts) + diff, err := processNamespaceHpas(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } - output := FormatOutput(namespace, diff, "Hpas") + if opts.DeleteFlag { + if diff, err = DeleteResource(diff, clientset, namespace, "HPA", opts.NoInteractive); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete HPA %s in namespace %s: %v\n", diff, namespace, err) + } + } + output := FormatOutput(namespace, diff, "HPAs") outputBuffer.WriteString(output) outputBuffer.WriteString("\n") @@ -116,7 +121,7 @@ func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, return "", err } - unusedHpas, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedHpas, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/hpas_test.go b/pkg/kor/hpas_test.go index eaeb6471..14ec5518 100644 --- a/pkg/kor/hpas_test.go +++ b/pkg/kor/hpas_test.go @@ -75,13 +75,15 @@ func TestGetUnusedHpasStructured(t *testing.T) { ExcludeListStr: "", } - slackopts := SlackOpts{ - WebhookURL: "", - Channel: "", - Token: "", + opts := Opts{ + WebhookURL: "", + Channel: "", + Token: "", + DeleteFlag: false, + NoInteractive: true, } - output, err := GetUnusedHpas(includeExcludeLists, &FilterOptions{}, clientset, "json", slackopts) + output, err := GetUnusedHpas(includeExcludeLists, &FilterOptions{}, clientset, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedHpasStructured: %v", err) } diff --git a/pkg/kor/ingresses.go b/pkg/kor/ingresses.go index a36ad917..737b347b 100644 --- a/pkg/kor/ingresses.go +++ b/pkg/kor/ingresses.go @@ -25,7 +25,7 @@ func validateServiceBackend(clientset kubernetes.Interface, namespace string, ba return true } -func retrieveUsedIngress(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { +func retrieveUsedIngress(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { ingresses, err := clientset.NetworkingV1().Ingresses(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err @@ -40,12 +40,12 @@ func retrieveUsedIngress(clientset kubernetes.Interface, namespace string, opts // checks if the resource has any labels that match the excluded selector specified in opts.ExcludeLabels. // If it does, the resource is skipped. - if excluded, _ := HasExcludedLabel(ingress.Labels, opts.ExcludeLabels); excluded { + if excluded, _ := HasExcludedLabel(ingress.Labels, filterOpts.ExcludeLabels); excluded { continue } // checks if the resource's age (measured from its last modified time) matches the included criteria // specified by the filter options. - if included, _ := HasIncludedAge(ingress.CreationTimestamp, opts); !included { + if included, _ := HasIncludedAge(ingress.CreationTimestamp, filterOpts); !included { continue } @@ -88,8 +88,8 @@ func retrieveIngressNames(clientset kubernetes.Interface, namespace string) ([]s return names, nil } -func processNamespaceIngresses(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { - usedIngresses, err := retrieveUsedIngress(clientset, namespace, opts) +func processNamespaceIngresses(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { + usedIngresses, err := retrieveUsedIngress(clientset, namespace, filterOpts) if err != nil { return nil, err } @@ -103,19 +103,24 @@ func processNamespaceIngresses(clientset kubernetes.Interface, namespace string, } -func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, filterOpts *FilterOptions, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) for _, namespace := range namespaces { - diff, err := processNamespaceIngresses(clientset, namespace, opts) + diff, err := processNamespaceIngresses(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } - output := FormatOutput(namespace, diff, "Ingresses") + if opts.DeleteFlag { + if diff, err = DeleteResource(diff, clientset, namespace, "Ingress", opts.NoInteractive); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete Ingress %s in namespace %s: %v\n", diff, namespace, err) + } + } + output := FormatOutput(namespace, diff, "Ingresses") outputBuffer.WriteString(output) outputBuffer.WriteString("\n") @@ -129,7 +134,7 @@ func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, opts *FilterOpt return "", err } - unusedIngresses, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedIngresses, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/ingresses_test.go b/pkg/kor/ingresses_test.go index 6c00cb68..39e8fdb3 100644 --- a/pkg/kor/ingresses_test.go +++ b/pkg/kor/ingresses_test.go @@ -79,13 +79,15 @@ func TestGetUnusedIngressesStructured(t *testing.T) { ExcludeListStr: "", } - slackopts := SlackOpts{ - WebhookURL: "", - Channel: "", - Token: "", + opts := Opts{ + WebhookURL: "", + Channel: "", + Token: "", + DeleteFlag: false, + NoInteractive: true, } - output, err := GetUnusedIngresses(includeExcludeLists, &FilterOptions{}, clientset, "json", slackopts) + output, err := GetUnusedIngresses(includeExcludeLists, &FilterOptions{}, clientset, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedIngressesStructured: %v", err) } diff --git a/pkg/kor/kor.go b/pkg/kor/kor.go index 80f65749..e7585e51 100644 --- a/pkg/kor/kor.go +++ b/pkg/kor/kor.go @@ -27,6 +27,14 @@ type IncludeExcludeLists struct { ExcludeListStr string } +type Opts struct { + DeleteFlag bool + NoInteractive bool + WebhookURL string + Channel string + Token string +} + func RemoveDuplicatesAndSort(slice []string) []string { uniqueSet := make(map[string]bool) for _, item := range slice { @@ -191,11 +199,11 @@ func CalculateResourceDifference(usedResourceNames []string, allResourceNames [] return difference } -func unusedResourceFormatter(outputFormat string, outputBuffer bytes.Buffer, slackOpts SlackOpts, jsonResponse []byte) (string, error) { +func unusedResourceFormatter(outputFormat string, outputBuffer bytes.Buffer, opts Opts, jsonResponse []byte) (string, error) { if outputFormat == "table" { - if slackOpts != (SlackOpts{}) { - if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil { + if opts.WebhookURL != "" || opts.Channel != "" && opts.Token != "" { + if err := SendToSlack(SlackMessage{}, opts, outputBuffer.String()); err != nil { fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err) os.Exit(1) } diff --git a/pkg/kor/multi.go b/pkg/kor/multi.go index e7fe21bc..9f3479fe 100644 --- a/pkg/kor/multi.go +++ b/pkg/kor/multi.go @@ -55,7 +55,7 @@ func retrieveNamespaceDiffs(clientset kubernetes.Interface, namespace string, re return allDiffs } -func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourceNames string, slackOpts SlackOpts) { +func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourceNames string, opts Opts) { var clientset kubernetes.Interface var namespaces []string @@ -74,8 +74,8 @@ func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourc outputBuffer.WriteString("\n") } - if slackOpts != (SlackOpts{}) { - if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil { + if opts.WebhookURL != "" || opts.Channel != "" && opts.Token != "" { + if err := SendToSlack(SlackMessage{}, opts, outputBuffer.String()); err != nil { fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err) os.Exit(1) } diff --git a/pkg/kor/pdbs.go b/pkg/kor/pdbs.go index 38f2f4fd..20497efe 100644 --- a/pkg/kor/pdbs.go +++ b/pkg/kor/pdbs.go @@ -12,7 +12,7 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" ) -func processNamespacePdbs(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { +func processNamespacePdbs(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { var unusedPdbs []string pdbs, err := clientset.PolicyV1().PodDisruptionBudgets(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { @@ -26,12 +26,12 @@ func processNamespacePdbs(clientset kubernetes.Interface, namespace string, opts // checks if the resource has any labels that match the excluded selector specified in opts.ExcludeLabels. // If it does, the resource is skipped. - if excluded, _ := HasExcludedLabel(pdb.Labels, opts.ExcludeLabels); excluded { + if excluded, _ := HasExcludedLabel(pdb.Labels, filterOpts.ExcludeLabels); excluded { continue } // checks if the resource's age (measured from its last modified time) matches the included criteria // specified by the filter options. - if included, _ := HasIncludedAge(pdb.CreationTimestamp, opts); !included { + if included, _ := HasIncludedAge(pdb.CreationTimestamp, filterOpts); !included { continue } @@ -59,19 +59,24 @@ func processNamespacePdbs(clientset kubernetes.Interface, namespace string, opts return unusedPdbs, nil } -func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, filterOpts *FilterOptions, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) for _, namespace := range namespaces { - diff, err := processNamespacePdbs(clientset, namespace, opts) + diff, err := processNamespacePdbs(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } - output := FormatOutput(namespace, diff, "Pdbs") + if opts.DeleteFlag { + if diff, err = DeleteResource(diff, clientset, namespace, "PDB", opts.NoInteractive); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete PDB %s in namespace %s: %v\n", diff, namespace, err) + } + } + output := FormatOutput(namespace, diff, "PDBs") outputBuffer.WriteString(output) outputBuffer.WriteString("\n") @@ -85,7 +90,7 @@ func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, return "", err } - unusedPdbs, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedPdbs, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/pdbs_test.go b/pkg/kor/pdbs_test.go index 95531bd0..785aacc0 100644 --- a/pkg/kor/pdbs_test.go +++ b/pkg/kor/pdbs_test.go @@ -85,13 +85,15 @@ func TestGetUnusedPdbsStructured(t *testing.T) { ExcludeListStr: "", } - slackopts := SlackOpts{ - WebhookURL: "", - Channel: "", - Token: "", + opts := Opts{ + WebhookURL: "", + Channel: "", + Token: "", + DeleteFlag: false, + NoInteractive: true, } - output, err := GetUnusedPdbs(includeExcludeLists, &FilterOptions{}, clientset, "json", slackopts) + output, err := GetUnusedPdbs(includeExcludeLists, &FilterOptions{}, clientset, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedPdbsStructured: %v", err) } diff --git a/pkg/kor/pvc.go b/pkg/kor/pvc.go index 7b2f8359..8245decd 100644 --- a/pkg/kor/pvc.go +++ b/pkg/kor/pvc.go @@ -30,7 +30,7 @@ func retreiveUsedPvcs(clientset kubernetes.Interface, namespace string) ([]strin return usedPvcs, err } -func processNamespacePvcs(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { +func processNamespacePvcs(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { pvcs, err := clientset.CoreV1().PersistentVolumeClaims(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err @@ -43,12 +43,12 @@ func processNamespacePvcs(clientset kubernetes.Interface, namespace string, opts // checks if the resource has any labels that match the excluded selector specified in opts.ExcludeLabels. // If it does, the resource is skipped. - if excluded, _ := HasExcludedLabel(pvc.Labels, opts.ExcludeLabels); excluded { + if excluded, _ := HasExcludedLabel(pvc.Labels, filterOpts.ExcludeLabels); excluded { continue } // checks if the resource's age (measured from its last modified time) matches the included criteria // specified by the filter options. - if included, _ := HasIncludedAge(pvc.CreationTimestamp, opts); !included { + if included, _ := HasIncludedAge(pvc.CreationTimestamp, filterOpts); !included { continue } @@ -64,19 +64,24 @@ func processNamespacePvcs(clientset kubernetes.Interface, namespace string, opts return diff, nil } -func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, filterOpts *FilterOptions, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) for _, namespace := range namespaces { - diff, err := processNamespacePvcs(clientset, namespace, opts) + diff, err := processNamespacePvcs(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } - output := FormatOutput(namespace, diff, "Pvcs") + if opts.DeleteFlag { + if diff, err = DeleteResource(diff, clientset, namespace, "PVC", opts.NoInteractive); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete PVC %s in namespace %s: %v\n", diff, namespace, err) + } + } + output := FormatOutput(namespace, diff, "PVCs") outputBuffer.WriteString(output) outputBuffer.WriteString("\n") @@ -90,7 +95,7 @@ func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, return "", err } - unusedPvcs, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedPvcs, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/pvc_test.go b/pkg/kor/pvc_test.go index 8b876fb3..9b0124a6 100644 --- a/pkg/kor/pvc_test.go +++ b/pkg/kor/pvc_test.go @@ -90,13 +90,15 @@ func TestGetUnusedPvcsStructured(t *testing.T) { ExcludeListStr: "", } - slackopts := SlackOpts{ - WebhookURL: "", - Channel: "", - Token: "", + opts := Opts{ + WebhookURL: "", + Channel: "", + Token: "", + DeleteFlag: false, + NoInteractive: true, } - output, err := GetUnusedPvcs(includeExcludeLists, &FilterOptions{}, clientset, "json", slackopts) + output, err := GetUnusedPvcs(includeExcludeLists, &FilterOptions{}, clientset, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedPvcsStructured: %v", err) } diff --git a/pkg/kor/roles.go b/pkg/kor/roles.go index 44d5c4bd..bb5d7223 100644 --- a/pkg/kor/roles.go +++ b/pkg/kor/roles.go @@ -12,7 +12,7 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" ) -func retrieveUsedRoles(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { +func retrieveUsedRoles(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { // Get a list of all role bindings in the specified namespace roleBindings, err := clientset.RbacV1().RoleBindings(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { @@ -23,12 +23,12 @@ func retrieveUsedRoles(clientset kubernetes.Interface, namespace string, opts *F for _, rb := range roleBindings.Items { // checks if the resource has any labels that match the excluded selector specified in opts.ExcludeLabels. // If it does, the resource is skipped. - if excluded, _ := HasExcludedLabel(rb.Labels, opts.ExcludeLabels); excluded { + if excluded, _ := HasExcludedLabel(rb.Labels, filterOpts.ExcludeLabels); excluded { continue } // checks if the resource's age (measured from its last modified time) matches the included criteria // specified by the filter options. - if included, _ := HasIncludedAge(rb.CreationTimestamp, opts); !included { + if included, _ := HasIncludedAge(rb.CreationTimestamp, filterOpts); !included { continue } @@ -59,8 +59,8 @@ func retrieveRoleNames(clientset kubernetes.Interface, namespace string) ([]stri return names, nil } -func processNamespaceRoles(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { - usedRoles, err := retrieveUsedRoles(clientset, namespace, opts) +func processNamespaceRoles(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { + usedRoles, err := retrieveUsedRoles(clientset, namespace, filterOpts) if err != nil { return nil, err } @@ -77,19 +77,24 @@ func processNamespaceRoles(clientset kubernetes.Interface, namespace string, opt } -func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, filterOpts *FilterOptions, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) for _, namespace := range namespaces { - diff, err := processNamespaceRoles(clientset, namespace, opts) + diff, err := processNamespaceRoles(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } - output := FormatOutput(namespace, diff, "Roles") + if opts.DeleteFlag { + if diff, err = DeleteResource(diff, clientset, namespace, "Role", opts.NoInteractive); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete Role %s in namespace %s: %v\n", diff, namespace, err) + } + } + output := FormatOutput(namespace, diff, "Roles") outputBuffer.WriteString(output) outputBuffer.WriteString("\n") @@ -103,7 +108,7 @@ func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, opts *FilterOptions return "", err } - unusedRoles, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedRoles, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/roles_test.go b/pkg/kor/roles_test.go index e1af5004..3a963029 100644 --- a/pkg/kor/roles_test.go +++ b/pkg/kor/roles_test.go @@ -100,13 +100,15 @@ func TestGetUnusedRolesStructured(t *testing.T) { ExcludeListStr: "", } - slackopts := SlackOpts{ - WebhookURL: "", - Channel: "", - Token: "", + opts := Opts{ + WebhookURL: "", + Channel: "", + Token: "", + DeleteFlag: false, + NoInteractive: true, } - output, err := GetUnusedRoles(includeExcludeLists, &FilterOptions{}, clientset, "json", slackopts) + output, err := GetUnusedRoles(includeExcludeLists, &FilterOptions{}, clientset, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedRolesStructured: %v", err) } diff --git a/pkg/kor/secrets.go b/pkg/kor/secrets.go index 7b44012f..da001bba 100644 --- a/pkg/kor/secrets.go +++ b/pkg/kor/secrets.go @@ -37,7 +37,7 @@ func retrieveIngressTLS(clientset kubernetes.Interface, namespace string) ([]str } -func retrieveUsedSecret(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, []string, []string, []string, []string, []string, error) { +func retrieveUsedSecret(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, []string, []string, []string, []string, []string, error) { var envSecrets []string var envSecrets2 []string var volumeSecrets []string @@ -93,7 +93,7 @@ func retrieveUsedSecret(clientset kubernetes.Interface, namespace string, opts * return envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, tlsSecrets, nil } -func retrieveSecretNames(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { +func retrieveSecretNames(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { secrets, err := clientset.CoreV1().Secrets(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err @@ -106,12 +106,12 @@ func retrieveSecretNames(clientset kubernetes.Interface, namespace string, opts // checks if the resource has any labels that match the excluded selector specified in opts.ExcludeLabels. // If it does, the resource is skipped. - if excluded, _ := HasExcludedLabel(secret.Labels, opts.ExcludeLabels); excluded { + if excluded, _ := HasExcludedLabel(secret.Labels, filterOpts.ExcludeLabels); excluded { continue } // checks if the resource's age (measured from its last modified time) matches the included criteria // specified by the filter options. - if included, _ := HasIncludedAge(secret.CreationTimestamp, opts); !included { + if included, _ := HasIncludedAge(secret.CreationTimestamp, filterOpts); !included { continue } @@ -122,8 +122,8 @@ func retrieveSecretNames(clientset kubernetes.Interface, namespace string, opts return names, nil } -func processNamespaceSecret(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { - envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, tlsSecrets, err := retrieveUsedSecret(clientset, namespace, opts) +func processNamespaceSecret(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { + envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, tlsSecrets, err := retrieveUsedSecret(clientset, namespace, filterOpts) if err != nil { return nil, err } @@ -135,7 +135,7 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string, op pullSecrets = RemoveDuplicatesAndSort(pullSecrets) tlsSecrets = RemoveDuplicatesAndSort(tlsSecrets) - secretNames, err := retrieveSecretNames(clientset, namespace, opts) + secretNames, err := retrieveSecretNames(clientset, namespace, filterOpts) if err != nil { return nil, err } @@ -151,19 +151,24 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string, op } -func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, filterOpts *FilterOptions, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) for _, namespace := range namespaces { - diff, err := processNamespaceSecret(clientset, namespace, opts) + diff, err := processNamespaceSecret(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } - output := FormatOutput(namespace, diff, "Secrets") + if opts.DeleteFlag { + if diff, err = DeleteResource(diff, clientset, namespace, "Secret", opts.NoInteractive); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete Secret %s in namespace %s: %v\n", diff, namespace, err) + } + } + output := FormatOutput(namespace, diff, "Secrets") outputBuffer.WriteString(output) outputBuffer.WriteString("\n") @@ -177,7 +182,7 @@ func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, opts *FilterOptio return "", err } - unusedSecrets, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedSecrets, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/secrets_test.go b/pkg/kor/secrets_test.go index 9d3e44f8..c4def346 100644 --- a/pkg/kor/secrets_test.go +++ b/pkg/kor/secrets_test.go @@ -244,13 +244,15 @@ func TestGetUnusedSecretsStructured(t *testing.T) { ExcludeListStr: "", } - slackopts := SlackOpts{ - WebhookURL: "", - Channel: "", - Token: "", + opts := Opts{ + WebhookURL: "", + Channel: "", + Token: "", + DeleteFlag: false, + NoInteractive: true, } - output, err := GetUnusedSecrets(includeExcludeLists, &FilterOptions{}, clientset, "json", slackopts) + output, err := GetUnusedSecrets(includeExcludeLists, &FilterOptions{}, clientset, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedSecretsStructured: %v", err) } diff --git a/pkg/kor/serviceaccounts.go b/pkg/kor/serviceaccounts.go index 31717a1a..a6f3da0a 100644 --- a/pkg/kor/serviceaccounts.go +++ b/pkg/kor/serviceaccounts.go @@ -139,7 +139,7 @@ func processNamespaceSA(clientset kubernetes.Interface, namespace string) ([]str } -func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) @@ -151,8 +151,13 @@ func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } - output := FormatOutput(namespace, diff, "ServiceAccount") + if opts.DeleteFlag { + if diff, err = DeleteResource(diff, clientset, namespace, "Serviceaccount", opts.NoInteractive); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete Serviceaccount %s in namespace %s: %v\n", diff, namespace, err) + } + } + output := FormatOutput(namespace, diff, "Serviceaccounts") outputBuffer.WriteString(output) outputBuffer.WriteString("\n") @@ -166,7 +171,7 @@ func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset return "", err } - unusedServiceAccounts, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedServiceAccounts, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/serviceaccounts_test.go b/pkg/kor/serviceaccounts_test.go index b03956b3..3a44cd9d 100644 --- a/pkg/kor/serviceaccounts_test.go +++ b/pkg/kor/serviceaccounts_test.go @@ -175,13 +175,15 @@ func TestGetUnusedServiceAccountsStructured(t *testing.T) { ExcludeListStr: "", } - slackopts := SlackOpts{ - WebhookURL: "", - Channel: "", - Token: "", + opts := Opts{ + WebhookURL: "", + Channel: "", + Token: "", + DeleteFlag: false, + NoInteractive: true, } - output, err := GetUnusedServiceAccounts(includeExcludeLists, clientset, "json", slackopts) + output, err := GetUnusedServiceAccounts(includeExcludeLists, clientset, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedServiceAccountsStructured: %v", err) } diff --git a/pkg/kor/services.go b/pkg/kor/services.go index 7124201f..7d191268 100644 --- a/pkg/kor/services.go +++ b/pkg/kor/services.go @@ -32,7 +32,7 @@ func ProcessNamespaceServices(clientset kubernetes.Interface, namespace string) return endpointsWithoutSubsets, nil } -func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) @@ -44,8 +44,13 @@ func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset kubern fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } - output := FormatOutput(namespace, diff, "Services") + if opts.DeleteFlag { + if diff, err = DeleteResource(diff, clientset, namespace, "Service", opts.NoInteractive); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete Service %s in namespace %s: %v\n", diff, namespace, err) + } + } + output := FormatOutput(namespace, diff, "Services") outputBuffer.WriteString(output) outputBuffer.WriteString("\n") @@ -59,7 +64,7 @@ func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset kubern return "", err } - unusedServices, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedServices, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/services_test.go b/pkg/kor/services_test.go index 9526b456..a54c37ed 100644 --- a/pkg/kor/services_test.go +++ b/pkg/kor/services_test.go @@ -65,13 +65,15 @@ func TestGetUnusedServicesStructured(t *testing.T) { ExcludeListStr: "", } - slackopts := SlackOpts{ - WebhookURL: "", - Channel: "", - Token: "", + opts := Opts{ + WebhookURL: "", + Channel: "", + Token: "", + DeleteFlag: false, + NoInteractive: true, } - output, err := GetUnusedServices(includeExcludeLists, clientset, "json", slackopts) + output, err := GetUnusedServices(includeExcludeLists, clientset, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedServicesStructured: %v", err) } diff --git a/pkg/kor/slack.go b/pkg/kor/slack.go index 2eb8c926..8bb3cb6d 100644 --- a/pkg/kor/slack.go +++ b/pkg/kor/slack.go @@ -12,33 +12,27 @@ import ( ) type SendMessageToSlack interface { - SendToSlack(slackOpts SlackOpts, outputBuffer string) error -} - -type SlackOpts struct { - WebhookURL string - Channel string - Token string + SendToSlack(opts Opts, outputBuffer string) error } type SlackMessage struct { } -func SendToSlack(sm SendMessageToSlack, slackOpts SlackOpts, outputBuffer string) error { - return sm.SendToSlack(slackOpts, outputBuffer) +func SendToSlack(sm SendMessageToSlack, opts Opts, outputBuffer string) error { + return sm.SendToSlack(opts, outputBuffer) } -func (sm SlackMessage) SendToSlack(slackOpts SlackOpts, outputBuffer string) error { - if slackOpts.WebhookURL != "" { +func (sm SlackMessage) SendToSlack(opts Opts, outputBuffer string) error { + if opts.WebhookURL != "" { payload := []byte(`{"text": "` + outputBuffer + `"}`) - _, err := http.Post(slackOpts.WebhookURL, "application/json", bytes.NewBuffer(payload)) + _, err := http.Post(opts.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) + } else if opts.Channel != "" && opts.Token != "" { + fmt.Printf("Sending message to Slack channel %s...", opts.Channel) outputFilePath, _ := writeOutputToFile(outputBuffer) var formData bytes.Buffer @@ -58,7 +52,7 @@ func (sm SlackMessage) SendToSlack(slackOpts SlackOpts, outputBuffer string) err return err } - if err := writer.WriteField("channels", slackOpts.Channel); err != nil { + if err := writer.WriteField("channels", opts.Channel); err != nil { return err } @@ -68,7 +62,7 @@ func (sm SlackMessage) SendToSlack(slackOpts SlackOpts, outputBuffer string) err if err != nil { return err } - req.Header.Set("Authorization", "Bearer "+slackOpts.Token) + req.Header.Set("Authorization", "Bearer "+opts.Token) req.Header.Set("Content-Type", writer.FormDataContentType()) client := &http.Client{} diff --git a/pkg/kor/slack_test.go b/pkg/kor/slack_test.go index 1875b92e..f19b5389 100644 --- a/pkg/kor/slack_test.go +++ b/pkg/kor/slack_test.go @@ -10,29 +10,29 @@ import ( type SendToSlackTestCase struct { Name string - SlackOpts SlackOpts + Opts Opts OutputBuffer string } var testCases = []SendToSlackTestCase{ { Name: "Test using WebhookURL", - SlackOpts: SlackOpts{ + Opts: Opts{ WebhookURL: "slack.webhookurl.com", }, OutputBuffer: "Test message", }, { Name: "Test using Channel and Token", - SlackOpts: SlackOpts{ + Opts: Opts{ Channel: "your_channel", Token: "your_token", }, OutputBuffer: "Test message", }, { - Name: "Test with empty SlackOpts", - SlackOpts: SlackOpts{}, + Name: "Test with empty Opts", + Opts: Opts{}, OutputBuffer: "Test message", }, } @@ -40,7 +40,7 @@ var testCases = []SendToSlackTestCase{ 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 { + if err := SendToSlack(SlackMessage{}, tc.Opts, tc.OutputBuffer); err != nil { t.Errorf("Expected no error, got %v", err) } })) diff --git a/pkg/kor/statefulsets.go b/pkg/kor/statefulsets.go index 2b6a0390..c5957918 100644 --- a/pkg/kor/statefulsets.go +++ b/pkg/kor/statefulsets.go @@ -11,7 +11,7 @@ import ( "k8s.io/client-go/kubernetes" ) -func ProcessNamespaceStatefulSets(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ([]string, error) { +func ProcessNamespaceStatefulSets(clientset kubernetes.Interface, namespace string, filterOpts *FilterOptions) ([]string, error) { statefulSetsList, err := clientset.AppsV1().StatefulSets(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err @@ -22,12 +22,12 @@ func ProcessNamespaceStatefulSets(clientset kubernetes.Interface, namespace stri for _, statefulSet := range statefulSetsList.Items { // checks if the resource has any labels that match the excluded selector specified in opts.ExcludeLabels. // If it does, the resource is skipped. - if excluded, _ := HasExcludedLabel(statefulSet.Labels, opts.ExcludeLabels); excluded { + if excluded, _ := HasExcludedLabel(statefulSet.Labels, filterOpts.ExcludeLabels); excluded { continue } // checks if the resource's age (measured from its last modified time) matches the included criteria // specified by the filter options. - if included, _ := HasIncludedAge(statefulSet.CreationTimestamp, opts); !included { + if included, _ := HasIncludedAge(statefulSet.CreationTimestamp, filterOpts); !included { continue } @@ -39,19 +39,23 @@ func ProcessNamespaceStatefulSets(clientset kubernetes.Interface, namespace stri return statefulSetsWithoutReplicas, nil } -func GetUnusedStatefulSets(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) { +func GetUnusedStatefulSets(includeExcludeLists IncludeExcludeLists, filterOpts *FilterOptions, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) { var outputBuffer bytes.Buffer namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) for _, namespace := range namespaces { - diff, err := ProcessNamespaceStatefulSets(clientset, namespace, opts) + diff, err := ProcessNamespaceStatefulSets(clientset, namespace, filterOpts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } + if opts.DeleteFlag { + if diff, err = DeleteResource(diff, clientset, namespace, "Statefulset", opts.NoInteractive); err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete Statefulset %s in namespace %s: %v\n", diff, namespace, err) + } + } output := FormatOutput(namespace, diff, "Statefulsets") - outputBuffer.WriteString(output) outputBuffer.WriteString("\n") @@ -65,7 +69,7 @@ func GetUnusedStatefulSets(includeExcludeLists IncludeExcludeLists, opts *Filter return "", err } - unusedStatefulsets, err := unusedResourceFormatter(outputFormat, outputBuffer, slackOpts, jsonResponse) + unusedStatefulsets, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/statefulsets_test.go b/pkg/kor/statefulsets_test.go index df5f51a0..c6c1fe21 100644 --- a/pkg/kor/statefulsets_test.go +++ b/pkg/kor/statefulsets_test.go @@ -67,13 +67,15 @@ func TestGetUnusedStatefulSetsStructured(t *testing.T) { ExcludeListStr: "", } - slackopts := SlackOpts{ - WebhookURL: "", - Channel: "", - Token: "", + opts := Opts{ + WebhookURL: "", + Channel: "", + Token: "", + DeleteFlag: false, + NoInteractive: true, } - output, err := GetUnusedStatefulSets(includeExcludeLists, &FilterOptions{}, clientset, "json", slackopts) + output, err := GetUnusedStatefulSets(includeExcludeLists, &FilterOptions{}, clientset, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedStatefulSetsStructured: %v", err) }