diff --git a/README.md b/README.md index aa3145d0..d1efd0e4 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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]`. @@ -46,8 +48,10 @@ kor [subcommand] --help | 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 | | | Services | Services with no endpoints | | | Deployments | Deployments with 0 Replicas | | -| ServiceAccounts | ServiceAccounts used by pods | | +| ServiceAccounts | ServiceAccounts unused by pods
ServiceAccounts unused by roleBinding or clusterRoleBinding | | | Statefulsets | Statefulsets with no endpoints | | +| Roles | Roles not used in roleBinding | | + ## Contributing diff --git a/cmd/kor/roles.go b/cmd/kor/roles.go new file mode 100644 index 00000000..415970ba --- /dev/null +++ b/cmd/kor/roles.go @@ -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) +} diff --git a/pkg/kor/all.go b/pkg/kor/all.go index 60d45b91..a3134507 100644 --- a/pkg/kor/all.go +++ b/pkg/kor/all.go @@ -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 @@ -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 @@ -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() diff --git a/pkg/kor/roles.go b/pkg/kor/roles.go new file mode 100644 index 00000000..6c4f2649 --- /dev/null +++ b/pkg/kor/roles.go @@ -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() + } +} diff --git a/pkg/kor/serviceaccounts.go b/pkg/kor/serviceaccounts.go index fa5de81d..d2d1d6a5 100644 --- a/pkg/kor/serviceaccounts.go +++ b/pkg/kor/serviceaccounts.go @@ -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 @@ -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) { @@ -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 {