Repository: yonahd/kor
Pull request #57 (2023-09-06 16:24:32)

Enables to get multiple specific unused resources
to solve #2 

rootcmd now calls for `GetUnusedMulti` and `GetUnusedMultiStructured` when it indicates a list of resources,  `multi.go` uses functions like `getUnusedSVCs` from `all.go`.

@yonahd let me know what you think and also if `--exclude-resource` for `all.go` should be in this PR.


yonahd: Thanks for the MR, I'll review it hopefully in the next few days.
Regarding exclude-resource, I feel like it's an unnecessary option.


Patch:
diff --git a/README.md b/README.md
index e634022..7ba54fc 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Kor is a tool to discover unused Kubernetes resources. Currently, Kor can identi
 - Services
 - ServiceAccounts
 - Deployments
-- Statefulsets
+- StatefulSets
 - Roles
 - Hpas
 - Pvcs
@@ -46,10 +46,11 @@ Kor provides various subcommands to identify and list unused resources. The avai
 
 ### Supported Flags
 ```
--h, --help                help for role
--k, --kubeconfig string   Path to kubeconfig file (optional)
--n, --namespace string    Namespace to run on
---output string       Output format (table, json or yaml) (default "table")
+-e, --exclude-namespaces string   Namespaces to be excluded, splited by comma. Example: --exclude-namespace ns1,ns2,ns3. If --include-namespace is set, --exclude-namespaces will be ignored.
+-h, --help                        help for kor
+-n, --include-namespaces string   Namespaces to run on, splited by comma. Example: --include-namespace ns1,ns2,ns3. 
+-k, --kubeconfig string           Path to kubeconfig file (optional)
+    --output string               Output format (table or json) (default "table")
 ```
 
 To use a specific subcommand, run `kor [subcommand] [flags]`.
@@ -73,12 +74,12 @@ kor [subcommand] --help
 | 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                                                                                                                                                                                                       |                                                                                                                              |
+| StatefulSets    | Statefulsets with 0 Replicas                                                                                                                                                                                                       |                                                                                                                              |
 | Roles           | Roles not used in roleBinding                                                                                                                                                                                                      |                                                                                                                              |
 | Pvcs            | Pvcs not used in pods                                                                                                                                                                                                              |                                                                                                                              |
 | Ingresses       | Ingresses not pointing at any service.                                                                                                                                                                                             |                                                                                                                              |
-| Hpas            | Hpas not used in Deployments   <br/>    Hpas not used in Statefulsets                                                                                                                                                              |                                                                                                                              |
-| Pdbs            | Pdbs not used in Deployments   <br/>    Pdbs not used in Statefulsets                                                                                                                                                              |                                                                                                                              |
+| Hpas            | Hpas not used in Deployments   <br/>    Hpas not used in StatefulSets                                                                                                                                                              |                                                                                                                              |
+| Pdbs            | Pdbs not used in Deployments   <br/>    Pdbs not used in StatefulSets                                                                                                                                                              |                                                                                                                              |
 
 
 
@@ -87,15 +88,19 @@ You can also use kor as a Go library to programmatically discover unused resourc
 
 ```go
 import (
-"github.com/yonahd/kor/pkg/kor"
+    "github.com/yonahd/kor/pkg/kor"
 )
 
+
+
 func main() {
-namespace := "my-namespace"
-outputFormat := "json" // Set to "json" for JSON output
+    myNamespaces := kor.IncludeExcludeLists{
+        IncludeListStr: "my-namespace1, my-namespace2",
+    }
+    outputFormat := "json" // Set to "json" for JSON output
 
     if outputFormat == "json" {
-        jsonResponse, err := kor.GetUnusedDeploymentsJSON(namespace, kubeconfig)
+        jsonResponse, err := kor.GetUnusedDeploymentsStructured(myNamespaces, kubeconfig, "json")
         if err != nil {
             // Handle error
         }
diff --git a/cmd/kor/root.go b/cmd/kor/root.go
index fa99d12..b68d905 100644
--- a/cmd/kor/root.go
+++ b/cmd/kor/root.go
@@ -3,6 +3,7 @@ package kor
 import (
 	"fmt"
 	"os"
+	"strings"
 
 	"github.com/spf13/cobra"
 	"github.com/yonahd/kor/pkg/kor"
@@ -15,6 +16,20 @@ var rootCmd = &cobra.Command{
 	kor can currently discover unused configmaps and secrets`,
 	Args: cobra.MinimumNArgs(1),
 	Run: func(cmd *cobra.Command, args []string) {
+		resourceNames := args[0]
+
+		// Cheks whether the string contains a comma, indicating that it represents a list of resources
+		if strings.ContainsRune(resourceNames, 44) {
+			if outputFormat == "json" || outputFormat == "yaml" {
+				if response, err := kor.GetUnusedMultiStructured(includeExcludeLists, kubeconfig, outputFormat, resourceNames); err != nil {
+					fmt.Println(err)
+				} else {
+					fmt.Println(response)
+				}
+			} else {
+				kor.GetUnusedMulti(includeExcludeLists, kubeconfig, resourceNames)
+			}
+		}
 	},
 }
 
diff --git a/pkg/kor/kor.go b/pkg/kor/kor.go
index 03da176..a137b0c 100644
--- a/pkg/kor/kor.go
+++ b/pkg/kor/kor.go
@@ -43,9 +43,15 @@ func GetKubeConfigPath() string {
 	return filepath.Join(home, ".kube", "config")
 }
 
+// GetKubeClient selects kubeconfig path and returns kubeClient
+// kubeconfig path selection priority: 1) user supplied kubeconfig, 2) KUBECONFIG envvar, 3) default kubeconfig
 func GetKubeClient(kubeconfig string) *kubernetes.Clientset {
 	if kubeconfig == "" {
-		kubeconfig = GetKubeConfigPath()
+		if configEnv := os.Getenv("KUBECONFIG"); configEnv != "" {
+			kubeconfig = configEnv
+		} else {
+			kubeconfig = GetKubeConfigPath()
+		}
 	}
 	config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
 	if err != nil {
diff --git a/pkg/kor/multi.go b/pkg/kor/multi.go
new file mode 100644
index 0000000..c06f95f
--- /dev/null
+++ b/pkg/kor/multi.go
@@ -0,0 +1,108 @@
+package kor
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"k8s.io/client-go/kubernetes"
+	"sigs.k8s.io/yaml"
+)
+
+func retrieveNamespaceDiffs(kubeClient *kubernetes.Clientset, namespace string, resourceList []string) []ResourceDiff {
+	var allDiffs []ResourceDiff
+	for _, resource := range resourceList {
+		switch resource {
+		case "cm", "configmap":
+			namespaceCMDiff := getUnusedCMs(kubeClient, namespace)
+			allDiffs = append(allDiffs, namespaceCMDiff)
+		case "svc", "service", "services":
+			namespaceSVCDiff := getUnusedSVCs(kubeClient, namespace)
+			allDiffs = append(allDiffs, namespaceSVCDiff)
+		case "scrt", "secret":
+			namespaceSecretDiff := getUnusedSecrets(kubeClient, namespace)
+			allDiffs = append(allDiffs, namespaceSecretDiff)
+		case "sa", "serviceaccount":
+			namespaceSADiff := getUnusedServiceAccounts(kubeClient, namespace)
+			allDiffs = append(allDiffs, namespaceSADiff)
+		case "deploy", "deployments":
+			namespaceDeploymentDiff := getUnusedDeployments(kubeClient, namespace)
+			allDiffs = append(allDiffs, namespaceDeploymentDiff)
+		case "sts", "statefulsets":
+			namespaceStatefulsetDiff := getUnusedStatefulsets(kubeClient, namespace)
+			allDiffs = append(allDiffs, namespaceStatefulsetDiff)
+		case "role":
+			namespaceRoleDiff := getUnusedRoles(kubeClient, namespace)
+			allDiffs = append(allDiffs, namespaceRoleDiff)
+		case "hpa":
+			namespaceHpaDiff := getUnusedHpas(kubeClient, namespace)
+			allDiffs = append(allDiffs, namespaceHpaDiff)
+		case "pvc":
+			namespacePvcDiff := getUnusedPvcs(kubeClient, namespace)
+			allDiffs = append(allDiffs, namespacePvcDiff)
+		case "ing", "ingress":
+			namespaceIngressDiff := getUnusedIngresses(kubeClient, namespace)
+			allDiffs = append(allDiffs, namespaceIngressDiff)
+		case "pdb":
+			namespacePdbDiff := getUnusedPdbs(kubeClient, namespace)
+			allDiffs = append(allDiffs, namespacePdbDiff)
+		}
+	}
+	return allDiffs
+}
+
+func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourceNames string) {
+	var kubeClient *kubernetes.Clientset
+	var namespaces []string
+
+	kubeClient = GetKubeClient(kubeconfig)
+
+	resourceList := strings.Split(resourceNames, ",")
+	namespaces = SetNamespaceList(includeExcludeLists, kubeClient)
+
+	for _, namespace := range namespaces {
+		allDiffs := retrieveNamespaceDiffs(kubeClient, namespace, resourceList)
+		output := FormatOutputAll(namespace, allDiffs)
+		fmt.Println(output)
+		fmt.Println()
+	}
+}
+
+func GetUnusedMultiStructured(includeExcludeLists IncludeExcludeLists, kubeconfig, outputFormat, resourceNames string) (string, error) {
+	var kubeClient *kubernetes.Clientset
+	var namespaces []string
+
+	kubeClient = GetKubeClient(kubeconfig)
+
+	resourceList := strings.Split(resourceNames, ",")
+	namespaces = SetNamespaceList(includeExcludeLists, kubeClient)
+
+	// Create the JSON response object
+	response := make(map[string]map[string][]string)
+
+	for _, namespace := range namespaces {
+		allDiffs := retrieveNamespaceDiffs(kubeClient, namespace, resourceList)
+		// Store the unused resources for each resource type in the JSON response
+		resourceMap := make(map[string][]string)
+		for _, diff := range allDiffs {
+			resourceMap[diff.resourceType] = diff.diff
+		}
+		response[namespace] = resourceMap
+	}
+
+	// Convert the response object to JSON
+	jsonResponse, err := json.MarshalIndent(response, "", "  ")
+	if err != nil {
+		return "", err
+	}
+
+	if outputFormat == "yaml" {
+		yamlResponse, err := yaml.JSONToYAML(jsonResponse)
+		if err != nil {
+			fmt.Printf("err: %v\n", err)
+		}
+		return string(yamlResponse), nil
+	} else {
+		return string(jsonResponse), nil
+	}
+}


Test patch:
diff --git a/pkg/kor/kor_test.go b/pkg/kor/kor_test.go
index d569544..1a91d57 100644
--- a/pkg/kor/kor_test.go
+++ b/pkg/kor/kor_test.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"os"
 	"sort"
 	"testing"
 )
@@ -60,3 +61,62 @@ func TestCalculateResourceDifference(t *testing.T) {
 		}
 	}
 }
+
+func getFakeConfigContent() string {
+	fakeContent := `
+apiVersion: v1
+clusters:
+- cluster:
+    server: https://localhost:8080
+    extensions:
+    - name: client.authentication.k8s.io/exec
+      extension:
+        audience: foo
+        other: bar
+  name: foo-cluster
+contexts:
+- context:
+    cluster: foo-cluster
+    namespace: bar
+  name: foo-context
+current-context: foo-context
+kind: Config
+`
+	return fakeContent
+}
+
+func TestGetKubeClientFromEnvVar(t *testing.T) {
+	configFile, err := os.CreateTemp("", "kubeconfig-")
+	if err != nil {
+		t.Error(err)
+	}
+	defer os.Remove(configFile.Name())
+	if err := os.WriteFile(configFile.Name(), []byte(getFakeConfigContent()), 0666); err != nil {
+		t.Error(err)
+	}
+
+	originalKCEnv := os.Getenv("KUBECONFIG")
+	defer os.Setenv("KUBECONFIG", originalKCEnv)
+	os.Setenv("KUBECONFIG", configFile.Name())
+
+	kcs := GetKubeClient("")
+	if kcs == nil {
+		t.Errorf("Expected valid clientSet")
+	}
+}
+
+func TestGetKubeClientFromInput(t *testing.T) {
+	configFile, err := os.CreateTemp("", "kubeconfig")
+	if err != nil {
+		t.Error(err)
+	}
+	defer os.Remove(configFile.Name())
+	if err := os.WriteFile(configFile.Name(), []byte(getFakeConfigContent()), 0666); err != nil {
+		t.Error(err)
+	}
+
+	kcs := GetKubeClient(configFile.Name())
+	if kcs == nil {
+		t.Errorf("Expected valid clientSet")
+	}
+}