Skip to content

Commit 0436215

Browse files
authored
Merge pull request #584 from danielvegamyhre/automated-cherry-pick-of-#580-upstream-release-0.5
Automated cherry pick of #580: relax validation on replicated jobs
2 parents 43f8137 + d3a991a commit 0436215

File tree

4 files changed

+151
-43
lines changed

4 files changed

+151
-43
lines changed

pkg/util/testing/wrappers.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,13 @@ func (j *JobTemplateWrapper) CompletionMode(mode batchv1.CompletionMode) *JobTem
225225
return j
226226
}
227227

228-
// PodSpec Containers sets the pod template spec containers.
228+
// PodTemplateSpec sets the pod template spec in a Job spec.
229+
func (j *JobTemplateWrapper) PodTemplateSpec(podTemplateSpec corev1.PodTemplateSpec) *JobTemplateWrapper {
230+
j.Spec.Template = podTemplateSpec
231+
return j
232+
}
233+
234+
// PodSpec sets the pod spec in a Job template.
229235
func (j *JobTemplateWrapper) PodSpec(podSpec corev1.PodSpec) *JobTemplateWrapper {
230236
j.Spec.Template.Spec = podSpec
231237
return j

pkg/webhooks/jobset_webhook.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,19 @@ func (j *jobSetWebhook) ValidateUpdate(ctx context.Context, old, newObj runtime.
218218
return nil, fmt.Errorf("expected a JobSet from old object but got a %T", old)
219219
}
220220
mungedSpec := js.Spec.DeepCopy()
221+
222+
// Allow pod template to be mutated for suspended JobSets.
223+
// This is needed for integration with Kueue/DWS.
221224
if ptr.Deref(oldJS.Spec.Suspend, false) {
222225
for index := range js.Spec.ReplicatedJobs {
226+
// Pod values which must be mutable for Kueue are defined here: https://github.com/kubernetes-sigs/kueue/blob/a50d395c36a2cb3965be5232162cf1fded1bdb08/apis/kueue/v1beta1/workload_types.go#L256-L260
227+
mungedSpec.ReplicatedJobs[index].Template.Spec.Template.Annotations = oldJS.Spec.ReplicatedJobs[index].Template.Spec.Template.Annotations
228+
mungedSpec.ReplicatedJobs[index].Template.Spec.Template.Labels = oldJS.Spec.ReplicatedJobs[index].Template.Spec.Template.Labels
223229
mungedSpec.ReplicatedJobs[index].Template.Spec.Template.Spec.NodeSelector = oldJS.Spec.ReplicatedJobs[index].Template.Spec.Template.Spec.NodeSelector
230+
mungedSpec.ReplicatedJobs[index].Template.Spec.Template.Spec.Tolerations = oldJS.Spec.ReplicatedJobs[index].Template.Spec.Template.Spec.Tolerations
224231
}
225232
}
233+
226234
// Note that SucccessPolicy and failurePolicy are made immutable via CEL.
227235
errs := apivalidation.ValidateImmutableField(mungedSpec.ReplicatedJobs, oldJS.Spec.ReplicatedJobs, field.NewPath("spec").Child("replicatedJobs"))
228236
errs = append(errs, apivalidation.ValidateImmutableField(mungedSpec.ManagedBy, oldJS.Spec.ManagedBy, field.NewPath("spec").Child("managedBy"))...)

pkg/webhooks/jobset_webhook_test.go

Lines changed: 91 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/google/go-cmp/cmp"
12+
"github.com/google/go-cmp/cmp/cmpopts"
1213
"github.com/stretchr/testify/assert"
1314
batchv1 "k8s.io/api/batch/v1"
1415
corev1 "k8s.io/api/core/v1"
@@ -993,7 +994,7 @@ func TestValidateUpdate(t *testing.T) {
993994
}.ToAggregate(),
994995
},
995996
{
996-
name: "replicated jobs are immutable",
997+
name: "replicated job pod template can be updated for suspended jobset",
997998
js: &jobset.JobSet{
998999
ObjectMeta: validObjectMeta,
9991000
Spec: jobset.JobSetSpec{
@@ -1002,17 +1003,56 @@ func TestValidateUpdate(t *testing.T) {
10021003
Name: "test-jobset-replicated-job-0",
10031004
Replicas: 2,
10041005
Template: batchv1.JobTemplateSpec{
1006+
// Adding an annotation.
10051007
Spec: batchv1.JobSpec{
10061008
Parallelism: ptr.To[int32](2),
1009+
Template: corev1.PodTemplateSpec{
1010+
ObjectMeta: metav1.ObjectMeta{
1011+
Annotations: map[string]string{"key": "value"},
1012+
},
1013+
},
10071014
},
10081015
},
10091016
},
1017+
},
1018+
},
1019+
},
1020+
oldJs: &jobset.JobSet{
1021+
ObjectMeta: validObjectMeta,
1022+
Spec: jobset.JobSetSpec{
1023+
Suspend: ptr.To(true),
1024+
ReplicatedJobs: []jobset.ReplicatedJob{
10101025
{
1011-
Name: "test-jobset-replicated-job-1",
1012-
Replicas: 1,
1026+
Name: "test-jobset-replicated-job-0",
1027+
Replicas: 2,
10131028
Template: batchv1.JobTemplateSpec{
10141029
Spec: batchv1.JobSpec{
1015-
Parallelism: ptr.To[int32](1),
1030+
Parallelism: ptr.To[int32](2),
1031+
},
1032+
},
1033+
},
1034+
},
1035+
},
1036+
},
1037+
},
1038+
{
1039+
name: "replicated job pod template cannot be updated for running jobset",
1040+
js: &jobset.JobSet{
1041+
ObjectMeta: validObjectMeta,
1042+
Spec: jobset.JobSetSpec{
1043+
ReplicatedJobs: []jobset.ReplicatedJob{
1044+
{
1045+
Name: "test-jobset-replicated-job-0",
1046+
Replicas: 2,
1047+
Template: batchv1.JobTemplateSpec{
1048+
// Adding an annotation.
1049+
Spec: batchv1.JobSpec{
1050+
Parallelism: ptr.To[int32](2),
1051+
Template: corev1.PodTemplateSpec{
1052+
ObjectMeta: metav1.ObjectMeta{
1053+
Annotations: map[string]string{"key": "value"},
1054+
},
1055+
},
10161056
},
10171057
},
10181058
},
@@ -1022,31 +1062,60 @@ func TestValidateUpdate(t *testing.T) {
10221062
oldJs: &jobset.JobSet{
10231063
ObjectMeta: validObjectMeta,
10241064
Spec: jobset.JobSetSpec{
1025-
Suspend: ptr.To(true),
1026-
ReplicatedJobs: validReplicatedJobs,
1065+
ReplicatedJobs: []jobset.ReplicatedJob{
1066+
{
1067+
Name: "test-jobset-replicated-job-0",
1068+
Replicas: 2,
1069+
Template: batchv1.JobTemplateSpec{
1070+
Spec: batchv1.JobSpec{
1071+
Parallelism: ptr.To[int32](2),
1072+
},
1073+
},
1074+
},
1075+
},
10271076
},
10281077
},
10291078
want: field.ErrorList{
1030-
field.Invalid(field.NewPath("spec").Child("replicatedJobs"), []jobset.ReplicatedJob{
1031-
{
1032-
Name: "test-jobset-replicated-job-0",
1033-
Replicas: 2,
1034-
Template: batchv1.JobTemplateSpec{
1035-
Spec: batchv1.JobSpec{
1036-
Parallelism: ptr.To[int32](2),
1079+
field.Invalid(field.NewPath("spec").Child("replicatedJobs"), "", "field is immutable"),
1080+
}.ToAggregate(),
1081+
},
1082+
{
1083+
name: "replicated job name cannot be updated",
1084+
js: &jobset.JobSet{
1085+
ObjectMeta: validObjectMeta,
1086+
Spec: jobset.JobSetSpec{
1087+
ReplicatedJobs: []jobset.ReplicatedJob{
1088+
{
1089+
Name: "new-replicated-job-name",
1090+
Replicas: 2,
1091+
Template: batchv1.JobTemplateSpec{
1092+
Spec: batchv1.JobSpec{
1093+
Parallelism: ptr.To[int32](2),
1094+
},
10371095
},
10381096
},
10391097
},
1040-
{
1041-
Name: "test-jobset-replicated-job-1",
1042-
Replicas: 1,
1043-
Template: batchv1.JobTemplateSpec{
1044-
Spec: batchv1.JobSpec{
1045-
Parallelism: ptr.To[int32](1),
1098+
},
1099+
},
1100+
oldJs: &jobset.JobSet{
1101+
ObjectMeta: validObjectMeta,
1102+
Spec: jobset.JobSetSpec{
1103+
Suspend: ptr.To(true),
1104+
ReplicatedJobs: []jobset.ReplicatedJob{
1105+
{
1106+
Name: "test-jobset-replicated-job-0",
1107+
Replicas: 2,
1108+
Template: batchv1.JobTemplateSpec{
1109+
Spec: batchv1.JobSpec{
1110+
Parallelism: ptr.To[int32](2),
1111+
},
10461112
},
10471113
},
10481114
},
1049-
}, "field is immutable"),
1115+
},
1116+
},
1117+
want: field.ErrorList{
1118+
field.Invalid(field.NewPath("spec").Child("replicatedJobs"), "", "field is immutable"),
10501119
}.ToAggregate(),
10511120
},
10521121
}
@@ -1059,7 +1128,8 @@ func TestValidateUpdate(t *testing.T) {
10591128
newObj := tc.js.DeepCopyObject()
10601129
oldObj := tc.oldJs.DeepCopyObject()
10611130
_, err = webhook.ValidateUpdate(context.TODO(), oldObj, newObj)
1062-
if diff := cmp.Diff(tc.want, err); diff != "" {
1131+
// Ignore bad value to keep test cases short and readable.
1132+
if diff := cmp.Diff(tc.want, err, cmpopts.IgnoreFields(field.Error{}, "BadValue")); diff != "" {
10631133
t.Errorf("ValidateResources() mismatch (-want +got):\n%s", diff)
10641134
}
10651135
})

test/integration/webhook/jobset_webhook_test.go

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ var _ = ginkgo.Describe("jobset webhook defaulting", func() {
7373
updateShouldFail bool
7474
}
7575

76-
ginkgo.DescribeTable("defaulting on jobset creation",
76+
ginkgo.DescribeTable("jobset webhook tests",
7777
func(tc *testCase) {
7878
ctx := context.Background()
7979

@@ -219,26 +219,6 @@ var _ = ginkgo.Describe("jobset webhook defaulting", func() {
219219
},
220220
updateShouldFail: true,
221221
}),
222-
ginkgo.Entry("validate jobSet immutable for fields over than NodeSelector when jobSet is suspended", &testCase{
223-
makeJobSet: func(ns *corev1.Namespace) *testing.JobSetWrapper {
224-
return testing.MakeJobSet("js-hostnames-non-indexed", ns.Name).
225-
Suspend(true).
226-
EnableDNSHostnames(true).
227-
ReplicatedJob(testing.MakeReplicatedJob("rjob").
228-
Job(testing.MakeJobTemplate("job", ns.Name).
229-
PodSpec(testing.TestPodSpec).
230-
CompletionMode(batchv1.IndexedCompletion).Obj()).
231-
Obj())
232-
},
233-
defaultsApplied: func(js *jobset.JobSet) bool {
234-
return true
235-
},
236-
updateJobSet: func(js *jobset.JobSet) {
237-
js.Spec.ReplicatedJobs[0].Template.Spec.Template.Spec.Hostname = "test"
238-
js.Spec.ReplicatedJobs[0].Template.Spec.Template.Spec.Subdomain = "test"
239-
},
240-
updateShouldFail: true,
241-
}),
242222
ginkgo.Entry("success policy defaults to all", &testCase{
243223
makeJobSet: func(ns *corev1.Namespace) *testing.JobSetWrapper {
244224
return testing.MakeJobSet("success-policy", ns.Name).
@@ -354,5 +334,49 @@ var _ = ginkgo.Describe("jobset webhook defaulting", func() {
354334
},
355335
updateShouldFail: true,
356336
}),
337+
ginkgo.Entry("updating pod template in suspended jobset is allowed", &testCase{
338+
makeJobSet: func(ns *corev1.Namespace) *testing.JobSetWrapper {
339+
return testing.MakeJobSet("suspended", ns.Name).
340+
Suspend(true).
341+
ReplicatedJob(testing.MakeReplicatedJob("rjob").
342+
Job(testing.MakeJobTemplate("job", ns.Name).
343+
CompletionMode(batchv1.IndexedCompletion).
344+
PodTemplateSpec(corev1.PodTemplateSpec{
345+
ObjectMeta: metav1.ObjectMeta{
346+
Annotations: map[string]string{"old": "annotation"},
347+
},
348+
Spec: testing.TestPodSpec,
349+
}).Obj()).
350+
Obj())
351+
},
352+
defaultsApplied: func(js *jobset.JobSet) bool {
353+
return true
354+
},
355+
updateJobSet: func(js *jobset.JobSet) {
356+
js.Spec.ReplicatedJobs[0].Template.Spec.Template.Annotations["new"] = "annotation"
357+
},
358+
}),
359+
ginkgo.Entry("updating pod template in running jobset is not allowed", &testCase{
360+
makeJobSet: func(ns *corev1.Namespace) *testing.JobSetWrapper {
361+
return testing.MakeJobSet("suspended", ns.Name).
362+
ReplicatedJob(testing.MakeReplicatedJob("rjob").
363+
Job(testing.MakeJobTemplate("job", ns.Name).
364+
CompletionMode(batchv1.IndexedCompletion).
365+
PodTemplateSpec(corev1.PodTemplateSpec{
366+
ObjectMeta: metav1.ObjectMeta{
367+
Annotations: map[string]string{"old": "annotation"},
368+
},
369+
Spec: testing.TestPodSpec,
370+
}).Obj()).
371+
Obj())
372+
},
373+
defaultsApplied: func(js *jobset.JobSet) bool {
374+
return true
375+
},
376+
updateJobSet: func(js *jobset.JobSet) {
377+
js.Spec.ReplicatedJobs[0].Template.Spec.Template.Annotations["new"] = "annotation"
378+
},
379+
updateShouldFail: true,
380+
}),
357381
) // end of DescribeTable
358382
}) // end of Describe

0 commit comments

Comments
 (0)