Skip to content

Commit 0f23005

Browse files
authored
Merge pull request kubernetes#108032 from deejross/kep3140-cronjob-timezone
KEP 3140: TimeZone support for CronJob
2 parents d429c98 + d26e6cc commit 0f23005

35 files changed

+971
-167
lines changed

api/openapi-spec/swagger.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/openapi-spec/v3/apis__batch__v1_openapi.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@
114114
"suspend": {
115115
"description": "This flag tells the controller to suspend subsequent executions, it does not apply to already started executions. Defaults to false.",
116116
"type": "boolean"
117+
},
118+
"timeZone": {
119+
"description": "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.",
120+
"type": "string"
117121
}
118122
},
119123
"required": [

api/openapi-spec/v3/apis__batch__v1beta1_openapi.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@
164164
"suspend": {
165165
"description": "This flag tells the controller to suspend subsequent executions, it does not apply to already started executions. Defaults to false.",
166166
"type": "boolean"
167+
},
168+
"timeZone": {
169+
"description": "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.",
170+
"type": "string"
167171
}
168172
},
169173
"required": [

cmd/kube-apiserver/apiserver.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package main
2020

2121
import (
2222
"os"
23+
_ "time/tzdata" // for timeZone support in CronJob
2324

2425
"k8s.io/component-base/cli"
2526
_ "k8s.io/component-base/logs/json/register" // for JSON log format registration

cmd/kube-controller-manager/controller-manager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package main
2222

2323
import (
2424
"os"
25+
_ "time/tzdata" // for CronJob Time Zone support
2526

2627
"k8s.io/component-base/cli"
2728
_ "k8s.io/component-base/logs/json/register" // for JSON log format registration

pkg/apis/batch/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,12 @@ type CronJobSpec struct {
376376
// The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron.
377377
Schedule string
378378

379+
// The time zone for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
380+
// If not specified, this will rely on the time zone of the kube-controller-manager process.
381+
// ALPHA: This field is in alpha and must be enabled via the `CronJobTimeZone` feature gate.
382+
// +optional
383+
TimeZone *string
384+
379385
// Optional deadline in seconds for starting the job if it misses scheduled
380386
// time for any reason. Missed jobs executions will be counted as failed ones.
381387
// +optional

pkg/apis/batch/v1/zz_generated.conversion.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apis/batch/v1beta1/zz_generated.conversion.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apis/batch/validation/validation.go

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package validation
1818

1919
import (
2020
"fmt"
21+
"strings"
22+
"time"
2123

2224
"github.com/robfig/cron/v3"
2325
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -29,6 +31,7 @@ import (
2931
"k8s.io/kubernetes/pkg/apis/batch"
3032
api "k8s.io/kubernetes/pkg/apis/core"
3133
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
34+
"k8s.io/utils/pointer"
3235
)
3336

3437
// maxParallelismForIndexJob is the maximum parallelism that an Indexed Job
@@ -277,11 +280,11 @@ func ValidateJobStatusUpdate(status, oldStatus batch.JobStatus) field.ErrorList
277280
return allErrs
278281
}
279282

280-
// ValidateCronJob validates a CronJob and returns an ErrorList with any errors.
281-
func ValidateCronJob(cronJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList {
283+
// ValidateCronJobCreate validates a CronJob on creation and returns an ErrorList with any errors.
284+
func ValidateCronJobCreate(cronJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList {
282285
// CronJobs and rcs have the same name validation
283286
allErrs := apivalidation.ValidateObjectMeta(&cronJob.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata"))
284-
allErrs = append(allErrs, ValidateCronJobSpec(&cronJob.Spec, field.NewPath("spec"), opts)...)
287+
allErrs = append(allErrs, validateCronJobSpec(&cronJob.Spec, nil, field.NewPath("spec"), opts)...)
285288
if len(cronJob.ObjectMeta.Name) > apimachineryvalidation.DNS1035LabelMaxLength-11 {
286289
// The cronjob controller appends a 11-character suffix to the cronjob (`-$TIMESTAMP`) when
287290
// creating a job. The job name length limit is 63 characters.
@@ -295,24 +298,31 @@ func ValidateCronJob(cronJob *batch.CronJob, opts apivalidation.PodValidationOpt
295298
// ValidateCronJobUpdate validates an update to a CronJob and returns an ErrorList with any errors.
296299
func ValidateCronJobUpdate(job, oldJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList {
297300
allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
298-
allErrs = append(allErrs, ValidateCronJobSpec(&job.Spec, field.NewPath("spec"), opts)...)
301+
allErrs = append(allErrs, validateCronJobSpec(&job.Spec, &oldJob.Spec, field.NewPath("spec"), opts)...)
302+
299303
// skip the 52-character name validation limit on update validation
300304
// to allow old cronjobs with names > 52 chars to be updated/deleted
301305
return allErrs
302306
}
303307

304-
// ValidateCronJobSpec validates a CronJobSpec and returns an ErrorList with any errors.
305-
func ValidateCronJobSpec(spec *batch.CronJobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
308+
// validateCronJobSpec validates a CronJobSpec and returns an ErrorList with any errors.
309+
func validateCronJobSpec(spec, oldSpec *batch.CronJobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
306310
allErrs := field.ErrorList{}
307311

308312
if len(spec.Schedule) == 0 {
309313
allErrs = append(allErrs, field.Required(fldPath.Child("schedule"), ""))
310314
} else {
311-
allErrs = append(allErrs, validateScheduleFormat(spec.Schedule, fldPath.Child("schedule"))...)
315+
allErrs = append(allErrs, validateScheduleFormat(spec.Schedule, spec.TimeZone, fldPath.Child("schedule"))...)
312316
}
317+
313318
if spec.StartingDeadlineSeconds != nil {
314319
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.StartingDeadlineSeconds), fldPath.Child("startingDeadlineSeconds"))...)
315320
}
321+
322+
if oldSpec == nil || !pointer.StringEqual(oldSpec.TimeZone, spec.TimeZone) {
323+
allErrs = append(allErrs, validateTimeZone(spec.TimeZone, fldPath.Child("timeZone"))...)
324+
}
325+
316326
allErrs = append(allErrs, validateConcurrencyPolicy(&spec.ConcurrencyPolicy, fldPath.Child("concurrencyPolicy"))...)
317327
allErrs = append(allErrs, ValidateJobTemplateSpec(&spec.JobTemplate, fldPath.Child("jobTemplate"), opts)...)
318328

@@ -343,11 +353,36 @@ func validateConcurrencyPolicy(concurrencyPolicy *batch.ConcurrencyPolicy, fldPa
343353
return allErrs
344354
}
345355

346-
func validateScheduleFormat(schedule string, fldPath *field.Path) field.ErrorList {
356+
func validateScheduleFormat(schedule string, timeZone *string, fldPath *field.Path) field.ErrorList {
347357
allErrs := field.ErrorList{}
348358
if _, err := cron.ParseStandard(schedule); err != nil {
349359
allErrs = append(allErrs, field.Invalid(fldPath, schedule, err.Error()))
350360
}
361+
if strings.Contains(schedule, "TZ") && timeZone != nil {
362+
allErrs = append(allErrs, field.Invalid(fldPath, schedule, "cannot use both timeZone field and TZ or CRON_TZ in schedule"))
363+
}
364+
365+
return allErrs
366+
}
367+
368+
func validateTimeZone(timeZone *string, fldPath *field.Path) field.ErrorList {
369+
allErrs := field.ErrorList{}
370+
if timeZone == nil {
371+
return allErrs
372+
}
373+
374+
if len(*timeZone) == 0 {
375+
allErrs = append(allErrs, field.Invalid(fldPath, timeZone, "timeZone must be nil or non-empty string"))
376+
return allErrs
377+
}
378+
379+
if strings.EqualFold(*timeZone, "Local") {
380+
allErrs = append(allErrs, field.Invalid(fldPath, timeZone, "timeZone must be an explicit time zone as defined in https://www.iana.org/time-zones"))
381+
}
382+
383+
if _, err := time.LoadLocation(*timeZone); err != nil {
384+
allErrs = append(allErrs, field.Invalid(fldPath, timeZone, err.Error()))
385+
}
351386

352387
return allErrs
353388
}

0 commit comments

Comments
 (0)