Containers:    []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, } <del> oldParallelism := int32(10) newParallelism := int32(11) oldJob := &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "10", }, Spec: batch.JobSpec{ Selector:    validSelector, Template:    validPodTemplateSpec, Parallelism: &oldParallelism, }, Status: batch.JobStatus{ Active: 11, }, } newJob := &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "9", }, Spec: batch.JobSpec{ Selector:    validSelector, Template:    validPodTemplateSpec, Parallelism: &newParallelism, }, Status: batch.JobStatus{ Active: 12, }, } </del> <ins> cases := map[string]struct { oldJob            *batch.Job job               *batch.Job wantWarningsCount int32 }{ "generation 0 for both": { job: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "0", Generation:      0, }, Spec: batch.JobSpec{ Selector:       validSelector, Template:       validPodTemplateSpec, ManualSelector: pointer.BoolPtr(true), Parallelism:    pointer.Int32Ptr(1), }, }, </ins> <del> StatusStrategy.PrepareForUpdate(ctx, newJob, oldJob) if newJob.Status.Active != 12 { t.Errorf("Job status updates must allow changes to job status") } if *newJob.Spec.Parallelism != 10 { t.Errorf("Job status updates must now allow changes to job spec") </del> <ins> oldJob: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "0", Generation:      0, }, Spec: batch.JobSpec{ Selector:       validSelector, Template:       validPodTemplateSpec, ManualSelector: pointer.BoolPtr(true), Parallelism:    pointer.Int32Ptr(1), }, }, }, "generation 1 for new; force WarningsOnUpdate to check PodTemplate for updates": { job: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "0", Generation:      1, }, Spec: batch.JobSpec{ Selector:       validSelector, Template:       validPodTemplateSpec, ManualSelector: pointer.BoolPtr(true), Parallelism:    pointer.Int32Ptr(1), }, }, oldJob: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "0", Generation:      0, }, Spec: batch.JobSpec{ Selector:       validSelector, Template:       validPodTemplateSpec, ManualSelector: pointer.BoolPtr(true), Parallelism:    pointer.Int32Ptr(1), }, }, }, "force validation failure in pod template": { job: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "0", Generation:      1, }, Spec: batch.JobSpec{ Selector: validSelector, Template: api.PodTemplateSpec{ Spec: api.PodSpec{Volumes: []api.Volume{{Name: "volume-name"}, {Name: "volume-name"}}}, }, ManualSelector: pointer.BoolPtr(true), Parallelism:    pointer.Int32Ptr(1), }, }, oldJob: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "0", Generation:      0, }, Spec: batch.JobSpec{ Selector:       validSelector, Template:       validPodTemplateSpec, ManualSelector: pointer.BoolPtr(true), Parallelism:    pointer.Int32Ptr(1), }, }, wantWarningsCount: 1, }, } for val, tc := range cases { t.Run(val, func(t *testing.T) { gotWarnings := Strategy.WarningsOnUpdate(ctx, tc.job, tc.oldJob) if len(gotWarnings) != int(tc.wantWarningsCount) { t.Errorf("got warning length of %d but expected %d", len(gotWarnings), tc.wantWarningsCount) } }) } } func TestJobStrategy_WarningsOnCreate(t *testing.T) { ctx := genericapirequest.NewDefaultContext() theUID := types.UID("1a2b3c4d5e6f7g8h9i0k") validSelector := &metav1.LabelSelector{ MatchLabels: map[string]string{"a": "b"}, } validSpec := batch.JobSpec{ Selector: nil, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector.MatchLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, DNSPolicy:     api.DNSClusterFirst, Containers:    []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, }, } testcases := map[string]struct { job               *batch.Job wantWarningsCount int32 }{ "happy path job": { job: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:      "myjob2", Namespace: metav1.NamespaceDefault, UID:       theUID, }, Spec: validSpec, }, }, "dns invalid name": { wantWarningsCount: 1, job: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:      "my job2", Namespace: metav1.NamespaceDefault, UID:       theUID, }, Spec: validSpec, }, }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { gotWarnings := Strategy.WarningsOnCreate(ctx, tc.job) if len(gotWarnings) != int(tc.wantWarningsCount) { t.Errorf("got warning length of %d but expected %d", len(gotWarnings), tc.wantWarningsCount) } }) } } func TestJobStrategy_Validate(t *testing.T) { ctx := genericapirequest.NewDefaultContext() theUID := types.UID("1a2b3c4d5e6f7g8h9i0k") validSelector := &metav1.LabelSelector{ MatchLabels: map[string]string{"a": "b"}, } validPodSpec := api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, DNSPolicy:     api.DNSClusterFirst, Containers:    []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, } validObjectMeta := metav1.ObjectMeta{ Name:      "myjob2", Namespace: metav1.NamespaceDefault, UID:       theUID, } testcases := map[string]struct { job              *batch.Job wantJob          *batch.Job wantWarningCount int32 }{ "valid job with labels in pod template": { job: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: nil, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector.MatchLabels, }, Spec: validPodSpec, }}, }, wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"controller-uid": string(theUID)}}, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector.MatchLabels, }, Spec: validPodSpec, }}, }, }, "no labels in job": { job: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: nil, Template: api.PodTemplateSpec{ Spec: validPodSpec, }}, }, wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"controller-uid": string(theUID)}}, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"job-name": "myjob2", "controller-uid": string(theUID)}, }, Spec: validPodSpec, }}, }, }, "labels exist": { job: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: nil, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b", "job-name": "myjob2", "controller-uid": string(theUID)}, }, Spec: validPodSpec, }}, }, wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"controller-uid": string(theUID)}}, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b", "job-name": "myjob2", "controller-uid": string(theUID)}, }, Spec: validPodSpec, }}, }, }, "manual selector; do not generate labels": { job: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: validSelector, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector.MatchLabels, }, Spec: validPodSpec, }, Completions:    pointer.Int32Ptr(2), ManualSelector: pointer.BoolPtr(true), }, }, wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: validSelector, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector.MatchLabels, }, Spec: validPodSpec, }, Completions:    pointer.Int32Ptr(2), ManualSelector: pointer.BoolPtr(true), }, }, }, "valid job with extended configuration": { job: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: nil, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector.MatchLabels, }, Spec: validPodSpec, }, Completions:             pointer.Int32Ptr(2), Suspend:                 pointer.BoolPtr(true), TTLSecondsAfterFinished: pointer.Int32Ptr(0), CompletionMode:          completionModePtr(batch.IndexedCompletion), }, }, wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"controller-uid": string(theUID)}}, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b", "job-name": "myjob2", "controller-uid": string(theUID)}, }, Spec: validPodSpec, }, Completions:             pointer.Int32Ptr(2), Suspend:                 pointer.BoolPtr(true), TTLSecondsAfterFinished: pointer.Int32Ptr(0), CompletionMode:          completionModePtr(batch.IndexedCompletion), }, }, }, "fail validation due to invalid volume spec": { job: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: nil, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector.MatchLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, DNSPolicy:     api.DNSClusterFirst, Containers:    []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, Volumes:       []api.Volume{{Name: "volume-name"}}, }, }, }, }, wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"controller-uid": string(theUID)}}, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b", "job-name": "myjob2", "controller-uid": string(theUID)}, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, DNSPolicy:     api.DNSClusterFirst, Containers:    []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, Volumes:       []api.Volume{{Name: "volume-name"}}, }, }, }, }, wantWarningCount: 1, }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { errs := Strategy.Validate(ctx, tc.job) if len(errs) != int(tc.wantWarningCount) { t.Errorf("want warnings %d but got %d", tc.wantWarningCount, len(errs)) } if diff := cmp.Diff(tc.wantJob, tc.job); diff != "" { t.Errorf("Unexpected job (-want,+got):n%s", diff) } }) } } func TestStrategy_ResetFields(t *testing.T) { resetFields := Strategy.GetResetFields() if len(resetFields) != 1 { t.Error("ResetFields should have 1 element") } } func TestJobStatusStrategy_ResetFields(t *testing.T) { resetFields := StatusStrategy.GetResetFields() if len(resetFields) != 1 { t.Error("ResetFields should have 1 element") } } func TestStatusStrategy_PrepareForUpdate(t *testing.T) { ctx := genericapirequest.NewDefaultContext() validSelector := &metav1.LabelSelector{ MatchLabels: map[string]string{"a": "b"}, } validPodTemplateSpec := api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector.MatchLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, DNSPolicy:     api.DNSClusterFirst, Containers:    []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, } validObjectMeta := metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "10", } cases := map[string]struct { job     *batch.Job newJob  *batch.Job wantJob *batch.Job }{ "job must allow status updates": { job: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector:    validSelector, Template:    validPodTemplateSpec, Parallelism: pointer.Int32(4), }, Status: batch.JobStatus{ Active: 11, }, }, newJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector:    validSelector, Template:    validPodTemplateSpec, Parallelism: pointer.Int32(4), }, Status: batch.JobStatus{ Active: 12, }, }, wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector:    validSelector, Template:    validPodTemplateSpec, Parallelism: pointer.Int32(4), }, Status: batch.JobStatus{ Active: 12, }, }, }, "parallelism changes not allowed": { job: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector:    validSelector, Template:    validPodTemplateSpec, Parallelism: pointer.Int32(3), }, }, newJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector:    validSelector, Template:    validPodTemplateSpec, Parallelism: pointer.Int32(4), }, }, wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ Selector:    validSelector, Template:    validPodTemplateSpec, Parallelism: pointer.Int32(3), }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { StatusStrategy.PrepareForUpdate(ctx, tc.newJob, tc.job) if diff := cmp.Diff(tc.wantJob, tc.newJob); diff != "" { t.Errorf("Unexpected job (-want,+got):n%s", diff) } }) } } func TestStatusStrategy_ValidateUpdate(t *testing.T) { ctx := genericapirequest.NewDefaultContext() validSelector := &metav1.LabelSelector{ MatchLabels: map[string]string{"a": "b"}, } validPodTemplateSpec := api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector.MatchLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, DNSPolicy:     api.DNSClusterFirst, Containers:    []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, } cases := map[string]struct { job     *batch.Job newJob  *batch.Job wantJob *batch.Job }{ "incoming resource version on update should not be mutated": { job: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "10", }, Spec: batch.JobSpec{ Selector:    validSelector, Template:    validPodTemplateSpec, Parallelism: pointer.Int32(4), }, }, newJob: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "9", }, Spec: batch.JobSpec{ Selector:    validSelector, Template:    validPodTemplateSpec, Parallelism: pointer.Int32(4), }, }, wantJob: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "9", }, Spec: batch.JobSpec{ Selector:    validSelector, Template:    validPodTemplateSpec, Parallelism: pointer.Int32(4), }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { errs := StatusStrategy.ValidateUpdate(ctx, tc.newJob, tc.job) if len(errs) != 0 { t.Errorf("Unexpected error %v", errs) } if diff := cmp.Diff(tc.wantJob, tc.newJob); diff != "" { t.Errorf("Unexpected job (-want,+got):n%s", diff) } }) } } func TestJobStrategy_GetAttrs(t *testing.T) { validSelector := &metav1.LabelSelector{ MatchLabels: map[string]string{"a": "b"}, } validPodTemplateSpec := api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector.MatchLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, DNSPolicy:     api.DNSClusterFirst, Containers:    []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, </ins> } <del> errs := StatusStrategy.ValidateUpdate(ctx, newJob, oldJob) if len(errs) != 0 { t.Errorf("Unexpected error %v", errs) </del> <ins> cases := map[string]struct { job          *batch.Job wantErr      string nonJobObject *api.Pod }{ "valid job with no labels": { job: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "0", }, Spec: batch.JobSpec{ Selector:       validSelector, Template:       validPodTemplateSpec, ManualSelector: pointer.BoolPtr(true), Parallelism:    pointer.Int32Ptr(1), }, }, }, "valid job with a label": { job: &batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name:            "myjob", Namespace:       metav1.NamespaceDefault, ResourceVersion: "0", Labels:          map[string]string{"a": "b"}, }, Spec: batch.JobSpec{ Selector:       validSelector, Template:       validPodTemplateSpec, ManualSelector: pointer.BoolPtr(true), Parallelism:    pointer.Int32Ptr(1), }, }, }, "pod instead": { job:          nil, nonJobObject: &api.Pod{}, wantErr:      "given object is not a job.", }, </ins> } <del> if newJob.ResourceVersion != "9" { t.Errorf("Incoming resource version on update should not be mutated") </del> <ins> for name, tc := range cases { t.Run(name, func(t *testing.T) { if tc.job == nil { _, _, err := GetAttrs(tc.nonJobObject) if diff := cmp.Diff(tc.wantErr, err.Error()); diff != "" { t.Errorf("Unexpected errors (-want,+got):n%s", diff) } } else { gotLabels, _, err := GetAttrs(tc.job) if err != nil { t.Errorf("Error %s supposed to be nil", err.Error()) } if diff := cmp.Diff(labels.Set(tc.job.ObjectMeta.Labels), gotLabels); diff != "" { t.Errorf("Unexpected attrs (-want,+got):n%s", diff) } } }) </ins> } } <del> func TestSelectableFieldLabelConversions(t *testing.T) { </del> <ins> func TestJobToSelectiableFields(t *testing.T) { </ins> apitesting.TestSelectableFieldLabelConversionsOfKind(t, "batch/v1", "Job",