1
16
17 package cronjob
18
19 import (
20 "testing"
21
22 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23 genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
24 "k8s.io/apiserver/pkg/registry/rest"
25 "k8s.io/kubernetes/pkg/apis/batch"
26 api "k8s.io/kubernetes/pkg/apis/core"
27 "k8s.io/utils/pointer"
28 )
29
30 var (
31 validPodTemplateSpec = api.PodTemplateSpec{
32 Spec: api.PodSpec{
33 RestartPolicy: api.RestartPolicyOnFailure,
34 DNSPolicy: api.DNSClusterFirst,
35 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
36 },
37 }
38 validCronjobSpec = batch.CronJobSpec{
39 Schedule: "5 5 * * ?",
40 ConcurrencyPolicy: batch.AllowConcurrent,
41 TimeZone: pointer.String("Asia/Shanghai"),
42 JobTemplate: batch.JobTemplateSpec{
43 Spec: batch.JobSpec{
44 Template: validPodTemplateSpec,
45 CompletionMode: completionModePtr(batch.IndexedCompletion),
46 Completions: pointer.Int32(10),
47 Parallelism: pointer.Int32(10),
48 },
49 },
50 }
51 cronjobSpecWithTZinSchedule = batch.CronJobSpec{
52 Schedule: "CRON_TZ=UTC 5 5 * * ?",
53 ConcurrencyPolicy: batch.AllowConcurrent,
54 TimeZone: pointer.String("Asia/DoesNotExist"),
55 JobTemplate: batch.JobTemplateSpec{
56 Spec: batch.JobSpec{
57 Template: validPodTemplateSpec,
58 },
59 },
60 }
61 )
62
63 func completionModePtr(m batch.CompletionMode) *batch.CompletionMode {
64 return &m
65 }
66
67 func TestCronJobStrategy(t *testing.T) {
68 ctx := genericapirequest.NewDefaultContext()
69 if !Strategy.NamespaceScoped() {
70 t.Errorf("CronJob must be namespace scoped")
71 }
72 if Strategy.AllowCreateOnUpdate() {
73 t.Errorf("CronJob should not allow create on update")
74 }
75
76 cronJob := &batch.CronJob{
77 ObjectMeta: metav1.ObjectMeta{
78 Name: "mycronjob",
79 Namespace: metav1.NamespaceDefault,
80 Generation: 999,
81 },
82 Spec: batch.CronJobSpec{
83 Schedule: "* * * * ?",
84 ConcurrencyPolicy: batch.AllowConcurrent,
85 JobTemplate: batch.JobTemplateSpec{
86 Spec: batch.JobSpec{
87 Template: validPodTemplateSpec,
88 },
89 },
90 },
91 }
92
93 Strategy.PrepareForCreate(ctx, cronJob)
94 if len(cronJob.Status.Active) != 0 {
95 t.Errorf("CronJob does not allow setting status on create")
96 }
97 if cronJob.Generation != 1 {
98 t.Errorf("expected Generation=1, got %d", cronJob.Generation)
99 }
100 errs := Strategy.Validate(ctx, cronJob)
101 if len(errs) != 0 {
102 t.Errorf("Unexpected error validating %v", errs)
103 }
104 now := metav1.Now()
105
106
107 updatedLabelCronJob := cronJob.DeepCopy()
108 updatedLabelCronJob.Labels = map[string]string{"a": "true"}
109 Strategy.PrepareForUpdate(ctx, updatedLabelCronJob, cronJob)
110 if updatedLabelCronJob.Generation != 1 {
111 t.Errorf("expected Generation=1, got %d", updatedLabelCronJob.Generation)
112 }
113
114 updatedCronJob := &batch.CronJob{
115 ObjectMeta: metav1.ObjectMeta{Name: "bar", ResourceVersion: "4"},
116 Spec: batch.CronJobSpec{
117 Schedule: "5 5 5 * ?",
118 },
119 Status: batch.CronJobStatus{
120 LastScheduleTime: &now,
121 },
122 }
123
124
125 Strategy.PrepareForUpdate(ctx, updatedCronJob, cronJob)
126 if updatedCronJob.Status.Active != nil {
127 t.Errorf("PrepareForUpdate should have preserved prior version status")
128 }
129 if updatedCronJob.Generation != 2 {
130 t.Errorf("expected Generation=2, got %d", updatedCronJob.Generation)
131 }
132 errs = Strategy.ValidateUpdate(ctx, updatedCronJob, cronJob)
133 if len(errs) == 0 {
134 t.Errorf("Expected a validation error")
135 }
136
137
138
139 var gcds rest.GarbageCollectionDeleteStrategy = Strategy
140 if got, want := gcds.DefaultGarbageCollectionPolicy(genericapirequest.NewContext()), rest.DeleteDependents; got != want {
141 t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want)
142 }
143
144 var (
145 v1beta1Ctx = genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{APIGroup: "batch", APIVersion: "v1beta1", Resource: "cronjobs"})
146 otherVersionCtx = genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{APIGroup: "batch", APIVersion: "v100", Resource: "cronjobs"})
147 )
148 if got, want := gcds.DefaultGarbageCollectionPolicy(v1beta1Ctx), rest.OrphanDependents; got != want {
149 t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want)
150 }
151 if got, want := gcds.DefaultGarbageCollectionPolicy(otherVersionCtx), rest.DeleteDependents; got != want {
152 t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want)
153 }
154 }
155
156 func TestCronJobStatusStrategy(t *testing.T) {
157 ctx := genericapirequest.NewDefaultContext()
158 if !StatusStrategy.NamespaceScoped() {
159 t.Errorf("CronJob must be namespace scoped")
160 }
161 if StatusStrategy.AllowCreateOnUpdate() {
162 t.Errorf("CronJob should not allow create on update")
163 }
164 validPodTemplateSpec := api.PodTemplateSpec{
165 Spec: api.PodSpec{
166 RestartPolicy: api.RestartPolicyOnFailure,
167 DNSPolicy: api.DNSClusterFirst,
168 Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
169 },
170 }
171 oldSchedule := "* * * * ?"
172 oldCronJob := &batch.CronJob{
173 ObjectMeta: metav1.ObjectMeta{
174 Name: "mycronjob",
175 Namespace: metav1.NamespaceDefault,
176 ResourceVersion: "10",
177 },
178 Spec: batch.CronJobSpec{
179 Schedule: oldSchedule,
180 ConcurrencyPolicy: batch.AllowConcurrent,
181 JobTemplate: batch.JobTemplateSpec{
182 Spec: batch.JobSpec{
183 Template: validPodTemplateSpec,
184 },
185 },
186 },
187 }
188 now := metav1.Now()
189 newCronJob := &batch.CronJob{
190 ObjectMeta: metav1.ObjectMeta{
191 Name: "mycronjob",
192 Namespace: metav1.NamespaceDefault,
193 ResourceVersion: "9",
194 },
195 Spec: batch.CronJobSpec{
196 Schedule: "5 5 * * ?",
197 ConcurrencyPolicy: batch.AllowConcurrent,
198 JobTemplate: batch.JobTemplateSpec{
199 Spec: batch.JobSpec{
200 Template: validPodTemplateSpec,
201 },
202 },
203 },
204 Status: batch.CronJobStatus{
205 LastScheduleTime: &now,
206 },
207 }
208
209 StatusStrategy.PrepareForUpdate(ctx, newCronJob, oldCronJob)
210 if newCronJob.Status.LastScheduleTime == nil {
211 t.Errorf("CronJob status updates must allow changes to cronJob status")
212 }
213 if newCronJob.Spec.Schedule != oldSchedule {
214 t.Errorf("CronJob status updates must now allow changes to cronJob spec")
215 }
216 errs := StatusStrategy.ValidateUpdate(ctx, newCronJob, oldCronJob)
217 if len(errs) != 0 {
218 t.Errorf("Unexpected error %v", errs)
219 }
220 if newCronJob.ResourceVersion != "9" {
221 t.Errorf("Incoming resource version on update should not be mutated")
222 }
223 }
224
225 func TestStrategy_ResetFields(t *testing.T) {
226 resetFields := Strategy.GetResetFields()
227 if len(resetFields) != 2 {
228 t.Errorf("ResetFields should have 2 elements, but have %d", len(resetFields))
229 }
230 }
231
232 func TestCronJobStatusStrategy_ResetFields(t *testing.T) {
233 resetFields := StatusStrategy.GetResetFields()
234 if len(resetFields) != 2 {
235 t.Errorf("ResetFields should have 2 elements, but have %d", len(resetFields))
236 }
237 }
238
239 func TestCronJobStrategy_WarningsOnCreate(t *testing.T) {
240 ctx := genericapirequest.NewDefaultContext()
241
242 now := metav1.Now()
243
244 testcases := map[string]struct {
245 cronjob *batch.CronJob
246 wantWarningsCount int32
247 }{
248 "happy path cronjob": {
249 wantWarningsCount: 0,
250 cronjob: &batch.CronJob{
251 ObjectMeta: metav1.ObjectMeta{
252 Name: "mycronjob",
253 Namespace: metav1.NamespaceDefault,
254 ResourceVersion: "9",
255 },
256 Spec: validCronjobSpec,
257 Status: batch.CronJobStatus{
258 LastScheduleTime: &now,
259 },
260 },
261 },
262 "dns invalid name": {
263 wantWarningsCount: 1,
264 cronjob: &batch.CronJob{
265 ObjectMeta: metav1.ObjectMeta{
266 Name: "my cronjob",
267 Namespace: metav1.NamespaceDefault,
268 ResourceVersion: "9",
269 },
270 Spec: validCronjobSpec,
271 Status: batch.CronJobStatus{
272 LastScheduleTime: &now,
273 },
274 },
275 },
276 }
277 for name, tc := range testcases {
278 t.Run(name, func(t *testing.T) {
279 gotWarnings := Strategy.WarningsOnCreate(ctx, tc.cronjob)
280 if len(gotWarnings) != int(tc.wantWarningsCount) {
281 t.Errorf("%s: got warning length of %d but expected %d", name, len(gotWarnings), tc.wantWarningsCount)
282 }
283 })
284 }
285 }
286
287 func TestCronJobStrategy_WarningsOnUpdate(t *testing.T) {
288 ctx := genericapirequest.NewDefaultContext()
289 now := metav1.Now()
290
291 cases := map[string]struct {
292 oldCronJob *batch.CronJob
293 cronjob *batch.CronJob
294 wantWarningsCount int32
295 }{
296 "generation 0 for both": {
297 wantWarningsCount: 0,
298 oldCronJob: &batch.CronJob{
299 ObjectMeta: metav1.ObjectMeta{
300 Name: "mycronjob",
301 Namespace: metav1.NamespaceDefault,
302 ResourceVersion: "9",
303 Generation: 0,
304 },
305 Spec: validCronjobSpec,
306 Status: batch.CronJobStatus{
307 LastScheduleTime: &now,
308 },
309 },
310 cronjob: &batch.CronJob{
311 ObjectMeta: metav1.ObjectMeta{
312 Name: "mycronjob",
313 Namespace: metav1.NamespaceDefault,
314 ResourceVersion: "9",
315 Generation: 0,
316 },
317 Spec: validCronjobSpec,
318 Status: batch.CronJobStatus{
319 LastScheduleTime: &now,
320 },
321 },
322 },
323 "generation 1 for new; force WarningsOnUpdate to check PodTemplate for updates": {
324 wantWarningsCount: 0,
325 oldCronJob: &batch.CronJob{
326 ObjectMeta: metav1.ObjectMeta{
327 Name: "mycronjob",
328 Namespace: metav1.NamespaceDefault,
329 ResourceVersion: "9",
330 Generation: 1,
331 },
332 Spec: validCronjobSpec,
333 Status: batch.CronJobStatus{
334 LastScheduleTime: &now,
335 },
336 },
337 cronjob: &batch.CronJob{
338 ObjectMeta: metav1.ObjectMeta{
339 Name: "mycronjob",
340 Namespace: metav1.NamespaceDefault,
341 ResourceVersion: "9",
342 Generation: 0,
343 },
344 Spec: validCronjobSpec,
345 Status: batch.CronJobStatus{
346 LastScheduleTime: &now,
347 },
348 },
349 },
350 "force validation failure in pod template": {
351 oldCronJob: &batch.CronJob{
352 ObjectMeta: metav1.ObjectMeta{
353 Name: "mycronjob",
354 Namespace: metav1.NamespaceDefault,
355 ResourceVersion: "0",
356 Generation: 1,
357 },
358 Spec: validCronjobSpec,
359 Status: batch.CronJobStatus{
360 LastScheduleTime: &now,
361 },
362 },
363 cronjob: &batch.CronJob{
364 ObjectMeta: metav1.ObjectMeta{
365 Name: "mycronjob",
366 Namespace: metav1.NamespaceDefault,
367 ResourceVersion: "0",
368 Generation: 0,
369 },
370 Spec: batch.CronJobSpec{
371 Schedule: "5 5 * * ?",
372 ConcurrencyPolicy: batch.AllowConcurrent,
373 JobTemplate: batch.JobTemplateSpec{
374 Spec: batch.JobSpec{
375 Template: api.PodTemplateSpec{
376 Spec: api.PodSpec{ImagePullSecrets: []api.LocalObjectReference{{Name: ""}}},
377 },
378 },
379 },
380 },
381 Status: batch.CronJobStatus{
382 LastScheduleTime: &now,
383 },
384 },
385 wantWarningsCount: 1,
386 },
387 "timezone invalid failure": {
388 oldCronJob: &batch.CronJob{
389 ObjectMeta: metav1.ObjectMeta{
390 Name: "mycronjob",
391 Namespace: metav1.NamespaceDefault,
392 ResourceVersion: "0",
393 Generation: 1,
394 },
395 Spec: validCronjobSpec,
396 Status: batch.CronJobStatus{
397 LastScheduleTime: &now,
398 },
399 },
400 cronjob: &batch.CronJob{
401 ObjectMeta: metav1.ObjectMeta{
402 Name: "mycronjob",
403 Namespace: metav1.NamespaceDefault,
404 ResourceVersion: "0",
405 Generation: 0,
406 },
407 Spec: cronjobSpecWithTZinSchedule,
408 Status: batch.CronJobStatus{
409 LastScheduleTime: &now,
410 },
411 },
412 wantWarningsCount: 1,
413 },
414 }
415 for val, tc := range cases {
416 t.Run(val, func(t *testing.T) {
417 gotWarnings := Strategy.WarningsOnUpdate(ctx, tc.cronjob, tc.oldCronJob)
418 if len(gotWarnings) != int(tc.wantWarningsCount) {
419 t.Errorf("%s: got warning length of %d but expected %d", val, len(gotWarnings), tc.wantWarningsCount)
420 }
421 })
422 }
423 }
424
View as plain text