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