Fix validation for Job with suspend=true,completions=0 to set Complete condition

This commit is contained in:
Michal Wozniak 2025-06-30 07:57:28 +02:00
parent f208b6c73d
commit 3851253305
3 changed files with 55 additions and 1 deletions

View File

@ -379,6 +379,7 @@ func getStatusValidationOptions(newJob, oldJob *batch.Job) batchvalidation.JobSt
isUncountedTerminatedPodsChanged := !apiequality.Semantic.DeepEqual(oldJob.Status.UncountedTerminatedPods, newJob.Status.UncountedTerminatedPods) isUncountedTerminatedPodsChanged := !apiequality.Semantic.DeepEqual(oldJob.Status.UncountedTerminatedPods, newJob.Status.UncountedTerminatedPods)
isReadyChanged := !ptr.Equal(oldJob.Status.Ready, newJob.Status.Ready) isReadyChanged := !ptr.Equal(oldJob.Status.Ready, newJob.Status.Ready)
isTerminatingChanged := !ptr.Equal(oldJob.Status.Terminating, newJob.Status.Terminating) isTerminatingChanged := !ptr.Equal(oldJob.Status.Terminating, newJob.Status.Terminating)
isSuspendedWithZeroCompletions := newJob.Spec.Completions != nil && *newJob.Spec.Completions == 0 && newJob.Spec.Suspend != nil && *newJob.Spec.Suspend
return batchvalidation.JobStatusValidationOptions{ return batchvalidation.JobStatusValidationOptions{
// We allow to decrease the counter for succeeded pods for jobs which // We allow to decrease the counter for succeeded pods for jobs which
@ -394,7 +395,7 @@ func getStatusValidationOptions(newJob, oldJob *batch.Job) batchvalidation.JobSt
RejectFailedJobWithoutFailureTarget: isJobFailedChanged || isFailedIndexesChanged, RejectFailedJobWithoutFailureTarget: isJobFailedChanged || isFailedIndexesChanged,
RejectCompleteJobWithoutSuccessCriteriaMet: isJobCompleteChanged || isJobSuccessCriteriaMetChanged, RejectCompleteJobWithoutSuccessCriteriaMet: isJobCompleteChanged || isJobSuccessCriteriaMetChanged,
RejectFinishedJobWithActivePods: isJobFinishedChanged || isActiveChanged, RejectFinishedJobWithActivePods: isJobFinishedChanged || isActiveChanged,
RejectFinishedJobWithoutStartTime: isJobFinishedChanged || isStartTimeChanged, RejectFinishedJobWithoutStartTime: (isJobFinishedChanged || isStartTimeChanged) && !isSuspendedWithZeroCompletions,
RejectFinishedJobWithUncountedTerminatedPods: isJobFinishedChanged || isUncountedTerminatedPodsChanged, RejectFinishedJobWithUncountedTerminatedPods: isJobFinishedChanged || isUncountedTerminatedPodsChanged,
RejectStartTimeUpdateForUnsuspendedJob: isStartTimeChanged, RejectStartTimeUpdateForUnsuspendedJob: isStartTimeChanged,
RejectCompletionTimeBeforeStartTime: isStartTimeChanged || isCompletionTimeChanged, RejectCompletionTimeBeforeStartTime: isStartTimeChanged || isCompletionTimeChanged,

View File

@ -3535,6 +3535,36 @@ func TestStatusStrategy_ValidateUpdate(t *testing.T) {
{Type: field.ErrorTypeInvalid, Field: "status.ready"}, {Type: field.ErrorTypeInvalid, Field: "status.ready"},
}, },
}, },
"valid transition to Complete for suspended Job with completions=0; without startTime": {
enableJobManagedBy: true,
job: &batch.Job{
ObjectMeta: validObjectMeta,
Spec: batch.JobSpec{
Completions: ptr.To[int32](0),
Suspend: ptr.To(true),
},
},
newJob: &batch.Job{
ObjectMeta: validObjectMeta,
Spec: batch.JobSpec{
Completions: ptr.To[int32](0),
Suspend: ptr.To(true),
},
Status: batch.JobStatus{
CompletionTime: &now,
Conditions: []batch.JobCondition{
{
Type: batch.JobSuccessCriteriaMet,
Status: api.ConditionTrue,
},
{
Type: batch.JobComplete,
Status: api.ConditionTrue,
},
},
},
},
},
} }
for name, tc := range cases { for name, tc := range cases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {

View File

@ -2801,6 +2801,29 @@ func TestParallelJobWithCompletions(t *testing.T) {
}) })
} }
// TestSuspendedJobWithZeroCompletions verifies the suspended Job with
// completions=0 is marked as Complete.
func TestSuspendedJobWithZeroCompletions(t *testing.T) {
closeFn, restConfig, clientSet, ns := setup(t, "suspended-with-zero-completions")
t.Cleanup(closeFn)
ctx, cancel := startJobControllerAndWaitForCaches(t, restConfig)
t.Cleanup(func() {
cancel()
})
jobObj, err := createJobWithDefaults(ctx, clientSet, ns.Name, &batchv1.Job{
Spec: batchv1.JobSpec{
Completions: ptr.To[int32](0),
Suspend: ptr.To(true),
},
})
if err != nil {
t.Fatalf("Failed to create Job: %v", err)
}
for _, condition := range []batchv1.JobConditionType{batchv1.JobSuccessCriteriaMet, batchv1.JobComplete} {
validateJobCondition(ctx, t, clientSet, jobObj, condition)
}
}
func TestIndexedJob(t *testing.T) { func TestIndexedJob(t *testing.T) {
t.Cleanup(setDurationDuringTest(&jobcontroller.DefaultJobPodFailureBackOff, fastPodFailureBackoff)) t.Cleanup(setDurationDuringTest(&jobcontroller.DefaultJobPodFailureBackOff, fastPodFailureBackoff))
closeFn, restConfig, clientSet, ns := setup(t, "indexed") closeFn, restConfig, clientSet, ns := setup(t, "indexed")