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

Support finding unused pvcs #28

Merged
merged 4 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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