Skip to content

Commit 26abc85

Browse files
committed
Update CronJob and Multiversion tutorials to use Status Conditions
- Add Status Conditions support following K8s API conventions - Move changes to generator code in hack/docs/internal/ as per maintainer feedback - Update tests to verify Status Conditions behavior
1 parent 5e331e7 commit 26abc85

File tree

6 files changed

+486
-18
lines changed

6 files changed

+486
-18
lines changed

docs/book/src/cronjob-tutorial/testdata/project/internal/controller/cronjob_controller.go

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
"github.com/robfig/cron"
3232
kbatch "k8s.io/api/batch/v1"
3333
corev1 "k8s.io/api/core/v1"
34+
apierrors "k8s.io/apimachinery/pkg/api/errors"
35+
"k8s.io/apimachinery/pkg/api/meta"
3436
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3537
"k8s.io/apimachinery/pkg/runtime"
3638
ref "k8s.io/client-go/tools/reference"
@@ -68,6 +70,16 @@ type Clock interface {
6870

6971
// +kubebuilder:docs-gen:collapse=Clock
7072

73+
// Definitions to manage status conditions
74+
const (
75+
// typeAvailableCronJob represents the status of the CronJob reconciliation
76+
typeAvailableCronJob = "Available"
77+
// typeProgressingCronJob represents the status used when the CronJob is being reconciled
78+
typeProgressingCronJob = "Progressing"
79+
// typeDegradedCronJob represents the status used when the CronJob has encountered an error
80+
typeDegradedCronJob = "Degraded"
81+
)
82+
7183
/*
7284
Notice that we need a few more RBAC permissions -- since we're creating and
7385
managing jobs now, we'll need permissions for those, which means adding
@@ -114,11 +126,35 @@ func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
114126
*/
115127
var cronJob batchv1.CronJob
116128
if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil {
117-
log.Error(err, "unable to fetch CronJob")
118-
// we'll ignore not-found errors, since they can't be fixed by an immediate
119-
// requeue (we'll need to wait for a new notification), and we can get them
120-
// on deleted requests.
121-
return ctrl.Result{}, client.IgnoreNotFound(err)
129+
if apierrors.IsNotFound(err) {
130+
// If the custom resource is not found then it usually means that it was deleted or not created
131+
// In this way, we will stop the reconciliation
132+
log.Info("CronJob resource not found. Ignoring since object must be deleted")
133+
return ctrl.Result{}, nil
134+
}
135+
// Error reading the object - requeue the request.
136+
log.Error(err, "Failed to get CronJob")
137+
return ctrl.Result{}, err
138+
}
139+
140+
// Initialize status conditions if not yet present
141+
if len(cronJob.Status.Conditions) == 0 {
142+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
143+
Type: typeProgressingCronJob,
144+
Status: metav1.ConditionUnknown,
145+
Reason: "Reconciling",
146+
Message: "Starting reconciliation",
147+
})
148+
if err := r.Status().Update(ctx, &cronJob); err != nil {
149+
log.Error(err, "Failed to update CronJob status")
150+
return ctrl.Result{}, err
151+
}
152+
153+
// Re-fetch the CronJob after updating the status
154+
if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil {
155+
log.Error(err, "Failed to re-fetch CronJob")
156+
return ctrl.Result{}, err
157+
}
122158
}
123159

124160
/*
@@ -131,6 +167,16 @@ func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
131167
var childJobs kbatch.JobList
132168
if err := r.List(ctx, &childJobs, client.InNamespace(req.Namespace), client.MatchingFields{jobOwnerKey: req.Name}); err != nil {
133169
log.Error(err, "unable to list child Jobs")
170+
// Update status condition to reflect the error
171+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
172+
Type: typeDegradedCronJob,
173+
Status: metav1.ConditionTrue,
174+
Reason: "ReconciliationError",
175+
Message: fmt.Sprintf("Failed to list child jobs: %v", err),
176+
})
177+
if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil {
178+
log.Error(statusErr, "Failed to update CronJob status")
179+
}
134180
return ctrl.Result{}, err
135181
}
136182

@@ -247,6 +293,58 @@ func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
247293
*/
248294
log.V(1).Info("job count", "active jobs", len(activeJobs), "successful jobs", len(successfulJobs), "failed jobs", len(failedJobs))
249295

296+
// Check if CronJob is suspended
297+
isSuspended := cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend
298+
299+
// Update status conditions based on current state
300+
if isSuspended {
301+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
302+
Type: typeAvailableCronJob,
303+
Status: metav1.ConditionFalse,
304+
Reason: "Suspended",
305+
Message: "CronJob is suspended",
306+
})
307+
} else if len(failedJobs) > 0 {
308+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
309+
Type: typeDegradedCronJob,
310+
Status: metav1.ConditionTrue,
311+
Reason: "JobsFailed",
312+
Message: fmt.Sprintf("%d job(s) have failed", len(failedJobs)),
313+
})
314+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
315+
Type: typeAvailableCronJob,
316+
Status: metav1.ConditionFalse,
317+
Reason: "JobsFailed",
318+
Message: fmt.Sprintf("%d job(s) have failed", len(failedJobs)),
319+
})
320+
} else if len(activeJobs) > 0 {
321+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
322+
Type: typeProgressingCronJob,
323+
Status: metav1.ConditionTrue,
324+
Reason: "JobsActive",
325+
Message: fmt.Sprintf("%d job(s) are currently active", len(activeJobs)),
326+
})
327+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
328+
Type: typeAvailableCronJob,
329+
Status: metav1.ConditionTrue,
330+
Reason: "JobsActive",
331+
Message: fmt.Sprintf("CronJob is progressing with %d active job(s)", len(activeJobs)),
332+
})
333+
} else {
334+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
335+
Type: typeAvailableCronJob,
336+
Status: metav1.ConditionTrue,
337+
Reason: "AllJobsCompleted",
338+
Message: "All jobs have completed successfully",
339+
})
340+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
341+
Type: typeProgressingCronJob,
342+
Status: metav1.ConditionFalse,
343+
Reason: "NoJobsActive",
344+
Message: "No jobs are currently active",
345+
})
346+
}
347+
250348
/*
251349
Using the data we've gathered, we'll update the status of our CRD.
252350
Just like before, we use our client. To specifically update the status
@@ -400,6 +498,16 @@ func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
400498
missedRun, nextRun, err := getNextSchedule(&cronJob, r.Now())
401499
if err != nil {
402500
log.Error(err, "unable to figure out CronJob schedule")
501+
// Update status condition to reflect the schedule error
502+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
503+
Type: typeDegradedCronJob,
504+
Status: metav1.ConditionTrue,
505+
Reason: "InvalidSchedule",
506+
Message: fmt.Sprintf("Failed to parse schedule: %v", err),
507+
})
508+
if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil {
509+
log.Error(statusErr, "Failed to update CronJob status")
510+
}
403511
// we don't really care about requeuing until we get an update that
404512
// fixes the schedule, so don't return an error
405513
return ctrl.Result{}, nil
@@ -430,7 +538,16 @@ func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
430538
}
431539
if tooLate {
432540
log.V(1).Info("missed starting deadline for last run, sleeping till next")
433-
// TODO(directxman12): events
541+
// Update status condition to reflect missed deadline
542+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
543+
Type: typeDegradedCronJob,
544+
Status: metav1.ConditionTrue,
545+
Reason: "MissedSchedule",
546+
Message: fmt.Sprintf("Missed starting deadline for run at %v", missedRun),
547+
})
548+
if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil {
549+
log.Error(statusErr, "Failed to update CronJob status")
550+
}
434551
return scheduledResult, nil
435552
}
436553

@@ -511,11 +628,32 @@ func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
511628
// ...and create it on the cluster
512629
if err := r.Create(ctx, job); err != nil {
513630
log.Error(err, "unable to create Job for CronJob", "job", job)
631+
// Update status condition to reflect the error
632+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
633+
Type: typeDegradedCronJob,
634+
Status: metav1.ConditionTrue,
635+
Reason: "JobCreationFailed",
636+
Message: fmt.Sprintf("Failed to create job: %v", err),
637+
})
638+
if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil {
639+
log.Error(statusErr, "Failed to update CronJob status")
640+
}
514641
return ctrl.Result{}, err
515642
}
516643

517644
log.V(1).Info("created Job for CronJob run", "job", job)
518645

646+
// Update status condition to reflect successful job creation
647+
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
648+
Type: typeProgressingCronJob,
649+
Status: metav1.ConditionTrue,
650+
Reason: "JobCreated",
651+
Message: fmt.Sprintf("Created job %s", job.Name),
652+
})
653+
if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil {
654+
log.Error(statusErr, "Failed to update CronJob status")
655+
}
656+
519657
/*
520658
### 7: Requeue when we either see a running job or it's time for the next scheduled run
521659

docs/book/src/cronjob-tutorial/testdata/project/internal/controller/cronjob_controller_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ import (
4141

4242
// +kubebuilder:docs-gen:collapse=Imports
4343

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
49+
}
50+
}
51+
return false
52+
}
53+
4454
/*
4555
The first step to writing a simple integration test is to actually create an instance of CronJob you can run tests against.
4656
Note that to create a CronJob, you’ll need to create a stub CronJob struct that contains your CronJob’s specifications.
@@ -185,6 +195,14 @@ var _ = Describe("CronJob controller", func() {
185195
g.Expect(createdCronjob.Status.Active).To(HaveLen(1), "should have exactly one active job")
186196
g.Expect(createdCronjob.Status.Active[0].Name).To(Equal(JobName), "the wrong job is active")
187197
}, timeout, interval).Should(Succeed(), "should list our active job %s in the active jobs list in status", JobName)
198+
199+
By("By checking that the CronJob status conditions are properly set")
200+
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")
205+
}, timeout, interval).Should(Succeed())
188206
})
189207
})
190208

0 commit comments

Comments
 (0)