Skip to content

Commit

Permalink
Support json response and kubeconfig flag (#13)
Browse files Browse the repository at this point in the history
* Support JSON return in roles command

* Support JSON return in cms command

* Support JSON return in all command

* Support JSON return in all commands

* Small fix Readme

* Reformat JSON response

* Support optional kubeconfig flag

---------

Co-authored-by: Yonah Dissen <ydissen@vmware.com>
  • Loading branch information
yonahd and Yonah Dissen authored Aug 1, 2023
1 parent efd4955 commit ab78d3d
Show file tree
Hide file tree
Showing 19 changed files with 367 additions and 32 deletions.
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Kor - Kubernetes Orphaned Resources Finder

Kor is a CLI tool to discover unused Kubernetes resources. Currently, Kor can identify and list unused:
Kor is a tool to discover unused Kubernetes resources. Currently, Kor can identify and list unused:
- ConfigMaps
- Secrets.
- Services
Expand Down Expand Up @@ -28,6 +28,14 @@ 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.

### 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 or json) (default "table")
```

To use a specific subcommand, run `kor [subcommand] [flags]`.

```sh
Expand All @@ -49,10 +57,35 @@ 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 no endpoints | |
| Statefulsets | Statefulsets with 0 Replicas | |
| Roles | Roles not used in roleBinding | |


## Import Option
You can also use kor as a Go library to programmatically discover unused resources. By importing the github.com/yonahd/kor/pkg/kor package, you can call the relevant functions to retrieve unused resources. The library provides the option to get the results in JSON format by specifying the outputFormat parameter.

```go
import (
"github.com/yonahd/kor/pkg/kor"
)

func main() {
namespace := "my-namespace"
outputFormat := "json" // Set to "json" for JSON output

if outputFormat == "json" {
jsonResponse, err := kor.GetUnusedDeploymentsJSON(namespace)
if err != nil {
// Handle error
}
// Process the JSON response
// ...
} else {
kor.GetUnusedDeployments(namespace)
}
}
```


## Contributing

Expand Down
8 changes: 7 additions & 1 deletion cmd/kor/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ var allCmd = &cobra.Command{
Short: "Gets unused resources",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
kor.GetUnusedAll(namespace)
if outputFormat == "json" {
kor.GetUnusedAllJSON(namespace, kubeconfig)
} else {
kor.GetUnusedAll(namespace, kubeconfig)
}

},
}

func init() {
allCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "Path to kubeconfig file (optional)")
allCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Namespace to run on")
allCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table or json)")
rootCmd.AddCommand(allCmd)
}
8 changes: 7 additions & 1 deletion cmd/kor/configmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ var configmapCmd = &cobra.Command{
Short: "Gets unused configmaps",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
kor.GetUnusedConfigmaps(namespace)
if outputFormat == "json" {
kor.GetUnusedConfigmapsJSON(namespace, kubeconfig)
} else {
kor.GetUnusedConfigmaps(namespace, kubeconfig)
}

},
}

func init() {
configmapCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "Path to kubeconfig file (optional)")
configmapCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Namespace to run on")
configmapCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table or json)")
rootCmd.AddCommand(configmapCmd)
}
8 changes: 7 additions & 1 deletion cmd/kor/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ var deployCmd = &cobra.Command{
Short: "Gets unused deployments",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
kor.GetUnusedDeployments(namespace)
if outputFormat == "json" {
kor.GetUnusedDeploymentsJSON(namespace, kubeconfig)
} else {
kor.GetUnusedDeployments(namespace, kubeconfig)
}

},
}

func init() {
deployCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "Path to kubeconfig file (optional)")
deployCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Namespace to run on")
deployCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table or json)")
rootCmd.AddCommand(deployCmd)
}
9 changes: 7 additions & 2 deletions cmd/kor/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ var roleCmd = &cobra.Command{
Short: "Gets unused roles",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
kor.GetUnusedRoles(namespace)

if outputFormat == "json" {
kor.GetUnusedRolesJSON(namespace, kubeconfig)
} else {
kor.GetUnusedRoles(namespace, kubeconfig)
}
},
}

func init() {
roleCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "Path to kubeconfig file (optional)")
roleCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Namespace to run on")
roleCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table or json)")
rootCmd.AddCommand(roleCmd)
}
3 changes: 2 additions & 1 deletion cmd/kor/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ var rootCmd = &cobra.Command{
}

var namespace string
var outputFormat string
var kubeconfig string

func Execute() {

if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error while executing your CLI '%s'", err)
os.Exit(1)
Expand Down
8 changes: 7 additions & 1 deletion cmd/kor/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ var secretCmd = &cobra.Command{
Short: "Gets unused secrets",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
kor.GetUnusedSecrets(namespace)
if outputFormat == "json" {
kor.GetUnusedSecretsJSON(namespace, kubeconfig)
} else {
kor.GetUnusedSecrets(namespace, kubeconfig)
}

},
}

func init() {
secretCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "Path to kubeconfig file (optional)")
secretCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Namespace to run on")
secretCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table or json)")
rootCmd.AddCommand(secretCmd)
}
8 changes: 7 additions & 1 deletion cmd/kor/serviceaccounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ var serviceAccountCmd = &cobra.Command{
Short: "Gets unused service accounts",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
kor.GetUnusedServiceAccounts(namespace)
if outputFormat == "json" {
kor.GetUnusedServiceAccountsJSON(namespace, kubeconfig)
} else {
kor.GetUnusedServiceAccounts(namespace, kubeconfig)
}

},
}

func init() {
serviceAccountCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "Path to kubeconfig file (optional)")
serviceAccountCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Namespace to run on")
serviceAccountCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table or json)")
rootCmd.AddCommand(serviceAccountCmd)
}
8 changes: 7 additions & 1 deletion cmd/kor/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ var serviceCmd = &cobra.Command{
Short: "Gets unused services",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
kor.GetUnusedServices(namespace)
if outputFormat == "json" {
kor.GetUnusedServicesJSON(namespace, kubeconfig)
} else {
kor.GetUnusedServices(namespace, kubeconfig)
}

},
}

func init() {
serviceCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "Path to kubeconfig file (optional)")
serviceCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Namespace to run on")
serviceCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table or json)")
rootCmd.AddCommand(serviceCmd)
}
8 changes: 7 additions & 1 deletion cmd/kor/statefulsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ var stsCmd = &cobra.Command{
Short: "Gets unused statefulsets",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
kor.GetUnusedStatefulsets(namespace)
if outputFormat == "json" {
kor.GetUnusedStatefulsetsJSON(namespace, kubeconfig)
} else {
kor.GetUnusedStatefulsets(namespace, kubeconfig)
}

},
}

func init() {
stsCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "Path to kubeconfig file (optional)")
stsCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Namespace to run on")
stsCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table or json)")
rootCmd.AddCommand(stsCmd)
}
62 changes: 60 additions & 2 deletions pkg/kor/all.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package kor

import (
"encoding/json"
"fmt"
"k8s.io/client-go/kubernetes"
"os"
)

type GetUnusedResourceJSONResponse struct {
ResourceType string `json:"resourceType"`
Namespaces map[string][]string `json:"namespaces"`
}

type ResourceDiff struct {
resourceType string
diff []string
Expand Down Expand Up @@ -74,12 +80,12 @@ func getUnusedRoles(kubeClient *kubernetes.Clientset, namespace string) Resource
return namespaceSADiff
}

func GetUnusedAll(namespace string) {
func GetUnusedAll(namespace string, kubeconfig string) {
var kubeClient *kubernetes.Clientset
var namespaces []string
var allDiffs []ResourceDiff

kubeClient = GetKubeClient()
kubeClient = GetKubeClient(kubeconfig)

namespaces = SetNamespaceList(namespace, kubeClient)
for _, namespace := range namespaces {
Expand All @@ -102,3 +108,55 @@ func GetUnusedAll(namespace string) {
fmt.Println()
}
}

func GetUnusedAllJSON(namespace string, kubeconfig string) (string, error) {
var kubeClient *kubernetes.Clientset
var namespaces []string

kubeClient = GetKubeClient(kubeconfig)

namespaces = SetNamespaceList(namespace, kubeClient)

// Create the JSON response object
response := make(map[string]map[string][]string)

for _, namespace := range namespaces {
var allDiffs []ResourceDiff

namespaceCMDiff := getUnusedCMs(kubeClient, namespace)
allDiffs = append(allDiffs, namespaceCMDiff)

namespaceSVCDiff := getUnusedSVCs(kubeClient, namespace)
allDiffs = append(allDiffs, namespaceSVCDiff)

namespaceSecretDiff := getUnusedSecrets(kubeClient, namespace)
allDiffs = append(allDiffs, namespaceSecretDiff)

namespaceSADiff := getUnusedServiceAccounts(kubeClient, namespace)
allDiffs = append(allDiffs, namespaceSADiff)

namespaceDeploymentDiff := getUnusedDeployments(kubeClient, namespace)
allDiffs = append(allDiffs, namespaceDeploymentDiff)

namespaceStatefulsetDiff := getUnusedStatefulsets(kubeClient, namespace)
allDiffs = append(allDiffs, namespaceStatefulsetDiff)

namespaceRoleDiff := getUnusedRoles(kubeClient, namespace)
allDiffs = append(allDiffs, namespaceRoleDiff)

// 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
}

return string(jsonResponse), nil
}
32 changes: 30 additions & 2 deletions pkg/kor/confimgmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kor

import (
"context"
"encoding/json"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
Expand Down Expand Up @@ -121,11 +122,11 @@ func processNamespaceCM(kubeClient *kubernetes.Clientset, namespace string) ([]s

}

func GetUnusedConfigmaps(namespace string) {
func GetUnusedConfigmaps(namespace string, kubeconfig string) {
var kubeClient *kubernetes.Clientset
var namespaces []string

kubeClient = GetKubeClient()
kubeClient = GetKubeClient(kubeconfig)

namespaces = SetNamespaceList(namespace, kubeClient)

Expand All @@ -140,3 +141,30 @@ func GetUnusedConfigmaps(namespace string) {
fmt.Println()
}
}

func GetUnusedConfigmapsJSON(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 := processNamespaceCM(kubeClient, namespace)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
resourceMap := make(map[string][]string)
resourceMap["ConfigMap"] = diff
response[namespace] = resourceMap
}

jsonResponse, err := json.MarshalIndent(response, "", " ")
if err != nil {
return "", err
}

return string(jsonResponse), nil
}
Loading

0 comments on commit ab78d3d

Please sign in to comment.