Repository: kubernetes/kubernetes (language: go)

Issue:
Job terminates when FailureTarget=False condition is added manually
### What happened?

When the FailureTarget=False condition is added manually, then the Job terminates.

### What did you expect to happen?

The condition should be ignored by the Job controller.

### How can we reproduce it (as minimally and precisely as possible)?

1. Prepare the `job.yaml` like this:
```yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: test-job
spec:
  template:
    spec:
      containers:
      - name: test-container
        image: busybox
        command: ["sleep", "3m"]
      restartPolicy: Never
```
2. Run the following script to repro:
```sh
kubectl create -f job.yaml

NAMESPACE=${NAMESPACE:-default}
JOB_NAME=${JOB_NAME:-test-job}

kubectl wait --for=jsonpath='{.status.ready}'=1 job $JOB_NAME -n $NAMESPACE

JOB_JSON=$(kubectl get job $JOB_NAME -n $NAMESPACE -o json)

UPDATED_JOB_JSON=$(echo $JOB_JSON | jq --arg type "FailureTarget" \
                                         --arg status "False" \
                                         --arg reason "ManualUpdateByScript" \
'.status.conditions += [{"type": $type, "status": $status, "reason": $reason, "lastTransitionTime": (now | strftime("%Y-%m-%dT%H:%M:%SZ"))}]')

kubectl patch job $JOB_NAME -n $NAMESPACE --patch "$UPDATED_JOB_JSON" --subresource='status'
```

**Issue**: the Job gets terminated with the status like this:

```
  status:
    conditions:
    - lastProbeTime: null
      lastTransitionTime: "2024-07-05T09:29:38Z"
      reason: ManualUpdateByScript
      status: "False"
      type: FailureTarget
    - lastProbeTime: "2024-07-05T09:29:39Z"
      lastTransitionTime: "2024-07-05T09:29:39Z"
      reason: ManualUpdateByScript
      status: "True"
      type: Failed
    failed: 1
    ready: 1
    startTime: "2024-07-05T09:29:35Z"
    terminating: 0
    uncountedTerminatedPods: {}
```

### Anything else we need to know?

_No response_

### Kubernetes version

<details>

```console
Client Version: v1.28.1
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
Server Version: v1.30.2
WARNING: version difference between client (1.28) and server (1.30) exceeds the supported minor version skew of +/-1
```

</details>


### Cloud provider

<details>
Kind
</details>


### OS version

<details>



</details>


### Install tools

<details>

</details>


### Container runtime (CRI) and version (if applicable)

<details>

</details>


### Related plugins (CNI, CSI, ...) and versions (if applicable)

<details>

</details>



Discussion / hints:
This issue is currently awaiting triage.

If a SIG or subproject determines this is a relevant issue, they will accept it by applying the `triage/accepted` label and provide further guidance.

The `triage/accepted` label can be added by org members by writing `/triage accepted` in a comment.


<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
/cc @atiratree @alculquicondor @kaisoz 
/assign 

Thanks! 😊 
/sig apps

Fix patch:
diff --git a/pkg/controller/job/job_controller.go b/pkg/controller/job/job_controller.go
index b34860bef66dc..872ee7dbe6c2b 100644
--- a/pkg/controller/job/job_controller.go
+++ b/pkg/controller/job/job_controller.go
@@ -845,7 +845,8 @@ func (jm *Controller) syncJob(ctx context.Context, key string) (rErr error) {
 	// Given that the Job already has the SuccessCriteriaMet condition, the termination condition already had confirmed in another cycle.
 	// So, the job-controller evaluates the podFailurePolicy only when the Job doesn't have the SuccessCriteriaMet condition.
 	if jobCtx.finishedCondition == nil && feature.DefaultFeatureGate.Enabled(features.JobPodFailurePolicy) {
-		if failureTargetCondition := findConditionByType(job.Status.Conditions, batch.JobFailureTarget); failureTargetCondition != nil {
+		failureTargetCondition := findConditionByType(job.Status.Conditions, batch.JobFailureTarget)
+		if failureTargetCondition != nil && failureTargetCondition.Status == v1.ConditionTrue {
 			jobCtx.finishedCondition = newFailedConditionForFailureTarget(failureTargetCondition, jm.clock.Now())
 		} else if failJobMessage := getFailJobMessage(&job, pods); failJobMessage != nil {
 			// Prepare the interim FailureTarget condition to record the failure message before the finalizers (allowing removal of the pods) are removed.


Test patch:
diff --git a/pkg/controller/job/job_controller_test.go b/pkg/controller/job/job_controller_test.go
index 0a525404dff11..58b9c9ca3b32d 100644
--- a/pkg/controller/job/job_controller_test.go
+++ b/pkg/controller/job/job_controller_test.go
@@ -232,10 +232,11 @@ func setPodsStatusesWithIndexes(podIndexer cache.Indexer, job *batch.Job, status
 }
 
 type jobInitialStatus struct {
-	active    int
-	succeed   int
-	failed    int
-	startTime *time.Time
+	active     int
+	succeed    int
+	failed     int
+	startTime  *time.Time
+	conditions []batch.JobCondition
 }
 
 func TestControllerSyncJob(t *testing.T) {
@@ -1154,6 +1155,38 @@ func TestControllerSyncJob(t *testing.T) {
 			podIndexLabelDisabled:  true,
 			expectedReady:          ptr.To[int32](0),
 		},
+		"FailureTarget=False condition added manually is ignored": {
+			jobPodFailurePolicy: true,
+			parallelism:         1,
+			completions:         1,
+			activePods:          1,
+			readyPods:           1,
+			initialStatus: &jobInitialStatus{
+				active: 1,
+				startTime: func() *time.Time {
+					now := time.Now()
+					return &now
+				}(),
+				conditions: []batch.JobCondition{
+					{
+						Type:    batch.JobFailureTarget,
+						Status:  v1.ConditionFalse,
+						Reason:  "ConditionAddedManually",
+						Message: "Testing",
+					},
+				},
+			},
+			expectedActive: 1,
+			expectedReady:  ptr.To[int32](1),
+			expectedConditions: []batch.JobCondition{
+				{
+					Type:    batch.JobFailureTarget,
+					Status:  v1.ConditionFalse,
+					Reason:  "ConditionAddedManually",
+					Message: "Testing",
+				},
+			},
+		},
 	}
 
 	for name, tc := range testCases {
@@ -1197,6 +1230,7 @@ func TestControllerSyncJob(t *testing.T) {
 					startTime := metav1.NewTime(*tc.initialStatus.startTime)
 					job.Status.StartTime = &startTime
 				}
+				job.Status.Conditions = append(job.Status.Conditions, tc.initialStatus.conditions...)
 			}
 
 			key, err := controller.KeyFunc(job)