Skip to content

Commit

Permalink
Support finding unused roles and SA fix (#12)
Browse files Browse the repository at this point in the history
* add support for finding unused roles

* find serviceaccounts used in rolebinding and clusterrolebinding

* update documentation with role command and SA fix

---------

Co-authored-by: Yonah Dissen <ydissen@vmware.com>
  • Loading branch information
yonahd and Yonah Dissen authored Jul 25, 2023
1 parent 631cee3 commit efd4955
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 10 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Kor is a CLI tool to discover unused Kubernetes resources. Currently, Kor can id
- ServiceAccounts
- Deployments
- Statefulsets
- Roles

![Kor Screenshot](/images/screenshot.png)

Expand All @@ -18,13 +19,14 @@ Download the binary for your operating system from the [releases page](https://g

Kor provides various subcommands to identify and list unused resources. The available commands are:

- `all`: Gets all unused resources (configmaps, secrets, services, and service accounts) for the specified namespace or all namespaces.
- `all`: Gets all unused resources for the specified namespace or all namespaces.
- `configmap`: Gets unused configmaps for the specified namespace or all namespaces.
- `secret`: Gets unused secrets for the specified namespace or all namespaces.
- `services`: Gets unused services for the specified namespace or all namespaces.
- `serviceaccount`: Gets unused service accounts for the specified namespace or all namespaces.
- `deployments`: Gets unused service accounts for the specified namespace or all namespaces.
- `statefulsets`: Gets unused service accounts for the specified namespace or all namespaces.
- `role`: Gets unused roles for the specified namespace or all namespaces.

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

Expand All @@ -46,8 +48,10 @@ kor [subcommand] --help
| 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 | |
| Services | Services with no endpoints | |
| Deployments | Deployments with 0 Replicas | |
| ServiceAccounts | ServiceAccounts used by pods | |
| ServiceAccounts | ServiceAccounts unused by pods<br/>ServiceAccounts unused by roleBinding or clusterRoleBinding | |
| Statefulsets | Statefulsets with no endpoints | |
| Roles | Roles not used in roleBinding | |



## Contributing
Expand Down
21 changes: 21 additions & 0 deletions cmd/kor/roles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package kor

import (
"github.com/spf13/cobra"
"github.com/yonahd/kor/pkg/kor"
)

var roleCmd = &cobra.Command{
Use: "role",
Short: "Gets unused roles",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
kor.GetUnusedRoles(namespace)

},
}

func init() {
roleCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Namespace to run on")
rootCmd.AddCommand(roleCmd)
}
19 changes: 15 additions & 4 deletions pkg/kor/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func getUnusedServiceAccounts(kubeClient *kubernetes.Clientset, namespace string
func getUnusedDeployments(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff {
deployDiff, err := ProcessNamespaceDeployments(kubeClient, namespace)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "serviceaccounts", namespace, err)
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "deployments", namespace, err)
}
namespaceSADiff := ResourceDiff{"Deployment", deployDiff}
return namespaceSADiff
Expand All @@ -59,12 +59,21 @@ func getUnusedDeployments(kubeClient *kubernetes.Clientset, namespace string) Re
func getUnusedStatefulsets(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff {
stsDiff, err := ProcessNamespaceStatefulsets(kubeClient, namespace)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "serviceaccounts", namespace, err)
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "statefulsets", namespace, err)
}
namespaceSADiff := ResourceDiff{"Statefulset", stsDiff}
return namespaceSADiff
}

func getUnusedRoles(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff {
roleDiff, err := processNamespaceRoles(kubeClient, namespace)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "roles", namespace, err)
}
namespaceSADiff := ResourceDiff{"Role", roleDiff}
return namespaceSADiff
}

func GetUnusedAll(namespace string) {
var kubeClient *kubernetes.Clientset
var namespaces []string
Expand All @@ -84,8 +93,10 @@ func GetUnusedAll(namespace string) {
allDiffs = append(allDiffs, namespaceSADiff)
namespaceDeploymentDiff := getUnusedDeployments(kubeClient, namespace)
allDiffs = append(allDiffs, namespaceDeploymentDiff)
namespacestatefulsetDiff := getUnusedStatefulsets(kubeClient, namespace)
allDiffs = append(allDiffs, namespacestatefulsetDiff)
namespaceStatefulsetDiff := getUnusedStatefulsets(kubeClient, namespace)
allDiffs = append(allDiffs, namespaceStatefulsetDiff)
namespaceRoleDiff := getUnusedRoles(kubeClient, namespace)
allDiffs = append(allDiffs, namespaceRoleDiff)
output := FormatOutputAll(namespace, allDiffs)
fmt.Println(output)
fmt.Println()
Expand Down
103 changes: 103 additions & 0 deletions pkg/kor/roles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package kor

import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
"os"
)

func retrieveUsedRoles(clientset *kubernetes.Clientset, namespace string) ([]string, error) {
// Get a list of all role bindings in the specified namespace
roleBindings, err := clientset.RbacV1().RoleBindings(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list role bindings in namespace %s: %v", namespace, err)
}

// Create a map to store role binding names
usedRoles := make(map[string]bool)

// Populate the map with role binding names
for _, rb := range roleBindings.Items {
usedRoles[rb.RoleRef.Name] = true
}

// Create a slice to store used role names
var usedRoleNames []string

// Extract used role names from the map
for role := range usedRoles {
usedRoleNames = append(usedRoleNames, role)
}

return usedRoleNames, nil
}

func retrieveRoleNames(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) {
roles, err := kubeClient.RbacV1().Roles(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, err
}
names := make([]string, 0, len(roles.Items))
for _, role := range roles.Items {
names = append(names, role.Name)
}
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 {
return nil, err
}

usedRoles = RemoveDuplicatesAndSort(usedRoles)

roleNames, err := retrieveRoleNames(kubeClient, namespace)
if err != nil {
return nil, err
}

diff := calculateRoleDifference(usedRoles, roleNames)
return diff, nil

}

func GetUnusedRoles(namespace string) {
var kubeClient *kubernetes.Clientset
var namespaces []string

kubeClient = GetKubeClient()

namespaces = SetNamespaceList(namespace, kubeClient)

for _, namespace := range namespaces {
diff, err := processNamespaceRoles(kubeClient, namespace)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
output := FormatOutput(namespace, diff, "Roles")
fmt.Println(output)
fmt.Println()
}
}
58 changes: 54 additions & 4 deletions pkg/kor/serviceaccounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,58 @@ var exceptionServiceAccounts = []ExceptionResource{
{ResourceName: "default", Namespace: "*"},
}

func retrieveUsedSA(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) {
func getServiceAccountsFromClusterRoleBindings(clientset *kubernetes.Clientset, namespace string) ([]string, error) {
// Get a list of all role bindings in the specified namespace
roleBindings, err := clientset.RbacV1().ClusterRoleBindings().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list role bindings in namespace %s: %v", namespace, err)
}

// Create a slice to store service account names
var serviceAccounts []string

// Extract service account names from the role bindings
for _, rb := range roleBindings.Items {
for _, subject := range rb.Subjects {
if subject.Kind == "ServiceAccount" {
serviceAccounts = append(serviceAccounts, subject.Name)
}
}
}

return serviceAccounts, nil
}

func getServiceAccountsFromRoleBindings(clientset *kubernetes.Clientset, namespace string) ([]string, error) {
// Get a list of all role bindings in the specified namespace
roleBindings, err := clientset.RbacV1().RoleBindings(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list role bindings in namespace %s: %v", namespace, err)
}

// Create a slice to store service account names
var serviceAccounts []string

// Extract service account names from the role bindings
for _, rb := range roleBindings.Items {
for _, subject := range rb.Subjects {
if subject.Kind == "ServiceAccount" {
serviceAccounts = append(serviceAccounts, subject.Name)
}
}
}

return serviceAccounts, nil
}

func retrieveUsedSA(kubeClient *kubernetes.Clientset, namespace string) ([]string, []string, []string, error) {

podServiceAccounts := []string{}

// Retrieve pods in the specified namespace
pods, err := kubeClient.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, err
return nil, nil, nil, err
}

// Extract service account names from pods
Expand All @@ -35,7 +79,9 @@ func retrieveUsedSA(kubeClient *kubernetes.Clientset, namespace string) ([]strin
}
}

return podServiceAccounts, nil
roleServiceAccounts, err := getServiceAccountsFromRoleBindings(kubeClient, namespace)
clusterRoleServiceAccounts, err := getServiceAccountsFromClusterRoleBindings(kubeClient, namespace)
return podServiceAccounts, roleServiceAccounts, clusterRoleServiceAccounts, nil
}

func retrieveServiceAccountNames(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) {
Expand All @@ -51,12 +97,16 @@ func retrieveServiceAccountNames(kubeClient *kubernetes.Clientset, namespace str
}

func processNamespaceSA(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) {
usedServiceAccounts, err := retrieveUsedSA(kubeClient, namespace)
usedServiceAccounts, roleServiceAccounts, clusterRoleServiceAccounts, err := retrieveUsedSA(kubeClient, namespace)
if err != nil {
return nil, err
}

usedServiceAccounts = RemoveDuplicatesAndSort(usedServiceAccounts)
roleServiceAccounts = RemoveDuplicatesAndSort(roleServiceAccounts)
clusterRoleServiceAccounts = RemoveDuplicatesAndSort(clusterRoleServiceAccounts)

usedServiceAccounts = append(append(usedServiceAccounts, roleServiceAccounts...), clusterRoleServiceAccounts...)

serviceAccountNames, err := retrieveServiceAccountNames(kubeClient, namespace)
if err != nil {
Expand Down

0 comments on commit efd4955

Please sign in to comment.