Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: show unused reason #278

Merged
merged 18 commits into from
Jun 17, 2024
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")
yonahd marked this conversation as resolved.
Show resolved Hide resolved
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