1
16
17 package cronjob
18
19 import (
20 "context"
21 "fmt"
22 "reflect"
23 "sort"
24 "strings"
25 "testing"
26 "time"
27
28 batchv1 "k8s.io/api/batch/v1"
29 v1 "k8s.io/api/core/v1"
30 "k8s.io/apimachinery/pkg/api/errors"
31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32 "k8s.io/apimachinery/pkg/runtime"
33 "k8s.io/apimachinery/pkg/runtime/schema"
34 "k8s.io/apimachinery/pkg/types"
35 "k8s.io/client-go/informers"
36 "k8s.io/client-go/kubernetes/fake"
37 "k8s.io/client-go/tools/record"
38 "k8s.io/client-go/util/workqueue"
39 "k8s.io/klog/v2/ktesting"
40 "k8s.io/utils/pointer"
41
42 _ "k8s.io/kubernetes/pkg/apis/batch/install"
43 _ "k8s.io/kubernetes/pkg/apis/core/install"
44 "k8s.io/kubernetes/pkg/controller"
45 )
46
47 var (
48 shortDead int64 = 10
49 mediumDead int64 = 2 * 60 * 60
50 longDead int64 = 1000000
51 noDead int64 = -12345
52
53 errorSchedule = "obvious error schedule"
54
55 onTheHour = "0 * * * ?"
56 everyHour = "@every 1h"
57
58 errorTimeZone = "bad timezone"
59 newYork = "America/New_York"
60 )
61
62
63 func cronJob() batchv1.CronJob {
64 return batchv1.CronJob{
65 ObjectMeta: metav1.ObjectMeta{
66 Name: "mycronjob",
67 Namespace: "snazzycats",
68 UID: types.UID("1a2b3c"),
69 CreationTimestamp: metav1.Time{Time: justBeforeTheHour()},
70 },
71 Spec: batchv1.CronJobSpec{
72 Schedule: "* * * * ?",
73 ConcurrencyPolicy: "Allow",
74 JobTemplate: batchv1.JobTemplateSpec{
75 ObjectMeta: metav1.ObjectMeta{
76 Labels: map[string]string{"a": "b"},
77 Annotations: map[string]string{"x": "y"},
78 },
79 Spec: jobSpec(),
80 },
81 },
82 }
83 }
84
85 func jobSpec() batchv1.JobSpec {
86 one := int32(1)
87 return batchv1.JobSpec{
88 Parallelism: &one,
89 Completions: &one,
90 Template: v1.PodTemplateSpec{
91 ObjectMeta: metav1.ObjectMeta{
92 Labels: map[string]string{
93 "foo": "bar",
94 },
95 },
96 Spec: v1.PodSpec{
97 Containers: []v1.Container{
98 {Image: "foo/bar"},
99 },
100 },
101 },
102 }
103 }
104
105 func justASecondBeforeTheHour() time.Time {
106 T1, err := time.Parse(time.RFC3339, "2016-05-19T09:59:59Z")
107 if err != nil {
108 panic("test setup error")
109 }
110 return T1
111 }
112
113 func justAfterThePriorHour() time.Time {
114 T1, err := time.Parse(time.RFC3339, "2016-05-19T09:01:00Z")
115 if err != nil {
116 panic("test setup error")
117 }
118 return T1
119 }
120
121 func justBeforeThePriorHour() time.Time {
122 T1, err := time.Parse(time.RFC3339, "2016-05-19T08:59:00Z")
123 if err != nil {
124 panic("test setup error")
125 }
126 return T1
127 }
128
129 func justAfterTheHour() *time.Time {
130 T1, err := time.Parse(time.RFC3339, "2016-05-19T10:01:00Z")
131 if err != nil {
132 panic("test setup error")
133 }
134 return &T1
135 }
136
137 func justAfterTheHourInZone(tz string) time.Time {
138 location, err := time.LoadLocation(tz)
139 if err != nil {
140 panic("tz error: " + err.Error())
141 }
142
143 T1, err := time.ParseInLocation(time.RFC3339, "2016-05-19T10:01:00Z", location)
144 if err != nil {
145 panic("test setup error: " + err.Error())
146 }
147 return T1
148 }
149
150 func justBeforeTheHour() time.Time {
151 T1, err := time.Parse(time.RFC3339, "2016-05-19T09:59:00Z")
152 if err != nil {
153 panic("test setup error")
154 }
155 return T1
156 }
157
158 func justBeforeTheNextHour() time.Time {
159 T1, err := time.Parse(time.RFC3339, "2016-05-19T10:59:00Z")
160 if err != nil {
161 panic("test setup error")
162 }
163 return T1
164 }
165
166 func weekAfterTheHour() time.Time {
167 T1, err := time.Parse(time.RFC3339, "2016-05-26T10:00:00Z")
168 if err != nil {
169 panic("test setup error")
170 }
171 return T1
172 }
173
174 func TestControllerV2SyncCronJob(t *testing.T) {
175
176 if shortDead/60/60 >= 1 {
177 t.Errorf("shortDead should be less than one hour")
178 }
179
180 if mediumDead/60/60 < 1 || mediumDead/60/60 >= 24 {
181 t.Errorf("mediumDead should be between one hour and one day")
182 }
183
184 if longDead/60/60/24 < 10 {
185 t.Errorf("longDead should be at least ten days")
186 }
187
188 testCases := map[string]struct {
189
190 concurrencyPolicy batchv1.ConcurrencyPolicy
191 suspend bool
192 schedule string
193 timeZone *string
194 deadline int64
195
196
197 ranPreviously bool
198 stillActive bool
199
200
201 cronjobCreationTime time.Time
202 jobCreationTime time.Time
203 lastScheduleTime time.Time
204 now time.Time
205 jobCreateError error
206 jobGetErr error
207
208
209 expectCreate bool
210 expectDelete bool
211 expectActive int
212 expectedWarnings int
213 expectErr bool
214 expectRequeueAfter bool
215 expectedRequeueDuration time.Duration
216 expectUpdateStatus bool
217 jobStillNotFoundInLister bool
218 jobPresentInCJActiveStatus bool
219 }{
220 "never ran, not valid schedule, A": {
221 concurrencyPolicy: "Allow",
222 schedule: errorSchedule,
223 deadline: noDead,
224 jobCreationTime: justAfterThePriorHour(),
225 now: justBeforeTheHour(),
226 expectedWarnings: 1,
227 jobPresentInCJActiveStatus: true,
228 },
229 "never ran, not valid schedule, F": {
230 concurrencyPolicy: "Forbid",
231 schedule: errorSchedule,
232 deadline: noDead,
233 jobCreationTime: justAfterThePriorHour(),
234 now: justBeforeTheHour(),
235 expectedWarnings: 1,
236 jobPresentInCJActiveStatus: true,
237 },
238 "never ran, not valid schedule, R": {
239 concurrencyPolicy: "Forbid",
240 schedule: errorSchedule,
241 deadline: noDead,
242 jobCreationTime: justAfterThePriorHour(),
243 now: justBeforeTheHour(),
244 expectedWarnings: 1,
245 jobPresentInCJActiveStatus: true,
246 },
247 "never ran, not valid time zone": {
248 concurrencyPolicy: "Allow",
249 schedule: onTheHour,
250 timeZone: &errorTimeZone,
251 deadline: noDead,
252 jobCreationTime: justAfterThePriorHour(),
253 now: justBeforeTheHour(),
254 expectedWarnings: 1,
255 jobPresentInCJActiveStatus: true,
256 },
257 "never ran, not time, A": {
258 concurrencyPolicy: "Allow",
259 schedule: onTheHour,
260 deadline: noDead,
261 jobCreationTime: justAfterThePriorHour(),
262 now: justBeforeTheHour(),
263 expectRequeueAfter: true,
264 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
265 jobPresentInCJActiveStatus: true},
266 "never ran, not time, F": {
267 concurrencyPolicy: "Forbid",
268 schedule: onTheHour,
269 deadline: noDead,
270 jobCreationTime: justAfterThePriorHour(),
271 now: justBeforeTheHour(),
272 expectRequeueAfter: true,
273 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
274 jobPresentInCJActiveStatus: true,
275 },
276 "never ran, not time, R": {
277 concurrencyPolicy: "Replace",
278 schedule: onTheHour,
279 deadline: noDead,
280 jobCreationTime: justAfterThePriorHour(),
281 now: justBeforeTheHour(),
282 expectRequeueAfter: true,
283 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
284 jobPresentInCJActiveStatus: true,
285 },
286 "never ran, not time in zone": {
287 concurrencyPolicy: "Allow",
288 schedule: onTheHour,
289 timeZone: &newYork,
290 deadline: noDead,
291 jobCreationTime: justAfterThePriorHour(),
292 now: justBeforeTheHour(),
293 expectRequeueAfter: true,
294 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
295 jobPresentInCJActiveStatus: true,
296 },
297 "never ran, is time, A": {
298 concurrencyPolicy: "Allow",
299 schedule: onTheHour,
300 deadline: noDead,
301 jobCreationTime: justAfterThePriorHour(),
302 now: *justAfterTheHour(),
303 expectCreate: true,
304 expectActive: 1,
305 expectRequeueAfter: true,
306 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
307 expectUpdateStatus: true,
308 jobPresentInCJActiveStatus: true,
309 },
310 "never ran, is time, F": {
311 concurrencyPolicy: "Forbid",
312 schedule: onTheHour,
313 deadline: noDead,
314 jobCreationTime: justAfterThePriorHour(),
315 now: *justAfterTheHour(),
316 expectCreate: true,
317 expectActive: 1,
318 expectRequeueAfter: true,
319 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
320 expectUpdateStatus: true,
321 jobPresentInCJActiveStatus: true,
322 },
323 "never ran, is time, R": {
324 concurrencyPolicy: "Replace",
325 schedule: onTheHour,
326 deadline: noDead,
327 jobCreationTime: justAfterThePriorHour(),
328 now: *justAfterTheHour(),
329 expectCreate: true,
330 expectActive: 1,
331 expectRequeueAfter: true,
332 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
333 expectUpdateStatus: true,
334 jobPresentInCJActiveStatus: true,
335 },
336 "never ran, is time in zone, but time zone disabled": {
337 concurrencyPolicy: "Allow",
338 schedule: onTheHour,
339 timeZone: &newYork,
340 deadline: noDead,
341 jobCreationTime: justAfterThePriorHour(),
342 now: justAfterTheHourInZone(newYork),
343 expectCreate: true,
344 expectActive: 1,
345 expectRequeueAfter: true,
346 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
347 expectUpdateStatus: true,
348 jobPresentInCJActiveStatus: true,
349 },
350 "never ran, is time in zone": {
351 concurrencyPolicy: "Allow",
352 schedule: onTheHour,
353 timeZone: &newYork,
354 deadline: noDead,
355 jobCreationTime: justAfterThePriorHour(),
356 now: justAfterTheHourInZone(newYork),
357 expectCreate: true,
358 expectActive: 1,
359 expectRequeueAfter: true,
360 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
361 expectUpdateStatus: true,
362 jobPresentInCJActiveStatus: true,
363 },
364 "never ran, is time in zone, but TZ is also set in schedule": {
365 concurrencyPolicy: "Allow",
366 schedule: "TZ=UTC " + onTheHour,
367 timeZone: &newYork,
368 deadline: noDead,
369 jobCreationTime: justAfterThePriorHour(),
370 now: justAfterTheHourInZone(newYork),
371 expectCreate: true,
372 expectedWarnings: 1,
373 expectRequeueAfter: true,
374 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
375 expectUpdateStatus: true,
376 jobPresentInCJActiveStatus: true,
377 },
378 "never ran, is time, suspended": {
379 concurrencyPolicy: "Allow",
380 suspend: true,
381 schedule: onTheHour,
382 deadline: noDead,
383 jobCreationTime: justAfterThePriorHour(),
384 now: *justAfterTheHour(),
385 jobPresentInCJActiveStatus: true,
386 },
387 "never ran, is time, past deadline": {
388 concurrencyPolicy: "Allow",
389 schedule: onTheHour,
390 deadline: shortDead,
391 jobCreationTime: justAfterThePriorHour(),
392 now: justAfterTheHour().Add(time.Minute * time.Duration(shortDead+1)),
393 expectRequeueAfter: true,
394 expectedRequeueDuration: 1*time.Hour - 1*time.Minute - time.Minute*time.Duration(shortDead+1) + nextScheduleDelta,
395 jobPresentInCJActiveStatus: true,
396 },
397 "never ran, is time, not past deadline": {
398 concurrencyPolicy: "Allow",
399 schedule: onTheHour,
400 deadline: longDead,
401 jobCreationTime: justAfterThePriorHour(),
402 now: *justAfterTheHour(),
403 expectCreate: true,
404 expectActive: 1,
405 expectRequeueAfter: true,
406 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
407 expectUpdateStatus: true,
408 jobPresentInCJActiveStatus: true,
409 },
410
411 "prev ran but done, not time, A": {
412 concurrencyPolicy: "Allow",
413 schedule: onTheHour,
414 deadline: noDead,
415 ranPreviously: true,
416 jobCreationTime: justAfterThePriorHour(),
417 now: justBeforeTheHour(),
418 expectRequeueAfter: true,
419 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
420 expectUpdateStatus: true,
421 jobPresentInCJActiveStatus: true,
422 },
423 "prev ran but done, not time, F": {
424 concurrencyPolicy: "Forbid",
425 schedule: onTheHour,
426 deadline: noDead,
427 ranPreviously: true,
428 jobCreationTime: justAfterThePriorHour(),
429 now: justBeforeTheHour(),
430 expectRequeueAfter: true,
431 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
432 expectUpdateStatus: true,
433 jobPresentInCJActiveStatus: true,
434 },
435 "prev ran but done, not time, R": {
436 concurrencyPolicy: "Replace",
437 schedule: onTheHour,
438 deadline: noDead,
439 ranPreviously: true,
440 jobCreationTime: justAfterThePriorHour(),
441 now: justBeforeTheHour(),
442 expectRequeueAfter: true,
443 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
444 expectUpdateStatus: true,
445 jobPresentInCJActiveStatus: true,
446 },
447 "prev ran but done, is time, A": {
448 concurrencyPolicy: "Allow",
449 schedule: onTheHour,
450 deadline: noDead,
451 ranPreviously: true,
452 jobCreationTime: justAfterThePriorHour(),
453 now: *justAfterTheHour(),
454 expectCreate: true,
455 expectActive: 1,
456 expectRequeueAfter: true,
457 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
458 expectUpdateStatus: true,
459 jobPresentInCJActiveStatus: true,
460 },
461 "prev ran but done, is time, create job failed, A": {
462 concurrencyPolicy: "Allow",
463 schedule: onTheHour,
464 deadline: noDead,
465 ranPreviously: true,
466 jobCreationTime: justAfterThePriorHour(),
467 now: *justAfterTheHour(),
468 jobCreateError: errors.NewAlreadyExists(schema.GroupResource{Resource: "job", Group: "batch"}, ""),
469 expectErr: false,
470 expectUpdateStatus: true,
471 jobPresentInCJActiveStatus: true,
472 },
473 "prev ran but done, is time, job not present in CJ active status, create job failed, A": {
474 concurrencyPolicy: "Allow",
475 schedule: onTheHour,
476 deadline: noDead,
477 ranPreviously: true,
478 jobCreationTime: justAfterThePriorHour(),
479 now: *justAfterTheHour(),
480 jobCreateError: errors.NewAlreadyExists(schema.GroupResource{Resource: "job", Group: "batch"}, ""),
481 expectErr: false,
482 expectUpdateStatus: true,
483 jobPresentInCJActiveStatus: false,
484 },
485 "prev ran but done, is time, F": {
486 concurrencyPolicy: "Forbid",
487 schedule: onTheHour,
488 deadline: noDead,
489 ranPreviously: true,
490 jobCreationTime: justAfterThePriorHour(),
491 now: *justAfterTheHour(),
492 expectCreate: true,
493 expectActive: 1,
494 expectRequeueAfter: true,
495 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
496 expectUpdateStatus: true,
497 jobPresentInCJActiveStatus: true,
498 },
499 "prev ran but done, is time, R": {
500 concurrencyPolicy: "Replace",
501 schedule: onTheHour,
502 deadline: noDead,
503 ranPreviously: true,
504 jobCreationTime: justAfterThePriorHour(),
505 now: *justAfterTheHour(),
506 expectCreate: true,
507 expectActive: 1,
508 expectRequeueAfter: true,
509 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
510 expectUpdateStatus: true,
511 jobPresentInCJActiveStatus: true,
512 },
513 "prev ran but done, is time, suspended": {
514 concurrencyPolicy: "Allow",
515 suspend: true,
516 schedule: onTheHour,
517 deadline: noDead,
518 ranPreviously: true,
519 jobCreationTime: justAfterThePriorHour(),
520 now: *justAfterTheHour(),
521 expectUpdateStatus: true,
522 jobPresentInCJActiveStatus: true,
523 },
524 "prev ran but done, is time, past deadline": {
525 concurrencyPolicy: "Allow",
526 schedule: onTheHour,
527 deadline: shortDead,
528 ranPreviously: true,
529 jobCreationTime: justAfterThePriorHour(),
530 now: *justAfterTheHour(),
531 expectRequeueAfter: true,
532 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
533 expectUpdateStatus: true,
534 jobPresentInCJActiveStatus: true,
535 },
536 "prev ran but done, is time, not past deadline": {
537 concurrencyPolicy: "Allow",
538 schedule: onTheHour,
539 deadline: longDead,
540 ranPreviously: true,
541 jobCreationTime: justAfterThePriorHour(),
542 now: *justAfterTheHour(),
543 expectCreate: true,
544 expectActive: 1,
545 expectRequeueAfter: true,
546 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
547 expectUpdateStatus: true,
548 jobPresentInCJActiveStatus: true,
549 },
550
551 "still active, not time, A": {
552 concurrencyPolicy: "Allow",
553 schedule: onTheHour,
554 deadline: noDead,
555 ranPreviously: true,
556 stillActive: true,
557 jobCreationTime: justAfterThePriorHour(),
558 now: justBeforeTheHour(),
559 expectActive: 1,
560 expectRequeueAfter: true,
561 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
562 jobPresentInCJActiveStatus: true,
563 },
564 "still active, not time, F": {
565 concurrencyPolicy: "Forbid",
566 schedule: onTheHour,
567 deadline: noDead,
568 ranPreviously: true,
569 stillActive: true,
570 jobCreationTime: justAfterThePriorHour(),
571 now: justBeforeTheHour(),
572 expectActive: 1,
573 expectRequeueAfter: true,
574 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
575 jobPresentInCJActiveStatus: true,
576 },
577 "still active, not time, R": {
578 concurrencyPolicy: "Replace",
579 schedule: onTheHour,
580 deadline: noDead,
581 ranPreviously: true,
582 stillActive: true,
583 jobCreationTime: justAfterThePriorHour(),
584 now: justBeforeTheHour(),
585 expectActive: 1,
586 expectRequeueAfter: true,
587 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
588 jobPresentInCJActiveStatus: true,
589 },
590 "still active, is time, A": {
591 concurrencyPolicy: "Allow",
592 schedule: onTheHour,
593 deadline: noDead,
594 ranPreviously: true,
595 stillActive: true,
596 jobCreationTime: justAfterThePriorHour(),
597 now: *justAfterTheHour(),
598 expectCreate: true,
599 expectActive: 2,
600 expectRequeueAfter: true,
601 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
602 expectUpdateStatus: true,
603 jobPresentInCJActiveStatus: true,
604 },
605 "still active, is time, F": {
606 concurrencyPolicy: "Forbid",
607 schedule: onTheHour,
608 deadline: noDead,
609 ranPreviously: true,
610 stillActive: true,
611 jobCreationTime: justAfterThePriorHour(),
612 now: *justAfterTheHour(),
613 expectActive: 1,
614 expectRequeueAfter: true,
615 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
616 jobPresentInCJActiveStatus: true,
617 },
618 "still active, is time, R": {
619 concurrencyPolicy: "Replace",
620 schedule: onTheHour,
621 deadline: noDead,
622 ranPreviously: true,
623 stillActive: true,
624 jobCreationTime: justAfterThePriorHour(),
625 now: *justAfterTheHour(),
626 expectCreate: true,
627 expectDelete: true,
628 expectActive: 1,
629 expectRequeueAfter: true,
630 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
631 expectUpdateStatus: true,
632 jobPresentInCJActiveStatus: true,
633 },
634 "still active, is time, get job failed, R": {
635 concurrencyPolicy: "Replace",
636 schedule: onTheHour,
637 deadline: noDead,
638 ranPreviously: true,
639 stillActive: true,
640 jobCreationTime: justAfterThePriorHour(),
641 now: *justAfterTheHour(),
642 jobGetErr: errors.NewBadRequest("request is invalid"),
643 expectActive: 1,
644 expectedWarnings: 1,
645 jobPresentInCJActiveStatus: true,
646 },
647 "still active, is time, suspended": {
648 concurrencyPolicy: "Allow",
649 suspend: true,
650 schedule: onTheHour,
651 deadline: noDead,
652 ranPreviously: true,
653 stillActive: true,
654 jobCreationTime: justAfterThePriorHour(),
655 now: *justAfterTheHour(),
656 expectActive: 1,
657 jobPresentInCJActiveStatus: true,
658 },
659 "still active, is time, past deadline": {
660 concurrencyPolicy: "Allow",
661 schedule: onTheHour,
662 deadline: shortDead,
663 ranPreviously: true,
664 stillActive: true,
665 jobCreationTime: justAfterThePriorHour(),
666 now: *justAfterTheHour(),
667 expectActive: 1,
668 expectRequeueAfter: true,
669 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
670 jobPresentInCJActiveStatus: true,
671 },
672 "still active, is time, not past deadline": {
673 concurrencyPolicy: "Allow",
674 schedule: onTheHour,
675 deadline: longDead,
676 ranPreviously: true,
677 stillActive: true,
678 jobCreationTime: justAfterThePriorHour(),
679 now: *justAfterTheHour(),
680 expectCreate: true,
681 expectActive: 2,
682 expectRequeueAfter: true,
683 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
684 expectUpdateStatus: true,
685 jobPresentInCJActiveStatus: true,
686 },
687
688
689
690 "prev ran but done, long overdue, not past deadline, A": {
691 concurrencyPolicy: "Allow",
692 schedule: onTheHour,
693 deadline: longDead,
694 ranPreviously: true,
695 jobCreationTime: justAfterThePriorHour(),
696 now: weekAfterTheHour(),
697 expectCreate: true,
698 expectActive: 1,
699 expectedWarnings: 1,
700 expectRequeueAfter: true,
701 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
702 expectUpdateStatus: true,
703 jobPresentInCJActiveStatus: true,
704 },
705 "prev ran but done, long overdue, not past deadline, R": {
706 concurrencyPolicy: "Replace",
707 schedule: onTheHour,
708 deadline: longDead,
709 ranPreviously: true,
710 jobCreationTime: justAfterThePriorHour(),
711 now: weekAfterTheHour(),
712 expectCreate: true,
713 expectActive: 1,
714 expectedWarnings: 1,
715 expectRequeueAfter: true,
716 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
717 expectUpdateStatus: true,
718 jobPresentInCJActiveStatus: true,
719 },
720 "prev ran but done, long overdue, not past deadline, F": {
721 concurrencyPolicy: "Forbid",
722 schedule: onTheHour,
723 deadline: longDead,
724 ranPreviously: true,
725 jobCreationTime: justAfterThePriorHour(),
726 now: weekAfterTheHour(),
727 expectCreate: true,
728 expectActive: 1,
729 expectedWarnings: 1,
730 expectRequeueAfter: true,
731 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
732 expectUpdateStatus: true,
733 jobPresentInCJActiveStatus: true,
734 },
735 "prev ran but done, long overdue, no deadline, A": {
736 concurrencyPolicy: "Allow",
737 schedule: onTheHour,
738 deadline: noDead,
739 ranPreviously: true,
740 jobCreationTime: justAfterThePriorHour(),
741 now: weekAfterTheHour(),
742 expectCreate: true,
743 expectActive: 1,
744 expectedWarnings: 1,
745 expectRequeueAfter: true,
746 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
747 expectUpdateStatus: true,
748 jobPresentInCJActiveStatus: true,
749 },
750 "prev ran but done, long overdue, no deadline, R": {
751 concurrencyPolicy: "Replace",
752 schedule: onTheHour,
753 deadline: noDead,
754 ranPreviously: true,
755 jobCreationTime: justAfterThePriorHour(),
756 now: weekAfterTheHour(),
757 expectCreate: true,
758 expectActive: 1,
759 expectedWarnings: 1,
760 expectRequeueAfter: true,
761 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
762 expectUpdateStatus: true,
763 jobPresentInCJActiveStatus: true,
764 },
765 "prev ran but done, long overdue, no deadline, F": {
766 concurrencyPolicy: "Forbid",
767 schedule: onTheHour,
768 deadline: noDead,
769 ranPreviously: true,
770 jobCreationTime: justAfterThePriorHour(),
771 now: weekAfterTheHour(),
772 expectCreate: true,
773 expectActive: 1,
774 expectedWarnings: 1,
775 expectRequeueAfter: true,
776 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
777 expectUpdateStatus: true,
778 jobPresentInCJActiveStatus: true,
779 },
780
781 "prev ran but done, long overdue, past medium deadline, A": {
782 concurrencyPolicy: "Allow",
783 schedule: onTheHour,
784 deadline: mediumDead,
785 ranPreviously: true,
786 jobCreationTime: justAfterThePriorHour(),
787 now: weekAfterTheHour(),
788 expectCreate: true,
789 expectActive: 1,
790 expectRequeueAfter: true,
791 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
792 expectUpdateStatus: true,
793 jobPresentInCJActiveStatus: true,
794 },
795 "prev ran but done, long overdue, past short deadline, A": {
796 concurrencyPolicy: "Allow",
797 schedule: onTheHour,
798 deadline: shortDead,
799 ranPreviously: true,
800 jobCreationTime: justAfterThePriorHour(),
801 now: weekAfterTheHour(),
802 expectCreate: true,
803 expectActive: 1,
804 expectRequeueAfter: true,
805 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
806 expectUpdateStatus: true,
807 jobPresentInCJActiveStatus: true,
808 },
809
810 "prev ran but done, long overdue, past medium deadline, R": {
811 concurrencyPolicy: "Replace",
812 schedule: onTheHour,
813 deadline: mediumDead,
814 ranPreviously: true,
815 jobCreationTime: justAfterThePriorHour(),
816 now: weekAfterTheHour(),
817 expectCreate: true,
818 expectActive: 1,
819 expectRequeueAfter: true,
820 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
821 expectUpdateStatus: true,
822 jobPresentInCJActiveStatus: true,
823 },
824 "prev ran but done, long overdue, past short deadline, R": {
825 concurrencyPolicy: "Replace",
826 schedule: onTheHour,
827 deadline: shortDead,
828 ranPreviously: true,
829 jobCreationTime: justAfterThePriorHour(),
830 now: weekAfterTheHour(),
831 expectCreate: true,
832 expectActive: 1,
833 expectRequeueAfter: true,
834 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
835 expectUpdateStatus: true,
836 jobPresentInCJActiveStatus: true,
837 },
838
839 "prev ran but done, long overdue, past medium deadline, F": {
840 concurrencyPolicy: "Forbid",
841 schedule: onTheHour,
842 deadline: mediumDead,
843 ranPreviously: true,
844 jobCreationTime: justAfterThePriorHour(),
845 now: weekAfterTheHour(),
846 expectCreate: true,
847 expectActive: 1,
848 expectRequeueAfter: true,
849 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
850 expectUpdateStatus: true,
851 jobPresentInCJActiveStatus: true,
852 },
853 "prev ran but done, long overdue, past short deadline, F": {
854 concurrencyPolicy: "Forbid",
855 schedule: onTheHour,
856 deadline: shortDead,
857 ranPreviously: true,
858 jobCreationTime: justAfterThePriorHour(),
859 now: weekAfterTheHour(),
860 expectCreate: true,
861 expectActive: 1,
862 expectRequeueAfter: true,
863 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
864 expectUpdateStatus: true,
865 jobPresentInCJActiveStatus: true,
866 },
867
868
869
870 "this ran but done, time drifted back, F": {
871 concurrencyPolicy: "Forbid",
872 schedule: onTheHour,
873 deadline: noDead,
874 ranPreviously: true,
875 jobCreationTime: *justAfterTheHour(),
876 now: justBeforeTheHour(),
877 jobCreateError: errors.NewAlreadyExists(schema.GroupResource{Resource: "jobs", Group: "batch"}, ""),
878 expectRequeueAfter: true,
879 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
880 expectUpdateStatus: true,
881 },
882
883
884 "this started but went missing, not past deadline, A": {
885 concurrencyPolicy: "Allow",
886 schedule: onTheHour,
887 deadline: longDead,
888 ranPreviously: true,
889 stillActive: true,
890 jobCreationTime: topOfTheHour().Add(time.Millisecond * 100),
891 now: justAfterTheHour().Add(time.Millisecond * 100),
892 expectActive: 1,
893 expectRequeueAfter: true,
894 expectedRequeueDuration: 1*time.Hour - 1*time.Minute - time.Millisecond*100 + nextScheduleDelta,
895 jobStillNotFoundInLister: true,
896 jobPresentInCJActiveStatus: true,
897 },
898 "this started but went missing, not past deadline, f": {
899 concurrencyPolicy: "Forbid",
900 schedule: onTheHour,
901 deadline: longDead,
902 ranPreviously: true,
903 stillActive: true,
904 jobCreationTime: topOfTheHour().Add(time.Millisecond * 100),
905 now: justAfterTheHour().Add(time.Millisecond * 100),
906 expectActive: 1,
907 expectRequeueAfter: true,
908 expectedRequeueDuration: 1*time.Hour - 1*time.Minute - time.Millisecond*100 + nextScheduleDelta,
909 jobStillNotFoundInLister: true,
910 jobPresentInCJActiveStatus: true,
911 },
912 "this started but went missing, not past deadline, R": {
913 concurrencyPolicy: "Replace",
914 schedule: onTheHour,
915 deadline: longDead,
916 ranPreviously: true,
917 stillActive: true,
918 jobCreationTime: topOfTheHour().Add(time.Millisecond * 100),
919 now: justAfterTheHour().Add(time.Millisecond * 100),
920 expectActive: 1,
921 expectRequeueAfter: true,
922 expectedRequeueDuration: 1*time.Hour - 1*time.Minute - time.Millisecond*100 + nextScheduleDelta,
923 jobStillNotFoundInLister: true,
924 jobPresentInCJActiveStatus: true,
925 },
926
927
928 "this started but is not present in cronjob active list, not past deadline, A": {
929 concurrencyPolicy: "Allow",
930 schedule: onTheHour,
931 deadline: longDead,
932 ranPreviously: true,
933 stillActive: true,
934 jobCreationTime: topOfTheHour().Add(time.Millisecond * 100),
935 now: justAfterTheHour().Add(time.Millisecond * 100),
936 expectActive: 1,
937 expectRequeueAfter: true,
938 expectedRequeueDuration: 1*time.Hour - 1*time.Minute - time.Millisecond*100 + nextScheduleDelta,
939 },
940 "this started but is not present in cronjob active list, not past deadline, f": {
941 concurrencyPolicy: "Forbid",
942 schedule: onTheHour,
943 deadline: longDead,
944 ranPreviously: true,
945 stillActive: true,
946 jobCreationTime: topOfTheHour().Add(time.Millisecond * 100),
947 now: justAfterTheHour().Add(time.Millisecond * 100),
948 expectActive: 1,
949 expectRequeueAfter: true,
950 expectedRequeueDuration: 1*time.Hour - 1*time.Minute - time.Millisecond*100 + nextScheduleDelta,
951 },
952 "this started but is not present in cronjob active list, not past deadline, R": {
953 concurrencyPolicy: "Replace",
954 schedule: onTheHour,
955 deadline: longDead,
956 ranPreviously: true,
957 stillActive: true,
958 jobCreationTime: topOfTheHour().Add(time.Millisecond * 100),
959 now: justAfterTheHour().Add(time.Millisecond * 100),
960 expectActive: 1,
961 expectRequeueAfter: true,
962 expectedRequeueDuration: 1*time.Hour - 1*time.Minute - time.Millisecond*100 + nextScheduleDelta,
963 },
964
965
966 "with @every schedule, never ran, not time": {
967 concurrencyPolicy: "Allow",
968 schedule: everyHour,
969 deadline: noDead,
970 cronjobCreationTime: justBeforeTheHour(),
971 jobCreationTime: justBeforeTheHour(),
972 now: *topOfTheHour(),
973 expectRequeueAfter: true,
974 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
975 jobPresentInCJActiveStatus: true,
976 },
977 "with @every schedule, never ran, is time": {
978 concurrencyPolicy: "Allow",
979 schedule: everyHour,
980 deadline: noDead,
981 cronjobCreationTime: justBeforeThePriorHour(),
982 jobCreationTime: justBeforeThePriorHour(),
983 now: justBeforeTheHour(),
984 expectRequeueAfter: true,
985 expectedRequeueDuration: 1*time.Hour + nextScheduleDelta,
986 jobPresentInCJActiveStatus: true,
987 expectCreate: true,
988 expectActive: 1,
989 expectUpdateStatus: true,
990 },
991 "with @every schedule, never ran, is time, past deadline": {
992 concurrencyPolicy: "Allow",
993 schedule: everyHour,
994 deadline: shortDead,
995 cronjobCreationTime: justBeforeThePriorHour(),
996 jobCreationTime: justBeforeThePriorHour(),
997 now: justBeforeTheHour().Add(time.Second * time.Duration(shortDead+1)),
998 expectRequeueAfter: true,
999 expectedRequeueDuration: 1*time.Hour - time.Second*time.Duration(shortDead+1) + nextScheduleDelta,
1000 jobPresentInCJActiveStatus: true,
1001 },
1002 "with @every schedule, never ran, is time, not past deadline": {
1003 concurrencyPolicy: "Allow",
1004 schedule: everyHour,
1005 deadline: longDead,
1006 cronjobCreationTime: justBeforeThePriorHour(),
1007 jobCreationTime: justBeforeThePriorHour(),
1008 now: justBeforeTheHour().Add(time.Second * time.Duration(shortDead-1)),
1009 expectCreate: true,
1010 expectActive: 1,
1011 expectRequeueAfter: true,
1012 expectedRequeueDuration: 1*time.Hour - time.Second*time.Duration(shortDead-1) + nextScheduleDelta,
1013 expectUpdateStatus: true,
1014 jobPresentInCJActiveStatus: true,
1015 },
1016 "with @every schedule, prev ran but done, not time": {
1017 concurrencyPolicy: "Allow",
1018 schedule: everyHour,
1019 deadline: noDead,
1020 ranPreviously: true,
1021 cronjobCreationTime: justBeforeThePriorHour(),
1022 jobCreationTime: justBeforeThePriorHour(),
1023 lastScheduleTime: justBeforeTheHour(),
1024 now: *topOfTheHour(),
1025 expectRequeueAfter: true,
1026 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
1027 expectUpdateStatus: true,
1028 jobPresentInCJActiveStatus: true,
1029 },
1030 "with @every schedule, prev ran but done, is time": {
1031 concurrencyPolicy: "Allow",
1032 schedule: everyHour,
1033 deadline: noDead,
1034 ranPreviously: true,
1035 cronjobCreationTime: justBeforeThePriorHour(),
1036 jobCreationTime: justBeforeThePriorHour(),
1037 lastScheduleTime: justBeforeTheHour(),
1038 now: topOfTheHour().Add(1 * time.Hour),
1039 expectCreate: true,
1040 expectActive: 1,
1041 expectRequeueAfter: true,
1042 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
1043 expectUpdateStatus: true,
1044 jobPresentInCJActiveStatus: true,
1045 },
1046 "with @every schedule, prev ran but done, is time, past deadline": {
1047 concurrencyPolicy: "Allow",
1048 schedule: everyHour,
1049 deadline: shortDead,
1050 ranPreviously: true,
1051 cronjobCreationTime: justBeforeThePriorHour(),
1052 jobCreationTime: justBeforeThePriorHour(),
1053 lastScheduleTime: justBeforeTheHour(),
1054 now: justBeforeTheNextHour().Add(time.Second * time.Duration(shortDead+1)),
1055 expectRequeueAfter: true,
1056 expectedRequeueDuration: 1*time.Hour - time.Second*time.Duration(shortDead+1) + nextScheduleDelta,
1057 expectUpdateStatus: true,
1058 jobPresentInCJActiveStatus: true,
1059 },
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080 "with @every schedule, still active, not time": {
1081 concurrencyPolicy: "Allow",
1082 schedule: everyHour,
1083 deadline: noDead,
1084 ranPreviously: true,
1085 stillActive: true,
1086 cronjobCreationTime: justBeforeThePriorHour(),
1087 jobCreationTime: justBeforeTheHour(),
1088 lastScheduleTime: justBeforeTheHour(),
1089 now: *topOfTheHour(),
1090 expectActive: 1,
1091 expectRequeueAfter: true,
1092 expectedRequeueDuration: 1*time.Hour - 1*time.Minute + nextScheduleDelta,
1093 jobPresentInCJActiveStatus: true,
1094 },
1095 "with @every schedule, still active, is time": {
1096 concurrencyPolicy: "Allow",
1097 schedule: everyHour,
1098 deadline: noDead,
1099 ranPreviously: true,
1100 stillActive: true,
1101 cronjobCreationTime: justBeforeThePriorHour(),
1102 jobCreationTime: justBeforeThePriorHour(),
1103 lastScheduleTime: justBeforeThePriorHour(),
1104 now: *justAfterTheHour(),
1105 expectCreate: true,
1106 expectActive: 2,
1107 expectRequeueAfter: true,
1108 expectedRequeueDuration: 1*time.Hour - 2*time.Minute + nextScheduleDelta,
1109 expectUpdateStatus: true,
1110 jobPresentInCJActiveStatus: true,
1111 },
1112 "with @every schedule, still active, is time, past deadline": {
1113 concurrencyPolicy: "Allow",
1114 schedule: everyHour,
1115 deadline: shortDead,
1116 ranPreviously: true,
1117 stillActive: true,
1118 cronjobCreationTime: justBeforeThePriorHour(),
1119 jobCreationTime: justBeforeTheHour(),
1120 lastScheduleTime: justBeforeTheHour(),
1121 now: justBeforeTheNextHour().Add(time.Second * time.Duration(shortDead+1)),
1122 expectActive: 1,
1123 expectRequeueAfter: true,
1124 expectedRequeueDuration: 1*time.Hour - time.Second*time.Duration(shortDead+1) + nextScheduleDelta,
1125 jobPresentInCJActiveStatus: true,
1126 },
1127 "with @every schedule, still active, is time, not past deadline": {
1128 concurrencyPolicy: "Allow",
1129 schedule: everyHour,
1130 deadline: longDead,
1131 ranPreviously: true,
1132 stillActive: true,
1133 cronjobCreationTime: justBeforeThePriorHour(),
1134 jobCreationTime: justBeforeTheHour(),
1135 lastScheduleTime: justBeforeTheHour(),
1136 now: justBeforeTheNextHour().Add(time.Second * time.Duration(shortDead-1)),
1137 expectCreate: true,
1138 expectActive: 2,
1139 expectRequeueAfter: true,
1140 expectedRequeueDuration: 1*time.Hour - time.Second*time.Duration(shortDead-1) + nextScheduleDelta,
1141 expectUpdateStatus: true,
1142 jobPresentInCJActiveStatus: true,
1143 },
1144 "with @every schedule, prev ran but done, long overdue, no deadline": {
1145 concurrencyPolicy: "Allow",
1146 schedule: everyHour,
1147 deadline: noDead,
1148 ranPreviously: true,
1149 cronjobCreationTime: justAfterThePriorHour(),
1150 lastScheduleTime: *justAfterTheHour(),
1151 jobCreationTime: justAfterThePriorHour(),
1152 now: weekAfterTheHour(),
1153 expectCreate: true,
1154 expectActive: 1,
1155 expectedWarnings: 1,
1156 expectRequeueAfter: true,
1157 expectedRequeueDuration: 1*time.Minute + nextScheduleDelta,
1158 expectUpdateStatus: true,
1159 jobPresentInCJActiveStatus: true,
1160 },
1161 "with @every schedule, prev ran but done, long overdue, past deadline": {
1162 concurrencyPolicy: "Allow",
1163 schedule: everyHour,
1164 deadline: shortDead,
1165 ranPreviously: true,
1166 cronjobCreationTime: justAfterThePriorHour(),
1167 lastScheduleTime: *justAfterTheHour(),
1168 jobCreationTime: justAfterThePriorHour(),
1169 now: weekAfterTheHour().Add(1 * time.Minute).Add(time.Second * time.Duration(shortDead+1)),
1170 expectActive: 1,
1171 expectRequeueAfter: true,
1172 expectedRequeueDuration: 1*time.Hour - time.Second*time.Duration(shortDead+1) + nextScheduleDelta,
1173 expectUpdateStatus: true,
1174 jobPresentInCJActiveStatus: true,
1175 },
1176 "do nothing if the namespace is terminating": {
1177 jobCreateError: &errors.StatusError{ErrStatus: metav1.Status{Details: &metav1.StatusDetails{Causes: []metav1.StatusCause{
1178 {
1179 Type: v1.NamespaceTerminatingCause,
1180 Message: fmt.Sprintf("namespace %s is being terminated", metav1.NamespaceDefault),
1181 Field: "metadata.namespace",
1182 }}}}},
1183 concurrencyPolicy: "Allow",
1184 schedule: onTheHour,
1185 deadline: noDead,
1186 ranPreviously: true,
1187 stillActive: true,
1188 jobCreationTime: justAfterThePriorHour(),
1189 now: *justAfterTheHour(),
1190 expectActive: 0,
1191 expectRequeueAfter: false,
1192 expectUpdateStatus: false,
1193 expectErr: true,
1194 jobPresentInCJActiveStatus: false,
1195 },
1196 }
1197 for name, tc := range testCases {
1198 name := name
1199 tc := tc
1200
1201 t.Run(name, func(t *testing.T) {
1202 cj := cronJob()
1203 cj.Spec.ConcurrencyPolicy = tc.concurrencyPolicy
1204 cj.Spec.Suspend = &tc.suspend
1205 cj.Spec.Schedule = tc.schedule
1206 cj.Spec.TimeZone = tc.timeZone
1207 if tc.deadline != noDead {
1208 cj.Spec.StartingDeadlineSeconds = &tc.deadline
1209 }
1210
1211 var (
1212 job *batchv1.Job
1213 err error
1214 )
1215 js := []*batchv1.Job{}
1216 realCJ := cj.DeepCopy()
1217 if tc.ranPreviously {
1218 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: justBeforeThePriorHour()}
1219 if !tc.cronjobCreationTime.IsZero() {
1220 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: tc.cronjobCreationTime}
1221 }
1222 cj.Status.LastScheduleTime = &metav1.Time{Time: justAfterThePriorHour()}
1223 if !tc.lastScheduleTime.IsZero() {
1224 cj.Status.LastScheduleTime = &metav1.Time{Time: tc.lastScheduleTime}
1225 }
1226 job, err = getJobFromTemplate2(&cj, tc.jobCreationTime)
1227 if err != nil {
1228 t.Fatalf("%s: unexpected error creating a job from template: %v", name, err)
1229 }
1230 job.UID = "1234"
1231 job.Namespace = cj.Namespace
1232 if tc.stillActive {
1233 ref, err := getRef(job)
1234 if err != nil {
1235 t.Fatalf("%s: unexpected error getting the job object reference: %v", name, err)
1236 }
1237 if tc.jobPresentInCJActiveStatus {
1238 cj.Status.Active = []v1.ObjectReference{*ref}
1239 }
1240 realCJ.Status.Active = []v1.ObjectReference{*ref}
1241 if !tc.jobStillNotFoundInLister {
1242 js = append(js, job)
1243 }
1244 } else {
1245 job.Status.CompletionTime = &metav1.Time{Time: job.ObjectMeta.CreationTimestamp.Add(time.Second * 10)}
1246 job.Status.Conditions = append(job.Status.Conditions, batchv1.JobCondition{
1247 Type: batchv1.JobComplete,
1248 Status: v1.ConditionTrue,
1249 })
1250 if !tc.jobStillNotFoundInLister {
1251 js = append(js, job)
1252 }
1253 }
1254 } else {
1255 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: justBeforeTheHour()}
1256 if !tc.cronjobCreationTime.IsZero() {
1257 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: tc.cronjobCreationTime}
1258 }
1259 if tc.stillActive {
1260 t.Errorf("%s: test setup error: this case makes no sense", name)
1261 }
1262 }
1263
1264 jc := &fakeJobControl{Job: job, CreateErr: tc.jobCreateError, Err: tc.jobGetErr}
1265 cjc := &fakeCJControl{CronJob: realCJ}
1266 recorder := record.NewFakeRecorder(10)
1267
1268 jm := ControllerV2{
1269 jobControl: jc,
1270 cronJobControl: cjc,
1271 recorder: recorder,
1272 now: func() time.Time {
1273 return tc.now
1274 },
1275 }
1276 cjCopy := cj.DeepCopy()
1277 requeueAfter, updateStatus, err := jm.syncCronJob(context.TODO(), cjCopy, js)
1278 if tc.expectErr && err == nil {
1279 t.Errorf("%s: expected error got none with requeueAfter time: %#v", name, requeueAfter)
1280 }
1281 if tc.expectRequeueAfter {
1282 if !reflect.DeepEqual(requeueAfter, &tc.expectedRequeueDuration) {
1283 t.Errorf("%s: expected requeueAfter: %+v, got requeueAfter time: %+v", name, tc.expectedRequeueDuration, requeueAfter)
1284 }
1285 }
1286 if updateStatus != tc.expectUpdateStatus {
1287 t.Errorf("%s: expected updateStatus: %t, actually: %t", name, tc.expectUpdateStatus, updateStatus)
1288 }
1289 expectedCreates := 0
1290 if tc.expectCreate {
1291 expectedCreates = 1
1292 }
1293 if tc.ranPreviously && !tc.stillActive {
1294 completionTime := tc.jobCreationTime.Add(10 * time.Second)
1295 if cjCopy.Status.LastSuccessfulTime == nil || !cjCopy.Status.LastSuccessfulTime.Time.Equal(completionTime) {
1296 t.Errorf("cj.status.lastSuccessfulTime: %s expected, got %#v", completionTime, cj.Status.LastSuccessfulTime)
1297 }
1298 }
1299 if len(jc.Jobs) != expectedCreates {
1300 t.Errorf("%s: expected %d job started, actually %v", name, expectedCreates, len(jc.Jobs))
1301 }
1302 for i := range jc.Jobs {
1303 job := &jc.Jobs[i]
1304 controllerRef := metav1.GetControllerOf(job)
1305 if controllerRef == nil {
1306 t.Errorf("%s: expected job to have ControllerRef: %#v", name, job)
1307 } else {
1308 if got, want := controllerRef.APIVersion, "batch/v1"; got != want {
1309 t.Errorf("%s: controllerRef.APIVersion = %q, want %q", name, got, want)
1310 }
1311 if got, want := controllerRef.Kind, "CronJob"; got != want {
1312 t.Errorf("%s: controllerRef.Kind = %q, want %q", name, got, want)
1313 }
1314 if got, want := controllerRef.Name, cj.Name; got != want {
1315 t.Errorf("%s: controllerRef.Name = %q, want %q", name, got, want)
1316 }
1317 if got, want := controllerRef.UID, cj.UID; got != want {
1318 t.Errorf("%s: controllerRef.UID = %q, want %q", name, got, want)
1319 }
1320 if controllerRef.Controller == nil || *controllerRef.Controller != true {
1321 t.Errorf("%s: controllerRef.Controller is not set to true", name)
1322 }
1323 }
1324 }
1325
1326 expectedDeletes := 0
1327 if tc.expectDelete {
1328 expectedDeletes = 1
1329 }
1330 if len(jc.DeleteJobName) != expectedDeletes {
1331 t.Errorf("%s: expected %d job deleted, actually %v", name, expectedDeletes, len(jc.DeleteJobName))
1332 }
1333
1334
1335 expectUpdates := 1
1336 expectedEvents := 0
1337 if tc.expectCreate {
1338 expectedEvents++
1339 expectUpdates++
1340 }
1341 if tc.expectDelete {
1342 expectedEvents++
1343 }
1344 if name == "still active, is time, F" {
1345
1346 expectedEvents++
1347 }
1348 expectedEvents += tc.expectedWarnings
1349
1350 if len(recorder.Events) != expectedEvents {
1351 t.Errorf("%s: expected %d event, actually %v", name, expectedEvents, len(recorder.Events))
1352 }
1353
1354 numWarnings := 0
1355 for i := 1; i <= len(recorder.Events); i++ {
1356 e := <-recorder.Events
1357 if strings.HasPrefix(e, v1.EventTypeWarning) {
1358 numWarnings++
1359 }
1360 }
1361 if numWarnings != tc.expectedWarnings {
1362 t.Errorf("%s: expected %d warnings, actually %v", name, tc.expectedWarnings, numWarnings)
1363 }
1364
1365 if len(cjc.Updates) == expectUpdates && tc.expectActive != len(cjc.Updates[expectUpdates-1].Status.Active) {
1366 t.Errorf("%s: expected Active size %d, got %d", name, tc.expectActive, len(cjc.Updates[expectUpdates-1].Status.Active))
1367 }
1368
1369 if &cj == cjCopy {
1370 t.Errorf("syncCronJob is not creating a copy of the original cronjob")
1371 }
1372 })
1373 }
1374
1375 }
1376
1377 type fakeQueue struct {
1378 workqueue.RateLimitingInterface
1379 delay time.Duration
1380 key interface{}
1381 }
1382
1383 func (f *fakeQueue) AddAfter(key interface{}, delay time.Duration) {
1384 f.delay = delay
1385 f.key = key
1386 }
1387
1388
1389 func TestControllerV2UpdateCronJob(t *testing.T) {
1390 tests := []struct {
1391 name string
1392 oldCronJob *batchv1.CronJob
1393 newCronJob *batchv1.CronJob
1394 expectedDelay time.Duration
1395 }{
1396 {
1397 name: "spec.template changed",
1398 oldCronJob: &batchv1.CronJob{
1399 Spec: batchv1.CronJobSpec{
1400 JobTemplate: batchv1.JobTemplateSpec{
1401 ObjectMeta: metav1.ObjectMeta{
1402 Labels: map[string]string{"a": "b"},
1403 Annotations: map[string]string{"x": "y"},
1404 },
1405 Spec: jobSpec(),
1406 },
1407 },
1408 },
1409 newCronJob: &batchv1.CronJob{
1410 Spec: batchv1.CronJobSpec{
1411 JobTemplate: batchv1.JobTemplateSpec{
1412 ObjectMeta: metav1.ObjectMeta{
1413 Labels: map[string]string{"a": "foo"},
1414 Annotations: map[string]string{"x": "y"},
1415 },
1416 Spec: jobSpec(),
1417 },
1418 },
1419 },
1420 expectedDelay: 0 * time.Second,
1421 },
1422 {
1423 name: "spec.schedule changed",
1424 oldCronJob: &batchv1.CronJob{
1425 Spec: batchv1.CronJobSpec{
1426 Schedule: "30 * * * *",
1427 JobTemplate: batchv1.JobTemplateSpec{
1428 ObjectMeta: metav1.ObjectMeta{
1429 Labels: map[string]string{"a": "b"},
1430 Annotations: map[string]string{"x": "y"},
1431 },
1432 Spec: jobSpec(),
1433 },
1434 },
1435 Status: batchv1.CronJobStatus{
1436 LastScheduleTime: &metav1.Time{Time: justBeforeTheHour()},
1437 },
1438 },
1439 newCronJob: &batchv1.CronJob{
1440 Spec: batchv1.CronJobSpec{
1441 Schedule: "*/1 * * * *",
1442 JobTemplate: batchv1.JobTemplateSpec{
1443 ObjectMeta: metav1.ObjectMeta{
1444 Labels: map[string]string{"a": "foo"},
1445 Annotations: map[string]string{"x": "y"},
1446 },
1447 Spec: jobSpec(),
1448 },
1449 },
1450 Status: batchv1.CronJobStatus{
1451 LastScheduleTime: &metav1.Time{Time: justBeforeTheHour()},
1452 },
1453 },
1454 expectedDelay: 1*time.Second + nextScheduleDelta,
1455 },
1456 {
1457 name: "spec.schedule with @every changed - cadence decrease",
1458 oldCronJob: &batchv1.CronJob{
1459 Spec: batchv1.CronJobSpec{
1460 Schedule: "@every 1m",
1461 JobTemplate: batchv1.JobTemplateSpec{
1462 ObjectMeta: metav1.ObjectMeta{
1463 Labels: map[string]string{"a": "b"},
1464 Annotations: map[string]string{"x": "y"},
1465 },
1466 Spec: jobSpec(),
1467 },
1468 },
1469 Status: batchv1.CronJobStatus{
1470 LastScheduleTime: &metav1.Time{Time: justBeforeTheHour()},
1471 },
1472 },
1473 newCronJob: &batchv1.CronJob{
1474 Spec: batchv1.CronJobSpec{
1475 Schedule: "@every 3m",
1476 JobTemplate: batchv1.JobTemplateSpec{
1477 ObjectMeta: metav1.ObjectMeta{
1478 Labels: map[string]string{"a": "foo"},
1479 Annotations: map[string]string{"x": "y"},
1480 },
1481 Spec: jobSpec(),
1482 },
1483 },
1484 Status: batchv1.CronJobStatus{
1485 LastScheduleTime: &metav1.Time{Time: justBeforeTheHour()},
1486 },
1487 },
1488 expectedDelay: 2*time.Minute + 1*time.Second + nextScheduleDelta,
1489 },
1490 {
1491 name: "spec.schedule with @every changed - cadence increase",
1492 oldCronJob: &batchv1.CronJob{
1493 Spec: batchv1.CronJobSpec{
1494 Schedule: "@every 3m",
1495 JobTemplate: batchv1.JobTemplateSpec{
1496 ObjectMeta: metav1.ObjectMeta{
1497 Labels: map[string]string{"a": "b"},
1498 Annotations: map[string]string{"x": "y"},
1499 },
1500 Spec: jobSpec(),
1501 },
1502 },
1503 Status: batchv1.CronJobStatus{
1504 LastScheduleTime: &metav1.Time{Time: justBeforeTheHour()},
1505 },
1506 },
1507 newCronJob: &batchv1.CronJob{
1508 Spec: batchv1.CronJobSpec{
1509 Schedule: "@every 1m",
1510 JobTemplate: batchv1.JobTemplateSpec{
1511 ObjectMeta: metav1.ObjectMeta{
1512 Labels: map[string]string{"a": "foo"},
1513 Annotations: map[string]string{"x": "y"},
1514 },
1515 Spec: jobSpec(),
1516 },
1517 },
1518 Status: batchv1.CronJobStatus{
1519 LastScheduleTime: &metav1.Time{Time: justBeforeTheHour()},
1520 },
1521 },
1522 expectedDelay: 1*time.Second + nextScheduleDelta,
1523 },
1524 {
1525 name: "spec.timeZone not changed",
1526 oldCronJob: &batchv1.CronJob{
1527 Spec: batchv1.CronJobSpec{
1528 TimeZone: &newYork,
1529 JobTemplate: batchv1.JobTemplateSpec{
1530 ObjectMeta: metav1.ObjectMeta{
1531 Labels: map[string]string{"a": "b"},
1532 Annotations: map[string]string{"x": "y"},
1533 },
1534 Spec: jobSpec(),
1535 },
1536 },
1537 },
1538 newCronJob: &batchv1.CronJob{
1539 Spec: batchv1.CronJobSpec{
1540 TimeZone: &newYork,
1541 JobTemplate: batchv1.JobTemplateSpec{
1542 ObjectMeta: metav1.ObjectMeta{
1543 Labels: map[string]string{"a": "foo"},
1544 Annotations: map[string]string{"x": "y"},
1545 },
1546 Spec: jobSpec(),
1547 },
1548 },
1549 },
1550 expectedDelay: 0 * time.Second,
1551 },
1552 {
1553 name: "spec.timeZone changed",
1554 oldCronJob: &batchv1.CronJob{
1555 Spec: batchv1.CronJobSpec{
1556 TimeZone: &newYork,
1557 JobTemplate: batchv1.JobTemplateSpec{
1558 ObjectMeta: metav1.ObjectMeta{
1559 Labels: map[string]string{"a": "b"},
1560 Annotations: map[string]string{"x": "y"},
1561 },
1562 Spec: jobSpec(),
1563 },
1564 },
1565 },
1566 newCronJob: &batchv1.CronJob{
1567 Spec: batchv1.CronJobSpec{
1568 TimeZone: nil,
1569 JobTemplate: batchv1.JobTemplateSpec{
1570 ObjectMeta: metav1.ObjectMeta{
1571 Labels: map[string]string{"a": "foo"},
1572 Annotations: map[string]string{"x": "y"},
1573 },
1574 Spec: jobSpec(),
1575 },
1576 },
1577 },
1578 expectedDelay: 0 * time.Second,
1579 },
1580
1581
1582 }
1583 for _, tt := range tests {
1584 t.Run(tt.name, func(t *testing.T) {
1585 logger, ctx := ktesting.NewTestContext(t)
1586 ctx, cancel := context.WithCancel(ctx)
1587 defer cancel()
1588 kubeClient := fake.NewSimpleClientset()
1589 sharedInformers := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
1590 jm, err := NewControllerV2(ctx, sharedInformers.Batch().V1().Jobs(), sharedInformers.Batch().V1().CronJobs(), kubeClient)
1591 if err != nil {
1592 t.Errorf("unexpected error %v", err)
1593 return
1594 }
1595 jm.now = justASecondBeforeTheHour
1596 queue := &fakeQueue{RateLimitingInterface: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test-update-cronjob")}
1597 jm.queue = queue
1598 jm.jobControl = &fakeJobControl{}
1599 jm.cronJobControl = &fakeCJControl{}
1600 jm.recorder = record.NewFakeRecorder(10)
1601
1602 jm.updateCronJob(logger, tt.oldCronJob, tt.newCronJob)
1603 if queue.delay.Seconds() != tt.expectedDelay.Seconds() {
1604 t.Errorf("Expected delay %#v got %#v", tt.expectedDelay.Seconds(), queue.delay.Seconds())
1605 }
1606 })
1607 }
1608 }
1609
1610 func TestControllerV2GetJobsToBeReconciled(t *testing.T) {
1611 trueRef := true
1612 tests := []struct {
1613 name string
1614 cronJob *batchv1.CronJob
1615 jobs []runtime.Object
1616 expected []*batchv1.Job
1617 }{
1618 {
1619 name: "test getting jobs in namespace without controller reference",
1620 cronJob: &batchv1.CronJob{ObjectMeta: metav1.ObjectMeta{Namespace: "foo-ns", Name: "fooer"}},
1621 jobs: []runtime.Object{
1622 &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "foo-ns"}},
1623 &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "foo-ns"}},
1624 &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "foo2", Namespace: "foo-ns"}},
1625 },
1626 expected: []*batchv1.Job{},
1627 },
1628 {
1629 name: "test getting jobs in namespace with a controller reference",
1630 cronJob: &batchv1.CronJob{ObjectMeta: metav1.ObjectMeta{Namespace: "foo-ns", Name: "fooer"}},
1631 jobs: []runtime.Object{
1632 &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "foo-ns"}},
1633 &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "foo-ns",
1634 OwnerReferences: []metav1.OwnerReference{{Name: "fooer", Controller: &trueRef}}}},
1635 &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "foo2", Namespace: "foo-ns"}},
1636 },
1637 expected: []*batchv1.Job{
1638 {ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "foo-ns",
1639 OwnerReferences: []metav1.OwnerReference{{Name: "fooer", Controller: &trueRef}}}},
1640 },
1641 },
1642 {
1643 name: "test getting jobs in other namespaces",
1644 cronJob: &batchv1.CronJob{ObjectMeta: metav1.ObjectMeta{Namespace: "foo-ns", Name: "fooer"}},
1645 jobs: []runtime.Object{
1646 &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar-ns"}},
1647 &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "bar-ns"}},
1648 &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "foo2", Namespace: "bar-ns"}},
1649 },
1650 expected: []*batchv1.Job{},
1651 },
1652 {
1653 name: "test getting jobs whose labels do not match job template",
1654 cronJob: &batchv1.CronJob{
1655 ObjectMeta: metav1.ObjectMeta{Namespace: "foo-ns", Name: "fooer"},
1656 Spec: batchv1.CronJobSpec{JobTemplate: batchv1.JobTemplateSpec{
1657 ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"key": "value"}},
1658 }},
1659 },
1660 jobs: []runtime.Object{
1661 &batchv1.Job{ObjectMeta: metav1.ObjectMeta{
1662 Namespace: "foo-ns",
1663 Name: "foo-fooer-owner-ref",
1664 Labels: map[string]string{"key": "different-value"},
1665 OwnerReferences: []metav1.OwnerReference{{Name: "fooer", Controller: &trueRef}}},
1666 },
1667 &batchv1.Job{ObjectMeta: metav1.ObjectMeta{
1668 Namespace: "foo-ns",
1669 Name: "foo-other-owner-ref",
1670 Labels: map[string]string{"key": "different-value"},
1671 OwnerReferences: []metav1.OwnerReference{{Name: "another-cronjob", Controller: &trueRef}}},
1672 },
1673 },
1674 expected: []*batchv1.Job{{
1675 ObjectMeta: metav1.ObjectMeta{
1676 Namespace: "foo-ns",
1677 Name: "foo-fooer-owner-ref",
1678 Labels: map[string]string{"key": "different-value"},
1679 OwnerReferences: []metav1.OwnerReference{{Name: "fooer", Controller: &trueRef}}},
1680 }},
1681 },
1682 }
1683 for _, tt := range tests {
1684 t.Run(tt.name, func(t *testing.T) {
1685 _, ctx := ktesting.NewTestContext(t)
1686 ctx, cancel := context.WithCancel(ctx)
1687 defer cancel()
1688 kubeClient := fake.NewSimpleClientset()
1689 sharedInformers := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
1690 for _, job := range tt.jobs {
1691 sharedInformers.Batch().V1().Jobs().Informer().GetIndexer().Add(job)
1692 }
1693 jm, err := NewControllerV2(ctx, sharedInformers.Batch().V1().Jobs(), sharedInformers.Batch().V1().CronJobs(), kubeClient)
1694 if err != nil {
1695 t.Errorf("unexpected error %v", err)
1696 return
1697 }
1698
1699 actual, err := jm.getJobsToBeReconciled(tt.cronJob)
1700 if err != nil {
1701 t.Errorf("unexpected error %v", err)
1702 return
1703 }
1704 if !reflect.DeepEqual(actual, tt.expected) {
1705 t.Errorf("\nExpected %#v,\nbut got %#v", tt.expected, actual)
1706 }
1707 })
1708 }
1709 }
1710
1711 func TestControllerV2CleanupFinishedJobs(t *testing.T) {
1712 tests := []struct {
1713 name string
1714 now time.Time
1715 cronJob *batchv1.CronJob
1716 finishedJobs []*batchv1.Job
1717 jobCreateError error
1718 expectedDeletedJobs []string
1719 }{
1720 {
1721 name: "jobs are still deleted when a cronjob can't create jobs due to jobs quota being reached (avoiding a deadlock)",
1722 now: *justAfterTheHour(),
1723 cronJob: &batchv1.CronJob{
1724 ObjectMeta: metav1.ObjectMeta{Namespace: "foo-ns", Name: "fooer"},
1725 Spec: batchv1.CronJobSpec{
1726 Schedule: onTheHour,
1727 SuccessfulJobsHistoryLimit: pointer.Int32(1),
1728 JobTemplate: batchv1.JobTemplateSpec{
1729 ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"key": "value"}},
1730 },
1731 },
1732 Status: batchv1.CronJobStatus{LastScheduleTime: &metav1.Time{Time: justAfterThePriorHour()}},
1733 },
1734 finishedJobs: []*batchv1.Job{
1735 {
1736 ObjectMeta: metav1.ObjectMeta{
1737 Namespace: "foo-ns",
1738 Name: "finished-job-started-hour-ago",
1739 OwnerReferences: []metav1.OwnerReference{{Name: "fooer", Controller: pointer.Bool(true)}},
1740 },
1741 Status: batchv1.JobStatus{StartTime: &metav1.Time{Time: justBeforeThePriorHour()}},
1742 },
1743 {
1744 ObjectMeta: metav1.ObjectMeta{
1745 Namespace: "foo-ns",
1746 Name: "finished-job-started-minute-ago",
1747 OwnerReferences: []metav1.OwnerReference{{Name: "fooer", Controller: pointer.Bool(true)}},
1748 },
1749 Status: batchv1.JobStatus{StartTime: &metav1.Time{Time: justBeforeTheHour()}},
1750 },
1751 },
1752 jobCreateError: errors.NewInternalError(fmt.Errorf("quota for # of jobs reached")),
1753 expectedDeletedJobs: []string{"finished-job-started-hour-ago"},
1754 },
1755 {
1756 name: "jobs are not deleted if history limit not reached",
1757 now: justBeforeTheHour(),
1758 cronJob: &batchv1.CronJob{
1759 ObjectMeta: metav1.ObjectMeta{Namespace: "foo-ns", Name: "fooer"},
1760 Spec: batchv1.CronJobSpec{
1761 Schedule: onTheHour,
1762 SuccessfulJobsHistoryLimit: pointer.Int32(2),
1763 JobTemplate: batchv1.JobTemplateSpec{
1764 ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"key": "value"}},
1765 },
1766 },
1767 Status: batchv1.CronJobStatus{LastScheduleTime: &metav1.Time{Time: justAfterThePriorHour()}},
1768 },
1769 finishedJobs: []*batchv1.Job{
1770 {
1771 ObjectMeta: metav1.ObjectMeta{
1772 Namespace: "foo-ns",
1773 Name: "finished-job-started-hour-ago",
1774 OwnerReferences: []metav1.OwnerReference{{Name: "fooer", Controller: pointer.Bool(true)}},
1775 },
1776 Status: batchv1.JobStatus{StartTime: &metav1.Time{Time: justBeforeThePriorHour()}},
1777 },
1778 },
1779 jobCreateError: nil,
1780 expectedDeletedJobs: []string{},
1781 },
1782 }
1783
1784 for _, tt := range tests {
1785 t.Run(tt.name, func(t *testing.T) {
1786 _, ctx := ktesting.NewTestContext(t)
1787
1788 for _, job := range tt.finishedJobs {
1789 job.Status.Conditions = []batchv1.JobCondition{{Type: batchv1.JobComplete, Status: v1.ConditionTrue}}
1790 }
1791
1792 client := fake.NewSimpleClientset()
1793
1794 informerFactory := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
1795 _ = informerFactory.Batch().V1().CronJobs().Informer().GetIndexer().Add(tt.cronJob)
1796 for _, job := range tt.finishedJobs {
1797 _ = informerFactory.Batch().V1().Jobs().Informer().GetIndexer().Add(job)
1798 }
1799
1800 jm, err := NewControllerV2(ctx, informerFactory.Batch().V1().Jobs(), informerFactory.Batch().V1().CronJobs(), client)
1801 if err != nil {
1802 t.Errorf("unexpected error %v", err)
1803 return
1804 }
1805 jobControl := &fakeJobControl{CreateErr: tt.jobCreateError}
1806 jm.jobControl = jobControl
1807 jm.now = func() time.Time {
1808 return tt.now
1809 }
1810
1811 jm.enqueueController(tt.cronJob)
1812 jm.processNextWorkItem(ctx)
1813
1814 if len(tt.expectedDeletedJobs) != len(jobControl.DeleteJobName) {
1815 t.Fatalf("expected '%v' jobs to be deleted, instead deleted '%s'", tt.expectedDeletedJobs, jobControl.DeleteJobName)
1816 }
1817 sort.Strings(jobControl.DeleteJobName)
1818 sort.Strings(tt.expectedDeletedJobs)
1819 for i, deletedJob := range jobControl.DeleteJobName {
1820 if deletedJob != tt.expectedDeletedJobs[i] {
1821 t.Fatalf("expected '%v' jobs to be deleted, instead deleted '%s'", tt.expectedDeletedJobs, jobControl.DeleteJobName)
1822 }
1823 }
1824 })
1825 }
1826 }
1827
1828
1829
1830
1831 func TestControllerV2JobAlreadyExistsButNotInActiveStatus(t *testing.T) {
1832 _, ctx := ktesting.NewTestContext(t)
1833
1834 cj := cronJob()
1835 cj.Spec.ConcurrencyPolicy = "Forbid"
1836 cj.Spec.Schedule = everyHour
1837 cj.Status.LastScheduleTime = &metav1.Time{Time: justBeforeThePriorHour()}
1838 cj.Status.Active = []v1.ObjectReference{}
1839 cjCopy := cj.DeepCopy()
1840
1841 job, err := getJobFromTemplate2(&cj, justAfterThePriorHour())
1842 if err != nil {
1843 t.Fatalf("Unexpected error creating a job from template: %v", err)
1844 }
1845 job.UID = "1234"
1846 job.Namespace = cj.Namespace
1847
1848 client := fake.NewSimpleClientset(cjCopy, job)
1849 informerFactory := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
1850 _ = informerFactory.Batch().V1().CronJobs().Informer().GetIndexer().Add(cjCopy)
1851
1852 jm, err := NewControllerV2(ctx, informerFactory.Batch().V1().Jobs(), informerFactory.Batch().V1().CronJobs(), client)
1853 if err != nil {
1854 t.Fatalf("unexpected error %v", err)
1855 }
1856
1857 jobControl := &fakeJobControl{Job: job, CreateErr: errors.NewAlreadyExists(schema.GroupResource{Resource: "job", Group: "batch"}, "")}
1858 jm.jobControl = jobControl
1859 cronJobControl := &fakeCJControl{}
1860 jm.cronJobControl = cronJobControl
1861 jm.now = justBeforeTheHour
1862
1863 jm.enqueueController(cjCopy)
1864 jm.processNextWorkItem(ctx)
1865
1866 if len(cronJobControl.Updates) != 1 {
1867 t.Fatalf("Unexpected updates to cronjob, got: %d, expected 1", len(cronJobControl.Updates))
1868 }
1869 if len(cronJobControl.Updates[0].Status.Active) != 1 {
1870 t.Errorf("Unexpected active jobs count, got: %d, expected 1", len(cronJobControl.Updates[0].Status.Active))
1871 }
1872
1873 expectedActiveRef, err := getRef(job)
1874 if err != nil {
1875 t.Fatalf("Error getting expected job ref: %v", err)
1876 }
1877 if !reflect.DeepEqual(cronJobControl.Updates[0].Status.Active[0], *expectedActiveRef) {
1878 t.Errorf("Unexpected job reference in cronjob active list, got: %v, expected: %v", cronJobControl.Updates[0].Status.Active[0], expectedActiveRef)
1879 }
1880 }
1881
1882
1883
1884 func TestControllerV2JobAlreadyExistsButDifferentOwner(t *testing.T) {
1885 _, ctx := ktesting.NewTestContext(t)
1886
1887 cj := cronJob()
1888 cj.Spec.ConcurrencyPolicy = "Forbid"
1889 cj.Spec.Schedule = everyHour
1890 cj.Status.LastScheduleTime = &metav1.Time{Time: justBeforeThePriorHour()}
1891 cj.Status.Active = []v1.ObjectReference{}
1892 cjCopy := cj.DeepCopy()
1893
1894 job, err := getJobFromTemplate2(&cj, justAfterThePriorHour())
1895 if err != nil {
1896 t.Fatalf("Unexpected error creating a job from template: %v", err)
1897 }
1898 job.UID = "1234"
1899 job.Namespace = cj.Namespace
1900
1901
1902
1903 job.OwnerReferences = []metav1.OwnerReference{}
1904
1905 client := fake.NewSimpleClientset(cjCopy, job)
1906 informerFactory := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
1907 _ = informerFactory.Batch().V1().CronJobs().Informer().GetIndexer().Add(cjCopy)
1908
1909 jm, err := NewControllerV2(ctx, informerFactory.Batch().V1().Jobs(), informerFactory.Batch().V1().CronJobs(), client)
1910 if err != nil {
1911 t.Fatalf("unexpected error %v", err)
1912 }
1913
1914 jobControl := &fakeJobControl{Job: job, CreateErr: errors.NewAlreadyExists(schema.GroupResource{Resource: "job", Group: "batch"}, "")}
1915 jm.jobControl = jobControl
1916 cronJobControl := &fakeCJControl{}
1917 jm.cronJobControl = cronJobControl
1918 jm.now = justBeforeTheHour
1919
1920 jm.enqueueController(cjCopy)
1921 jm.processNextWorkItem(ctx)
1922
1923 if len(cronJobControl.Updates) != 0 {
1924 t.Fatalf("Unexpected updates to cronjob, got: %d, expected 0", len(cronJobControl.Updates))
1925 }
1926 }
1927
View as plain text