Repository: yonahd/kor (go)

Issue:
feature: Enhance unused Jobs discovery
Following [#283](https://github.com/yonahd/kor/pull/283), we added another indication for unused Jobs for Job that had reached BackoffLimit and failed.

Add the following indications to enhance the discovery:
1. Jobs that failed with `DeadlineExceeded`.
2. Job that failed with [FailedIndexes](https://kubernetes.io/docs/concepts/workloads/controllers/job/#backoff-limit-per-index) which is another type of BackoffLimit.
3. Suspended Jobs.

Pull request description:
Feat: Enhance unused jobs discovery
<!-- Please read [CONTRIBUTING.md](CONTRIBUTING.md) for additional information on contributing to this repository! -->

<!--
  !!!! README !!!! Please fill this out.

  Please follow conventional commit naming conventions:

  https://www.conventionalcommits.org/en/v1.0.0/#summary
-->

<!-- A short description of what your PR does and what it solves. -->

## What this PR does / why we need it?
Enhance unused Jobs discovery.
 - [DeadlineExceeded](https://kubernetes.io/docs/concepts/workloads/controllers/job/#job-termination-and-cleanup)
 - [FailedIndexes](https://kubernetes.io/docs/concepts/workloads/controllers/job/#backoff-limit-per-index)
 - [Suspended](https://kubernetes.io/docs/concepts/workloads/controllers/job/#suspending-a-job)
## PR Checklist

- [ ] This PR adds K8s exceptions (false positives)
- [x] This PR adds new code
- [x] This PR includes tests for new/existing code
- [ ] This PR adds docs
<!-- - [ ] This PR does something else -->

## GitHub Issue

Closes #315

<!-- Notes that may be helpful for anyone reviewing this PR -->

## Notes for your reviewers
![image](https://github.com/user-attachments/assets/deff02c9-ab8d-4004-87ee-4de40dc2b7d5)


Patch:
diff --git a/README.md b/README.md
index 748a2da..6fbc2c3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
 ![GitHub go.mod Go version (subdirectory of monorepo)](https://img.shields.io/github/go-mod/go-version/yonahd/kor)
-![GitHub release (with filter)](https://img.shields.io/github/v/release/yonahd/kor?color=green&link=https://github.com/yonahd/kor/releases)
+![GitHub Release](https://img.shields.io/github/v/release/yonahd/kor?filter=v*&color=green&link=https://github.com/yonahd/kor/releases)
+![GitHub Release](https://img.shields.io/github/v/release/yonahd/kor?filter=kor-*&logo=Helm&color=green&link=https://github.com/yonahd/kor/releases)
 ![Docker Pulls](https://img.shields.io/docker/pulls/yonahdissen/kor)
 [![codecov](https://codecov.io/gh/yonahd/kor/branch/main/graph/badge.svg?token=tNKcOjlxLo)](https://codecov.io/gh/yonahd/kor)
 [![Discord](https://discord.com/api/guilds/1159544275722321990/embed.png)](https://discord.gg/ajptYPwcJY)
@@ -166,14 +167,14 @@ kor [subcommand] --help
 | ServiceAccounts | ServiceAccounts unused by Pods<br/>ServiceAccounts unused by roleBinding or clusterRoleBinding                                                                                                                                    |                                                                                                                                                                       |
 | StatefulSets    | Statefulsets with no Replicas                                                                                                                                                                                                     |                                                                                                                                                                       |
 | Roles           | Roles not used in roleBinding                                                                                                                                                                                                     |                                                                                                                                                                       |
-| ClusterRoles    | ClusterRoles not used in roleBinding or clusterRoleBinding                                                                                                                                                                        |                                                                                                                                                                       |
+| ClusterRoles    | ClusterRoles not used in roleBinding or clusterRoleBinding<br/>ClusterRoles not used in ClusterRole aggregation                                                                                                                                                                        |                                                                                                                                                                       |
 | PVCs            | PVCs not used in Pods                                                                                                                                                                                                             |                                                                                                                                                                       |
 | Ingresses       | Ingresses not pointing at any Service                                                                                                                                                                                             |                                                                                                                                                                       |
 | Hpas            | HPAs not used in Deployments<br/> HPAs not used in StatefulSets                                                                                                                                                                   |                                                                                                                                                                       |
 | CRDs            | CRDs not used the cluster                                                                                                                                                                                                         |                                                                                                                                                                       |
 | Pvs             | PVs not bound to a PVC                                                                                                                                                                                                            |                                                                                                                                                                       |
 | Pdbs            | PDBs not used in Deployments<br/> PDBs not used in StatefulSets                                                                                                                                                                   |                                                                                                                                                                       |
-| Jobs            | Jobs status is completed<br/>  Jobs failed with no retries left                                                                                                                                                                                                         |                                                                                                                                                                       |
+| Jobs            | Jobs status is completed<br/>  Jobs status is suspended<br/>  Jobs failed with backoff limit exceeded (including indexed jobs) <br/> Jobs failed with dedaline exceeded                                                                                                                                              |                                                                                                                                                                       |
 | ReplicaSets     | replicaSets that specify replicas to 0 and has already completed it's work                                                                                                                                                        |
 | DaemonSets      | DaemonSets not scheduled on any nodes                                                                                                                                                                                             |
 | StorageClasses  | StorageClasses not used by any PVs/PVCs                                                                                                                                                                                           |
diff --git a/charts/kor/Chart.yaml b/charts/kor/Chart.yaml
index fffea60..cc20330 100644
--- a/charts/kor/Chart.yaml
+++ b/charts/kor/Chart.yaml
@@ -2,8 +2,8 @@ apiVersion: v2
 name: kor
 description: A Kubernetes Helm Chart to discover orphaned resources using kor
 type: application
-version: 0.1.10
-appVersion: "0.5.2"
+version: 0.1.11
+appVersion: "0.5.3"
 maintainers:
   - name: "yonahd"
     url: "https://github.com/yonahd/kor"
diff --git a/charts/kor/README.md b/charts/kor/README.md
index 7eb79fd..bebf0cc 100644
--- a/charts/kor/README.md
+++ b/charts/kor/README.md
@@ -1,6 +1,6 @@
 # kor
 
-![Version: 0.1.10](https://img.shields.io/badge/Version-0.1.10-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.5.2](https://img.shields.io/badge/AppVersion-0.5.2-informational?style=flat-square)
+![Version: 0.1.11](https://img.shields.io/badge/Version-0.1.11-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.5.3](https://img.shields.io/badge/AppVersion-0.5.3-informational?style=flat-square)
 
 A Kubernetes Helm Chart to discover orphaned resources using kor
 
diff --git a/pkg/kor/jobs.go b/pkg/kor/jobs.go
index d25757f..ddae0f9 100644
--- a/pkg/kor/jobs.go
+++ b/pkg/kor/jobs.go
@@ -7,6 +7,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"os"
+	"slices"
 
 	batchv1 "k8s.io/api/batch/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -52,11 +53,16 @@ func processNamespaceJobs(clientset kubernetes.Interface, namespace string, filt
 			unusedJobNames = append(unusedJobNames, ResourceInfo{Name: job.Name, Reason: reason})
 			continue
 		} else {
-			// Check if the job has a condition indicating it has exceeded the backoff limit
+			failureReasons := []string{"BackoffLimitExceeded", "DeadlineExceeded", "FailedIndexes"}
+
+			// Check if the job has a condition indicating it has failed
 			for _, condition := range job.Status.Conditions {
-				if condition.Type == batchv1.JobFailed && condition.Reason == "BackoffLimitExceeded" {
-					reason := "Job has exceeded backoff limit"
-					unusedJobNames = append(unusedJobNames, ResourceInfo{Name: job.Name, Reason: reason})
+				if condition.Type == batchv1.JobFailed && slices.Contains(failureReasons, condition.Reason) {
+					unusedJobNames = append(unusedJobNames, ResourceInfo{Name: job.Name, Reason: condition.Message})
+					break
+				}
+				if condition.Type == batchv1.JobSuspended {
+					unusedJobNames = append(unusedJobNames, ResourceInfo{Name: job.Name, Reason: condition.Message})
 					break
 				}
 			}


Test patch:
diff --git a/pkg/kor/jobs_test.go b/pkg/kor/jobs_test.go
index 557f679..3c30c42 100644
--- a/pkg/kor/jobs_test.go
+++ b/pkg/kor/jobs_test.go
@@ -90,6 +90,60 @@ func createTestJobs(t *testing.T) *fake.Clientset {
 		t.Fatalf("Error creating fake job: %v", err)
 	}
 
+	job6 := CreateTestJob(testNamespace, "test-job6", &batchv1.JobStatus{
+		Succeeded: 0,
+		Failed:    1,
+		Conditions: []batchv1.JobCondition{
+			{
+				Type:    batchv1.JobFailed,
+				Status:  corev1.ConditionTrue,
+				Reason:  "DeadlineExceeded",
+				Message: "Job was active longer than specified deadline",
+			},
+		},
+	}, AppLabels)
+
+	_, err = clientset.BatchV1().Jobs(testNamespace).Create(context.TODO(), job6, v1.CreateOptions{})
+	if err != nil {
+		t.Fatalf("Error creating fake job: %v", err)
+	}
+
+	job7 := CreateTestJob(testNamespace, "test-job7", &batchv1.JobStatus{
+		Succeeded: 0,
+		Failed:    1,
+		Conditions: []batchv1.JobCondition{
+			{
+				Type:    batchv1.JobFailed,
+				Status:  corev1.ConditionTrue,
+				Reason:  "FailedIndexes",
+				Message: "Job has failed indexes",
+			},
+		},
+	}, AppLabels)
+
+	_, err = clientset.BatchV1().Jobs(testNamespace).Create(context.TODO(), job7, v1.CreateOptions{})
+	if err != nil {
+		t.Fatalf("Error creating fake job: %v", err)
+	}
+
+	job8 := CreateTestJob(testNamespace, "test-job8", &batchv1.JobStatus{
+		Succeeded: 0,
+		Failed:    1,
+		Conditions: []batchv1.JobCondition{
+			{
+				Type:    batchv1.JobSuspended,
+				Status:  corev1.ConditionTrue,
+				Reason:  "JobSuspended",
+				Message: "Job suspended",
+			},
+		},
+	}, AppLabels)
+
+	_, err = clientset.BatchV1().Jobs(testNamespace).Create(context.TODO(), job8, v1.CreateOptions{})
+	if err != nil {
+		t.Fatalf("Error creating fake job: %v", err)
+	}
+
 	return clientset
 }
 
@@ -101,12 +155,16 @@ func TestProcessNamespaceJobs(t *testing.T) {
 		t.Errorf("Expected no error, got %v", err)
 	}
 
-	if len(unusedJobs) != 3 {
-		t.Errorf("Expected 3 jobs unused got %d", len(unusedJobs))
+	expectedJobsNames := []string{"test-job2", "test-job4", "test-job5", "test-job6", "test-job7", "test-job8"}
+
+	if len(unusedJobs) != len(expectedJobsNames) {
+		t.Errorf("Expected %d jobs unused got %d", len(expectedJobsNames), len(unusedJobs))
 	}
 
-	if unusedJobs[0].Name != "test-job2" && unusedJobs[1].Name != "test-job4" && unusedJobs[2].Name != "test-job5" {
-		t.Errorf("job2', got %s", unusedJobs[0])
+	for i, job := range unusedJobs {
+		if job.Name != expectedJobsNames[i] {
+			t.Errorf("Expected %s, got %s", expectedJobsNames[i], job.Name)
+		}
 	}
 }
 
@@ -133,6 +191,9 @@ func TestGetUnusedJobsStructured(t *testing.T) {
 				"test-job2",
 				"test-job4",
 				"test-job5",
+				"test-job6",
+				"test-job7",
+				"test-job8",
 			},
 		},
 	}