Merge pull request #107395 from alculquicondor/indexed-job

Graduate IndexedJob to stable
This commit is contained in:
Kubernetes Prow Robot 2022-03-23 17:44:41 -07:00 committed by GitHub
commit f97825e1ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 72 additions and 141 deletions

View File

@ -4221,7 +4221,7 @@
"type": "integer" "type": "integer"
}, },
"completionMode": { "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.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. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nThis field is beta-level. 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.", "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.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. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nMore completion modes can be added in the future. If the Job controller observes a mode that it doesn't recognize, which is possible during upgrades due to version skew, the controller skips updates for the Job.",
"type": "string" "type": "string"
}, },
"completions": { "completions": {

View File

@ -270,7 +270,7 @@
"type": "integer" "type": "integer"
}, },
"completionMode": { "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.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. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nThis field is beta-level. 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.", "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.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. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nMore completion modes can be added in the future. If the Job controller observes a mode that it doesn't recognize, which is possible during upgrades due to version skew, the controller skips updates for the Job.",
"type": "string" "type": "string"
}, },
"completions": { "completions": {

View File

@ -15,7 +15,7 @@
"type": "integer" "type": "integer"
}, },
"completionMode": { "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.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. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nThis field is beta-level. 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.", "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.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. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nMore completion modes can be added in the future. If the Job controller observes a mode that it doesn't recognize, which is possible during upgrades due to version skew, the controller skips updates for the Job.",
"type": "string" "type": "string"
}, },
"completions": { "completions": {

View File

@ -194,9 +194,10 @@ type JobSpec struct {
// `$(job-name)-$(index)-$(random-string)`, // `$(job-name)-$(index)-$(random-string)`,
// the Pod hostname takes the form `$(job-name)-$(index)`. // the Pod hostname takes the form `$(job-name)-$(index)`.
// //
// This field is beta-level. More completion modes can be added in the future. // More completion modes can be added in the future.
// If the Job controller observes a mode that it doesn't recognize, the // If the Job controller observes a mode that it doesn't recognize, which
// controller skips updates for the Job. // is possible during upgrades due to version skew, the controller
// skips updates for the Job.
// +optional // +optional
CompletionMode *CompletionMode CompletionMode *CompletionMode

View File

@ -19,8 +19,6 @@ package v1
import ( import (
batchv1 "k8s.io/api/batch/v1" batchv1 "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/features"
utilpointer "k8s.io/utils/pointer" utilpointer "k8s.io/utils/pointer"
) )
@ -45,7 +43,7 @@ func SetDefaults_Job(obj *batchv1.Job) {
if labels != nil && len(obj.Labels) == 0 { if labels != nil && len(obj.Labels) == 0 {
obj.Labels = labels obj.Labels = labels
} }
if utilfeature.DefaultFeatureGate.Enabled(features.IndexedJob) && obj.Spec.CompletionMode == nil { if obj.Spec.CompletionMode == nil {
mode := batchv1.NonIndexedCompletion mode := batchv1.NonIndexedCompletion
obj.Spec.CompletionMode = &mode obj.Spec.CompletionMode = &mode
} }

View File

@ -25,12 +25,9 @@ import (
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/api/legacyscheme"
_ "k8s.io/kubernetes/pkg/apis/batch/install" _ "k8s.io/kubernetes/pkg/apis/batch/install"
_ "k8s.io/kubernetes/pkg/apis/core/install" _ "k8s.io/kubernetes/pkg/apis/core/install"
"k8s.io/kubernetes/pkg/features"
"k8s.io/utils/pointer" "k8s.io/utils/pointer"
. "k8s.io/kubernetes/pkg/apis/batch/v1" . "k8s.io/kubernetes/pkg/apis/batch/v1"
@ -39,31 +36,11 @@ import (
func TestSetDefaultJob(t *testing.T) { func TestSetDefaultJob(t *testing.T) {
defaultLabels := map[string]string{"default": "default"} defaultLabels := map[string]string{"default": "default"}
tests := map[string]struct { tests := map[string]struct {
indexedJobEnabled bool
original *batchv1.Job original *batchv1.Job
expected *batchv1.Job expected *batchv1.Job
expectLabels bool expectLabels bool
}{ }{
"All unspecified -> sets all to default values": { "All unspecified -> sets all to default values": {
original: &batchv1.Job{
Spec: batchv1.JobSpec{
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),
Suspend: pointer.BoolPtr(false),
},
},
expectLabels: true,
},
"All unspecified, indexed job enabled -> sets all to default values": {
indexedJobEnabled: true,
original: &batchv1.Job{ original: &batchv1.Job{
Spec: batchv1.JobSpec{ Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{ Template: v1.PodTemplateSpec{
@ -95,6 +72,7 @@ func TestSetDefaultJob(t *testing.T) {
Completions: pointer.Int32Ptr(1), Completions: pointer.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(1), Parallelism: pointer.Int32Ptr(1),
BackoffLimit: pointer.Int32Ptr(6), BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: completionModePtr(batchv1.NonIndexedCompletion),
Suspend: pointer.BoolPtr(false), Suspend: pointer.BoolPtr(false),
}, },
}, },
@ -114,6 +92,7 @@ func TestSetDefaultJob(t *testing.T) {
Completions: pointer.Int32Ptr(1), Completions: pointer.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(1), Parallelism: pointer.Int32Ptr(1),
BackoffLimit: pointer.Int32Ptr(6), BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: completionModePtr(batchv1.NonIndexedCompletion),
Suspend: pointer.BoolPtr(true), Suspend: pointer.BoolPtr(true),
}, },
}, },
@ -135,6 +114,7 @@ func TestSetDefaultJob(t *testing.T) {
Completions: pointer.Int32Ptr(1), Completions: pointer.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(1), Parallelism: pointer.Int32Ptr(1),
BackoffLimit: pointer.Int32Ptr(6), BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: completionModePtr(batchv1.NonIndexedCompletion),
Suspend: pointer.BoolPtr(false), Suspend: pointer.BoolPtr(false),
}, },
}, },
@ -152,6 +132,7 @@ func TestSetDefaultJob(t *testing.T) {
Spec: batchv1.JobSpec{ Spec: batchv1.JobSpec{
Parallelism: pointer.Int32Ptr(0), Parallelism: pointer.Int32Ptr(0),
BackoffLimit: pointer.Int32Ptr(6), BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: completionModePtr(batchv1.NonIndexedCompletion),
Suspend: pointer.BoolPtr(false), Suspend: pointer.BoolPtr(false),
}, },
}, },
@ -170,6 +151,7 @@ func TestSetDefaultJob(t *testing.T) {
Spec: batchv1.JobSpec{ Spec: batchv1.JobSpec{
Parallelism: pointer.Int32Ptr(2), Parallelism: pointer.Int32Ptr(2),
BackoffLimit: pointer.Int32Ptr(6), BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: completionModePtr(batchv1.NonIndexedCompletion),
Suspend: pointer.BoolPtr(false), Suspend: pointer.BoolPtr(false),
}, },
}, },
@ -189,6 +171,7 @@ func TestSetDefaultJob(t *testing.T) {
Completions: pointer.Int32Ptr(2), Completions: pointer.Int32Ptr(2),
Parallelism: pointer.Int32Ptr(1), Parallelism: pointer.Int32Ptr(1),
BackoffLimit: pointer.Int32Ptr(6), BackoffLimit: pointer.Int32Ptr(6),
CompletionMode: completionModePtr(batchv1.NonIndexedCompletion),
Suspend: pointer.BoolPtr(false), Suspend: pointer.BoolPtr(false),
}, },
}, },
@ -208,6 +191,7 @@ func TestSetDefaultJob(t *testing.T) {
Completions: pointer.Int32Ptr(1), Completions: pointer.Int32Ptr(1),
Parallelism: pointer.Int32Ptr(1), Parallelism: pointer.Int32Ptr(1),
BackoffLimit: pointer.Int32Ptr(5), BackoffLimit: pointer.Int32Ptr(5),
CompletionMode: completionModePtr(batchv1.NonIndexedCompletion),
Suspend: pointer.BoolPtr(false), Suspend: pointer.BoolPtr(false),
}, },
}, },
@ -268,8 +252,6 @@ func TestSetDefaultJob(t *testing.T) {
for name, test := range tests { for name, test := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.IndexedJob, test.indexedJobEnabled)()
original := test.original original := test.original
expected := test.expected expected := test.expected
obj2 := roundTrip(t, runtime.Object(original)) obj2 := roundTrip(t, runtime.Object(original))

View File

@ -634,11 +634,6 @@ func (jm *Controller) syncJob(ctx context.Context, key string) (forget bool, rEr
return true, nil return true, nil
} }
// Cannot create Pods if this is an Indexed Job and the feature is disabled.
if !feature.DefaultFeatureGate.Enabled(features.IndexedJob) && isIndexedJob(&job) {
jm.recorder.Event(&job, v1.EventTypeWarning, "IndexedJobDisabled", "Skipped Indexed Job sync because feature is disabled.")
return false, nil
}
if job.Spec.CompletionMode != nil && *job.Spec.CompletionMode != batch.NonIndexedCompletion && *job.Spec.CompletionMode != batch.IndexedCompletion { if job.Spec.CompletionMode != nil && *job.Spec.CompletionMode != batch.NonIndexedCompletion && *job.Spec.CompletionMode != batch.IndexedCompletion {
jm.recorder.Event(&job, v1.EventTypeWarning, "UnknownCompletionMode", "Skipped Job sync because completion mode is unknown") jm.recorder.Event(&job, v1.EventTypeWarning, "UnknownCompletionMode", "Skipped Job sync because completion mode is unknown")
return false, nil return false, nil

View File

@ -224,7 +224,6 @@ func TestControllerSyncJob(t *testing.T) {
expectedPodPatches int expectedPodPatches int
// features // features
indexedJobEnabled bool
jobReadyPodsEnabled bool jobReadyPodsEnabled bool
}{ }{
"job start": { "job start": {
@ -490,7 +489,6 @@ func TestControllerSyncJob(t *testing.T) {
expectedCreations: 2, expectedCreations: 2,
expectedActive: 2, expectedActive: 2,
expectedCreatedIndexes: sets.NewInt(0, 1), expectedCreatedIndexes: sets.NewInt(0, 1),
indexedJobEnabled: true,
}, },
"indexed job completed": { "indexed job completed": {
parallelism: 2, parallelism: 2,
@ -510,7 +508,6 @@ func TestControllerSyncJob(t *testing.T) {
expectedCondition: &jobConditionComplete, expectedCondition: &jobConditionComplete,
expectedConditionStatus: v1.ConditionTrue, expectedConditionStatus: v1.ConditionTrue,
expectedPodPatches: 4, expectedPodPatches: 4,
indexedJobEnabled: true,
}, },
"indexed job repeated completed index": { "indexed job repeated completed index": {
parallelism: 2, parallelism: 2,
@ -529,7 +526,6 @@ func TestControllerSyncJob(t *testing.T) {
expectedCompletedIdxs: "0,1", expectedCompletedIdxs: "0,1",
expectedCreatedIndexes: sets.NewInt(2), expectedCreatedIndexes: sets.NewInt(2),
expectedPodPatches: 3, expectedPodPatches: 3,
indexedJobEnabled: true,
}, },
"indexed job some running and completed pods": { "indexed job some running and completed pods": {
parallelism: 8, parallelism: 8,
@ -553,7 +549,6 @@ func TestControllerSyncJob(t *testing.T) {
expectedCompletedIdxs: "2,4,5,7-9", expectedCompletedIdxs: "2,4,5,7-9",
expectedCreatedIndexes: sets.NewInt(1, 6, 10, 11, 12, 13), expectedCreatedIndexes: sets.NewInt(1, 6, 10, 11, 12, 13),
expectedPodPatches: 6, expectedPodPatches: 6,
indexedJobEnabled: true,
}, },
"indexed job some failed pods": { "indexed job some failed pods": {
parallelism: 3, parallelism: 3,
@ -570,7 +565,6 @@ func TestControllerSyncJob(t *testing.T) {
expectedFailed: 2, expectedFailed: 2,
expectedCreatedIndexes: sets.NewInt(0, 2), expectedCreatedIndexes: sets.NewInt(0, 2),
expectedPodPatches: 2, expectedPodPatches: 2,
indexedJobEnabled: true,
}, },
"indexed job some pods without index": { "indexed job some pods without index": {
parallelism: 2, parallelism: 2,
@ -596,7 +590,6 @@ func TestControllerSyncJob(t *testing.T) {
expectedFailed: 0, expectedFailed: 0,
expectedCompletedIdxs: "0", expectedCompletedIdxs: "0",
expectedPodPatches: 8, expectedPodPatches: 8,
indexedJobEnabled: true,
}, },
"indexed job repeated indexes": { "indexed job repeated indexes": {
parallelism: 5, parallelism: 5,
@ -619,7 +612,6 @@ func TestControllerSyncJob(t *testing.T) {
expectedSucceeded: 1, expectedSucceeded: 1,
expectedCompletedIdxs: "0", expectedCompletedIdxs: "0",
expectedPodPatches: 5, expectedPodPatches: 5,
indexedJobEnabled: true,
}, },
"indexed job with indexes outside of range": { "indexed job with indexes outside of range": {
parallelism: 2, parallelism: 2,
@ -641,19 +633,6 @@ func TestControllerSyncJob(t *testing.T) {
expectedActive: 0, expectedActive: 0,
expectedFailed: 0, expectedFailed: 0,
expectedPodPatches: 5, expectedPodPatches: 5,
indexedJobEnabled: true,
},
"indexed job feature disabled": {
parallelism: 2,
completions: 3,
backoffLimit: 6,
completionMode: batch.IndexedCompletion,
podsWithIndexes: []indexPhase{
{"0", v1.PodRunning},
{"1", v1.PodSucceeded},
},
// No status updates.
indexedJobEnabled: false,
}, },
"suspending a job with satisfied expectations": { "suspending a job with satisfied expectations": {
// Suspended Job should delete active pods when expectations are // Suspended Job should delete active pods when expectations are
@ -727,7 +706,6 @@ func TestControllerSyncJob(t *testing.T) {
if wFinalizers && tc.podControllerError != nil { if wFinalizers && tc.podControllerError != nil {
t.Skip("Can't track status if finalizers can't be removed") t.Skip("Can't track status if finalizers can't be removed")
} }
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.IndexedJob, tc.indexedJobEnabled)()
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobReadyPods, tc.jobReadyPodsEnabled)() defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobReadyPods, tc.jobReadyPodsEnabled)()
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobTrackingWithFinalizers, wFinalizers)() defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobTrackingWithFinalizers, wFinalizers)()
@ -857,7 +835,7 @@ func TestControllerSyncJob(t *testing.T) {
if actual.Status.StartTime != nil && tc.suspend { if actual.Status.StartTime != nil && tc.suspend {
t.Error("Unexpected .status.startTime not nil when suspend is true") t.Error("Unexpected .status.startTime not nil when suspend is true")
} }
if actual.Status.StartTime == nil && tc.indexedJobEnabled && !tc.suspend { if actual.Status.StartTime == nil && !tc.suspend {
t.Error("Missing .status.startTime") t.Error("Missing .status.startTime")
} }
// validate conditions // validate conditions

View File

@ -215,6 +215,7 @@ const (
// owner: @alculquicondor // owner: @alculquicondor
// alpha: v1.21 // alpha: v1.21
// beta: v1.22 // beta: v1.22
// stable: v1.24
// //
// Allows Job controller to manage Pod completions per completion index. // Allows Job controller to manage Pod completions per completion index.
IndexedJob featuregate.Feature = "IndexedJob" IndexedJob featuregate.Feature = "IndexedJob"
@ -891,7 +892,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
NetworkPolicyEndPort: {Default: true, PreRelease: featuregate.Beta}, NetworkPolicyEndPort: {Default: true, PreRelease: featuregate.Beta},
ProcMountType: {Default: false, PreRelease: featuregate.Alpha}, ProcMountType: {Default: false, PreRelease: featuregate.Alpha},
TTLAfterFinished: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.25 TTLAfterFinished: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.25
IndexedJob: {Default: true, PreRelease: featuregate.Beta}, IndexedJob: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.26
JobTrackingWithFinalizers: {Default: true, PreRelease: featuregate.Beta}, JobTrackingWithFinalizers: {Default: true, PreRelease: featuregate.Beta},
JobReadyPods: {Default: false, PreRelease: featuregate.Alpha}, JobReadyPods: {Default: false, PreRelease: featuregate.Alpha},
KubeletPodResources: {Default: true, PreRelease: featuregate.Beta}, KubeletPodResources: {Default: true, PreRelease: featuregate.Beta},

View File

@ -12973,7 +12973,7 @@ func schema_k8sio_api_batch_v1_JobSpec(ref common.ReferenceCallback) common.Open
}, },
"completionMode": { "completionMode": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
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.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. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nThis field is beta-level. 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.", 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.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. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nMore completion modes can be added in the future. If the Job controller observes a mode that it doesn't recognize, which is possible during upgrades due to version skew, the controller skips updates for the Job.",
Type: []string{"string"}, Type: []string{"string"},
Format: "", Format: "",
}, },

View File

@ -93,10 +93,6 @@ func (jobStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
job.Generation = 1 job.Generation = 1
if !utilfeature.DefaultFeatureGate.Enabled(features.IndexedJob) {
job.Spec.CompletionMode = nil
}
if utilfeature.DefaultFeatureGate.Enabled(features.JobTrackingWithFinalizers) { if utilfeature.DefaultFeatureGate.Enabled(features.JobTrackingWithFinalizers) {
// Until this feature graduates to GA and soaks in clusters, we use an // Until this feature graduates to GA and soaks in clusters, we use an
// annotation to mark whether jobs are tracked with it. // annotation to mark whether jobs are tracked with it.
@ -133,10 +129,6 @@ func (jobStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object
oldJob := old.(*batch.Job) oldJob := old.(*batch.Job)
newJob.Status = oldJob.Status newJob.Status = oldJob.Status
if !utilfeature.DefaultFeatureGate.Enabled(features.IndexedJob) && oldJob.Spec.CompletionMode == nil {
newJob.Spec.CompletionMode = nil
}
if !utilfeature.DefaultFeatureGate.Enabled(features.JobTrackingWithFinalizers) && !hasJobTrackingAnnotation(oldJob) { if !utilfeature.DefaultFeatureGate.Enabled(features.JobTrackingWithFinalizers) && !hasJobTrackingAnnotation(oldJob) {
dropJobTrackingAnnotation(newJob) dropJobTrackingAnnotation(newJob)
} }

View File

@ -42,20 +42,15 @@ var ignoreErrValueDetail = cmpopts.IgnoreFields(field.Error{}, "BadValue", "Deta
func TestJobStrategy(t *testing.T) { func TestJobStrategy(t *testing.T) {
cases := map[string]struct { cases := map[string]struct {
indexedJobEnabled bool
trackingWithFinalizersEnabled bool trackingWithFinalizersEnabled bool
}{ }{
"features disabled": {}, "features disabled": {},
"indexed job enabled": {
indexedJobEnabled: true,
},
"new job tracking enabled": { "new job tracking enabled": {
trackingWithFinalizersEnabled: true, trackingWithFinalizersEnabled: true,
}, },
} }
for name, tc := range cases { for name, tc := range cases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IndexedJob, tc.indexedJobEnabled)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobTrackingWithFinalizers, tc.trackingWithFinalizersEnabled)() defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.JobTrackingWithFinalizers, tc.trackingWithFinalizersEnabled)()
testJobStrategy(t) testJobStrategy(t)
}) })
@ -63,7 +58,6 @@ func TestJobStrategy(t *testing.T) {
} }
func testJobStrategy(t *testing.T) { func testJobStrategy(t *testing.T) {
indexedJobEnabled := utilfeature.DefaultFeatureGate.Enabled(features.IndexedJob)
trackingWithFinalizersEnabled := utilfeature.DefaultFeatureGate.Enabled(features.JobTrackingWithFinalizers) trackingWithFinalizersEnabled := utilfeature.DefaultFeatureGate.Enabled(features.JobTrackingWithFinalizers)
ctx := genericapirequest.NewDefaultContext() ctx := genericapirequest.NewDefaultContext()
if !Strategy.NamespaceScoped() { if !Strategy.NamespaceScoped() {
@ -121,8 +115,8 @@ func testJobStrategy(t *testing.T) {
if len(errs) != 0 { if len(errs) != 0 {
t.Errorf("Unexpected error validating %v", errs) t.Errorf("Unexpected error validating %v", errs)
} }
if indexedJobEnabled != (job.Spec.CompletionMode != nil) { if job.Spec.CompletionMode == nil {
t.Errorf("Job should allow setting .spec.completionMode only when %v feature is enabled", features.IndexedJob) t.Errorf("Job should allow setting .spec.completionMode")
} }
wantAnnotations := map[string]string{"foo": "bar"} wantAnnotations := map[string]string{"foo": "bar"}
if trackingWithFinalizersEnabled { if trackingWithFinalizersEnabled {

View File

@ -248,9 +248,10 @@ message JobSpec {
// `$(job-name)-$(index)-$(random-string)`, // `$(job-name)-$(index)-$(random-string)`,
// the Pod hostname takes the form `$(job-name)-$(index)`. // the Pod hostname takes the form `$(job-name)-$(index)`.
// //
// This field is beta-level. More completion modes can be added in the future. // More completion modes can be added in the future.
// If the Job controller observes a mode that it doesn't recognize, the // If the Job controller observes a mode that it doesn't recognize, which
// controller skips updates for the Job. // is possible during upgrades due to version skew, the controller
// skips updates for the Job.
// +optional // +optional
optional string completionMode = 9; optional string completionMode = 9;

View File

@ -176,9 +176,10 @@ type JobSpec struct {
// `$(job-name)-$(index)-$(random-string)`, // `$(job-name)-$(index)-$(random-string)`,
// the Pod hostname takes the form `$(job-name)-$(index)`. // the Pod hostname takes the form `$(job-name)-$(index)`.
// //
// This field is beta-level. More completion modes can be added in the future. // More completion modes can be added in the future.
// If the Job controller observes a mode that it doesn't recognize, the // If the Job controller observes a mode that it doesn't recognize, which
// controller skips updates for the Job. // is possible during upgrades due to version skew, the controller
// skips updates for the Job.
// +optional // +optional
CompletionMode *CompletionMode `json:"completionMode,omitempty" protobuf:"bytes,9,opt,name=completionMode,casttype=CompletionMode"` CompletionMode *CompletionMode `json:"completionMode,omitempty" protobuf:"bytes,9,opt,name=completionMode,casttype=CompletionMode"`

View File

@ -119,7 +119,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", "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/", "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.", "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.",
"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.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. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nThis field is beta-level. 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.", "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.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. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nMore completion modes can be added in the future. If the Job controller observes a mode that it doesn't recognize, which is possible during upgrades due to version skew, 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. Defaults to false.", "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. Defaults to false.",
} }

View File

@ -763,6 +763,13 @@
pod. Modify the labels of one of the Job's Pods. The Job MUST release the Pod. pod. Modify the labels of one of the Job's Pods. The Job MUST release the Pod.
release: v1.16 release: v1.16
file: test/e2e/apps/job.go file: test/e2e/apps/job.go
- testname: Ensure Pods of an Indexed Job get a unique index.
codename: '[sig-apps] Job should create pods for an Indexed job with completion
indexes and specified hostname [Conformance]'
description: Create an Indexed job. Job MUST complete successfully. Ensure that
created pods have completion index annotation and environment variable.
release: v1.24
file: test/e2e/apps/job.go
- testname: Jobs, active pods, graceful termination - testname: Jobs, active pods, graceful termination
codename: '[sig-apps] Job should delete a job [Conformance]' codename: '[sig-apps] Job should delete a job [Conformance]'
description: Create a job. Ensure the active pods reflect paralellism in the namespace description: Create a job. Ensure the active pods reflect paralellism in the namespace

View File

@ -150,10 +150,12 @@ var _ = SIGDescribe("Job", func() {
}) })
/* /*
Testcase: Ensure Pods of an Indexed Job get a unique index. Release: v1.24
Description: Create an Indexed Job, wait for completion, capture the output of the pods and verify that they contain the completion index. Testname: Ensure Pods of an Indexed Job get a unique index.
Description: Create an Indexed job. Job MUST complete successfully.
Ensure that created pods have completion index annotation and environment variable.
*/ */
ginkgo.It("should create pods for an Indexed job with completion indexes and specified hostname", func() { framework.ConformanceIt("should create pods for an Indexed job with completion indexes and specified hostname", func() {
ginkgo.By("Creating Indexed job") ginkgo.By("Creating Indexed job")
job := e2ejob.NewTestJob("succeed", "indexed-job", v1.RestartPolicyNever, parallelism, completions, nil, backoffLimit) job := e2ejob.NewTestJob("succeed", "indexed-job", v1.RestartPolicyNever, parallelism, completions, nil, backoffLimit)
mode := batchv1.IndexedCompletion mode := batchv1.IndexedCompletion

View File

@ -373,7 +373,6 @@ func TestIndexedJob(t *testing.T) {
for _, wFinalizers := range []bool{false, true} { for _, wFinalizers := range []bool{false, true} {
t.Run(fmt.Sprintf("finalizers=%t", wFinalizers), func(t *testing.T) { t.Run(fmt.Sprintf("finalizers=%t", wFinalizers), func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobTrackingWithFinalizers, wFinalizers)() defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobTrackingWithFinalizers, wFinalizers)()
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.IndexedJob, true)()
closeFn, restConfig, clientSet, ns := setup(t, "indexed") closeFn, restConfig, clientSet, ns := setup(t, "indexed")
defer closeFn() defer closeFn()
@ -411,30 +410,10 @@ func TestIndexedJob(t *testing.T) {
}, wFinalizers) }, wFinalizers)
validateIndexedJobPods(ctx, t, clientSet, jobObj, sets.NewInt(0, 2, 3), "1") validateIndexedJobPods(ctx, t, clientSet, jobObj, sets.NewInt(0, 2, 3), "1")
// Disable feature gate and restart controller. // One Pod fails, which should be recreated.
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.IndexedJob, false)()
cancel()
ctx, cancel = startJobController(restConfig, clientSet)
events, err := clientSet.EventsV1().Events(ns.Name).Watch(ctx, metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
defer events.Stop()
// One Pod fails, but no recreations happen because feature is disabled.
if err := setJobPhaseForIndex(ctx, clientSet, jobObj, v1.PodFailed, 2); err != nil { if err := setJobPhaseForIndex(ctx, clientSet, jobObj, v1.PodFailed, 2); err != nil {
t.Fatal("Failed trying to succeed pod with index 2") t.Fatal("Failed trying to succeed pod with index 2")
} }
if err := waitForEvent(events, jobObj.UID, "IndexedJobDisabled"); err != nil {
t.Errorf("Waiting for an event for IndexedJobDisabled: %v", err)
}
validateIndexedJobPods(ctx, t, clientSet, jobObj, sets.NewInt(0, 3), "1")
// Re-enable feature gate and restart controller. Failed Pod should be recreated now.
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.IndexedJob, true)()
cancel()
ctx, cancel = startJobController(restConfig, clientSet)
validateJobPodsStatus(ctx, t, clientSet, jobObj, podsByStatus{ validateJobPodsStatus(ctx, t, clientSet, jobObj, podsByStatus{
Active: 3, Active: 3,
Failed: 1, Failed: 1,