diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index fd107f2ea009..a7b3901e5cae 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -4247,6 +4247,10 @@ "format": "int32", "type": "integer" }, + "completionMode": { + "description": "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.", + "type": "string" + }, "completions": { "description": "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/", "format": "int32", @@ -4288,6 +4292,10 @@ "format": "int32", "type": "integer" }, + "completedIndexes": { + "description": "CompletedIndexes holds the completed indexes when .spec.completionMode = \"Indexed\" in a text format. The indexes are represented as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the completed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\".", + "type": "string" + }, "completionTime": { "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Time", "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." diff --git a/pkg/apis/batch/fuzzer/fuzzer.go b/pkg/apis/batch/fuzzer/fuzzer.go index b6f82a2b1e30..a9c947d92cd4 100644 --- a/pkg/apis/batch/fuzzer/fuzzer.go +++ b/pkg/apis/batch/fuzzer/fuzzer.go @@ -18,7 +18,6 @@ package fuzzer import ( fuzz "github.com/google/gofuzz" - runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/kubernetes/pkg/apis/batch" ) @@ -53,6 +52,11 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { } else { j.ManualSelector = nil } + if c.Rand.Int31()%2 == 0 { + j.CompletionMode = batch.NonIndexedCompletion + } else { + j.CompletionMode = batch.IndexedCompletion + } }, func(sj *batch.CronJobSpec, c fuzz.Continue) { c.FuzzNoCustom(sj) diff --git a/pkg/apis/batch/types.go b/pkg/apis/batch/types.go index 396f6429a94c..570ebfd97f1c 100644 --- a/pkg/apis/batch/types.go +++ b/pkg/apis/batch/types.go @@ -85,6 +85,22 @@ type JobTemplateSpec struct { Spec JobSpec } +// CompletionMode specifies how Pod completions of a Job are tracked. +type CompletionMode string + +const ( + // NonIndexedCompletion is a Job completion mode. In this mode, the Job is + // considered complete when there have been .spec.completions + // successfully completed Pods. Pod completions are homologous to each other. + NonIndexedCompletion CompletionMode = "NonIndexed" + + // IndexedCompletion is a Job completion mode. In this mode, the Pods of a + // Job get an associated completion index from 0 to (.spec.completions - 1). + // The Job is considered complete when a Pod completes for each completion + // index. + IndexedCompletion CompletionMode = "Indexed" +) + // JobSpec describes how the job execution will look like. type JobSpec struct { @@ -149,6 +165,28 @@ type JobSpec struct { // TTLAfterFinished feature. // +optional TTLSecondsAfterFinished *int32 + + // CompletionMode specifies how Pod completions are tracked. It can be + // `NonIndexed` (default) or `Indexed`. + // + // `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. + // + // `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. + // + // This 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. + // +optional + CompletionMode CompletionMode } // JobStatus represents the current state of a Job. @@ -183,6 +221,16 @@ type JobStatus struct { // The number of pods which reached phase Failed. // +optional Failed int32 + + // CompletedIndexes holds the completed indexes when .spec.completionMode = + // "Indexed" in a text format. The indexes are represented as decimal integers + // separated by commas. The numbers are listed in increasing order. Three or + // more consecutive numbers are compressed and represented by the first and + // last element of the series, separated by a hyphen. + // For example, if the completed indexes are 1, 3, 4, 5 and 7, they are + // represented as "1,3-5,7". + // +optional + CompletedIndexes string } // JobConditionType is a valid value for JobCondition.Type diff --git a/pkg/apis/batch/v1/defaults.go b/pkg/apis/batch/v1/defaults.go index 72e7e2a9d5bb..f25ef7d6f17e 100644 --- a/pkg/apis/batch/v1/defaults.go +++ b/pkg/apis/batch/v1/defaults.go @@ -46,4 +46,7 @@ func SetDefaults_Job(obj *batchv1.Job) { if labels != nil && len(obj.Labels) == 0 { obj.Labels = labels } + if len(obj.Spec.CompletionMode) == 0 { + obj.Spec.CompletionMode = batchv1.NonIndexedCompletion + } } diff --git a/pkg/apis/batch/v1/defaults_test.go b/pkg/apis/batch/v1/defaults_test.go index 0f9c8c5c1797..e7c7adead3c0 100644 --- a/pkg/apis/batch/v1/defaults_test.go +++ b/pkg/apis/batch/v1/defaults_test.go @@ -21,15 +21,15 @@ import ( "testing" batchv1 "k8s.io/api/batch/v1" - "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/api/legacyscheme" _ "k8s.io/kubernetes/pkg/apis/batch/install" - . "k8s.io/kubernetes/pkg/apis/batch/v1" _ "k8s.io/kubernetes/pkg/apis/core/install" utilpointer "k8s.io/utils/pointer" + + . "k8s.io/kubernetes/pkg/apis/batch/v1" ) func TestSetDefaultJob(t *testing.T) { @@ -49,9 +49,10 @@ func TestSetDefaultJob(t *testing.T) { }, expected: &batchv1.Job{ Spec: batchv1.JobSpec{ - Completions: utilpointer.Int32Ptr(1), - Parallelism: utilpointer.Int32Ptr(1), - BackoffLimit: utilpointer.Int32Ptr(6), + Completions: utilpointer.Int32Ptr(1), + Parallelism: utilpointer.Int32Ptr(1), + BackoffLimit: utilpointer.Int32Ptr(6), + CompletionMode: batchv1.NonIndexedCompletion, }, }, expectLabels: true, @@ -69,9 +70,10 @@ func TestSetDefaultJob(t *testing.T) { }, expected: &batchv1.Job{ Spec: batchv1.JobSpec{ - Completions: utilpointer.Int32Ptr(1), - Parallelism: utilpointer.Int32Ptr(1), - BackoffLimit: utilpointer.Int32Ptr(6), + Completions: utilpointer.Int32Ptr(1), + Parallelism: utilpointer.Int32Ptr(1), + BackoffLimit: utilpointer.Int32Ptr(6), + CompletionMode: batchv1.NonIndexedCompletion, }, }, }, @@ -86,8 +88,9 @@ func TestSetDefaultJob(t *testing.T) { }, expected: &batchv1.Job{ Spec: batchv1.JobSpec{ - Parallelism: utilpointer.Int32Ptr(0), - BackoffLimit: utilpointer.Int32Ptr(6), + Parallelism: utilpointer.Int32Ptr(0), + BackoffLimit: utilpointer.Int32Ptr(6), + CompletionMode: batchv1.NonIndexedCompletion, }, }, expectLabels: true, @@ -103,8 +106,9 @@ func TestSetDefaultJob(t *testing.T) { }, expected: &batchv1.Job{ Spec: batchv1.JobSpec{ - Parallelism: utilpointer.Int32Ptr(2), - BackoffLimit: utilpointer.Int32Ptr(6), + Parallelism: utilpointer.Int32Ptr(2), + BackoffLimit: utilpointer.Int32Ptr(6), + CompletionMode: batchv1.NonIndexedCompletion, }, }, expectLabels: true, @@ -120,9 +124,10 @@ func TestSetDefaultJob(t *testing.T) { }, expected: &batchv1.Job{ Spec: batchv1.JobSpec{ - Completions: utilpointer.Int32Ptr(2), - Parallelism: utilpointer.Int32Ptr(1), - BackoffLimit: utilpointer.Int32Ptr(6), + Completions: utilpointer.Int32Ptr(2), + Parallelism: utilpointer.Int32Ptr(1), + BackoffLimit: utilpointer.Int32Ptr(6), + CompletionMode: batchv1.NonIndexedCompletion, }, }, expectLabels: true, @@ -138,9 +143,10 @@ func TestSetDefaultJob(t *testing.T) { }, expected: &batchv1.Job{ Spec: batchv1.JobSpec{ - Completions: utilpointer.Int32Ptr(1), - Parallelism: utilpointer.Int32Ptr(1), - BackoffLimit: utilpointer.Int32Ptr(5), + Completions: utilpointer.Int32Ptr(1), + Parallelism: utilpointer.Int32Ptr(1), + BackoffLimit: utilpointer.Int32Ptr(5), + CompletionMode: batchv1.NonIndexedCompletion, }, }, expectLabels: true, @@ -148,9 +154,10 @@ 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: utilpointer.Int32Ptr(8), + Parallelism: utilpointer.Int32Ptr(9), + BackoffLimit: utilpointer.Int32Ptr(10), + CompletionMode: batchv1.NonIndexedCompletion, Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: defaultLabels}, }, @@ -158,9 +165,10 @@ func TestSetDefaultJob(t *testing.T) { }, expected: &batchv1.Job{ Spec: batchv1.JobSpec{ - Completions: utilpointer.Int32Ptr(8), - Parallelism: utilpointer.Int32Ptr(9), - BackoffLimit: utilpointer.Int32Ptr(10), + Completions: utilpointer.Int32Ptr(8), + Parallelism: utilpointer.Int32Ptr(9), + BackoffLimit: utilpointer.Int32Ptr(10), + CompletionMode: batchv1.NonIndexedCompletion, Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: defaultLabels}, }, @@ -171,9 +179,10 @@ 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: utilpointer.Int32Ptr(11), + Parallelism: utilpointer.Int32Ptr(10), + BackoffLimit: utilpointer.Int32Ptr(9), + CompletionMode: batchv1.IndexedCompletion, Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: defaultLabels}, }, @@ -181,9 +190,10 @@ func TestSetDefaultJob(t *testing.T) { }, expected: &batchv1.Job{ Spec: batchv1.JobSpec{ - Completions: utilpointer.Int32Ptr(11), - Parallelism: utilpointer.Int32Ptr(10), - BackoffLimit: utilpointer.Int32Ptr(9), + Completions: utilpointer.Int32Ptr(11), + Parallelism: utilpointer.Int32Ptr(10), + BackoffLimit: utilpointer.Int32Ptr(9), + CompletionMode: batchv1.IndexedCompletion, }, }, expectLabels: true, @@ -191,37 +201,41 @@ func TestSetDefaultJob(t *testing.T) { } for name, test := range tests { - original := test.original - expected := test.expected - obj2 := roundTrip(t, runtime.Object(original)) - actual, ok := obj2.(*batchv1.Job) - if !ok { - t.Errorf("%s: unexpected object: %v", name, actual) - t.FailNow() - } + t.Run(name, func(t *testing.T) { - validateDefaultInt32(t, name, "Completions", actual.Spec.Completions, expected.Spec.Completions) - validateDefaultInt32(t, name, "Parallelism", actual.Spec.Parallelism, expected.Spec.Parallelism) - validateDefaultInt32(t, name, "BackoffLimit", actual.Spec.BackoffLimit, expected.Spec.BackoffLimit) - - if test.expectLabels != reflect.DeepEqual(actual.Labels, actual.Spec.Template.Labels) { - if test.expectLabels { - t.Errorf("%s: expected: %v, got: %v", name, actual.Spec.Template.Labels, actual.Labels) - } else { - t.Errorf("%s: unexpected equality: %v", name, actual.Labels) + original := test.original + expected := test.expected + obj2 := roundTrip(t, runtime.Object(original)) + actual, ok := obj2.(*batchv1.Job) + if !ok { + t.Fatalf("Unexpected object: %v", actual) } - } + 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) + + if test.expectLabels != reflect.DeepEqual(actual.Labels, actual.Spec.Template.Labels) { + if test.expectLabels { + t.Errorf("Expected labels: %v, got: %v", actual.Spec.Template.Labels, actual.Labels) + } else { + t.Errorf("Unexpected equality: %v", actual.Labels) + } + } + if actual.Spec.CompletionMode != expected.Spec.CompletionMode { + t.Errorf("Got CompletionMode: %v, want: %v", actual.Spec.CompletionMode, expected.Spec.CompletionMode) + } + }) } } -func validateDefaultInt32(t *testing.T, name string, field string, actual *int32, expected *int32) { +func validateDefaultInt32(t *testing.T, field string, actual *int32, expected *int32) { if (actual == nil) != (expected == nil) { - t.Errorf("%s: got different *%s than expected: %v %v", name, field, actual, expected) + t.Errorf("Got different *%s than expected: %v %v", field, actual, expected) } if actual != nil && expected != nil { if *actual != *expected { - t.Errorf("%s: got different %s than expected: %d %d", name, field, *actual, *expected) + t.Errorf("Got different %s than expected: %d %d", field, *actual, *expected) } } } diff --git a/pkg/apis/batch/v1/zz_generated.conversion.go b/pkg/apis/batch/v1/zz_generated.conversion.go index 5b66fd3d6354..26eeacebabb8 100644 --- a/pkg/apis/batch/v1/zz_generated.conversion.go +++ b/pkg/apis/batch/v1/zz_generated.conversion.go @@ -208,6 +208,7 @@ func autoConvert_v1_JobSpec_To_batch_JobSpec(in *v1.JobSpec, out *batch.JobSpec, return err } out.TTLSecondsAfterFinished = (*int32)(unsafe.Pointer(in.TTLSecondsAfterFinished)) + out.CompletionMode = batch.CompletionMode(in.CompletionMode) return nil } @@ -222,6 +223,7 @@ func autoConvert_batch_JobSpec_To_v1_JobSpec(in *batch.JobSpec, out *v1.JobSpec, return err } out.TTLSecondsAfterFinished = (*int32)(unsafe.Pointer(in.TTLSecondsAfterFinished)) + out.CompletionMode = v1.CompletionMode(in.CompletionMode) return nil } @@ -232,6 +234,7 @@ func autoConvert_v1_JobStatus_To_batch_JobStatus(in *v1.JobStatus, out *batch.Jo out.Active = in.Active out.Succeeded = in.Succeeded out.Failed = in.Failed + out.CompletedIndexes = in.CompletedIndexes return nil } @@ -247,6 +250,7 @@ func autoConvert_batch_JobStatus_To_v1_JobStatus(in *batch.JobStatus, out *v1.Jo out.Active = in.Active out.Succeeded = in.Succeeded out.Failed = in.Failed + out.CompletedIndexes = in.CompletedIndexes return nil } diff --git a/pkg/apis/batch/validation/validation.go b/pkg/apis/batch/validation/validation.go index 8466b92e0388..b0b534cc52e3 100644 --- a/pkg/apis/batch/validation/validation.go +++ b/pkg/apis/batch/validation/validation.go @@ -31,6 +31,11 @@ import ( apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" ) +// maxParallelismForIndexJob is the maximum parallelism that an Indexed Job +// is allowed to have. This threshold allows to cap the length of +// .status.completedIndexes. +const maxParallelismForIndexedJob = 100000 + // ValidateGeneratedSelector validates that the generated selector on a controller object match the controller object // metadata, and the labels on the pod template are as generated. // @@ -124,6 +129,20 @@ func validateJobSpec(spec *batch.JobSpec, fldPath *field.Path, opts apivalidatio if spec.TTLSecondsAfterFinished != nil { allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.TTLSecondsAfterFinished), fldPath.Child("ttlSecondsAfterFinished"))...) } + // CompletionMode might be empty when IndexedJob feature gate is disabled. + if spec.CompletionMode != "" { + if spec.CompletionMode != batch.NonIndexedCompletion && spec.CompletionMode != batch.IndexedCompletion { + allErrs = append(allErrs, field.NotSupported(fldPath.Child("completionMode"), spec.CompletionMode, []string{string(batch.NonIndexedCompletion), string(batch.IndexedCompletion)})) + } + if spec.CompletionMode == batch.IndexedCompletion { + if spec.Completions == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("completions"), fmt.Sprintf("when completion mode is %s", batch.IndexedCompletion))) + } + if spec.Parallelism != nil && *spec.Parallelism > maxParallelismForIndexedJob { + allErrs = append(allErrs, field.Invalid(fldPath.Child("parallelism"), *spec.Parallelism, fmt.Sprintf("must be less than or equal to %d when completion mode is %s", maxParallelismForIndexedJob, batch.IndexedCompletion))) + } + } + } allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"), opts)...) @@ -170,6 +189,7 @@ func ValidateJobSpecUpdate(spec, oldSpec batch.JobSpec, fldPath *field.Path, opt allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Completions, oldSpec.Completions, fldPath.Child("completions"))...) allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Selector, oldSpec.Selector, fldPath.Child("selector"))...) allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Template, oldSpec.Template, fldPath.Child("template"))...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.CompletionMode, oldSpec.CompletionMode, fldPath.Child("completionMode"))...) return allErrs } diff --git a/pkg/apis/batch/validation/validation_test.go b/pkg/apis/batch/validation/validation_test.go index d16b4e0ffd49..4d40ac85a3d5 100644 --- a/pkg/apis/batch/validation/validation_test.go +++ b/pkg/apis/batch/validation/validation_test.go @@ -78,7 +78,7 @@ func TestValidateJob(t *testing.T) { validPodTemplateSpecForGenerated := getValidPodTemplateSpecForGenerated(validGeneratedSelector) successCases := map[string]batch.Job{ - "manual selector": { + "valid manual selector": { ObjectMeta: metav1.ObjectMeta{ Name: "myjob", Namespace: metav1.NamespaceDefault, @@ -90,7 +90,7 @@ func TestValidateJob(t *testing.T) { Template: validPodTemplateSpecForManual, }, }, - "generated selector": { + "valid generated selector": { ObjectMeta: metav1.ObjectMeta{ Name: "myjob", Namespace: metav1.NamespaceDefault, @@ -101,6 +101,32 @@ func TestValidateJob(t *testing.T) { Template: validPodTemplateSpecForGenerated, }, }, + "valid NonIndexed completion mode": { + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + CompletionMode: batch.NonIndexedCompletion, + }, + }, + "valid Indexed completion mode": { + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + CompletionMode: batch.IndexedCompletion, + Completions: pointer.Int32Ptr(2), + Parallelism: pointer.Int32Ptr(100000), + }, + }, } for k, v := range successCases { t.Run(k, func(t *testing.T) { @@ -158,7 +184,7 @@ func TestValidateJob(t *testing.T) { Template: validPodTemplateSpecForGenerated, }, }, - "spec.template.metadata.labels: Invalid value: {\"y\":\"z\"}: `selector` does not match template `labels`": { + "spec.template.metadata.labels: Invalid value: map[string]string{\"y\":\"z\"}: `selector` does not match template `labels`": { ObjectMeta: metav1.ObjectMeta{ Name: "myjob", Namespace: metav1.NamespaceDefault, @@ -179,7 +205,7 @@ func TestValidateJob(t *testing.T) { }, }, }, - "spec.template.metadata.labels: Invalid value: {\"controller-uid\":\"4d5e6f\"}: `selector` does not match template `labels`": { + "spec.template.metadata.labels: Invalid value: map[string]string{\"controller-uid\":\"4d5e6f\"}: `selector` does not match template `labels`": { ObjectMeta: metav1.ObjectMeta{ Name: "myjob", Namespace: metav1.NamespaceDefault, @@ -242,7 +268,7 @@ func TestValidateJob(t *testing.T) { }, }, }, - "spec.ttlSecondsAfterFinished:must be greater than or equal to 0": { + "spec.ttlSecondsAfterFinished: must be greater than or equal to 0": { ObjectMeta: metav1.ObjectMeta{ Name: "myjob", Namespace: metav1.NamespaceDefault, @@ -254,6 +280,32 @@ func TestValidateJob(t *testing.T) { Template: validPodTemplateSpecForGenerated, }, }, + "spec.completions: Required value: when completion mode is Indexed": { + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + CompletionMode: batch.IndexedCompletion, + }, + }, + "spec.parallelism: must be less than or equal to 100000 when completion mode is Indexed": { + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + CompletionMode: batch.IndexedCompletion, + Completions: pointer.Int32Ptr(2), + Parallelism: pointer.Int32Ptr(100001), + }, + }, } for k, v := range errorCases { @@ -262,7 +314,7 @@ func TestValidateJob(t *testing.T) { if len(errs) == 0 { t.Errorf("expected failure for %s", k) } else { - s := strings.Split(k, ":") + s := strings.SplitN(k, ":", 2) err := errs[0] if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) { t.Errorf("unexpected error: %v, expected: %s", err, k) @@ -346,6 +398,24 @@ func TestValidateJobUpdate(t *testing.T) { Field: "spec.template", }, }, + "immutable completion mode": { + old: batch.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + CompletionMode: batch.IndexedCompletion, + Completions: pointer.Int32Ptr(2), + }, + }, + update: func(job *batch.Job) { + job.Spec.CompletionMode = batch.NonIndexedCompletion + }, + err: &field.Error{ + Type: field.ErrorTypeInvalid, + Field: "spec.completionMode", + }, + }, } ignoreValueAndDetail := cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail") for k, tc := range cases { diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 8fb22fe5c6de..fb3cdfd4cd7c 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -311,6 +311,12 @@ const ( // Allow TTL controller to clean up Pods and Jobs after they finish. TTLAfterFinished featuregate.Feature = "TTLAfterFinished" + // owner: @alculquicondor + // alpha: v1.21 + // + // Allows Job controller to manage Pod completions per completion index. + IndexedJob featuregate.Feature = "IndexedJob" + // owner: @dashpole // alpha: v1.13 // beta: v1.15 @@ -733,6 +739,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS VolumeSnapshotDataSource: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.21 ProcMountType: {Default: false, PreRelease: featuregate.Alpha}, TTLAfterFinished: {Default: true, PreRelease: featuregate.Beta}, + IndexedJob: {Default: false, PreRelease: featuregate.Alpha}, KubeletPodResources: {Default: true, PreRelease: featuregate.Beta}, LocalStorageCapacityIsolationFSQuotaMonitoring: {Default: false, PreRelease: featuregate.Alpha}, NonPreemptingPriority: {Default: true, PreRelease: featuregate.Beta}, diff --git a/pkg/registry/batch/job/strategy.go b/pkg/registry/batch/job/strategy.go index 906ca95c4a88..8df719df45bb 100644 --- a/pkg/registry/batch/job/strategy.go +++ b/pkg/registry/batch/job/strategy.go @@ -80,6 +80,10 @@ func (jobStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { job.Spec.TTLSecondsAfterFinished = nil } + if !utilfeature.DefaultFeatureGate.Enabled(features.IndexedJob) { + job.Spec.CompletionMode = batch.NonIndexedCompletion + } + pod.DropDisabledTemplateFields(&job.Spec.Template, nil) } @@ -93,6 +97,10 @@ func (jobStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object newJob.Spec.TTLSecondsAfterFinished = nil } + if !utilfeature.DefaultFeatureGate.Enabled(features.IndexedJob) && oldJob.Spec.CompletionMode == batch.NonIndexedCompletion { + newJob.Spec.CompletionMode = batch.NonIndexedCompletion + } + pod.DropDisabledTemplateFields(&newJob.Spec.Template, &oldJob.Spec.Template) } diff --git a/pkg/registry/batch/job/strategy_test.go b/pkg/registry/batch/job/strategy_test.go index 4cc78d44f2c9..b76f9d7ff6ac 100644 --- a/pkg/registry/batch/job/strategy_test.go +++ b/pkg/registry/batch/job/strategy_test.go @@ -43,16 +43,21 @@ func newInt32(i int32) *int32 { func TestJobStrategy(t *testing.T) { cases := map[string]struct { - ttlEnabled bool + ttlEnabled bool + indexedJobEnabled bool }{ "features disabled": {}, "ttl enabled": { ttlEnabled: true, }, + "indexed job enabled": { + indexedJobEnabled: 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)() testJobStrategy(t) }) } @@ -60,6 +65,7 @@ func TestJobStrategy(t *testing.T) { func testJobStrategy(t *testing.T) { ttlEnabled := utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) + indexedJobEnabled := utilfeature.DefaultFeatureGate.Enabled(features.IndexedJob) ctx := genericapirequest.NewDefaultContext() if !Strategy.NamespaceScoped() { t.Errorf("Job must be namespace scoped") @@ -87,10 +93,13 @@ func testJobStrategy(t *testing.T) { Namespace: metav1.NamespaceDefault, }, Spec: batch.JobSpec{ - Selector: validSelector, - Template: validPodTemplateSpec, - TTLSecondsAfterFinished: newInt32(0), // Set TTL - ManualSelector: newBool(true), + Selector: validSelector, + Template: validPodTemplateSpec, + ManualSelector: newBool(true), + Completions: newInt32(2), + // Set gated values. + TTLSecondsAfterFinished: newInt32(0), + CompletionMode: batch.IndexedCompletion, }, Status: batch.JobStatus{ Active: 11, @@ -106,15 +115,21 @@ func testJobStrategy(t *testing.T) { t.Errorf("Unexpected error validating %v", errs) } if ttlEnabled != (job.Spec.TTLSecondsAfterFinished != nil) { - t.Errorf("Job should allow setting .spec.ttlSecondsAfterFinished when %v feature is enabled", features.TTLAfterFinished) + t.Errorf("Job should allow setting .spec.ttlSecondsAfterFinished only when %v feature is enabled", features.TTLAfterFinished) + } + if indexedJobEnabled != (job.Spec.CompletionMode != batch.NonIndexedCompletion) { + t.Errorf("Job should allow setting .spec.completionMode=Indexed only when %v feature is enabled", features.IndexedJob) } parallelism := int32(10) updatedJob := &batch.Job{ ObjectMeta: metav1.ObjectMeta{Name: "bar", ResourceVersion: "4"}, Spec: batch.JobSpec{ - Parallelism: ¶llelism, - TTLSecondsAfterFinished: newInt32(1), // Update TTL + Parallelism: ¶llelism, + Completions: newInt32(2), + // Update gated features. + TTLSecondsAfterFinished: newInt32(1), + CompletionMode: batch.IndexedCompletion, // No change because field is immutable. }, Status: batch.JobStatus{ Active: 11, @@ -135,13 +150,18 @@ func testJobStrategy(t *testing.T) { t.Errorf("Expected a validation error") } - // Existing TTLSecondsAfterFinished should be preserved + // Existing gated fields should be preserved job.Spec.TTLSecondsAfterFinished = newInt32(1) + job.Spec.CompletionMode = batch.IndexedCompletion updatedJob.Spec.TTLSecondsAfterFinished = newInt32(2) + updatedJob.Spec.CompletionMode = batch.IndexedCompletion Strategy.PrepareForUpdate(ctx, updatedJob, job) if job.Spec.TTLSecondsAfterFinished == nil || updatedJob.Spec.TTLSecondsAfterFinished == nil { t.Errorf("existing TTLSecondsAfterFinished should be preserved") } + if job.Spec.CompletionMode == "" || updatedJob.Spec.CompletionMode == "" { + t.Errorf("existing completionMode should be preserved") + } // Make sure we correctly implement the interface. // Otherwise a typo could silently change the default. diff --git a/staging/src/k8s.io/api/batch/v1/generated.pb.go b/staging/src/k8s.io/api/batch/v1/generated.pb.go index dc43514c6ae2..af7f36c4ce72 100644 --- a/staging/src/k8s.io/api/batch/v1/generated.pb.go +++ b/staging/src/k8s.io/api/batch/v1/generated.pb.go @@ -198,66 +198,69 @@ func init() { } var fileDescriptor_3b52da57c93de713 = []byte{ - // 929 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x54, 0x5d, 0x6f, 0xe3, 0x44, - 0x14, 0xad, 0x9b, 0xa6, 0x4d, 0xa6, 0x1f, 0x5b, 0x06, 0x55, 0x1b, 0x0a, 0xb2, 0x97, 0x20, 0xa1, - 0x82, 0x84, 0x4d, 0x4b, 0x85, 0x10, 0x02, 0xa4, 0x75, 0x51, 0x25, 0xaa, 0x54, 0x5b, 0x26, 0x59, - 0x21, 0x21, 0x90, 0x18, 0xdb, 0x37, 0x89, 0x89, 0xed, 0xb1, 0x3c, 0x93, 0x48, 0x7d, 0xe3, 0x27, - 0xf0, 0x23, 0x10, 0x7f, 0x82, 0x77, 0xd4, 0xc7, 0x7d, 0xdc, 0x27, 0x8b, 0x9a, 0x1f, 0xc0, 0xfb, - 0x3e, 0xa1, 0x19, 0x3b, 0xb6, 0xd3, 0x26, 0xa2, 0xcb, 0x5b, 0xe6, 0xcc, 0x39, 0xe7, 0x5e, 0xcf, - 0x3d, 0xb9, 0xe8, 0x8b, 0xc9, 0x67, 0xdc, 0xf4, 0x99, 0x35, 0x99, 0x3a, 0x90, 0x44, 0x20, 0x80, - 0x5b, 0x33, 0x88, 0x3c, 0x96, 0x58, 0xc5, 0x05, 0x8d, 0x7d, 0xcb, 0xa1, 0xc2, 0x1d, 0x5b, 0xb3, - 0x63, 0x6b, 0x04, 0x11, 0x24, 0x54, 0x80, 0x67, 0xc6, 0x09, 0x13, 0x0c, 0xbf, 0x99, 0x93, 0x4c, - 0x1a, 0xfb, 0xa6, 0x22, 0x99, 0xb3, 0xe3, 0xc3, 0x8f, 0x46, 0xbe, 0x18, 0x4f, 0x1d, 0xd3, 0x65, - 0xa1, 0x35, 0x62, 0x23, 0x66, 0x29, 0xae, 0x33, 0x1d, 0xaa, 0x93, 0x3a, 0xa8, 0x5f, 0xb9, 0xc7, - 0x61, 0xb7, 0x56, 0xc8, 0x65, 0x09, 0x2c, 0xa9, 0x73, 0x78, 0x5a, 0x71, 0x42, 0xea, 0x8e, 0xfd, - 0x08, 0x92, 0x6b, 0x2b, 0x9e, 0x8c, 0x24, 0xc0, 0xad, 0x10, 0x04, 0x5d, 0xa6, 0xb2, 0x56, 0xa9, - 0x92, 0x69, 0x24, 0xfc, 0x10, 0xee, 0x09, 0x3e, 0xfd, 0x2f, 0x01, 0x77, 0xc7, 0x10, 0xd2, 0xbb, - 0xba, 0xee, 0x3f, 0x1a, 0x6a, 0x5c, 0x30, 0x07, 0xff, 0x84, 0x5a, 0xb2, 0x17, 0x8f, 0x0a, 0xda, - 0xd1, 0x9e, 0x68, 0x47, 0xdb, 0x27, 0x1f, 0x9b, 0xd5, 0x0b, 0x95, 0x96, 0x66, 0x3c, 0x19, 0x49, - 0x80, 0x9b, 0x92, 0x6d, 0xce, 0x8e, 0xcd, 0x67, 0xce, 0xcf, 0xe0, 0x8a, 0x4b, 0x10, 0xd4, 0xc6, - 0x37, 0xa9, 0xb1, 0x96, 0xa5, 0x06, 0xaa, 0x30, 0x52, 0xba, 0xe2, 0xaf, 0xd0, 0x06, 0x8f, 0xc1, - 0xed, 0xac, 0x2b, 0xf7, 0x77, 0xcc, 0x25, 0xef, 0x6f, 0x5e, 0x30, 0xa7, 0x1f, 0x83, 0x6b, 0xef, - 0x14, 0x4e, 0x1b, 0xf2, 0x44, 0x94, 0x0e, 0x9f, 0xa3, 0x4d, 0x2e, 0xa8, 0x98, 0xf2, 0x4e, 0x43, - 0x39, 0xe8, 0x2b, 0x1d, 0x14, 0xcb, 0xde, 0x2b, 0x3c, 0x36, 0xf3, 0x33, 0x29, 0xd4, 0xdd, 0x3f, - 0x1b, 0x68, 0xe7, 0x82, 0x39, 0x67, 0x2c, 0xf2, 0x7c, 0xe1, 0xb3, 0x08, 0x9f, 0xa2, 0x0d, 0x71, - 0x1d, 0x83, 0xfa, 0xec, 0xb6, 0xfd, 0x64, 0x5e, 0x7a, 0x70, 0x1d, 0xc3, 0xab, 0xd4, 0xd8, 0xaf, - 0x73, 0x25, 0x46, 0x14, 0x1b, 0xf7, 0xca, 0x76, 0xd6, 0x95, 0xee, 0x74, 0xb1, 0xdc, 0xab, 0xd4, - 0x58, 0x92, 0x0e, 0xb3, 0x74, 0x5a, 0x6c, 0x0a, 0x8f, 0xd0, 0x6e, 0x40, 0xb9, 0xb8, 0x4a, 0x98, - 0x03, 0x03, 0x3f, 0x84, 0xe2, 0x1b, 0x3f, 0x7c, 0xd8, 0x0c, 0xa4, 0xc2, 0x3e, 0x28, 0x1a, 0xd8, - 0xed, 0xd5, 0x8d, 0xc8, 0xa2, 0x2f, 0x9e, 0x21, 0x2c, 0x81, 0x41, 0x42, 0x23, 0x9e, 0x7f, 0x92, - 0xac, 0xb6, 0xf1, 0xda, 0xd5, 0x0e, 0x8b, 0x6a, 0xb8, 0x77, 0xcf, 0x8d, 0x2c, 0xa9, 0x80, 0xdf, - 0x47, 0x9b, 0x09, 0x50, 0xce, 0xa2, 0x4e, 0x53, 0x3d, 0x57, 0x39, 0x1d, 0xa2, 0x50, 0x52, 0xdc, - 0xe2, 0x0f, 0xd0, 0x56, 0x08, 0x9c, 0xd3, 0x11, 0x74, 0x36, 0x15, 0xf1, 0x51, 0x41, 0xdc, 0xba, - 0xcc, 0x61, 0x32, 0xbf, 0xef, 0xfe, 0xae, 0xa1, 0xad, 0x0b, 0xe6, 0xf4, 0x7c, 0x2e, 0xf0, 0x0f, - 0xf7, 0xe2, 0x6b, 0x3e, 0xec, 0x63, 0xa4, 0x5a, 0x85, 0x77, 0xbf, 0xa8, 0xd3, 0x9a, 0x23, 0xb5, - 0xe8, 0x7e, 0x89, 0x9a, 0xbe, 0x80, 0x50, 0x8e, 0xba, 0x71, 0xb4, 0x7d, 0xd2, 0x59, 0x95, 0x3c, - 0x7b, 0xb7, 0x30, 0x69, 0x7e, 0x23, 0xe9, 0x24, 0x57, 0x75, 0xff, 0xd8, 0x50, 0x8d, 0xca, 0x2c, - 0xe3, 0x63, 0xb4, 0x1d, 0xd3, 0x84, 0x06, 0x01, 0x04, 0x3e, 0x0f, 0x55, 0xaf, 0x4d, 0xfb, 0x51, - 0x96, 0x1a, 0xdb, 0x57, 0x15, 0x4c, 0xea, 0x1c, 0x29, 0x71, 0x59, 0x18, 0x07, 0x20, 0x1f, 0x33, - 0x8f, 0x5b, 0x21, 0x39, 0xab, 0x60, 0x52, 0xe7, 0xe0, 0x67, 0xe8, 0x80, 0xba, 0xc2, 0x9f, 0xc1, - 0xd7, 0x40, 0xbd, 0xc0, 0x8f, 0xa0, 0x0f, 0x2e, 0x8b, 0xbc, 0xfc, 0xaf, 0xd3, 0xb0, 0xdf, 0xca, - 0x52, 0xe3, 0xe0, 0xe9, 0x32, 0x02, 0x59, 0xae, 0xc3, 0xa7, 0x68, 0xc7, 0xa1, 0xee, 0x84, 0x0d, - 0x87, 0x3d, 0x3f, 0xf4, 0x45, 0x67, 0x4b, 0x35, 0xb1, 0x9f, 0xa5, 0xc6, 0x8e, 0x5d, 0xc3, 0xc9, - 0x02, 0x0b, 0xff, 0x88, 0x5a, 0x1c, 0x02, 0x70, 0x05, 0x4b, 0x8a, 0x88, 0x7d, 0xf2, 0xc0, 0xa9, - 0x50, 0x07, 0x82, 0x7e, 0x21, 0xb5, 0x77, 0xe4, 0x58, 0xe6, 0x27, 0x52, 0x5a, 0xe2, 0xcf, 0xd1, - 0x5e, 0x48, 0xa3, 0x29, 0x2d, 0x99, 0x2a, 0x5b, 0x2d, 0x1b, 0x67, 0xa9, 0xb1, 0x77, 0xb9, 0x70, - 0x43, 0xee, 0x30, 0xf1, 0xb7, 0xa8, 0x25, 0x20, 0x8c, 0x03, 0x2a, 0xf2, 0xa0, 0x6d, 0x9f, 0xbc, - 0x57, 0x9f, 0xaa, 0xfc, 0xbf, 0xca, 0x46, 0xae, 0x98, 0x37, 0x28, 0x68, 0x6a, 0x31, 0x95, 0x29, - 0x99, 0xa3, 0xa4, 0xb4, 0xc1, 0xcf, 0xd1, 0x63, 0x21, 0x82, 0xe2, 0xc5, 0x9e, 0x0e, 0x05, 0x24, - 0xe7, 0x7e, 0xe4, 0xf3, 0x31, 0x78, 0x9d, 0x96, 0x7a, 0xae, 0xb7, 0xb3, 0xd4, 0x78, 0x3c, 0x18, - 0xf4, 0x96, 0x51, 0xc8, 0x2a, 0x6d, 0xf7, 0xb7, 0x06, 0x6a, 0x97, 0x5b, 0x0d, 0x3f, 0x47, 0xc8, - 0x9d, 0xef, 0x10, 0xde, 0xd1, 0x54, 0x1e, 0xdf, 0x5d, 0x95, 0xc7, 0x72, 0xdb, 0x54, 0xab, 0xb9, - 0x84, 0x38, 0xa9, 0x19, 0xe1, 0xef, 0x50, 0x9b, 0x0b, 0x9a, 0x08, 0xb5, 0x0d, 0xd6, 0x5f, 0x7b, - 0x1b, 0xec, 0x66, 0xa9, 0xd1, 0xee, 0xcf, 0x0d, 0x48, 0xe5, 0x85, 0x87, 0x68, 0xaf, 0x0a, 0xe6, - 0xff, 0xdc, 0x6c, 0x6a, 0x9e, 0x67, 0x0b, 0x2e, 0xe4, 0x8e, 0xab, 0xdc, 0x2f, 0x79, 0x72, 0x55, - 0xd0, 0x9a, 0xd5, 0x7e, 0xc9, 0x63, 0x4e, 0x8a, 0x5b, 0x6c, 0xa1, 0x36, 0x9f, 0xba, 0x2e, 0x80, - 0x07, 0x9e, 0x8a, 0x4b, 0xd3, 0x7e, 0xa3, 0xa0, 0xb6, 0xfb, 0xf3, 0x0b, 0x52, 0x71, 0xa4, 0xf1, - 0x90, 0xfa, 0x01, 0x78, 0x2a, 0x26, 0x35, 0xe3, 0x73, 0x85, 0x92, 0xe2, 0xd6, 0x3e, 0xba, 0xb9, - 0xd5, 0xd7, 0x5e, 0xdc, 0xea, 0x6b, 0x2f, 0x6f, 0xf5, 0xb5, 0x5f, 0x32, 0x5d, 0xbb, 0xc9, 0x74, - 0xed, 0x45, 0xa6, 0x6b, 0x2f, 0x33, 0x5d, 0xfb, 0x2b, 0xd3, 0xb5, 0x5f, 0xff, 0xd6, 0xd7, 0xbe, - 0x5f, 0x9f, 0x1d, 0xff, 0x1b, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x73, 0xe7, 0x7a, 0xb8, 0x08, 0x00, - 0x00, + // 979 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x54, 0x5f, 0x6f, 0xe3, 0xc4, + 0x17, 0x6d, 0x9a, 0xa6, 0x4d, 0xa6, 0x7f, 0xb6, 0xbf, 0xf9, 0xa9, 0x5a, 0x53, 0x50, 0xbc, 0x04, + 0x09, 0x15, 0x24, 0x6c, 0x5a, 0x2a, 0x84, 0x10, 0x20, 0xad, 0xbb, 0xaa, 0xb4, 0x55, 0xaa, 0x2d, + 0xd3, 0xac, 0x90, 0x10, 0x48, 0x8c, 0xed, 0x9b, 0xd4, 0xd4, 0xf6, 0x58, 0x9e, 0x49, 0x44, 0xdf, + 0xf8, 0x02, 0x48, 0x7c, 0x0a, 0x3e, 0x0a, 0xea, 0xe3, 0x3e, 0xee, 0x93, 0x45, 0xcd, 0x1b, 0x2f, + 0xbc, 0xf7, 0x09, 0xcd, 0x78, 0x62, 0x3b, 0x6d, 0x22, 0xba, 0xbc, 0x79, 0xee, 0x3d, 0xe7, 0xdc, + 0xeb, 0x3b, 0x67, 0x2e, 0xfa, 0xe2, 0xf2, 0x33, 0x6e, 0x05, 0xcc, 0xbe, 0x1c, 0xbb, 0x90, 0xc6, + 0x20, 0x80, 0xdb, 0x13, 0x88, 0x7d, 0x96, 0xda, 0x3a, 0x41, 0x93, 0xc0, 0x76, 0xa9, 0xf0, 0x2e, + 0xec, 0xc9, 0xbe, 0x3d, 0x82, 0x18, 0x52, 0x2a, 0xc0, 0xb7, 0x92, 0x94, 0x09, 0x86, 0xff, 0x5f, + 0x80, 0x2c, 0x9a, 0x04, 0x96, 0x02, 0x59, 0x93, 0xfd, 0xdd, 0x8f, 0x46, 0x81, 0xb8, 0x18, 0xbb, + 0x96, 0xc7, 0x22, 0x7b, 0xc4, 0x46, 0xcc, 0x56, 0x58, 0x77, 0x3c, 0x54, 0x27, 0x75, 0x50, 0x5f, + 0x85, 0xc6, 0x6e, 0xaf, 0x56, 0xc8, 0x63, 0x29, 0xcc, 0xa9, 0xb3, 0x7b, 0x58, 0x61, 0x22, 0xea, + 0x5d, 0x04, 0x31, 0xa4, 0x57, 0x76, 0x72, 0x39, 0x92, 0x01, 0x6e, 0x47, 0x20, 0xe8, 0x3c, 0x96, + 0xbd, 0x88, 0x95, 0x8e, 0x63, 0x11, 0x44, 0x70, 0x8f, 0xf0, 0xe9, 0xbf, 0x11, 0xb8, 0x77, 0x01, + 0x11, 0xbd, 0xcb, 0xeb, 0xfd, 0xdd, 0x40, 0xcd, 0x13, 0xe6, 0xe2, 0x1f, 0x50, 0x5b, 0xf6, 0xe2, + 0x53, 0x41, 0x8d, 0xc6, 0x93, 0xc6, 0xde, 0xfa, 0xc1, 0xc7, 0x56, 0x35, 0xa1, 0x52, 0xd2, 0x4a, + 0x2e, 0x47, 0x32, 0xc0, 0x2d, 0x89, 0xb6, 0x26, 0xfb, 0xd6, 0x0b, 0xf7, 0x47, 0xf0, 0xc4, 0x29, + 0x08, 0xea, 0xe0, 0xeb, 0xcc, 0x5c, 0xca, 0x33, 0x13, 0x55, 0x31, 0x52, 0xaa, 0xe2, 0xaf, 0xd0, + 0x0a, 0x4f, 0xc0, 0x33, 0x96, 0x95, 0xfa, 0x3b, 0xd6, 0x9c, 0xf9, 0x5b, 0x27, 0xcc, 0x3d, 0x4f, + 0xc0, 0x73, 0x36, 0xb4, 0xd2, 0x8a, 0x3c, 0x11, 0xc5, 0xc3, 0xc7, 0x68, 0x95, 0x0b, 0x2a, 0xc6, + 0xdc, 0x68, 0x2a, 0x85, 0xee, 0x42, 0x05, 0x85, 0x72, 0xb6, 0xb4, 0xc6, 0x6a, 0x71, 0x26, 0x9a, + 0xdd, 0xfb, 0xbd, 0x89, 0x36, 0x4e, 0x98, 0x7b, 0xc4, 0x62, 0x3f, 0x10, 0x01, 0x8b, 0xf1, 0x21, + 0x5a, 0x11, 0x57, 0x09, 0xa8, 0xdf, 0xee, 0x38, 0x4f, 0xa6, 0xa5, 0x07, 0x57, 0x09, 0xdc, 0x66, + 0xe6, 0x76, 0x1d, 0x2b, 0x63, 0x44, 0xa1, 0x71, 0xbf, 0x6c, 0x67, 0x59, 0xf1, 0x0e, 0x67, 0xcb, + 0xdd, 0x66, 0xe6, 0x1c, 0x77, 0x58, 0xa5, 0xd2, 0x6c, 0x53, 0x78, 0x84, 0x36, 0x43, 0xca, 0xc5, + 0x59, 0xca, 0x5c, 0x18, 0x04, 0x11, 0xe8, 0x7f, 0xfc, 0xf0, 0x61, 0x77, 0x20, 0x19, 0xce, 0x8e, + 0x6e, 0x60, 0xb3, 0x5f, 0x17, 0x22, 0xb3, 0xba, 0x78, 0x82, 0xb0, 0x0c, 0x0c, 0x52, 0x1a, 0xf3, + 0xe2, 0x97, 0x64, 0xb5, 0x95, 0x37, 0xae, 0xb6, 0xab, 0xab, 0xe1, 0xfe, 0x3d, 0x35, 0x32, 0xa7, + 0x02, 0x7e, 0x1f, 0xad, 0xa6, 0x40, 0x39, 0x8b, 0x8d, 0x96, 0x1a, 0x57, 0x79, 0x3b, 0x44, 0x45, + 0x89, 0xce, 0xe2, 0x0f, 0xd0, 0x5a, 0x04, 0x9c, 0xd3, 0x11, 0x18, 0xab, 0x0a, 0xf8, 0x48, 0x03, + 0xd7, 0x4e, 0x8b, 0x30, 0x99, 0xe6, 0x7b, 0xbf, 0x35, 0xd0, 0xda, 0x09, 0x73, 0xfb, 0x01, 0x17, + 0xf8, 0xbb, 0x7b, 0xf6, 0xb5, 0x1e, 0xf6, 0x33, 0x92, 0xad, 0xcc, 0xbb, 0xad, 0xeb, 0xb4, 0xa7, + 0x91, 0x9a, 0x75, 0xbf, 0x44, 0xad, 0x40, 0x40, 0x24, 0xaf, 0xba, 0xb9, 0xb7, 0x7e, 0x60, 0x2c, + 0x72, 0x9e, 0xb3, 0xa9, 0x45, 0x5a, 0xcf, 0x25, 0x9c, 0x14, 0xac, 0xde, 0x2f, 0x2d, 0xd5, 0xa8, + 0xf4, 0x32, 0xde, 0x47, 0xeb, 0x09, 0x4d, 0x69, 0x18, 0x42, 0x18, 0xf0, 0x48, 0xf5, 0xda, 0x72, + 0x1e, 0xe5, 0x99, 0xb9, 0x7e, 0x56, 0x85, 0x49, 0x1d, 0x23, 0x29, 0x1e, 0x8b, 0x92, 0x10, 0xe4, + 0x30, 0x0b, 0xbb, 0x69, 0xca, 0x51, 0x15, 0x26, 0x75, 0x0c, 0x7e, 0x81, 0x76, 0xa8, 0x27, 0x82, + 0x09, 0x3c, 0x03, 0xea, 0x87, 0x41, 0x0c, 0xe7, 0xe0, 0xb1, 0xd8, 0x2f, 0x9e, 0x4e, 0xd3, 0x79, + 0x2b, 0xcf, 0xcc, 0x9d, 0xa7, 0xf3, 0x00, 0x64, 0x3e, 0x0f, 0x1f, 0xa2, 0x0d, 0x97, 0x7a, 0x97, + 0x6c, 0x38, 0xec, 0x07, 0x51, 0x20, 0x8c, 0x35, 0xd5, 0xc4, 0x76, 0x9e, 0x99, 0x1b, 0x4e, 0x2d, + 0x4e, 0x66, 0x50, 0xf8, 0x7b, 0xd4, 0xe6, 0x10, 0x82, 0x27, 0x58, 0xaa, 0x2d, 0xf6, 0xc9, 0x03, + 0x6f, 0x85, 0xba, 0x10, 0x9e, 0x6b, 0xaa, 0xb3, 0x21, 0xaf, 0x65, 0x7a, 0x22, 0xa5, 0x24, 0xfe, + 0x1c, 0x6d, 0x45, 0x34, 0x1e, 0xd3, 0x12, 0xa9, 0xbc, 0xd5, 0x76, 0x70, 0x9e, 0x99, 0x5b, 0xa7, + 0x33, 0x19, 0x72, 0x07, 0x89, 0xbf, 0x46, 0x6d, 0x01, 0x51, 0x12, 0x52, 0x51, 0x18, 0x6d, 0xfd, + 0xe0, 0xbd, 0xfa, 0xad, 0xca, 0xf7, 0x2a, 0x1b, 0x39, 0x63, 0xfe, 0x40, 0xc3, 0xd4, 0x62, 0x2a, + 0x5d, 0x32, 0x8d, 0x92, 0x52, 0x06, 0xbf, 0x44, 0x8f, 0x85, 0x08, 0xf5, 0xc4, 0x9e, 0x0e, 0x05, + 0xa4, 0xc7, 0x41, 0x1c, 0xf0, 0x0b, 0xf0, 0x8d, 0xb6, 0x1a, 0xd7, 0xdb, 0x79, 0x66, 0x3e, 0x1e, + 0x0c, 0xfa, 0xf3, 0x20, 0x64, 0x11, 0x17, 0x9f, 0xa1, 0xad, 0xea, 0x6a, 0x4f, 0x99, 0x0f, 0x46, + 0x47, 0x3d, 0x8c, 0x3d, 0xdd, 0xca, 0xd6, 0xd1, 0x4c, 0xf6, 0xf6, 0x5e, 0x84, 0xdc, 0xe1, 0xf7, + 0xfe, 0x6a, 0xa2, 0x4e, 0xb9, 0x27, 0xf1, 0x4b, 0x84, 0xbc, 0xe9, 0x56, 0xe2, 0x46, 0x43, 0x39, + 0xfc, 0xdd, 0x45, 0x0e, 0x2f, 0xf7, 0x57, 0xb5, 0xec, 0xcb, 0x10, 0x27, 0x35, 0x21, 0xfc, 0x0d, + 0xea, 0x70, 0x41, 0x53, 0xa1, 0xf6, 0xcb, 0xf2, 0x1b, 0xef, 0x97, 0xcd, 0x3c, 0x33, 0x3b, 0xe7, + 0x53, 0x01, 0x52, 0x69, 0xe1, 0x61, 0x7d, 0x1e, 0xff, 0x71, 0x57, 0xe2, 0xd9, 0xb9, 0xa9, 0x12, + 0x77, 0x54, 0xe5, 0xc6, 0x2a, 0xde, 0x82, 0xb2, 0x6e, 0xab, 0xda, 0x58, 0xc5, 0xc3, 0x21, 0x3a, + 0x8b, 0x6d, 0xd4, 0xe1, 0x63, 0xcf, 0x03, 0xf0, 0xc1, 0x57, 0x06, 0x6c, 0x39, 0xff, 0xd3, 0xd0, + 0xce, 0xf9, 0x34, 0x41, 0x2a, 0x8c, 0x14, 0x1e, 0xd2, 0x20, 0x04, 0x5f, 0x19, 0xaf, 0x26, 0x7c, + 0xac, 0xa2, 0x44, 0x67, 0xf1, 0x33, 0xb4, 0xad, 0x5b, 0x02, 0xff, 0x79, 0xec, 0xc3, 0x4f, 0xc0, + 0xd5, 0xbb, 0xeb, 0x38, 0x86, 0x66, 0x6c, 0x1f, 0xdd, 0xc9, 0x93, 0x7b, 0x0c, 0x67, 0xef, 0xfa, + 0xa6, 0xbb, 0xf4, 0xea, 0xa6, 0xbb, 0xf4, 0xfa, 0xa6, 0xbb, 0xf4, 0x73, 0xde, 0x6d, 0x5c, 0xe7, + 0xdd, 0xc6, 0xab, 0xbc, 0xdb, 0x78, 0x9d, 0x77, 0x1b, 0x7f, 0xe4, 0xdd, 0xc6, 0xaf, 0x7f, 0x76, + 0x97, 0xbe, 0x5d, 0x9e, 0xec, 0xff, 0x13, 0x00, 0x00, 0xff, 0xff, 0xe3, 0x79, 0x3b, 0xba, 0x50, + 0x09, 0x00, 0x00, } func (m *Job) Marshal() (dAtA []byte, err error) { @@ -443,6 +446,11 @@ func (m *JobSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + i -= len(m.CompletionMode) + copy(dAtA[i:], m.CompletionMode) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.CompletionMode))) + i-- + dAtA[i] = 0x4a if m.TTLSecondsAfterFinished != nil { i = encodeVarintGenerated(dAtA, i, uint64(*m.TTLSecondsAfterFinished)) i-- @@ -523,6 +531,11 @@ func (m *JobStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + i -= len(m.CompletedIndexes) + copy(dAtA[i:], m.CompletedIndexes) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.CompletedIndexes))) + i-- + dAtA[i] = 0x3a i = encodeVarintGenerated(dAtA, i, uint64(m.Failed)) i-- dAtA[i] = 0x30 @@ -667,6 +680,8 @@ func (m *JobSpec) Size() (n int) { if m.TTLSecondsAfterFinished != nil { n += 1 + sovGenerated(uint64(*m.TTLSecondsAfterFinished)) } + l = len(m.CompletionMode) + n += 1 + l + sovGenerated(uint64(l)) return n } @@ -693,6 +708,8 @@ func (m *JobStatus) Size() (n int) { n += 1 + sovGenerated(uint64(m.Active)) n += 1 + sovGenerated(uint64(m.Succeeded)) n += 1 + sovGenerated(uint64(m.Failed)) + l = len(m.CompletedIndexes) + n += 1 + l + sovGenerated(uint64(l)) return n } @@ -758,6 +775,7 @@ func (this *JobSpec) String() string { `Template:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.Template), "PodTemplateSpec", "v11.PodTemplateSpec", 1), `&`, ``, 1) + `,`, `BackoffLimit:` + valueToStringGenerated(this.BackoffLimit) + `,`, `TTLSecondsAfterFinished:` + valueToStringGenerated(this.TTLSecondsAfterFinished) + `,`, + `CompletionMode:` + fmt.Sprintf("%v", this.CompletionMode) + `,`, `}`, }, "") return s @@ -778,6 +796,7 @@ func (this *JobStatus) String() string { `Active:` + fmt.Sprintf("%v", this.Active) + `,`, `Succeeded:` + fmt.Sprintf("%v", this.Succeeded) + `,`, `Failed:` + fmt.Sprintf("%v", this.Failed) + `,`, + `CompletedIndexes:` + fmt.Sprintf("%v", this.CompletedIndexes) + `,`, `}`, }, "") return s @@ -1519,6 +1538,38 @@ func (m *JobSpec) Unmarshal(dAtA []byte) error { } } m.TTLSecondsAfterFinished = &v + case 9: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field CompletionMode", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.CompletionMode = CompletionMode(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -1732,6 +1783,38 @@ func (m *JobStatus) Unmarshal(dAtA []byte) error { break } } + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field CompletedIndexes", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.CompletedIndexes = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) diff --git a/staging/src/k8s.io/api/batch/v1/generated.proto b/staging/src/k8s.io/api/batch/v1/generated.proto index 7548c04dc190..74d437640dac 100644 --- a/staging/src/k8s.io/api/batch/v1/generated.proto +++ b/staging/src/k8s.io/api/batch/v1/generated.proto @@ -146,6 +146,28 @@ message JobSpec { // TTLAfterFinished feature. // +optional optional int32 ttlSecondsAfterFinished = 8; + + // CompletionMode specifies how Pod completions are tracked. It can be + // `NonIndexed` (default) or `Indexed`. + // + // `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. + // + // `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. + // + // This 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. + // +optional + optional string completionMode = 9; } // JobStatus represents the current state of a Job. @@ -182,5 +204,15 @@ message JobStatus { // The number of pods which reached phase Failed. // +optional optional int32 failed = 6; + + // CompletedIndexes holds the completed indexes when .spec.completionMode = + // "Indexed" in a text format. The indexes are represented as decimal integers + // separated by commas. The numbers are listed in increasing order. Three or + // more consecutive numbers are compressed and represented by the first and + // last element of the series, separated by a hyphen. + // For example, if the completed indexes are 1, 3, 4, 5 and 7, they are + // represented as "1,3-5,7". + // +optional + optional string completedIndexes = 7; } diff --git a/staging/src/k8s.io/api/batch/v1/types.go b/staging/src/k8s.io/api/batch/v1/types.go index fd478874a13d..c148d77b6e3f 100644 --- a/staging/src/k8s.io/api/batch/v1/types.go +++ b/staging/src/k8s.io/api/batch/v1/types.go @@ -21,6 +21,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const JobCompletionIndexAnnotationAlpha = "batch.alpha.kubernetes.io/job-completion-index" + // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -57,6 +59,22 @@ type JobList struct { Items []Job `json:"items" protobuf:"bytes,2,rep,name=items"` } +// CompletionMode specifies how Pod completions of a Job are tracked. +type CompletionMode string + +const ( + // NonIndexedCompletion is a Job completion mode. In this mode, the Job is + // considered complete when there have been .spec.completions + // successfully completed Pods. Pod completions are homologous to each other. + NonIndexedCompletion CompletionMode = "NonIndexed" + + // IndexedCompletion is a Job completion mode. In this mode, the Pods of a + // Job get an associated completion index from 0 to (.spec.completions - 1). + // The Job is considered complete when a Pod completes for each completion + // index. + IndexedCompletion CompletionMode = "Indexed" +) + // JobSpec describes how the job execution will look like. type JobSpec struct { @@ -126,6 +144,28 @@ type JobSpec struct { // TTLAfterFinished feature. // +optional TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty" protobuf:"varint,8,opt,name=ttlSecondsAfterFinished"` + + // CompletionMode specifies how Pod completions are tracked. It can be + // `NonIndexed` (default) or `Indexed`. + // + // `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. + // + // `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. + // + // This 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. + // +optional + CompletionMode CompletionMode `json:"completionMode,omitempty" protobuf:"bytes,9,opt,name=completionMode,casttype=CompletionMode"` } // JobStatus represents the current state of a Job. @@ -162,6 +202,16 @@ type JobStatus struct { // The number of pods which reached phase Failed. // +optional Failed int32 `json:"failed,omitempty" protobuf:"varint,6,opt,name=failed"` + + // CompletedIndexes holds the completed indexes when .spec.completionMode = + // "Indexed" in a text format. The indexes are represented as decimal integers + // separated by commas. The numbers are listed in increasing order. Three or + // more consecutive numbers are compressed and represented by the first and + // last element of the series, separated by a hyphen. + // For example, if the completed indexes are 1, 3, 4, 5 and 7, they are + // represented as "1,3-5,7". + // +optional + CompletedIndexes string `json:"completedIndexes,omitempty" protobuf:"bytes,7,opt,name=completedIndexes"` } type JobConditionType string diff --git a/staging/src/k8s.io/api/batch/v1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/batch/v1/types_swagger_doc_generated.go index 0d8003a727fb..8a7eb17ab1b9 100644 --- a/staging/src/k8s.io/api/batch/v1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/batch/v1/types_swagger_doc_generated.go @@ -72,6 +72,7 @@ var map_JobSpec = map[string]string{ "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.", } func (JobSpec) SwaggerDoc() map[string]string { @@ -79,13 +80,14 @@ 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.", - "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.", - "failed": "The number of pods which reached phase Failed.", + "": "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.", + "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.", + "failed": "The number of pods which reached phase Failed.", + "completedIndexes": "CompletedIndexes holds the completed indexes when .spec.completionMode = \"Indexed\" in a text format. The indexes are represented as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the completed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\".", } func (JobStatus) SwaggerDoc() map[string]string { diff --git a/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.json b/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.json index 869198a9b9c4..662f1df7654a 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.json +++ b/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.json @@ -1474,21 +1474,23 @@ "setHostnameAsFQDN": false } }, - "ttlSecondsAfterFinished": 1192652907 + "ttlSecondsAfterFinished": 1192652907, + "completionMode": "莾簏ì淵歔ųd," }, "status": { "conditions": [ { - "type": "", - "status": "簏ì淵歔ųd,4", - "lastProbeTime": "2813-03-11T20:08:42Z", - "lastTransitionTime": "2793-11-20T00:30:11Z", + "type": ";蛡媈U", + "status": "Oa2ƒƈɈ达iʍjʒu+,妧縖%Á", + "lastProbeTime": "2823-10-04T11:14:04Z", + "lastTransitionTime": "2882-02-07T11:38:45Z", "reason": "464", "message": "465" } ], - "active": -1983720493, - "succeeded": -2026748262, - "failed": 1049326966 + "active": -1993578228, + "succeeded": 1971731732, + "failed": 165851549, + "completedIndexes": "466" } } \ No newline at end of file diff --git a/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.pb b/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.pb index eb3570f162b1..26eb05c55cc9 100644 Binary files a/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.pb and b/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.pb differ diff --git a/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.yaml b/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.yaml index 8ddb75e7efa6..6a22b9de09ed 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.yaml +++ b/staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.yaml @@ -32,6 +32,7 @@ metadata: spec: activeDeadlineSeconds: -5584804243908071872 backoffLimit: -783752440 + completionMode: 莾簏ì淵歔ųd, completions: 1305381319 manualSelector: true parallelism: 896585016 @@ -1009,13 +1010,14 @@ spec: volumePath: "101" ttlSecondsAfterFinished: 1192652907 status: - active: -1983720493 + active: -1993578228 + completedIndexes: "466" conditions: - - lastProbeTime: "2813-03-11T20:08:42Z" - lastTransitionTime: "2793-11-20T00:30:11Z" + - lastProbeTime: "2823-10-04T11:14:04Z" + lastTransitionTime: "2882-02-07T11:38:45Z" message: "465" reason: "464" - status: 簏ì淵歔ųd,4 - type: "" - failed: 1049326966 - succeeded: -2026748262 + status: Oa2ƒƈɈ达iʍjʒu+,妧縖%Á + type: ;蛡媈U + failed: 165851549 + succeeded: 1971731732 diff --git a/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.json b/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.json index c9df7e54c1ef..82b06266a42c 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.json +++ b/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.json @@ -1522,11 +1522,12 @@ "setHostnameAsFQDN": false } }, - "ttlSecondsAfterFinished": -339602975 + "ttlSecondsAfterFinished": -339602975, + "completionMode": "泐ɻvŰ`Ǧɝ憑ǖ菐u鸚Y髬.ʂmD" } }, - "successfulJobsHistoryLimit": 305459364, - "failedJobsHistoryLimit": -1565042829 + "successfulJobsHistoryLimit": 1380163777, + "failedJobsHistoryLimit": -406189540 }, "status": { "active": [ @@ -1534,7 +1535,7 @@ "kind": "479", "namespace": "480", "name": "481", - "uid": "vÐ仆dždĄ跞肞=ɴ", + "uid": "ɅĀ埰ʀ", "apiVersion": "482", "resourceVersion": "483", "fieldPath": "484" diff --git a/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.pb b/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.pb index 68aaf6d2a79a..333cb33d8e0e 100644 Binary files a/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.pb and b/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.pb differ diff --git a/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.yaml b/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.yaml index 6848d6eefd22..15ca59cac210 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.yaml +++ b/staging/src/k8s.io/api/testdata/HEAD/batch.v1beta1.CronJob.yaml @@ -31,7 +31,7 @@ metadata: uid: "7" spec: concurrencyPolicy: Hr鯹)晿\n") } + if job.Spec.CompletionMode != "" { + w.Write(LEVEL_0, "Completion Mode:\t%s\n", job.Spec.CompletionMode) + } if job.Status.StartTime != nil { w.Write(LEVEL_0, "Start Time:\t%s\n", job.Status.StartTime.Time.Format(time.RFC1123Z)) } @@ -2173,6 +2176,9 @@ func describeJob(job *batchv1.Job, events *corev1.EventList) (string, error) { w.Write(LEVEL_0, "Active Deadline Seconds:\t%ds\n", *job.Spec.ActiveDeadlineSeconds) } w.Write(LEVEL_0, "Pods Statuses:\t%d Running / %d Succeeded / %d Failed\n", job.Status.Active, job.Status.Succeeded, job.Status.Failed) + if job.Spec.CompletionMode == batchv1.IndexedCompletion { + w.Write(LEVEL_0, "Completed Indexes:\t%s\n", capIndexesListOrNone(job.Status.CompletedIndexes, 50)) + } DescribePodTemplate(&job.Spec.Template, w) if events != nil { DescribeEvents(events, w) @@ -2181,6 +2187,22 @@ func describeJob(job *batchv1.Job, events *corev1.EventList) (string, error) { }) } +func capIndexesListOrNone(indexes string, softLimit int) string { + if len(indexes) == 0 { + return "" + } + ix := softLimit + for ; ix < len(indexes); ix++ { + if indexes[ix] == ',' { + break + } + } + if ix >= len(indexes) { + return indexes + } + return indexes[:ix+1] + "..." +} + // CronJobDescriber generates information about a cron job and the jobs it has created. type CronJobDescriber struct { client clientset.Interface diff --git a/staging/src/k8s.io/kubectl/pkg/describe/describe_test.go b/staging/src/k8s.io/kubectl/pkg/describe/describe_test.go index d1e6f169312a..9ec40285aadf 100644 --- a/staging/src/k8s.io/kubectl/pkg/describe/describe_test.go +++ b/staging/src/k8s.io/kubectl/pkg/describe/describe_test.go @@ -2062,6 +2062,88 @@ func TestDescribeDeployment(t *testing.T) { } } +func TestDescribeJob(t *testing.T) { + cases := map[string]struct { + job *batchv1.Job + wantCompletedIndexes string + }{ + "not indexed": { + job: &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + }, + Spec: batchv1.JobSpec{ + CompletionMode: batchv1.NonIndexedCompletion, + }, + }, + }, + "no indexes": { + job: &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + }, + Spec: batchv1.JobSpec{ + CompletionMode: batchv1.IndexedCompletion, + }, + }, + wantCompletedIndexes: "", + }, + "few completed indexes": { + job: &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + }, + Spec: batchv1.JobSpec{ + CompletionMode: batchv1.IndexedCompletion, + }, + Status: batchv1.JobStatus{ + CompletedIndexes: "0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32", + }, + }, + wantCompletedIndexes: "0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32", + }, + "too many completed indexes": { + job: &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + }, + Spec: batchv1.JobSpec{ + CompletionMode: batchv1.IndexedCompletion, + }, + Status: batchv1.JobStatus{ + CompletedIndexes: "0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32-34,36,37", + }, + }, + wantCompletedIndexes: "0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32-34,...", + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + client := &describeClient{ + T: t, + Namespace: tc.job.Namespace, + Interface: fake.NewSimpleClientset(tc.job), + } + describer := JobDescriber{Interface: client} + out, err := describer.Describe(tc.job.Namespace, tc.job.Name, DescriberSettings{ShowEvents: true}) + if err != nil { + t.Fatalf("Unexpected error describing object: %v", err) + } + if tc.wantCompletedIndexes != "" { + if !strings.Contains(out, fmt.Sprintf("Completed Indexes: %s\n", tc.wantCompletedIndexes)) { + t.Errorf("Output didn't contain wanted Completed Indexes:\n%s", out) + } + } else if strings.Contains(out, fmt.Sprintf("Completed Indexes:")) { + t.Errorf("Output contains unexpected completed indexes:\n%s", out) + } + }) + } +} + func TestDescribeIngress(t *testing.T) { backendV1beta1 := networkingv1beta1.IngressBackend{ ServiceName: "default-backend",