Skip to content

Commit ead3582

Browse files
committed
feat: Update CronJob and Multiversion tutorials to use Status Conditions
- Add Status Conditions support following K8s API conventions - Implement meta.SetStatusCondition() throughout reconciliation - Add condition types: Available, Progressing, Degraded - Update tests to verify Status Conditions behavior - Add test cases for suspended CronJobs This aligns the tutorials with best practices demonstrated in the Deploy Image Plugin example, addressing issue #5024 and #4019. Note: This PR contains manual changes to the tutorial code. The CI will fail as make generate needs updates (will be handled in a follow-up PR per the proposed solution).
1 parent 5687685 commit ead3582

File tree

4 files changed

+384
-12
lines changed

4 files changed

+384
-12
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: 10 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.

0 commit comments

Comments
 (0)