Skip to content

Commit

Permalink
Support finding unused pvcs (#28)
Browse files Browse the repository at this point in the history
* support finding unused pvcs

* move calculate difference to common function

* add test for calculate difference

---------

Co-authored-by: Yonah Dissen <ydissen@vmware.com>
  • Loading branch information
yonahd and Yonah Dissen authored Aug 21, 2023
1 parent a88351f commit d28eb54
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 65 deletions.
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
```
Expand All @@ -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:<br/>- Pods<br/>- Containers <br/>- Configmaps used through volumes <br/>- Configmaps used through environment variables | Configmaps used by resources which don't explicitly state them in the config.<br/> 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:<br/>- Pods<br/>- Containers <br/>- Configmaps used through volumes <br/>- Configmaps used through environment variables | Configmaps used by resources which don't explicitly state them in the config.<br/> e.g Grafana dashboards loaded dynamically opa policies fluentd configs |
| Secrets | Secrets not used in the following places:<br/>- Pods<br/>- Containers <br/>- Secrets used through volumes <br/>- Secrets used through environment variables<br/>- Secrets used by ingress TLS<br/>-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<br/>ServiceAccounts unused by roleBinding or clusterRoleBinding | |
| Statefulsets | Statefulsets with 0 Replicas | |
| Roles | Roles not used in roleBinding | |
| Hpas | Hpas not used in Deployments <br/> Hpas not used in Statefulsets | |
| Services | Services with no endpoints | |
| Deployments | Deployments with 0 Replicas | |
| ServiceAccounts | ServiceAccounts unused by pods<br/>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 <br/> Hpas not used in Statefulsets | |



Expand Down
23 changes: 23 additions & 0 deletions cmd/kor/pvc.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions pkg/kor/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 1 addition & 18 deletions pkg/kor/confimgmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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

}
Expand Down
17 changes: 17 additions & 0 deletions pkg/kor/kor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
18 changes: 18 additions & 0 deletions pkg/kor/kor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
103 changes: 103 additions & 0 deletions pkg/kor/pvc.go
Original file line number Diff line number Diff line change
@@ -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
}
19 changes: 1 addition & 18 deletions pkg/kor/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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

}
Expand Down
19 changes: 1 addition & 18 deletions pkg/kor/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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

}
Expand Down
2 changes: 1 addition & 1 deletion pkg/kor/serviceaccounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

}
Expand Down

0 comments on commit d28eb54

Please sign in to comment.