...

Source file src/k8s.io/kubernetes/pkg/controller/cronjob/utils_test.go

Documentation: k8s.io/kubernetes/pkg/controller/cronjob

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cronjob
    18  
    19  import (
    20  	"reflect"
    21  	"sort"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	cron "github.com/robfig/cron/v3"
    27  	batchv1 "k8s.io/api/batch/v1"
    28  	v1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    32  	"k8s.io/client-go/tools/record"
    33  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    34  	"k8s.io/klog/v2/ktesting"
    35  	"k8s.io/kubernetes/pkg/features"
    36  	"k8s.io/utils/pointer"
    37  )
    38  
    39  func TestGetJobFromTemplate2(t *testing.T) {
    40  	// getJobFromTemplate2() needs to take the job template and copy the labels and annotations
    41  	// and other fields, and add a created-by reference.
    42  	var (
    43  		one             int64 = 1
    44  		no              bool
    45  		timeZoneUTC     = "UTC"
    46  		timeZoneCorrect = "Europe/Rome"
    47  		scheduledTime   = *topOfTheHour()
    48  	)
    49  
    50  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CronJobsScheduledAnnotation, true)()
    51  
    52  	cj := batchv1.CronJob{
    53  		ObjectMeta: metav1.ObjectMeta{
    54  			Name:      "mycronjob",
    55  			Namespace: "snazzycats",
    56  			UID:       types.UID("1a2b3c"),
    57  		},
    58  		Spec: batchv1.CronJobSpec{
    59  			Schedule:          "* * * * ?",
    60  			ConcurrencyPolicy: batchv1.AllowConcurrent,
    61  			JobTemplate: batchv1.JobTemplateSpec{
    62  				ObjectMeta: metav1.ObjectMeta{
    63  					CreationTimestamp: metav1.Time{Time: scheduledTime},
    64  					Labels:            map[string]string{"a": "b"},
    65  				},
    66  				Spec: batchv1.JobSpec{
    67  					ActiveDeadlineSeconds: &one,
    68  					ManualSelector:        &no,
    69  					Template: v1.PodTemplateSpec{
    70  						ObjectMeta: metav1.ObjectMeta{
    71  							Labels: map[string]string{
    72  								"foo": "bar",
    73  							},
    74  						},
    75  						Spec: v1.PodSpec{
    76  							Containers: []v1.Container{
    77  								{Image: "foo/bar"},
    78  							},
    79  						},
    80  					},
    81  				},
    82  			},
    83  		},
    84  	}
    85  
    86  	testCases := []struct {
    87  		name                        string
    88  		timeZone                    *string
    89  		inputAnnotations            map[string]string
    90  		expectedScheduledTime       func() time.Time
    91  		expectedNumberOfAnnotations int
    92  	}{
    93  		{
    94  			name:             "UTC timezone and one annotation",
    95  			timeZone:         &timeZoneUTC,
    96  			inputAnnotations: map[string]string{"x": "y"},
    97  			expectedScheduledTime: func() time.Time {
    98  				return scheduledTime
    99  			},
   100  			expectedNumberOfAnnotations: 2,
   101  		},
   102  		{
   103  			name:             "nil timezone and one annotation",
   104  			timeZone:         nil,
   105  			inputAnnotations: map[string]string{"x": "y"},
   106  			expectedScheduledTime: func() time.Time {
   107  				return scheduledTime
   108  			},
   109  			expectedNumberOfAnnotations: 2,
   110  		},
   111  		{
   112  			name:             "correct timezone and multiple annotation",
   113  			timeZone:         &timeZoneCorrect,
   114  			inputAnnotations: map[string]string{"x": "y", "z": "x"},
   115  			expectedScheduledTime: func() time.Time {
   116  				location, _ := time.LoadLocation(timeZoneCorrect)
   117  				return scheduledTime.In(location)
   118  			},
   119  			expectedNumberOfAnnotations: 3,
   120  		},
   121  	}
   122  
   123  	for _, tt := range testCases {
   124  		t.Run(tt.name, func(t *testing.T) {
   125  			cj.Spec.JobTemplate.Annotations = tt.inputAnnotations
   126  			cj.Spec.TimeZone = tt.timeZone
   127  
   128  			var job *batchv1.Job
   129  			job, err := getJobFromTemplate2(&cj, scheduledTime)
   130  			if err != nil {
   131  				t.Errorf("Did not expect error: %s", err)
   132  			}
   133  			if !strings.HasPrefix(job.ObjectMeta.Name, "mycronjob-") {
   134  				t.Errorf("Wrong Name")
   135  			}
   136  			if len(job.ObjectMeta.Labels) != 1 {
   137  				t.Errorf("Wrong number of labels")
   138  			}
   139  			if len(job.ObjectMeta.Annotations) != tt.expectedNumberOfAnnotations {
   140  				t.Errorf("Wrong number of annotations")
   141  			}
   142  
   143  			scheduledAnnotation := job.ObjectMeta.Annotations[batchv1.CronJobScheduledTimestampAnnotation]
   144  			timeZoneLocation, err := time.LoadLocation(pointer.StringDeref(tt.timeZone, ""))
   145  			if err != nil {
   146  				t.Errorf("Wrong timezone location")
   147  			}
   148  			if len(job.ObjectMeta.Annotations) != 0 && scheduledAnnotation != tt.expectedScheduledTime().Format(time.RFC3339) {
   149  				t.Errorf("Wrong cronJob scheduled timestamp annotation, expexted %s, got %s.", tt.expectedScheduledTime().In(timeZoneLocation).Format(time.RFC3339), scheduledAnnotation)
   150  			}
   151  		})
   152  	}
   153  }
   154  
   155  func TestNextScheduleTime(t *testing.T) {
   156  	logger, _ := ktesting.NewTestContext(t)
   157  	// schedule is hourly on the hour
   158  	schedule := "0 * * * ?"
   159  
   160  	ParseSchedule := func(schedule string) cron.Schedule {
   161  		sched, err := cron.ParseStandard(schedule)
   162  		if err != nil {
   163  			t.Errorf("Error parsing schedule: %#v", err)
   164  			return nil
   165  		}
   166  		return sched
   167  	}
   168  	recorder := record.NewFakeRecorder(50)
   169  	// T1 is a scheduled start time of that schedule
   170  	T1 := *topOfTheHour()
   171  	// T2 is a scheduled start time of that schedule after T1
   172  	T2 := *deltaTimeAfterTopOfTheHour(1 * time.Hour)
   173  
   174  	cj := batchv1.CronJob{
   175  		ObjectMeta: metav1.ObjectMeta{
   176  			Name:      "mycronjob",
   177  			Namespace: metav1.NamespaceDefault,
   178  			UID:       types.UID("1a2b3c"),
   179  		},
   180  		Spec: batchv1.CronJobSpec{
   181  			Schedule:          schedule,
   182  			ConcurrencyPolicy: batchv1.AllowConcurrent,
   183  			JobTemplate:       batchv1.JobTemplateSpec{},
   184  		},
   185  	}
   186  	{
   187  		// Case 1: no known start times, and none needed yet.
   188  		// Creation time is before T1.
   189  		cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
   190  		// Current time is more than creation time, but less than T1.
   191  		now := T1.Add(-7 * time.Minute)
   192  		schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder)
   193  		if schedule != nil {
   194  			t.Errorf("expected no start time, got:  %v", schedule)
   195  		}
   196  	}
   197  	{
   198  		// Case 2: no known start times, and one needed.
   199  		// Creation time is before T1.
   200  		cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
   201  		// Current time is after T1
   202  		now := T1.Add(2 * time.Second)
   203  		schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder)
   204  		if schedule == nil {
   205  			t.Errorf("expected 1 start time, got nil")
   206  		} else if !schedule.Equal(T1) {
   207  			t.Errorf("expected: %v, got: %v", T1, schedule)
   208  		}
   209  	}
   210  	{
   211  		// Case 3: known LastScheduleTime, no start needed.
   212  		// Creation time is before T1.
   213  		cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
   214  		// Status shows a start at the expected time.
   215  		cj.Status.LastScheduleTime = &metav1.Time{Time: T1}
   216  		// Current time is after T1
   217  		now := T1.Add(2 * time.Minute)
   218  		schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder)
   219  		if schedule != nil {
   220  			t.Errorf("expected 0 start times, got: %v", schedule)
   221  		}
   222  	}
   223  	{
   224  		// Case 4: known LastScheduleTime, a start needed
   225  		// Creation time is before T1.
   226  		cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
   227  		// Status shows a start at the expected time.
   228  		cj.Status.LastScheduleTime = &metav1.Time{Time: T1}
   229  		// Current time is after T1 and after T2
   230  		now := T2.Add(5 * time.Minute)
   231  		schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder)
   232  		if schedule == nil {
   233  			t.Errorf("expected 1 start times, got nil")
   234  		} else if !schedule.Equal(T2) {
   235  			t.Errorf("expected: %v, got: %v", T2, schedule)
   236  		}
   237  	}
   238  	{
   239  		// Case 5: known LastScheduleTime, two starts needed
   240  		cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-2 * time.Hour)}
   241  		cj.Status.LastScheduleTime = &metav1.Time{Time: T1.Add(-1 * time.Hour)}
   242  		// Current time is after T1 and after T2
   243  		now := T2.Add(5 * time.Minute)
   244  		schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder)
   245  		if schedule == nil {
   246  			t.Errorf("expected 1 start times, got nil")
   247  		} else if !schedule.Equal(T2) {
   248  			t.Errorf("expected: %v, got: %v", T2, schedule)
   249  		}
   250  	}
   251  	{
   252  		// Case 6: now is way way ahead of last start time, and there is no deadline.
   253  		cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-2 * time.Hour)}
   254  		cj.Status.LastScheduleTime = &metav1.Time{Time: T1.Add(-1 * time.Hour)}
   255  		now := T2.Add(10 * 24 * time.Hour)
   256  		schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder)
   257  		if schedule == nil {
   258  			t.Errorf("expected more than 0 missed times")
   259  		}
   260  	}
   261  	{
   262  		// Case 7: now is way way ahead of last start time, but there is a short deadline.
   263  		cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-2 * time.Hour)}
   264  		cj.Status.LastScheduleTime = &metav1.Time{Time: T1.Add(-1 * time.Hour)}
   265  		now := T2.Add(10 * 24 * time.Hour)
   266  		// Deadline is short
   267  		deadline := int64(2 * 60 * 60)
   268  		cj.Spec.StartingDeadlineSeconds = &deadline
   269  		schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder)
   270  		if schedule == nil {
   271  			t.Errorf("expected more than 0 missed times")
   272  		}
   273  	}
   274  	{
   275  		// Case 8: ensure the error from mostRecentScheduleTime gets populated up
   276  		cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(10 * time.Second)}
   277  		cj.Status.LastScheduleTime = nil
   278  		now := *deltaTimeAfterTopOfTheHour(1 * time.Hour)
   279  		// rouge schedule
   280  		schedule, err := nextScheduleTime(logger, &cj, now, ParseSchedule("59 23 31 2 *"), recorder)
   281  		if schedule != nil {
   282  			t.Errorf("expected no start time, got:  %v", schedule)
   283  		}
   284  		if err == nil {
   285  			t.Errorf("expected error")
   286  		}
   287  	}
   288  }
   289  
   290  func TestByJobStartTime(t *testing.T) {
   291  	now := metav1.NewTime(time.Date(2018, time.January, 1, 2, 3, 4, 5, time.UTC))
   292  	later := metav1.NewTime(time.Date(2019, time.January, 1, 2, 3, 4, 5, time.UTC))
   293  	aNil := &batchv1.Job{
   294  		ObjectMeta: metav1.ObjectMeta{Name: "a"},
   295  		Status:     batchv1.JobStatus{},
   296  	}
   297  	bNil := &batchv1.Job{
   298  		ObjectMeta: metav1.ObjectMeta{Name: "b"},
   299  		Status:     batchv1.JobStatus{},
   300  	}
   301  	aSet := &batchv1.Job{
   302  		ObjectMeta: metav1.ObjectMeta{Name: "a"},
   303  		Status:     batchv1.JobStatus{StartTime: &now},
   304  	}
   305  	bSet := &batchv1.Job{
   306  		ObjectMeta: metav1.ObjectMeta{Name: "b"},
   307  		Status:     batchv1.JobStatus{StartTime: &now},
   308  	}
   309  	aSetLater := &batchv1.Job{
   310  		ObjectMeta: metav1.ObjectMeta{Name: "a"},
   311  		Status:     batchv1.JobStatus{StartTime: &later},
   312  	}
   313  
   314  	testCases := []struct {
   315  		name            string
   316  		input, expected []*batchv1.Job
   317  	}{
   318  		{
   319  			name:     "both have nil start times",
   320  			input:    []*batchv1.Job{bNil, aNil},
   321  			expected: []*batchv1.Job{aNil, bNil},
   322  		},
   323  		{
   324  			name:     "only the first has a nil start time",
   325  			input:    []*batchv1.Job{aNil, bSet},
   326  			expected: []*batchv1.Job{bSet, aNil},
   327  		},
   328  		{
   329  			name:     "only the second has a nil start time",
   330  			input:    []*batchv1.Job{aSet, bNil},
   331  			expected: []*batchv1.Job{aSet, bNil},
   332  		},
   333  		{
   334  			name:     "both have non-nil, equal start time",
   335  			input:    []*batchv1.Job{bSet, aSet},
   336  			expected: []*batchv1.Job{aSet, bSet},
   337  		},
   338  		{
   339  			name:     "both have non-nil, different start time",
   340  			input:    []*batchv1.Job{aSetLater, bSet},
   341  			expected: []*batchv1.Job{bSet, aSetLater},
   342  		},
   343  	}
   344  
   345  	for _, testCase := range testCases {
   346  		sort.Sort(byJobStartTime(testCase.input))
   347  		if !reflect.DeepEqual(testCase.input, testCase.expected) {
   348  			t.Errorf("case: '%s', jobs not sorted as expected", testCase.name)
   349  		}
   350  	}
   351  }
   352  
   353  func TestMostRecentScheduleTime(t *testing.T) {
   354  	metav1TopOfTheHour := metav1.NewTime(*topOfTheHour())
   355  	metav1HalfPastTheHour := metav1.NewTime(*deltaTimeAfterTopOfTheHour(30 * time.Minute))
   356  	metav1MinuteAfterTopOfTheHour := metav1.NewTime(*deltaTimeAfterTopOfTheHour(1 * time.Minute))
   357  	oneMinute := int64(60)
   358  	tenSeconds := int64(10)
   359  
   360  	tests := []struct {
   361  		name                  string
   362  		cj                    *batchv1.CronJob
   363  		includeSDS            bool
   364  		now                   time.Time
   365  		expectedEarliestTime  time.Time
   366  		expectedRecentTime    *time.Time
   367  		expectedTooManyMissed missedSchedulesType
   368  		wantErr               bool
   369  	}{
   370  		{
   371  			name: "now before next schedule",
   372  			cj: &batchv1.CronJob{
   373  				ObjectMeta: metav1.ObjectMeta{
   374  					CreationTimestamp: metav1TopOfTheHour,
   375  				},
   376  				Spec: batchv1.CronJobSpec{
   377  					Schedule: "0 * * * *",
   378  				},
   379  			},
   380  			now:                  topOfTheHour().Add(30 * time.Second),
   381  			expectedRecentTime:   nil,
   382  			expectedEarliestTime: *topOfTheHour(),
   383  		},
   384  		{
   385  			name: "now just after next schedule",
   386  			cj: &batchv1.CronJob{
   387  				ObjectMeta: metav1.ObjectMeta{
   388  					CreationTimestamp: metav1TopOfTheHour,
   389  				},
   390  				Spec: batchv1.CronJobSpec{
   391  					Schedule: "0 * * * *",
   392  				},
   393  			},
   394  			now:                  topOfTheHour().Add(61 * time.Minute),
   395  			expectedRecentTime:   deltaTimeAfterTopOfTheHour(60 * time.Minute),
   396  			expectedEarliestTime: *topOfTheHour(),
   397  		},
   398  		{
   399  			name: "missed 5 schedules",
   400  			cj: &batchv1.CronJob{
   401  				ObjectMeta: metav1.ObjectMeta{
   402  					CreationTimestamp: metav1.NewTime(*deltaTimeAfterTopOfTheHour(10 * time.Second)),
   403  				},
   404  				Spec: batchv1.CronJobSpec{
   405  					Schedule: "0 * * * *",
   406  				},
   407  			},
   408  			now:                   *deltaTimeAfterTopOfTheHour(301 * time.Minute),
   409  			expectedRecentTime:    deltaTimeAfterTopOfTheHour(300 * time.Minute),
   410  			expectedEarliestTime:  *deltaTimeAfterTopOfTheHour(10 * time.Second),
   411  			expectedTooManyMissed: fewMissed,
   412  		},
   413  		{
   414  			name: "complex schedule",
   415  			cj: &batchv1.CronJob{
   416  				ObjectMeta: metav1.ObjectMeta{
   417  					CreationTimestamp: metav1TopOfTheHour,
   418  				},
   419  				Spec: batchv1.CronJobSpec{
   420  					Schedule: "30 6-16/4 * * 1-5",
   421  				},
   422  				Status: batchv1.CronJobStatus{
   423  					LastScheduleTime: &metav1HalfPastTheHour,
   424  				},
   425  			},
   426  			now:                   *deltaTimeAfterTopOfTheHour(24*time.Hour + 31*time.Minute),
   427  			expectedRecentTime:    deltaTimeAfterTopOfTheHour(24*time.Hour + 30*time.Minute),
   428  			expectedEarliestTime:  *deltaTimeAfterTopOfTheHour(30 * time.Minute),
   429  			expectedTooManyMissed: fewMissed,
   430  		},
   431  		{
   432  			name: "another complex schedule",
   433  			cj: &batchv1.CronJob{
   434  				ObjectMeta: metav1.ObjectMeta{
   435  					CreationTimestamp: metav1TopOfTheHour,
   436  				},
   437  				Spec: batchv1.CronJobSpec{
   438  					Schedule: "30 10,11,12 * * 1-5",
   439  				},
   440  				Status: batchv1.CronJobStatus{
   441  					LastScheduleTime: &metav1HalfPastTheHour,
   442  				},
   443  			},
   444  			now:                   *deltaTimeAfterTopOfTheHour(30*time.Hour + 30*time.Minute),
   445  			expectedRecentTime:    nil,
   446  			expectedEarliestTime:  *deltaTimeAfterTopOfTheHour(30 * time.Minute),
   447  			expectedTooManyMissed: fewMissed,
   448  		},
   449  		{
   450  			name: "complex schedule with longer diff between executions",
   451  			cj: &batchv1.CronJob{
   452  				ObjectMeta: metav1.ObjectMeta{
   453  					CreationTimestamp: metav1TopOfTheHour,
   454  				},
   455  				Spec: batchv1.CronJobSpec{
   456  					Schedule: "30 6-16/4 * * 1-5",
   457  				},
   458  				Status: batchv1.CronJobStatus{
   459  					LastScheduleTime: &metav1HalfPastTheHour,
   460  				},
   461  			},
   462  			now:                   *deltaTimeAfterTopOfTheHour(96*time.Hour + 31*time.Minute),
   463  			expectedRecentTime:    deltaTimeAfterTopOfTheHour(96*time.Hour + 30*time.Minute),
   464  			expectedEarliestTime:  *deltaTimeAfterTopOfTheHour(30 * time.Minute),
   465  			expectedTooManyMissed: fewMissed,
   466  		},
   467  		{
   468  			name: "complex schedule with shorter diff between executions",
   469  			cj: &batchv1.CronJob{
   470  				ObjectMeta: metav1.ObjectMeta{
   471  					CreationTimestamp: metav1TopOfTheHour,
   472  				},
   473  				Spec: batchv1.CronJobSpec{
   474  					Schedule: "30 6-16/4 * * 1-5",
   475  				},
   476  			},
   477  			now:                   *deltaTimeAfterTopOfTheHour(24*time.Hour + 31*time.Minute),
   478  			expectedRecentTime:    deltaTimeAfterTopOfTheHour(24*time.Hour + 30*time.Minute),
   479  			expectedEarliestTime:  *topOfTheHour(),
   480  			expectedTooManyMissed: fewMissed,
   481  		},
   482  		{
   483  			name: "@every schedule",
   484  			cj: &batchv1.CronJob{
   485  				ObjectMeta: metav1.ObjectMeta{
   486  					CreationTimestamp: metav1.NewTime(*deltaTimeAfterTopOfTheHour(-59 * time.Minute)),
   487  				},
   488  				Spec: batchv1.CronJobSpec{
   489  					Schedule:                "@every 1h",
   490  					StartingDeadlineSeconds: &tenSeconds,
   491  				},
   492  				Status: batchv1.CronJobStatus{
   493  					LastScheduleTime: &metav1MinuteAfterTopOfTheHour,
   494  				},
   495  			},
   496  			now:                   *deltaTimeAfterTopOfTheHour(7 * 24 * time.Hour),
   497  			expectedRecentTime:    deltaTimeAfterTopOfTheHour((6 * 24 * time.Hour) + 23*time.Hour + 1*time.Minute),
   498  			expectedEarliestTime:  *deltaTimeAfterTopOfTheHour(1 * time.Minute),
   499  			expectedTooManyMissed: manyMissed,
   500  		},
   501  		{
   502  			name: "rogue cronjob",
   503  			cj: &batchv1.CronJob{
   504  				ObjectMeta: metav1.ObjectMeta{
   505  					CreationTimestamp: metav1.NewTime(*deltaTimeAfterTopOfTheHour(10 * time.Second)),
   506  				},
   507  				Spec: batchv1.CronJobSpec{
   508  					Schedule: "59 23 31 2 *",
   509  				},
   510  			},
   511  			now:                *deltaTimeAfterTopOfTheHour(1 * time.Hour),
   512  			expectedRecentTime: nil,
   513  			wantErr:            true,
   514  		},
   515  		{
   516  			name: "earliestTime being CreationTimestamp and LastScheduleTime",
   517  			cj: &batchv1.CronJob{
   518  				ObjectMeta: metav1.ObjectMeta{
   519  					CreationTimestamp: metav1TopOfTheHour,
   520  				},
   521  				Spec: batchv1.CronJobSpec{
   522  					Schedule: "0 * * * *",
   523  				},
   524  				Status: batchv1.CronJobStatus{
   525  					LastScheduleTime: &metav1TopOfTheHour,
   526  				},
   527  			},
   528  			now:                  *deltaTimeAfterTopOfTheHour(30 * time.Second),
   529  			expectedEarliestTime: *topOfTheHour(),
   530  			expectedRecentTime:   nil,
   531  		},
   532  		{
   533  			name: "earliestTime being LastScheduleTime",
   534  			cj: &batchv1.CronJob{
   535  				ObjectMeta: metav1.ObjectMeta{
   536  					CreationTimestamp: metav1TopOfTheHour,
   537  				},
   538  				Spec: batchv1.CronJobSpec{
   539  					Schedule: "*/5 * * * *",
   540  				},
   541  				Status: batchv1.CronJobStatus{
   542  					LastScheduleTime: &metav1HalfPastTheHour,
   543  				},
   544  			},
   545  			now:                  *deltaTimeAfterTopOfTheHour(31 * time.Minute),
   546  			expectedEarliestTime: *deltaTimeAfterTopOfTheHour(30 * time.Minute),
   547  			expectedRecentTime:   nil,
   548  		},
   549  		{
   550  			name: "earliestTime being LastScheduleTime (within StartingDeadlineSeconds)",
   551  			cj: &batchv1.CronJob{
   552  				ObjectMeta: metav1.ObjectMeta{
   553  					CreationTimestamp: metav1TopOfTheHour,
   554  				},
   555  				Spec: batchv1.CronJobSpec{
   556  					Schedule:                "*/5 * * * *",
   557  					StartingDeadlineSeconds: &oneMinute,
   558  				},
   559  				Status: batchv1.CronJobStatus{
   560  					LastScheduleTime: &metav1HalfPastTheHour,
   561  				},
   562  			},
   563  			now:                  *deltaTimeAfterTopOfTheHour(31 * time.Minute),
   564  			expectedEarliestTime: *deltaTimeAfterTopOfTheHour(30 * time.Minute),
   565  			expectedRecentTime:   nil,
   566  		},
   567  		{
   568  			name: "earliestTime being LastScheduleTime (outside StartingDeadlineSeconds)",
   569  			cj: &batchv1.CronJob{
   570  				ObjectMeta: metav1.ObjectMeta{
   571  					CreationTimestamp: metav1TopOfTheHour,
   572  				},
   573  				Spec: batchv1.CronJobSpec{
   574  					Schedule:                "*/5 * * * *",
   575  					StartingDeadlineSeconds: &oneMinute,
   576  				},
   577  				Status: batchv1.CronJobStatus{
   578  					LastScheduleTime: &metav1HalfPastTheHour,
   579  				},
   580  			},
   581  			includeSDS:           true,
   582  			now:                  *deltaTimeAfterTopOfTheHour(32 * time.Minute),
   583  			expectedEarliestTime: *deltaTimeAfterTopOfTheHour(31 * time.Minute),
   584  			expectedRecentTime:   nil,
   585  		},
   586  	}
   587  	for _, tt := range tests {
   588  		t.Run(tt.name, func(t *testing.T) {
   589  			sched, err := cron.ParseStandard(tt.cj.Spec.Schedule)
   590  			if err != nil {
   591  				t.Errorf("error setting up the test, %s", err)
   592  			}
   593  			gotEarliestTime, gotRecentTime, gotTooManyMissed, err := mostRecentScheduleTime(tt.cj, tt.now, sched, tt.includeSDS)
   594  			if tt.wantErr {
   595  				if err == nil {
   596  					t.Error("mostRecentScheduleTime() got no error when expected one")
   597  				}
   598  				return
   599  			}
   600  			if !tt.wantErr && err != nil {
   601  				t.Error("mostRecentScheduleTime() got error when none expected")
   602  			}
   603  			if gotEarliestTime.IsZero() {
   604  				t.Errorf("earliestTime should never be 0, want %v", tt.expectedEarliestTime)
   605  			}
   606  			if !gotEarliestTime.Equal(tt.expectedEarliestTime) {
   607  				t.Errorf("expectedEarliestTime - got %v, want %v", gotEarliestTime, tt.expectedEarliestTime)
   608  			}
   609  			if !reflect.DeepEqual(gotRecentTime, tt.expectedRecentTime) {
   610  				t.Errorf("expectedRecentTime - got %v, want %v", gotRecentTime, tt.expectedRecentTime)
   611  			}
   612  			if gotTooManyMissed != tt.expectedTooManyMissed {
   613  				t.Errorf("expectedNumberOfMisses - got %v, want %v", gotTooManyMissed, tt.expectedTooManyMissed)
   614  			}
   615  		})
   616  	}
   617  }
   618  
   619  func TestNextScheduleTimeDuration(t *testing.T) {
   620  	metav1TopOfTheHour := metav1.NewTime(*topOfTheHour())
   621  	metav1HalfPastTheHour := metav1.NewTime(*deltaTimeAfterTopOfTheHour(30 * time.Minute))
   622  	metav1TwoHoursLater := metav1.NewTime(*deltaTimeAfterTopOfTheHour(2 * time.Hour))
   623  
   624  	tests := []struct {
   625  		name             string
   626  		cj               *batchv1.CronJob
   627  		now              time.Time
   628  		expectedDuration time.Duration
   629  	}{
   630  		{
   631  			name: "complex schedule skipping weekend",
   632  			cj: &batchv1.CronJob{
   633  				ObjectMeta: metav1.ObjectMeta{
   634  					CreationTimestamp: metav1TopOfTheHour,
   635  				},
   636  				Spec: batchv1.CronJobSpec{
   637  					Schedule: "30 6-16/4 * * 1-5",
   638  				},
   639  				Status: batchv1.CronJobStatus{
   640  					LastScheduleTime: &metav1HalfPastTheHour,
   641  				},
   642  			},
   643  			now:              *deltaTimeAfterTopOfTheHour(24*time.Hour + 31*time.Minute),
   644  			expectedDuration: 3*time.Hour + 59*time.Minute + nextScheduleDelta,
   645  		},
   646  		{
   647  			name: "another complex schedule skipping weekend",
   648  			cj: &batchv1.CronJob{
   649  				ObjectMeta: metav1.ObjectMeta{
   650  					CreationTimestamp: metav1TopOfTheHour,
   651  				},
   652  				Spec: batchv1.CronJobSpec{
   653  					Schedule: "30 10,11,12 * * 1-5",
   654  				},
   655  				Status: batchv1.CronJobStatus{
   656  					LastScheduleTime: &metav1HalfPastTheHour,
   657  				},
   658  			},
   659  			now:              *deltaTimeAfterTopOfTheHour(30*time.Hour + 30*time.Minute),
   660  			expectedDuration: 66*time.Hour + nextScheduleDelta,
   661  		},
   662  		{
   663  			name: "once a week cronjob, missed two runs",
   664  			cj: &batchv1.CronJob{
   665  				ObjectMeta: metav1.ObjectMeta{
   666  					CreationTimestamp: metav1TopOfTheHour,
   667  				},
   668  				Spec: batchv1.CronJobSpec{
   669  					Schedule: "0 12 * * 4",
   670  				},
   671  				Status: batchv1.CronJobStatus{
   672  					LastScheduleTime: &metav1TwoHoursLater,
   673  				},
   674  			},
   675  			now:              *deltaTimeAfterTopOfTheHour(19*24*time.Hour + 1*time.Hour + 30*time.Minute),
   676  			expectedDuration: 48*time.Hour + 30*time.Minute + nextScheduleDelta,
   677  		},
   678  		{
   679  			name: "no previous run of a cronjob",
   680  			cj: &batchv1.CronJob{
   681  				ObjectMeta: metav1.ObjectMeta{
   682  					CreationTimestamp: metav1TopOfTheHour,
   683  				},
   684  				Spec: batchv1.CronJobSpec{
   685  					Schedule: "0 12 * * 5",
   686  				},
   687  			},
   688  			now:              *deltaTimeAfterTopOfTheHour(6 * time.Hour),
   689  			expectedDuration: 20*time.Hour + nextScheduleDelta,
   690  		},
   691  	}
   692  	for _, tt := range tests {
   693  		t.Run(tt.name, func(t *testing.T) {
   694  			sched, err := cron.ParseStandard(tt.cj.Spec.Schedule)
   695  			if err != nil {
   696  				t.Errorf("error setting up the test, %s", err)
   697  			}
   698  			gotScheduleTimeDuration := nextScheduleTimeDuration(tt.cj, tt.now, sched)
   699  			if *gotScheduleTimeDuration < 0 {
   700  				t.Errorf("scheduleTimeDuration should never be less than 0, got %s", gotScheduleTimeDuration)
   701  			}
   702  			if !reflect.DeepEqual(gotScheduleTimeDuration, &tt.expectedDuration) {
   703  				t.Errorf("scheduleTimeDuration - got %s, want %s", gotScheduleTimeDuration, tt.expectedDuration)
   704  			}
   705  		})
   706  	}
   707  }
   708  
   709  func TestIsJobSucceeded(t *testing.T) {
   710  	tests := map[string]struct {
   711  		job        batchv1.Job
   712  		wantResult bool
   713  	}{
   714  		"job doesn't have any conditions": {
   715  			wantResult: false,
   716  		},
   717  		"job has Complete=True condition": {
   718  			job: batchv1.Job{
   719  				Status: batchv1.JobStatus{
   720  					Conditions: []batchv1.JobCondition{
   721  						{
   722  							Type:   batchv1.JobSuspended,
   723  							Status: v1.ConditionFalse,
   724  						},
   725  						{
   726  							Type:   batchv1.JobComplete,
   727  							Status: v1.ConditionTrue,
   728  						},
   729  					},
   730  				},
   731  			},
   732  			wantResult: true,
   733  		},
   734  		"job has Complete=False condition": {
   735  			job: batchv1.Job{
   736  				Status: batchv1.JobStatus{
   737  					Conditions: []batchv1.JobCondition{
   738  						{
   739  							Type:   batchv1.JobFailed,
   740  							Status: v1.ConditionTrue,
   741  						},
   742  						{
   743  							Type:   batchv1.JobComplete,
   744  							Status: v1.ConditionFalse,
   745  						},
   746  					},
   747  				},
   748  			},
   749  			wantResult: false,
   750  		},
   751  	}
   752  	for name, tc := range tests {
   753  		t.Run(name, func(t *testing.T) {
   754  			gotResult := IsJobSucceeded(&tc.job)
   755  			if tc.wantResult != gotResult {
   756  				t.Errorf("unexpected result, want=%v, got=%v", tc.wantResult, gotResult)
   757  			}
   758  		})
   759  	}
   760  }
   761  
   762  func topOfTheHour() *time.Time {
   763  	T1, err := time.Parse(time.RFC3339, "2016-05-19T10:00:00Z")
   764  	if err != nil {
   765  		panic("test setup error")
   766  	}
   767  	return &T1
   768  }
   769  
   770  func deltaTimeAfterTopOfTheHour(duration time.Duration) *time.Time {
   771  	T1, err := time.Parse(time.RFC3339, "2016-05-19T10:00:00Z")
   772  	if err != nil {
   773  		panic("test setup error")
   774  	}
   775  	t := T1.Add(duration)
   776  	return &t
   777  }
   778  

View as plain text