Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add delete flag #120

Merged
merged 12 commits into from
Nov 3, 2023
2 changes: 1 addition & 1 deletion cmd/kor/configmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedConfigmaps(includeExcludeLists, clientset, outputFormat, slackOpts, deleteOpts); err != nil {
patricktalmeida marked this conversation as resolved.
Show resolved Hide resolved
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedDeployments(includeExcludeLists, clientset, outputFormat, slackOpts, deleteOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/hpas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedHpas(includeExcludeLists, clientset, outputFormat, slackOpts, deleteOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/ingresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedIngresses(includeExcludeLists, clientset, outputFormat, slackOpts, deleteOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/pdbs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedPdbs(includeExcludeLists, clientset, outputFormat, slackOpts, deleteOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/pvc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedPvcs(includeExcludeLists, clientset, outputFormat, slackOpts, deleteOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedRoles(includeExcludeLists, clientset, outputFormat, slackOpts, deleteOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
3 changes: 3 additions & 0 deletions cmd/kor/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ var (
kubeconfig string
includeExcludeLists kor.IncludeExcludeLists
slackOpts kor.SlackOpts
deleteOpts kor.DeleteOpts
)

func Execute() {
Expand All @@ -52,6 +53,8 @@ func Execute() {
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().BoolVar(&deleteOpts.DeleteFlag, "delete", false, "Delete unused resources")
rootCmd.PersistentFlags().BoolVar(&deleteOpts.NoInteractive, "no-interactive", false, "Do not prompt for confirmation when deleting resources. Be careful using this flag!")
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 @@ -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, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedSecrets(includeExcludeLists, clientset, outputFormat, slackOpts, deleteOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/serviceaccounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, slackOpts, deleteOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, slackOpts, deleteOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/statefulsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedStatefulSets(includeExcludeLists, clientset, outputFormat, slackOpts, deleteOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
7 changes: 6 additions & 1 deletion pkg/kor/configmaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,12 @@ func TestGetUnusedConfigmapsStructured(t *testing.T) {
Token: "",
}

output, err := GetUnusedConfigmaps(includeExcludeLists, clientset, "json", slackopts)
deleteOpts := DeleteOpts{
DeleteFlag: false,
NoInteractive: true,
}

output, err := GetUnusedConfigmaps(includeExcludeLists, clientset, "json", slackopts, deleteOpts)
if err != nil {
t.Fatalf("Error calling GetUnusedConfigmapsStructured: %v", err)
}
Expand Down
9 changes: 7 additions & 2 deletions pkg/kor/confimgmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string) ([]str

}

func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) {
func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts, deleteOpts DeleteOpts) (string, error) {
var outputBuffer bytes.Buffer

namespaces := SetNamespaceList(includeExcludeLists, clientset)
Expand All @@ -140,8 +140,13 @@ func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset kube
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
output := FormatOutput(namespace, diff, "Config Maps")

if deleteOpts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "ConfigMap", deleteOpts.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")

Expand Down
91 changes: 91 additions & 0 deletions pkg/kor/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package kor

import (
"context"
"fmt"
"os"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

type DeleteOpts struct {
DeleteFlag bool
NoInteractive bool
}

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 confirmation != "y" && confirmation != "Y" && confirmation != "yes" {
patricktalmeida marked this conversation as resolved.
Show resolved Hide resolved
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
}
39 changes: 39 additions & 0 deletions pkg/kor/delete_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}
9 changes: 7 additions & 2 deletions pkg/kor/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func ProcessNamespaceDeployments(clientset kubernetes.Interface, namespace strin
return deploymentsWithoutReplicas, nil
}

func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) {
func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts, deleteOpts DeleteOpts) (string, error) {
var outputBuffer bytes.Buffer

namespaces := SetNamespaceList(includeExcludeLists, clientset)
Expand All @@ -44,8 +44,13 @@ func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset kub
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
output := FormatOutput(namespace, diff, "Deployments")

if deleteOpts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "Deployment", deleteOpts.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")

Expand Down
7 changes: 6 additions & 1 deletion pkg/kor/deployments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ func TestGetUnusedDeploymentsStructured(t *testing.T) {
Token: "",
}

output, err := GetUnusedDeployments(includeExcludeLists, clientset, "json", slackopts)
deleteopts := DeleteOpts{
DeleteFlag: false,
NoInteractive: false,
}

output, err := GetUnusedDeployments(includeExcludeLists, clientset, "json", slackopts, deleteopts)
if err != nil {
t.Fatalf("Error calling GetUnusedDeploymentsStructured: %v", err)
}
Expand Down
9 changes: 7 additions & 2 deletions pkg/kor/hpas.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func processNamespaceHpas(clientset kubernetes.Interface, namespace string) ([]s
return unusedHpas, nil
}

func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) {
func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts, deleteOpts DeleteOpts) (string, error) {
var outputBuffer bytes.Buffer

namespaces := SetNamespaceList(includeExcludeLists, clientset)
Expand All @@ -91,8 +91,13 @@ func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset kubernetes
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
output := FormatOutput(namespace, diff, "Hpas")

if deleteOpts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "HPA", deleteOpts.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")

Expand Down
7 changes: 6 additions & 1 deletion pkg/kor/hpas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,12 @@ func TestGetUnusedHpasStructured(t *testing.T) {
Token: "",
}

output, err := GetUnusedHpas(includeExcludeLists, clientset, "json", slackopts)
deleteopts := DeleteOpts{
DeleteFlag: false,
NoInteractive: false,
}

output, err := GetUnusedHpas(includeExcludeLists, clientset, "json", slackopts, deleteopts)
if err != nil {
t.Fatalf("Error calling GetUnusedHpasStructured: %v", err)
}
Expand Down
9 changes: 7 additions & 2 deletions pkg/kor/ingresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func processNamespaceIngresses(clientset kubernetes.Interface, namespace string)

}

func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) {
func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts, deleteOpts DeleteOpts) (string, error) {
var outputBuffer bytes.Buffer

namespaces := SetNamespaceList(includeExcludeLists, clientset)
Expand All @@ -104,8 +104,13 @@ func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset kuber
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
output := FormatOutput(namespace, diff, "Ingresses")

if deleteOpts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "Ingress", deleteOpts.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")

Expand Down
7 changes: 6 additions & 1 deletion pkg/kor/ingresses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ func TestGetUnusedIngressesStructured(t *testing.T) {
Token: "",
}

output, err := GetUnusedIngresses(includeExcludeLists, clientset, "json", slackopts)
deleteopts := DeleteOpts{
DeleteFlag: false,
NoInteractive: false,
}

output, err := GetUnusedIngresses(includeExcludeLists, clientset, "json", slackopts, deleteopts)
if err != nil {
t.Fatalf("Error calling GetUnusedIngressesStructured: %v", err)
}
Expand Down
Loading