Commit 80e6492f authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #40932 from peay/cronjob-max-finished-jobs

Automatic merge from submit-queue (batch tested with PRs 40932, 41896, 41815, 41309, 41628) Modify CronJob API to add job history limits, cleanup jobs in controller **What this PR does / why we need it**: As discussed in #34710: this adds two limits to `CronJobSpec`, to limit the number of finished jobs created by a CronJob to keep. **Which issue this PR fixes**: fixes #34710 **Special notes for your reviewer**: cc @soltysh, please have a look and let me know what you think -- I'll then add end to end testing and update the doc in a separate commit. What is the timeline to get this into 1.6? The plan: - [x] API changes - [x] Changing versioned APIs - [x] `types.go` - [x] `defaults.go` (nothing to do) - [x] `conversion.go` (nothing to do?) - [x] `conversion_test.go` (nothing to do?) - [x] Changing the internal structure - [x] `types.go` - [x] `validation.go` - [x] `validation_test.go` - [x] Edit version conversions - [x] Edit (nothing to do?) - [x] Run `hack/update-codegen.sh` - [x] Generate protobuf objects - [x] Run `hack/update-generated-protobuf.sh` - [x] Generate json (un)marshaling code - [x] Run `hack/update-codecgen.sh` - [x] Update fuzzer - [x] Actual logic - [x] Unit tests - [x] End to end tests - [x] Documentation changes and API specs update in separate commit **Release note**: ```release-note Add configurable limits to CronJob resource to specify how many successful and failed jobs are preserved. ```
parents 5c3791b9 ca3c4b39
...@@ -40985,6 +40985,11 @@ ...@@ -40985,6 +40985,11 @@
"description": "ConcurrencyPolicy specifies how to treat concurrent executions of a Job.", "description": "ConcurrencyPolicy specifies how to treat concurrent executions of a Job.",
"type": "string" "type": "string"
}, },
"failedJobsHistoryLimit": {
"description": "The number of failed finished jobs to retain. This is a pointer to distinguish between explicit zero and not specified.",
"type": "integer",
"format": "int32"
},
"jobTemplate": { "jobTemplate": {
"description": "JobTemplate is the object that describes the job that will be created when executing a CronJob.", "description": "JobTemplate is the object that describes the job that will be created when executing a CronJob.",
"$ref": "#/definitions/io.k8s.kubernetes.pkg.apis.batch.v2alpha1.JobTemplateSpec" "$ref": "#/definitions/io.k8s.kubernetes.pkg.apis.batch.v2alpha1.JobTemplateSpec"
...@@ -40998,6 +41003,11 @@ ...@@ -40998,6 +41003,11 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"successfulJobsHistoryLimit": {
"description": "The number of successful finished jobs to retain. This is a pointer to distinguish between explicit zero and not specified.",
"type": "integer",
"format": "int32"
},
"suspend": { "suspend": {
"description": "Suspend flag tells the controller to suspend subsequent executions, it does not apply to already started executions. Defaults to false.", "description": "Suspend flag tells the controller to suspend subsequent executions, it does not apply to already started executions. Defaults to false.",
"type": "boolean" "type": "boolean"
...@@ -542,6 +542,14 @@ func batchFuncs(t apitesting.TestingCommon) []interface{} { ...@@ -542,6 +542,14 @@ func batchFuncs(t apitesting.TestingCommon) []interface{} {
sds := int64(c.RandUint64()) sds := int64(c.RandUint64())
sj.StartingDeadlineSeconds = &sds sj.StartingDeadlineSeconds = &sds
sj.Schedule = c.RandString() sj.Schedule = c.RandString()
if hasSuccessLimit := c.RandBool(); hasSuccessLimit {
successfulJobsHistoryLimit := int32(c.Rand.Int31())
sj.SuccessfulJobsHistoryLimit = &successfulJobsHistoryLimit
}
if hasFailedLimit := c.RandBool(); hasFailedLimit {
failedJobsHistoryLimit := int32(c.Rand.Int31())
sj.FailedJobsHistoryLimit = &failedJobsHistoryLimit
}
}, },
func(cp *batch.ConcurrencyPolicy, c fuzz.Continue) { func(cp *batch.ConcurrencyPolicy, c fuzz.Continue) {
policies := []batch.ConcurrencyPolicy{batch.AllowConcurrent, batch.ForbidConcurrent, batch.ReplaceConcurrent} policies := []batch.ConcurrencyPolicy{batch.AllowConcurrent, batch.ForbidConcurrent, batch.ReplaceConcurrent}
......
...@@ -244,6 +244,16 @@ type CronJobSpec struct { ...@@ -244,6 +244,16 @@ type CronJobSpec struct {
// JobTemplate is the object that describes the job that will be created when // JobTemplate is the object that describes the job that will be created when
// executing a CronJob. // executing a CronJob.
JobTemplate JobTemplateSpec JobTemplate JobTemplateSpec
// The number of successful finished jobs to retain.
// This is a pointer to distinguish between explicit zero and not specified.
// +optional
SuccessfulJobsHistoryLimit *int32
// The number of failed finished jobs to retain.
// This is a pointer to distinguish between explicit zero and not specified.
// +optional
FailedJobsHistoryLimit *int32
} }
// ConcurrencyPolicy describes how the job will be handled. // ConcurrencyPolicy describes how the job will be handled.
......
...@@ -82,6 +82,16 @@ message CronJobSpec { ...@@ -82,6 +82,16 @@ message CronJobSpec {
// JobTemplate is the object that describes the job that will be created when // JobTemplate is the object that describes the job that will be created when
// executing a CronJob. // executing a CronJob.
optional JobTemplateSpec jobTemplate = 5; optional JobTemplateSpec jobTemplate = 5;
// The number of successful finished jobs to retain.
// This is a pointer to distinguish between explicit zero and not specified.
// +optional
optional int32 successfulJobsHistoryLimit = 6;
// The number of failed finished jobs to retain.
// This is a pointer to distinguish between explicit zero and not specified.
// +optional
optional int32 failedJobsHistoryLimit = 7;
} }
// CronJobStatus represents the current state of a cron job. // CronJobStatus represents the current state of a cron job.
......
...@@ -3793,15 +3793,17 @@ func (x *CronJobSpec) CodecEncodeSelf(e *codec1978.Encoder) { ...@@ -3793,15 +3793,17 @@ func (x *CronJobSpec) CodecEncodeSelf(e *codec1978.Encoder) {
} else { } else {
yysep2 := !z.EncBinary() yysep2 := !z.EncBinary()
yy2arr2 := z.EncBasicHandle().StructToArray yy2arr2 := z.EncBasicHandle().StructToArray
var yyq2 [5]bool var yyq2 [7]bool
_, _, _ = yysep2, yyq2, yy2arr2 _, _, _ = yysep2, yyq2, yy2arr2
const yyr2 bool = false const yyr2 bool = false
yyq2[1] = x.StartingDeadlineSeconds != nil yyq2[1] = x.StartingDeadlineSeconds != nil
yyq2[2] = x.ConcurrencyPolicy != "" yyq2[2] = x.ConcurrencyPolicy != ""
yyq2[3] = x.Suspend != nil yyq2[3] = x.Suspend != nil
yyq2[5] = x.SuccessfulJobsHistoryLimit != nil
yyq2[6] = x.FailedJobsHistoryLimit != nil
var yynn2 int var yynn2 int
if yyr2 || yy2arr2 { if yyr2 || yy2arr2 {
r.EncodeArrayStart(5) r.EncodeArrayStart(7)
} else { } else {
yynn2 = 2 yynn2 = 2
for _, b := range yyq2 { for _, b := range yyq2 {
...@@ -3928,6 +3930,76 @@ func (x *CronJobSpec) CodecEncodeSelf(e *codec1978.Encoder) { ...@@ -3928,6 +3930,76 @@ func (x *CronJobSpec) CodecEncodeSelf(e *codec1978.Encoder) {
yy22.CodecEncodeSelf(e) yy22.CodecEncodeSelf(e)
} }
if yyr2 || yy2arr2 { if yyr2 || yy2arr2 {
z.EncSendContainerState(codecSelfer_containerArrayElem1234)
if yyq2[5] {
if x.SuccessfulJobsHistoryLimit == nil {
r.EncodeNil()
} else {
yy25 := *x.SuccessfulJobsHistoryLimit
yym26 := z.EncBinary()
_ = yym26
if false {
} else {
r.EncodeInt(int64(yy25))
}
}
} else {
r.EncodeNil()
}
} else {
if yyq2[5] {
z.EncSendContainerState(codecSelfer_containerMapKey1234)
r.EncodeString(codecSelferC_UTF81234, string("successfulJobsHistoryLimit"))
z.EncSendContainerState(codecSelfer_containerMapValue1234)
if x.SuccessfulJobsHistoryLimit == nil {
r.EncodeNil()
} else {
yy27 := *x.SuccessfulJobsHistoryLimit
yym28 := z.EncBinary()
_ = yym28
if false {
} else {
r.EncodeInt(int64(yy27))
}
}
}
}
if yyr2 || yy2arr2 {
z.EncSendContainerState(codecSelfer_containerArrayElem1234)
if yyq2[6] {
if x.FailedJobsHistoryLimit == nil {
r.EncodeNil()
} else {
yy30 := *x.FailedJobsHistoryLimit
yym31 := z.EncBinary()
_ = yym31
if false {
} else {
r.EncodeInt(int64(yy30))
}
}
} else {
r.EncodeNil()
}
} else {
if yyq2[6] {
z.EncSendContainerState(codecSelfer_containerMapKey1234)
r.EncodeString(codecSelferC_UTF81234, string("failedJobsHistoryLimit"))
z.EncSendContainerState(codecSelfer_containerMapValue1234)
if x.FailedJobsHistoryLimit == nil {
r.EncodeNil()
} else {
yy32 := *x.FailedJobsHistoryLimit
yym33 := z.EncBinary()
_ = yym33
if false {
} else {
r.EncodeInt(int64(yy32))
}
}
}
}
if yyr2 || yy2arr2 {
z.EncSendContainerState(codecSelfer_containerArrayEnd1234) z.EncSendContainerState(codecSelfer_containerArrayEnd1234)
} else { } else {
z.EncSendContainerState(codecSelfer_containerMapEnd1234) z.EncSendContainerState(codecSelfer_containerMapEnd1234)
...@@ -4046,6 +4118,38 @@ func (x *CronJobSpec) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) { ...@@ -4046,6 +4118,38 @@ func (x *CronJobSpec) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) {
yyv11 := &x.JobTemplate yyv11 := &x.JobTemplate
yyv11.CodecDecodeSelf(d) yyv11.CodecDecodeSelf(d)
} }
case "successfulJobsHistoryLimit":
if r.TryDecodeAsNil() {
if x.SuccessfulJobsHistoryLimit != nil {
x.SuccessfulJobsHistoryLimit = nil
}
} else {
if x.SuccessfulJobsHistoryLimit == nil {
x.SuccessfulJobsHistoryLimit = new(int32)
}
yym13 := z.DecBinary()
_ = yym13
if false {
} else {
*((*int32)(x.SuccessfulJobsHistoryLimit)) = int32(r.DecodeInt(32))
}
}
case "failedJobsHistoryLimit":
if r.TryDecodeAsNil() {
if x.FailedJobsHistoryLimit != nil {
x.FailedJobsHistoryLimit = nil
}
} else {
if x.FailedJobsHistoryLimit == nil {
x.FailedJobsHistoryLimit = new(int32)
}
yym15 := z.DecBinary()
_ = yym15
if false {
} else {
*((*int32)(x.FailedJobsHistoryLimit)) = int32(r.DecodeInt(32))
}
}
default: default:
z.DecStructFieldNotFound(-1, yys3) z.DecStructFieldNotFound(-1, yys3)
} // end switch yys3 } // end switch yys3
...@@ -4057,16 +4161,16 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { ...@@ -4057,16 +4161,16 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) {
var h codecSelfer1234 var h codecSelfer1234
z, r := codec1978.GenHelperDecoder(d) z, r := codec1978.GenHelperDecoder(d)
_, _, _ = h, z, r _, _, _ = h, z, r
var yyj12 int var yyj16 int
var yyb12 bool var yyb16 bool
var yyhl12 bool = l >= 0 var yyhl16 bool = l >= 0
yyj12++ yyj16++
if yyhl12 { if yyhl16 {
yyb12 = yyj12 > l yyb16 = yyj16 > l
} else { } else {
yyb12 = r.CheckBreak() yyb16 = r.CheckBreak()
} }
if yyb12 { if yyb16 {
z.DecSendContainerState(codecSelfer_containerArrayEnd1234) z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
return return
} }
...@@ -4074,21 +4178,21 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { ...@@ -4074,21 +4178,21 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) {
if r.TryDecodeAsNil() { if r.TryDecodeAsNil() {
x.Schedule = "" x.Schedule = ""
} else { } else {
yyv13 := &x.Schedule yyv17 := &x.Schedule
yym14 := z.DecBinary() yym18 := z.DecBinary()
_ = yym14 _ = yym18
if false { if false {
} else { } else {
*((*string)(yyv13)) = r.DecodeString() *((*string)(yyv17)) = r.DecodeString()
} }
} }
yyj12++ yyj16++
if yyhl12 { if yyhl16 {
yyb12 = yyj12 > l yyb16 = yyj16 > l
} else { } else {
yyb12 = r.CheckBreak() yyb16 = r.CheckBreak()
} }
if yyb12 { if yyb16 {
z.DecSendContainerState(codecSelfer_containerArrayEnd1234) z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
return return
} }
...@@ -4101,20 +4205,20 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { ...@@ -4101,20 +4205,20 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) {
if x.StartingDeadlineSeconds == nil { if x.StartingDeadlineSeconds == nil {
x.StartingDeadlineSeconds = new(int64) x.StartingDeadlineSeconds = new(int64)
} }
yym16 := z.DecBinary() yym20 := z.DecBinary()
_ = yym16 _ = yym20
if false { if false {
} else { } else {
*((*int64)(x.StartingDeadlineSeconds)) = int64(r.DecodeInt(64)) *((*int64)(x.StartingDeadlineSeconds)) = int64(r.DecodeInt(64))
} }
} }
yyj12++ yyj16++
if yyhl12 { if yyhl16 {
yyb12 = yyj12 > l yyb16 = yyj16 > l
} else { } else {
yyb12 = r.CheckBreak() yyb16 = r.CheckBreak()
} }
if yyb12 { if yyb16 {
z.DecSendContainerState(codecSelfer_containerArrayEnd1234) z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
return return
} }
...@@ -4122,16 +4226,16 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { ...@@ -4122,16 +4226,16 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) {
if r.TryDecodeAsNil() { if r.TryDecodeAsNil() {
x.ConcurrencyPolicy = "" x.ConcurrencyPolicy = ""
} else { } else {
yyv17 := &x.ConcurrencyPolicy yyv21 := &x.ConcurrencyPolicy
yyv17.CodecDecodeSelf(d) yyv21.CodecDecodeSelf(d)
} }
yyj12++ yyj16++
if yyhl12 { if yyhl16 {
yyb12 = yyj12 > l yyb16 = yyj16 > l
} else { } else {
yyb12 = r.CheckBreak() yyb16 = r.CheckBreak()
} }
if yyb12 { if yyb16 {
z.DecSendContainerState(codecSelfer_containerArrayEnd1234) z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
return return
} }
...@@ -4144,20 +4248,20 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { ...@@ -4144,20 +4248,20 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) {
if x.Suspend == nil { if x.Suspend == nil {
x.Suspend = new(bool) x.Suspend = new(bool)
} }
yym19 := z.DecBinary() yym23 := z.DecBinary()
_ = yym19 _ = yym23
if false { if false {
} else { } else {
*((*bool)(x.Suspend)) = r.DecodeBool() *((*bool)(x.Suspend)) = r.DecodeBool()
} }
} }
yyj12++ yyj16++
if yyhl12 { if yyhl16 {
yyb12 = yyj12 > l yyb16 = yyj16 > l
} else { } else {
yyb12 = r.CheckBreak() yyb16 = r.CheckBreak()
} }
if yyb12 { if yyb16 {
z.DecSendContainerState(codecSelfer_containerArrayEnd1234) z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
return return
} }
...@@ -4165,21 +4269,73 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { ...@@ -4165,21 +4269,73 @@ func (x *CronJobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) {
if r.TryDecodeAsNil() { if r.TryDecodeAsNil() {
x.JobTemplate = JobTemplateSpec{} x.JobTemplate = JobTemplateSpec{}
} else { } else {
yyv20 := &x.JobTemplate yyv24 := &x.JobTemplate
yyv20.CodecDecodeSelf(d) yyv24.CodecDecodeSelf(d)
}
yyj16++
if yyhl16 {
yyb16 = yyj16 > l
} else {
yyb16 = r.CheckBreak()
}
if yyb16 {
z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
return
}
z.DecSendContainerState(codecSelfer_containerArrayElem1234)
if r.TryDecodeAsNil() {
if x.SuccessfulJobsHistoryLimit != nil {
x.SuccessfulJobsHistoryLimit = nil
}
} else {
if x.SuccessfulJobsHistoryLimit == nil {
x.SuccessfulJobsHistoryLimit = new(int32)
}
yym26 := z.DecBinary()
_ = yym26
if false {
} else {
*((*int32)(x.SuccessfulJobsHistoryLimit)) = int32(r.DecodeInt(32))
}
}
yyj16++
if yyhl16 {
yyb16 = yyj16 > l
} else {
yyb16 = r.CheckBreak()
}
if yyb16 {
z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
return
}
z.DecSendContainerState(codecSelfer_containerArrayElem1234)
if r.TryDecodeAsNil() {
if x.FailedJobsHistoryLimit != nil {
x.FailedJobsHistoryLimit = nil
}
} else {
if x.FailedJobsHistoryLimit == nil {
x.FailedJobsHistoryLimit = new(int32)
}
yym28 := z.DecBinary()
_ = yym28
if false {
} else {
*((*int32)(x.FailedJobsHistoryLimit)) = int32(r.DecodeInt(32))
}
} }
for { for {
yyj12++ yyj16++
if yyhl12 { if yyhl16 {
yyb12 = yyj12 > l yyb16 = yyj16 > l
} else { } else {
yyb12 = r.CheckBreak() yyb16 = r.CheckBreak()
} }
if yyb12 { if yyb16 {
break break
} }
z.DecSendContainerState(codecSelfer_containerArrayElem1234) z.DecSendContainerState(codecSelfer_containerArrayElem1234)
z.DecStructFieldNotFound(yyj12-1, "") z.DecStructFieldNotFound(yyj16-1, "")
} }
z.DecSendContainerState(codecSelfer_containerArrayEnd1234) z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
} }
...@@ -4772,7 +4928,7 @@ func (x codecSelfer1234) decSliceCronJob(v *[]CronJob, d *codec1978.Decoder) { ...@@ -4772,7 +4928,7 @@ func (x codecSelfer1234) decSliceCronJob(v *[]CronJob, d *codec1978.Decoder) {
yyrg1 := len(yyv1) > 0 yyrg1 := len(yyv1) > 0
yyv21 := yyv1 yyv21 := yyv1
yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 1128) yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 1144)
if yyrt1 { if yyrt1 {
if yyrl1 <= cap(yyv1) { if yyrl1 <= cap(yyv1) {
yyv1 = yyv1[:yyrl1] yyv1 = yyv1[:yyrl1]
......
...@@ -250,6 +250,16 @@ type CronJobSpec struct { ...@@ -250,6 +250,16 @@ type CronJobSpec struct {
// JobTemplate is the object that describes the job that will be created when // JobTemplate is the object that describes the job that will be created when
// executing a CronJob. // executing a CronJob.
JobTemplate JobTemplateSpec `json:"jobTemplate" protobuf:"bytes,5,opt,name=jobTemplate"` JobTemplate JobTemplateSpec `json:"jobTemplate" protobuf:"bytes,5,opt,name=jobTemplate"`
// The number of successful finished jobs to retain.
// This is a pointer to distinguish between explicit zero and not specified.
// +optional
SuccessfulJobsHistoryLimit *int32 `json:"successfulJobsHistoryLimit,omitempty" protobuf:"varint,6,opt,name=successfulJobsHistoryLimit"`
// The number of failed finished jobs to retain.
// This is a pointer to distinguish between explicit zero and not specified.
// +optional
FailedJobsHistoryLimit *int32 `json:"failedJobsHistoryLimit,omitempty" protobuf:"varint,7,opt,name=failedJobsHistoryLimit"`
} }
// ConcurrencyPolicy describes how the job will be handled. // ConcurrencyPolicy describes how the job will be handled.
......
...@@ -49,12 +49,14 @@ func (CronJobList) SwaggerDoc() map[string]string { ...@@ -49,12 +49,14 @@ func (CronJobList) SwaggerDoc() map[string]string {
} }
var map_CronJobSpec = map[string]string{ var map_CronJobSpec = map[string]string{
"": "CronJobSpec describes how the job execution will look like and when it will actually run.", "": "CronJobSpec describes how the job execution will look like and when it will actually run.",
"schedule": "Schedule contains the schedule in Cron format, see https://en.wikipedia.org/wiki/Cron.", "schedule": "Schedule contains the schedule in Cron format, see https://en.wikipedia.org/wiki/Cron.",
"startingDeadlineSeconds": "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.", "startingDeadlineSeconds": "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.",
"concurrencyPolicy": "ConcurrencyPolicy specifies how to treat concurrent executions of a Job.", "concurrencyPolicy": "ConcurrencyPolicy specifies how to treat concurrent executions of a Job.",
"suspend": "Suspend flag tells the controller to suspend subsequent executions, it does not apply to already started executions. Defaults to false.", "suspend": "Suspend flag tells the controller to suspend subsequent executions, it does not apply to already started executions. Defaults to false.",
"jobTemplate": "JobTemplate is the object that describes the job that will be created when executing a CronJob.", "jobTemplate": "JobTemplate is the object that describes the job that will be created when executing a CronJob.",
"successfulJobsHistoryLimit": "The number of successful finished jobs to retain. This is a pointer to distinguish between explicit zero and not specified.",
"failedJobsHistoryLimit": "The number of failed finished jobs to retain. This is a pointer to distinguish between explicit zero and not specified.",
} }
func (CronJobSpec) SwaggerDoc() map[string]string { func (CronJobSpec) SwaggerDoc() map[string]string {
......
...@@ -141,6 +141,8 @@ func autoConvert_v2alpha1_CronJobSpec_To_batch_CronJobSpec(in *CronJobSpec, out ...@@ -141,6 +141,8 @@ func autoConvert_v2alpha1_CronJobSpec_To_batch_CronJobSpec(in *CronJobSpec, out
if err := Convert_v2alpha1_JobTemplateSpec_To_batch_JobTemplateSpec(&in.JobTemplate, &out.JobTemplate, s); err != nil { if err := Convert_v2alpha1_JobTemplateSpec_To_batch_JobTemplateSpec(&in.JobTemplate, &out.JobTemplate, s); err != nil {
return err return err
} }
out.SuccessfulJobsHistoryLimit = (*int32)(unsafe.Pointer(in.SuccessfulJobsHistoryLimit))
out.FailedJobsHistoryLimit = (*int32)(unsafe.Pointer(in.FailedJobsHistoryLimit))
return nil return nil
} }
...@@ -156,6 +158,8 @@ func autoConvert_batch_CronJobSpec_To_v2alpha1_CronJobSpec(in *batch.CronJobSpec ...@@ -156,6 +158,8 @@ func autoConvert_batch_CronJobSpec_To_v2alpha1_CronJobSpec(in *batch.CronJobSpec
if err := Convert_batch_JobTemplateSpec_To_v2alpha1_JobTemplateSpec(&in.JobTemplate, &out.JobTemplate, s); err != nil { if err := Convert_batch_JobTemplateSpec_To_v2alpha1_JobTemplateSpec(&in.JobTemplate, &out.JobTemplate, s); err != nil {
return err return err
} }
out.SuccessfulJobsHistoryLimit = (*int32)(unsafe.Pointer(in.SuccessfulJobsHistoryLimit))
out.FailedJobsHistoryLimit = (*int32)(unsafe.Pointer(in.FailedJobsHistoryLimit))
return nil return nil
} }
......
...@@ -106,6 +106,16 @@ func DeepCopy_v2alpha1_CronJobSpec(in interface{}, out interface{}, c *conversio ...@@ -106,6 +106,16 @@ func DeepCopy_v2alpha1_CronJobSpec(in interface{}, out interface{}, c *conversio
if err := DeepCopy_v2alpha1_JobTemplateSpec(&in.JobTemplate, &out.JobTemplate, c); err != nil { if err := DeepCopy_v2alpha1_JobTemplateSpec(&in.JobTemplate, &out.JobTemplate, c); err != nil {
return err return err
} }
if in.SuccessfulJobsHistoryLimit != nil {
in, out := &in.SuccessfulJobsHistoryLimit, &out.SuccessfulJobsHistoryLimit
*out = new(int32)
**out = **in
}
if in.FailedJobsHistoryLimit != nil {
in, out := &in.FailedJobsHistoryLimit, &out.FailedJobsHistoryLimit
*out = new(int32)
**out = **in
}
return nil return nil
} }
} }
......
...@@ -179,6 +179,15 @@ func ValidateCronJobSpec(spec *batch.CronJobSpec, fldPath *field.Path) field.Err ...@@ -179,6 +179,15 @@ func ValidateCronJobSpec(spec *batch.CronJobSpec, fldPath *field.Path) field.Err
allErrs = append(allErrs, validateConcurrencyPolicy(&spec.ConcurrencyPolicy, fldPath.Child("concurrencyPolicy"))...) allErrs = append(allErrs, validateConcurrencyPolicy(&spec.ConcurrencyPolicy, fldPath.Child("concurrencyPolicy"))...)
allErrs = append(allErrs, ValidateJobTemplateSpec(&spec.JobTemplate, fldPath.Child("jobTemplate"))...) allErrs = append(allErrs, ValidateJobTemplateSpec(&spec.JobTemplate, fldPath.Child("jobTemplate"))...)
if spec.SuccessfulJobsHistoryLimit != nil {
// zero is a valid SuccessfulJobsHistoryLimit
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.SuccessfulJobsHistoryLimit), fldPath.Child("successfulJobsHistoryLimit"))...)
}
if spec.FailedJobsHistoryLimit != nil {
// zero is a valid SuccessfulJobsHistoryLimit
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.FailedJobsHistoryLimit), fldPath.Child("failedJobsHistoryLimit"))...)
}
return allErrs return allErrs
} }
......
...@@ -402,6 +402,40 @@ func TestValidateCronJob(t *testing.T) { ...@@ -402,6 +402,40 @@ func TestValidateCronJob(t *testing.T) {
}, },
}, },
}, },
"spec.successfulJobsHistoryLimit: must be greater than or equal to 0": {
ObjectMeta: metav1.ObjectMeta{
Name: "mycronjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.CronJobSpec{
Schedule: "* * * * ?",
ConcurrencyPolicy: batch.AllowConcurrent,
SuccessfulJobsHistoryLimit: &negative,
JobTemplate: batch.JobTemplateSpec{
Spec: batch.JobSpec{
Template: validPodTemplateSpec,
},
},
},
},
"spec.failedJobsHistoryLimit: must be greater than or equal to 0": {
ObjectMeta: metav1.ObjectMeta{
Name: "mycronjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.CronJobSpec{
Schedule: "* * * * ?",
ConcurrencyPolicy: batch.AllowConcurrent,
FailedJobsHistoryLimit: &negative,
JobTemplate: batch.JobTemplateSpec{
Spec: batch.JobSpec{
Template: validPodTemplateSpec,
},
},
},
},
"spec.concurrencyPolicy: Required value": { "spec.concurrencyPolicy: Required value": {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "mycronjob", Name: "mycronjob",
......
...@@ -106,6 +106,16 @@ func DeepCopy_batch_CronJobSpec(in interface{}, out interface{}, c *conversion.C ...@@ -106,6 +106,16 @@ func DeepCopy_batch_CronJobSpec(in interface{}, out interface{}, c *conversion.C
if err := DeepCopy_batch_JobTemplateSpec(&in.JobTemplate, &out.JobTemplate, c); err != nil { if err := DeepCopy_batch_JobTemplateSpec(&in.JobTemplate, &out.JobTemplate, c); err != nil {
return err return err
} }
if in.SuccessfulJobsHistoryLimit != nil {
in, out := &in.SuccessfulJobsHistoryLimit, &out.SuccessfulJobsHistoryLimit
*out = new(int32)
**out = **in
}
if in.FailedJobsHistoryLimit != nil {
in, out := &in.FailedJobsHistoryLimit, &out.FailedJobsHistoryLimit
*out = new(int32)
**out = **in
}
return nil return nil
} }
} }
......
...@@ -17,6 +17,8 @@ limitations under the License. ...@@ -17,6 +17,8 @@ limitations under the License.
package cronjob package cronjob
import ( import (
"sort"
"strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
...@@ -81,6 +83,14 @@ func justAfterThePriorHour() time.Time { ...@@ -81,6 +83,14 @@ func justAfterThePriorHour() time.Time {
return T1 return T1
} }
func startTimeStringToTime(startTime string) time.Time {
T1, err := time.Parse(time.RFC3339, startTime)
if err != nil {
panic("test setup error")
}
return T1
}
// returns a cronJob with some fields filled in. // returns a cronJob with some fields filled in.
func cronJob() batch.CronJob { func cronJob() batch.CronJob {
return batch.CronJob{ return batch.CronJob{
...@@ -270,7 +280,7 @@ func TestSyncOne_RunOrNot(t *testing.T) { ...@@ -270,7 +280,7 @@ func TestSyncOne_RunOrNot(t *testing.T) {
pc := &fakePodControl{} pc := &fakePodControl{}
recorder := record.NewFakeRecorder(10) recorder := record.NewFakeRecorder(10)
SyncOne(sj, js, tc.now, jc, sjc, pc, recorder) syncOne(&sj, js, tc.now, jc, sjc, pc, recorder)
expectedCreates := 0 expectedCreates := 0
if tc.expectCreate { if tc.expectCreate {
expectedCreates = 1 expectedCreates = 1
...@@ -320,10 +330,237 @@ func TestSyncOne_RunOrNot(t *testing.T) { ...@@ -320,10 +330,237 @@ func TestSyncOne_RunOrNot(t *testing.T) {
} }
} }
type CleanupJobSpec struct {
StartTime string
IsFinished bool
IsSuccessful bool
ExpectDelete bool
IsStillInActiveList bool // only when IsFinished is set
}
func TestCleanupFinishedJobs_DeleteOrNot(t *testing.T) {
limitThree := int32(3)
limitTwo := int32(2)
limitOne := int32(1)
limitZero := int32(0)
// Starting times are assumed to be sorted by increasing start time
// in all the test cases
testCases := map[string]struct {
jobSpecs []CleanupJobSpec
now time.Time
successfulJobsHistoryLimit *int32
failedJobsHistoryLimit *int32
expectActive int
}{
"success. job limit reached": {
[]CleanupJobSpec{
{"2016-05-19T04:00:00Z", T, T, T, F},
{"2016-05-19T05:00:00Z", T, T, T, F},
{"2016-05-19T06:00:00Z", T, T, F, F},
{"2016-05-19T07:00:00Z", T, T, F, F},
{"2016-05-19T08:00:00Z", F, F, F, F},
{"2016-05-19T09:00:00Z", T, F, F, F},
}, justBeforeTheHour(), &limitTwo, &limitOne, 1},
"success. jobs not processed by Sync yet": {
[]CleanupJobSpec{
{"2016-05-19T04:00:00Z", T, T, T, F},
{"2016-05-19T05:00:00Z", T, T, T, T},
{"2016-05-19T06:00:00Z", T, T, F, T},
{"2016-05-19T07:00:00Z", T, T, F, T},
{"2016-05-19T08:00:00Z", F, F, F, F},
{"2016-05-19T09:00:00Z", T, F, F, T},
}, justBeforeTheHour(), &limitTwo, &limitOne, 4},
"failed job limit reached": {
[]CleanupJobSpec{
{"2016-05-19T04:00:00Z", T, F, T, F},
{"2016-05-19T05:00:00Z", T, F, T, F},
{"2016-05-19T06:00:00Z", T, T, F, F},
{"2016-05-19T07:00:00Z", T, T, F, F},
{"2016-05-19T08:00:00Z", T, F, F, F},
{"2016-05-19T09:00:00Z", T, F, F, F},
}, justBeforeTheHour(), &limitTwo, &limitTwo, 0},
"success. job limit set to zero": {
[]CleanupJobSpec{
{"2016-05-19T04:00:00Z", T, T, T, F},
{"2016-05-19T05:00:00Z", T, F, T, F},
{"2016-05-19T06:00:00Z", T, T, T, F},
{"2016-05-19T07:00:00Z", T, T, T, F},
{"2016-05-19T08:00:00Z", F, F, F, F},
{"2016-05-19T09:00:00Z", T, F, F, F},
}, justBeforeTheHour(), &limitZero, &limitOne, 1},
"failed job limit set to zero": {
[]CleanupJobSpec{
{"2016-05-19T04:00:00Z", T, T, F, F},
{"2016-05-19T05:00:00Z", T, F, T, F},
{"2016-05-19T06:00:00Z", T, T, F, F},
{"2016-05-19T07:00:00Z", T, T, F, F},
{"2016-05-19T08:00:00Z", F, F, F, F},
{"2016-05-19T09:00:00Z", T, F, T, F},
}, justBeforeTheHour(), &limitThree, &limitZero, 1},
"no limits reached": {
[]CleanupJobSpec{
{"2016-05-19T04:00:00Z", T, T, F, F},
{"2016-05-19T05:00:00Z", T, F, F, F},
{"2016-05-19T06:00:00Z", T, T, F, F},
{"2016-05-19T07:00:00Z", T, T, F, F},
{"2016-05-19T08:00:00Z", T, F, F, F},
{"2016-05-19T09:00:00Z", T, F, F, F},
}, justBeforeTheHour(), &limitThree, &limitThree, 0},
// This test case should trigger the short-circuit
"limits disabled": {
[]CleanupJobSpec{
{"2016-05-19T04:00:00Z", T, T, F, F},
{"2016-05-19T05:00:00Z", T, F, F, F},
{"2016-05-19T06:00:00Z", T, T, F, F},
{"2016-05-19T07:00:00Z", T, T, F, F},
{"2016-05-19T08:00:00Z", T, F, F, F},
{"2016-05-19T09:00:00Z", T, F, F, F},
}, justBeforeTheHour(), nil, nil, 0},
"success limit disabled": {
[]CleanupJobSpec{
{"2016-05-19T04:00:00Z", T, T, F, F},
{"2016-05-19T05:00:00Z", T, F, F, F},
{"2016-05-19T06:00:00Z", T, T, F, F},
{"2016-05-19T07:00:00Z", T, T, F, F},
{"2016-05-19T08:00:00Z", T, F, F, F},
{"2016-05-19T09:00:00Z", T, F, F, F},
}, justBeforeTheHour(), nil, &limitThree, 0},
"failure limit disabled": {
[]CleanupJobSpec{
{"2016-05-19T04:00:00Z", T, T, F, F},
{"2016-05-19T05:00:00Z", T, F, F, F},
{"2016-05-19T06:00:00Z", T, T, F, F},
{"2016-05-19T07:00:00Z", T, T, F, F},
{"2016-05-19T08:00:00Z", T, F, F, F},
{"2016-05-19T09:00:00Z", T, F, F, F},
}, justBeforeTheHour(), &limitThree, nil, 0},
"no limits reached because still active": {
[]CleanupJobSpec{
{"2016-05-19T04:00:00Z", F, F, F, F},
{"2016-05-19T05:00:00Z", F, F, F, F},
{"2016-05-19T06:00:00Z", F, F, F, F},
{"2016-05-19T07:00:00Z", F, F, F, F},
{"2016-05-19T08:00:00Z", F, F, F, F},
{"2016-05-19T09:00:00Z", F, F, F, F},
}, justBeforeTheHour(), &limitZero, &limitZero, 6},
}
for name, tc := range testCases {
sj := cronJob()
suspend := false
sj.Spec.ConcurrencyPolicy = f
sj.Spec.Suspend = &suspend
sj.Spec.Schedule = onTheHour
sj.Spec.SuccessfulJobsHistoryLimit = tc.successfulJobsHistoryLimit
sj.Spec.FailedJobsHistoryLimit = tc.failedJobsHistoryLimit
var (
job *batch.Job
err error
)
// Set consistent timestamps for the CronJob
if len(tc.jobSpecs) != 0 {
firstTime := startTimeStringToTime(tc.jobSpecs[0].StartTime)
lastTime := startTimeStringToTime(tc.jobSpecs[len(tc.jobSpecs)-1].StartTime)
sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: firstTime}
sj.Status.LastScheduleTime = &metav1.Time{Time: lastTime}
} else {
sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: justBeforeTheHour()}
}
// Create jobs
js := []batch.Job{}
jobsToDelete := []string{}
sj.Status.Active = []v1.ObjectReference{}
for i, spec := range tc.jobSpecs {
job, err = getJobFromTemplate(&sj, startTimeStringToTime(spec.StartTime))
if err != nil {
t.Fatalf("%s: unexpected error creating a job from template: %v", name, err)
}
job.UID = types.UID(strconv.Itoa(i))
job.Namespace = ""
if spec.IsFinished {
var conditionType batch.JobConditionType
if spec.IsSuccessful {
conditionType = batch.JobComplete
} else {
conditionType = batch.JobFailed
}
condition := batch.JobCondition{Type: conditionType, Status: v1.ConditionTrue}
job.Status.Conditions = append(job.Status.Conditions, condition)
if spec.IsStillInActiveList {
sj.Status.Active = append(sj.Status.Active, v1.ObjectReference{UID: job.UID})
}
} else {
if spec.IsSuccessful || spec.IsStillInActiveList {
t.Errorf("%s: test setup error: this case makes no sense", name)
}
sj.Status.Active = append(sj.Status.Active, v1.ObjectReference{UID: job.UID})
}
js = append(js, *job)
if spec.ExpectDelete {
jobsToDelete = append(jobsToDelete, job.Name)
}
}
jc := &fakeJobControl{Job: job}
pc := &fakePodControl{}
sjc := &fakeSJControl{}
recorder := record.NewFakeRecorder(10)
cleanupFinishedJobs(&sj, js, jc, sjc, pc, recorder)
// Check we have actually deleted the correct jobs
if len(jc.DeleteJobName) != len(jobsToDelete) {
t.Errorf("%s: expected %d job deleted, actually %d", name, len(jobsToDelete), len(jc.DeleteJobName))
} else {
sort.Strings(jobsToDelete)
sort.Strings(jc.DeleteJobName)
for i, expectedJobName := range jobsToDelete {
if expectedJobName != jc.DeleteJobName[i] {
t.Errorf("%s: expected job %s deleted, actually %v -- %v vs %v", name, expectedJobName, jc.DeleteJobName[i], jc.DeleteJobName, jobsToDelete)
}
}
}
// Check for events
expectedEvents := len(jobsToDelete)
if len(recorder.Events) != expectedEvents {
t.Errorf("%s: expected %d event, actually %v", name, expectedEvents, len(recorder.Events))
}
// Check for jobs still in active list
numActive := 0
if len(sjc.Updates) != 0 {
numActive = len(sjc.Updates[len(sjc.Updates)-1].Status.Active)
}
if tc.expectActive != numActive {
t.Errorf("%s: expected Active size %d, got %d", name, tc.expectActive, numActive)
}
}
}
// TODO: simulation where the controller randomly doesn't run, and randomly has errors starting jobs or deleting jobs, // TODO: simulation where the controller randomly doesn't run, and randomly has errors starting jobs or deleting jobs,
// but over time, all jobs run as expected (assuming Allow and no deadline). // but over time, all jobs run as expected (assuming Allow and no deadline).
// TestSyncOne_Status tests sj.UpdateStatus in SyncOne // TestSyncOne_Status tests sj.UpdateStatus in syncOne
func TestSyncOne_Status(t *testing.T) { func TestSyncOne_Status(t *testing.T) {
finishedJob := newJob("1") finishedJob := newJob("1")
finishedJob.Status.Conditions = append(finishedJob.Status.Conditions, batch.JobCondition{Type: batch.JobComplete, Status: v1.ConditionTrue}) finishedJob.Status.Conditions = append(finishedJob.Status.Conditions, batch.JobCondition{Type: batch.JobComplete, Status: v1.ConditionTrue})
...@@ -443,7 +680,7 @@ func TestSyncOne_Status(t *testing.T) { ...@@ -443,7 +680,7 @@ func TestSyncOne_Status(t *testing.T) {
recorder := record.NewFakeRecorder(10) recorder := record.NewFakeRecorder(10)
// Run the code // Run the code
SyncOne(sj, jobs, tc.now, jc, sjc, pc, recorder) syncOne(&sj, jobs, tc.now, jc, sjc, pc, recorder)
// Status update happens once when ranging through job list, and another one if create jobs. // Status update happens once when ranging through job list, and another one if create jobs.
expectUpdates := 1 expectUpdates := 1
......
...@@ -234,11 +234,34 @@ func makeCreatedByRefJson(object runtime.Object) (string, error) { ...@@ -234,11 +234,34 @@ func makeCreatedByRefJson(object runtime.Object) (string, error) {
return string(createdByRefJson), nil return string(createdByRefJson), nil
} }
func IsJobFinished(j *batch.Job) bool { func getFinishedStatus(j *batch.Job) (bool, batch.JobConditionType) {
for _, c := range j.Status.Conditions { for _, c := range j.Status.Conditions {
if (c.Type == batch.JobComplete || c.Type == batch.JobFailed) && c.Status == v1.ConditionTrue { if (c.Type == batch.JobComplete || c.Type == batch.JobFailed) && c.Status == v1.ConditionTrue {
return true return true, c.Type
} }
} }
return false return false, ""
}
func IsJobFinished(j *batch.Job) bool {
isFinished, _ := getFinishedStatus(j)
return isFinished
}
// byJobStartTime sorts a list of jobs by start timestamp, using their names as a tie breaker.
type byJobStartTime []batch.Job
func (o byJobStartTime) Len() int { return len(o) }
func (o byJobStartTime) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
func (o byJobStartTime) Less(i, j int) bool {
if o[j].Status.StartTime == nil {
return o[i].Status.StartTime != nil
}
if (*o[i].Status.StartTime).Equal(*o[j].Status.StartTime) {
return o[i].Name < o[j].Name
}
return (*o[i].Status.StartTime).Before(*o[j].Status.StartTime)
} }
...@@ -16226,6 +16226,20 @@ func GetOpenAPIDefinitions(ref openapi.ReferenceCallback) map[string]openapi.Ope ...@@ -16226,6 +16226,20 @@ func GetOpenAPIDefinitions(ref openapi.ReferenceCallback) map[string]openapi.Ope
Ref: ref("k8s.io/kubernetes/pkg/apis/batch/v2alpha1.JobTemplateSpec"), Ref: ref("k8s.io/kubernetes/pkg/apis/batch/v2alpha1.JobTemplateSpec"),
}, },
}, },
"successfulJobsHistoryLimit": {
SchemaProps: spec.SchemaProps{
Description: "The number of successful finished jobs to retain. This is a pointer to distinguish between explicit zero and not specified.",
Type: []string{"integer"},
Format: "int32",
},
},
"failedJobsHistoryLimit": {
SchemaProps: spec.SchemaProps{
Description: "The number of failed finished jobs to retain. This is a pointer to distinguish between explicit zero and not specified.",
Type: []string{"integer"},
Format: "int32",
},
},
}, },
Required: []string{"schedule", "jobTemplate"}, Required: []string{"schedule", "jobTemplate"},
}, },
......
...@@ -52,6 +52,11 @@ var ( ...@@ -52,6 +52,11 @@ var (
var _ = framework.KubeDescribe("CronJob", func() { var _ = framework.KubeDescribe("CronJob", func() {
f := framework.NewDefaultGroupVersionFramework("cronjob", BatchV2Alpha1GroupVersion) f := framework.NewDefaultGroupVersionFramework("cronjob", BatchV2Alpha1GroupVersion)
sleepCommand := []string{"sleep", "300"}
// Pod will complete instantly
successCommand := []string{"/bin/true"}
BeforeEach(func() { BeforeEach(func() {
framework.SkipIfMissingResource(f.ClientPool, CronJobGroupVersionResource, f.Namespace.Name) framework.SkipIfMissingResource(f.ClientPool, CronJobGroupVersionResource, f.Namespace.Name)
}) })
...@@ -59,7 +64,8 @@ var _ = framework.KubeDescribe("CronJob", func() { ...@@ -59,7 +64,8 @@ var _ = framework.KubeDescribe("CronJob", func() {
// multiple jobs running at once // multiple jobs running at once
It("should schedule multiple jobs concurrently", func() { It("should schedule multiple jobs concurrently", func() {
By("Creating a cronjob") By("Creating a cronjob")
cronJob := newTestCronJob("concurrent", "*/1 * * * ?", batch.AllowConcurrent, true) cronJob := newTestCronJob("concurrent", "*/1 * * * ?", batch.AllowConcurrent,
sleepCommand, nil)
cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob) cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
...@@ -70,7 +76,7 @@ var _ = framework.KubeDescribe("CronJob", func() { ...@@ -70,7 +76,7 @@ var _ = framework.KubeDescribe("CronJob", func() {
By("Ensuring at least two running jobs exists by listing jobs explicitly") By("Ensuring at least two running jobs exists by listing jobs explicitly")
jobs, err := f.ClientSet.Batch().Jobs(f.Namespace.Name).List(metav1.ListOptions{}) jobs, err := f.ClientSet.Batch().Jobs(f.Namespace.Name).List(metav1.ListOptions{})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
activeJobs := filterActiveJobs(jobs) activeJobs, _ := filterActiveJobs(jobs)
Expect(len(activeJobs) >= 2).To(BeTrue()) Expect(len(activeJobs) >= 2).To(BeTrue())
By("Removing cronjob") By("Removing cronjob")
...@@ -81,7 +87,8 @@ var _ = framework.KubeDescribe("CronJob", func() { ...@@ -81,7 +87,8 @@ var _ = framework.KubeDescribe("CronJob", func() {
// suspended should not schedule jobs // suspended should not schedule jobs
It("should not schedule jobs when suspended [Slow]", func() { It("should not schedule jobs when suspended [Slow]", func() {
By("Creating a suspended cronjob") By("Creating a suspended cronjob")
cronJob := newTestCronJob("suspended", "*/1 * * * ?", batch.AllowConcurrent, true) cronJob := newTestCronJob("suspended", "*/1 * * * ?", batch.AllowConcurrent,
sleepCommand, nil)
cronJob.Spec.Suspend = newBool(true) cronJob.Spec.Suspend = newBool(true)
cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob) cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
...@@ -103,7 +110,8 @@ var _ = framework.KubeDescribe("CronJob", func() { ...@@ -103,7 +110,8 @@ var _ = framework.KubeDescribe("CronJob", func() {
// only single active job is allowed for ForbidConcurrent // only single active job is allowed for ForbidConcurrent
It("should not schedule new jobs when ForbidConcurrent [Slow]", func() { It("should not schedule new jobs when ForbidConcurrent [Slow]", func() {
By("Creating a ForbidConcurrent cronjob") By("Creating a ForbidConcurrent cronjob")
cronJob := newTestCronJob("forbid", "*/1 * * * ?", batch.ForbidConcurrent, true) cronJob := newTestCronJob("forbid", "*/1 * * * ?", batch.ForbidConcurrent,
sleepCommand, nil)
cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob) cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
...@@ -119,7 +127,7 @@ var _ = framework.KubeDescribe("CronJob", func() { ...@@ -119,7 +127,7 @@ var _ = framework.KubeDescribe("CronJob", func() {
By("Ensuring exaclty one running job exists by listing jobs explicitly") By("Ensuring exaclty one running job exists by listing jobs explicitly")
jobs, err := f.ClientSet.Batch().Jobs(f.Namespace.Name).List(metav1.ListOptions{}) jobs, err := f.ClientSet.Batch().Jobs(f.Namespace.Name).List(metav1.ListOptions{})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
activeJobs := filterActiveJobs(jobs) activeJobs, _ := filterActiveJobs(jobs)
Expect(activeJobs).To(HaveLen(1)) Expect(activeJobs).To(HaveLen(1))
By("Ensuring no more jobs are scheduled") By("Ensuring no more jobs are scheduled")
...@@ -134,7 +142,8 @@ var _ = framework.KubeDescribe("CronJob", func() { ...@@ -134,7 +142,8 @@ var _ = framework.KubeDescribe("CronJob", func() {
// only single active job is allowed for ReplaceConcurrent // only single active job is allowed for ReplaceConcurrent
It("should replace jobs when ReplaceConcurrent", func() { It("should replace jobs when ReplaceConcurrent", func() {
By("Creating a ReplaceConcurrent cronjob") By("Creating a ReplaceConcurrent cronjob")
cronJob := newTestCronJob("replace", "*/1 * * * ?", batch.ReplaceConcurrent, true) cronJob := newTestCronJob("replace", "*/1 * * * ?", batch.ReplaceConcurrent,
sleepCommand, nil)
cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob) cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
...@@ -150,7 +159,7 @@ var _ = framework.KubeDescribe("CronJob", func() { ...@@ -150,7 +159,7 @@ var _ = framework.KubeDescribe("CronJob", func() {
By("Ensuring exaclty one running job exists by listing jobs explicitly") By("Ensuring exaclty one running job exists by listing jobs explicitly")
jobs, err := f.ClientSet.Batch().Jobs(f.Namespace.Name).List(metav1.ListOptions{}) jobs, err := f.ClientSet.Batch().Jobs(f.Namespace.Name).List(metav1.ListOptions{})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
activeJobs := filterActiveJobs(jobs) activeJobs, _ := filterActiveJobs(jobs)
Expect(activeJobs).To(HaveLen(1)) Expect(activeJobs).To(HaveLen(1))
By("Ensuring the job is replaced with a new one") By("Ensuring the job is replaced with a new one")
...@@ -165,7 +174,8 @@ var _ = framework.KubeDescribe("CronJob", func() { ...@@ -165,7 +174,8 @@ var _ = framework.KubeDescribe("CronJob", func() {
// shouldn't give us unexpected warnings // shouldn't give us unexpected warnings
It("should not emit unexpected warnings", func() { It("should not emit unexpected warnings", func() {
By("Creating a cronjob") By("Creating a cronjob")
cronJob := newTestCronJob("concurrent", "*/1 * * * ?", batch.AllowConcurrent, false) cronJob := newTestCronJob("concurrent", "*/1 * * * ?", batch.AllowConcurrent,
nil, nil)
cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob) cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
...@@ -187,7 +197,8 @@ var _ = framework.KubeDescribe("CronJob", func() { ...@@ -187,7 +197,8 @@ var _ = framework.KubeDescribe("CronJob", func() {
// deleted jobs should be removed from the active list // deleted jobs should be removed from the active list
It("should remove from active list jobs that have been deleted", func() { It("should remove from active list jobs that have been deleted", func() {
By("Creating a ForbidConcurrent cronjob") By("Creating a ForbidConcurrent cronjob")
cronJob := newTestCronJob("forbid", "*/1 * * * ?", batch.ForbidConcurrent, true) cronJob := newTestCronJob("forbid", "*/1 * * * ?", batch.ForbidConcurrent,
sleepCommand, nil)
cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob) cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
...@@ -225,10 +236,49 @@ var _ = framework.KubeDescribe("CronJob", func() { ...@@ -225,10 +236,49 @@ var _ = framework.KubeDescribe("CronJob", func() {
err = deleteCronJob(f.ClientSet, f.Namespace.Name, cronJob.Name) err = deleteCronJob(f.ClientSet, f.Namespace.Name, cronJob.Name)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
}) })
// cleanup of successful finished jobs, with limit of one successful job
It("should delete successful finished jobs with limit of one successful job", func() {
By("Creating a AllowConcurrent cronjob with custom history limits")
successLimit := int32(1)
cronJob := newTestCronJob("concurrent-limit", "*/1 * * * ?", batch.AllowConcurrent,
successCommand, &successLimit)
cronJob, err := createCronJob(f.ClientSet, f.Namespace.Name, cronJob)
Expect(err).NotTo(HaveOccurred())
// Job is going to complete instantly: do not check for an active job
// as we are most likely to miss it
By("Ensuring a finished job exists")
err = waitForAnyFinishedJob(f.ClientSet, f.Namespace.Name)
Expect(err).NotTo(HaveOccurred())
By("Ensuring a finished job exists by listing jobs explicitly")
jobs, err := f.ClientSet.Batch().Jobs(f.Namespace.Name).List(metav1.ListOptions{})
Expect(err).NotTo(HaveOccurred())
_, finishedJobs := filterActiveJobs(jobs)
Expect(len(finishedJobs) == 1).To(BeTrue())
// Job should get deleted when the next job finishes the next minute
By("Ensuring this job does not exist anymore")
err = waitForJobNotExist(f.ClientSet, f.Namespace.Name, finishedJobs[0])
Expect(err).NotTo(HaveOccurred())
By("Ensuring there is 1 finished job by listing jobs explicitly")
jobs, err = f.ClientSet.Batch().Jobs(f.Namespace.Name).List(metav1.ListOptions{})
Expect(err).NotTo(HaveOccurred())
_, finishedJobs = filterActiveJobs(jobs)
Expect(len(finishedJobs) == 1).To(BeTrue())
By("Removing cronjob")
err = deleteCronJob(f.ClientSet, f.Namespace.Name, cronJob.Name)
Expect(err).NotTo(HaveOccurred())
})
}) })
// newTestCronJob returns a cronjob which does one of several testing behaviors. // newTestCronJob returns a cronjob which does one of several testing behaviors.
func newTestCronJob(name, schedule string, concurrencyPolicy batch.ConcurrencyPolicy, sleep bool) *batch.CronJob { func newTestCronJob(name, schedule string, concurrencyPolicy batch.ConcurrencyPolicy, command []string,
successfulJobsHistoryLimit *int32) *batch.CronJob {
parallelism := int32(1) parallelism := int32(1)
completions := int32(1) completions := int32(1)
sj := &batch.CronJob{ sj := &batch.CronJob{
...@@ -271,8 +321,9 @@ func newTestCronJob(name, schedule string, concurrencyPolicy batch.ConcurrencyPo ...@@ -271,8 +321,9 @@ func newTestCronJob(name, schedule string, concurrencyPolicy batch.ConcurrencyPo
}, },
}, },
} }
if sleep { sj.Spec.SuccessfulJobsHistoryLimit = successfulJobsHistoryLimit
sj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Command = []string{"sleep", "300"} if command != nil {
sj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Command = command
} }
return sj return sj
} }
...@@ -319,6 +370,23 @@ func waitForNoJobs(c clientset.Interface, ns, jobName string, failIfNonEmpty boo ...@@ -319,6 +370,23 @@ func waitForNoJobs(c clientset.Interface, ns, jobName string, failIfNonEmpty boo
}) })
} }
// Wait for a job to not exist by listing jobs explicitly.
func waitForJobNotExist(c clientset.Interface, ns string, targetJob *batchv1.Job) error {
return wait.Poll(framework.Poll, cronJobTimeout, func() (bool, error) {
jobs, err := c.Batch().Jobs(ns).List(metav1.ListOptions{})
if err != nil {
return false, err
}
_, finishedJobs := filterActiveJobs(jobs)
for _, job := range finishedJobs {
if targetJob.Namespace == job.Namespace && targetJob.Name == job.Name {
return false, nil
}
}
return true, nil
})
}
// Wait for a job to be replaced with a new one. // Wait for a job to be replaced with a new one.
func waitForJobReplaced(c clientset.Interface, ns, previousJobName string) error { func waitForJobReplaced(c clientset.Interface, ns, previousJobName string) error {
return wait.Poll(framework.Poll, cronJobTimeout, func() (bool, error) { return wait.Poll(framework.Poll, cronJobTimeout, func() (bool, error) {
...@@ -383,11 +451,13 @@ func checkNoEventWithReason(c clientset.Interface, ns, cronJobName string, reaso ...@@ -383,11 +451,13 @@ func checkNoEventWithReason(c clientset.Interface, ns, cronJobName string, reaso
return nil return nil
} }
func filterActiveJobs(jobs *batchv1.JobList) (active []*batchv1.Job) { func filterActiveJobs(jobs *batchv1.JobList) (active []*batchv1.Job, finished []*batchv1.Job) {
for i := range jobs.Items { for i := range jobs.Items {
j := jobs.Items[i] j := jobs.Items[i]
if !job.IsJobFinished(&j) { if !job.IsJobFinished(&j) {
active = append(active, &j) active = append(active, &j)
} else {
finished = append(finished, &j)
} }
} }
return return
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment