k8s.io/kubernetes@v1.29.3/pkg/controller/ttlafterfinished/ttlafterfinished_controller_test.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 ttlafterfinished 18 19 import ( 20 "strings" 21 "testing" 22 "time" 23 24 batchv1 "k8s.io/api/batch/v1" 25 corev1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/klog/v2" 28 "k8s.io/klog/v2/ktesting" 29 "k8s.io/utils/pointer" 30 ) 31 32 func newJob(completionTime, failedTime metav1.Time, ttl *int32) *batchv1.Job { 33 j := &batchv1.Job{ 34 TypeMeta: metav1.TypeMeta{Kind: "Job"}, 35 ObjectMeta: metav1.ObjectMeta{ 36 Name: "foobar", 37 Namespace: metav1.NamespaceDefault, 38 }, 39 Spec: batchv1.JobSpec{ 40 Selector: &metav1.LabelSelector{ 41 MatchLabels: map[string]string{"foo": "bar"}, 42 }, 43 Template: corev1.PodTemplateSpec{ 44 ObjectMeta: metav1.ObjectMeta{ 45 Labels: map[string]string{ 46 "foo": "bar", 47 }, 48 }, 49 Spec: corev1.PodSpec{ 50 Containers: []corev1.Container{ 51 {Image: "foo/bar"}, 52 }, 53 }, 54 }, 55 }, 56 } 57 58 if !completionTime.IsZero() { 59 c := batchv1.JobCondition{Type: batchv1.JobComplete, Status: corev1.ConditionTrue, LastTransitionTime: completionTime} 60 j.Status.Conditions = append(j.Status.Conditions, c) 61 } 62 63 if !failedTime.IsZero() { 64 c := batchv1.JobCondition{Type: batchv1.JobFailed, Status: corev1.ConditionTrue, LastTransitionTime: failedTime} 65 j.Status.Conditions = append(j.Status.Conditions, c) 66 } 67 68 if ttl != nil { 69 j.Spec.TTLSecondsAfterFinished = ttl 70 } 71 72 return j 73 } 74 75 func TestTimeLeft(t *testing.T) { 76 now := metav1.Now() 77 78 testCases := []struct { 79 name string 80 completionTime metav1.Time 81 failedTime metav1.Time 82 ttl *int32 83 since *time.Time 84 expectErr bool 85 expectErrStr string 86 expectedTimeLeft *time.Duration 87 expectedExpireAt time.Time 88 }{ 89 { 90 name: "Error case: Job unfinished", 91 ttl: pointer.Int32(100), 92 since: &now.Time, 93 expectErr: true, 94 expectErrStr: "should not be cleaned up", 95 }, 96 { 97 name: "Error case: Job completed now, no TTL", 98 completionTime: now, 99 since: &now.Time, 100 expectErr: true, 101 expectErrStr: "should not be cleaned up", 102 }, 103 { 104 name: "Job completed now, 0s TTL", 105 completionTime: now, 106 ttl: pointer.Int32(0), 107 since: &now.Time, 108 expectedTimeLeft: pointer.Duration(0 * time.Second), 109 expectedExpireAt: now.Time, 110 }, 111 { 112 name: "Job completed now, 10s TTL", 113 completionTime: now, 114 ttl: pointer.Int32(10), 115 since: &now.Time, 116 expectedTimeLeft: pointer.Duration(10 * time.Second), 117 expectedExpireAt: now.Add(10 * time.Second), 118 }, 119 { 120 name: "Job completed 10s ago, 15s TTL", 121 completionTime: metav1.NewTime(now.Add(-10 * time.Second)), 122 ttl: pointer.Int32(15), 123 since: &now.Time, 124 expectedTimeLeft: pointer.Duration(5 * time.Second), 125 expectedExpireAt: now.Add(5 * time.Second), 126 }, 127 { 128 name: "Error case: Job failed now, no TTL", 129 failedTime: now, 130 since: &now.Time, 131 expectErr: true, 132 expectErrStr: "should not be cleaned up", 133 }, 134 { 135 name: "Job failed now, 0s TTL", 136 failedTime: now, 137 ttl: pointer.Int32(0), 138 since: &now.Time, 139 expectedTimeLeft: pointer.Duration(0 * time.Second), 140 expectedExpireAt: now.Time, 141 }, 142 { 143 name: "Job failed now, 10s TTL", 144 failedTime: now, 145 ttl: pointer.Int32(10), 146 since: &now.Time, 147 expectedTimeLeft: pointer.Duration(10 * time.Second), 148 expectedExpireAt: now.Add(10 * time.Second), 149 }, 150 { 151 name: "Job failed 10s ago, 15s TTL", 152 failedTime: metav1.NewTime(now.Add(-10 * time.Second)), 153 ttl: pointer.Int32(15), 154 since: &now.Time, 155 expectedTimeLeft: pointer.Duration(5 * time.Second), 156 expectedExpireAt: now.Add(5 * time.Second), 157 }, 158 } 159 160 for _, tc := range testCases { 161 162 job := newJob(tc.completionTime, tc.failedTime, tc.ttl) 163 _, ctx := ktesting.NewTestContext(t) 164 logger := klog.FromContext(ctx) 165 gotTimeLeft, gotExpireAt, gotErr := timeLeft(logger, job, tc.since) 166 if tc.expectErr != (gotErr != nil) { 167 t.Errorf("%s: expected error is %t, got %t, error: %v", tc.name, tc.expectErr, gotErr != nil, gotErr) 168 } 169 if tc.expectErr && len(tc.expectErrStr) == 0 { 170 t.Errorf("%s: invalid test setup; error message must not be empty for error cases", tc.name) 171 } 172 if tc.expectErr && !strings.Contains(gotErr.Error(), tc.expectErrStr) { 173 t.Errorf("%s: expected error message contains %q, got %v", tc.name, tc.expectErrStr, gotErr) 174 } 175 if !tc.expectErr { 176 if *gotTimeLeft != *tc.expectedTimeLeft { 177 t.Errorf("%s: expected time left %v, got %v", tc.name, tc.expectedTimeLeft, gotTimeLeft) 178 } 179 if *gotExpireAt != tc.expectedExpireAt { 180 t.Errorf("%s: expected expire at %v, got %v", tc.name, tc.expectedExpireAt, *gotExpireAt) 181 } 182 } 183 } 184 }