diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 813cdb6c339..9625fab5aa3 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1760,6 +1760,10 @@ "Comment": "v1.0.0-842-g8992d74", "Rev": "8992d7483a06748dea706e4716d042a4a9e73918" }, + { + "ImportPath": "github.com/robfig/cron", + "Rev": "0f39cf7ebc65a602f45692f9894bd6a193faf8fa" + }, { "ImportPath": "github.com/russross/blackfriday", "Comment": "v1.2-42-g77efab5", diff --git a/Godeps/LICENSES b/Godeps/LICENSES index 4a9be26cb77..cb22b7a56b8 100644 --- a/Godeps/LICENSES +++ b/Godeps/LICENSES @@ -59125,6 +59125,32 @@ specific language governing permissions and limitations under the License. END OF TERMS AND CONDITIONS +================================================================================ += vendor/github.com/robfig/cron licensed under: = + +Copyright (C) 2012 Rob Figueiredo +All Rights Reserved. + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ================================================================================ = vendor/github.com/russross/blackfriday licensed under: = diff --git a/pkg/apis/batch/register.go b/pkg/apis/batch/register.go index a3dc9b4de9e..8406f9ffbae 100644 --- a/pkg/apis/batch/register.go +++ b/pkg/apis/batch/register.go @@ -49,10 +49,14 @@ func addKnownTypes(scheme *runtime.Scheme) { &Job{}, &JobList{}, &JobTemplate{}, + &ScheduledJob{}, + &ScheduledJobList{}, &api.ListOptions{}, ) } -func (obj *Job) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } -func (obj *JobList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } -func (obj *JobTemplate) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *Job) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *JobList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *JobTemplate) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *ScheduledJob) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *ScheduledJobList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } diff --git a/pkg/apis/batch/types.go b/pkg/apis/batch/types.go index 4e9e6b62301..756d4c9d6b1 100644 --- a/pkg/apis/batch/types.go +++ b/pkg/apis/batch/types.go @@ -165,6 +165,8 @@ type JobCondition struct { Message string `json:"message,omitempty"` } +// +genclient=true + // ScheduledJob represents the configuration of a single scheduled job. type ScheduledJob struct { unversioned.TypeMeta `json:",inline"` diff --git a/pkg/apis/batch/v2alpha1/conversion_generated.go b/pkg/apis/batch/v2alpha1/conversion_generated.go index cecbfe4d6e5..fd65487317d 100644 --- a/pkg/apis/batch/v2alpha1/conversion_generated.go +++ b/pkg/apis/batch/v2alpha1/conversion_generated.go @@ -48,6 +48,12 @@ func init() { Convert_unversioned_LabelSelector_To_v2alpha1_LabelSelector, Convert_v2alpha1_LabelSelectorRequirement_To_unversioned_LabelSelectorRequirement, Convert_unversioned_LabelSelectorRequirement_To_v2alpha1_LabelSelectorRequirement, + Convert_v2alpha1_ScheduledJob_To_batch_ScheduledJob, + Convert_batch_ScheduledJob_To_v2alpha1_ScheduledJob, + Convert_v2alpha1_ScheduledJobList_To_batch_ScheduledJobList, + Convert_batch_ScheduledJobList_To_v2alpha1_ScheduledJobList, + Convert_v2alpha1_ScheduledJobSpec_To_batch_ScheduledJobSpec, + Convert_batch_ScheduledJobSpec_To_v2alpha1_ScheduledJobSpec, Convert_v2alpha1_ScheduledJobStatus_To_batch_ScheduledJobStatus, Convert_batch_ScheduledJobStatus_To_v2alpha1_ScheduledJobStatus, ); err != nil { @@ -509,6 +515,141 @@ func Convert_unversioned_LabelSelectorRequirement_To_v2alpha1_LabelSelectorRequi return autoConvert_unversioned_LabelSelectorRequirement_To_v2alpha1_LabelSelectorRequirement(in, out, s) } +func autoConvert_v2alpha1_ScheduledJob_To_batch_ScheduledJob(in *ScheduledJob, out *batch.ScheduledJob, s conversion.Scope) error { + SetDefaults_ScheduledJob(in) + if err := api.Convert_unversioned_TypeMeta_To_unversioned_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + // TODO: Inefficient conversion - can we improve it? + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := Convert_v2alpha1_ScheduledJobSpec_To_batch_ScheduledJobSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_v2alpha1_ScheduledJobStatus_To_batch_ScheduledJobStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +func Convert_v2alpha1_ScheduledJob_To_batch_ScheduledJob(in *ScheduledJob, out *batch.ScheduledJob, s conversion.Scope) error { + return autoConvert_v2alpha1_ScheduledJob_To_batch_ScheduledJob(in, out, s) +} + +func autoConvert_batch_ScheduledJob_To_v2alpha1_ScheduledJob(in *batch.ScheduledJob, out *ScheduledJob, s conversion.Scope) error { + if err := api.Convert_unversioned_TypeMeta_To_unversioned_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + // TODO: Inefficient conversion - can we improve it? + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := Convert_batch_ScheduledJobSpec_To_v2alpha1_ScheduledJobSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_batch_ScheduledJobStatus_To_v2alpha1_ScheduledJobStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +func Convert_batch_ScheduledJob_To_v2alpha1_ScheduledJob(in *batch.ScheduledJob, out *ScheduledJob, s conversion.Scope) error { + return autoConvert_batch_ScheduledJob_To_v2alpha1_ScheduledJob(in, out, s) +} + +func autoConvert_v2alpha1_ScheduledJobList_To_batch_ScheduledJobList(in *ScheduledJobList, out *batch.ScheduledJobList, s conversion.Scope) error { + if err := api.Convert_unversioned_TypeMeta_To_unversioned_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := api.Convert_unversioned_ListMeta_To_unversioned_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]batch.ScheduledJob, len(*in)) + for i := range *in { + if err := Convert_v2alpha1_ScheduledJob_To_batch_ScheduledJob(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + +func Convert_v2alpha1_ScheduledJobList_To_batch_ScheduledJobList(in *ScheduledJobList, out *batch.ScheduledJobList, s conversion.Scope) error { + return autoConvert_v2alpha1_ScheduledJobList_To_batch_ScheduledJobList(in, out, s) +} + +func autoConvert_batch_ScheduledJobList_To_v2alpha1_ScheduledJobList(in *batch.ScheduledJobList, out *ScheduledJobList, s conversion.Scope) error { + if err := api.Convert_unversioned_TypeMeta_To_unversioned_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := api.Convert_unversioned_ListMeta_To_unversioned_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ScheduledJob, len(*in)) + for i := range *in { + if err := Convert_batch_ScheduledJob_To_v2alpha1_ScheduledJob(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + +func Convert_batch_ScheduledJobList_To_v2alpha1_ScheduledJobList(in *batch.ScheduledJobList, out *ScheduledJobList, s conversion.Scope) error { + return autoConvert_batch_ScheduledJobList_To_v2alpha1_ScheduledJobList(in, out, s) +} + +func autoConvert_v2alpha1_ScheduledJobSpec_To_batch_ScheduledJobSpec(in *ScheduledJobSpec, out *batch.ScheduledJobSpec, s conversion.Scope) error { + out.Schedule = in.Schedule + if in.StartingDeadlineSeconds != nil { + in, out := &in.StartingDeadlineSeconds, &out.StartingDeadlineSeconds + *out = new(int64) + **out = **in + } else { + out.StartingDeadlineSeconds = nil + } + out.ConcurrencyPolicy = batch.ConcurrencyPolicy(in.ConcurrencyPolicy) + out.Suspend = in.Suspend + if err := Convert_v2alpha1_JobTemplateSpec_To_batch_JobTemplateSpec(&in.JobTemplate, &out.JobTemplate, s); err != nil { + return err + } + return nil +} + +func Convert_v2alpha1_ScheduledJobSpec_To_batch_ScheduledJobSpec(in *ScheduledJobSpec, out *batch.ScheduledJobSpec, s conversion.Scope) error { + return autoConvert_v2alpha1_ScheduledJobSpec_To_batch_ScheduledJobSpec(in, out, s) +} + +func autoConvert_batch_ScheduledJobSpec_To_v2alpha1_ScheduledJobSpec(in *batch.ScheduledJobSpec, out *ScheduledJobSpec, s conversion.Scope) error { + out.Schedule = in.Schedule + if in.StartingDeadlineSeconds != nil { + in, out := &in.StartingDeadlineSeconds, &out.StartingDeadlineSeconds + *out = new(int64) + **out = **in + } else { + out.StartingDeadlineSeconds = nil + } + out.ConcurrencyPolicy = ConcurrencyPolicy(in.ConcurrencyPolicy) + out.Suspend = in.Suspend + if err := Convert_batch_JobTemplateSpec_To_v2alpha1_JobTemplateSpec(&in.JobTemplate, &out.JobTemplate, s); err != nil { + return err + } + return nil +} + +func Convert_batch_ScheduledJobSpec_To_v2alpha1_ScheduledJobSpec(in *batch.ScheduledJobSpec, out *ScheduledJobSpec, s conversion.Scope) error { + return autoConvert_batch_ScheduledJobSpec_To_v2alpha1_ScheduledJobSpec(in, out, s) +} + func autoConvert_v2alpha1_ScheduledJobStatus_To_batch_ScheduledJobStatus(in *ScheduledJobStatus, out *batch.ScheduledJobStatus, s conversion.Scope) error { if in.Active != nil { in, out := &in.Active, &out.Active diff --git a/pkg/apis/batch/v2alpha1/deep_copy_generated.go b/pkg/apis/batch/v2alpha1/deep_copy_generated.go index 3cc325bbc42..8e0eb343a5e 100644 --- a/pkg/apis/batch/v2alpha1/deep_copy_generated.go +++ b/pkg/apis/batch/v2alpha1/deep_copy_generated.go @@ -287,14 +287,8 @@ func DeepCopy_v2alpha1_ScheduledJobSpec(in ScheduledJobSpec, out *ScheduledJobSp } out.ConcurrencyPolicy = in.ConcurrencyPolicy out.Suspend = in.Suspend - if in.JobTemplate != nil { - in, out := in.JobTemplate, &out.JobTemplate - *out = new(JobTemplateSpec) - if err := DeepCopy_v2alpha1_JobTemplateSpec(*in, *out, c); err != nil { - return err - } - } else { - out.JobTemplate = nil + if err := DeepCopy_v2alpha1_JobTemplateSpec(in.JobTemplate, &out.JobTemplate, c); err != nil { + return err } return nil } diff --git a/pkg/apis/batch/v2alpha1/defaults.go b/pkg/apis/batch/v2alpha1/defaults.go index d1b90e33d06..72da797c77f 100644 --- a/pkg/apis/batch/v2alpha1/defaults.go +++ b/pkg/apis/batch/v2alpha1/defaults.go @@ -23,6 +23,7 @@ import ( func addDefaultingFuncs(scheme *runtime.Scheme) { scheme.AddDefaultingFuncs( SetDefaults_Job, + SetDefaults_ScheduledJob, ) } @@ -40,3 +41,9 @@ func SetDefaults_Job(obj *Job) { *obj.Spec.Parallelism = 1 } } + +func SetDefaults_ScheduledJob(obj *ScheduledJob) { + if obj.Spec.ConcurrencyPolicy == "" { + obj.Spec.ConcurrencyPolicy = AllowConcurrent + } +} diff --git a/pkg/apis/batch/v2alpha1/generated.pb.go b/pkg/apis/batch/v2alpha1/generated.pb.go index 94c97b8e398..a642fa691c7 100644 --- a/pkg/apis/batch/v2alpha1/generated.pb.go +++ b/pkg/apis/batch/v2alpha1/generated.pb.go @@ -644,16 +644,14 @@ func (m *ScheduledJobSpec) MarshalTo(data []byte) (int, error) { data[i] = 0 } i++ - if m.JobTemplate != nil { - data[i] = 0x2a - i++ - i = encodeVarintGenerated(data, i, uint64(m.JobTemplate.Size())) - n19, err := m.JobTemplate.MarshalTo(data[i:]) - if err != nil { - return 0, err - } - i += n19 + data[i] = 0x2a + i++ + i = encodeVarintGenerated(data, i, uint64(m.JobTemplate.Size())) + n19, err := m.JobTemplate.MarshalTo(data[i:]) + if err != nil { + return 0, err } + i += n19 return i, nil } @@ -908,10 +906,8 @@ func (m *ScheduledJobSpec) Size() (n int) { l = len(m.ConcurrencyPolicy) n += 1 + l + sovGenerated(uint64(l)) n += 2 - if m.JobTemplate != nil { - l = m.JobTemplate.Size() - n += 1 + l + sovGenerated(uint64(l)) - } + l = m.JobTemplate.Size() + n += 1 + l + sovGenerated(uint64(l)) return n } @@ -2772,9 +2768,6 @@ func (m *ScheduledJobSpec) Unmarshal(data []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - if m.JobTemplate == nil { - m.JobTemplate = &JobTemplateSpec{} - } if err := m.JobTemplate.Unmarshal(data[iNdEx:postIndex]); err != nil { return err } diff --git a/pkg/apis/batch/v2alpha1/register.go b/pkg/apis/batch/v2alpha1/register.go index f887d716564..3d9dcb83f6c 100644 --- a/pkg/apis/batch/v2alpha1/register.go +++ b/pkg/apis/batch/v2alpha1/register.go @@ -41,11 +41,15 @@ func addKnownTypes(scheme *runtime.Scheme) { &Job{}, &JobList{}, &JobTemplate{}, + &ScheduledJob{}, + &ScheduledJobList{}, &v1.ListOptions{}, ) versionedwatch.AddToGroupVersion(scheme, SchemeGroupVersion) } -func (obj *JobTemplate) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } -func (obj *Job) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } -func (obj *JobList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *Job) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *JobList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *JobTemplate) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *ScheduledJob) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *ScheduledJobList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } diff --git a/pkg/apis/batch/v2alpha1/types.generated.go b/pkg/apis/batch/v2alpha1/types.generated.go index 239ae8c2c0d..7b64c81d248 100644 --- a/pkg/apis/batch/v2alpha1/types.generated.go +++ b/pkg/apis/batch/v2alpha1/types.generated.go @@ -3597,20 +3597,14 @@ func (x *ScheduledJobSpec) CodecEncodeSelf(e *codec1978.Encoder) { } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayElem1234) - if x.JobTemplate == nil { - r.EncodeNil() - } else { - x.JobTemplate.CodecEncodeSelf(e) - } + yy18 := &x.JobTemplate + yy18.CodecEncodeSelf(e) } else { z.EncSendContainerState(codecSelfer_containerMapKey1234) r.EncodeString(codecSelferC_UTF81234, string("jobTemplate")) z.EncSendContainerState(codecSelfer_containerMapValue1234) - if x.JobTemplate == nil { - r.EncodeNil() - } else { - x.JobTemplate.CodecEncodeSelf(e) - } + yy20 := &x.JobTemplate + yy20.CodecEncodeSelf(e) } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayEnd1234) @@ -3709,14 +3703,10 @@ func (x *ScheduledJobSpec) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) { } case "jobTemplate": if r.TryDecodeAsNil() { - if x.JobTemplate != nil { - x.JobTemplate = nil - } + x.JobTemplate = JobTemplateSpec{} } else { - if x.JobTemplate == nil { - x.JobTemplate = new(JobTemplateSpec) - } - x.JobTemplate.CodecDecodeSelf(d) + yyv9 := &x.JobTemplate + yyv9.CodecDecodeSelf(d) } default: z.DecStructFieldNotFound(-1, yys3) @@ -3818,14 +3808,10 @@ func (x *ScheduledJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) } z.DecSendContainerState(codecSelfer_containerArrayElem1234) if r.TryDecodeAsNil() { - if x.JobTemplate != nil { - x.JobTemplate = nil - } + x.JobTemplate = JobTemplateSpec{} } else { - if x.JobTemplate == nil { - x.JobTemplate = new(JobTemplateSpec) - } - x.JobTemplate.CodecDecodeSelf(d) + yyv16 := &x.JobTemplate + yyv16.CodecDecodeSelf(d) } for { yyj10++ @@ -4975,7 +4961,7 @@ func (x codecSelfer1234) decSliceScheduledJob(v *[]ScheduledJob, d *codec1978.De yyrg1 := len(yyv1) > 0 yyv21 := yyv1 - yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 328) + yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 1024) if yyrt1 { if yyrl1 <= cap(yyv1) { yyv1 = yyv1[:yyrl1] diff --git a/pkg/apis/batch/v2alpha1/types.go b/pkg/apis/batch/v2alpha1/types.go index f6615f1ef65..f33dfc1ed6d 100644 --- a/pkg/apis/batch/v2alpha1/types.go +++ b/pkg/apis/batch/v2alpha1/types.go @@ -215,7 +215,7 @@ type ScheduledJobSpec struct { // JobTemplate is the object that describes the job that will be created when // executing a ScheduledJob. - JobTemplate *JobTemplateSpec `json:"jobTemplate" protobuf:"bytes,5,opt,name=jobTemplate"` + JobTemplate JobTemplateSpec `json:"jobTemplate" protobuf:"bytes,5,opt,name=jobTemplate"` } // ConcurrencyPolicy describes how the job will be handled. diff --git a/pkg/apis/batch/validation/validation.go b/pkg/apis/batch/validation/validation.go index d4d18757834..ca1ea5b9e95 100644 --- a/pkg/apis/batch/validation/validation.go +++ b/pkg/apis/batch/validation/validation.go @@ -17,6 +17,8 @@ limitations under the License. package validation import ( + "github.com/robfig/cron" + "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/unversioned" unversionedvalidation "k8s.io/kubernetes/pkg/api/unversioned/validation" @@ -155,3 +157,65 @@ func ValidateJobStatusUpdate(status, oldStatus batch.JobStatus) field.ErrorList allErrs = append(allErrs, ValidateJobStatus(&status, field.NewPath("status"))...) return allErrs } + +func ValidateScheduledJob(scheduledJob *batch.ScheduledJob) field.ErrorList { + // ScheduledJobs and rcs have the same name validation + allErrs := apivalidation.ValidateObjectMeta(&scheduledJob.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateScheduledJobSpec(&scheduledJob.Spec, field.NewPath("spec"))...) + return allErrs +} + +func ValidateScheduledJobSpec(spec *batch.ScheduledJobSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if len(spec.Schedule) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("schedule"), "")) + } else { + allErrs = append(allErrs, validateScheduleFormat(spec.Schedule, fldPath.Child("schedule"))...) + } + if spec.StartingDeadlineSeconds != nil { + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.StartingDeadlineSeconds), fldPath.Child("startingDeadlineSeconds"))...) + } + allErrs = append(allErrs, validateConcurrencyPolicy(&spec.ConcurrencyPolicy, fldPath.Child("concurrencyPolicy"))...) + allErrs = append(allErrs, ValidateJobTemplateSpec(&spec.JobTemplate, fldPath.Child("jobTemplate"))...) + + return allErrs +} + +func validateConcurrencyPolicy(concurrencyPolicy *batch.ConcurrencyPolicy, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + switch *concurrencyPolicy { + case batch.AllowConcurrent, batch.ForbidConcurrent, batch.ReplaceConcurrent: + break + case "": + allErrs = append(allErrs, field.Required(fldPath, "")) + default: + validValues := []string{string(batch.AllowConcurrent), string(batch.ForbidConcurrent), string(batch.ReplaceConcurrent)} + allErrs = append(allErrs, field.NotSupported(fldPath, *concurrencyPolicy, validValues)) + } + + return allErrs +} + +func validateScheduleFormat(schedule string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + _, err := cron.Parse(schedule) + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath, schedule, err.Error())) + } + + return allErrs +} + +func ValidateJobTemplate(job *batch.JobTemplate) field.ErrorList { + // this method should be identical to ValidateJob + allErrs := apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateJobTemplateSpec(&job.Template, field.NewPath("template"))...) + return allErrs +} + +func ValidateJobTemplateSpec(spec *batch.JobTemplateSpec, fldPath *field.Path) field.ErrorList { + // this method should be identical to ValidateJob + allErrs := ValidateJobSpec(&spec.Spec, fldPath.Child("spec")) + return allErrs +} diff --git a/pkg/apis/batch/validation/validation_test.go b/pkg/apis/batch/validation/validation_test.go index 5b59a4c6e19..2f5fb534db2 100644 --- a/pkg/apis/batch/validation/validation_test.go +++ b/pkg/apis/batch/validation/validation_test.go @@ -26,26 +26,35 @@ import ( "k8s.io/kubernetes/pkg/types" ) -func TestValidateJob(t *testing.T) { - validManualSelector := &unversioned.LabelSelector{ +func getValidManualSelector() *unversioned.LabelSelector { + return &unversioned.LabelSelector{ MatchLabels: map[string]string{"a": "b"}, } - validGeneratedSelector := &unversioned.LabelSelector{ +} + +func getValidPodTemplateSpecForManual(selector *unversioned.LabelSelector) api.PodTemplateSpec { + return api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: selector.MatchLabels, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + } +} + +func getValidGeneratedSelector() *unversioned.LabelSelector { + return &unversioned.LabelSelector{ MatchLabels: map[string]string{"controller-uid": "1a2b3c", "job-name": "myjob"}, } - validPodTemplateSpecForManual := api.PodTemplateSpec{ +} + +func getValidPodTemplateSpecForGenerated(selector *unversioned.LabelSelector) api.PodTemplateSpec { + return api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ - Labels: validManualSelector.MatchLabels, - }, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicyOnFailure, - DNSPolicy: api.DNSClusterFirst, - Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, - }, - } - validPodTemplateSpecForGenerated := api.PodTemplateSpec{ - ObjectMeta: api.ObjectMeta{ - Labels: validGeneratedSelector.MatchLabels, + Labels: selector.MatchLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, @@ -53,6 +62,14 @@ func TestValidateJob(t *testing.T) { Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, } +} + +func TestValidateJob(t *testing.T) { + validManualSelector := getValidManualSelector() + validPodTemplateSpecForManual := getValidPodTemplateSpecForManual(validManualSelector) + validGeneratedSelector := getValidGeneratedSelector() + validPodTemplateSpecForGenerated := getValidPodTemplateSpecForGenerated(validGeneratedSelector) + successCases := map[string]batch.Job{ "manual selector": { ObjectMeta: api.ObjectMeta{ @@ -73,9 +90,8 @@ func TestValidateJob(t *testing.T) { UID: types.UID("1a2b3c"), }, Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - ManualSelector: newBool(false), - Template: validPodTemplateSpecForGenerated, + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, }, }, } @@ -94,9 +110,9 @@ func TestValidateJob(t *testing.T) { UID: types.UID("1a2b3c"), }, Spec: batch.JobSpec{ - Parallelism: &negative, - ManualSelector: newBool(true), - Template: validPodTemplateSpecForGenerated, + Parallelism: &negative, + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, }, }, "spec.completions:must be greater than or equal to 0": { @@ -106,10 +122,9 @@ func TestValidateJob(t *testing.T) { UID: types.UID("1a2b3c"), }, Spec: batch.JobSpec{ - Completions: &negative, - Selector: validManualSelector, - ManualSelector: newBool(true), - Template: validPodTemplateSpecForGenerated, + Completions: &negative, + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, }, }, "spec.activeDeadlineSeconds:must be greater than or equal to 0": { @@ -120,8 +135,7 @@ func TestValidateJob(t *testing.T) { }, Spec: batch.JobSpec{ ActiveDeadlineSeconds: &negative64, - Selector: validManualSelector, - ManualSelector: newBool(true), + Selector: validGeneratedSelector, Template: validPodTemplateSpecForGenerated, }, }, @@ -290,6 +304,276 @@ func TestValidateJobUpdateStatus(t *testing.T) { } } +func TestValidateScheduledJob(t *testing.T) { + validManualSelector := getValidManualSelector() + validGeneratedSelector := getValidGeneratedSelector() + validPodTemplateSpec := getValidPodTemplateSpecForGenerated(validGeneratedSelector) + + successCases := map[string]batch.ScheduledJob{ + "basic scheduled job": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "* * * * * ?", + ConcurrencyPolicy: batch.AllowConcurrent, + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpec, + }, + }, + }, + }, + } + for k, v := range successCases { + if errs := ValidateScheduledJob(&v); len(errs) != 0 { + t.Errorf("expected success for %s: %v", k, errs) + } + } + + negative := int32(-1) + negative64 := int64(-1) + + errorCases := map[string]batch.ScheduledJob{ + "spec.schedule: Invalid value": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "error", + ConcurrencyPolicy: batch.AllowConcurrent, + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpec, + }, + }, + }, + }, + "spec.schedule: Required value": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "", + ConcurrencyPolicy: batch.AllowConcurrent, + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpec, + }, + }, + }, + }, + "spec.startingDeadlineSeconds:must be greater than or equal to 0": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "* * * * * ?", + ConcurrencyPolicy: batch.AllowConcurrent, + StartingDeadlineSeconds: &negative64, + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpec, + }, + }, + }, + }, + "spec.concurrencyPolicy: Required value": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "* * * * * ?", + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpec, + }, + }, + }, + }, + "spec.jobTemplate.spec.parallelism:must be greater than or equal to 0": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "* * * * * ?", + ConcurrencyPolicy: batch.AllowConcurrent, + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Parallelism: &negative, + Template: validPodTemplateSpec, + }, + }, + }, + }, + "spec.jobTemplate.spec.completions:must be greater than or equal to 0": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "* * * * * ?", + ConcurrencyPolicy: batch.AllowConcurrent, + JobTemplate: batch.JobTemplateSpec{ + + Spec: batch.JobSpec{ + Completions: &negative, + Selector: validGeneratedSelector, + Template: validPodTemplateSpec, + }, + }, + }, + }, + "spec.jobTemplate.spec.activeDeadlineSeconds:must be greater than or equal to 0": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "* * * * * ?", + ConcurrencyPolicy: batch.AllowConcurrent, + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + ActiveDeadlineSeconds: &negative64, + Selector: validGeneratedSelector, + Template: validPodTemplateSpec, + }, + }, + }, + }, + "spec.jobTemplate.spec.selector:Required value": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "* * * * * ?", + ConcurrencyPolicy: batch.AllowConcurrent, + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + Template: validPodTemplateSpec, + }, + }, + }, + }, + "spec.jobTemplate.spec.template.metadata.labels: Invalid value: {\"y\":\"z\"}: `selector` does not match template `labels`": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "* * * * * ?", + ConcurrencyPolicy: batch.AllowConcurrent, + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + Selector: validManualSelector, + ManualSelector: newBool(true), + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"y": "z"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + }, + }, + }, + }, + }, + "spec.jobTemplate.spec.template.metadata.labels: Invalid value: {\"controller-uid\":\"4d5e6f\"}: `selector` does not match template `labels`": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "* * * * * ?", + ConcurrencyPolicy: batch.AllowConcurrent, + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + Selector: validManualSelector, + ManualSelector: newBool(true), + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"controller-uid": "4d5e6f"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + }, + }, + }, + }, + }, + "spec.jobTemplate.spec.template.spec.restartPolicy: Unsupported value": { + ObjectMeta: api.ObjectMeta{ + Name: "myscheduledjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.ScheduledJobSpec{ + Schedule: "* * * * * ?", + ConcurrencyPolicy: batch.AllowConcurrent, + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + Selector: validManualSelector, + ManualSelector: newBool(true), + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validManualSelector.MatchLabels, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + }, + }, + }, + }, + }, + } + + for k, v := range errorCases { + errs := ValidateScheduledJob(&v) + if len(errs) == 0 { + t.Errorf("expected failure for %s", k) + } else { + s := strings.Split(k, ":") + err := errs[0] + if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) { + t.Errorf("unexpected error: %v, expected: %s", err, k) + } + } + } +} + func newBool(val bool) *bool { p := new(bool) *p = val diff --git a/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/batch_client.go b/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/batch_client.go index 58b67ceda07..83d9d749c45 100644 --- a/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/batch_client.go +++ b/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/batch_client.go @@ -25,6 +25,7 @@ import ( type BatchInterface interface { GetRESTClient() *restclient.RESTClient JobsGetter + ScheduledJobsGetter } // BatchClient is used to interact with features provided by the Batch group. @@ -36,6 +37,10 @@ func (c *BatchClient) Jobs(namespace string) JobInterface { return newJobs(c, namespace) } +func (c *BatchClient) ScheduledJobs(namespace string) ScheduledJobInterface { + return newScheduledJobs(c, namespace) +} + // NewForConfig creates a new BatchClient for the given config. func NewForConfig(c *restclient.Config) (*BatchClient, error) { config := *c diff --git a/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/fake/fake_batch_client.go b/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/fake/fake_batch_client.go index 8845934671c..bf2a41a76a2 100644 --- a/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/fake/fake_batch_client.go +++ b/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/fake/fake_batch_client.go @@ -30,6 +30,10 @@ func (c *FakeBatch) Jobs(namespace string) unversioned.JobInterface { return &FakeJobs{c, namespace} } +func (c *FakeBatch) ScheduledJobs(namespace string) unversioned.ScheduledJobInterface { + return &FakeScheduledJobs{c, namespace} +} + // GetRESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeBatch) GetRESTClient() *restclient.RESTClient { diff --git a/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/fake/fake_scheduledjob.go b/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/fake/fake_scheduledjob.go new file mode 100644 index 00000000000..db09aa9bc96 --- /dev/null +++ b/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/fake/fake_scheduledjob.go @@ -0,0 +1,116 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fake + +import ( + api "k8s.io/kubernetes/pkg/api" + unversioned "k8s.io/kubernetes/pkg/api/unversioned" + batch "k8s.io/kubernetes/pkg/apis/batch" + core "k8s.io/kubernetes/pkg/client/testing/core" + labels "k8s.io/kubernetes/pkg/labels" + watch "k8s.io/kubernetes/pkg/watch" +) + +// FakeScheduledJobs implements ScheduledJobInterface +type FakeScheduledJobs struct { + Fake *FakeBatch + ns string +} + +var scheduledjobsResource = unversioned.GroupVersionResource{Group: "batch", Version: "", Resource: "scheduledjobs"} + +func (c *FakeScheduledJobs) Create(scheduledJob *batch.ScheduledJob) (result *batch.ScheduledJob, err error) { + obj, err := c.Fake. + Invokes(core.NewCreateAction(scheduledjobsResource, c.ns, scheduledJob), &batch.ScheduledJob{}) + + if obj == nil { + return nil, err + } + return obj.(*batch.ScheduledJob), err +} + +func (c *FakeScheduledJobs) Update(scheduledJob *batch.ScheduledJob) (result *batch.ScheduledJob, err error) { + obj, err := c.Fake. + Invokes(core.NewUpdateAction(scheduledjobsResource, c.ns, scheduledJob), &batch.ScheduledJob{}) + + if obj == nil { + return nil, err + } + return obj.(*batch.ScheduledJob), err +} + +func (c *FakeScheduledJobs) UpdateStatus(scheduledJob *batch.ScheduledJob) (*batch.ScheduledJob, error) { + obj, err := c.Fake. + Invokes(core.NewUpdateSubresourceAction(scheduledjobsResource, "status", c.ns, scheduledJob), &batch.ScheduledJob{}) + + if obj == nil { + return nil, err + } + return obj.(*batch.ScheduledJob), err +} + +func (c *FakeScheduledJobs) Delete(name string, options *api.DeleteOptions) error { + _, err := c.Fake. + Invokes(core.NewDeleteAction(scheduledjobsResource, c.ns, name), &batch.ScheduledJob{}) + + return err +} + +func (c *FakeScheduledJobs) DeleteCollection(options *api.DeleteOptions, listOptions api.ListOptions) error { + action := core.NewDeleteCollectionAction(scheduledjobsResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &batch.ScheduledJobList{}) + return err +} + +func (c *FakeScheduledJobs) Get(name string) (result *batch.ScheduledJob, err error) { + obj, err := c.Fake. + Invokes(core.NewGetAction(scheduledjobsResource, c.ns, name), &batch.ScheduledJob{}) + + if obj == nil { + return nil, err + } + return obj.(*batch.ScheduledJob), err +} + +func (c *FakeScheduledJobs) List(opts api.ListOptions) (result *batch.ScheduledJobList, err error) { + obj, err := c.Fake. + Invokes(core.NewListAction(scheduledjobsResource, c.ns, opts), &batch.ScheduledJobList{}) + + if obj == nil { + return nil, err + } + + label := opts.LabelSelector + if label == nil { + label = labels.Everything() + } + list := &batch.ScheduledJobList{} + for _, item := range obj.(*batch.ScheduledJobList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested scheduledJobs. +func (c *FakeScheduledJobs) Watch(opts api.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(core.NewWatchAction(scheduledjobsResource, c.ns, opts)) + +} diff --git a/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/generated_expansion.go b/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/generated_expansion.go index 08c0e265d29..f876ef63fc2 100644 --- a/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/generated_expansion.go +++ b/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/generated_expansion.go @@ -17,3 +17,5 @@ limitations under the License. package unversioned type JobExpansion interface{} + +type ScheduledJobExpansion interface{} diff --git a/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/scheduledjob.go b/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/scheduledjob.go new file mode 100644 index 00000000000..2675d11c48f --- /dev/null +++ b/pkg/client/clientset_generated/internalclientset/typed/batch/unversioned/scheduledjob.go @@ -0,0 +1,150 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package unversioned + +import ( + api "k8s.io/kubernetes/pkg/api" + batch "k8s.io/kubernetes/pkg/apis/batch" + watch "k8s.io/kubernetes/pkg/watch" +) + +// ScheduledJobsGetter has a method to return a ScheduledJobInterface. +// A group's client should implement this interface. +type ScheduledJobsGetter interface { + ScheduledJobs(namespace string) ScheduledJobInterface +} + +// ScheduledJobInterface has methods to work with ScheduledJob resources. +type ScheduledJobInterface interface { + Create(*batch.ScheduledJob) (*batch.ScheduledJob, error) + Update(*batch.ScheduledJob) (*batch.ScheduledJob, error) + UpdateStatus(*batch.ScheduledJob) (*batch.ScheduledJob, error) + Delete(name string, options *api.DeleteOptions) error + DeleteCollection(options *api.DeleteOptions, listOptions api.ListOptions) error + Get(name string) (*batch.ScheduledJob, error) + List(opts api.ListOptions) (*batch.ScheduledJobList, error) + Watch(opts api.ListOptions) (watch.Interface, error) + ScheduledJobExpansion +} + +// scheduledJobs implements ScheduledJobInterface +type scheduledJobs struct { + client *BatchClient + ns string +} + +// newScheduledJobs returns a ScheduledJobs +func newScheduledJobs(c *BatchClient, namespace string) *scheduledJobs { + return &scheduledJobs{ + client: c, + ns: namespace, + } +} + +// Create takes the representation of a scheduledJob and creates it. Returns the server's representation of the scheduledJob, and an error, if there is any. +func (c *scheduledJobs) Create(scheduledJob *batch.ScheduledJob) (result *batch.ScheduledJob, err error) { + result = &batch.ScheduledJob{} + err = c.client.Post(). + Namespace(c.ns). + Resource("scheduledjobs"). + Body(scheduledJob). + Do(). + Into(result) + return +} + +// Update takes the representation of a scheduledJob and updates it. Returns the server's representation of the scheduledJob, and an error, if there is any. +func (c *scheduledJobs) Update(scheduledJob *batch.ScheduledJob) (result *batch.ScheduledJob, err error) { + result = &batch.ScheduledJob{} + err = c.client.Put(). + Namespace(c.ns). + Resource("scheduledjobs"). + Name(scheduledJob.Name). + Body(scheduledJob). + Do(). + Into(result) + return +} + +func (c *scheduledJobs) UpdateStatus(scheduledJob *batch.ScheduledJob) (result *batch.ScheduledJob, err error) { + result = &batch.ScheduledJob{} + err = c.client.Put(). + Namespace(c.ns). + Resource("scheduledjobs"). + Name(scheduledJob.Name). + SubResource("status"). + Body(scheduledJob). + Do(). + Into(result) + return +} + +// Delete takes name of the scheduledJob and deletes it. Returns an error if one occurs. +func (c *scheduledJobs) Delete(name string, options *api.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("scheduledjobs"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *scheduledJobs) DeleteCollection(options *api.DeleteOptions, listOptions api.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("scheduledjobs"). + VersionedParams(&listOptions, api.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Get takes name of the scheduledJob, and returns the corresponding scheduledJob object, and an error if there is any. +func (c *scheduledJobs) Get(name string) (result *batch.ScheduledJob, err error) { + result = &batch.ScheduledJob{} + err = c.client.Get(). + Namespace(c.ns). + Resource("scheduledjobs"). + Name(name). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ScheduledJobs that match those selectors. +func (c *scheduledJobs) List(opts api.ListOptions) (result *batch.ScheduledJobList, err error) { + result = &batch.ScheduledJobList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("scheduledjobs"). + VersionedParams(&opts, api.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested scheduledJobs. +func (c *scheduledJobs) Watch(opts api.ListOptions) (watch.Interface, error) { + return c.client.Get(). + Prefix("watch"). + Namespace(c.ns). + Resource("scheduledjobs"). + VersionedParams(&opts, api.ParameterCodec). + Watch() +} diff --git a/pkg/client/unversioned/batch.go b/pkg/client/unversioned/batch.go index c228ccf1e0b..40fc49dc121 100644 --- a/pkg/client/unversioned/batch.go +++ b/pkg/client/unversioned/batch.go @@ -27,6 +27,7 @@ import ( type BatchInterface interface { JobsNamespacer + ScheduledJobsNamespacer } // BatchClient is used to interact with Kubernetes batch features. @@ -38,6 +39,10 @@ func (c *BatchClient) Jobs(namespace string) JobInterface { return newJobsV1(c, namespace) } +func (c *BatchClient) ScheduledJobs(namespace string) ScheduledJobInterface { + return newScheduledJobs(c, namespace) +} + func NewBatch(c *restclient.Config) (*BatchClient, error) { config := *c if err := setBatchDefaults(&config, nil); err != nil { diff --git a/pkg/client/unversioned/scheduledjobs.go b/pkg/client/unversioned/scheduledjobs.go new file mode 100644 index 00000000000..d2b83fce200 --- /dev/null +++ b/pkg/client/unversioned/scheduledjobs.go @@ -0,0 +1,103 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package unversioned + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/batch" + "k8s.io/kubernetes/pkg/watch" +) + +// ScheduledJobsNamespacer has methods to work with ScheduledJob resources in a namespace +type ScheduledJobsNamespacer interface { + ScheduledJobs(namespace string) ScheduledJobInterface +} + +// ScheduledJobInterface exposes methods to work on ScheduledJob resources. +type ScheduledJobInterface interface { + List(opts api.ListOptions) (*batch.ScheduledJobList, error) + Get(name string) (*batch.ScheduledJob, error) + Create(scheduledJob *batch.ScheduledJob) (*batch.ScheduledJob, error) + Update(scheduledJob *batch.ScheduledJob) (*batch.ScheduledJob, error) + Delete(name string, options *api.DeleteOptions) error + Watch(opts api.ListOptions) (watch.Interface, error) + UpdateStatus(scheduledJob *batch.ScheduledJob) (*batch.ScheduledJob, error) +} + +// scheduledJobs implements ScheduledJobsNamespacer interface +type scheduledJobs struct { + r *BatchClient + ns string +} + +// newScheduledJobs returns a scheduledJobs +func newScheduledJobs(c *BatchClient, namespace string) *scheduledJobs { + return &scheduledJobs{c, namespace} +} + +// Ensure statically that scheduledJobs implements ScheduledJobInterface. +var _ ScheduledJobInterface = &scheduledJobs{} + +// List returns a list of scheduled jobs that match the label and field selectors. +func (c *scheduledJobs) List(opts api.ListOptions) (result *batch.ScheduledJobList, err error) { + result = &batch.ScheduledJobList{} + err = c.r.Get().Namespace(c.ns).Resource("scheduledJobs").VersionedParams(&opts, api.ParameterCodec).Do().Into(result) + return +} + +// Get returns information about a particular scheduled job. +func (c *scheduledJobs) Get(name string) (result *batch.ScheduledJob, err error) { + result = &batch.ScheduledJob{} + err = c.r.Get().Namespace(c.ns).Resource("scheduledJobs").Name(name).Do().Into(result) + return +} + +// Create creates a new scheduled job. +func (c *scheduledJobs) Create(job *batch.ScheduledJob) (result *batch.ScheduledJob, err error) { + result = &batch.ScheduledJob{} + err = c.r.Post().Namespace(c.ns).Resource("scheduledJobs").Body(job).Do().Into(result) + return +} + +// Update updates an existing scheduled job. +func (c *scheduledJobs) Update(job *batch.ScheduledJob) (result *batch.ScheduledJob, err error) { + result = &batch.ScheduledJob{} + err = c.r.Put().Namespace(c.ns).Resource("scheduledJobs").Name(job.Name).Body(job).Do().Into(result) + return +} + +// Delete deletes a scheduled job, returns error if one occurs. +func (c *scheduledJobs) Delete(name string, options *api.DeleteOptions) (err error) { + return c.r.Delete().Namespace(c.ns).Resource("scheduledJobs").Name(name).Body(options).Do().Error() +} + +// Watch returns a watch.Interface that watches the requested scheduled jobs. +func (c *scheduledJobs) Watch(opts api.ListOptions) (watch.Interface, error) { + return c.r.Get(). + Prefix("watch"). + Namespace(c.ns). + Resource("scheduledJobs"). + VersionedParams(&opts, api.ParameterCodec). + Watch() +} + +// UpdateStatus takes the name of the scheduled job and the new status. Returns the server's representation of the scheduled job, and an error, if it occurs. +func (c *scheduledJobs) UpdateStatus(job *batch.ScheduledJob) (result *batch.ScheduledJob, err error) { + result = &batch.ScheduledJob{} + err = c.r.Put().Namespace(c.ns).Resource("scheduledJobs").Name(job.Name).SubResource("status").Body(job).Do().Into(result) + return +} diff --git a/pkg/client/unversioned/testclient/fake_scheduledjobs.go b/pkg/client/unversioned/testclient/fake_scheduledjobs.go new file mode 100644 index 00000000000..7036682bac3 --- /dev/null +++ b/pkg/client/unversioned/testclient/fake_scheduledjobs.go @@ -0,0 +1,84 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testclient + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/batch" + "k8s.io/kubernetes/pkg/watch" +) + +// FakeScheduledJobs implements ScheduledJobInterface. Meant to be embedded into a struct to get a default +// implementation. This makes faking out just the methods you want to test easier. +type FakeScheduledJobs struct { + Fake *FakeBatch + Namespace string +} + +func (c *FakeScheduledJobs) Get(name string) (*batch.ScheduledJob, error) { + obj, err := c.Fake.Invokes(NewGetAction("scheduledjobs", c.Namespace, name), &batch.ScheduledJob{}) + if obj == nil { + return nil, err + } + + return obj.(*batch.ScheduledJob), err +} + +func (c *FakeScheduledJobs) List(opts api.ListOptions) (*batch.ScheduledJobList, error) { + obj, err := c.Fake.Invokes(NewListAction("scheduledjobs", c.Namespace, opts), &batch.ScheduledJobList{}) + if obj == nil { + return nil, err + } + + return obj.(*batch.ScheduledJobList), err +} + +func (c *FakeScheduledJobs) Create(scheduledJob *batch.ScheduledJob) (*batch.ScheduledJob, error) { + obj, err := c.Fake.Invokes(NewCreateAction("scheduledjobs", c.Namespace, scheduledJob), scheduledJob) + if obj == nil { + return nil, err + } + + return obj.(*batch.ScheduledJob), err +} + +func (c *FakeScheduledJobs) Update(scheduledJob *batch.ScheduledJob) (*batch.ScheduledJob, error) { + obj, err := c.Fake.Invokes(NewUpdateAction("scheduledjobs", c.Namespace, scheduledJob), scheduledJob) + if obj == nil { + return nil, err + } + + return obj.(*batch.ScheduledJob), err +} + +func (c *FakeScheduledJobs) Delete(name string, options *api.DeleteOptions) error { + _, err := c.Fake.Invokes(NewDeleteAction("scheduledjobs", c.Namespace, name), &batch.ScheduledJob{}) + return err +} + +func (c *FakeScheduledJobs) Watch(opts api.ListOptions) (watch.Interface, error) { + return c.Fake.InvokesWatch(NewWatchAction("scheduledjobs", c.Namespace, opts)) +} + +func (c *FakeScheduledJobs) UpdateStatus(scheduledJob *batch.ScheduledJob) (result *batch.ScheduledJob, err error) { + obj, err := c.Fake.Invokes(NewUpdateSubresourceAction("scheduledjobs", "status", c.Namespace, scheduledJob), scheduledJob) + if obj == nil { + return nil, err + } + + return obj.(*batch.ScheduledJob), err +} diff --git a/pkg/client/unversioned/testclient/testclient.go b/pkg/client/unversioned/testclient/testclient.go index c2898f1892e..55368719782 100644 --- a/pkg/client/unversioned/testclient/testclient.go +++ b/pkg/client/unversioned/testclient/testclient.go @@ -341,6 +341,10 @@ func (c *FakeBatch) Jobs(namespace string) client.JobInterface { return &FakeJobsV1{Fake: c, Namespace: namespace} } +func (c *FakeBatch) ScheduledJobs(namespace string) client.ScheduledJobInterface { + return &FakeScheduledJobs{Fake: c, Namespace: namespace} +} + // NewSimpleFakeExp returns a client that will respond with the provided objects func NewSimpleFakeExp(objects ...runtime.Object) *FakeExperimental { return &FakeExperimental{Fake: NewSimpleFake(objects...)} diff --git a/pkg/kubectl/scale_test.go b/pkg/kubectl/scale_test.go index f3ccb3ac98a..0dc55cda18e 100644 --- a/pkg/kubectl/scale_test.go +++ b/pkg/kubectl/scale_test.go @@ -249,7 +249,7 @@ func TestValidateReplicationController(t *testing.T) { } type ErrorJobs struct { - testclient.FakeJobs + testclient.FakeJobsV1 invalid bool } @@ -270,16 +270,16 @@ func (c *ErrorJobs) Get(name string) (*batch.Job, error) { } type ErrorJobClient struct { - testclient.FakeExperimental + testclient.FakeBatch invalid bool } func (c *ErrorJobClient) Jobs(namespace string) client.JobInterface { - return &ErrorJobs{testclient.FakeJobs{Fake: &c.FakeExperimental, Namespace: namespace}, c.invalid} + return &ErrorJobs{testclient.FakeJobsV1{Fake: &c.FakeBatch, Namespace: namespace}, c.invalid} } func TestJobScaleRetry(t *testing.T) { - fake := &ErrorJobClient{FakeExperimental: testclient.FakeExperimental{}, invalid: false} + fake := &ErrorJobClient{FakeBatch: testclient.FakeBatch{}, invalid: false} scaler := JobScaler{fake} preconditions := ScalePrecondition{-1, ""} count := uint(3) @@ -303,7 +303,7 @@ func TestJobScaleRetry(t *testing.T) { } func TestJobScale(t *testing.T) { - fake := &testclient.FakeExperimental{Fake: &testclient.Fake{}} + fake := &testclient.FakeBatch{Fake: &testclient.Fake{}} scaler := JobScaler{fake} preconditions := ScalePrecondition{-1, ""} count := uint(3) @@ -323,7 +323,7 @@ func TestJobScale(t *testing.T) { } func TestJobScaleInvalid(t *testing.T) { - fake := &ErrorJobClient{FakeExperimental: testclient.FakeExperimental{}, invalid: true} + fake := &ErrorJobClient{FakeBatch: testclient.FakeBatch{}, invalid: true} scaler := JobScaler{fake} preconditions := ScalePrecondition{-1, ""} count := uint(3) @@ -348,7 +348,7 @@ func TestJobScaleFailsPreconditions(t *testing.T) { Parallelism: &ten, }, }) - scaler := JobScaler{&testclient.FakeExperimental{Fake: fake}} + scaler := JobScaler{&testclient.FakeBatch{Fake: fake}} preconditions := ScalePrecondition{2, ""} count := uint(3) name := "foo" diff --git a/vendor/github.com/robfig/cron/.gitignore b/vendor/github.com/robfig/cron/.gitignore new file mode 100644 index 00000000000..00268614f04 --- /dev/null +++ b/vendor/github.com/robfig/cron/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/github.com/robfig/cron/.travis.yml b/vendor/github.com/robfig/cron/.travis.yml new file mode 100644 index 00000000000..4f2ee4d9733 --- /dev/null +++ b/vendor/github.com/robfig/cron/.travis.yml @@ -0,0 +1 @@ +language: go diff --git a/vendor/github.com/robfig/cron/LICENSE b/vendor/github.com/robfig/cron/LICENSE new file mode 100644 index 00000000000..3a0f627ffeb --- /dev/null +++ b/vendor/github.com/robfig/cron/LICENSE @@ -0,0 +1,21 @@ +Copyright (C) 2012 Rob Figueiredo +All Rights Reserved. + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/robfig/cron/README.md b/vendor/github.com/robfig/cron/README.md new file mode 100644 index 00000000000..e43638ff437 --- /dev/null +++ b/vendor/github.com/robfig/cron/README.md @@ -0,0 +1,2 @@ +[![GoDoc](http://godoc.org/github.com/robfig/cron?status.png)](http://godoc.org/github.com/robfig/cron) +[![Build Status](https://travis-ci.org/robfig/cron.svg?branch=master)](https://travis-ci.org/robfig/cron) diff --git a/vendor/github.com/robfig/cron/constantdelay.go b/vendor/github.com/robfig/cron/constantdelay.go new file mode 100644 index 00000000000..cd6e7b1be91 --- /dev/null +++ b/vendor/github.com/robfig/cron/constantdelay.go @@ -0,0 +1,27 @@ +package cron + +import "time" + +// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". +// It does not support jobs more frequent than once a second. +type ConstantDelaySchedule struct { + Delay time.Duration +} + +// Every returns a crontab Schedule that activates once every duration. +// Delays of less than a second are not supported (will round up to 1 second). +// Any fields less than a Second are truncated. +func Every(duration time.Duration) ConstantDelaySchedule { + if duration < time.Second { + duration = time.Second + } + return ConstantDelaySchedule{ + Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, + } +} + +// Next returns the next time this should be run. +// This rounds so that the next activation time will be on the second. +func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { + return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) +} diff --git a/vendor/github.com/robfig/cron/cron.go b/vendor/github.com/robfig/cron/cron.go new file mode 100644 index 00000000000..9c1e6c31a5c --- /dev/null +++ b/vendor/github.com/robfig/cron/cron.go @@ -0,0 +1,227 @@ +// This library implements a cron spec parser and runner. See the README for +// more details. +package cron + +import ( + "log" + "runtime" + "sort" + "time" +) + +// Cron keeps track of any number of entries, invoking the associated func as +// specified by the schedule. It may be started, stopped, and the entries may +// be inspected while running. +type Cron struct { + entries []*Entry + stop chan struct{} + add chan *Entry + snapshot chan []*Entry + running bool + ErrorLog *log.Logger +} + +// Job is an interface for submitted cron jobs. +type Job interface { + Run() +} + +// The Schedule describes a job's duty cycle. +type Schedule interface { + // Return the next activation time, later than the given time. + // Next is invoked initially, and then each time the job is run. + Next(time.Time) time.Time +} + +// Entry consists of a schedule and the func to execute on that schedule. +type Entry struct { + // The schedule on which this job should be run. + Schedule Schedule + + // The next time the job will run. This is the zero time if Cron has not been + // started or this entry's schedule is unsatisfiable + Next time.Time + + // The last time this job was run. This is the zero time if the job has never + // been run. + Prev time.Time + + // The Job to run. + Job Job +} + +// byTime is a wrapper for sorting the entry array by time +// (with zero time at the end). +type byTime []*Entry + +func (s byTime) Len() int { return len(s) } +func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byTime) Less(i, j int) bool { + // Two zero times should return false. + // Otherwise, zero is "greater" than any other time. + // (To sort it at the end of the list.) + if s[i].Next.IsZero() { + return false + } + if s[j].Next.IsZero() { + return true + } + return s[i].Next.Before(s[j].Next) +} + +// New returns a new Cron job runner. +func New() *Cron { + return &Cron{ + entries: nil, + add: make(chan *Entry), + stop: make(chan struct{}), + snapshot: make(chan []*Entry), + running: false, + ErrorLog: nil, + } +} + +// A wrapper that turns a func() into a cron.Job +type FuncJob func() + +func (f FuncJob) Run() { f() } + +// AddFunc adds a func to the Cron to be run on the given schedule. +func (c *Cron) AddFunc(spec string, cmd func()) error { + return c.AddJob(spec, FuncJob(cmd)) +} + +// AddJob adds a Job to the Cron to be run on the given schedule. +func (c *Cron) AddJob(spec string, cmd Job) error { + schedule, err := Parse(spec) + if err != nil { + return err + } + c.Schedule(schedule, cmd) + return nil +} + +// Schedule adds a Job to the Cron to be run on the given schedule. +func (c *Cron) Schedule(schedule Schedule, cmd Job) { + entry := &Entry{ + Schedule: schedule, + Job: cmd, + } + if !c.running { + c.entries = append(c.entries, entry) + return + } + + c.add <- entry +} + +// Entries returns a snapshot of the cron entries. +func (c *Cron) Entries() []*Entry { + if c.running { + c.snapshot <- nil + x := <-c.snapshot + return x + } + return c.entrySnapshot() +} + +// Start the cron scheduler in its own go-routine. +func (c *Cron) Start() { + c.running = true + go c.run() +} + +func (c *Cron) runWithRecovery(j Job) { + defer func() { + if r := recover(); r != nil { + const size = 64 << 10 + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + c.logf("cron: panic running job: %v\n%s", r, buf) + } + }() + j.Run() +} + +// Run the scheduler.. this is private just due to the need to synchronize +// access to the 'running' state variable. +func (c *Cron) run() { + // Figure out the next activation times for each entry. + now := time.Now().Local() + for _, entry := range c.entries { + entry.Next = entry.Schedule.Next(now) + } + + for { + // Determine the next entry to run. + sort.Sort(byTime(c.entries)) + + var effective time.Time + if len(c.entries) == 0 || c.entries[0].Next.IsZero() { + // If there are no entries yet, just sleep - it still handles new entries + // and stop requests. + effective = now.AddDate(10, 0, 0) + } else { + effective = c.entries[0].Next + } + + select { + case now = <-time.After(effective.Sub(now)): + // Run every entry whose next time was this effective time. + for _, e := range c.entries { + if e.Next != effective { + break + } + go c.runWithRecovery(e.Job) + e.Prev = e.Next + e.Next = e.Schedule.Next(effective) + } + continue + + case newEntry := <-c.add: + c.entries = append(c.entries, newEntry) + newEntry.Next = newEntry.Schedule.Next(time.Now().Local()) + + case <-c.snapshot: + c.snapshot <- c.entrySnapshot() + + case <-c.stop: + return + } + + // 'now' should be updated after newEntry and snapshot cases. + now = time.Now().Local() + } +} + +// Logs an error to stderr or to the configured error log +func (c *Cron) logf(format string, args ...interface{}) { + if c.ErrorLog != nil { + c.ErrorLog.Printf(format, args...) + } else { + log.Printf(format, args...) + } +} + +// Stop stops the cron scheduler if it is running; otherwise it does nothing. +func (c *Cron) Stop() { + if !c.running { + return + } + c.stop <- struct{}{} + c.running = false +} + +// entrySnapshot returns a copy of the current cron entry list. +func (c *Cron) entrySnapshot() []*Entry { + entries := []*Entry{} + for _, e := range c.entries { + entries = append(entries, &Entry{ + Schedule: e.Schedule, + Next: e.Next, + Prev: e.Prev, + Job: e.Job, + }) + } + return entries +} diff --git a/vendor/github.com/robfig/cron/doc.go b/vendor/github.com/robfig/cron/doc.go new file mode 100644 index 00000000000..dbdf50127a5 --- /dev/null +++ b/vendor/github.com/robfig/cron/doc.go @@ -0,0 +1,129 @@ +/* +Package cron implements a cron spec parser and job runner. + +Usage + +Callers may register Funcs to be invoked on a given schedule. Cron will run +them in their own goroutines. + + c := cron.New() + c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) + c.AddFunc("@hourly", func() { fmt.Println("Every hour") }) + c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }) + c.Start() + .. + // Funcs are invoked in their own goroutine, asynchronously. + ... + // Funcs may also be added to a running Cron + c.AddFunc("@daily", func() { fmt.Println("Every day") }) + .. + // Inspect the cron job entries' next and previous run times. + inspect(c.Entries()) + .. + c.Stop() // Stop the scheduler (does not stop any jobs already running). + +CRON Expression Format + +A cron expression represents a set of times, using 6 space-separated fields. + + Field name | Mandatory? | Allowed values | Allowed special characters + ---------- | ---------- | -------------- | -------------------------- + Seconds | Yes | 0-59 | * / , - + Minutes | Yes | 0-59 | * / , - + Hours | Yes | 0-23 | * / , - + Day of month | Yes | 1-31 | * / , - ? + Month | Yes | 1-12 or JAN-DEC | * / , - + Day of week | Yes | 0-6 or SUN-SAT | * / , - ? + +Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun", +and "sun" are equally accepted. + +Special Characters + +Asterisk ( * ) + +The asterisk indicates that the cron expression will match for all values of the +field; e.g., using an asterisk in the 5th field (month) would indicate every +month. + +Slash ( / ) + +Slashes are used to describe increments of ranges. For example 3-59/15 in the +1st field (minutes) would indicate the 3rd minute of the hour and every 15 +minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...", +that is, an increment over the largest possible range of the field. The form +"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the +increment until the end of that specific range. It does not wrap around. + +Comma ( , ) + +Commas are used to separate items of a list. For example, using "MON,WED,FRI" in +the 5th field (day of week) would mean Mondays, Wednesdays and Fridays. + +Hyphen ( - ) + +Hyphens are used to define ranges. For example, 9-17 would indicate every +hour between 9am and 5pm inclusive. + +Question mark ( ? ) + +Question mark may be used instead of '*' for leaving either day-of-month or +day-of-week blank. + +Predefined schedules + +You may use one of several pre-defined schedules in place of a cron expression. + + Entry | Description | Equivalent To + ----- | ----------- | ------------- + @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * + @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * + @weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 + @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * + @hourly | Run once an hour, beginning of hour | 0 0 * * * * + +Intervals + +You may also schedule a job to execute at fixed intervals. This is supported by +formatting the cron spec like this: + + @every + +where "duration" is a string accepted by time.ParseDuration +(http://golang.org/pkg/time/#ParseDuration). + +For example, "@every 1h30m10s" would indicate a schedule that activates every +1 hour, 30 minutes, 10 seconds. + +Note: The interval does not take the job runtime into account. For example, +if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, +it will have only 2 minutes of idle time between each run. + +Time zones + +All interpretation and scheduling is done in the machine's local time zone (as +provided by the Go time package (http://www.golang.org/pkg/time). + +Be aware that jobs scheduled during daylight-savings leap-ahead transitions will +not be run! + +Thread safety + +Since the Cron service runs concurrently with the calling code, some amount of +care must be taken to ensure proper synchronization. + +All cron methods are designed to be correctly synchronized as long as the caller +ensures that invocations have a clear happens-before ordering between them. + +Implementation + +Cron entries are stored in an array, sorted by their next activation time. Cron +sleeps until the next job is due to be run. + +Upon waking: + - it runs each entry that is active on that second + - it calculates the next run times for the jobs that were run + - it re-sorts the array of entries by next activation time. + - it goes to sleep until the soonest job. +*/ +package cron diff --git a/vendor/github.com/robfig/cron/parser.go b/vendor/github.com/robfig/cron/parser.go new file mode 100644 index 00000000000..4224fa93083 --- /dev/null +++ b/vendor/github.com/robfig/cron/parser.go @@ -0,0 +1,231 @@ +package cron + +import ( + "fmt" + "log" + "math" + "strconv" + "strings" + "time" +) + +// Parse returns a new crontab schedule representing the given spec. +// It returns a descriptive error if the spec is not valid. +// +// It accepts +// - Full crontab specs, e.g. "* * * * * ?" +// - Descriptors, e.g. "@midnight", "@every 1h30m" +func Parse(spec string) (_ Schedule, err error) { + // Convert panics into errors + defer func() { + if recovered := recover(); recovered != nil { + err = fmt.Errorf("%v", recovered) + } + }() + + if spec[0] == '@' { + return parseDescriptor(spec), nil + } + + // Split on whitespace. We require 5 or 6 fields. + // (second) (minute) (hour) (day of month) (month) (day of week, optional) + fields := strings.Fields(spec) + if len(fields) != 5 && len(fields) != 6 { + log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec) + } + + // If a sixth field is not provided (DayOfWeek), then it is equivalent to star. + if len(fields) == 5 { + fields = append(fields, "*") + } + + schedule := &SpecSchedule{ + Second: getField(fields[0], seconds), + Minute: getField(fields[1], minutes), + Hour: getField(fields[2], hours), + Dom: getField(fields[3], dom), + Month: getField(fields[4], months), + Dow: getField(fields[5], dow), + } + + return schedule, nil +} + +// getField returns an Int with the bits set representing all of the times that +// the field represents. A "field" is a comma-separated list of "ranges". +func getField(field string, r bounds) uint64 { + // list = range {"," range} + var bits uint64 + ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) + for _, expr := range ranges { + bits |= getRange(expr, r) + } + return bits +} + +// getRange returns the bits indicated by the given expression: +// number | number "-" number [ "/" number ] +func getRange(expr string, r bounds) uint64 { + + var ( + start, end, step uint + rangeAndStep = strings.Split(expr, "/") + lowAndHigh = strings.Split(rangeAndStep[0], "-") + singleDigit = len(lowAndHigh) == 1 + ) + + var extra_star uint64 + if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { + start = r.min + end = r.max + extra_star = starBit + } else { + start = parseIntOrName(lowAndHigh[0], r.names) + switch len(lowAndHigh) { + case 1: + end = start + case 2: + end = parseIntOrName(lowAndHigh[1], r.names) + default: + log.Panicf("Too many hyphens: %s", expr) + } + } + + switch len(rangeAndStep) { + case 1: + step = 1 + case 2: + step = mustParseInt(rangeAndStep[1]) + + // Special handling: "N/step" means "N-max/step". + if singleDigit { + end = r.max + } + default: + log.Panicf("Too many slashes: %s", expr) + } + + if start < r.min { + log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr) + } + if end > r.max { + log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr) + } + if start > end { + log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr) + } + + return getBits(start, end, step) | extra_star +} + +// parseIntOrName returns the (possibly-named) integer contained in expr. +func parseIntOrName(expr string, names map[string]uint) uint { + if names != nil { + if namedInt, ok := names[strings.ToLower(expr)]; ok { + return namedInt + } + } + return mustParseInt(expr) +} + +// mustParseInt parses the given expression as an int or panics. +func mustParseInt(expr string) uint { + num, err := strconv.Atoi(expr) + if err != nil { + log.Panicf("Failed to parse int from %s: %s", expr, err) + } + if num < 0 { + log.Panicf("Negative number (%d) not allowed: %s", num, expr) + } + + return uint(num) +} + +// getBits sets all bits in the range [min, max], modulo the given step size. +func getBits(min, max, step uint) uint64 { + var bits uint64 + + // If step is 1, use shifts. + if step == 1 { + return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) + } + + // Else, use a simple loop. + for i := min; i <= max; i += step { + bits |= 1 << i + } + return bits +} + +// all returns all bits within the given bounds. (plus the star bit) +func all(r bounds) uint64 { + return getBits(r.min, r.max, 1) | starBit +} + +// parseDescriptor returns a pre-defined schedule for the expression, or panics +// if none matches. +func parseDescriptor(spec string) Schedule { + switch spec { + case "@yearly", "@annually": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: 1 << dom.min, + Month: 1 << months.min, + Dow: all(dow), + } + + case "@monthly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: 1 << dom.min, + Month: all(months), + Dow: all(dow), + } + + case "@weekly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: all(dom), + Month: all(months), + Dow: 1 << dow.min, + } + + case "@daily", "@midnight": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: all(dom), + Month: all(months), + Dow: all(dow), + } + + case "@hourly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: all(hours), + Dom: all(dom), + Month: all(months), + Dow: all(dow), + } + } + + const every = "@every " + if strings.HasPrefix(spec, every) { + duration, err := time.ParseDuration(spec[len(every):]) + if err != nil { + log.Panicf("Failed to parse duration %s: %s", spec, err) + } + return Every(duration) + } + + log.Panicf("Unrecognized descriptor: %s", spec) + return nil +} diff --git a/vendor/github.com/robfig/cron/spec.go b/vendor/github.com/robfig/cron/spec.go new file mode 100644 index 00000000000..afa5ac86cc3 --- /dev/null +++ b/vendor/github.com/robfig/cron/spec.go @@ -0,0 +1,159 @@ +package cron + +import "time" + +// SpecSchedule specifies a duty cycle (to the second granularity), based on a +// traditional crontab specification. It is computed initially and stored as bit sets. +type SpecSchedule struct { + Second, Minute, Hour, Dom, Month, Dow uint64 +} + +// bounds provides a range of acceptable values (plus a map of name to value). +type bounds struct { + min, max uint + names map[string]uint +} + +// The bounds for each field. +var ( + seconds = bounds{0, 59, nil} + minutes = bounds{0, 59, nil} + hours = bounds{0, 23, nil} + dom = bounds{1, 31, nil} + months = bounds{1, 12, map[string]uint{ + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "may": 5, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "oct": 10, + "nov": 11, + "dec": 12, + }} + dow = bounds{0, 6, map[string]uint{ + "sun": 0, + "mon": 1, + "tue": 2, + "wed": 3, + "thu": 4, + "fri": 5, + "sat": 6, + }} +) + +const ( + // Set the top bit if a star was included in the expression. + starBit = 1 << 63 +) + +// Next returns the next time this schedule is activated, greater than the given +// time. If no time can be found to satisfy the schedule, return the zero time. +func (s *SpecSchedule) Next(t time.Time) time.Time { + // General approach: + // For Month, Day, Hour, Minute, Second: + // Check if the time value matches. If yes, continue to the next field. + // If the field doesn't match the schedule, then increment the field until it matches. + // While incrementing the field, a wrap-around brings it back to the beginning + // of the field list (since it is necessary to re-verify previous field + // values) + + // Start at the earliest possible time (the upcoming second). + t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) + + // This flag indicates whether a field has been incremented. + added := false + + // If no time is found within five years, return zero. + yearLimit := t.Year() + 5 + +WRAP: + if t.Year() > yearLimit { + return time.Time{} + } + + // Find the first applicable month. + // If it's this month, then do nothing. + for 1< 0 + dowMatch bool = 1< 0 + ) + + if s.Dom&starBit > 0 || s.Dow&starBit > 0 { + return domMatch && dowMatch + } + return domMatch || dowMatch +}