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:  &parallelism,
   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  }