k8s.io/kubernetes@v1.29.3/test/e2e/apps/cronjob.go (about) 1 /* 2 Copyright 2016 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 "encoding/json" 22 "fmt" 23 "time" 24 25 "github.com/onsi/ginkgo/v2" 26 "github.com/onsi/gomega" 27 "github.com/onsi/gomega/format" 28 29 batchv1 "k8s.io/api/batch/v1" 30 v1 "k8s.io/api/core/v1" 31 apierrors "k8s.io/apimachinery/pkg/api/errors" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 "k8s.io/apimachinery/pkg/types" 36 "k8s.io/apimachinery/pkg/util/wait" 37 "k8s.io/apimachinery/pkg/watch" 38 clientset "k8s.io/client-go/kubernetes" 39 "k8s.io/client-go/kubernetes/scheme" 40 "k8s.io/client-go/util/retry" 41 batchinternal "k8s.io/kubernetes/pkg/apis/batch" 42 "k8s.io/kubernetes/pkg/controller/job" 43 "k8s.io/kubernetes/test/e2e/framework" 44 e2ejob "k8s.io/kubernetes/test/e2e/framework/job" 45 e2eresource "k8s.io/kubernetes/test/e2e/framework/resource" 46 imageutils "k8s.io/kubernetes/test/utils/image" 47 admissionapi "k8s.io/pod-security-admission/api" 48 ) 49 50 const ( 51 // How long to wait for a cronjob 52 cronJobTimeout = 5 * time.Minute 53 ) 54 55 var _ = SIGDescribe("CronJob", func() { 56 f := framework.NewDefaultFramework("cronjob") 57 f.NamespacePodSecurityLevel = admissionapi.LevelBaseline 58 59 sleepCommand := []string{"sleep", "300"} 60 61 // Pod will complete instantly 62 successCommand := []string{"/bin/true"} 63 failureCommand := []string{"/bin/false"} 64 65 /* 66 Release: v1.21 67 Testname: CronJob AllowConcurrent 68 Description: CronJob MUST support AllowConcurrent policy, allowing to run multiple jobs at the same time. 69 */ 70 framework.ConformanceIt("should schedule multiple jobs concurrently", func(ctx context.Context) { 71 ginkgo.By("Creating a cronjob") 72 cronJob := newTestCronJob("concurrent", "*/1 * * * ?", batchv1.AllowConcurrent, 73 sleepCommand, nil, nil) 74 cronJob, err := createCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob) 75 framework.ExpectNoError(err, "Failed to create CronJob in namespace %s", f.Namespace.Name) 76 77 ginkgo.By("Ensuring more than one job is running at a time") 78 err = waitForActiveJobs(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name, 2) 79 framework.ExpectNoError(err, "Failed to wait for active jobs in CronJob %s in namespace %s", cronJob.Name, f.Namespace.Name) 80 81 ginkgo.By("Ensuring at least two running jobs exists by listing jobs explicitly") 82 jobs, err := f.ClientSet.BatchV1().Jobs(f.Namespace.Name).List(ctx, metav1.ListOptions{}) 83 framework.ExpectNoError(err, "Failed to list the CronJobs in namespace %s", f.Namespace.Name) 84 activeJobs, _ := filterActiveJobs(jobs) 85 gomega.Expect(len(activeJobs)).To(gomega.BeNumerically(">=", 2)) 86 87 ginkgo.By("Removing cronjob") 88 err = deleteCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name) 89 framework.ExpectNoError(err, "Failed to delete CronJob %s in namespace %s", cronJob.Name, f.Namespace.Name) 90 }) 91 92 /* 93 Release: v1.21 94 Testname: CronJob Suspend 95 Description: CronJob MUST support suspension, which suppresses creation of new jobs. 96 */ 97 framework.ConformanceIt("should not schedule jobs when suspended", f.WithSlow(), func(ctx context.Context) { 98 ginkgo.By("Creating a suspended cronjob") 99 cronJob := newTestCronJob("suspended", "*/1 * * * ?", batchv1.AllowConcurrent, 100 sleepCommand, nil, nil) 101 t := true 102 cronJob.Spec.Suspend = &t 103 cronJob, err := createCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob) 104 framework.ExpectNoError(err, "Failed to create CronJob in namespace %s", f.Namespace.Name) 105 106 ginkgo.By("Ensuring no jobs are scheduled") 107 err = waitForNoJobs(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name, false) 108 framework.ExpectError(err) 109 110 ginkgo.By("Ensuring no job exists by listing jobs explicitly") 111 jobs, err := f.ClientSet.BatchV1().Jobs(f.Namespace.Name).List(ctx, metav1.ListOptions{}) 112 framework.ExpectNoError(err, "Failed to list the CronJobs in namespace %s", f.Namespace.Name) 113 gomega.Expect(jobs.Items).To(gomega.BeEmpty()) 114 115 ginkgo.By("Removing cronjob") 116 err = deleteCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name) 117 framework.ExpectNoError(err, "Failed to delete CronJob %s in namespace %s", cronJob.Name, f.Namespace.Name) 118 }) 119 120 /* 121 Release: v1.21 122 Testname: CronJob ForbidConcurrent 123 Description: CronJob MUST support ForbidConcurrent policy, allowing to run single, previous job at the time. 124 */ 125 framework.ConformanceIt("should not schedule new jobs when ForbidConcurrent", f.WithSlow(), func(ctx context.Context) { 126 ginkgo.By("Creating a ForbidConcurrent cronjob") 127 cronJob := newTestCronJob("forbid", "*/1 * * * ?", batchv1.ForbidConcurrent, 128 sleepCommand, nil, nil) 129 cronJob, err := createCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob) 130 framework.ExpectNoError(err, "Failed to create CronJob in namespace %s", f.Namespace.Name) 131 132 ginkgo.By("Ensuring a job is scheduled") 133 err = waitForActiveJobs(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name, 1) 134 framework.ExpectNoError(err, "Failed to schedule CronJob %s", cronJob.Name) 135 136 ginkgo.By("Ensuring exactly one is scheduled") 137 cronJob, err = getCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name) 138 framework.ExpectNoError(err, "Failed to get CronJob %s", cronJob.Name) 139 gomega.Expect(cronJob.Status.Active).Should(gomega.HaveLen(1)) 140 141 ginkgo.By("Ensuring exactly one running job exists by listing jobs explicitly") 142 jobs, err := f.ClientSet.BatchV1().Jobs(f.Namespace.Name).List(ctx, metav1.ListOptions{}) 143 framework.ExpectNoError(err, "Failed to list the CronJobs in namespace %s", f.Namespace.Name) 144 activeJobs, _ := filterActiveJobs(jobs) 145 gomega.Expect(activeJobs).To(gomega.HaveLen(1)) 146 147 ginkgo.By("Ensuring no more jobs are scheduled") 148 err = waitForActiveJobs(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name, 2) 149 framework.ExpectError(err) 150 151 ginkgo.By("Removing cronjob") 152 err = deleteCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name) 153 framework.ExpectNoError(err, "Failed to delete CronJob %s in namespace %s", cronJob.Name, f.Namespace.Name) 154 }) 155 156 /* 157 Release: v1.21 158 Testname: CronJob ReplaceConcurrent 159 Description: CronJob MUST support ReplaceConcurrent policy, allowing to run single, newer job at the time. 160 */ 161 framework.ConformanceIt("should replace jobs when ReplaceConcurrent", func(ctx context.Context) { 162 ginkgo.By("Creating a ReplaceConcurrent cronjob") 163 cronJob := newTestCronJob("replace", "*/1 * * * ?", batchv1.ReplaceConcurrent, 164 sleepCommand, nil, nil) 165 cronJob, err := createCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob) 166 framework.ExpectNoError(err, "Failed to create CronJob in namespace %s", f.Namespace.Name) 167 168 ginkgo.By("Ensuring a job is scheduled") 169 err = waitForActiveJobs(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name, 1) 170 framework.ExpectNoError(err, "Failed to schedule CronJob %s in namespace %s", cronJob.Name, f.Namespace.Name) 171 172 ginkgo.By("Ensuring exactly one is scheduled") 173 cronJob, err = getCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name) 174 framework.ExpectNoError(err, "Failed to get CronJob %s", cronJob.Name) 175 gomega.Expect(cronJob.Status.Active).Should(gomega.HaveLen(1)) 176 177 ginkgo.By("Ensuring exactly one running job exists by listing jobs explicitly") 178 jobs, err := f.ClientSet.BatchV1().Jobs(f.Namespace.Name).List(ctx, metav1.ListOptions{}) 179 framework.ExpectNoError(err, "Failed to list the jobs in namespace %s", f.Namespace.Name) 180 activeJobs, _ := filterActiveJobs(jobs) 181 gomega.Expect(activeJobs).To(gomega.HaveLen(1)) 182 183 ginkgo.By("Ensuring the job is replaced with a new one") 184 err = waitForJobReplaced(ctx, f.ClientSet, f.Namespace.Name, jobs.Items[0].Name) 185 framework.ExpectNoError(err, "Failed to replace CronJob %s in namespace %s", jobs.Items[0].Name, f.Namespace.Name) 186 187 ginkgo.By("Removing cronjob") 188 err = deleteCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name) 189 framework.ExpectNoError(err, "Failed to delete CronJob %s in namespace %s", cronJob.Name, f.Namespace.Name) 190 }) 191 192 ginkgo.It("should be able to schedule after more than 100 missed schedule", func(ctx context.Context) { 193 ginkgo.By("Creating a cronjob") 194 cronJob := newTestCronJob("concurrent", "*/1 * * * ?", batchv1.ForbidConcurrent, 195 sleepCommand, nil, nil) 196 creationTime := time.Now().Add(-99 * 24 * time.Hour) 197 lastScheduleTime := creationTime.Add(1 * 24 * time.Hour) 198 cronJob.CreationTimestamp = metav1.Time{Time: creationTime} 199 cronJob.Status.LastScheduleTime = &metav1.Time{Time: lastScheduleTime} 200 cronJob, err := createCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob) 201 framework.ExpectNoError(err, "Failed to create CronJob in namespace %s", f.Namespace.Name) 202 203 ginkgo.By("Ensuring one job is running") 204 err = waitForActiveJobs(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name, 1) 205 framework.ExpectNoError(err, "Failed to wait for active jobs in CronJob %s in namespace %s", cronJob.Name, f.Namespace.Name) 206 207 ginkgo.By("Ensuring at least one running jobs exists by listing jobs explicitly") 208 jobs, err := f.ClientSet.BatchV1().Jobs(f.Namespace.Name).List(ctx, metav1.ListOptions{}) 209 framework.ExpectNoError(err, "Failed to list the CronJobs in namespace %s", f.Namespace.Name) 210 activeJobs, _ := filterActiveJobs(jobs) 211 gomega.Expect(activeJobs).ToNot(gomega.BeEmpty()) 212 213 ginkgo.By("Removing cronjob") 214 err = deleteCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name) 215 framework.ExpectNoError(err, "Failed to delete CronJob %s in namespace %s", cronJob.Name, f.Namespace.Name) 216 }) 217 218 // shouldn't give us unexpected warnings 219 ginkgo.It("should not emit unexpected warnings", func(ctx context.Context) { 220 ginkgo.By("Creating a cronjob") 221 cronJob := newTestCronJob("concurrent", "*/1 * * * ?", batchv1.AllowConcurrent, 222 nil, nil, nil) 223 cronJob, err := createCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob) 224 framework.ExpectNoError(err, "Failed to create CronJob in namespace %s", f.Namespace.Name) 225 226 ginkgo.By("Ensuring at least two jobs and at least one finished job exists by listing jobs explicitly") 227 err = waitForJobsAtLeast(ctx, f.ClientSet, f.Namespace.Name, 2) 228 framework.ExpectNoError(err, "Failed to ensure at least two job exists in namespace %s", f.Namespace.Name) 229 err = waitForAnyFinishedJob(ctx, f.ClientSet, f.Namespace.Name) 230 framework.ExpectNoError(err, "Failed to ensure at least on finished job exists in namespace %s", f.Namespace.Name) 231 232 ginkgo.By("Ensuring no unexpected event has happened") 233 err = waitForEventWithReason(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name, []string{"MissingJob", "UnexpectedJob"}) 234 framework.ExpectError(err) 235 236 ginkgo.By("Removing cronjob") 237 err = deleteCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name) 238 framework.ExpectNoError(err, "Failed to delete CronJob %s in namespace %s", cronJob.Name, f.Namespace.Name) 239 }) 240 241 // deleted jobs should be removed from the active list 242 ginkgo.It("should remove from active list jobs that have been deleted", func(ctx context.Context) { 243 ginkgo.By("Creating a ForbidConcurrent cronjob") 244 cronJob := newTestCronJob("forbid", "*/1 * * * ?", batchv1.ForbidConcurrent, 245 sleepCommand, nil, nil) 246 cronJob, err := createCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob) 247 framework.ExpectNoError(err, "Failed to create CronJob in namespace %s", f.Namespace.Name) 248 249 ginkgo.By("Ensuring a job is scheduled") 250 err = waitForActiveJobs(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name, 1) 251 framework.ExpectNoError(err, "Failed to ensure a %s cronjob is scheduled in namespace %s", cronJob.Name, f.Namespace.Name) 252 253 ginkgo.By("Ensuring exactly one is scheduled") 254 cronJob, err = getCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name) 255 framework.ExpectNoError(err, "Failed to ensure exactly one %s cronjob is scheduled in namespace %s", cronJob.Name, f.Namespace.Name) 256 gomega.Expect(cronJob.Status.Active).Should(gomega.HaveLen(1)) 257 258 ginkgo.By("Deleting the job") 259 job := cronJob.Status.Active[0] 260 framework.ExpectNoError(e2eresource.DeleteResourceAndWaitForGC(ctx, f.ClientSet, batchinternal.Kind("Job"), f.Namespace.Name, job.Name)) 261 262 ginkgo.By("Ensuring job was deleted") 263 _, err = e2ejob.GetJob(ctx, f.ClientSet, f.Namespace.Name, job.Name) 264 framework.ExpectError(err) 265 if !apierrors.IsNotFound(err) { 266 framework.Failf("Failed to delete %s cronjob in namespace %s", cronJob.Name, f.Namespace.Name) 267 } 268 269 ginkgo.By("Ensuring the job is not in the cronjob active list") 270 err = waitForJobNotActive(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name, job.Name) 271 framework.ExpectNoError(err, "Failed to ensure the %s cronjob is not in active list in namespace %s", cronJob.Name, f.Namespace.Name) 272 273 ginkgo.By("Ensuring MissingJob event has occurred") 274 err = waitForEventWithReason(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name, []string{"MissingJob"}) 275 framework.ExpectNoError(err, "Failed to ensure missing job event has occurred for %s cronjob in namespace %s", cronJob.Name, f.Namespace.Name) 276 277 ginkgo.By("Removing cronjob") 278 err = deleteCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob.Name) 279 framework.ExpectNoError(err, "Failed to remove %s cronjob in namespace %s", cronJob.Name, f.Namespace.Name) 280 }) 281 282 // cleanup of successful finished jobs, with limit of one successful job 283 ginkgo.It("should delete successful finished jobs with limit of one successful job", func(ctx context.Context) { 284 ginkgo.By("Creating an AllowConcurrent cronjob with custom history limit") 285 successLimit := int32(1) 286 failedLimit := int32(0) 287 cronJob := newTestCronJob("successful-jobs-history-limit", "*/1 * * * ?", batchv1.AllowConcurrent, 288 successCommand, &successLimit, &failedLimit) 289 290 ensureHistoryLimits(ctx, f.ClientSet, f.Namespace.Name, cronJob) 291 }) 292 293 // cleanup of failed finished jobs, with limit of one failed job 294 ginkgo.It("should delete failed finished jobs with limit of one job", func(ctx context.Context) { 295 ginkgo.By("Creating an AllowConcurrent cronjob with custom history limit") 296 successLimit := int32(0) 297 failedLimit := int32(1) 298 cronJob := newTestCronJob("failed-jobs-history-limit", "*/1 * * * ?", batchv1.AllowConcurrent, 299 failureCommand, &successLimit, &failedLimit) 300 301 ensureHistoryLimits(ctx, f.ClientSet, f.Namespace.Name, cronJob) 302 }) 303 304 ginkgo.It("should support timezone", func(ctx context.Context) { 305 ginkgo.By("Creating a cronjob with TimeZone") 306 cronJob := newTestCronJob("cronjob-with-timezone", "*/1 * * * ?", batchv1.AllowConcurrent, 307 failureCommand, nil, nil) 308 badTimeZone := "bad-time-zone" 309 cronJob.Spec.TimeZone = &badTimeZone 310 _, err := createCronJob(ctx, f.ClientSet, f.Namespace.Name, cronJob) 311 framework.ExpectError(err, "CronJob creation should fail with invalid time zone error") 312 if !apierrors.IsInvalid(err) { 313 framework.Failf("Failed to create CronJob, invalid time zone.") 314 } 315 }) 316 317 /* 318 Release: v1.21 319 Testname: CronJob API Operations 320 Description: 321 CronJob MUST support create, get, list, watch, update, patch, delete, and deletecollection. 322 CronJob/status MUST support get, update and patch. 323 */ 324 framework.ConformanceIt("should support CronJob API operations", func(ctx context.Context) { 325 ginkgo.By("Creating a cronjob") 326 successLimit := int32(1) 327 failedLimit := int32(0) 328 cjTemplate := newTestCronJob("test-api", "* */1 * * ?", batchv1.AllowConcurrent, 329 successCommand, &successLimit, &failedLimit) 330 cjTemplate.Labels = map[string]string{ 331 "special-label": f.UniqueName, 332 } 333 334 ns := f.Namespace.Name 335 cjVersion := "v1" 336 cjClient := f.ClientSet.BatchV1().CronJobs(ns) 337 338 ginkgo.By("creating") 339 createdCronJob, err := cjClient.Create(ctx, cjTemplate, metav1.CreateOptions{}) 340 framework.ExpectNoError(err) 341 342 ginkgo.By("getting") 343 gottenCronJob, err := cjClient.Get(ctx, createdCronJob.Name, metav1.GetOptions{}) 344 framework.ExpectNoError(err) 345 gomega.Expect(gottenCronJob.UID).To(gomega.Equal(createdCronJob.UID)) 346 347 ginkgo.By("listing") 348 cjs, err := cjClient.List(ctx, metav1.ListOptions{LabelSelector: "special-label=" + f.UniqueName}) 349 framework.ExpectNoError(err) 350 gomega.Expect(cjs.Items).To(gomega.HaveLen(1), "filtered list should have 1 item") 351 352 ginkgo.By("watching") 353 framework.Logf("starting watch") 354 cjWatch, err := cjClient.Watch(ctx, metav1.ListOptions{ResourceVersion: cjs.ResourceVersion, LabelSelector: "special-label=" + f.UniqueName}) 355 framework.ExpectNoError(err) 356 357 // Test cluster-wide list and watch 358 clusterCJClient := f.ClientSet.BatchV1().CronJobs("") 359 ginkgo.By("cluster-wide listing") 360 clusterCJs, err := clusterCJClient.List(ctx, metav1.ListOptions{LabelSelector: "special-label=" + f.UniqueName}) 361 framework.ExpectNoError(err) 362 gomega.Expect(clusterCJs.Items).To(gomega.HaveLen(1), "filtered list should have 1 item") 363 364 ginkgo.By("cluster-wide watching") 365 framework.Logf("starting watch") 366 _, err = clusterCJClient.Watch(ctx, metav1.ListOptions{ResourceVersion: cjs.ResourceVersion, LabelSelector: "special-label=" + f.UniqueName}) 367 framework.ExpectNoError(err) 368 369 ginkgo.By("patching") 370 patchedCronJob, err := cjClient.Patch(ctx, createdCronJob.Name, types.MergePatchType, 371 []byte(`{"metadata":{"annotations":{"patched":"true"}}}`), metav1.PatchOptions{}) 372 framework.ExpectNoError(err) 373 gomega.Expect(patchedCronJob.Annotations).To(gomega.HaveKeyWithValue("patched", "true"), "patched object should have the applied annotation") 374 375 ginkgo.By("updating") 376 var cjToUpdate, updatedCronJob *batchv1.CronJob 377 err = retry.RetryOnConflict(retry.DefaultRetry, func() error { 378 cjToUpdate, err = cjClient.Get(ctx, createdCronJob.Name, metav1.GetOptions{}) 379 if err != nil { 380 return err 381 } 382 cjToUpdate.Annotations["updated"] = "true" 383 updatedCronJob, err = cjClient.Update(ctx, cjToUpdate, metav1.UpdateOptions{}) 384 return err 385 }) 386 framework.ExpectNoError(err) 387 gomega.Expect(updatedCronJob.Annotations).To(gomega.HaveKeyWithValue("updated", "true"), "updated object should have the applied annotation") 388 389 framework.Logf("waiting for watch events with expected annotations") 390 for sawAnnotations := false; !sawAnnotations; { 391 select { 392 case evt, ok := <-cjWatch.ResultChan(): 393 394 if !ok { 395 framework.Fail("Watch channel is closed.") 396 } 397 gomega.Expect(evt.Type).To(gomega.Equal(watch.Modified)) 398 watchedCronJob, isCronJob := evt.Object.(*batchv1.CronJob) 399 if !isCronJob { 400 framework.Failf("expected CronJob, got %T", evt.Object) 401 } 402 if watchedCronJob.Annotations["patched"] == "true" { 403 framework.Logf("saw patched and updated annotations") 404 sawAnnotations = true 405 cjWatch.Stop() 406 } else { 407 framework.Logf("missing expected annotations, waiting: %#v", watchedCronJob.Annotations) 408 } 409 case <-time.After(wait.ForeverTestTimeout): 410 framework.Fail("timed out waiting for watch event") 411 } 412 } 413 414 // /status subresource operations 415 ginkgo.By("patching /status") 416 // we need to use RFC3339 version since conversion over the wire cuts nanoseconds 417 now1 := metav1.Now().Rfc3339Copy() 418 cjStatus := batchv1.CronJobStatus{ 419 LastScheduleTime: &now1, 420 } 421 cjStatusJSON, err := json.Marshal(cjStatus) 422 framework.ExpectNoError(err) 423 patchedStatus, err := cjClient.Patch(ctx, createdCronJob.Name, types.MergePatchType, 424 []byte(`{"metadata":{"annotations":{"patchedstatus":"true"}},"status":`+string(cjStatusJSON)+`}`), 425 metav1.PatchOptions{}, "status") 426 framework.ExpectNoError(err) 427 if !patchedStatus.Status.LastScheduleTime.Equal(&now1) { 428 framework.Failf("patched object should have the applied lastScheduleTime %#v, got %#v instead", cjStatus.LastScheduleTime, patchedStatus.Status.LastScheduleTime) 429 } 430 gomega.Expect(patchedStatus.Annotations).To(gomega.HaveKeyWithValue("patchedstatus", "true"), "patched object should have the applied annotation") 431 432 ginkgo.By("updating /status") 433 // we need to use RFC3339 version since conversion over the wire cuts nanoseconds 434 now2 := metav1.Now().Rfc3339Copy() 435 var statusToUpdate, updatedStatus *batchv1.CronJob 436 err = retry.RetryOnConflict(retry.DefaultRetry, func() error { 437 statusToUpdate, err = cjClient.Get(ctx, createdCronJob.Name, metav1.GetOptions{}) 438 if err != nil { 439 return err 440 } 441 statusToUpdate.Status.LastScheduleTime = &now2 442 updatedStatus, err = cjClient.UpdateStatus(ctx, statusToUpdate, metav1.UpdateOptions{}) 443 return err 444 }) 445 framework.ExpectNoError(err) 446 447 if !updatedStatus.Status.LastScheduleTime.Equal(&now2) { 448 framework.Failf("updated object status expected to have updated lastScheduleTime %#v, got %#v", statusToUpdate.Status.LastScheduleTime, updatedStatus.Status.LastScheduleTime) 449 } 450 451 ginkgo.By("get /status") 452 cjResource := schema.GroupVersionResource{Group: "batch", Version: cjVersion, Resource: "cronjobs"} 453 gottenStatus, err := f.DynamicClient.Resource(cjResource).Namespace(ns).Get(ctx, createdCronJob.Name, metav1.GetOptions{}, "status") 454 framework.ExpectNoError(err) 455 statusUID, _, err := unstructured.NestedFieldCopy(gottenStatus.Object, "metadata", "uid") 456 framework.ExpectNoError(err) 457 gomega.Expect(string(createdCronJob.UID)).To(gomega.Equal(statusUID), "createdCronJob.UID: %v expected to match statusUID: %v ", createdCronJob.UID, statusUID) 458 459 // CronJob resource delete operations 460 expectFinalizer := func(cj *batchv1.CronJob, msg string) { 461 gomega.Expect(cj.DeletionTimestamp).NotTo(gomega.BeNil(), fmt.Sprintf("expected deletionTimestamp, got nil on step: %q, cronjob: %+v", msg, cj)) 462 gomega.Expect(cj.Finalizers).ToNot(gomega.BeEmpty(), "expected finalizers on cronjob, got none on step: %q, cronjob: %+v", msg, cj) 463 } 464 465 ginkgo.By("deleting") 466 cjTemplate.Name = "for-removal" 467 forRemovalCronJob, err := cjClient.Create(ctx, cjTemplate, metav1.CreateOptions{}) 468 framework.ExpectNoError(err) 469 err = cjClient.Delete(ctx, forRemovalCronJob.Name, metav1.DeleteOptions{}) 470 framework.ExpectNoError(err) 471 cj, err := cjClient.Get(ctx, forRemovalCronJob.Name, metav1.GetOptions{}) 472 // If controller does not support finalizers, we expect a 404. Otherwise we validate finalizer behavior. 473 if err == nil { 474 expectFinalizer(cj, "deleting cronjob") 475 } else if !apierrors.IsNotFound(err) { 476 framework.Failf("expected 404, got %v", err) 477 } 478 479 ginkgo.By("deleting a collection") 480 err = cjClient.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: "special-label=" + f.UniqueName}) 481 framework.ExpectNoError(err) 482 cjs, err = cjClient.List(ctx, metav1.ListOptions{LabelSelector: "special-label=" + f.UniqueName}) 483 framework.ExpectNoError(err) 484 // Should have <= 2 items since some cronjobs might not have been deleted yet due to finalizers 485 gomega.Expect(len(cjs.Items)).To(gomega.BeNumerically("<=", 2), "filtered list length should be <= 2, got:\n%s", format.Object(cjs.Items, 1)) 486 // Validate finalizers 487 for _, cj := range cjs.Items { 488 expectFinalizer(&cj, "deleting cronjob collection") 489 } 490 }) 491 492 }) 493 494 func ensureHistoryLimits(ctx context.Context, c clientset.Interface, ns string, cronJob *batchv1.CronJob) { 495 cronJob, err := createCronJob(ctx, c, ns, cronJob) 496 framework.ExpectNoError(err, "Failed to create allowconcurrent cronjob with custom history limits in namespace %s", ns) 497 498 // Job is going to complete instantly: do not check for an active job 499 // as we are most likely to miss it 500 501 ginkgo.By("Ensuring a finished job exists") 502 err = waitForAnyFinishedJob(ctx, c, ns) 503 framework.ExpectNoError(err, "Failed to ensure a finished cronjob exists in namespace %s", ns) 504 505 ginkgo.By("Ensuring a finished job exists by listing jobs explicitly") 506 jobs, err := c.BatchV1().Jobs(ns).List(ctx, metav1.ListOptions{}) 507 framework.ExpectNoError(err, "Failed to ensure a finished cronjob exists by listing jobs explicitly in namespace %s", ns) 508 activeJobs, finishedJobs := filterActiveJobs(jobs) 509 if len(finishedJobs) != 1 { 510 framework.Logf("Expected one finished job in namespace %s; activeJobs=%v; finishedJobs=%v", ns, activeJobs, finishedJobs) 511 gomega.Expect(finishedJobs).To(gomega.HaveLen(1)) 512 } 513 514 // Job should get deleted when the next job finishes the next minute 515 ginkgo.By("Ensuring this job and its pods does not exist anymore") 516 err = waitForJobToDisappear(ctx, c, ns, finishedJobs[0]) 517 framework.ExpectNoError(err, "Failed to ensure that job does not exists anymore in namespace %s", ns) 518 err = waitForJobsPodToDisappear(ctx, c, ns, finishedJobs[0]) 519 framework.ExpectNoError(err, "Failed to ensure that pods for job does not exists anymore in namespace %s", ns) 520 521 ginkgo.By("Ensuring there is 1 finished job by listing jobs explicitly") 522 jobs, err = c.BatchV1().Jobs(ns).List(ctx, metav1.ListOptions{}) 523 framework.ExpectNoError(err, "Failed to ensure there is one finished job by listing job explicitly in namespace %s", ns) 524 activeJobs, finishedJobs = filterActiveJobs(jobs) 525 if len(finishedJobs) != 1 { 526 framework.Logf("Expected one finished job in namespace %s; activeJobs=%v; finishedJobs=%v", ns, activeJobs, finishedJobs) 527 gomega.Expect(finishedJobs).To(gomega.HaveLen(1)) 528 } 529 530 ginkgo.By("Removing cronjob") 531 err = deleteCronJob(ctx, c, ns, cronJob.Name) 532 framework.ExpectNoError(err, "Failed to remove the %s cronjob in namespace %s", cronJob.Name, ns) 533 } 534 535 // newTestCronJob returns a cronjob which does one of several testing behaviors. 536 func newTestCronJob(name, schedule string, concurrencyPolicy batchv1.ConcurrencyPolicy, 537 command []string, successfulJobsHistoryLimit *int32, failedJobsHistoryLimit *int32) *batchv1.CronJob { 538 parallelism := int32(1) 539 completions := int32(1) 540 backofflimit := int32(1) 541 sj := &batchv1.CronJob{ 542 ObjectMeta: metav1.ObjectMeta{ 543 Name: name, 544 }, 545 TypeMeta: metav1.TypeMeta{ 546 Kind: "CronJob", 547 }, 548 Spec: batchv1.CronJobSpec{ 549 Schedule: schedule, 550 ConcurrencyPolicy: concurrencyPolicy, 551 JobTemplate: batchv1.JobTemplateSpec{ 552 Spec: batchv1.JobSpec{ 553 Parallelism: ¶llelism, 554 Completions: &completions, 555 BackoffLimit: &backofflimit, 556 Template: v1.PodTemplateSpec{ 557 Spec: v1.PodSpec{ 558 RestartPolicy: v1.RestartPolicyOnFailure, 559 Volumes: []v1.Volume{ 560 { 561 Name: "data", 562 VolumeSource: v1.VolumeSource{ 563 EmptyDir: &v1.EmptyDirVolumeSource{}, 564 }, 565 }, 566 }, 567 Containers: []v1.Container{ 568 { 569 Name: "c", 570 Image: imageutils.GetE2EImage(imageutils.BusyBox), 571 VolumeMounts: []v1.VolumeMount{ 572 { 573 MountPath: "/data", 574 Name: "data", 575 }, 576 }, 577 }, 578 }, 579 }, 580 }, 581 }, 582 }, 583 }, 584 } 585 sj.Spec.SuccessfulJobsHistoryLimit = successfulJobsHistoryLimit 586 sj.Spec.FailedJobsHistoryLimit = failedJobsHistoryLimit 587 if command != nil { 588 sj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Command = command 589 } 590 return sj 591 } 592 593 func createCronJob(ctx context.Context, c clientset.Interface, ns string, cronJob *batchv1.CronJob) (*batchv1.CronJob, error) { 594 return c.BatchV1().CronJobs(ns).Create(ctx, cronJob, metav1.CreateOptions{}) 595 } 596 597 func getCronJob(ctx context.Context, c clientset.Interface, ns, name string) (*batchv1.CronJob, error) { 598 return c.BatchV1().CronJobs(ns).Get(ctx, name, metav1.GetOptions{}) 599 } 600 601 func deleteCronJob(ctx context.Context, c clientset.Interface, ns, name string) error { 602 propagationPolicy := metav1.DeletePropagationBackground // Also delete jobs and pods related to cronjob 603 return c.BatchV1().CronJobs(ns).Delete(ctx, name, metav1.DeleteOptions{PropagationPolicy: &propagationPolicy}) 604 } 605 606 // Wait for at least given amount of active jobs. 607 func waitForActiveJobs(ctx context.Context, c clientset.Interface, ns, cronJobName string, active int) error { 608 return wait.PollWithContext(ctx, framework.Poll, cronJobTimeout, func(ctx context.Context) (bool, error) { 609 curr, err := getCronJob(ctx, c, ns, cronJobName) 610 if err != nil { 611 return false, err 612 } 613 return len(curr.Status.Active) >= active, nil 614 }) 615 } 616 617 // Wait for jobs to appear in the active list of a cronjob or not. 618 // When failIfNonEmpty is set, this fails if the active set of jobs is still non-empty after 619 // the timeout. When failIfNonEmpty is not set, this fails if the active set of jobs is still 620 // empty after the timeout. 621 func waitForNoJobs(ctx context.Context, c clientset.Interface, ns, jobName string, failIfNonEmpty bool) error { 622 return wait.PollWithContext(ctx, framework.Poll, cronJobTimeout, func(ctx context.Context) (bool, error) { 623 curr, err := getCronJob(ctx, c, ns, jobName) 624 if err != nil { 625 return false, err 626 } 627 628 if failIfNonEmpty { 629 return len(curr.Status.Active) == 0, nil 630 } 631 return len(curr.Status.Active) != 0, nil 632 }) 633 } 634 635 // Wait till a given job actually goes away from the Active list for a given cronjob 636 func waitForJobNotActive(ctx context.Context, c clientset.Interface, ns, cronJobName, jobName string) error { 637 return wait.PollWithContext(ctx, framework.Poll, cronJobTimeout, func(ctx context.Context) (bool, error) { 638 curr, err := getCronJob(ctx, c, ns, cronJobName) 639 if err != nil { 640 return false, err 641 } 642 643 for _, j := range curr.Status.Active { 644 if j.Name == jobName { 645 return false, nil 646 } 647 } 648 return true, nil 649 }) 650 } 651 652 // Wait for a job to disappear by listing them explicitly. 653 func waitForJobToDisappear(ctx context.Context, c clientset.Interface, ns string, targetJob *batchv1.Job) error { 654 return wait.PollWithContext(ctx, framework.Poll, cronJobTimeout, func(ctx context.Context) (bool, error) { 655 jobs, err := c.BatchV1().Jobs(ns).List(ctx, metav1.ListOptions{}) 656 if err != nil { 657 return false, err 658 } 659 _, finishedJobs := filterActiveJobs(jobs) 660 for _, job := range finishedJobs { 661 if targetJob.Namespace == job.Namespace && targetJob.Name == job.Name { 662 return false, nil 663 } 664 } 665 return true, nil 666 }) 667 } 668 669 // Wait for a pod to disappear by listing them explicitly. 670 func waitForJobsPodToDisappear(ctx context.Context, c clientset.Interface, ns string, targetJob *batchv1.Job) error { 671 return wait.PollWithContext(ctx, framework.Poll, cronJobTimeout, func(ctx context.Context) (bool, error) { 672 options := metav1.ListOptions{LabelSelector: fmt.Sprintf("controller-uid=%s", targetJob.UID)} 673 pods, err := c.CoreV1().Pods(ns).List(ctx, options) 674 if err != nil { 675 return false, err 676 } 677 return len(pods.Items) == 0, nil 678 }) 679 } 680 681 // Wait for a job to be replaced with a new one. 682 func waitForJobReplaced(ctx context.Context, c clientset.Interface, ns, previousJobName string) error { 683 return wait.PollWithContext(ctx, framework.Poll, cronJobTimeout, func(ctx context.Context) (bool, error) { 684 jobs, err := c.BatchV1().Jobs(ns).List(ctx, metav1.ListOptions{}) 685 if err != nil { 686 return false, err 687 } 688 // Ignore Jobs pending deletion, since deletion of Jobs is now asynchronous. 689 aliveJobs := filterNotDeletedJobs(jobs) 690 if len(aliveJobs) > 1 { 691 return false, fmt.Errorf("more than one job is running %+v", jobs.Items) 692 } else if len(aliveJobs) == 0 { 693 framework.Logf("Warning: Found 0 jobs in namespace %v", ns) 694 return false, nil 695 } 696 return aliveJobs[0].Name != previousJobName, nil 697 }) 698 } 699 700 // waitForJobsAtLeast waits for at least a number of jobs to appear. 701 func waitForJobsAtLeast(ctx context.Context, c clientset.Interface, ns string, atLeast int) error { 702 return wait.PollWithContext(ctx, framework.Poll, cronJobTimeout, func(ctx context.Context) (bool, error) { 703 jobs, err := c.BatchV1().Jobs(ns).List(ctx, metav1.ListOptions{}) 704 if err != nil { 705 return false, err 706 } 707 return len(jobs.Items) >= atLeast, nil 708 }) 709 } 710 711 // waitForAnyFinishedJob waits for any completed job to appear. 712 func waitForAnyFinishedJob(ctx context.Context, c clientset.Interface, ns string) error { 713 return wait.PollWithContext(ctx, framework.Poll, cronJobTimeout, func(ctx context.Context) (bool, error) { 714 jobs, err := c.BatchV1().Jobs(ns).List(ctx, metav1.ListOptions{}) 715 if err != nil { 716 return false, err 717 } 718 for i := range jobs.Items { 719 if job.IsJobFinished(&jobs.Items[i]) { 720 return true, nil 721 } 722 } 723 return false, nil 724 }) 725 } 726 727 // waitForEventWithReason waits for events with a reason within a list has occurred 728 func waitForEventWithReason(ctx context.Context, c clientset.Interface, ns, cronJobName string, reasons []string) error { 729 return wait.PollWithContext(ctx, framework.Poll, 30*time.Second, func(ctx context.Context) (bool, error) { 730 sj, err := getCronJob(ctx, c, ns, cronJobName) 731 if err != nil { 732 return false, err 733 } 734 events, err := c.CoreV1().Events(ns).Search(scheme.Scheme, sj) 735 if err != nil { 736 return false, err 737 } 738 for _, e := range events.Items { 739 for _, reason := range reasons { 740 if e.Reason == reason { 741 return true, nil 742 } 743 } 744 } 745 return false, nil 746 }) 747 } 748 749 // filterNotDeletedJobs returns the job list without any jobs that are pending 750 // deletion. 751 func filterNotDeletedJobs(jobs *batchv1.JobList) []*batchv1.Job { 752 var alive []*batchv1.Job 753 for i := range jobs.Items { 754 job := &jobs.Items[i] 755 if job.DeletionTimestamp == nil { 756 alive = append(alive, job) 757 } 758 } 759 return alive 760 } 761 762 func filterActiveJobs(jobs *batchv1.JobList) (active []*batchv1.Job, finished []*batchv1.Job) { 763 for i := range jobs.Items { 764 j := jobs.Items[i] 765 if !job.IsJobFinished(&j) { 766 active = append(active, &j) 767 } else { 768 finished = append(finished, &j) 769 } 770 } 771 return 772 }