batch: add suspended job

Signed-off-by: Adhityaa Chandrasekar <adtac@google.com>
This commit is contained in:
Adhityaa Chandrasekar 2021-03-08 11:50:02 +00:00
parent 97cd5bb7e2
commit a0844da8f7
32 changed files with 818 additions and 245 deletions

View File

@ -4369,7 +4369,7 @@
"description": "JobSpec describes how the job execution will look like.",
"properties": {
"activeDeadlineSeconds": {
"description": "Specifies the duration in seconds relative to the startTime that the job may be active before the system tries to terminate it; value must be positive integer",
"description": "Specifies the duration in seconds relative to the startTime that the job may be continuously active before the system tries to terminate it; value must be positive integer. If a Job is suspended (at creation or through an update), this timer will effectively be stopped and reset when the Job is resumed again.",
"format": "int64",
"type": "integer"
},
@ -4400,6 +4400,10 @@
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector",
"description": "A label query over pods that should match the pod count. Normally, the system sets this field for you. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors"
},
"suspend": {
"description": "Suspend specifies whether the Job controller should create Pods or not. If a Job is created with suspend set to true, no Pods are created by the Job controller. If a Job is suspended after creation (i.e. the flag goes from false to true), the Job controller will delete all active Pods associated with this Job. Users must design their workload to gracefully handle this. Suspending a Job will reset the StartTime field of the Job, effectively resetting the ActiveDeadlineSeconds timer too. This is an alpha field and requires the SuspendJob feature gate to be enabled; otherwise this field may not be set to true. Defaults to false.",
"type": "boolean"
},
"template": {
"$ref": "#/definitions/io.k8s.api.core.v1.PodTemplateSpec",
"description": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/"
@ -4432,7 +4436,7 @@
"description": "Represents time when the job was completed. It is not guaranteed to be set in happens-before order across separate operations. It is represented in RFC3339 form and is in UTC. The completion time is only set when the job finishes successfully."
},
"conditions": {
"description": "The latest available observations of an object's current state. When a job fails, one of the conditions will have type == \"Failed\". More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
"description": "The latest available observations of an object's current state. When a Job fails, one of the conditions will have type \"Failed\" and status true. When a Job is suspended, one of the conditions will have type \"Suspended\" and status true; when the Job is resumed, the status of this condition will become false. When a Job is completed, one of the conditions will have type \"Complete\" and status true. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
"items": {
"$ref": "#/definitions/io.k8s.api.batch.v1.JobCondition"
},
@ -4448,7 +4452,7 @@
},
"startTime": {
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Time",
"description": "Represents time when the job was acknowledged by the job controller. It is not guaranteed to be set in happens-before order across separate operations. It is represented in RFC3339 form and is in UTC."
"description": "Represents time when the job controller started processing a job. When a Job is created in the suspended state, this field is not set until the first time it is resumed. This field is reset every time a Job is resumed from suspension. It is represented in RFC3339 form and is in UTC."
},
"succeeded": {
"description": "The number of pods which reached phase Succeeded.",

View File

@ -20,14 +20,9 @@ import (
fuzz "github.com/google/gofuzz"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/utils/pointer"
)
func newBool(val bool) *bool {
p := new(bool)
*p = val
return p
}
// Funcs returns the fuzzer functions for the batch api group.
var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
@ -48,7 +43,7 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
j.Parallelism = &parallelism
j.BackoffLimit = &backoffLimit
if c.Rand.Int31()%2 == 0 {
j.ManualSelector = newBool(true)
j.ManualSelector = pointer.BoolPtr(true)
} else {
j.ManualSelector = nil
}
@ -57,6 +52,9 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
} else {
j.CompletionMode = batch.IndexedCompletion
}
// We're fuzzing the internal JobSpec type, not the v1 type, so we don't
// need to fuzz the nil value.
j.Suspend = pointer.BoolPtr(c.RandBool())
},
func(sj *batch.CronJobSpec, c fuzz.Continue) {
c.FuzzNoCustom(sj)

View File

@ -119,8 +119,11 @@ type JobSpec struct {
// +optional
Completions *int32
// Optional duration in seconds relative to the startTime that the job may be active
// before the system tries to terminate it; value must be positive integer
// Specifies the duration in seconds relative to the startTime that the job
// may be continuously active before the system tries to terminate it; value
// must be positive integer. If a Job is suspended (at creation or through an
// update), this timer will effectively be stopped and reset when the Job is
// resumed again.
// +optional
ActiveDeadlineSeconds *int64
@ -187,19 +190,36 @@ type JobSpec struct {
// controller skips updates for the Job.
// +optional
CompletionMode CompletionMode
// Suspend specifies whether the Job controller should create Pods or not. If
// a Job is created with suspend set to true, no Pods are created by the Job
// controller. If a Job is suspended after creation (i.e. the flag goes from
// false to true), the Job controller will delete all active Pods associated
// with this Job. Users must design their workload to gracefully handle this.
// Suspending a Job will reset the StartTime field of the Job, effectively
// resetting the ActiveDeadlineSeconds timer too. This is an alpha field and
// requires the SuspendJob feature gate to be enabled; otherwise this field
// may not be set to true. Defaults to false.
// +optional
Suspend *bool
}
// JobStatus represents the current state of a Job.
type JobStatus struct {
// The latest available observations of an object's current state.
// When a job fails, one of the conditions will have type == "Failed".
// The latest available observations of an object's current state. When a Job
// fails, one of the conditions will have type "Failed" and status true. When
// a Job is suspended, one of the conditions will have type "Suspended" and
// status true; when the Job is resumed, the status of this condition will
// become false. When a Job is completed, one of the conditions will have
// type "Complete" and status true.
// +optional
Conditions []JobCondition
// Represents time when the job was acknowledged by the job controller.
// It is not guaranteed to be set in happens-before order across separate operations.
// It is represented in RFC3339 form and is in UTC.
// Represents time when the job controller started processing a job. When a
// Job is created in the suspended state, this field is not set until the
// first time it is resumed. This field is reset every time a Job is resumed
// from suspension. It is represented in RFC3339 form and is in UTC.
// +optional
StartTime *metav1.Time
@ -238,6 +258,8 @@ type JobConditionType string
// These are valid conditions of a job.
const (
// JobSuspended means the job has been suspended.
JobSuspended JobConditionType = "Suspended"
// JobComplete means the job has completed its execution.
JobComplete JobConditionType = "Complete"
// JobFailed means the job has failed its execution.
@ -246,7 +268,7 @@ const (
// JobCondition describes current state of a job.
type JobCondition struct {
// Type of job condition, Complete or Failed.
// Type of job condition.
Type JobConditionType
// Status of the condition, one of True, False, Unknown.
Status api.ConditionStatus

View File

@ -46,6 +46,9 @@ func SetDefaults_Job(obj *batchv1.Job) {
if len(obj.Spec.CompletionMode) == 0 {
obj.Spec.CompletionMode = batchv1.NonIndexedCompletion
}
if obj.Spec.Suspend == nil {
obj.Spec.Suspend = utilpointer.BoolPtr(false)
}
}
func SetDefaults_CronJob(obj *batchv1.CronJob) {

View File

@ -20,6 +20,7 @@ import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
batchv1 "k8s.io/api/batch/v1"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -27,7 +28,7 @@ import (
"k8s.io/kubernetes/pkg/api/legacyscheme"
_ "k8s.io/kubernetes/pkg/apis/batch/install"
_ "k8s.io/kubernetes/pkg/apis/core/install"
utilpointer "k8s.io/utils/pointer"
"k8s.io/utils/pointer"
. "k8s.io/kubernetes/pkg/apis/batch/v1"
)
@ -49,15 +50,36 @@ func TestSetDefaultJob(t *testing.T) {
},
expected: &batchv1.Job{
Spec: batchv1.JobSpec{
Completions: utilpointer.Int32Ptr(1),
Parallelism: utilpointer.Int32Ptr(1),
BackoffLimit: utilpointer.Int32Ptr(6),
Completions: pointer.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(1),
BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: batchv1.NonIndexedCompletion,
Suspend: pointer.BoolPtr(false),
},
},
expectLabels: true,
},
"All unspecified -> all integers are defaulted and no default labels": {
"suspend set, everything else is defaulted": {
original: &batchv1.Job{
Spec: batchv1.JobSpec{
Suspend: pointer.BoolPtr(true),
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: defaultLabels},
},
},
},
expected: &batchv1.Job{
Spec: batchv1.JobSpec{
Completions: pointer.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(1),
BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: batchv1.NonIndexedCompletion,
Suspend: pointer.BoolPtr(true),
},
},
expectLabels: true,
},
"All unspecified -> all pointers, CompletionMode are defaulted and no default labels": {
original: &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"mylabel": "myvalue"},
@ -70,17 +92,18 @@ func TestSetDefaultJob(t *testing.T) {
},
expected: &batchv1.Job{
Spec: batchv1.JobSpec{
Completions: utilpointer.Int32Ptr(1),
Parallelism: utilpointer.Int32Ptr(1),
BackoffLimit: utilpointer.Int32Ptr(6),
Completions: pointer.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(1),
BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: batchv1.NonIndexedCompletion,
Suspend: pointer.BoolPtr(false),
},
},
},
"WQ: Parallelism explicitly 0 and completions unset -> BackoffLimit is defaulted": {
original: &batchv1.Job{
Spec: batchv1.JobSpec{
Parallelism: utilpointer.Int32Ptr(0),
Parallelism: pointer.Int32Ptr(0),
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: defaultLabels},
},
@ -88,9 +111,10 @@ func TestSetDefaultJob(t *testing.T) {
},
expected: &batchv1.Job{
Spec: batchv1.JobSpec{
Parallelism: utilpointer.Int32Ptr(0),
BackoffLimit: utilpointer.Int32Ptr(6),
Parallelism: pointer.Int32Ptr(0),
BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: batchv1.NonIndexedCompletion,
Suspend: pointer.BoolPtr(false),
},
},
expectLabels: true,
@ -98,7 +122,7 @@ func TestSetDefaultJob(t *testing.T) {
"WQ: Parallelism explicitly 2 and completions unset -> BackoffLimit is defaulted": {
original: &batchv1.Job{
Spec: batchv1.JobSpec{
Parallelism: utilpointer.Int32Ptr(2),
Parallelism: pointer.Int32Ptr(2),
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: defaultLabels},
},
@ -106,9 +130,10 @@ func TestSetDefaultJob(t *testing.T) {
},
expected: &batchv1.Job{
Spec: batchv1.JobSpec{
Parallelism: utilpointer.Int32Ptr(2),
BackoffLimit: utilpointer.Int32Ptr(6),
Parallelism: pointer.Int32Ptr(2),
BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: batchv1.NonIndexedCompletion,
Suspend: pointer.BoolPtr(false),
},
},
expectLabels: true,
@ -116,7 +141,7 @@ func TestSetDefaultJob(t *testing.T) {
"Completions explicitly 2 and others unset -> parallelism and BackoffLimit are defaulted": {
original: &batchv1.Job{
Spec: batchv1.JobSpec{
Completions: utilpointer.Int32Ptr(2),
Completions: pointer.Int32Ptr(2),
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: defaultLabels},
},
@ -124,10 +149,11 @@ func TestSetDefaultJob(t *testing.T) {
},
expected: &batchv1.Job{
Spec: batchv1.JobSpec{
Completions: utilpointer.Int32Ptr(2),
Parallelism: utilpointer.Int32Ptr(1),
BackoffLimit: utilpointer.Int32Ptr(6),
Completions: pointer.Int32Ptr(2),
Parallelism: pointer.Int32Ptr(1),
BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: batchv1.NonIndexedCompletion,
Suspend: pointer.BoolPtr(false),
},
},
expectLabels: true,
@ -135,7 +161,7 @@ func TestSetDefaultJob(t *testing.T) {
"BackoffLimit explicitly 5 and others unset -> parallelism and completions are defaulted": {
original: &batchv1.Job{
Spec: batchv1.JobSpec{
BackoffLimit: utilpointer.Int32Ptr(5),
BackoffLimit: pointer.Int32Ptr(5),
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: defaultLabels},
},
@ -143,10 +169,11 @@ func TestSetDefaultJob(t *testing.T) {
},
expected: &batchv1.Job{
Spec: batchv1.JobSpec{
Completions: utilpointer.Int32Ptr(1),
Parallelism: utilpointer.Int32Ptr(1),
BackoffLimit: utilpointer.Int32Ptr(5),
Completions: pointer.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(1),
BackoffLimit: pointer.Int32Ptr(5),
CompletionMode: batchv1.NonIndexedCompletion,
Suspend: pointer.BoolPtr(false),
},
},
expectLabels: true,
@ -154,10 +181,11 @@ func TestSetDefaultJob(t *testing.T) {
"All set -> no change": {
original: &batchv1.Job{
Spec: batchv1.JobSpec{
Completions: utilpointer.Int32Ptr(8),
Parallelism: utilpointer.Int32Ptr(9),
BackoffLimit: utilpointer.Int32Ptr(10),
Completions: pointer.Int32Ptr(8),
Parallelism: pointer.Int32Ptr(9),
BackoffLimit: pointer.Int32Ptr(10),
CompletionMode: batchv1.NonIndexedCompletion,
Suspend: pointer.BoolPtr(true),
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: defaultLabels},
},
@ -165,10 +193,11 @@ func TestSetDefaultJob(t *testing.T) {
},
expected: &batchv1.Job{
Spec: batchv1.JobSpec{
Completions: utilpointer.Int32Ptr(8),
Parallelism: utilpointer.Int32Ptr(9),
BackoffLimit: utilpointer.Int32Ptr(10),
Completions: pointer.Int32Ptr(8),
Parallelism: pointer.Int32Ptr(9),
BackoffLimit: pointer.Int32Ptr(10),
CompletionMode: batchv1.NonIndexedCompletion,
Suspend: pointer.BoolPtr(true),
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: defaultLabels},
},
@ -179,10 +208,11 @@ func TestSetDefaultJob(t *testing.T) {
"All set, flipped -> no change": {
original: &batchv1.Job{
Spec: batchv1.JobSpec{
Completions: utilpointer.Int32Ptr(11),
Parallelism: utilpointer.Int32Ptr(10),
BackoffLimit: utilpointer.Int32Ptr(9),
Completions: pointer.Int32Ptr(11),
Parallelism: pointer.Int32Ptr(10),
BackoffLimit: pointer.Int32Ptr(9),
CompletionMode: batchv1.IndexedCompletion,
Suspend: pointer.BoolPtr(true),
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: defaultLabels},
},
@ -190,10 +220,11 @@ func TestSetDefaultJob(t *testing.T) {
},
expected: &batchv1.Job{
Spec: batchv1.JobSpec{
Completions: utilpointer.Int32Ptr(11),
Parallelism: utilpointer.Int32Ptr(10),
BackoffLimit: utilpointer.Int32Ptr(9),
Completions: pointer.Int32Ptr(11),
Parallelism: pointer.Int32Ptr(10),
BackoffLimit: pointer.Int32Ptr(9),
CompletionMode: batchv1.IndexedCompletion,
Suspend: pointer.BoolPtr(true),
},
},
expectLabels: true,
@ -211,6 +242,9 @@ func TestSetDefaultJob(t *testing.T) {
t.Fatalf("Unexpected object: %v", actual)
}
if diff := cmp.Diff(expected.Spec.Suspend, actual.Spec.Suspend); diff != "" {
t.Errorf(".spec.suspend does not match; -want,+got:\n%s", diff)
}
validateDefaultInt32(t, "Completions", actual.Spec.Completions, expected.Spec.Completions)
validateDefaultInt32(t, "Parallelism", actual.Spec.Parallelism, expected.Spec.Parallelism)
validateDefaultInt32(t, "BackoffLimit", actual.Spec.BackoffLimit, expected.Spec.BackoffLimit)
@ -271,8 +305,8 @@ func TestSetDefaultCronJob(t *testing.T) {
Spec: batchv1.CronJobSpec{
ConcurrencyPolicy: batchv1.AllowConcurrent,
Suspend: newBool(false),
SuccessfulJobsHistoryLimit: utilpointer.Int32Ptr(3),
FailedJobsHistoryLimit: utilpointer.Int32Ptr(1),
SuccessfulJobsHistoryLimit: pointer.Int32Ptr(3),
FailedJobsHistoryLimit: pointer.Int32Ptr(1),
},
},
},
@ -281,16 +315,16 @@ func TestSetDefaultCronJob(t *testing.T) {
Spec: batchv1.CronJobSpec{
ConcurrencyPolicy: batchv1.ForbidConcurrent,
Suspend: newBool(true),
SuccessfulJobsHistoryLimit: utilpointer.Int32Ptr(5),
FailedJobsHistoryLimit: utilpointer.Int32Ptr(5),
SuccessfulJobsHistoryLimit: pointer.Int32Ptr(5),
FailedJobsHistoryLimit: pointer.Int32Ptr(5),
},
},
expected: &batchv1.CronJob{
Spec: batchv1.CronJobSpec{
ConcurrencyPolicy: batchv1.ForbidConcurrent,
Suspend: newBool(true),
SuccessfulJobsHistoryLimit: utilpointer.Int32Ptr(5),
FailedJobsHistoryLimit: utilpointer.Int32Ptr(5),
SuccessfulJobsHistoryLimit: pointer.Int32Ptr(5),
FailedJobsHistoryLimit: pointer.Int32Ptr(5),
},
},
},

View File

@ -393,6 +393,7 @@ func autoConvert_v1_JobSpec_To_batch_JobSpec(in *v1.JobSpec, out *batch.JobSpec,
}
out.TTLSecondsAfterFinished = (*int32)(unsafe.Pointer(in.TTLSecondsAfterFinished))
out.CompletionMode = batch.CompletionMode(in.CompletionMode)
out.Suspend = (*bool)(unsafe.Pointer(in.Suspend))
return nil
}
@ -408,6 +409,7 @@ func autoConvert_batch_JobSpec_To_v1_JobSpec(in *batch.JobSpec, out *v1.JobSpec,
}
out.TTLSecondsAfterFinished = (*int32)(unsafe.Pointer(in.TTLSecondsAfterFinished))
out.CompletionMode = v1.CompletionMode(in.CompletionMode)
out.Suspend = (*bool)(unsafe.Pointer(in.Suspend))
return nil
}

View File

@ -271,6 +271,11 @@ func (in *JobSpec) DeepCopyInto(out *JobSpec) {
*out = new(int32)
**out = **in
}
if in.Suspend != nil {
in, out := &in.Suspend, &out.Suspend
*out = new(bool)
**out = **in
}
return
}

View File

@ -487,9 +487,9 @@ func (jm *Controller) syncJob(key string) (bool, error) {
activePods := controller.FilterActivePods(pods)
active := int32(len(activePods))
succeeded, failed := getStatus(&job, pods)
conditions := len(job.Status.Conditions)
// job first start
if job.Status.StartTime == nil {
// Job first start. Set StartTime and start the ActiveDeadlineSeconds timer
// only if the job is not in the suspended state.
if job.Status.StartTime == nil && !jobSuspended(&job) {
now := metav1.Now()
job.Status.StartTime = &now
// enqueue a sync to check if job past ActiveDeadlineSeconds
@ -524,6 +524,8 @@ func (jm *Controller) syncJob(key string) (bool, error) {
failureMessage = "Job was active longer than specified deadline"
}
jobConditionsChanged := false
manageJobCalled := false
if jobFailed {
// TODO(#28486): Account for pod failures in status once we can track
// completions without lingering pods.
@ -532,11 +534,13 @@ func (jm *Controller) syncJob(key string) (bool, error) {
// update status values accordingly
failed += active
active = 0
job.Status.Conditions = append(job.Status.Conditions, newCondition(batch.JobFailed, failureReason, failureMessage))
job.Status.Conditions = append(job.Status.Conditions, newCondition(batch.JobFailed, v1.ConditionTrue, failureReason, failureMessage))
jobConditionsChanged = true
jm.recorder.Event(&job, v1.EventTypeWarning, failureReason, failureMessage)
} else {
if jobNeedsSync && job.DeletionTimestamp == nil {
active, manageJobErr = jm.manageJob(&job, activePods, succeeded, pods)
manageJobCalled = true
}
completions := succeeded
complete := false
@ -566,10 +570,40 @@ func (jm *Controller) syncJob(key string) (bool, error) {
}
}
if complete {
job.Status.Conditions = append(job.Status.Conditions, newCondition(batch.JobComplete, "", ""))
job.Status.Conditions = append(job.Status.Conditions, newCondition(batch.JobComplete, v1.ConditionTrue, "", ""))
jobConditionsChanged = true
now := metav1.Now()
job.Status.CompletionTime = &now
jm.recorder.Event(&job, v1.EventTypeNormal, "Completed", "Job completed")
} else if utilfeature.DefaultFeatureGate.Enabled(features.SuspendJob) && manageJobCalled {
// Update the conditions / emit events only if manageJob was called in
// this syncJob. Otherwise wait for the right syncJob call to make
// updates.
if job.Spec.Suspend != nil && *job.Spec.Suspend {
// Job can be in the suspended state only if it is NOT completed.
var isUpdated bool
job.Status.Conditions, isUpdated = ensureJobConditionStatus(job.Status.Conditions, batch.JobSuspended, v1.ConditionTrue, "JobSuspended", "Job suspended")
if isUpdated {
jobConditionsChanged = true
jm.recorder.Event(&job, v1.EventTypeNormal, "Suspended", "Job suspended")
}
} else {
// Job not suspended.
var isUpdated bool
job.Status.Conditions, isUpdated = ensureJobConditionStatus(job.Status.Conditions, batch.JobSuspended, v1.ConditionFalse, "JobResumed", "Job resumed")
if isUpdated {
jobConditionsChanged = true
jm.recorder.Event(&job, v1.EventTypeNormal, "Resumed", "Job resumed")
// Resumed jobs will always reset StartTime to current time. This is
// done because the ActiveDeadlineSeconds timer shouldn't go off
// whilst the Job is still suspended and resetting StartTime is
// consistent with resuming a Job created in the suspended state.
// (ActiveDeadlineSeconds is interpreted as the number of seconds a
// Job is continuously active.)
now := metav1.Now()
job.Status.StartTime = &now
}
}
}
}
@ -583,7 +617,7 @@ func (jm *Controller) syncJob(key string) (bool, error) {
}
// no need to update the job if the status hasn't changed since last time
if job.Status.Active != active || job.Status.Succeeded != succeeded || job.Status.Failed != failed || len(job.Status.Conditions) != conditions {
if job.Status.Active != active || job.Status.Succeeded != succeeded || job.Status.Failed != failed || jobConditionsChanged {
job.Status.Active = active
job.Status.Succeeded = succeeded
job.Status.Failed = failed
@ -660,9 +694,11 @@ func pastBackoffLimitOnFailure(job *batch.Job, pods []*v1.Pod) bool {
return result >= *job.Spec.BackoffLimit
}
// pastActiveDeadline checks if job has ActiveDeadlineSeconds field set and if it is exceeded.
// pastActiveDeadline checks if job has ActiveDeadlineSeconds field set and if
// it is exceeded. If the job is currently suspended, the function will always
// return false.
func pastActiveDeadline(job *batch.Job) bool {
if job.Spec.ActiveDeadlineSeconds == nil || job.Status.StartTime == nil {
if job.Spec.ActiveDeadlineSeconds == nil || job.Status.StartTime == nil || jobSuspended(job) {
return false
}
now := metav1.Now()
@ -672,10 +708,10 @@ func pastActiveDeadline(job *batch.Job) bool {
return duration >= allowedDuration
}
func newCondition(conditionType batch.JobConditionType, reason, message string) batch.JobCondition {
func newCondition(conditionType batch.JobConditionType, status v1.ConditionStatus, reason, message string) batch.JobCondition {
return batch.JobCondition{
Type: conditionType,
Status: v1.ConditionTrue,
Status: status,
LastProbeTime: metav1.Now(),
LastTransitionTime: metav1.Now(),
Reason: reason,
@ -690,6 +726,12 @@ func getStatus(job *batch.Job, pods []*v1.Pod) (succeeded, failed int32) {
return
}
// jobSuspended returns whether a Job is suspended while taking the feature
// gate into account.
func jobSuspended(job *batch.Job) bool {
return utilfeature.DefaultFeatureGate.Enabled(features.SuspendJob) && job.Spec.Suspend != nil && *job.Spec.Suspend
}
// manageJob is the core method responsible for managing the number of running
// pods according to what is specified in the job.Spec.
// Does NOT modify <activePods>.
@ -702,6 +744,15 @@ func (jm *Controller) manageJob(job *batch.Job, activePods []*v1.Pod, succeeded
return 0, nil
}
if jobSuspended(job) {
klog.V(4).InfoS("Deleting all active pods in suspended job", "job", klog.KObj(job), "active", active)
podsToDelete := activePodsForRemoval(job, activePods, int(active))
jm.expectations.ExpectDeletions(jobKey, len(podsToDelete))
removed, err := jm.deleteJobPods(job, jobKey, podsToDelete)
active -= removed
return active, err
}
rmAtLeast := active - parallelism
if rmAtLeast < 0 {
rmAtLeast = 0
@ -709,7 +760,7 @@ func (jm *Controller) manageJob(job *batch.Job, activePods []*v1.Pod, succeeded
podsToDelete := activePodsForRemoval(job, activePods, int(rmAtLeast))
if len(podsToDelete) > 0 {
jm.expectations.ExpectDeletions(jobKey, len(podsToDelete))
klog.V(4).InfoS("Too many pods running for job", "job", klog.KObj(job), "deleted", len(podsToDelete), "target", parallelism)
klog.V(4).InfoS("Too many pods running for job", "job", klog.KObj(job), "deleted", rmAtLeast, "target", parallelism)
removed, err := jm.deleteJobPods(job, jobKey, podsToDelete)
active -= removed
if err != nil {
@ -910,3 +961,29 @@ func errorFromChannel(errCh <-chan error) error {
}
return nil
}
// ensureJobConditionStatus appends or updates an existing job condition of the
// given type with the given status value. Note that this function will not
// append to the conditions list if the new condition's status is false
// (because going from nothing to false is meaningless); it can, however,
// update the status condition to false. The function returns a bool to let the
// caller know if the list was changed (either appended or updated).
func ensureJobConditionStatus(list []batch.JobCondition, cType batch.JobConditionType, status v1.ConditionStatus, reason, message string) ([]batch.JobCondition, bool) {
for i := range list {
if list[i].Type == cType {
if list[i].Status != status || list[i].Reason != reason || list[i].Message != message {
list[i].Status = status
list[i].LastTransitionTime = metav1.Now()
list[i].Reason = reason
list[i].Message = message
return list, true
}
return list, false
}
}
// A condition with that type doesn't exist in the list.
if status != v1.ConditionFalse {
return append(list, newCondition(cType, status, reason, message)), true
}
return list, false
}

View File

@ -47,6 +47,7 @@ import (
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/controller/testutil"
"k8s.io/kubernetes/pkg/features"
"k8s.io/utils/pointer"
)
var alwaysReady = func() bool { return true }
@ -156,6 +157,7 @@ func setPodsStatusesWithIndexes(podIndexer cache.Indexer, job *batch.Job, status
func TestControllerSyncJob(t *testing.T) {
jobConditionComplete := batch.JobComplete
jobConditionFailed := batch.JobFailed
jobConditionSuspended := batch.JobSuspended
testCases := map[string]struct {
// job setup
@ -165,6 +167,8 @@ func TestControllerSyncJob(t *testing.T) {
deleting bool
podLimit int
completionMode batch.CompletionMode
wasSuspended bool
suspend bool
// pod setup
podControllerError error
@ -174,6 +178,7 @@ func TestControllerSyncJob(t *testing.T) {
succeededPods int32
failedPods int32
podsWithIndexes []indexPhase
fakeExpectationAtCreation int32 // negative: ExpectDeletions, positive: ExpectCreations
// expectations
expectedCreations int32
@ -183,11 +188,13 @@ func TestControllerSyncJob(t *testing.T) {
expectedCompletedIdxs string
expectedFailed int32
expectedCondition *batch.JobConditionType
expectedConditionStatus v1.ConditionStatus
expectedConditionReason string
expectedCreatedIndexes sets.Int
// features
indexedJobEnabled bool
suspendJobEnabled bool
}{
"job start": {
parallelism: 2,
@ -341,6 +348,7 @@ func TestControllerSyncJob(t *testing.T) {
succeededPods: 2,
expectedSucceeded: 2,
expectedCondition: &jobConditionComplete,
expectedConditionStatus: v1.ConditionTrue,
},
"WQ job all finished despite one failure": {
parallelism: 2,
@ -352,6 +360,7 @@ func TestControllerSyncJob(t *testing.T) {
expectedSucceeded: 1,
expectedFailed: 1,
expectedCondition: &jobConditionComplete,
expectedConditionStatus: v1.ConditionTrue,
},
"more active pods than completions": {
parallelism: 2,
@ -401,6 +410,7 @@ func TestControllerSyncJob(t *testing.T) {
failedPods: 1,
expectedFailed: 1,
expectedCondition: &jobConditionFailed,
expectedConditionStatus: v1.ConditionTrue,
expectedConditionReason: "BackoffLimitExceeded",
},
"indexed job start": {
@ -510,11 +520,78 @@ func TestControllerSyncJob(t *testing.T) {
// No status updates.
indexedJobEnabled: false,
},
"suspending a job with satisfied expectations": {
// Suspended Job should delete active pods when expectations are
// satisfied.
suspendJobEnabled: true,
suspend: true,
parallelism: 2,
activePods: 2, // parallelism == active, expectations satisfied
completions: 4,
backoffLimit: 6,
jobKeyForget: true,
expectedCreations: 0,
expectedDeletions: 2,
expectedActive: 0,
expectedCondition: &jobConditionSuspended,
expectedConditionStatus: v1.ConditionTrue,
expectedConditionReason: "JobSuspended",
},
"suspending a job with unsatisfied expectations": {
// Unlike the previous test, we expect the controller to NOT suspend the
// Job in the syncJob call because the controller will wait for
// expectations to be satisfied first. The next syncJob call (not tested
// here) will be the same as the previous test.
suspendJobEnabled: true,
suspend: true,
parallelism: 2,
activePods: 3, // active > parallelism, expectations unsatisfied
fakeExpectationAtCreation: -1, // the controller is expecting a deletion
completions: 4,
backoffLimit: 6,
jobKeyForget: true,
expectedCreations: 0,
expectedDeletions: 0,
expectedActive: 3,
},
"resuming a suspended job": {
suspendJobEnabled: true,
wasSuspended: true,
suspend: false,
parallelism: 2,
completions: 4,
backoffLimit: 6,
jobKeyForget: true,
expectedCreations: 2,
expectedDeletions: 0,
expectedActive: 2,
expectedCondition: &jobConditionSuspended,
expectedConditionStatus: v1.ConditionFalse,
expectedConditionReason: "JobResumed",
},
"suspending a deleted job": {
// We would normally expect the active pods to be deleted (see a few test
// cases above), but since this job is being deleted, we don't expect
// anything changed here from before the job was suspended. The
// JobSuspended condition is also missing.
suspendJobEnabled: true,
suspend: true,
deleting: true,
parallelism: 2,
activePods: 2, // parallelism == active, expectations satisfied
completions: 4,
backoffLimit: 6,
jobKeyForget: true,
expectedCreations: 0,
expectedDeletions: 0,
expectedActive: 2,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.IndexedJob, tc.indexedJobEnabled)()
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.SuspendJob, tc.suspendJobEnabled)()
// job manager setup
clientSet := clientset.NewForConfigOrDie(&restclient.Config{Host: "", ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}})
@ -526,6 +603,19 @@ func TestControllerSyncJob(t *testing.T) {
// job & pods setup
job := newJob(tc.parallelism, tc.completions, tc.backoffLimit, tc.completionMode)
job.Spec.Suspend = pointer.BoolPtr(tc.suspend)
key, err := controller.KeyFunc(job)
if err != nil {
t.Errorf("Unexpected error getting job key: %v", err)
}
if tc.fakeExpectationAtCreation < 0 {
manager.expectations.ExpectDeletions(key, int(-tc.fakeExpectationAtCreation))
} else if tc.fakeExpectationAtCreation > 0 {
manager.expectations.ExpectCreations(key, int(tc.fakeExpectationAtCreation))
}
if tc.wasSuspended {
job.Status.Conditions = append(job.Status.Conditions, newCondition(batch.JobSuspended, v1.ConditionTrue, "JobSuspended", "Job suspended"))
}
if tc.deleting {
now := metav1.Now()
job.DeletionTimestamp = &now
@ -608,13 +698,19 @@ func TestControllerSyncJob(t *testing.T) {
if actual.Status.Failed != tc.expectedFailed {
t.Errorf("Unexpected number of failed pods. Expected %d, saw %d\n", tc.expectedFailed, actual.Status.Failed)
}
if actual.Status.StartTime == nil && tc.indexedJobEnabled {
if actual.Status.StartTime != nil && tc.suspend {
t.Error("Unexpected .status.startTime not nil when suspend is true")
}
if actual.Status.StartTime == nil && tc.indexedJobEnabled && !tc.suspend {
t.Error("Missing .status.startTime")
}
// validate conditions
if tc.expectedCondition != nil && !getCondition(actual, *tc.expectedCondition, tc.expectedConditionReason) {
if tc.expectedCondition != nil && !getCondition(actual, *tc.expectedCondition, tc.expectedConditionStatus, tc.expectedConditionReason) {
t.Errorf("Expected completion condition. Got %#v", actual.Status.Conditions)
}
if tc.expectedCondition == nil && tc.suspend && len(actual.Status.Conditions) != 0 {
t.Errorf("Unexpected conditions %v", actual.Status.Conditions)
}
// validate slow start
expectedLimit := 0
for pass := uint8(0); expectedLimit <= tc.podLimit; pass++ {
@ -652,6 +748,7 @@ func TestSyncJobPastDeadline(t *testing.T) {
activeDeadlineSeconds int64
startTime int64
backoffLimit int32
suspend bool
// pod setup
activePods int32
@ -664,7 +761,11 @@ func TestSyncJobPastDeadline(t *testing.T) {
expectedActive int32
expectedSucceeded int32
expectedFailed int32
expectedCondition batch.JobConditionType
expectedConditionReason string
// features
suspendJobEnabled bool
}{
"activeDeadlineSeconds less than single pod execution": {
parallelism: 1,
@ -676,6 +777,7 @@ func TestSyncJobPastDeadline(t *testing.T) {
expectedForGetKey: true,
expectedDeletions: 1,
expectedFailed: 1,
expectedCondition: batch.JobFailed,
expectedConditionReason: "DeadlineExceeded",
},
"activeDeadlineSeconds bigger than single pod execution": {
@ -690,6 +792,7 @@ func TestSyncJobPastDeadline(t *testing.T) {
expectedDeletions: 1,
expectedSucceeded: 1,
expectedFailed: 1,
expectedCondition: batch.JobFailed,
expectedConditionReason: "DeadlineExceeded",
},
"activeDeadlineSeconds times-out before any pod starts": {
@ -699,6 +802,7 @@ func TestSyncJobPastDeadline(t *testing.T) {
startTime: 10,
backoffLimit: 6,
expectedForGetKey: true,
expectedCondition: batch.JobFailed,
expectedConditionReason: "DeadlineExceeded",
},
"activeDeadlineSeconds with backofflimit reach": {
@ -709,12 +813,27 @@ func TestSyncJobPastDeadline(t *testing.T) {
failedPods: 1,
expectedForGetKey: true,
expectedFailed: 1,
expectedCondition: batch.JobFailed,
expectedConditionReason: "BackoffLimitExceeded",
},
"activeDeadlineSeconds is not triggered when Job is suspended": {
suspendJobEnabled: true,
suspend: true,
parallelism: 1,
completions: 2,
activeDeadlineSeconds: 10,
startTime: 15,
backoffLimit: 6,
expectedForGetKey: true,
expectedCondition: batch.JobSuspended,
expectedConditionReason: "JobSuspended",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.SuspendJob, tc.suspendJobEnabled)()
// job manager setup
clientSet := clientset.NewForConfigOrDie(&restclient.Config{Host: "", ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}})
manager, sharedInformerFactory := newControllerFromClient(clientSet, controller.NoResyncPeriodFunc)
@ -731,6 +850,7 @@ func TestSyncJobPastDeadline(t *testing.T) {
// job & pods setup
job := newJob(tc.parallelism, tc.completions, tc.backoffLimit, batch.NonIndexedCompletion)
job.Spec.ActiveDeadlineSeconds = &tc.activeDeadlineSeconds
job.Spec.Suspend = pointer.BoolPtr(tc.suspend)
start := metav1.Unix(metav1.Now().Time.Unix()-tc.startTime, 0)
job.Status.StartTime = &start
sharedInformerFactory.Batch().V1().Jobs().Informer().GetIndexer().Add(job)
@ -766,16 +886,16 @@ func TestSyncJobPastDeadline(t *testing.T) {
t.Error("Missing .status.startTime")
}
// validate conditions
if !getCondition(actual, batch.JobFailed, tc.expectedConditionReason) {
if !getCondition(actual, tc.expectedCondition, v1.ConditionTrue, tc.expectedConditionReason) {
t.Errorf("Expected fail condition. Got %#v", actual.Status.Conditions)
}
})
}
}
func getCondition(job *batch.Job, condition batch.JobConditionType, reason string) bool {
func getCondition(job *batch.Job, condition batch.JobConditionType, status v1.ConditionStatus, reason string) bool {
for _, v := range job.Status.Conditions {
if v.Type == condition && v.Status == v1.ConditionTrue && v.Reason == reason {
if v.Type == condition && v.Status == status && v.Reason == reason {
return true
}
}
@ -800,7 +920,7 @@ func TestSyncPastDeadlineJobFinished(t *testing.T) {
job.Spec.ActiveDeadlineSeconds = &activeDeadlineSeconds
start := metav1.Unix(metav1.Now().Time.Unix()-15, 0)
job.Status.StartTime = &start
job.Status.Conditions = append(job.Status.Conditions, newCondition(batch.JobFailed, "DeadlineExceeded", "Job was active longer than specified deadline"))
job.Status.Conditions = append(job.Status.Conditions, newCondition(batch.JobFailed, v1.ConditionTrue, "DeadlineExceeded", "Job was active longer than specified deadline"))
sharedInformerFactory.Batch().V1().Jobs().Informer().GetIndexer().Add(job)
forget, err := manager.syncJob(testutil.GetKey(job, t))
if err != nil {
@ -829,7 +949,7 @@ func TestSyncJobComplete(t *testing.T) {
manager.jobStoreSynced = alwaysReady
job := newJob(1, 1, 6, batch.NonIndexedCompletion)
job.Status.Conditions = append(job.Status.Conditions, newCondition(batch.JobComplete, "", ""))
job.Status.Conditions = append(job.Status.Conditions, newCondition(batch.JobComplete, v1.ConditionTrue, "", ""))
sharedInformerFactory.Batch().V1().Jobs().Informer().GetIndexer().Add(job)
forget, err := manager.syncJob(testutil.GetKey(job, t))
if err != nil {
@ -1572,7 +1692,7 @@ func TestJobBackoffReset(t *testing.T) {
if retries != 0 {
t.Errorf("%s: expected exactly 0 retries, got %d", name, retries)
}
if getCondition(actual, batch.JobFailed, "BackoffLimitExceeded") {
if getCondition(actual, batch.JobFailed, v1.ConditionTrue, "BackoffLimitExceeded") {
t.Errorf("%s: unexpected job failure", name)
}
}
@ -1760,7 +1880,7 @@ func TestJobBackoffForOnFailure(t *testing.T) {
t.Errorf("unexpected number of failed pods. Expected %d, saw %d\n", tc.expectedFailed, actual.Status.Failed)
}
// validate conditions
if tc.expectedCondition != nil && !getCondition(actual, *tc.expectedCondition, tc.expectedConditionReason) {
if tc.expectedCondition != nil && !getCondition(actual, *tc.expectedCondition, v1.ConditionTrue, tc.expectedConditionReason) {
t.Errorf("expected completion condition. Got %#v", actual.Status.Conditions)
}
})
@ -1864,13 +1984,99 @@ func TestJobBackoffOnRestartPolicyNever(t *testing.T) {
t.Errorf("unexpected number of failed pods. Expected %d, saw %d\n", tc.expectedFailed, actual.Status.Failed)
}
// validate conditions
if tc.expectedCondition != nil && !getCondition(actual, *tc.expectedCondition, tc.expectedConditionReason) {
if tc.expectedCondition != nil && !getCondition(actual, *tc.expectedCondition, v1.ConditionTrue, tc.expectedConditionReason) {
t.Errorf("expected completion condition. Got %#v", actual.Status.Conditions)
}
})
}
}
func TestEnsureJobConditions(t *testing.T) {
testCases := []struct {
name string
haveList []batch.JobCondition
wantType batch.JobConditionType
wantStatus v1.ConditionStatus
wantReason string
expectList []batch.JobCondition
expectUpdate bool
}{
{
name: "append true condition",
haveList: []batch.JobCondition{},
wantType: batch.JobSuspended,
wantStatus: v1.ConditionTrue,
wantReason: "foo",
expectList: []batch.JobCondition{newCondition(batch.JobSuspended, v1.ConditionTrue, "foo", "")},
expectUpdate: true,
},
{
name: "append false condition",
haveList: []batch.JobCondition{},
wantType: batch.JobSuspended,
wantStatus: v1.ConditionFalse,
wantReason: "foo",
expectList: []batch.JobCondition{},
expectUpdate: false,
},
{
name: "update true condition reason",
haveList: []batch.JobCondition{newCondition(batch.JobSuspended, v1.ConditionTrue, "foo", "")},
wantType: batch.JobSuspended,
wantStatus: v1.ConditionTrue,
wantReason: "bar",
expectList: []batch.JobCondition{newCondition(batch.JobSuspended, v1.ConditionTrue, "bar", "")},
expectUpdate: true,
},
{
name: "update true condition status",
haveList: []batch.JobCondition{newCondition(batch.JobSuspended, v1.ConditionTrue, "foo", "")},
wantType: batch.JobSuspended,
wantStatus: v1.ConditionFalse,
wantReason: "foo",
expectList: []batch.JobCondition{newCondition(batch.JobSuspended, v1.ConditionFalse, "foo", "")},
expectUpdate: true,
},
{
name: "update false condition status",
haveList: []batch.JobCondition{newCondition(batch.JobSuspended, v1.ConditionFalse, "foo", "")},
wantType: batch.JobSuspended,
wantStatus: v1.ConditionTrue,
wantReason: "foo",
expectList: []batch.JobCondition{newCondition(batch.JobSuspended, v1.ConditionTrue, "foo", "")},
expectUpdate: true,
},
{
name: "condition already exists",
haveList: []batch.JobCondition{newCondition(batch.JobSuspended, v1.ConditionTrue, "foo", "")},
wantType: batch.JobSuspended,
wantStatus: v1.ConditionTrue,
wantReason: "foo",
expectList: []batch.JobCondition{newCondition(batch.JobSuspended, v1.ConditionTrue, "foo", "")},
expectUpdate: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotList, isUpdated := ensureJobConditionStatus(tc.haveList, tc.wantType, tc.wantStatus, tc.wantReason, "")
if isUpdated != tc.expectUpdate {
t.Errorf("Got isUpdated=%v, want %v", isUpdated, tc.expectUpdate)
}
if len(gotList) != len(tc.expectList) {
t.Errorf("got a list of length %d, want %d", len(gotList), len(tc.expectList))
}
for i := range gotList {
// Make timestamps the same before comparing the two lists.
gotList[i].LastProbeTime = tc.expectList[i].LastProbeTime
gotList[i].LastTransitionTime = tc.expectList[i].LastTransitionTime
}
if diff := cmp.Diff(tc.expectList, gotList); diff != "" {
t.Errorf("Unexpected JobCondition list: (-want,+got):\n%s", diff)
}
})
}
}
func checkJobCompletionEnvVariable(t *testing.T, spec *v1.PodSpec) {
t.Helper()
want := []v1.EnvVar{

View File

@ -716,6 +716,12 @@ const (
//
// Enables node-local routing for Service internal traffic
ServiceInternalTrafficPolicy featuregate.Feature = "ServiceInternalTrafficPolicy"
// owner: @adtac
// alpha: v1.21
//
// Allows jobs to be created in the suspended state.
SuspendJob featuregate.Feature = "SuspendJob"
)
func init() {
@ -824,6 +830,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
LogarithmicScaleDown: {Default: false, PreRelease: featuregate.Alpha},
IngressClassNamespacedParams: {Default: false, PreRelease: featuregate.Alpha},
ServiceInternalTrafficPolicy: {Default: false, PreRelease: featuregate.Alpha},
SuspendJob: {Default: false, PreRelease: featuregate.Alpha},
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side:

View File

@ -39,6 +39,7 @@ import (
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/apis/batch/validation"
"k8s.io/kubernetes/pkg/features"
"k8s.io/utils/pointer"
)
// jobStrategy implements verification logic for Replication Controllers.
@ -84,6 +85,10 @@ func (jobStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
job.Spec.CompletionMode = batch.NonIndexedCompletion
}
if !utilfeature.DefaultFeatureGate.Enabled(features.SuspendJob) {
job.Spec.Suspend = pointer.BoolPtr(false)
}
pod.DropDisabledTemplateFields(&job.Spec.Template, nil)
}
@ -101,6 +106,16 @@ func (jobStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object
newJob.Spec.CompletionMode = batch.NonIndexedCompletion
}
if !utilfeature.DefaultFeatureGate.Enabled(features.SuspendJob) {
// There are 3 possible values (nil, true, false) for each flag, so 9
// combinations. We want to disallow everything except true->false and
// true->nil when the feature gate is disabled. Or, basically allow this
// only when oldJob is true.
if oldJob.Spec.Suspend == nil || !*oldJob.Spec.Suspend {
newJob.Spec.Suspend = oldJob.Spec.Suspend
}
}
pod.DropDisabledTemplateFields(&newJob.Spec.Template, &oldJob.Spec.Template)
}

View File

@ -31,20 +31,14 @@ import (
_ "k8s.io/kubernetes/pkg/apis/batch/install"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/features"
"k8s.io/utils/pointer"
)
func newBool(a bool) *bool {
return &a
}
func newInt32(i int32) *int32 {
return &i
}
func TestJobStrategy(t *testing.T) {
cases := map[string]struct {
ttlEnabled bool
indexedJobEnabled bool
suspendJobEnabled bool
}{
"features disabled": {},
"ttl enabled": {
@ -53,11 +47,15 @@ func TestJobStrategy(t *testing.T) {
"indexed job enabled": {
indexedJobEnabled: true,
},
"suspend job enabled": {
suspendJobEnabled: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.TTLAfterFinished, tc.ttlEnabled)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IndexedJob, tc.indexedJobEnabled)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SuspendJob, tc.suspendJobEnabled)()
testJobStrategy(t)
})
}
@ -66,6 +64,7 @@ func TestJobStrategy(t *testing.T) {
func testJobStrategy(t *testing.T) {
ttlEnabled := utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished)
indexedJobEnabled := utilfeature.DefaultFeatureGate.Enabled(features.IndexedJob)
suspendJobEnabled := utilfeature.DefaultFeatureGate.Enabled(features.SuspendJob)
ctx := genericapirequest.NewDefaultContext()
if !Strategy.NamespaceScoped() {
t.Errorf("Job must be namespace scoped")
@ -95,10 +94,11 @@ func testJobStrategy(t *testing.T) {
Spec: batch.JobSpec{
Selector: validSelector,
Template: validPodTemplateSpec,
ManualSelector: newBool(true),
Completions: newInt32(2),
ManualSelector: pointer.BoolPtr(true),
Completions: pointer.Int32Ptr(2),
// Set gated values.
TTLSecondsAfterFinished: newInt32(0),
Suspend: pointer.BoolPtr(true),
TTLSecondsAfterFinished: pointer.Int32Ptr(0),
CompletionMode: batch.IndexedCompletion,
},
Status: batch.JobStatus{
@ -120,15 +120,18 @@ func testJobStrategy(t *testing.T) {
if indexedJobEnabled != (job.Spec.CompletionMode != batch.NonIndexedCompletion) {
t.Errorf("Job should allow setting .spec.completionMode=Indexed only when %v feature is enabled", features.IndexedJob)
}
if !suspendJobEnabled && *job.Spec.Suspend {
t.Errorf("[SuspendJob=%v] .spec.suspend should be set to true", suspendJobEnabled)
}
parallelism := int32(10)
updatedJob := &batch.Job{
ObjectMeta: metav1.ObjectMeta{Name: "bar", ResourceVersion: "4"},
Spec: batch.JobSpec{
Parallelism: &parallelism,
Completions: newInt32(2),
Completions: pointer.Int32Ptr(2),
// Update gated features.
TTLSecondsAfterFinished: newInt32(1),
TTLSecondsAfterFinished: pointer.Int32Ptr(1),
CompletionMode: batch.IndexedCompletion, // No change because field is immutable.
},
Status: batch.JobStatus{
@ -151,17 +154,28 @@ func testJobStrategy(t *testing.T) {
}
// Existing gated fields should be preserved
job.Spec.TTLSecondsAfterFinished = newInt32(1)
job.Spec.TTLSecondsAfterFinished = pointer.Int32Ptr(1)
job.Spec.CompletionMode = batch.IndexedCompletion
updatedJob.Spec.TTLSecondsAfterFinished = newInt32(2)
updatedJob.Spec.TTLSecondsAfterFinished = pointer.Int32Ptr(2)
updatedJob.Spec.CompletionMode = batch.IndexedCompletion
// Test updating suspend false->true and nil-> true when the feature gate is
// disabled. We don't care about other combinations.
job.Spec.Suspend, updatedJob.Spec.Suspend = pointer.BoolPtr(false), pointer.BoolPtr(true)
Strategy.PrepareForUpdate(ctx, updatedJob, job)
if job.Spec.TTLSecondsAfterFinished == nil || updatedJob.Spec.TTLSecondsAfterFinished == nil {
t.Errorf("existing TTLSecondsAfterFinished should be preserved")
t.Errorf("existing .spec.ttlSecondsAfterFinished should be preserved")
}
if job.Spec.CompletionMode == "" || updatedJob.Spec.CompletionMode == "" {
t.Errorf("existing completionMode should be preserved")
}
if !suspendJobEnabled && *updatedJob.Spec.Suspend {
t.Errorf("[SuspendJob=%v] .spec.suspend should not be updated from false to true", suspendJobEnabled)
}
job.Spec.Suspend, updatedJob.Spec.Suspend = nil, pointer.BoolPtr(true)
Strategy.PrepareForUpdate(ctx, updatedJob, job)
if !suspendJobEnabled && updatedJob.Spec.Suspend != nil {
t.Errorf("[SuspendJob=%v] .spec.suspend should not be updated from nil to non-nil", suspendJobEnabled)
}
// Make sure we correctly implement the interface.
// Otherwise a typo could silently change the default.

View File

@ -344,89 +344,89 @@ func init() {
}
var fileDescriptor_3b52da57c93de713 = []byte{
// 1301 bytes of a gzipped FileDescriptorProto
// 1307 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x56, 0xcf, 0x8f, 0xdb, 0xc4,
0x17, 0x5f, 0x6f, 0x36, 0x9b, 0x64, 0xb2, 0xbb, 0x4d, 0xa7, 0xdf, 0xb6, 0xf9, 0x86, 0x2a, 0x5e,
0xc2, 0x0f, 0x2d, 0x08, 0x1c, 0xb6, 0xac, 0x10, 0x42, 0x80, 0xb4, 0xde, 0xaa, 0xa2, 0x4b, 0x56,
0x5d, 0x26, 0x5b, 0x21, 0x41, 0x41, 0x4c, 0xec, 0x49, 0xd6, 0x5d, 0xdb, 0x63, 0x79, 0x26, 0x11,
0xb9, 0xf1, 0x0f, 0x20, 0xf1, 0x57, 0x20, 0x4e, 0x5c, 0xb8, 0x73, 0x44, 0x3d, 0xf6, 0xd8, 0x93,
0x45, 0xcd, 0x8d, 0x0b, 0xf7, 0xe5, 0x82, 0x3c, 0x9e, 0xd8, 0x4e, 0x62, 0x2f, 0x6d, 0x0f, 0x15,
0xb7, 0xf8, 0xcd, 0xe7, 0xf3, 0x99, 0x97, 0xf7, 0xde, 0xbc, 0xf7, 0xc0, 0x87, 0x67, 0xef, 0x33,
0xcd, 0xa2, 0xdd, 0xb3, 0xf1, 0x80, 0xf8, 0x2e, 0xe1, 0x84, 0x75, 0x27, 0xc4, 0x35, 0xa9, 0xdf,
0x95, 0x07, 0xd8, 0xb3, 0xba, 0x03, 0xcc, 0x8d, 0xd3, 0xee, 0x64, 0xb7, 0x3b, 0x22, 0x2e, 0xf1,
0x31, 0x27, 0xa6, 0xe6, 0xf9, 0x94, 0x53, 0x78, 0x25, 0x06, 0x69, 0xd8, 0xb3, 0x34, 0x01, 0xd2,
0x26, 0xbb, 0xad, 0xb7, 0x47, 0x16, 0x3f, 0x1d, 0x0f, 0x34, 0x83, 0x3a, 0xdd, 0x11, 0x1d, 0xd1,
0xae, 0xc0, 0x0e, 0xc6, 0x43, 0xf1, 0x25, 0x3e, 0xc4, 0xaf, 0x58, 0xa3, 0xd5, 0xc9, 0x5c, 0x64,
0x50, 0x9f, 0xe4, 0xdc, 0xd3, 0xda, 0x4b, 0x31, 0x0e, 0x36, 0x4e, 0x2d, 0x97, 0xf8, 0xd3, 0xae,
0x77, 0x36, 0x8a, 0x0c, 0xac, 0xeb, 0x10, 0x8e, 0xf3, 0x58, 0xdd, 0x22, 0x96, 0x3f, 0x76, 0xb9,
0xe5, 0x90, 0x25, 0xc2, 0x7b, 0xff, 0x46, 0x60, 0xc6, 0x29, 0x71, 0xf0, 0x22, 0xaf, 0xf3, 0xb7,
0x02, 0x2a, 0x07, 0x3e, 0x75, 0x0f, 0xe9, 0x00, 0x7e, 0x03, 0xaa, 0x91, 0x3f, 0x26, 0xe6, 0xb8,
0xa9, 0x6c, 0x2b, 0x3b, 0xf5, 0x9b, 0xef, 0x68, 0x69, 0x94, 0x12, 0x59, 0xcd, 0x3b, 0x1b, 0x45,
0x06, 0xa6, 0x45, 0x68, 0x6d, 0xb2, 0xab, 0xdd, 0x1d, 0x3c, 0x20, 0x06, 0x3f, 0x22, 0x1c, 0xeb,
0xf0, 0x61, 0xa0, 0xae, 0x84, 0x81, 0x0a, 0x52, 0x1b, 0x4a, 0x54, 0xa1, 0x0e, 0xd6, 0x98, 0x47,
0x8c, 0xe6, 0xaa, 0x50, 0xdf, 0xd6, 0x72, 0x72, 0xa0, 0x49, 0x6f, 0xfa, 0x1e, 0x31, 0xf4, 0x0d,
0xa9, 0xb6, 0x16, 0x7d, 0x21, 0xc1, 0x85, 0x87, 0x60, 0x9d, 0x71, 0xcc, 0xc7, 0xac, 0x59, 0x12,
0x2a, 0x9d, 0x0b, 0x55, 0x04, 0x52, 0xdf, 0x92, 0x3a, 0xeb, 0xf1, 0x37, 0x92, 0x0a, 0x9d, 0x9f,
0x15, 0x50, 0x97, 0xc8, 0x9e, 0xc5, 0x38, 0xbc, 0xbf, 0x14, 0x01, 0xed, 0xe9, 0x22, 0x10, 0xb1,
0xc5, 0xff, 0x6f, 0xc8, 0x9b, 0xaa, 0x33, 0x4b, 0xe6, 0xdf, 0xef, 0x83, 0xb2, 0xc5, 0x89, 0xc3,
0x9a, 0xab, 0xdb, 0xa5, 0x9d, 0xfa, 0xcd, 0x1b, 0x17, 0x39, 0xae, 0x6f, 0x4a, 0xa1, 0xf2, 0x9d,
0x88, 0x82, 0x62, 0x66, 0xe7, 0xa7, 0xb5, 0xc4, 0xe1, 0x28, 0x24, 0xf0, 0x2d, 0x50, 0x8d, 0x12,
0x6b, 0x8e, 0x6d, 0x22, 0x1c, 0xae, 0xa5, 0x0e, 0xf4, 0xa5, 0x1d, 0x25, 0x08, 0x78, 0x0f, 0x5c,
0x67, 0x1c, 0xfb, 0xdc, 0x72, 0x47, 0xb7, 0x08, 0x36, 0x6d, 0xcb, 0x25, 0x7d, 0x62, 0x50, 0xd7,
0x64, 0x22, 0x23, 0x25, 0xfd, 0xa5, 0x30, 0x50, 0xaf, 0xf7, 0xf3, 0x21, 0xa8, 0x88, 0x0b, 0xef,
0x83, 0xcb, 0x06, 0x75, 0x8d, 0xb1, 0xef, 0x13, 0xd7, 0x98, 0x1e, 0x53, 0xdb, 0x32, 0xa6, 0x22,
0x39, 0x35, 0x5d, 0x93, 0xde, 0x5c, 0x3e, 0x58, 0x04, 0x9c, 0xe7, 0x19, 0xd1, 0xb2, 0x10, 0x7c,
0x0d, 0x54, 0xd8, 0x98, 0x79, 0xc4, 0x35, 0x9b, 0x6b, 0xdb, 0xca, 0x4e, 0x55, 0xaf, 0x87, 0x81,
0x5a, 0xe9, 0xc7, 0x26, 0x34, 0x3b, 0x83, 0x5f, 0x82, 0xfa, 0x03, 0x3a, 0x38, 0x21, 0x8e, 0x67,
0x63, 0x4e, 0x9a, 0x65, 0x91, 0xbd, 0x57, 0x73, 0x43, 0x7c, 0x98, 0xe2, 0x44, 0x95, 0x5d, 0x91,
0x4e, 0xd6, 0x33, 0x07, 0x28, 0xab, 0x06, 0xbf, 0x06, 0x2d, 0x36, 0x36, 0x0c, 0xc2, 0xd8, 0x70,
0x6c, 0x1f, 0xd2, 0x01, 0xfb, 0xc4, 0x62, 0x9c, 0xfa, 0xd3, 0x9e, 0xe5, 0x58, 0xbc, 0xb9, 0xbe,
0xad, 0xec, 0x94, 0xf5, 0x76, 0x18, 0xa8, 0xad, 0x7e, 0x21, 0x0a, 0x5d, 0xa0, 0x00, 0x11, 0xb8,
0x36, 0xc4, 0x96, 0x4d, 0xcc, 0x25, 0xed, 0x8a, 0xd0, 0x6e, 0x85, 0x81, 0x7a, 0xed, 0x76, 0x2e,
0x02, 0x15, 0x30, 0x3b, 0xbf, 0xae, 0x82, 0xcd, 0xb9, 0x57, 0x00, 0x3f, 0x05, 0xeb, 0xd8, 0xe0,
0xd6, 0x24, 0x2a, 0x95, 0xa8, 0x00, 0x5f, 0xc9, 0x46, 0x27, 0xea, 0x5f, 0xe9, 0x5b, 0x46, 0x64,
0x48, 0xa2, 0x24, 0x90, 0xf4, 0xe9, 0xec, 0x0b, 0x2a, 0x92, 0x12, 0xd0, 0x06, 0x0d, 0x1b, 0x33,
0x3e, 0xab, 0xb2, 0x13, 0xcb, 0x21, 0x22, 0x3f, 0xf5, 0x9b, 0x6f, 0x3e, 0xdd, 0x93, 0x89, 0x18,
0xfa, 0xff, 0xc2, 0x40, 0x6d, 0xf4, 0x16, 0x74, 0xd0, 0x92, 0x32, 0xf4, 0x01, 0x14, 0xb6, 0x24,
0x84, 0xe2, 0xbe, 0xf2, 0x33, 0xdf, 0x77, 0x2d, 0x0c, 0x54, 0xd8, 0x5b, 0x52, 0x42, 0x39, 0xea,
0x9d, 0xbf, 0x14, 0x50, 0x7a, 0x31, 0x6d, 0xf1, 0xe3, 0xb9, 0xb6, 0x78, 0xa3, 0xa8, 0x68, 0x0b,
0x5b, 0xe2, 0xed, 0x85, 0x96, 0xd8, 0x2e, 0x54, 0xb8, 0xb8, 0x1d, 0xfe, 0x56, 0x02, 0x1b, 0x87,
0x74, 0x70, 0x40, 0x5d, 0xd3, 0xe2, 0x16, 0x75, 0xe1, 0x1e, 0x58, 0xe3, 0x53, 0x6f, 0xd6, 0x5a,
0xb6, 0x67, 0x57, 0x9f, 0x4c, 0x3d, 0x72, 0x1e, 0xa8, 0x8d, 0x2c, 0x36, 0xb2, 0x21, 0x81, 0x86,
0xbd, 0xc4, 0x9d, 0x55, 0xc1, 0xdb, 0x9b, 0xbf, 0xee, 0x3c, 0x50, 0x73, 0x06, 0xa7, 0x96, 0x28,
0xcd, 0x3b, 0x05, 0x47, 0x60, 0x33, 0x4a, 0xce, 0xb1, 0x4f, 0x07, 0x71, 0x95, 0x95, 0x9e, 0x39,
0xeb, 0x57, 0xa5, 0x03, 0x9b, 0xbd, 0xac, 0x10, 0x9a, 0xd7, 0x85, 0x93, 0xb8, 0xc6, 0x4e, 0x7c,
0xec, 0xb2, 0xf8, 0x2f, 0x3d, 0x5f, 0x4d, 0xb7, 0xe4, 0x6d, 0xa2, 0xce, 0xe6, 0xd5, 0x50, 0xce,
0x0d, 0xf0, 0x75, 0xb0, 0xee, 0x13, 0xcc, 0xa8, 0x2b, 0xea, 0xb9, 0x96, 0x66, 0x07, 0x09, 0x2b,
0x92, 0xa7, 0xf0, 0x0d, 0x50, 0x71, 0x08, 0x63, 0x78, 0x44, 0x44, 0xc7, 0xa9, 0xe9, 0x97, 0x24,
0xb0, 0x72, 0x14, 0x9b, 0xd1, 0xec, 0xbc, 0xf3, 0xa3, 0x02, 0x2a, 0x2f, 0x66, 0xa6, 0x7d, 0x34,
0x3f, 0xd3, 0x9a, 0x45, 0x95, 0x57, 0x30, 0xcf, 0xbe, 0x2f, 0x0b, 0x47, 0xc5, 0x2c, 0xdb, 0x05,
0x75, 0x0f, 0xfb, 0xd8, 0xb6, 0x89, 0x6d, 0x31, 0x47, 0xf8, 0x5a, 0xd6, 0x2f, 0x45, 0x7d, 0xf9,
0x38, 0x35, 0xa3, 0x2c, 0x26, 0xa2, 0x18, 0xd4, 0xf1, 0x6c, 0x12, 0x05, 0x33, 0x2e, 0x37, 0x49,
0x39, 0x48, 0xcd, 0x28, 0x8b, 0x81, 0x77, 0xc1, 0xd5, 0xb8, 0x83, 0x2d, 0x4e, 0xc0, 0x92, 0x98,
0x80, 0xff, 0x0f, 0x03, 0xf5, 0xea, 0x7e, 0x1e, 0x00, 0xe5, 0xf3, 0xe0, 0x1e, 0xd8, 0x18, 0x60,
0xe3, 0x8c, 0x0e, 0x87, 0xd9, 0x8e, 0xdd, 0x08, 0x03, 0x75, 0x43, 0xcf, 0xd8, 0xd1, 0x1c, 0x0a,
0x7e, 0x05, 0xaa, 0x8c, 0xd8, 0xc4, 0xe0, 0xd4, 0x97, 0x25, 0xf6, 0xee, 0x53, 0x66, 0x05, 0x0f,
0x88, 0xdd, 0x97, 0x54, 0x7d, 0x43, 0x4c, 0x7a, 0xf9, 0x85, 0x12, 0x49, 0xf8, 0x01, 0xd8, 0x72,
0xb0, 0x3b, 0xc6, 0x09, 0x52, 0xd4, 0x56, 0x55, 0x87, 0x61, 0xa0, 0x6e, 0x1d, 0xcd, 0x9d, 0xa0,
0x05, 0x24, 0xfc, 0x0c, 0x54, 0xf9, 0x6c, 0x8c, 0xae, 0x0b, 0xd7, 0x72, 0x07, 0xc5, 0x31, 0x35,
0xe7, 0xa6, 0x68, 0x52, 0x25, 0xc9, 0x08, 0x4d, 0x64, 0xa2, 0xc5, 0x83, 0x73, 0x5b, 0x46, 0x6c,
0x7f, 0xc8, 0x89, 0x7f, 0xdb, 0x72, 0x2d, 0x76, 0x4a, 0xcc, 0x66, 0x55, 0x84, 0x4b, 0x2c, 0x1e,
0x27, 0x27, 0xbd, 0x3c, 0x08, 0x2a, 0xe2, 0xc2, 0x63, 0xb0, 0x95, 0xa6, 0xf6, 0x88, 0x9a, 0xa4,
0x59, 0x13, 0x0f, 0x63, 0x47, 0xba, 0xb2, 0x75, 0x30, 0x77, 0x7a, 0xbe, 0x64, 0x41, 0x0b, 0xfc,
0xce, 0x9f, 0x25, 0x50, 0x4b, 0x07, 0xe6, 0x3d, 0x00, 0x8c, 0x59, 0x57, 0x62, 0x72, 0x68, 0xbe,
0x5c, 0x54, 0xe1, 0x49, 0xff, 0x4a, 0x9b, 0x7d, 0x62, 0x62, 0x28, 0x23, 0x04, 0x3f, 0x07, 0x35,
0xb1, 0x4a, 0x89, 0xfe, 0xb2, 0xfa, 0xcc, 0xfd, 0x65, 0x33, 0x0c, 0xd4, 0x5a, 0x7f, 0x26, 0x80,
0x52, 0x2d, 0x38, 0xcc, 0xc6, 0xe3, 0x39, 0x7b, 0x25, 0x9c, 0x8f, 0x9b, 0xb8, 0x62, 0x41, 0x35,
0xea, 0x58, 0x72, 0x91, 0x58, 0x13, 0xd9, 0x2b, 0xda, 0x11, 0xba, 0xa0, 0x26, 0x96, 0x1e, 0x62,
0x12, 0x53, 0x14, 0x60, 0x59, 0xbf, 0x2c, 0xa1, 0xb5, 0xfe, 0xec, 0x00, 0xa5, 0x98, 0x48, 0x38,
0xde, 0x66, 0xe4, 0x4e, 0x95, 0x08, 0xc7, 0xbb, 0x0f, 0x92, 0xa7, 0xf0, 0x16, 0x68, 0x48, 0x97,
0x88, 0x79, 0xc7, 0x35, 0xc9, 0xb7, 0x84, 0x89, 0x77, 0x57, 0xd3, 0x9b, 0x92, 0xd1, 0x38, 0x58,
0x38, 0x47, 0x4b, 0x8c, 0xce, 0x2f, 0x0a, 0xb8, 0xb4, 0xb0, 0x0b, 0xfe, 0xf7, 0x87, 0xbd, 0xbe,
0xf3, 0xf0, 0x49, 0x7b, 0xe5, 0xd1, 0x93, 0xf6, 0xca, 0xe3, 0x27, 0xed, 0x95, 0xef, 0xc2, 0xb6,
0xf2, 0x30, 0x6c, 0x2b, 0x8f, 0xc2, 0xb6, 0xf2, 0x38, 0x6c, 0x2b, 0xbf, 0x87, 0x6d, 0xe5, 0x87,
0x3f, 0xda, 0x2b, 0x5f, 0xac, 0x4e, 0x76, 0xff, 0x09, 0x00, 0x00, 0xff, 0xff, 0x27, 0x57, 0xd4,
0x63, 0x21, 0x0f, 0x00, 0x00,
0xb9, 0xf1, 0x27, 0xf0, 0x57, 0x20, 0x4e, 0x5c, 0xe0, 0xcc, 0x11, 0xf5, 0xd8, 0x63, 0x4f, 0x16,
0x35, 0x37, 0x2e, 0xdc, 0x97, 0x0b, 0xf2, 0x78, 0x62, 0x3b, 0x89, 0xbd, 0xb4, 0x3d, 0x54, 0xdc,
0xe2, 0x37, 0x9f, 0xcf, 0x67, 0x5e, 0xde, 0x7b, 0xf3, 0xde, 0x03, 0x1f, 0x9e, 0xbd, 0xcf, 0x34,
0x8b, 0x76, 0xcf, 0xc6, 0x03, 0xe2, 0xbb, 0x84, 0x13, 0xd6, 0x9d, 0x10, 0xd7, 0xa4, 0x7e, 0x57,
0x1e, 0x60, 0xcf, 0xea, 0x0e, 0x30, 0x37, 0x4e, 0xbb, 0x93, 0xdd, 0xee, 0x88, 0xb8, 0xc4, 0xc7,
0x9c, 0x98, 0x9a, 0xe7, 0x53, 0x4e, 0xe1, 0x95, 0x18, 0xa4, 0x61, 0xcf, 0xd2, 0x04, 0x48, 0x9b,
0xec, 0xb6, 0xde, 0x1e, 0x59, 0xfc, 0x74, 0x3c, 0xd0, 0x0c, 0xea, 0x74, 0x47, 0x74, 0x44, 0xbb,
0x02, 0x3b, 0x18, 0x0f, 0xc5, 0x97, 0xf8, 0x10, 0xbf, 0x62, 0x8d, 0x56, 0x27, 0x73, 0x91, 0x41,
0x7d, 0x92, 0x73, 0x4f, 0x6b, 0x2f, 0xc5, 0x38, 0xd8, 0x38, 0xb5, 0x5c, 0xe2, 0x4f, 0xbb, 0xde,
0xd9, 0x28, 0x32, 0xb0, 0xae, 0x43, 0x38, 0xce, 0x63, 0x75, 0x8b, 0x58, 0xfe, 0xd8, 0xe5, 0x96,
0x43, 0x96, 0x08, 0xef, 0xfd, 0x1b, 0x81, 0x19, 0xa7, 0xc4, 0xc1, 0x8b, 0xbc, 0xce, 0xdf, 0x0a,
0xa8, 0x1c, 0xf8, 0xd4, 0x3d, 0xa4, 0x03, 0xf8, 0x0d, 0xa8, 0x46, 0xfe, 0x98, 0x98, 0xe3, 0xa6,
0xb2, 0xad, 0xec, 0xd4, 0x6f, 0xbe, 0xa3, 0xa5, 0x51, 0x4a, 0x64, 0x35, 0xef, 0x6c, 0x14, 0x19,
0x98, 0x16, 0xa1, 0xb5, 0xc9, 0xae, 0x76, 0x77, 0xf0, 0x80, 0x18, 0xfc, 0x88, 0x70, 0xac, 0xc3,
0x87, 0x81, 0xba, 0x12, 0x06, 0x2a, 0x48, 0x6d, 0x28, 0x51, 0x85, 0x3a, 0x58, 0x63, 0x1e, 0x31,
0x9a, 0xab, 0x42, 0x7d, 0x5b, 0xcb, 0xc9, 0x81, 0x26, 0xbd, 0xe9, 0x7b, 0xc4, 0xd0, 0x37, 0xa4,
0xda, 0x5a, 0xf4, 0x85, 0x04, 0x17, 0x1e, 0x82, 0x75, 0xc6, 0x31, 0x1f, 0xb3, 0x66, 0x49, 0xa8,
0x74, 0x2e, 0x54, 0x11, 0x48, 0x7d, 0x4b, 0xea, 0xac, 0xc7, 0xdf, 0x48, 0x2a, 0x74, 0x7e, 0x52,
0x40, 0x5d, 0x22, 0x7b, 0x16, 0xe3, 0xf0, 0xfe, 0x52, 0x04, 0xb4, 0xa7, 0x8b, 0x40, 0xc4, 0x16,
0xff, 0xbf, 0x21, 0x6f, 0xaa, 0xce, 0x2c, 0x99, 0x7f, 0xbf, 0x0f, 0xca, 0x16, 0x27, 0x0e, 0x6b,
0xae, 0x6e, 0x97, 0x76, 0xea, 0x37, 0x6f, 0x5c, 0xe4, 0xb8, 0xbe, 0x29, 0x85, 0xca, 0x77, 0x22,
0x0a, 0x8a, 0x99, 0x9d, 0x1f, 0xd7, 0x12, 0x87, 0xa3, 0x90, 0xc0, 0xb7, 0x40, 0x35, 0x4a, 0xac,
0x39, 0xb6, 0x89, 0x70, 0xb8, 0x96, 0x3a, 0xd0, 0x97, 0x76, 0x94, 0x20, 0xe0, 0x3d, 0x70, 0x9d,
0x71, 0xec, 0x73, 0xcb, 0x1d, 0xdd, 0x22, 0xd8, 0xb4, 0x2d, 0x97, 0xf4, 0x89, 0x41, 0x5d, 0x93,
0x89, 0x8c, 0x94, 0xf4, 0x97, 0xc2, 0x40, 0xbd, 0xde, 0xcf, 0x87, 0xa0, 0x22, 0x2e, 0xbc, 0x0f,
0x2e, 0x1b, 0xd4, 0x35, 0xc6, 0xbe, 0x4f, 0x5c, 0x63, 0x7a, 0x4c, 0x6d, 0xcb, 0x98, 0x8a, 0xe4,
0xd4, 0x74, 0x4d, 0x7a, 0x73, 0xf9, 0x60, 0x11, 0x70, 0x9e, 0x67, 0x44, 0xcb, 0x42, 0xf0, 0x35,
0x50, 0x61, 0x63, 0xe6, 0x11, 0xd7, 0x6c, 0xae, 0x6d, 0x2b, 0x3b, 0x55, 0xbd, 0x1e, 0x06, 0x6a,
0xa5, 0x1f, 0x9b, 0xd0, 0xec, 0x0c, 0x7e, 0x09, 0xea, 0x0f, 0xe8, 0xe0, 0x84, 0x38, 0x9e, 0x8d,
0x39, 0x69, 0x96, 0x45, 0xf6, 0x5e, 0xcd, 0x0d, 0xf1, 0x61, 0x8a, 0x13, 0x55, 0x76, 0x45, 0x3a,
0x59, 0xcf, 0x1c, 0xa0, 0xac, 0x1a, 0xfc, 0x1a, 0xb4, 0xd8, 0xd8, 0x30, 0x08, 0x63, 0xc3, 0xb1,
0x7d, 0x48, 0x07, 0xec, 0x13, 0x8b, 0x71, 0xea, 0x4f, 0x7b, 0x96, 0x63, 0xf1, 0xe6, 0xfa, 0xb6,
0xb2, 0x53, 0xd6, 0xdb, 0x61, 0xa0, 0xb6, 0xfa, 0x85, 0x28, 0x74, 0x81, 0x02, 0x44, 0xe0, 0xda,
0x10, 0x5b, 0x36, 0x31, 0x97, 0xb4, 0x2b, 0x42, 0xbb, 0x15, 0x06, 0xea, 0xb5, 0xdb, 0xb9, 0x08,
0x54, 0xc0, 0xec, 0xfc, 0xba, 0x0a, 0x36, 0xe7, 0x5e, 0x01, 0xfc, 0x14, 0xac, 0x63, 0x83, 0x5b,
0x93, 0xa8, 0x54, 0xa2, 0x02, 0x7c, 0x25, 0x1b, 0x9d, 0xa8, 0x7f, 0xa5, 0x6f, 0x19, 0x91, 0x21,
0x89, 0x92, 0x40, 0xd2, 0xa7, 0xb3, 0x2f, 0xa8, 0x48, 0x4a, 0x40, 0x1b, 0x34, 0x6c, 0xcc, 0xf8,
0xac, 0xca, 0x4e, 0x2c, 0x87, 0x88, 0xfc, 0xd4, 0x6f, 0xbe, 0xf9, 0x74, 0x4f, 0x26, 0x62, 0xe8,
0xff, 0x0b, 0x03, 0xb5, 0xd1, 0x5b, 0xd0, 0x41, 0x4b, 0xca, 0xd0, 0x07, 0x50, 0xd8, 0x92, 0x10,
0x8a, 0xfb, 0xca, 0xcf, 0x7c, 0xdf, 0xb5, 0x30, 0x50, 0x61, 0x6f, 0x49, 0x09, 0xe5, 0xa8, 0x77,
0xfe, 0x52, 0x40, 0xe9, 0xc5, 0xb4, 0xc5, 0x8f, 0xe7, 0xda, 0xe2, 0x8d, 0xa2, 0xa2, 0x2d, 0x6c,
0x89, 0xb7, 0x17, 0x5a, 0x62, 0xbb, 0x50, 0xe1, 0xe2, 0x76, 0xf8, 0x5b, 0x09, 0x6c, 0x1c, 0xd2,
0xc1, 0x01, 0x75, 0x4d, 0x8b, 0x5b, 0xd4, 0x85, 0x7b, 0x60, 0x8d, 0x4f, 0xbd, 0x59, 0x6b, 0xd9,
0x9e, 0x5d, 0x7d, 0x32, 0xf5, 0xc8, 0x79, 0xa0, 0x36, 0xb2, 0xd8, 0xc8, 0x86, 0x04, 0x1a, 0xf6,
0x12, 0x77, 0x56, 0x05, 0x6f, 0x6f, 0xfe, 0xba, 0xf3, 0x40, 0xcd, 0x19, 0x9c, 0x5a, 0xa2, 0x34,
0xef, 0x14, 0x1c, 0x81, 0xcd, 0x28, 0x39, 0xc7, 0x3e, 0x1d, 0xc4, 0x55, 0x56, 0x7a, 0xe6, 0xac,
0x5f, 0x95, 0x0e, 0x6c, 0xf6, 0xb2, 0x42, 0x68, 0x5e, 0x17, 0x4e, 0xe2, 0x1a, 0x3b, 0xf1, 0xb1,
0xcb, 0xe2, 0xbf, 0xf4, 0x7c, 0x35, 0xdd, 0x92, 0xb7, 0x89, 0x3a, 0x9b, 0x57, 0x43, 0x39, 0x37,
0xc0, 0xd7, 0xc1, 0xba, 0x4f, 0x30, 0xa3, 0xae, 0xa8, 0xe7, 0x5a, 0x9a, 0x1d, 0x24, 0xac, 0x48,
0x9e, 0xc2, 0x37, 0x40, 0xc5, 0x21, 0x8c, 0xe1, 0x11, 0x11, 0x1d, 0xa7, 0xa6, 0x5f, 0x92, 0xc0,
0xca, 0x51, 0x6c, 0x46, 0xb3, 0xf3, 0xce, 0x0f, 0x0a, 0xa8, 0xbc, 0x98, 0x99, 0xf6, 0xd1, 0xfc,
0x4c, 0x6b, 0x16, 0x55, 0x5e, 0xc1, 0x3c, 0xfb, 0xa5, 0x2c, 0x1c, 0x15, 0xb3, 0x6c, 0x17, 0xd4,
0x3d, 0xec, 0x63, 0xdb, 0x26, 0xb6, 0xc5, 0x1c, 0xe1, 0x6b, 0x59, 0xbf, 0x14, 0xf5, 0xe5, 0xe3,
0xd4, 0x8c, 0xb2, 0x98, 0x88, 0x62, 0x50, 0xc7, 0xb3, 0x49, 0x14, 0xcc, 0xb8, 0xdc, 0x24, 0xe5,
0x20, 0x35, 0xa3, 0x2c, 0x06, 0xde, 0x05, 0x57, 0xe3, 0x0e, 0xb6, 0x38, 0x01, 0x4b, 0x62, 0x02,
0xfe, 0x3f, 0x0c, 0xd4, 0xab, 0xfb, 0x79, 0x00, 0x94, 0xcf, 0x83, 0x7b, 0x60, 0x63, 0x80, 0x8d,
0x33, 0x3a, 0x1c, 0x66, 0x3b, 0x76, 0x23, 0x0c, 0xd4, 0x0d, 0x3d, 0x63, 0x47, 0x73, 0x28, 0xf8,
0x15, 0xa8, 0x32, 0x62, 0x13, 0x83, 0x53, 0x5f, 0x96, 0xd8, 0xbb, 0x4f, 0x99, 0x15, 0x3c, 0x20,
0x76, 0x5f, 0x52, 0xf5, 0x0d, 0x31, 0xe9, 0xe5, 0x17, 0x4a, 0x24, 0xe1, 0x07, 0x60, 0xcb, 0xc1,
0xee, 0x18, 0x27, 0x48, 0x51, 0x5b, 0x55, 0x1d, 0x86, 0x81, 0xba, 0x75, 0x34, 0x77, 0x82, 0x16,
0x90, 0xf0, 0x33, 0x50, 0xe5, 0xb3, 0x31, 0xba, 0x2e, 0x5c, 0xcb, 0x1d, 0x14, 0xc7, 0xd4, 0x9c,
0x9b, 0xa2, 0x49, 0x95, 0x24, 0x23, 0x34, 0x91, 0x89, 0x16, 0x0f, 0xce, 0x6d, 0x19, 0xb1, 0xfd,
0x21, 0x27, 0xfe, 0x6d, 0xcb, 0xb5, 0xd8, 0x29, 0x31, 0x9b, 0x55, 0x11, 0x2e, 0xb1, 0x78, 0x9c,
0x9c, 0xf4, 0xf2, 0x20, 0xa8, 0x88, 0x0b, 0x8f, 0xc1, 0x56, 0x9a, 0xda, 0x23, 0x6a, 0x92, 0x66,
0x4d, 0x3c, 0x8c, 0x1d, 0xe9, 0xca, 0xd6, 0xc1, 0xdc, 0xe9, 0xf9, 0x92, 0x05, 0x2d, 0xf0, 0xb3,
0xcb, 0x06, 0x28, 0x5e, 0x36, 0x3a, 0x7f, 0x96, 0x40, 0x2d, 0x9d, 0xab, 0xf7, 0x00, 0x30, 0x66,
0xcd, 0x8b, 0xc9, 0xd9, 0xfa, 0x72, 0xd1, 0x43, 0x48, 0xda, 0x5c, 0x3a, 0x13, 0x12, 0x13, 0x43,
0x19, 0x21, 0xf8, 0x39, 0xa8, 0x89, 0x8d, 0x4b, 0xb4, 0xa1, 0xd5, 0x67, 0x6e, 0x43, 0x9b, 0x61,
0xa0, 0xd6, 0xfa, 0x33, 0x01, 0x94, 0x6a, 0xc1, 0x61, 0x36, 0x6c, 0xcf, 0xd9, 0x52, 0xe1, 0x7c,
0x78, 0xc5, 0x15, 0x0b, 0xaa, 0x51, 0x63, 0x93, 0xfb, 0xc6, 0x9a, 0x48, 0x72, 0xd1, 0x2a, 0xd1,
0x05, 0x35, 0xb1, 0x1b, 0x11, 0x93, 0x98, 0xa2, 0x4e, 0xcb, 0xfa, 0x65, 0x09, 0xad, 0xf5, 0x67,
0x07, 0x28, 0xc5, 0x44, 0xc2, 0xf1, 0xd2, 0x23, 0x57, 0xaf, 0x44, 0x38, 0x5e, 0x91, 0x90, 0x3c,
0x85, 0xb7, 0x40, 0x43, 0xba, 0x44, 0xcc, 0x3b, 0xae, 0x49, 0xbe, 0x25, 0x4c, 0x3c, 0xcf, 0x9a,
0xde, 0x94, 0x8c, 0xc6, 0xc1, 0xc2, 0x39, 0x5a, 0x62, 0x74, 0x7e, 0x56, 0xc0, 0xa5, 0x85, 0x95,
0xf1, 0xbf, 0xbf, 0x13, 0xe8, 0x3b, 0x0f, 0x9f, 0xb4, 0x57, 0x1e, 0x3d, 0x69, 0xaf, 0x3c, 0x7e,
0xd2, 0x5e, 0xf9, 0x2e, 0x6c, 0x2b, 0x0f, 0xc3, 0xb6, 0xf2, 0x28, 0x6c, 0x2b, 0x8f, 0xc3, 0xb6,
0xf2, 0x7b, 0xd8, 0x56, 0xbe, 0xff, 0xa3, 0xbd, 0xf2, 0xc5, 0xea, 0x64, 0xf7, 0x9f, 0x00, 0x00,
0x00, 0xff, 0xff, 0x3d, 0x6d, 0x62, 0x04, 0x48, 0x0f, 0x00, 0x00,
}
func (m *CronJob) Marshal() (dAtA []byte, err error) {
@ -841,6 +841,16 @@ func (m *JobSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
if m.Suspend != nil {
i--
if *m.Suspend {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i--
dAtA[i] = 0x50
}
i -= len(m.CompletionMode)
copy(dAtA[i:], m.CompletionMode)
i = encodeVarintGenerated(dAtA, i, uint64(len(m.CompletionMode)))
@ -1202,6 +1212,9 @@ func (m *JobSpec) Size() (n int) {
}
l = len(m.CompletionMode)
n += 1 + l + sovGenerated(uint64(l))
if m.Suspend != nil {
n += 2
}
return n
}
@ -1370,6 +1383,7 @@ func (this *JobSpec) String() string {
`BackoffLimit:` + valueToStringGenerated(this.BackoffLimit) + `,`,
`TTLSecondsAfterFinished:` + valueToStringGenerated(this.TTLSecondsAfterFinished) + `,`,
`CompletionMode:` + fmt.Sprintf("%v", this.CompletionMode) + `,`,
`Suspend:` + valueToStringGenerated(this.Suspend) + `,`,
`}`,
}, "")
return s
@ -2825,6 +2839,27 @@ func (m *JobSpec) Unmarshal(dAtA []byte) error {
}
m.CompletionMode = CompletionMode(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 10:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Suspend", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
b := bool(v != 0)
m.Suspend = &b
default:
iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:])

View File

@ -184,8 +184,11 @@ message JobSpec {
// +optional
optional int32 completions = 2;
// Specifies the duration in seconds relative to the startTime that the job may be active
// before the system tries to terminate it; value must be positive integer
// Specifies the duration in seconds relative to the startTime that the job
// may be continuously active before the system tries to terminate it; value
// must be positive integer. If a Job is suspended (at creation or through an
// update), this timer will effectively be stopped and reset when the Job is
// resumed again.
// +optional
optional int64 activeDeadlineSeconds = 3;
@ -250,12 +253,28 @@ message JobSpec {
// controller skips updates for the Job.
// +optional
optional string completionMode = 9;
// Suspend specifies whether the Job controller should create Pods or not. If
// a Job is created with suspend set to true, no Pods are created by the Job
// controller. If a Job is suspended after creation (i.e. the flag goes from
// false to true), the Job controller will delete all active Pods associated
// with this Job. Users must design their workload to gracefully handle this.
// Suspending a Job will reset the StartTime field of the Job, effectively
// resetting the ActiveDeadlineSeconds timer too. This is an alpha field and
// requires the SuspendJob feature gate to be enabled; otherwise this field
// may not be set to true. Defaults to false.
// +optional
optional bool suspend = 10;
}
// JobStatus represents the current state of a Job.
message JobStatus {
// The latest available observations of an object's current state.
// When a job fails, one of the conditions will have type == "Failed".
// The latest available observations of an object's current state. When a Job
// fails, one of the conditions will have type "Failed" and status true. When
// a Job is suspended, one of the conditions will have type "Suspended" and
// status true; when the Job is resumed, the status of this condition will
// become false. When a Job is completed, one of the conditions will have
// type "Complete" and status true.
// More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/
// +optional
// +patchMergeKey=type
@ -263,9 +282,10 @@ message JobStatus {
// +listType=atomic
repeated JobCondition conditions = 1;
// Represents time when the job was acknowledged by the job controller.
// It is not guaranteed to be set in happens-before order across separate operations.
// It is represented in RFC3339 form and is in UTC.
// Represents time when the job controller started processing a job. When a
// Job is created in the suspended state, this field is not set until the
// first time it is resumed. This field is reset every time a Job is resumed
// from suspension. It is represented in RFC3339 form and is in UTC.
// +optional
optional k8s.io.apimachinery.pkg.apis.meta.v1.Time startTime = 2;

View File

@ -95,8 +95,11 @@ type JobSpec struct {
// +optional
Completions *int32 `json:"completions,omitempty" protobuf:"varint,2,opt,name=completions"`
// Specifies the duration in seconds relative to the startTime that the job may be active
// before the system tries to terminate it; value must be positive integer
// Specifies the duration in seconds relative to the startTime that the job
// may be continuously active before the system tries to terminate it; value
// must be positive integer. If a Job is suspended (at creation or through an
// update), this timer will effectively be stopped and reset when the Job is
// resumed again.
// +optional
ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty" protobuf:"varint,3,opt,name=activeDeadlineSeconds"`
@ -166,12 +169,28 @@ type JobSpec struct {
// controller skips updates for the Job.
// +optional
CompletionMode CompletionMode `json:"completionMode,omitempty" protobuf:"bytes,9,opt,name=completionMode,casttype=CompletionMode"`
// Suspend specifies whether the Job controller should create Pods or not. If
// a Job is created with suspend set to true, no Pods are created by the Job
// controller. If a Job is suspended after creation (i.e. the flag goes from
// false to true), the Job controller will delete all active Pods associated
// with this Job. Users must design their workload to gracefully handle this.
// Suspending a Job will reset the StartTime field of the Job, effectively
// resetting the ActiveDeadlineSeconds timer too. This is an alpha field and
// requires the SuspendJob feature gate to be enabled; otherwise this field
// may not be set to true. Defaults to false.
// +optional
Suspend *bool `json:"suspend,omitempty" protobuf:"varint,10,opt,name=suspend"`
}
// JobStatus represents the current state of a Job.
type JobStatus struct {
// The latest available observations of an object's current state.
// When a job fails, one of the conditions will have type == "Failed".
// The latest available observations of an object's current state. When a Job
// fails, one of the conditions will have type "Failed" and status true. When
// a Job is suspended, one of the conditions will have type "Suspended" and
// status true; when the Job is resumed, the status of this condition will
// become false. When a Job is completed, one of the conditions will have
// type "Complete" and status true.
// More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/
// +optional
// +patchMergeKey=type
@ -179,9 +198,10 @@ type JobStatus struct {
// +listType=atomic
Conditions []JobCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
// Represents time when the job was acknowledged by the job controller.
// It is not guaranteed to be set in happens-before order across separate operations.
// It is represented in RFC3339 form and is in UTC.
// Represents time when the job controller started processing a job. When a
// Job is created in the suspended state, this field is not set until the
// first time it is resumed. This field is reset every time a Job is resumed
// from suspension. It is represented in RFC3339 form and is in UTC.
// +optional
StartTime *metav1.Time `json:"startTime,omitempty" protobuf:"bytes,2,opt,name=startTime"`
@ -219,6 +239,8 @@ type JobConditionType string
// These are valid conditions of a job.
const (
// JobSuspended means the job has been suspended.
JobSuspended JobConditionType = "Suspended"
// JobComplete means the job has completed its execution.
JobComplete JobConditionType = "Complete"
// JobFailed means the job has failed its execution.

View File

@ -113,13 +113,14 @@ var map_JobSpec = map[string]string{
"": "JobSpec describes how the job execution will look like.",
"parallelism": "Specifies the maximum desired number of pods the job should run at any given time. The actual number of pods running in steady state will be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), i.e. when the work left to do is less than max parallelism. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
"completions": "Specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
"activeDeadlineSeconds": "Specifies the duration in seconds relative to the startTime that the job may be active before the system tries to terminate it; value must be positive integer",
"activeDeadlineSeconds": "Specifies the duration in seconds relative to the startTime that the job may be continuously active before the system tries to terminate it; value must be positive integer. If a Job is suspended (at creation or through an update), this timer will effectively be stopped and reset when the Job is resumed again.",
"backoffLimit": "Specifies the number of retries before marking this job failed. Defaults to 6",
"selector": "A label query over pods that should match the pod count. Normally, the system sets this field for you. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors",
"manualSelector": "manualSelector controls generation of pod labels and pod selectors. Leave `manualSelector` unset unless you are certain what you are doing. When false or unset, the system pick labels unique to this job and appends those labels to the pod template. When true, the user is responsible for picking unique labels and specifying the selector. Failure to pick a unique label may cause this and other jobs to not function correctly. However, You may see `manualSelector=true` in jobs that were created with the old `extensions/v1beta1` API. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#specifying-your-own-pod-selector",
"template": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
"ttlSecondsAfterFinished": "ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed). If this field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. When the Job is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Job won't be automatically deleted. If this field is set to zero, the Job becomes eligible to be deleted immediately after it finishes. This field is alpha-level and is only honored by servers that enable the TTLAfterFinished feature.",
"completionMode": "CompletionMode specifies how Pod completions are tracked. It can be `NonIndexed` (default) or `Indexed`.\n\n`NonIndexed` means that the Job is considered complete when there have been .spec.completions successfully completed Pods. Each Pod completion is homologous to each other.\n\n`Indexed` means that the Pods of a Job get an associated completion index from 0 to (.spec.completions - 1), available in the annotation batch.alpha.kubernetes.io/job-completion-index. The Job is considered complete when there is one successfully completed Pod for each index. When value is `Indexed`, .spec.completions must be specified and `.spec.parallelism` must be less than or equal to 10^5.\n\nThis field is alpha-level and is only honored by servers that enable the IndexedJob feature gate. More completion modes can be added in the future. If the Job controller observes a mode that it doesn't recognize, the controller skips updates for the Job.",
"suspend": "Suspend specifies whether the Job controller should create Pods or not. If a Job is created with suspend set to true, no Pods are created by the Job controller. If a Job is suspended after creation (i.e. the flag goes from false to true), the Job controller will delete all active Pods associated with this Job. Users must design their workload to gracefully handle this. Suspending a Job will reset the StartTime field of the Job, effectively resetting the ActiveDeadlineSeconds timer too. This is an alpha field and requires the SuspendJob feature gate to be enabled; otherwise this field may not be set to true. Defaults to false.",
}
func (JobSpec) SwaggerDoc() map[string]string {
@ -128,8 +129,8 @@ func (JobSpec) SwaggerDoc() map[string]string {
var map_JobStatus = map[string]string{
"": "JobStatus represents the current state of a Job.",
"conditions": "The latest available observations of an object's current state. When a job fails, one of the conditions will have type == \"Failed\". More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
"startTime": "Represents time when the job was acknowledged by the job controller. It is not guaranteed to be set in happens-before order across separate operations. It is represented in RFC3339 form and is in UTC.",
"conditions": "The latest available observations of an object's current state. When a Job fails, one of the conditions will have type \"Failed\" and status true. When a Job is suspended, one of the conditions will have type \"Suspended\" and status true; when the Job is resumed, the status of this condition will become false. When a Job is completed, one of the conditions will have type \"Complete\" and status true. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
"startTime": "Represents time when the job controller started processing a job. When a Job is created in the suspended state, this field is not set until the first time it is resumed. This field is reset every time a Job is resumed from suspension. It is represented in RFC3339 form and is in UTC.",
"completionTime": "Represents time when the job was completed. It is not guaranteed to be set in happens-before order across separate operations. It is represented in RFC3339 form and is in UTC. The completion time is only set when the job finishes successfully.",
"active": "The number of actively running pods.",
"succeeded": "The number of pods which reached phase Succeeded.",

View File

@ -271,6 +271,11 @@ func (in *JobSpec) DeepCopyInto(out *JobSpec) {
*out = new(int32)
**out = **in
}
if in.Suspend != nil {
in, out := &in.Suspend, &out.Suspend
*out = new(bool)
**out = **in
}
return
}

View File

@ -1569,11 +1569,12 @@
"setHostnameAsFQDN": false
}
},
"ttlSecondsAfterFinished": -1285029915
"ttlSecondsAfterFinished": -1285029915,
"suspend": true
}
},
"successfulJobsHistoryLimit": -1887637570,
"failedJobsHistoryLimit": 1755548633
"successfulJobsHistoryLimit": 1729066291,
"failedJobsHistoryLimit": -908823020
},
"status": {
"active": [
@ -1581,7 +1582,7 @@
"kind": "503",
"namespace": "504",
"name": "505",
"uid": "`ɜɅco\\穜T睭憲Ħ焵i,ŋŨN",
"uid": "`",
"apiVersion": "506",
"resourceVersion": "507",
"fieldPath": "508"

View File

@ -31,7 +31,7 @@ metadata:
uid: "7"
spec:
concurrencyPolicy: Hr鯹)晿<o,c鮽ort昍řČ扷5Ɨ
failedJobsHistoryLimit: 1755548633
failedJobsHistoryLimit: -908823020
jobTemplate:
metadata:
annotations:
@ -74,6 +74,7 @@ spec:
operator: DoesNotExist
matchLabels:
2_kS91.e5K-_e63_-_3-n-_-__3u-.__P__.7U-Uo_4_-D7r__.am6-4_WE-_T: cd-2.-__E_Sv__26KX_R_.-.Nth._--S_4DAm
suspend: true
template:
metadata:
annotations:
@ -1073,7 +1074,7 @@ spec:
ttlSecondsAfterFinished: -1285029915
schedule: "19"
startingDeadlineSeconds: -2555947251840004808
successfulJobsHistoryLimit: -1887637570
successfulJobsHistoryLimit: 1729066291
suspend: true
status:
active:
@ -1083,4 +1084,4 @@ status:
name: "505"
namespace: "504"
resourceVersion: "507"
uid: 犓`ɜɅco\穜T睭憲Ħ焵i,ŋŨN
uid: '`'

View File

@ -1528,22 +1528,23 @@
}
},
"ttlSecondsAfterFinished": -1812920817,
"completionMode": "ʅ朁遐»"
"completionMode": "ʅ朁遐»",
"suspend": false
},
"status": {
"conditions": [
{
"type": "ƥf豯烠砖#囹J,R譏K譕ơ",
"status": "噓涫祲ŗȨ",
"lastProbeTime": "2108-10-11T06:42:59Z",
"lastTransitionTime": "2845-10-01T19:47:44Z",
"type": "ƥf豯烠砖#囹J",
"status": "ʝ3",
"lastProbeTime": "2358-11-16T05:24:38Z",
"lastTransitionTime": "2305-06-19T03:46:44Z",
"reason": "488",
"message": "489"
}
],
"active": -1576445541,
"succeeded": 416561398,
"failed": -291702642,
"active": -1842630179,
"succeeded": 1984090670,
"failed": -1949686005,
"completedIndexes": "490"
}
}

View File

@ -44,6 +44,7 @@ spec:
- 3_bQw.-dG6c-.x
matchLabels:
hjT9s-j41-0-6p-JFHn7y-74.-0MUORQQ.N4: 3L.u
suspend: false
template:
metadata:
annotations:
@ -1040,14 +1041,14 @@ spec:
volumePath: "101"
ttlSecondsAfterFinished: -1812920817
status:
active: -1576445541
active: -1842630179
completedIndexes: "490"
conditions:
- lastProbeTime: "2108-10-11T06:42:59Z"
lastTransitionTime: "2845-10-01T19:47:44Z"
- lastProbeTime: "2358-11-16T05:24:38Z"
lastTransitionTime: "2305-06-19T03:46:44Z"
message: "489"
reason: "488"
status: 噓涫祲ŗȨ
type: ƥf豯烠砖#囹J,R譏K譕ơ
failed: -291702642
succeeded: 416561398
status: ʝ3
type: ƥf豯烠砖#囹J
failed: -1949686005
succeeded: 1984090670

View File

@ -1569,11 +1569,12 @@
"setHostnameAsFQDN": false
}
},
"ttlSecondsAfterFinished": -1285029915
"ttlSecondsAfterFinished": -1285029915,
"suspend": true
}
},
"successfulJobsHistoryLimit": -1887637570,
"failedJobsHistoryLimit": 1755548633
"successfulJobsHistoryLimit": 1729066291,
"failedJobsHistoryLimit": -908823020
},
"status": {
"active": [
@ -1581,7 +1582,7 @@
"kind": "503",
"namespace": "504",
"name": "505",
"uid": "`ɜɅco\\穜T睭憲Ħ焵i,ŋŨN",
"uid": "`",
"apiVersion": "506",
"resourceVersion": "507",
"fieldPath": "508"

View File

@ -31,7 +31,7 @@ metadata:
uid: "7"
spec:
concurrencyPolicy: Hr鯹)晿<o,c鮽ort昍řČ扷5Ɨ
failedJobsHistoryLimit: 1755548633
failedJobsHistoryLimit: -908823020
jobTemplate:
metadata:
annotations:
@ -74,6 +74,7 @@ spec:
operator: DoesNotExist
matchLabels:
2_kS91.e5K-_e63_-_3-n-_-__3u-.__P__.7U-Uo_4_-D7r__.am6-4_WE-_T: cd-2.-__E_Sv__26KX_R_.-.Nth._--S_4DAm
suspend: true
template:
metadata:
annotations:
@ -1073,7 +1074,7 @@ spec:
ttlSecondsAfterFinished: -1285029915
schedule: "19"
startingDeadlineSeconds: -2555947251840004808
successfulJobsHistoryLimit: -1887637570
successfulJobsHistoryLimit: 1729066291
suspend: true
status:
active:
@ -1083,4 +1084,4 @@ status:
name: "505"
namespace: "504"
resourceVersion: "507"
uid: 犓`ɜɅco\穜T睭憲Ħ焵i,ŋŨN
uid: '`'

View File

@ -1578,7 +1578,8 @@
}
},
"ttlSecondsAfterFinished": 1315299341,
"completionMode": "ſZɐYɋsx羳ıȦj"
"completionMode": "ſZɐYɋsx羳ıȦj",
"suspend": false
}
}
}

View File

@ -74,6 +74,7 @@ template:
- Ou1.m_.5AW-_S-.3g.7_2fNc5-_.-RX8
matchLabels:
g5i9/l-Y._.-444: c2_kS91.e5K-_e63_-_3-n-_-__3u-.__P__.7U-Uo_4_-D7r__.am64
suspend: false
template:
metadata:
annotations:

View File

@ -36,6 +36,7 @@ type JobSpecApplyConfiguration struct {
Template *corev1.PodTemplateSpecApplyConfiguration `json:"template,omitempty"`
TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty"`
CompletionMode *batchv1.CompletionMode `json:"completionMode,omitempty"`
Suspend *bool `json:"suspend,omitempty"`
}
// JobSpecApplyConfiguration constructs an declarative configuration of the JobSpec type for use with
@ -115,3 +116,11 @@ func (b *JobSpecApplyConfiguration) WithCompletionMode(value batchv1.CompletionM
b.CompletionMode = &value
return b
}
// WithSuspend sets the Suspend field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Suspend field is set to the value of the last call.
func (b *JobSpecApplyConfiguration) WithSuspend(value bool) *JobSpecApplyConfiguration {
b.Suspend = &value
return b
}

View File

@ -35,6 +35,7 @@ import (
e2enode "k8s.io/kubernetes/test/e2e/framework/node"
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
e2eresource "k8s.io/kubernetes/test/e2e/framework/resource"
"k8s.io/utils/pointer"
"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
@ -69,6 +70,85 @@ var _ = SIGDescribe("Job", func() {
framework.ExpectEqual(successes, completions, "epected %d successful job pods, but got %d", completions, successes)
})
// Requires the alpha level feature gate SuspendJob. This e2e test will not
// pass without the following flag being passed to kubetest:
// --test_args="--feature-gates=SuspendJob=true"
ginkgo.It("[Feature:SuspendJob] should not create pods when created in suspend state", func() {
ginkgo.By("Creating a job with suspend=true")
job := e2ejob.NewTestJob("succeed", "suspend-true-to-false", v1.RestartPolicyNever, parallelism, completions, nil, backoffLimit)
job.Spec.Suspend = pointer.BoolPtr(true)
job, err := e2ejob.CreateJob(f.ClientSet, f.Namespace.Name, job)
framework.ExpectNoError(err, "failed to create job in namespace: %s", f.Namespace.Name)
ginkgo.By("Ensuring pods aren't created for job")
framework.ExpectEqual(wait.Poll(framework.Poll, wait.ForeverTestTimeout, func() (bool, error) {
pods, err := e2ejob.GetJobPods(f.ClientSet, f.Namespace.Name, job.Name)
if err != nil {
return false, err
}
return len(pods.Items) > 0, nil
}), wait.ErrWaitTimeout)
ginkgo.By("Checking Job status to observe Suspended state")
job, err = e2ejob.GetJob(f.ClientSet, f.Namespace.Name, job.Name)
framework.ExpectNoError(err, "failed to retrieve latest job object")
exists := false
for _, c := range job.Status.Conditions {
if c.Type == batchv1.JobSuspended {
exists = true
break
}
}
framework.ExpectEqual(exists, true)
ginkgo.By("Updating the job with suspend=false")
job.Spec.Suspend = pointer.BoolPtr(false)
job, err = e2ejob.UpdateJob(f.ClientSet, f.Namespace.Name, job)
framework.ExpectNoError(err, "failed to update job in namespace: %s", f.Namespace.Name)
ginkgo.By("Waiting for job to complete")
err = e2ejob.WaitForJobComplete(f.ClientSet, f.Namespace.Name, job.Name, completions)
framework.ExpectNoError(err, "failed to ensure job completion in namespace: %s", f.Namespace.Name)
})
// Requires the alpha level feature gate SuspendJob. This e2e test will not
// pass without the following flag being passed to kubetest:
// --test_args="--feature-gates=SuspendJob=true"
ginkgo.It("[Feature:SuspendJob] should delete pods when suspended", func() {
ginkgo.By("Creating a job with suspend=false")
job := e2ejob.NewTestJob("notTerminate", "suspend-false-to-true", v1.RestartPolicyNever, parallelism, completions, nil, backoffLimit)
job.Spec.Suspend = pointer.BoolPtr(false)
job, err := e2ejob.CreateJob(f.ClientSet, f.Namespace.Name, job)
framework.ExpectNoError(err, "failed to create job in namespace: %s", f.Namespace.Name)
ginkgo.By("Ensure pods equal to paralellism count is attached to the job")
err = e2ejob.WaitForAllJobPodsRunning(f.ClientSet, f.Namespace.Name, job.Name, parallelism)
framework.ExpectNoError(err, "failed to ensure number of pods associated with job %s is equal to parallelism count in namespace: %s", job.Name, f.Namespace.Name)
ginkgo.By("Updating the job with suspend=true")
job, err = e2ejob.GetJob(f.ClientSet, f.Namespace.Name, job.Name)
framework.ExpectNoError(err, "failed to retrieve latest job object")
job.Spec.Suspend = pointer.BoolPtr(true)
job, err = e2ejob.UpdateJob(f.ClientSet, f.Namespace.Name, job)
framework.ExpectNoError(err, "failed to update job in namespace: %s", f.Namespace.Name)
ginkgo.By("Ensuring pods are deleted")
err = e2ejob.WaitForAllJobPodsGone(f.ClientSet, f.Namespace.Name, job.Name)
framework.ExpectNoError(err, "failed to ensure pods are deleted after suspend=true")
ginkgo.By("Checking Job status to observe Suspended state")
job, err = e2ejob.GetJob(f.ClientSet, f.Namespace.Name, job.Name)
framework.ExpectNoError(err, "failed to retrieve latest job object")
exists := false
for _, c := range job.Status.Conditions {
if c.Type == batchv1.JobSuspended {
exists = true
break
}
}
framework.ExpectEqual(exists, true)
})
/*
Testcase: Ensure Pods of an Indexed Job get a unique index.
Description: Create an Indexed Job, wait for completion, capture the output of the pods and verify that they contain the completion index.

View File

@ -42,3 +42,9 @@ func GetJobPods(c clientset.Interface, ns, jobName string) (*v1.PodList, error)
func CreateJob(c clientset.Interface, ns string, job *batchv1.Job) (*batchv1.Job, error) {
return c.BatchV1().Jobs(ns).Create(context.TODO(), job, metav1.CreateOptions{})
}
// CreateJob uses c to update a job in namespace ns. If the returned error is
// nil, the returned Job is valid and has been updated.
func UpdateJob(c clientset.Interface, ns string, job *batchv1.Job) (*batchv1.Job, error) {
return c.BatchV1().Jobs(ns).Update(context.TODO(), job, metav1.UpdateOptions{})
}