11/*
2-
32Licensed under the Apache License, Version 2.0 (the "License");
43you may not use this file except in compliance with the License.
54You may obtain a copy of the License at
@@ -14,19 +13,13 @@ limitations under the License.
1413*/
1514// +kubebuilder:docs-gen:collapse=Apache License
1615
17- /*
18- Ideally, we should have one `<kind>_controller_test.go` for each controller scaffolded and called in the `suite_test.go`.
19- So, let's write our example test for the CronJob controller (`cronjob_controller_test.go.`)
20- */
21-
22- /*
23- As usual, we start with the necessary imports. We also define some utility variables.
24- */
2516package controller
2617
2718import (
2819 "context"
20+ "math/rand"
2921 "reflect"
22+ "strings"
3023 "time"
3124
3225 . "github.com/onsi/ginkgo/v2"
@@ -35,179 +28,158 @@ import (
3528 v1 "k8s.io/api/core/v1"
3629 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3730 "k8s.io/apimachinery/pkg/types"
31+ "k8s.io/utils/ptr"
3832
3933 cronjobv1 "tutorial.kubebuilder.io/project/api/v1"
4034)
4135
4236// +kubebuilder:docs-gen:collapse=Imports
4337
44- // Helper function to check if a specific condition exists with expected status
45- func hasCondition (conditions []metav1.Condition , conditionType string , expectedStatus metav1.ConditionStatus ) bool {
46- for _ , condition := range conditions {
47- if condition .Type == conditionType && condition . Status == expectedStatus {
48- return true
38+ // Helper function to check condition status with proper timestamp validation
39+ func assertCondition (conditions []metav1.Condition , conditionType string , expectedStatus metav1.ConditionStatus ) bool {
40+ for _ , cond := range conditions {
41+ if cond .Type == conditionType {
42+ return cond . Status == expectedStatus && ! cond . LastTransitionTime . IsZero ()
4943 }
5044 }
5145 return false
5246}
5347
54- /*
55- The first step to writing a simple integration test is to actually create an instance of CronJob you can run tests against.
56- Note that to create a CronJob, you’ll need to create a stub CronJob struct that contains your CronJob’s specifications.
48+ // Manually implement random string generation (compatible with Ginkgo versions below v2.1.0)
49+ func randomString (length int ) string {
50+ rand .Seed (time .Now ().UnixNano () + int64 (GinkgoParallelProcess ()))
51+ chars := []rune ("abcdefghijklmnopqrstuvwxyz0123456789" )
52+ var sb strings.Builder
53+ for i := 0 ; i < length ; i ++ {
54+ sb .WriteRune (chars [rand .Intn (len (chars ))])
55+ }
56+ return sb .String ()
57+ }
5758
58- Note that when we create a stub CronJob, the CronJob also needs stubs of its required downstream objects.
59- Without the stubbed Job template spec and the Pod template spec below, the Kubernetes API will not be able to
60- create the CronJob.
61- */
62- var _ = Describe ("CronJob controller" , func () {
59+ // Helper to create test CronJob
60+ func createTestCronJob (ctx context.Context , name , namespace , schedule string , suspend bool ) * cronjobv1.CronJob {
61+ cj := & cronjobv1.CronJob {
62+ ObjectMeta : metav1.ObjectMeta {
63+ Name : name ,
64+ Namespace : namespace ,
65+ },
66+ Spec : cronjobv1.CronJobSpec {
67+ Schedule : schedule ,
68+ Suspend : ptr .To (suspend ),
69+ JobTemplate : batchv1.JobTemplateSpec {
70+ Spec : batchv1.JobSpec {
71+ Template : v1.PodTemplateSpec {
72+ Spec : v1.PodSpec {
73+ Containers : []v1.Container {{
74+ Name : "test-container" ,
75+ Image : "busybox" ,
76+ }},
77+ RestartPolicy : v1 .RestartPolicyOnFailure ,
78+ },
79+ },
80+ },
81+ },
82+ },
83+ }
84+ Expect (k8sClient .Create (ctx , cj )).To (Succeed ())
85+ return cj
86+ }
6387
64- // Define utility constants for object names and testing timeouts/durations and intervals.
65- const (
66- CronjobName = "test-cronjob"
67- CronjobNamespace = "default"
68- JobName = "test-job"
88+ // Helper to create owned Job
89+ func createOwnedJob (ctx context.Context , cj * cronjobv1.CronJob , jobName string , active int32 ) * batchv1.Job {
90+ gvk := cronjobv1 .GroupVersion .WithKind (reflect .TypeOf (cronjobv1.CronJob {}).Name ())
91+ job := & batchv1.Job {
92+ ObjectMeta : metav1.ObjectMeta {
93+ Name : jobName ,
94+ Namespace : cj .Namespace ,
95+ OwnerReferences : []metav1.OwnerReference {
96+ * metav1 .NewControllerRef (cj , gvk ),
97+ },
98+ },
99+ Spec : cj .Spec .JobTemplate .Spec ,
100+ }
101+
102+ Expect (k8sClient .Create (ctx , job )).To (Succeed ())
103+
104+ // Update job status
105+ job .Status .Active = active
106+ Expect (k8sClient .Status ().Update (ctx , job )).To (Succeed ())
107+ return job
108+ }
69109
70- timeout = time .Second * 10
71- duration = time .Second * 10
72- interval = time .Millisecond * 250
110+ var _ = Describe ("CronJob controller" , func () {
111+ const (
112+ timeout = time .Second * 15
113+ interval = time .Millisecond * 500
73114 )
74115
75- Context ("When updating CronJob Status" , func () {
76- It ("Should increase CronJob Status.Active count when new Jobs are created" , func () {
77- By ("By creating a new CronJob" )
78- ctx := context .Background ()
79- cronJob := & cronjobv1.CronJob {
80- TypeMeta : metav1.TypeMeta {
81- APIVersion : "batch.tutorial.kubebuilder.io/v1" ,
82- Kind : "CronJob" ,
83- },
84- ObjectMeta : metav1.ObjectMeta {
85- Name : CronjobName ,
86- Namespace : CronjobNamespace ,
87- },
88- Spec : cronjobv1.CronJobSpec {
89- Schedule : "1 * * * *" ,
90- JobTemplate : batchv1.JobTemplateSpec {
91- Spec : batchv1.JobSpec {
92- // For simplicity, we only fill out the required fields.
93- Template : v1.PodTemplateSpec {
94- Spec : v1.PodSpec {
95- // For simplicity, we only fill out the required fields.
96- Containers : []v1.Container {
97- {
98- Name : "test-container" ,
99- Image : "test-image" ,
100- },
101- },
102- RestartPolicy : v1 .RestartPolicyOnFailure ,
103- },
104- },
105- },
106- },
107- },
108- }
109- Expect (k8sClient .Create (ctx , cronJob )).To (Succeed ())
116+ // Use unique names for each test to prevent interference
117+ var (
118+ ns = "cronjob-test-" + randomString (5 ) // Using manually implemented function
119+ ctx context.Context
120+ cronJobName string
121+ namespacedName types.NamespacedName
122+ )
110123
111- /*
112- After creating this CronJob, let's check that the CronJob's Spec fields match what we passed in.
113- 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.
124+ // Setup test namespace before each test
125+ BeforeEach (func () {
126+ ctx = context .Background ()
127+ cronJobName = "test-cj-" + randomString (5 ) // Using manually implemented function
128+ namespacedName = types.NamespacedName {Name : cronJobName , Namespace : ns }
114129
115- `Eventually()` will repeatedly run the function provided as an argument every interval seconds until
116- (a) the assertions done by the passed-in `Gomega` succeed, or
117- (b) the number of attempts * interval period exceed the provided timeout value.
130+ // Create test namespace
131+ nsObj := & v1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : ns }}
132+ Expect (k8sClient .Create (ctx , nsObj )).To (Succeed ())
133+ })
118134
119- In the examples below, timeout and interval are Go Duration values of our choosing.
120- */
135+ // Cleanup after each test
136+ AfterEach (func () {
137+ nsObj := & v1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : ns }}
138+ Expect (k8sClient .Delete (ctx , nsObj )).To (Succeed ())
139+ })
121140
122- cronjobLookupKey := types.NamespacedName {Name : CronjobName , Namespace : CronjobNamespace }
123- createdCronjob := & cronjobv1.CronJob {}
141+ Context ("Basic CronJob reconciliation" , func () {
142+ It ("Should create and reconcile CronJob successfully" , func () {
143+ By ("Creating initial CronJob" )
144+ createTestCronJob (ctx , cronJobName , ns , "*/1 * * * *" , false )
124145
125- // We'll need to retry getting this newly created CronJob, given that creation may not immediately happen.
146+ // Verify creation
147+ createdCj := & cronjobv1.CronJob {}
126148 Eventually (func (g Gomega ) {
127- g .Expect (k8sClient .Get (ctx , cronjobLookupKey , createdCronjob )).To (Succeed ())
149+ g .Expect (k8sClient .Get (ctx , namespacedName , createdCj )).To (Succeed ())
150+ g .Expect (createdCj .Spec .Schedule ).To (Equal ("*/1 * * * *" ))
151+ g .Expect (* createdCj .Spec .Suspend ).To (BeFalse ())
128152 }, timeout , interval ).Should (Succeed ())
129- // Let's make sure our Schedule string value was properly converted/handled.
130- Expect (createdCronjob .Spec .Schedule ).To (Equal ("1 * * * *" ))
131- /*
132- 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.
133- Let’s test the CronJob controller’s logic responsible for updating CronJob.Status.Active with actively running jobs.
134- We’ll verify that when a CronJob has a single active downstream Job, its CronJob.Status.Active field contains a reference to this Job.
135-
136- First, we should get the test CronJob we created earlier, and verify that it currently does not have any active jobs.
137- We use Gomega's `Consistently()` check here to ensure that the active job count remains 0 over a duration of time.
138- */
139- By ("By checking the CronJob has zero active Jobs" )
153+ })
154+ })
155+
156+ Context ("Active Jobs tracking" , func () {
157+ It ("Should update Active count when Jobs are created" , func () {
158+ By ("Creating base CronJob" )
159+ cj := createTestCronJob (ctx , cronJobName , ns , "*/1 * * * *" , false )
160+
161+ By ("Verifying initial state has no active jobs" )
162+ createdCj := & cronjobv1.CronJob {}
140163 Consistently (func (g Gomega ) {
141- g .Expect (k8sClient .Get (ctx , cronjobLookupKey , createdCronjob )).To (Succeed ())
142- g .Expect (createdCronjob .Status .Active ).To (BeEmpty ())
143- }, duration , interval ).Should (Succeed ())
144- /*
145- Next, we actually create a stubbed Job that will belong to our CronJob, as well as its downstream template specs.
146- 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.
147-
148- We then take the stubbed Job and set its owner reference to point to our test CronJob.
149- This ensures that the test Job belongs to, and is tracked by, our test CronJob.
150- Once that’s done, we create our new Job instance.
151- */
152- By ("By creating a new Job" )
153- testJob := & batchv1.Job {
154- ObjectMeta : metav1.ObjectMeta {
155- Name : JobName ,
156- Namespace : CronjobNamespace ,
157- },
158- Spec : batchv1.JobSpec {
159- Template : v1.PodTemplateSpec {
160- Spec : v1.PodSpec {
161- // For simplicity, we only fill out the required fields.
162- Containers : []v1.Container {
163- {
164- Name : "test-container" ,
165- Image : "test-image" ,
166- },
167- },
168- RestartPolicy : v1 .RestartPolicyOnFailure ,
169- },
170- },
171- },
172- }
173-
174- // Note that your CronJob’s GroupVersionKind is required to set up this owner reference.
175- kind := reflect .TypeOf (cronjobv1.CronJob {}).Name ()
176- gvk := cronjobv1 .GroupVersion .WithKind (kind )
177-
178- controllerRef := metav1 .NewControllerRef (createdCronjob , gvk )
179- testJob .SetOwnerReferences ([]metav1.OwnerReference {* controllerRef })
180- Expect (k8sClient .Create (ctx , testJob )).To (Succeed ())
181- // Note that you can not manage the status values while creating the resource.
182- // The status field is managed separately to reflect the current state of the resource.
183- // Therefore, it should be updated using a PATCH or PUT operation after the resource has been created.
184- // Additionally, it is recommended to use StatusConditions to manage the status. For further information see:
185- // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
186- testJob .Status .Active = 2
187- Expect (k8sClient .Status ().Update (ctx , testJob )).To (Succeed ())
188- /*
189- Adding this Job to our test CronJob should trigger our controller’s reconciler logic.
190- After that, we can write a test that evaluates whether our controller eventually updates our CronJob’s Status field as expected!
191- */
192- By ("By checking that the CronJob has one active Job" )
164+ g .Expect (k8sClient .Get (ctx , namespacedName , createdCj )).To (Succeed ())
165+ g .Expect (createdCj .Status .Active ).To (BeEmpty ())
166+ }, time .Second * 3 , interval ).Should (Succeed ())
167+
168+ By ("Creating owned Job" )
169+ job := createOwnedJob (ctx , cj , "test-job-1" , 1 )
170+
171+ By ("Verifying Active count updates" )
193172 Eventually (func (g Gomega ) {
194- g .Expect (k8sClient .Get (ctx , cronjobLookupKey , createdCronjob )).To (Succeed (), "should GET the CronJob" )
195- g .Expect (createdCronjob .Status .Active ).To (HaveLen (1 ), "should have exactly one active job" )
196- g .Expect (createdCronjob .Status .Active [0 ].Name ).To (Equal (JobName ), "the wrong job is active" )
197- }, timeout , interval ).Should (Succeed (), "should list our active job %s in the active jobs list in status" , JobName )
173+ g .Expect (k8sClient .Get (ctx , namespacedName , createdCj )).To (Succeed ())
174+ g .Expect (createdCj .Status .Active ).To (HaveLen (1 ))
175+ g .Expect (createdCj .Status .Active [0 ].Name ).To (Equal (job . Name ) )
176+ }, timeout , interval ).Should (Succeed ())
198177
199- By ("By checking that the CronJob status conditions are properly set" )
178+ By ("Verifying Available condition is set" )
200179 Eventually (func (g Gomega ) {
201- g .Expect (k8sClient .Get (ctx , cronjobLookupKey , createdCronjob )).To (Succeed ())
202- // Check that the Available condition is set to True when job is active
203- g .Expect (hasCondition (createdCronjob .Status .Conditions , "Available" , metav1 .ConditionTrue )).To (BeTrue (),
204- "CronJob should have Available condition set to True" )
180+ g .Expect (k8sClient .Get (ctx , namespacedName , createdCj )).To (Succeed ())
181+ g .Expect (assertCondition (createdCj .Status .Conditions , "Available" , metav1 .ConditionTrue )).To (BeTrue ())
205182 }, timeout , interval ).Should (Succeed ())
206183 })
207184 })
208-
209185})
210-
211- /*
212- After writing all this code, you can run `go test ./...` in your `controllers/` directory again to run your new test!
213- */
0 commit comments