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
}