mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 02:08:13 +00:00 
			
		
		
		
	TimeZone support for CronJobs
This commit is contained in:
		| @@ -22,6 +22,7 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	_ "time/tzdata" // for CronJob Time Zone support | ||||
|  | ||||
| 	"k8s.io/component-base/cli" | ||||
| 	_ "k8s.io/component-base/logs/json/register"          // for JSON log format registration | ||||
|   | ||||
| @@ -376,6 +376,12 @@ type CronJobSpec struct { | ||||
| 	// The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. | ||||
| 	Schedule string | ||||
|  | ||||
| 	// The time zone for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. | ||||
| 	// If not specified, this will rely on the time zone of the kube-controller-manager process. | ||||
| 	// ALPHA: This field is in alpha and must be enabled via the `CronJobTimeZone` feature gate. | ||||
| 	// +optional | ||||
| 	TimeZone *string | ||||
|  | ||||
| 	// Optional deadline in seconds for starting the job if it misses scheduled | ||||
| 	// time for any reason.  Missed jobs executions will be counted as failed ones. | ||||
| 	// +optional | ||||
|   | ||||
| @@ -18,6 +18,8 @@ package validation | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/robfig/cron/v3" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| @@ -308,11 +310,14 @@ func ValidateCronJobSpec(spec *batch.CronJobSpec, fldPath *field.Path, opts apiv | ||||
| 	if len(spec.Schedule) == 0 { | ||||
| 		allErrs = append(allErrs, field.Required(fldPath.Child("schedule"), "")) | ||||
| 	} else { | ||||
| 		allErrs = append(allErrs, validateScheduleFormat(spec.Schedule, fldPath.Child("schedule"))...) | ||||
| 		allErrs = append(allErrs, validateScheduleFormat(spec.Schedule, spec.TimeZone, fldPath.Child("schedule"))...) | ||||
| 	} | ||||
|  | ||||
| 	if spec.StartingDeadlineSeconds != nil { | ||||
| 		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.StartingDeadlineSeconds), fldPath.Child("startingDeadlineSeconds"))...) | ||||
| 	} | ||||
|  | ||||
| 	allErrs = append(allErrs, validateTimeZone(spec.TimeZone, fldPath.Child("timeZone"))...) | ||||
| 	allErrs = append(allErrs, validateConcurrencyPolicy(&spec.ConcurrencyPolicy, fldPath.Child("concurrencyPolicy"))...) | ||||
| 	allErrs = append(allErrs, ValidateJobTemplateSpec(&spec.JobTemplate, fldPath.Child("jobTemplate"), opts)...) | ||||
|  | ||||
| @@ -343,11 +348,36 @@ func validateConcurrencyPolicy(concurrencyPolicy *batch.ConcurrencyPolicy, fldPa | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| func validateScheduleFormat(schedule string, fldPath *field.Path) field.ErrorList { | ||||
| func validateScheduleFormat(schedule string, timeZone *string, fldPath *field.Path) field.ErrorList { | ||||
| 	allErrs := field.ErrorList{} | ||||
| 	if _, err := cron.ParseStandard(schedule); err != nil { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath, schedule, err.Error())) | ||||
| 	} | ||||
| 	if strings.Contains(schedule, "TZ") && timeZone != nil { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath, schedule, "cannot use both timeZone field and TZ or CRON_TZ in schedule")) | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| func validateTimeZone(timeZone *string, fldPath *field.Path) field.ErrorList { | ||||
| 	allErrs := field.ErrorList{} | ||||
| 	if timeZone == nil { | ||||
| 		return allErrs | ||||
| 	} | ||||
|  | ||||
| 	if len(*timeZone) == 0 { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath, timeZone, "timeZone must be nil or non-empty string")) | ||||
| 		return allErrs | ||||
| 	} | ||||
|  | ||||
| 	if strings.EqualFold(*timeZone, "Local") { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath, timeZone, "timeZone must be an explicit time zone as defined in https://www.iana.org/time-zones")) | ||||
| 	} | ||||
|  | ||||
| 	if _, err := time.LoadLocation(*timeZone); err != nil { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath, timeZone, err.Error())) | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|   | ||||
| @@ -31,6 +31,18 @@ import ( | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	timeZoneEmpty         = "" | ||||
| 	timeZoneLocal         = "LOCAL" | ||||
| 	timeZoneUTC           = "UTC" | ||||
| 	timeZoneCorrectCasing = "America/New_York" | ||||
| 	timeZoneBadCasing     = "AMERICA/new_york" | ||||
| 	timeZoneBadPrefix     = " America/New_York" | ||||
| 	timeZoneBadSuffix     = "America/New_York " | ||||
| 	timeZoneBadName       = "America/New York" | ||||
| 	timeZoneEmptySpace    = " " | ||||
| ) | ||||
|  | ||||
| var ignoreErrValueDetail = cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail") | ||||
|  | ||||
| func getValidManualSelector() *metav1.LabelSelector { | ||||
| @@ -902,6 +914,23 @@ func TestValidateCronJob(t *testing.T) { | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"correct timeZone value casing": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{ | ||||
| 				Name:      "mycronjob", | ||||
| 				Namespace: metav1.NamespaceDefault, | ||||
| 				UID:       types.UID("1a2b3c"), | ||||
| 			}, | ||||
| 			Spec: batch.CronJobSpec{ | ||||
| 				Schedule:          "0 * * * *", | ||||
| 				TimeZone:          &timeZoneCorrectCasing, | ||||
| 				ConcurrencyPolicy: batch.AllowConcurrent, | ||||
| 				JobTemplate: batch.JobTemplateSpec{ | ||||
| 					Spec: batch.JobSpec{ | ||||
| 						Template: validPodTemplateSpec, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for k, v := range successCases { | ||||
| 		if errs := ValidateCronJob(&v, corevalidation.PodValidationOptions{}); len(errs) != 0 { | ||||
| @@ -953,6 +982,142 @@ func TestValidateCronJob(t *testing.T) { | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"spec.schedule: cannot use both timeZone field and TZ or CRON_TZ in schedule": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{ | ||||
| 				Name:      "mycronjob", | ||||
| 				Namespace: metav1.NamespaceDefault, | ||||
| 				UID:       types.UID("1a2b3c"), | ||||
| 			}, | ||||
| 			Spec: batch.CronJobSpec{ | ||||
| 				Schedule:          "TZ=UTC 0 * * * *", | ||||
| 				TimeZone:          &timeZoneUTC, | ||||
| 				ConcurrencyPolicy: batch.AllowConcurrent, | ||||
| 				JobTemplate: batch.JobTemplateSpec{ | ||||
| 					Spec: batch.JobSpec{ | ||||
| 						Template: validPodTemplateSpec, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"spec.timeZone: timeZone must be nil or non-empty string": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{ | ||||
| 				Name:      "mycronjob", | ||||
| 				Namespace: metav1.NamespaceDefault, | ||||
| 				UID:       types.UID("1a2b3c"), | ||||
| 			}, | ||||
| 			Spec: batch.CronJobSpec{ | ||||
| 				Schedule:          "0 * * * *", | ||||
| 				TimeZone:          &timeZoneEmpty, | ||||
| 				ConcurrencyPolicy: batch.AllowConcurrent, | ||||
| 				JobTemplate: batch.JobTemplateSpec{ | ||||
| 					Spec: batch.JobSpec{ | ||||
| 						Template: validPodTemplateSpec, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"spec.timeZone: timeZone must be an explicit time zone as defined in https://www.iana.org/time-zones": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{ | ||||
| 				Name:      "mycronjob", | ||||
| 				Namespace: metav1.NamespaceDefault, | ||||
| 				UID:       types.UID("1a2b3c"), | ||||
| 			}, | ||||
| 			Spec: batch.CronJobSpec{ | ||||
| 				Schedule:          "0 * * * *", | ||||
| 				TimeZone:          &timeZoneLocal, | ||||
| 				ConcurrencyPolicy: batch.AllowConcurrent, | ||||
| 				JobTemplate: batch.JobTemplateSpec{ | ||||
| 					Spec: batch.JobSpec{ | ||||
| 						Template: validPodTemplateSpec, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"spec.timeZone: Invalid value: \"AMERICA/new_york\": unknown time zone AMERICA/new_york": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{ | ||||
| 				Name:      "mycronjob", | ||||
| 				Namespace: metav1.NamespaceDefault, | ||||
| 				UID:       types.UID("1a2b3c"), | ||||
| 			}, | ||||
| 			Spec: batch.CronJobSpec{ | ||||
| 				Schedule:          "0 * * * *", | ||||
| 				TimeZone:          &timeZoneBadCasing, | ||||
| 				ConcurrencyPolicy: batch.AllowConcurrent, | ||||
| 				JobTemplate: batch.JobTemplateSpec{ | ||||
| 					Spec: batch.JobSpec{ | ||||
| 						Template: validPodTemplateSpec, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"spec.timeZone: Invalid value: \" America/New_York\": unknown time zone  America/New_York": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{ | ||||
| 				Name:      "mycronjob", | ||||
| 				Namespace: metav1.NamespaceDefault, | ||||
| 				UID:       types.UID("1a2b3c"), | ||||
| 			}, | ||||
| 			Spec: batch.CronJobSpec{ | ||||
| 				Schedule:          "0 * * * *", | ||||
| 				TimeZone:          &timeZoneBadPrefix, | ||||
| 				ConcurrencyPolicy: batch.AllowConcurrent, | ||||
| 				JobTemplate: batch.JobTemplateSpec{ | ||||
| 					Spec: batch.JobSpec{ | ||||
| 						Template: validPodTemplateSpec, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"spec.timeZone: Invalid value: \"America/New_York \": unknown time zone America/New_York ": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{ | ||||
| 				Name:      "mycronjob", | ||||
| 				Namespace: metav1.NamespaceDefault, | ||||
| 				UID:       types.UID("1a2b3c"), | ||||
| 			}, | ||||
| 			Spec: batch.CronJobSpec{ | ||||
| 				Schedule:          "0 * * * *", | ||||
| 				TimeZone:          &timeZoneBadSuffix, | ||||
| 				ConcurrencyPolicy: batch.AllowConcurrent, | ||||
| 				JobTemplate: batch.JobTemplateSpec{ | ||||
| 					Spec: batch.JobSpec{ | ||||
| 						Template: validPodTemplateSpec, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"spec.timeZone: Invalid value: \"America/New York\": unknown time zone  America/New York": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{ | ||||
| 				Name:      "mycronjob", | ||||
| 				Namespace: metav1.NamespaceDefault, | ||||
| 				UID:       types.UID("1a2b3c"), | ||||
| 			}, | ||||
| 			Spec: batch.CronJobSpec{ | ||||
| 				Schedule:          "0 * * * *", | ||||
| 				TimeZone:          &timeZoneBadName, | ||||
| 				ConcurrencyPolicy: batch.AllowConcurrent, | ||||
| 				JobTemplate: batch.JobTemplateSpec{ | ||||
| 					Spec: batch.JobSpec{ | ||||
| 						Template: validPodTemplateSpec, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"spec.timeZone: Invalid value: \" \": unknown time zone  ": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{ | ||||
| 				Name:      "mycronjob", | ||||
| 				Namespace: metav1.NamespaceDefault, | ||||
| 				UID:       types.UID("1a2b3c"), | ||||
| 			}, | ||||
| 			Spec: batch.CronJobSpec{ | ||||
| 				Schedule:          "0 * * * *", | ||||
| 				TimeZone:          &timeZoneEmptySpace, | ||||
| 				ConcurrencyPolicy: batch.AllowConcurrent, | ||||
| 				JobTemplate: batch.JobTemplateSpec{ | ||||
| 					Spec: batch.JobSpec{ | ||||
| 						Template: validPodTemplateSpec, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"spec.startingDeadlineSeconds:must be greater than or equal to 0": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{ | ||||
| 				Name:      "mycronjob", | ||||
|   | ||||
| @@ -35,6 +35,8 @@ import ( | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	"k8s.io/apiserver/pkg/features" | ||||
| 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||
| 	batchv1informers "k8s.io/client-go/informers/batch/v1" | ||||
| 	clientset "k8s.io/client-go/kubernetes" | ||||
| 	"k8s.io/client-go/kubernetes/scheme" | ||||
| @@ -48,6 +50,7 @@ import ( | ||||
| 	"k8s.io/klog/v2" | ||||
| 	"k8s.io/kubernetes/pkg/controller" | ||||
| 	"k8s.io/kubernetes/pkg/controller/cronjob/metrics" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| @@ -371,6 +374,7 @@ func (jm *ControllerV2) enqueueControllerAfter(obj interface{}, t time.Duration) | ||||
| // updateCronJob re-queues the CronJob for next scheduled time if there is a | ||||
| // change in spec.schedule otherwise it re-queues it now | ||||
| func (jm *ControllerV2) updateCronJob(old interface{}, curr interface{}) { | ||||
| 	timeZoneEnabled := utilfeature.DefaultFeatureGate.Enabled(features.CronJobTimeZone) | ||||
| 	oldCJ, okOld := old.(*batchv1.CronJob) | ||||
| 	newCJ, okNew := curr.(*batchv1.CronJob) | ||||
|  | ||||
| @@ -381,9 +385,9 @@ func (jm *ControllerV2) updateCronJob(old interface{}, curr interface{}) { | ||||
| 	// if the change in schedule results in next requeue having to be sooner than it already was, | ||||
| 	// it will be handled here by the queue. If the next requeue is further than previous schedule, | ||||
| 	// the sync loop will essentially be a no-op for the already queued key with old schedule. | ||||
| 	if oldCJ.Spec.Schedule != newCJ.Spec.Schedule { | ||||
| 	if oldCJ.Spec.Schedule != newCJ.Spec.Schedule || (timeZoneEnabled && !pointer.StringEqual(oldCJ.Spec.TimeZone, newCJ.Spec.TimeZone)) { | ||||
| 		// schedule changed, change the requeue time | ||||
| 		sched, err := cron.ParseStandard(newCJ.Spec.Schedule) | ||||
| 		sched, err := cron.ParseStandard(formatSchedule(timeZoneEnabled, newCJ, jm.recorder)) | ||||
| 		if err != nil { | ||||
| 			// this is likely a user error in defining the spec value | ||||
| 			// we should log the error and not reconcile this cronjob until an update to spec | ||||
| @@ -420,6 +424,7 @@ func (jm *ControllerV2) syncCronJob( | ||||
| 	cronJob = cronJob.DeepCopy() | ||||
| 	now := jm.now() | ||||
| 	updateStatus := false | ||||
| 	timeZoneEnabled := utilfeature.DefaultFeatureGate.Enabled(features.CronJobTimeZone) | ||||
|  | ||||
| 	childrenJobs := make(map[types.UID]bool) | ||||
| 	for _, j := range jobs { | ||||
| @@ -487,12 +492,21 @@ func (jm *ControllerV2) syncCronJob( | ||||
| 		return cronJob, nil, updateStatus, nil | ||||
| 	} | ||||
|  | ||||
| 	if timeZoneEnabled && cronJob.Spec.TimeZone != nil { | ||||
| 		if _, err := time.LoadLocation(*cronJob.Spec.TimeZone); err != nil { | ||||
| 			timeZone := pointer.StringDeref(cronJob.Spec.TimeZone, "") | ||||
| 			klog.V(4).InfoS("Not starting job because timeZone is invalid", "cronjob", klog.KRef(cronJob.GetNamespace(), cronJob.GetName()), "timeZone", timeZone, "err", err) | ||||
| 			jm.recorder.Eventf(cronJob, corev1.EventTypeWarning, "UnknownTimeZone", "invalid timeZone: %q: %s", timeZone, err) | ||||
| 			return cronJob, nil, updateStatus, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend { | ||||
| 		klog.V(4).InfoS("Not starting job because the cron is suspended", "cronjob", klog.KRef(cronJob.GetNamespace(), cronJob.GetName())) | ||||
| 		return cronJob, nil, updateStatus, nil | ||||
| 	} | ||||
|  | ||||
| 	sched, err := cron.ParseStandard(cronJob.Spec.Schedule) | ||||
| 	sched, err := cron.ParseStandard(formatSchedule(timeZoneEnabled, cronJob, jm.recorder)) | ||||
| 	if err != nil { | ||||
| 		// this is likely a user error in defining the spec value | ||||
| 		// we should log the error and not reconcile this cronjob until an update to spec | ||||
| @@ -501,10 +515,6 @@ func (jm *ControllerV2) syncCronJob( | ||||
| 		return cronJob, nil, updateStatus, nil | ||||
| 	} | ||||
|  | ||||
| 	if strings.Contains(cronJob.Spec.Schedule, "TZ") { | ||||
| 		jm.recorder.Eventf(cronJob, corev1.EventTypeWarning, "UnsupportedSchedule", "CRON_TZ or TZ used in schedule %q is not officially supported, see https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/ for more details", cronJob.Spec.Schedule) | ||||
| 	} | ||||
|  | ||||
| 	scheduledTime, err := getNextScheduleTime(*cronJob, now, sched, jm.recorder) | ||||
| 	if err != nil { | ||||
| 		// this is likely a user error in defining the spec value | ||||
| @@ -739,3 +749,20 @@ func deleteJob(cj *batchv1.CronJob, job *batchv1.Job, jc jobControlInterface, re | ||||
| func getRef(object runtime.Object) (*corev1.ObjectReference, error) { | ||||
| 	return ref.GetReference(scheme.Scheme, object) | ||||
| } | ||||
|  | ||||
| func formatSchedule(timeZoneEnabled bool, cj *batchv1.CronJob, recorder record.EventRecorder) string { | ||||
| 	if strings.Contains(cj.Spec.Schedule, "TZ") { | ||||
| 		recorder.Eventf(cj, corev1.EventTypeWarning, "UnsupportedSchedule", "CRON_TZ or TZ used in schedule %q is not officially supported, see https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/ for more details", cj.Spec.Schedule) | ||||
| 		return cj.Spec.Schedule | ||||
| 	} | ||||
|  | ||||
| 	if timeZoneEnabled && cj.Spec.TimeZone != nil { | ||||
| 		if _, err := time.LoadLocation(*cj.Spec.TimeZone); err != nil { | ||||
| 			return cj.Spec.Schedule | ||||
| 		} | ||||
|  | ||||
| 		return fmt.Sprintf("TZ=%s %s", *cj.Spec.TimeZone, cj.Spec.Schedule) | ||||
| 	} | ||||
|  | ||||
| 	return cj.Spec.Schedule | ||||
| } | ||||
|   | ||||
| @@ -32,10 +32,13 @@ import ( | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	"k8s.io/apiserver/pkg/features" | ||||
| 	"k8s.io/apiserver/pkg/util/feature" | ||||
| 	"k8s.io/client-go/informers" | ||||
| 	"k8s.io/client-go/kubernetes/fake" | ||||
| 	"k8s.io/client-go/tools/record" | ||||
| 	"k8s.io/client-go/util/workqueue" | ||||
| 	featuregatetesting "k8s.io/component-base/featuregate/testing" | ||||
| 	_ "k8s.io/kubernetes/pkg/apis/batch/install" | ||||
| 	_ "k8s.io/kubernetes/pkg/apis/core/install" | ||||
| 	"k8s.io/kubernetes/pkg/controller" | ||||
| @@ -50,6 +53,9 @@ var ( | ||||
| 	errorSchedule = "obvious error schedule" | ||||
| 	// schedule is hourly on the hour | ||||
| 	onTheHour = "0 * * * ?" | ||||
|  | ||||
| 	errorTimeZone = "bad timezone" | ||||
| 	newYork       = "America/New_York" | ||||
| ) | ||||
|  | ||||
| // returns a cronJob with some fields filled in. | ||||
| @@ -127,6 +133,19 @@ func justAfterTheHour() *time.Time { | ||||
| 	return &T1 | ||||
| } | ||||
|  | ||||
| func justAfterTheHourInZone(tz string) time.Time { | ||||
| 	location, err := time.LoadLocation(tz) | ||||
| 	if err != nil { | ||||
| 		panic("tz error: " + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	T1, err := time.ParseInLocation(time.RFC3339, "2016-05-19T10:01:00Z", location) | ||||
| 	if err != nil { | ||||
| 		panic("test setup error: " + err.Error()) | ||||
| 	} | ||||
| 	return T1 | ||||
| } | ||||
|  | ||||
| func justBeforeTheHour() time.Time { | ||||
| 	T1, err := time.Parse(time.RFC3339, "2016-05-19T09:59:00Z") | ||||
| 	if err != nil { | ||||
| @@ -162,6 +181,7 @@ func TestControllerV2SyncCronJob(t *testing.T) { | ||||
| 		concurrencyPolicy batchv1.ConcurrencyPolicy | ||||
| 		suspend           bool | ||||
| 		schedule          string | ||||
| 		timeZone          *string | ||||
| 		deadline          int64 | ||||
|  | ||||
| 		// cj status | ||||
| @@ -173,6 +193,7 @@ func TestControllerV2SyncCronJob(t *testing.T) { | ||||
| 		now             time.Time | ||||
| 		jobCreateError  error | ||||
| 		jobGetErr       error | ||||
| 		enableTimeZone  bool | ||||
|  | ||||
| 		// expectations | ||||
| 		expectCreate               bool | ||||
| @@ -212,6 +233,17 @@ func TestControllerV2SyncCronJob(t *testing.T) { | ||||
| 			expectedWarnings:           1, | ||||
| 			jobPresentInCJActiveStatus: true, | ||||
| 		}, | ||||
| 		"never ran, not valid time zone": { | ||||
| 			concurrencyPolicy:          "Allow", | ||||
| 			schedule:                   onTheHour, | ||||
| 			timeZone:                   &errorTimeZone, | ||||
| 			deadline:                   noDead, | ||||
| 			jobCreationTime:            justAfterThePriorHour(), | ||||
| 			now:                        justBeforeTheHour(), | ||||
| 			enableTimeZone:             true, | ||||
| 			expectedWarnings:           1, | ||||
| 			jobPresentInCJActiveStatus: true, | ||||
| 		}, | ||||
| 		"never ran, not time, A": { | ||||
| 			concurrencyPolicy:          "Allow", | ||||
| 			schedule:                   onTheHour, | ||||
| @@ -238,6 +270,17 @@ func TestControllerV2SyncCronJob(t *testing.T) { | ||||
| 			expectRequeueAfter:         true, | ||||
| 			jobPresentInCJActiveStatus: true, | ||||
| 		}, | ||||
| 		"never ran, not time in zone": { | ||||
| 			concurrencyPolicy:          "Allow", | ||||
| 			schedule:                   onTheHour, | ||||
| 			timeZone:                   &newYork, | ||||
| 			deadline:                   noDead, | ||||
| 			jobCreationTime:            justAfterThePriorHour(), | ||||
| 			now:                        justBeforeTheHour(), | ||||
| 			enableTimeZone:             true, | ||||
| 			expectRequeueAfter:         true, | ||||
| 			jobPresentInCJActiveStatus: true, | ||||
| 		}, | ||||
| 		"never ran, is time, A": { | ||||
| 			concurrencyPolicy:          "Allow", | ||||
| 			schedule:                   onTheHour, | ||||
| @@ -274,6 +317,48 @@ func TestControllerV2SyncCronJob(t *testing.T) { | ||||
| 			expectUpdateStatus:         true, | ||||
| 			jobPresentInCJActiveStatus: true, | ||||
| 		}, | ||||
| 		"never ran, is time in zone, but time zone disabled": { | ||||
| 			concurrencyPolicy:          "Allow", | ||||
| 			schedule:                   onTheHour, | ||||
| 			timeZone:                   &newYork, | ||||
| 			deadline:                   noDead, | ||||
| 			jobCreationTime:            justAfterThePriorHour(), | ||||
| 			now:                        justAfterTheHourInZone(newYork), | ||||
| 			enableTimeZone:             false, | ||||
| 			expectCreate:               true, | ||||
| 			expectActive:               1, | ||||
| 			expectRequeueAfter:         true, | ||||
| 			expectUpdateStatus:         true, | ||||
| 			jobPresentInCJActiveStatus: true, | ||||
| 		}, | ||||
| 		"never ran, is time in zone": { | ||||
| 			concurrencyPolicy:          "Allow", | ||||
| 			schedule:                   onTheHour, | ||||
| 			timeZone:                   &newYork, | ||||
| 			deadline:                   noDead, | ||||
| 			jobCreationTime:            justAfterThePriorHour(), | ||||
| 			now:                        justAfterTheHourInZone(newYork), | ||||
| 			enableTimeZone:             true, | ||||
| 			expectCreate:               true, | ||||
| 			expectActive:               1, | ||||
| 			expectRequeueAfter:         true, | ||||
| 			expectUpdateStatus:         true, | ||||
| 			jobPresentInCJActiveStatus: true, | ||||
| 		}, | ||||
| 		"never ran, is time in zone, but TZ is also set in schedule": { | ||||
| 			concurrencyPolicy:          "Allow", | ||||
| 			schedule:                   "TZ=UTC " + onTheHour, | ||||
| 			timeZone:                   &newYork, | ||||
| 			deadline:                   noDead, | ||||
| 			jobCreationTime:            justAfterThePriorHour(), | ||||
| 			now:                        justAfterTheHourInZone(newYork), | ||||
| 			enableTimeZone:             true, | ||||
| 			expectCreate:               true, | ||||
| 			expectedWarnings:           1, | ||||
| 			expectRequeueAfter:         true, | ||||
| 			expectUpdateStatus:         true, | ||||
| 			jobPresentInCJActiveStatus: true, | ||||
| 		}, | ||||
| 		"never ran, is time, suspended": { | ||||
| 			concurrencyPolicy:          "Allow", | ||||
| 			suspend:                    true, | ||||
| @@ -820,10 +905,15 @@ func TestControllerV2SyncCronJob(t *testing.T) { | ||||
| 			cj.Spec.ConcurrencyPolicy = tc.concurrencyPolicy | ||||
| 			cj.Spec.Suspend = &tc.suspend | ||||
| 			cj.Spec.Schedule = tc.schedule | ||||
| 			cj.Spec.TimeZone = tc.timeZone | ||||
| 			if tc.deadline != noDead { | ||||
| 				cj.Spec.StartingDeadlineSeconds = &tc.deadline | ||||
| 			} | ||||
|  | ||||
| 			if tc.enableTimeZone { | ||||
| 				defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.CronJobTimeZone, true) | ||||
| 			} | ||||
|  | ||||
| 			var ( | ||||
| 				job *batchv1.Job | ||||
| 				err error | ||||
|   | ||||
| @@ -17,7 +17,7 @@ limitations under the License. | ||||
| package v1 | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/api/core/v1" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| ) | ||||
| @@ -146,7 +146,7 @@ type JobSpec struct { | ||||
|  | ||||
| 	// Describes the pod that will be created when executing a job. | ||||
| 	// More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/ | ||||
| 	Template v1.PodTemplateSpec `json:"template" protobuf:"bytes,6,opt,name=template"` | ||||
| 	Template corev1.PodTemplateSpec `json:"template" protobuf:"bytes,6,opt,name=template"` | ||||
|  | ||||
| 	// ttlSecondsAfterFinished limits the lifetime of a Job that has finished | ||||
| 	// execution (either Complete or Failed). If this field is set, | ||||
| @@ -304,7 +304,7 @@ type JobCondition struct { | ||||
| 	// Type of job condition, Complete or Failed. | ||||
| 	Type JobConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=JobConditionType"` | ||||
| 	// Status of the condition, one of True, False, Unknown. | ||||
| 	Status v1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=k8s.io/api/core/v1.ConditionStatus"` | ||||
| 	Status corev1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=k8s.io/api/core/v1.ConditionStatus"` | ||||
| 	// Last time the condition was checked. | ||||
| 	// +optional | ||||
| 	LastProbeTime metav1.Time `json:"lastProbeTime,omitempty" protobuf:"bytes,3,opt,name=lastProbeTime"` | ||||
| @@ -375,6 +375,12 @@ type CronJobSpec struct { | ||||
| 	// The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. | ||||
| 	Schedule string `json:"schedule" protobuf:"bytes,1,opt,name=schedule"` | ||||
|  | ||||
| 	// The time zone for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. | ||||
| 	// If not specified, this will rely on the time zone of the kube-controller-manager process. | ||||
| 	// ALPHA: This field is in alpha and must be enabled via the `CronJobTimeZone` feature gate. | ||||
| 	// +optional | ||||
| 	TimeZone *string `json:"timeZone,omitempty" protobuf:"bytes,8,opt,name=timeZone"` | ||||
|  | ||||
| 	// Optional deadline in seconds for starting the job if it misses scheduled | ||||
| 	// time for any reason.  Missed jobs executions will be counted as failed ones. | ||||
| 	// +optional | ||||
| @@ -431,7 +437,7 @@ type CronJobStatus struct { | ||||
| 	// A list of pointers to currently running jobs. | ||||
| 	// +optional | ||||
| 	// +listType=atomic | ||||
| 	Active []v1.ObjectReference `json:"active,omitempty" protobuf:"bytes,1,rep,name=active"` | ||||
| 	Active []corev1.ObjectReference `json:"active,omitempty" protobuf:"bytes,1,rep,name=active"` | ||||
|  | ||||
| 	// Information when was the last time the job was successfully scheduled. | ||||
| 	// +optional | ||||
|   | ||||
| @@ -104,6 +104,12 @@ type CronJobSpec struct { | ||||
| 	// The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. | ||||
| 	Schedule string `json:"schedule" protobuf:"bytes,1,opt,name=schedule"` | ||||
|  | ||||
| 	// The time zone for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. | ||||
| 	// If not specified, this will rely on the time zone of the kube-controller-manager process. | ||||
| 	// ALPHA: This field is in alpha and must be enabled via the `CronJobTimeZone` feature gate. | ||||
| 	// +optional | ||||
| 	TimeZone *string `json:"timeZone,omitempty" protobuf:"bytes,8,opt,name=timeZone"` | ||||
|  | ||||
| 	// Optional deadline in seconds for starting the job if it misses scheduled | ||||
| 	// time for any reason.  Missed jobs executions will be counted as failed ones. | ||||
| 	// +optional | ||||
|   | ||||
| @@ -178,6 +178,13 @@ const ( | ||||
| 	// | ||||
| 	// Enables server-side field validation. | ||||
| 	ServerSideFieldValidation featuregate.Feature = "ServerSideFieldValidation" | ||||
|  | ||||
| 	// owner: @deejross | ||||
| 	// kep: http://kep.k8s.io/3140 | ||||
| 	// alpha: v1.24 | ||||
| 	// | ||||
| 	// Enables support for time zones in CronJobs. | ||||
| 	CronJobTimeZone featuregate.Feature = "CronJobTimeZone" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| @@ -207,4 +214,5 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS | ||||
| 	CustomResourceValidationExpressions: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| 	OpenAPIV3:                           {Default: false, PreRelease: featuregate.Alpha}, | ||||
| 	ServerSideFieldValidation:           {Default: true, PreRelease: featuregate.Beta}, | ||||
| 	CronJobTimeZone:                     {Default: false, PreRelease: featuregate.Alpha}, | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Ross Peoples
					Ross Peoples