Skip to content

Commit

Permalink
Merge pull request #127402 from mimowo/managed-by-beta-update
Browse files Browse the repository at this point in the history
Graduate JobManagedBy to Beta in 1.32
  • Loading branch information
k8s-ci-robot authored Oct 17, 2024
2 parents c5a85ab + ec983bd commit 51f76fe
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 10 deletions.
2 changes: 1 addition & 1 deletion api/openapi-spec/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/openapi-spec/v3/apis__batch__v1_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@
"type": "integer"
},
"managedBy": {
"description": "ManagedBy field indicates the controller that manages a Job. The k8s Job controller reconciles jobs which don't have this field at all or the field value is the reserved string `kubernetes.io/job-controller`, but skips reconciling Jobs with a custom value for this field. The value must be a valid domain-prefixed path (e.g. acme.io/foo) - all characters before the first \"/\" must be a valid subdomain as defined by RFC 1123. All characters trailing the first \"/\" must be valid HTTP Path characters as defined by RFC 3986. The value cannot exceed 63 characters. This field is immutable.\n\nThis field is alpha-level. The job controller accepts setting the field when the feature gate JobManagedBy is enabled (disabled by default).",
"description": "ManagedBy field indicates the controller that manages a Job. The k8s Job controller reconciles jobs which don't have this field at all or the field value is the reserved string `kubernetes.io/job-controller`, but skips reconciling Jobs with a custom value for this field. The value must be a valid domain-prefixed path (e.g. acme.io/foo) - all characters before the first \"/\" must be a valid subdomain as defined by RFC 1123. All characters trailing the first \"/\" must be valid HTTP Path characters as defined by RFC 3986. The value cannot exceed 63 characters. This field is immutable.\n\nThis field is beta-level. The job controller accepts setting the field when the feature gate JobManagedBy is enabled (enabled by default).",
"type": "string"
},
"manualSelector": {
Expand Down
4 changes: 2 additions & 2 deletions pkg/apis/batch/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,8 +477,8 @@ type JobSpec struct {
// characters as defined by RFC 3986. The value cannot exceed 63 characters.
// This field is immutable.
//
// This field is alpha-level. The job controller accepts setting the field
// when the feature gate JobManagedBy is enabled (disabled by default).
// This field is beta-level. The job controller accepts setting the field
// when the feature gate JobManagedBy is enabled (enabled by default).
// +optional
ManagedBy *string
}
Expand Down
102 changes: 102 additions & 0 deletions pkg/controller/job/job_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3087,6 +3087,7 @@ func TestSyncJobWithJobPodFailurePolicy(t *testing.T) {

testCases := map[string]struct {
enableJobPodReplacementPolicy bool
enableJobManagedBy bool
job batch.Job
pods []v1.Pod
wantConditions []batch.JobCondition
Expand Down Expand Up @@ -3420,6 +3421,56 @@ func TestSyncJobWithJobPodFailurePolicy(t *testing.T) {
wantStatusFailed: 2,
wantStatusSucceeded: 0,
},
"fail job with multiple pods; JobManagedBy enabled delays setting terminal condition": {
enableJobManagedBy: true,
job: batch.Job{
TypeMeta: metav1.TypeMeta{Kind: "Job"},
ObjectMeta: validObjectMeta,
Spec: batch.JobSpec{
Selector: validSelector,
Template: validTemplate,
Parallelism: ptr.To[int32](2),
Completions: ptr.To[int32](2),
BackoffLimit: ptr.To[int32](6),
PodFailurePolicy: &batch.PodFailurePolicy{
Rules: onExitCodeRules,
},
},
},
pods: []v1.Pod{
{
Status: v1.PodStatus{
Phase: v1.PodRunning,
},
},
{
Status: v1.PodStatus{
Phase: v1.PodFailed,
ContainerStatuses: []v1.ContainerStatus{
{
Name: "main-container",
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
ExitCode: 5,
},
},
},
},
},
},
},
wantConditions: []batch.JobCondition{
{
Type: batch.JobFailureTarget,
Status: v1.ConditionTrue,
Reason: batch.JobReasonPodFailurePolicy,
Message: "Container main-container for pod default/mypod-1 failed with exit code 5 matching FailJob rule at index 1",
},
},
wantStatusActive: 0,
wantStatusFailed: 2,
wantStatusSucceeded: 0,
},
"fail indexed job based on OnExitCodes": {
job: batch.Job{
TypeMeta: metav1.TypeMeta{Kind: "Job"},
Expand Down Expand Up @@ -3759,6 +3810,56 @@ func TestSyncJobWithJobPodFailurePolicy(t *testing.T) {
wantStatusFailed: 1,
wantStatusSucceeded: 0,
},
"default job based on OnExitCodes; JobManagedBy enabled triggers adding interim condition": {
enableJobManagedBy: true,
job: batch.Job{
TypeMeta: metav1.TypeMeta{Kind: "Job"},
ObjectMeta: validObjectMeta,
Spec: batch.JobSpec{
Selector: validSelector,
Template: validTemplate,
Parallelism: ptr.To[int32](1),
Completions: ptr.To[int32](1),
BackoffLimit: ptr.To[int32](0),
PodFailurePolicy: &batch.PodFailurePolicy{
Rules: onExitCodeRules,
},
},
},
pods: []v1.Pod{
{
Status: v1.PodStatus{
Phase: v1.PodFailed,
ContainerStatuses: []v1.ContainerStatus{
{
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
ExitCode: 10,
},
},
},
},
},
},
},
wantConditions: []batch.JobCondition{
{
Type: batch.JobFailureTarget,
Status: v1.ConditionTrue,
Reason: batch.JobReasonBackoffLimitExceeded,
Message: "Job has reached the specified backoff limit",
},
{
Type: batch.JobFailed,
Status: v1.ConditionTrue,
Reason: batch.JobReasonBackoffLimitExceeded,
Message: "Job has reached the specified backoff limit",
},
},
wantStatusActive: 0,
wantStatusFailed: 1,
wantStatusSucceeded: 0,
},
"count pod failure based on OnExitCodes; both rules are matching, the first is executed only": {
job: batch.Job{
TypeMeta: metav1.TypeMeta{Kind: "Job"},
Expand Down Expand Up @@ -4057,6 +4158,7 @@ func TestSyncJobWithJobPodFailurePolicy(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobPodReplacementPolicy, tc.enableJobPodReplacementPolicy)
featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobManagedBy, tc.enableJobManagedBy)

if tc.job.Spec.PodReplacementPolicy == nil {
tc.job.Spec.PodReplacementPolicy = podReplacementPolicy(batch.Failed)
Expand Down
2 changes: 2 additions & 0 deletions pkg/features/kube_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ const (

// owner: @mimowo
// kep: https://kep.k8s.io/4368
// alpha: v1.30
// beta: v1.32
//
// Allows to delegate reconciliation of a Job object to an external controller.
JobManagedBy featuregate.Feature = "JobManagedBy"
Expand Down
1 change: 1 addition & 0 deletions pkg/features/versioned_kube_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate

JobManagedBy: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
},

JobPodFailurePolicy: {
Expand Down
2 changes: 1 addition & 1 deletion pkg/generated/openapi/zz_generated.openapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions staging/src/k8s.io/api/batch/v1/generated.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions staging/src/k8s.io/api/batch/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,8 @@ type JobSpec struct {
// characters as defined by RFC 3986. The value cannot exceed 63 characters.
// This field is immutable.
//
// This field is alpha-level. The job controller accepts setting the field
// when the feature gate JobManagedBy is enabled (disabled by default).
// This field is beta-level. The job controller accepts setting the field
// when the feature gate JobManagedBy is enabled (enabled by default).
// +optional
ManagedBy *string `json:"managedBy,omitempty" protobuf:"bytes,15,opt,name=managedBy"`
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions test/e2e/apps/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"math"
"strconv"
"time"

batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -1237,6 +1238,31 @@ done`}
err = e2ejob.WaitForJobReady(ctx, f.ClientSet, f.Namespace.Name, job.Name, ptr.To[int32](0))
framework.ExpectNoError(err, "failed to ensure job status ready field in namespace: %s", f.Namespace.Name)
})

/*
Testname: Jobs, managed-by mechanism
Description: This test verifies the built-in Job controller does not
reconcile the Job, allowing to delegate the reconciliation to an
external controller.
*/
ginkgo.It("should allow to delegate reconciliation to external controller", func(ctx context.Context) {
parallelism := int32(2)
completions := int32(4)
backoffLimit := int32(6)

ginkgo.By("Creating a job with a custom managed-by field")
job := e2ejob.NewTestJob("succeed", "managed-by", v1.RestartPolicyNever, parallelism, completions, nil, backoffLimit)
job.Spec.ManagedBy = ptr.To("example.com/custom-job-controller")
job, err := e2ejob.CreateJob(ctx, f.ClientSet, f.Namespace.Name, job)
framework.ExpectNoError(err, "failed to create job in namespace: %s/%s", job.Namespace, job.Name)

ginkgo.By(fmt.Sprintf("Verify the Job %s/%s status isn't updated by the built-in controller", job.Namespace, job.Name))
// This get function uses HandleRetry to retry on transient API errors
get := framework.GetObject(f.ClientSet.BatchV1().Jobs(f.Namespace.Name).Get, job.Name, metav1.GetOptions{})
gomega.Consistently(ctx, get).
WithPolling(time.Second).WithTimeout(3 * time.Second).
Should(gomega.HaveField("Status", gomega.BeEquivalentTo(batchv1.JobStatus{})))
})
})

func updateJobSuspendWithRetries(ctx context.Context, f *framework.Framework, job *batchv1.Job, suspend *bool) error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,10 @@
lockToDefault: false
preRelease: Alpha
version: "1.30"
- default: true
lockToDefault: false
preRelease: Beta
version: "1.32"
- name: JobPodFailurePolicy
versionedSpecs:
- default: false
Expand Down

0 comments on commit 51f76fe

Please sign in to comment.