Skip to content

Commit

Permalink
Feat: show unused reason (#278)
Browse files Browse the repository at this point in the history
* first batch

* second batch

* second batch

* second batch

* edit the all

* fix all, multi only shows reason(need to fix)

* fix multi

* adjust tests

* import order

* adjust test

* Change flag name

* fix formatter

* add flag to readme

* rebase

* rebase

* fix unused reason

---------

Co-authored-by: Yonah Dissen <yonahdissen@Yonahs-MacBook-Pro.local>
  • Loading branch information
yonahd and Yonah Dissen authored Jun 17, 2024
1 parent d9e9b4d commit 107c885
Show file tree
Hide file tree
Showing 46 changed files with 633 additions and 362 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Kor provides various subcommands to identify and list unused resources. The avai
--no-interactive Do not prompt for confirmation when deleting resources. Be careful using this flag!
--older-than string The minimum age of the resources to be considered unused. This flag cannot be used together with newer-than flag. Example: --older-than=1h2m
-o, --output string Output format (table, json or yaml) (default "table")
-r, --show-reason Print reason resource is considered unused
--slack-auth-token string Slack auth token to send notifications to. --slack-auth-token requires --slack-channel to be set.
--slack-channel string Slack channel to send notifications to. --slack-channel requires --slack-auth-token to be set.
--slack-webhook-url string Slack webhook URL to send notifications to
Expand Down
1 change: 1 addition & 0 deletions cmd/kor/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func init() {
rootCmd.PersistentFlags().BoolVar(&opts.NoInteractive, "no-interactive", false, "Do not prompt for confirmation when deleting resources. Be careful using this flag!")
rootCmd.PersistentFlags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Verbose output (print empty namespaces)")
rootCmd.PersistentFlags().StringVar(&opts.GroupBy, "group-by", "namespace", "Group output by (namespace, resource)")
rootCmd.PersistentFlags().BoolVarP(&opts.ShowReason, "show-reason", "r", false, "Print reason resource is considered unused")
addFilterOptionsFlag(rootCmd, filterOptions)
}

Expand Down
10 changes: 5 additions & 5 deletions pkg/kor/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type GetUnusedResourceJSONResponse struct {

type ResourceDiff struct {
resourceType string
diff []string
diff []ResourceInfo
}

func getUnusedCMs(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ResourceDiff {
Expand Down Expand Up @@ -252,11 +252,11 @@ func getUnusedStorageClasses(clientset kubernetes.Interface, filterOpts *filters
}

func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
resources := make(map[string]map[string][]string)
resources := make(map[string]map[string][]ResourceInfo)
for _, namespace := range filterOpts.Namespaces(clientset) {
switch opts.GroupBy {
case "namespace":
resources[namespace] = make(map[string][]string)
resources[namespace] = make(map[string][]ResourceInfo)
resources[namespace]["ConfigMap"] = getUnusedCMs(clientset, namespace, filterOpts).diff
resources[namespace]["Service"] = getUnusedSVCs(clientset, namespace, filterOpts).diff
resources[namespace]["Secret"] = getUnusedSecrets(clientset, namespace, filterOpts).diff
Expand Down Expand Up @@ -312,10 +312,10 @@ func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.In
}

func GetUnusedAllNonNamespaced(filterOpts *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts Opts) (string, error) {
resources := make(map[string]map[string][]string)
resources := make(map[string]map[string][]ResourceInfo)
switch opts.GroupBy {
case "namespace":
resources[""] = make(map[string][]string)
resources[""] = make(map[string][]ResourceInfo)
resources[""]["Crd"] = getUnusedCrds(apiExtClient, dynamicClient, filterOpts).diff
resources[""]["Pv"] = getUnusedPvs(clientset, filterOpts).diff
resources[""]["ClusterRole"] = getUnusedClusterRoles(clientset, filterOpts).diff
Expand Down
21 changes: 15 additions & 6 deletions pkg/kor/clusterroles.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func retrieveClusterRoleNames(clientset kubernetes.Interface, filterOpts *filter
return names, unusedClusterRoles, nil
}

func processClusterRoles(clientset kubernetes.Interface, filterOpts *filters.Options) ([]string, error) {
func processClusterRoles(clientset kubernetes.Interface, filterOpts *filters.Options) ([]ResourceInfo, error) {
usedClusterRoles, err := retrieveUsedClusterRoles(clientset, filterOpts)
if err != nil {
return nil, err
Expand All @@ -169,28 +169,37 @@ func processClusterRoles(clientset kubernetes.Interface, filterOpts *filters.Opt
return nil, err
}

diff := CalculateResourceDifference(usedClusterRoles, clusterRoleNames)
diff = append(diff, unusedClusterRoles...)
var diff []ResourceInfo

for _, name := range CalculateResourceDifference(usedClusterRoles, clusterRoleNames) {
reason := "ClusterRole is not used by any RoleBinding or ClusterRoleBinding"
diff = append(diff, ResourceInfo{Name: name, Reason: reason})
}

for _, name := range unusedClusterRoles {
reason := "Marked with unused label"
diff = append(diff, ResourceInfo{Name: name, Reason: reason})
}

return diff, nil

}

func GetUnusedClusterRoles(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
resources := make(map[string]map[string][]string)
resources := make(map[string]map[string][]ResourceInfo)
diff, err := processClusterRoles(clientset, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process cluster role : %v\n", err)
}
switch opts.GroupBy {
case "namespace":
resources[""] = make(map[string][]string)
resources[""] = make(map[string][]ResourceInfo)
resources[""]["ClusterRole"] = diff
case "resource":
appendResources(resources, "ClusterRole", "", diff)
}
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, "", "ClusterRole", opts.NoInteractive); err != nil {
if diff, err = DeleteResource2(diff, clientset, "", "ClusterRole", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete clusterRole %s : %v\n", diff, err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/kor/clusterroles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func TestProcessClusterRoles(t *testing.T) {
t.Errorf("Expected 2 unused role, got %d", len(unusedClusterRoles))
}

if unusedClusterRoles[0] != "test-clusterRole1" && unusedClusterRoles[1] != "test-clusterRole5" {
if unusedClusterRoles[0].Name != "test-clusterRole1" && unusedClusterRoles[1].Name != "test-clusterRole5" {
t.Errorf("Expected 'test-clusterRole1', 'test-clusterRole5', got %s, %s", unusedClusterRoles[0], unusedClusterRoles[1])
}
}
Expand Down
26 changes: 15 additions & 11 deletions pkg/kor/configmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func retrieveConfigMapNames(clientset kubernetes.Interface, namespace string, fi
return names, unusedConfigmapNames, nil
}

func processNamespaceCM(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
func processNamespaceCM(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]ResourceInfo, error) {
volumesCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, err := retrieveUsedCM(clientset, namespace)
if err != nil {
return nil, err
Expand Down Expand Up @@ -135,27 +135,31 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, filter
usedConfigMaps = append(usedConfigMaps, slice...)
}

diff := CalculateResourceDifference(usedConfigMaps, configMapNames)
diff = append(diff, unusedConfigmapNames...)
var diff []ResourceInfo

var result []string
for _, cmName := range diff {
exceptionFound, err := isResourceException(cmName, namespace, config.ExceptionConfigMaps)
for _, name := range CalculateResourceDifference(usedConfigMaps, configMapNames) {
exceptionFound, err := isResourceException(name, namespace, config.ExceptionConfigMaps)
if err != nil {
return nil, err
}

if exceptionFound {
continue
}
result = append(result, cmName)
reason := "ConfigMap is not used in any pod or container"
diff = append(diff, ResourceInfo{Name: name, Reason: reason})
}

return result, nil
for _, name := range unusedConfigmapNames {
reason := "Marked with unused label"
diff = append(diff, ResourceInfo{Name: name, Reason: reason})
}

return diff, nil
}

func GetUnusedConfigmaps(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
resources := make(map[string]map[string][]string)
resources := make(map[string]map[string][]ResourceInfo)
for _, namespace := range filterOpts.Namespaces(clientset) {
diff, err := processNamespaceCM(clientset, namespace, filterOpts)
if err != nil {
Expand All @@ -164,13 +168,13 @@ func GetUnusedConfigmaps(filterOpts *filters.Options, clientset kubernetes.Inter
}
switch opts.GroupBy {
case "namespace":
resources[namespace] = make(map[string][]string)
resources[namespace] = make(map[string][]ResourceInfo)
resources[namespace]["ConfigMap"] = diff
case "resource":
appendResources(resources, "ConfigMap", namespace, diff)
}
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "ConfigMap", opts.NoInteractive); err != nil {
if diff, err = DeleteResource2(diff, clientset, namespace, "ConfigMap", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete ConfigMap %s in namespace %s: %v\n", diff, namespace, err)
}
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/kor/configmaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,11 @@ func TestProcessNamespaceCM(t *testing.T) {
t.Fatalf("Error processing namespace CM: %v", err)
}

unusedConfigmaps := []string{
"configmap-3",
"configmap-5",
unusedConfigmaps := []ResourceInfo{
{Name: "configmap-3", Reason: "ConfigMap is not used in any pod or container"},
{Name: "configmap-5", Reason: "Marked with unused label"},
}
if !equalSlices(diff, unusedConfigmaps) {
if !equalResourceInfoSlices(diff, unusedConfigmaps) {
t.Errorf("Expected diff %v, got %v", unusedConfigmaps, diff)
}
}
Expand Down
11 changes: 6 additions & 5 deletions pkg/kor/crds.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import (
//go:embed exceptions/crds/crds.json
var crdsConfig []byte

func processCrds(apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, filterOpts *filters.Options) ([]string, error) {
func processCrds(apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, filterOpts *filters.Options) ([]ResourceInfo, error) {

var unusedCRDs []string
var unusedCRDs []ResourceInfo

crds, err := apiExtClient.ApiextensionsV1().CustomResourceDefinitions().List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
if err != nil {
Expand Down Expand Up @@ -58,21 +58,22 @@ func processCrds(apiExtClient apiextensionsclientset.Interface, dynamicClient dy
return nil, err
}
if len(instances.Items) == 0 {
unusedCRDs = append(unusedCRDs, crd.Name)
reason := "CRD has no instances"
unusedCRDs = append(unusedCRDs, ResourceInfo{Name: crd.Name, Reason: reason})
}
}
return unusedCRDs, nil
}

func GetUnusedCrds(_ *filters.Options, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts Opts) (string, error) {
resources := make(map[string]map[string][]string)
resources := make(map[string]map[string][]ResourceInfo)
diff, err := processCrds(apiExtClient, dynamicClient, &filters.Options{})
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process crds: %v\n", err)
}
switch opts.GroupBy {
case "namespace":
resources[""] = make(map[string][]string)
resources[""] = make(map[string][]ResourceInfo)
resources[""]["Crd"] = diff
case "resource":
appendResources(resources, "Crd", "", diff)
Expand Down
16 changes: 9 additions & 7 deletions pkg/kor/daemonsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ import (
//go:embed exceptions/daemonsets/daemonsets.json
var daemonsetsConfig []byte

func processNamespaceDaemonSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
func processNamespaceDaemonSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]ResourceInfo, error) {
daemonSetsList, err := clientset.AppsV1().DaemonSets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
if err != nil {
return nil, err
}

var daemonSetsWithoutReplicas []string
var daemonSetsWithoutReplicas []ResourceInfo

for _, daemonSet := range daemonSetsList.Items {
if pass, _ := filter.SetObject(&daemonSet).Run(filterOpts); pass {
Expand All @@ -45,20 +45,22 @@ func processNamespaceDaemonSets(clientset kubernetes.Interface, namespace string
}

if daemonSet.Labels["kor/used"] == "false" {
daemonSetsWithoutReplicas = append(daemonSetsWithoutReplicas, daemonSet.Name)
reason := "Marked with unused label"
daemonSetsWithoutReplicas = append(daemonSetsWithoutReplicas, ResourceInfo{Name: daemonSet.Name, Reason: reason})
continue
}

if daemonSet.Status.CurrentNumberScheduled == 0 {
daemonSetsWithoutReplicas = append(daemonSetsWithoutReplicas, daemonSet.Name)
reason := "DaemonSet has no replicas"
daemonSetsWithoutReplicas = append(daemonSetsWithoutReplicas, ResourceInfo{Name: daemonSet.Name, Reason: reason})
}
}

return daemonSetsWithoutReplicas, nil
}

func GetUnusedDaemonSets(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
resources := make(map[string]map[string][]string)
resources := make(map[string]map[string][]ResourceInfo)
for _, namespace := range filterOpts.Namespaces(clientset) {
diff, err := processNamespaceDaemonSets(clientset, namespace, filterOpts)
if err != nil {
Expand All @@ -67,13 +69,13 @@ func GetUnusedDaemonSets(filterOpts *filters.Options, clientset kubernetes.Inter
}
switch opts.GroupBy {
case "namespace":
resources[namespace] = make(map[string][]string)
resources[namespace] = make(map[string][]ResourceInfo)
resources[namespace]["DaemonSet"] = diff
case "resource":
appendResources(resources, "DaemonSet", namespace, diff)
}
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "DaemonSet", opts.NoInteractive); err != nil {
if diff, err = DeleteResource2(diff, clientset, namespace, "DaemonSet", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete DaemonSet %s in namespace %s: %v\n", diff, namespace, err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/kor/daemonsets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestProcessNamespaceDaemonSets(t *testing.T) {
t.Errorf("Expected 1 DaemonSet without replicas, got %d", len(daemonSetsWithoutReplicas))
}

if daemonSetsWithoutReplicas[0] != "test-ds1" && daemonSetsWithoutReplicas[1] != "test-ds4" {
if daemonSetsWithoutReplicas[0].Name != "test-ds1" && daemonSetsWithoutReplicas[1].Name != "test-ds4" {
t.Errorf("Expected 'test-ds1', 'test-ds4', got %s, %s", daemonSetsWithoutReplicas[0], daemonSetsWithoutReplicas[1])
}
}
Expand Down
Loading

0 comments on commit 107c885

Please sign in to comment.