From d28eb54de244c7b8c5600b7a879426902d67ee20 Mon Sep 17 00:00:00 2001 From: Yonah Dissen <47282577+yonahd@users.noreply.github.com> Date: Mon, 21 Aug 2023 10:54:59 +0300 Subject: [PATCH] Support finding unused pvcs (#28) * support finding unused pvcs * move calculate difference to common function * add test for calculate difference --------- Co-authored-by: Yonah Dissen --- README.md | 21 ++++---- cmd/kor/pvc.go | 23 +++++++++ go.mod | 2 +- pkg/kor/all.go | 17 ++++++ pkg/kor/confimgmaps.go | 19 +------ pkg/kor/kor.go | 17 ++++++ pkg/kor/kor_test.go | 18 +++++++ pkg/kor/pvc.go | 103 +++++++++++++++++++++++++++++++++++++ pkg/kor/roles.go | 19 +------ pkg/kor/secrets.go | 19 +------ pkg/kor/serviceaccounts.go | 2 +- 11 files changed, 195 insertions(+), 65 deletions(-) create mode 100644 cmd/kor/pvc.go create mode 100644 pkg/kor/pvc.go diff --git a/README.md b/README.md index 37584e18..61d25136 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Kor is a tool to discover unused Kubernetes resources. Currently, Kor can identi - Statefulsets - Roles - Hpas +- Pvcs ![Kor Screenshot](/images/screenshot.png) @@ -32,6 +33,7 @@ Kor provides various subcommands to identify and list unused resources. The avai - `statefulsets`: Gets unused service accounts for the specified namespace or all namespaces. - `role`: Gets unused roles for the specified namespace or all namespaces. - `hps`: Gets unused hpa for the specified namespace or all namespaces. +- `pvc`: Gets unused pvcs for the specified namespace or all namespaces. ### Supported Flags ``` @@ -55,16 +57,17 @@ kor [subcommand] --help ## Supported resources and limitations -| Resource | What it looks for | Known False Positives ⚠️ | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------| -| Configmaps | Configmaps not used in the following places:
- Pods
- Containers
- Configmaps used through volumes
- Configmaps used through environment variables | Configmaps used by resources which don't explicitly state them in the config.
e.g Grafana dashboards loaded dynamically opa policies fluentd configs | +| Resource | What it looks for | Known False Positives ⚠️ | +|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| Configmaps | Configmaps not used in the following places:
- Pods
- Containers
- Configmaps used through volumes
- Configmaps used through environment variables | Configmaps used by resources which don't explicitly state them in the config.
e.g Grafana dashboards loaded dynamically opa policies fluentd configs | | Secrets | Secrets not used in the following places:
- Pods
- Containers
- Secrets used through volumes
- Secrets used through environment variables
- Secrets used by ingress TLS
-Secrets used by ServiceAccounts | Secrets used by resources which don't explicitly state them in the config | -| Services | Services with no endpoints | | -| Deployments | Deployments with 0 Replicas | | -| ServiceAccounts | ServiceAccounts unused by pods
ServiceAccounts unused by roleBinding or clusterRoleBinding | | -| Statefulsets | Statefulsets with 0 Replicas | | -| Roles | Roles not used in roleBinding | | -| Hpas | Hpas not used in Deployments
Hpas not used in Statefulsets | | +| Services | Services with no endpoints | | +| Deployments | Deployments with 0 Replicas | | +| ServiceAccounts | ServiceAccounts unused by pods
ServiceAccounts unused by roleBinding or clusterRoleBinding | | +| Statefulsets | Statefulsets with 0 Replicas | | +| Roles | Roles not used in roleBinding | | +| Pvcs | Pvcs not used in pods | | +| Hpas | Hpas not used in Deployments
Hpas not used in Statefulsets | | diff --git a/cmd/kor/pvc.go b/cmd/kor/pvc.go new file mode 100644 index 00000000..acc24ac8 --- /dev/null +++ b/cmd/kor/pvc.go @@ -0,0 +1,23 @@ +package kor + +import ( + "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/kor" +) + +var pvcCmd = &cobra.Command{ + Use: "pvc", + Short: "Gets unused pvcs", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + if outputFormat == "json" { + kor.GetUnusedPvcsJson(namespace, kubeconfig) + } else { + kor.GetUnusedPvcs(namespace, kubeconfig) + } + }, +} + +func init() { + rootCmd.AddCommand(pvcCmd) +} diff --git a/go.mod b/go.mod index c8dc1f20..35017d39 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/spf13/cobra v1.7.0 k8s.io/apimachinery v0.27.3 k8s.io/client-go v0.27.3 + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 ) require ( @@ -48,7 +49,6 @@ require ( k8s.io/api v0.27.3 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect - k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/pkg/kor/all.go b/pkg/kor/all.go index d26c5b24..c305973b 100644 --- a/pkg/kor/all.go +++ b/pkg/kor/all.go @@ -90,6 +90,15 @@ func getUnusedHpas(kubeClient *kubernetes.Clientset, namespace string) ResourceD return namespaceHpaDiff } +func getUnusedPvcs(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { + pvcDiff, err := processNamespacePvcs(kubeClient, namespace) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "pvcs", namespace, err) + } + namespacePvcDiff := ResourceDiff{"Pvc", pvcDiff} + return namespacePvcDiff +} + func GetUnusedAll(namespace string, kubeconfig string) { var kubeClient *kubernetes.Clientset var namespaces []string @@ -115,6 +124,8 @@ func GetUnusedAll(namespace string, kubeconfig string) { allDiffs = append(allDiffs, namespaceRoleDiff) namespaceHpaDiff := getUnusedHpas(kubeClient, namespace) allDiffs = append(allDiffs, namespaceHpaDiff) + namespacePvcDiff := getUnusedPvcs(kubeClient, namespace) + allDiffs = append(allDiffs, namespacePvcDiff) output := FormatOutputAll(namespace, allDiffs) fmt.Println(output) fmt.Println() @@ -156,6 +167,12 @@ func GetUnusedAllJSON(namespace string, kubeconfig string) (string, error) { namespaceRoleDiff := getUnusedRoles(kubeClient, namespace) allDiffs = append(allDiffs, namespaceRoleDiff) + namespaceHpaDiff := getUnusedHpas(kubeClient, namespace) + allDiffs = append(allDiffs, namespaceHpaDiff) + + namespacePvcDiff := getUnusedPvcs(kubeClient, namespace) + allDiffs = append(allDiffs, namespacePvcDiff) + // Store the unused resources for each resource type in the JSON response resourceMap := make(map[string][]string) for _, diff := range allDiffs { diff --git a/pkg/kor/confimgmaps.go b/pkg/kor/confimgmaps.go index 57f16838..bc861bb0 100644 --- a/pkg/kor/confimgmaps.go +++ b/pkg/kor/confimgmaps.go @@ -83,23 +83,6 @@ func retrieveConfigMapNames(kubeClient *kubernetes.Clientset, namespace string) return names, nil } -func calculateCMDifference(usedConfigMaps []string, configMapNames []string) []string { - difference := []string{} - for _, name := range configMapNames { - found := false - for _, usedName := range usedConfigMaps { - if name == usedName { - found = true - break - } - } - if !found { - difference = append(difference, name) - } - } - return difference -} - func processNamespaceCM(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { volumesCM, volumesProjectedCM, envCM, envFromCM, envFromContainerCM, err := retrieveUsedCM(kubeClient, namespace) if err != nil { @@ -118,7 +101,7 @@ func processNamespaceCM(kubeClient *kubernetes.Clientset, namespace string) ([]s } usedConfigMaps := append(append(append(append(volumesCM, volumesProjectedCM...), envCM...), envFromCM...), envFromContainerCM...) - diff := calculateCMDifference(usedConfigMaps, configMapNames) + diff := CalculateResourceDifference(usedConfigMaps, configMapNames) return diff, nil } diff --git a/pkg/kor/kor.go b/pkg/kor/kor.go index 1feda4c3..440ff2cd 100644 --- a/pkg/kor/kor.go +++ b/pkg/kor/kor.go @@ -110,3 +110,20 @@ func FormatOutputAll(namespace string, allDiffs []ResourceDiff) string { } // TODO create formatter by resource "#", "Resource Name", "Namespace" + +func CalculateResourceDifference(usedResourceNames []string, allResourceNames []string) []string { + difference := []string{} + for _, name := range allResourceNames { + found := false + for _, usedName := range usedResourceNames { + if name == usedName { + found = true + break + } + } + if !found { + difference = append(difference, name) + } + } + return difference +} diff --git a/pkg/kor/kor_test.go b/pkg/kor/kor_test.go index bd765ac9..d569544b 100644 --- a/pkg/kor/kor_test.go +++ b/pkg/kor/kor_test.go @@ -42,3 +42,21 @@ func TestRemoveDuplicatesAndSort(t *testing.T) { t.Errorf("RemoveDuplicatesAndSort failed for empty slice, expected: %v, got: %v", emptyExpected, emptyResult) } } + +func TestCalculateResourceDifference(t *testing.T) { + usedResourceNames := []string{"resource1", "resource2", "resource3"} + allResourceNames := []string{"resource1", "resource2", "resource3", "resource4", "resource5"} + + expectedDifference := []string{"resource4", "resource5"} + difference := CalculateResourceDifference(usedResourceNames, allResourceNames) + + if len(difference) != len(expectedDifference) { + t.Errorf("Expected %d difference items, but got %d", len(expectedDifference), len(difference)) + } + + for i, item := range difference { + if item != expectedDifference[i] { + t.Errorf("Difference item at index %d should be %s, but got %s", i, expectedDifference[i], item) + } + } +} diff --git a/pkg/kor/pvc.go b/pkg/kor/pvc.go new file mode 100644 index 00000000..1a9e2c43 --- /dev/null +++ b/pkg/kor/pvc.go @@ -0,0 +1,103 @@ +package kor + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" +) + +func retreiveUsedPvcs(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { + pods, err := kubeClient.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + fmt.Printf("Failed to list Pods: %v\n", err) + os.Exit(1) + } + var usedPvcs []string + // Iterate through each Pod and check for PVC usage + for _, pod := range pods.Items { + for _, volume := range pod.Spec.Volumes { + if volume.PersistentVolumeClaim != nil { + usedPvcs = append(usedPvcs, volume.PersistentVolumeClaim.ClaimName) + } + } + } + return usedPvcs, err +} + +func processNamespacePvcs(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { + pvcs, err := kubeClient.CoreV1().PersistentVolumeClaims(namespace).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, err + } + pvcNames := make([]string, 0, len(pvcs.Items)) + for _, pvc := range pvcs.Items { + pvcNames = append(pvcNames, pvc.Name) + } + + usedPvcs, err := retreiveUsedPvcs(kubeClient, namespace) + if err != nil { + return nil, err + } + + diff := CalculateResourceDifference(usedPvcs, pvcNames) + return diff, nil +} + +func GetUnusedPvcs(namespace string, kubeconfig string) { + var kubeClient *kubernetes.Clientset + var namespaces []string + + kubeClient = GetKubeClient(kubeconfig) + + namespaces = SetNamespaceList(namespace, kubeClient) + + for _, namespace := range namespaces { + diff, err := processNamespacePvcs(kubeClient, namespace) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) + continue + } + output := FormatOutput(namespace, diff, "Pvcs") + fmt.Println(output) + fmt.Println() + } + +} + +func GetUnusedPvcsJson(namespace string, kubeconfig string) (string, error) { + var kubeClient *kubernetes.Clientset + var namespaces []string + + kubeClient = GetKubeClient(kubeconfig) + + namespaces = SetNamespaceList(namespace, kubeClient) + response := make(map[string]map[string][]string) + + for _, namespace := range namespaces { + diff, err := processNamespacePvcs(kubeClient, namespace) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) + continue + } + if len(diff) > 0 { + if response[namespace] == nil { + response[namespace] = make(map[string][]string) + } + response[namespace]["Pvc"] = diff + } + } + + jsonResponse, err := json.MarshalIndent(response, "", " ") + if err != nil { + return "", err + } + + log.Println(string(jsonResponse)) + return string(jsonResponse), nil +} diff --git a/pkg/kor/roles.go b/pkg/kor/roles.go index e42446e4..31a72588 100644 --- a/pkg/kor/roles.go +++ b/pkg/kor/roles.go @@ -49,23 +49,6 @@ func retrieveRoleNames(kubeClient *kubernetes.Clientset, namespace string) ([]st return names, nil } -func calculateRoleDifference(usedRoles []string, roleNames []string) []string { - difference := []string{} - for _, name := range roleNames { - found := false - for _, usedName := range usedRoles { - if name == usedName { - found = true - break - } - } - if !found { - difference = append(difference, name) - } - } - return difference -} - func processNamespaceRoles(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { usedRoles, err := retrieveUsedRoles(kubeClient, namespace) if err != nil { @@ -79,7 +62,7 @@ func processNamespaceRoles(kubeClient *kubernetes.Clientset, namespace string) ( return nil, err } - diff := calculateRoleDifference(usedRoles, roleNames) + diff := CalculateResourceDifference(usedRoles, roleNames) return diff, nil } diff --git a/pkg/kor/secrets.go b/pkg/kor/secrets.go index e254d0d4..e6028203 100644 --- a/pkg/kor/secrets.go +++ b/pkg/kor/secrets.go @@ -96,23 +96,6 @@ func retrieveSecretNames(kubeClient *kubernetes.Clientset, namespace string) ([] return names, nil } -func calculateSecretDifference(usedSecrets []string, secretNames []string) []string { - difference := []string{} - for _, name := range secretNames { - found := false - for _, usedName := range usedSecrets { - if name == usedName { - found = true - break - } - } - if !found { - difference = append(difference, name) - } - } - return difference -} - func processNamespaceSecret(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { envSecrets, envSecrets2, volumeSecrets, pullSecrets, tlsSecrets, err := retrieveUsedSecret(kubeClient, namespace) if err != nil { @@ -131,7 +114,7 @@ func processNamespaceSecret(kubeClient *kubernetes.Clientset, namespace string) } usedSecrets := append(append(append(append(envSecrets, envSecrets2...), volumeSecrets...), pullSecrets...), tlsSecrets...) - diff := calculateSecretDifference(usedSecrets, secretNames) + diff := CalculateResourceDifference(usedSecrets, secretNames) return diff, nil } diff --git a/pkg/kor/serviceaccounts.go b/pkg/kor/serviceaccounts.go index e9117fd5..bdab5c8f 100644 --- a/pkg/kor/serviceaccounts.go +++ b/pkg/kor/serviceaccounts.go @@ -115,7 +115,7 @@ func processNamespaceSA(kubeClient *kubernetes.Clientset, namespace string) ([]s return nil, err } - diff := calculateCMDifference(usedServiceAccounts, serviceAccountNames) + diff := CalculateResourceDifference(usedServiceAccounts, serviceAccountNames) return diff, nil }