sigs.k8s.io/kubebuilder/v3@v3.14.0/hack/docs/internal/cronjob-tutorial/writing_tests_controller.go (about) 1 /* 2 Copyright 2023 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 cronjob 18 19 const ControllerTest = `/* 20 21 Licensed under the Apache License, Version 2.0 (the "License"); 22 you may not use this file except in compliance with the License. 23 You may obtain a copy of the License at 24 25 http://www.apache.org/licenses/LICENSE-2.0 26 27 Unless required by applicable law or agreed to in writing, software 28 distributed under the License is distributed on an "AS IS" BASIS, 29 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 30 See the License for the specific language governing permissions and 31 limitations under the License. 32 */ 33 // +kubebuilder:docs-gen:collapse=Apache License 34 35 /* 36 Ideally, we should have one` + " `" + `<kind>_controller_test.go` + "`" + ` for each controller scaffolded and called in the` + " `" + `suite_test.go` + "`" + `. 37 So, let's write our example test for the CronJob controller (` + "`" + `cronjob_controller_test.go.` + "`" + `) 38 */ 39 40 /* 41 As usual, we start with the necessary imports. We also define some utility variables. 42 */ 43 package controller 44 45 import ( 46 "context" 47 "reflect" 48 "time" 49 50 . "github.com/onsi/ginkgo/v2" 51 . "github.com/onsi/gomega" 52 batchv1 "k8s.io/api/batch/v1" 53 v1 "k8s.io/api/core/v1" 54 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 55 "k8s.io/apimachinery/pkg/types" 56 57 cronjobv1 "tutorial.kubebuilder.io/project/api/v1" 58 ) 59 60 // +kubebuilder:docs-gen:collapse=Imports 61 62 /* 63 The first step to writing a simple integration test is to actually create an instance of CronJob you can run tests against. 64 Note that to create a CronJob, you’ll need to create a stub CronJob struct that contains your CronJob’s specifications. 65 66 Note that when we create a stub CronJob, the CronJob also needs stubs of its required downstream objects. 67 Without the stubbed Job template spec and the Pod template spec below, the Kubernetes API will not be able to 68 create the CronJob. 69 */ 70 var _ = Describe("CronJob controller", func() { 71 72 // Define utility constants for object names and testing timeouts/durations and intervals. 73 const ( 74 CronjobName = "test-cronjob" 75 CronjobNamespace = "default" 76 JobName = "test-job" 77 78 timeout = time.Second * 10 79 duration = time.Second * 10 80 interval = time.Millisecond * 250 81 ) 82 83 Context("When updating CronJob Status", func() { 84 It("Should increase CronJob Status.Active count when new Jobs are created", func() { 85 By("By creating a new CronJob") 86 ctx := context.Background() 87 cronJob := &cronjobv1.CronJob{ 88 TypeMeta: metav1.TypeMeta{ 89 APIVersion: "batch.tutorial.kubebuilder.io/v1", 90 Kind: "CronJob", 91 }, 92 ObjectMeta: metav1.ObjectMeta{ 93 Name: CronjobName, 94 Namespace: CronjobNamespace, 95 }, 96 Spec: cronjobv1.CronJobSpec{ 97 Schedule: "1 * * * *", 98 JobTemplate: batchv1.JobTemplateSpec{ 99 Spec: batchv1.JobSpec{ 100 // For simplicity, we only fill out the required fields. 101 Template: v1.PodTemplateSpec{ 102 Spec: v1.PodSpec{ 103 // For simplicity, we only fill out the required fields. 104 Containers: []v1.Container{ 105 { 106 Name: "test-container", 107 Image: "test-image", 108 }, 109 }, 110 RestartPolicy: v1.RestartPolicyOnFailure, 111 }, 112 }, 113 }, 114 }, 115 }, 116 } 117 Expect(k8sClient.Create(ctx, cronJob)).Should(Succeed()) 118 119 /* 120 After creating this CronJob, let's check that the CronJob's Spec fields match what we passed in. 121 Note that, because the k8s apiserver may not have finished creating a CronJob after our` + " `" + `Create()` + "`" + ` call from earlier, we will use Gomega’s Eventually() testing function instead of Expect() to give the apiserver an opportunity to finish creating our CronJob.` + ` 122 123 ` + 124 "`" + `Eventually()` + "`" + ` will repeatedly run the function provided as an argument every interval seconds until 125 (a) the function’s output matches what’s expected in the subsequent` + " `" + `Should()` + "`" + ` call, or 126 (b) the number of attempts * interval period exceed the provided timeout value. 127 128 In the examples below, timeout and interval are Go Duration values of our choosing. 129 */ 130 131 cronjobLookupKey := types.NamespacedName{Name: CronjobName, Namespace: CronjobNamespace} 132 createdCronjob := &cronjobv1.CronJob{} 133 134 // We'll need to retry getting this newly created CronJob, given that creation may not immediately happen. 135 Eventually(func() bool { 136 err := k8sClient.Get(ctx, cronjobLookupKey, createdCronjob) 137 if err != nil { 138 return false 139 } 140 return true 141 }, timeout, interval).Should(BeTrue()) 142 // Let's make sure our Schedule string value was properly converted/handled. 143 Expect(createdCronjob.Spec.Schedule).Should(Equal("1 * * * *")) 144 /* 145 Now that we've created a CronJob in our test cluster, the next step is to write a test that actually tests our CronJob controller’s behavior. 146 Let’s test the CronJob controller’s logic responsible for updating CronJob.Status.Active with actively running jobs. 147 We’ll verify that when a CronJob has a single active downstream Job, its CronJob.Status.Active field contains a reference to this Job. 148 149 First, we should get the test CronJob we created earlier, and verify that it currently does not have any active jobs. 150 We use Gomega's` + " `" + `Consistently()` + "`" + ` check here to ensure that the active job count remains 0 over a duration of time. 151 */ 152 By("By checking the CronJob has zero active Jobs") 153 Consistently(func() (int, error) { 154 err := k8sClient.Get(ctx, cronjobLookupKey, createdCronjob) 155 if err != nil { 156 return -1, err 157 } 158 return len(createdCronjob.Status.Active), nil 159 }, duration, interval).Should(Equal(0)) 160 /* 161 Next, we actually create a stubbed Job that will belong to our CronJob, as well as its downstream template specs. 162 We set the Job's status's "Active" count to 2 to simulate the Job running two pods, which means the Job is actively running. 163 164 We then take the stubbed Job and set its owner reference to point to our test CronJob. 165 This ensures that the test Job belongs to, and is tracked by, our test CronJob. 166 Once that’s done, we create our new Job instance. 167 */ 168 By("By creating a new Job") 169 testJob := &batchv1.Job{ 170 ObjectMeta: metav1.ObjectMeta{ 171 Name: JobName, 172 Namespace: CronjobNamespace, 173 }, 174 Spec: batchv1.JobSpec{ 175 Template: v1.PodTemplateSpec{ 176 Spec: v1.PodSpec{ 177 // For simplicity, we only fill out the required fields. 178 Containers: []v1.Container{ 179 { 180 Name: "test-container", 181 Image: "test-image", 182 }, 183 }, 184 RestartPolicy: v1.RestartPolicyOnFailure, 185 }, 186 }, 187 }, 188 Status: batchv1.JobStatus{ 189 Active: 2, 190 }, 191 } 192 193 // Note that your CronJob’s GroupVersionKind is required to set up this owner reference. 194 kind := reflect.TypeOf(cronjobv1.CronJob{}).Name() 195 gvk := cronjobv1.GroupVersion.WithKind(kind) 196 197 controllerRef := metav1.NewControllerRef(createdCronjob, gvk) 198 testJob.SetOwnerReferences([]metav1.OwnerReference{*controllerRef}) 199 Expect(k8sClient.Create(ctx, testJob)).Should(Succeed()) 200 /* 201 Adding this Job to our test CronJob should trigger our controller’s reconciler logic. 202 After that, we can write a test that evaluates whether our controller eventually updates our CronJob’s Status field as expected! 203 */ 204 By("By checking that the CronJob has one active Job") 205 Eventually(func() ([]string, error) { 206 err := k8sClient.Get(ctx, cronjobLookupKey, createdCronjob) 207 if err != nil { 208 return nil, err 209 } 210 211 names := []string{} 212 for _, job := range createdCronjob.Status.Active { 213 names = append(names, job.Name) 214 } 215 return names, nil 216 }, timeout, interval).Should(ConsistOf(JobName), "should list our active job %s in the active jobs list in status", JobName) 217 }) 218 }) 219 220 }) 221 222 /* 223 After writing all this code, you can run` + " `" + `go test ./...` + "`" + ` in your` + " `" + `controllers/` + "`" + ` directory again to run your new test! 224 */ 225 `