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