k8s.io/kubernetes@v1.29.3/test/e2e/apps/ttl_after_finished.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package apps 18 19 import ( 20 "context" 21 "fmt" 22 "time" 23 24 batchv1 "k8s.io/api/batch/v1" 25 v1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/util/wait" 28 clientset "k8s.io/client-go/kubernetes" 29 "k8s.io/kubernetes/pkg/util/slice" 30 "k8s.io/kubernetes/test/e2e/framework" 31 e2ejob "k8s.io/kubernetes/test/e2e/framework/job" 32 admissionapi "k8s.io/pod-security-admission/api" 33 34 "github.com/onsi/ginkgo/v2" 35 "github.com/onsi/gomega" 36 ) 37 38 const ( 39 dummyFinalizer = "k8s.io/dummy-finalizer" 40 41 // JobTimeout is how long to wait for a job to finish. 42 JobTimeout = 15 * time.Minute 43 ) 44 45 var _ = SIGDescribe("TTLAfterFinished", func() { 46 f := framework.NewDefaultFramework("ttlafterfinished") 47 f.NamespacePodSecurityLevel = admissionapi.LevelBaseline 48 49 ginkgo.It("job should be deleted once it finishes after TTL seconds", func(ctx context.Context) { 50 testFinishedJob(ctx, f) 51 }) 52 }) 53 54 func cleanupJob(ctx context.Context, f *framework.Framework, job *batchv1.Job) { 55 ns := f.Namespace.Name 56 c := f.ClientSet 57 58 framework.Logf("Remove the Job's dummy finalizer; the Job should be deleted cascadingly") 59 removeFinalizerFunc := func(j *batchv1.Job) { 60 j.ObjectMeta.Finalizers = slice.RemoveString(j.ObjectMeta.Finalizers, dummyFinalizer, nil) 61 } 62 _, err := updateJobWithRetries(ctx, c, ns, job.Name, removeFinalizerFunc) 63 framework.ExpectNoError(err) 64 e2ejob.WaitForJobGone(ctx, c, ns, job.Name, wait.ForeverTestTimeout) 65 66 err = e2ejob.WaitForAllJobPodsGone(ctx, c, ns, job.Name) 67 framework.ExpectNoError(err) 68 } 69 70 func testFinishedJob(ctx context.Context, f *framework.Framework) { 71 ns := f.Namespace.Name 72 c := f.ClientSet 73 74 parallelism := int32(1) 75 completions := int32(1) 76 backoffLimit := int32(2) 77 ttl := int32(10) 78 79 job := e2ejob.NewTestJob("randomlySucceedOrFail", "rand-non-local", v1.RestartPolicyNever, parallelism, completions, nil, backoffLimit) 80 job.Spec.TTLSecondsAfterFinished = &ttl 81 job.ObjectMeta.Finalizers = []string{dummyFinalizer} 82 ginkgo.DeferCleanup(cleanupJob, f, job) 83 84 framework.Logf("Create a Job %s/%s with TTL", ns, job.Name) 85 job, err := e2ejob.CreateJob(ctx, c, ns, job) 86 framework.ExpectNoError(err) 87 88 framework.Logf("Wait for the Job to finish") 89 err = e2ejob.WaitForJobFinish(ctx, c, ns, job.Name) 90 framework.ExpectNoError(err) 91 92 framework.Logf("Wait for TTL after finished controller to delete the Job") 93 err = waitForJobDeleting(ctx, c, ns, job.Name) 94 framework.ExpectNoError(err) 95 96 framework.Logf("Check Job's deletionTimestamp and compare with the time when the Job finished") 97 job, err = e2ejob.GetJob(ctx, c, ns, job.Name) 98 framework.ExpectNoError(err) 99 jobFinishTime := finishTime(job) 100 finishTimeUTC := jobFinishTime.UTC() 101 if jobFinishTime.IsZero() { 102 framework.Fail("Expected job finish time not to be zero.") 103 } 104 105 deleteAtUTC := job.ObjectMeta.DeletionTimestamp.UTC() 106 gomega.Expect(deleteAtUTC).NotTo(gomega.BeNil()) 107 108 expireAtUTC := finishTimeUTC.Add(time.Duration(ttl) * time.Second) 109 if deleteAtUTC.Before(expireAtUTC) { 110 framework.Fail("Expected job's deletion time to be after expiration time.") 111 } 112 } 113 114 // finishTime returns finish time of the specified job. 115 func finishTime(finishedJob *batchv1.Job) metav1.Time { 116 var finishTime metav1.Time 117 for _, c := range finishedJob.Status.Conditions { 118 if (c.Type == batchv1.JobComplete || c.Type == batchv1.JobFailed) && c.Status == v1.ConditionTrue { 119 return c.LastTransitionTime 120 } 121 } 122 return finishTime 123 } 124 125 // updateJobWithRetries updates job with retries. 126 func updateJobWithRetries(ctx context.Context, c clientset.Interface, namespace, name string, applyUpdate func(*batchv1.Job)) (job *batchv1.Job, err error) { 127 jobs := c.BatchV1().Jobs(namespace) 128 var updateErr error 129 pollErr := wait.PollUntilContextTimeout(ctx, framework.Poll, JobTimeout, true, func(ctx context.Context) (bool, error) { 130 if job, err = jobs.Get(ctx, name, metav1.GetOptions{}); err != nil { 131 return false, err 132 } 133 // Apply the update, then attempt to push it to the apiserver. 134 applyUpdate(job) 135 if job, err = jobs.Update(ctx, job, metav1.UpdateOptions{}); err == nil { 136 framework.Logf("Updating job %s", name) 137 return true, nil 138 } 139 updateErr = err 140 return false, nil 141 }) 142 if wait.Interrupted(pollErr) { 143 pollErr = fmt.Errorf("couldn't apply the provided updated to job %q: %v", name, updateErr) 144 } 145 return job, pollErr 146 } 147 148 // waitForJobDeleting uses c to wait for the Job jobName in namespace ns to have 149 // a non-nil deletionTimestamp (i.e. being deleted). 150 func waitForJobDeleting(ctx context.Context, c clientset.Interface, ns, jobName string) error { 151 return wait.PollUntilContextTimeout(ctx, framework.Poll, JobTimeout, true, func(ctx context.Context) (bool, error) { 152 curr, err := c.BatchV1().Jobs(ns).Get(ctx, jobName, metav1.GetOptions{}) 153 if err != nil { 154 return false, err 155 } 156 return curr.ObjectMeta.DeletionTimestamp != nil, nil 157 }) 158 }