1
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
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
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
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
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
149
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 }
159
View as plain text