Skip to content

Commit f190816

Browse files
committed
feat: determine backup need based on LastBackupTime
A new sub-object was added to the operator config that reflect a current status of the backup controller and stores a last time the backup was executed. This value is used to determine whether a backup of the workspace is needed or if it already has been executed. Signed-off-by: Ales Raszka <[email protected]>
1 parent ea08f66 commit f190816

File tree

10 files changed

+517
-15
lines changed

10 files changed

+517
-15
lines changed

apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,14 +349,26 @@ type ConfigmapReference struct {
349349
Namespace string `json:"namespace"`
350350
}
351351

352+
type OperatorConfigurationStatus struct {
353+
// Conditions represent the latest available observations of the OperatorConfiguration's state
354+
Conditions []metav1.Condition `json:"conditions,omitempty"`
355+
// LastBackupTime is the timestamp of the last successful backup. Nil if
356+
// no backup is configured or no backup has yet succeeded.
357+
LastBackupTime *metav1.Time `json:"lastBackupTime,omitempty"`
358+
}
359+
352360
// DevWorkspaceOperatorConfig is the Schema for the devworkspaceoperatorconfigs API
353361
// +kubebuilder:object:root=true
362+
// +kubebuilder:subresource:status
354363
// +kubebuilder:resource:path=devworkspaceoperatorconfigs,scope=Namespaced,shortName=dwoc
355364
type DevWorkspaceOperatorConfig struct {
356365
metav1.TypeMeta `json:",inline"`
357366
metav1.ObjectMeta `json:"metadata,omitempty"`
358367

359368
Config *OperatorConfiguration `json:"config,omitempty"`
369+
// Status represents the current status of the DevWorkspaceOperatorConfig
370+
// automatically managed by the DevWorkspace Operator.
371+
Status *OperatorConfigurationStatus `json:"status,omitempty"`
360372
}
361373

362374
// DevWorkspaceOperatorConfigList contains a list of DevWorkspaceOperatorConfig

apis/controller/v1alpha1/zz_generated.deepcopy.go

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

controllers/backupcronjob/backupcronjob_controller.go

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package controllers
1818
import (
1919
"context"
2020

21+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2122
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2223

2324
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
@@ -180,7 +181,7 @@ func (r *BackupCronJobReconciler) isBackupEnabled(config *controllerv1alpha1.Dev
180181
}
181182

182183
// startCron starts the cron scheduler with the backup job according to the provided configuration.
183-
func (r *BackupCronJobReconciler) startCron(ctx context.Context, req ctrl.Request, backUpConfig *controllerv1alpha1.BackupCronJobConfig, logger logr.Logger) {
184+
func (r *BackupCronJobReconciler) startCron(ctx context.Context, dwOperatorConfig *controllerv1alpha1.DevWorkspaceOperatorConfig, logger logr.Logger) {
184185
log := logger.WithName("backup cron")
185186
log.Info("Starting backup cron scheduler")
186187

@@ -193,12 +194,13 @@ func (r *BackupCronJobReconciler) startCron(ctx context.Context, req ctrl.Reques
193194
}
194195

195196
// add cronjob task
197+
backUpConfig := dwOperatorConfig.Config.Workspace.BackupCronJob
196198
log.Info("Adding cronjob task", "schedule", backUpConfig.Schedule)
197199
_, err := r.cron.AddFunc(backUpConfig.Schedule, func() {
198200
taskLog := logger.WithName("cronTask")
199201

200202
taskLog.Info("Starting DevWorkspace backup job")
201-
if err := r.executeBackupSync(ctx, req, backUpConfig, logger); err != nil {
203+
if err := r.executeBackupSync(ctx, dwOperatorConfig, logger); err != nil {
202204
taskLog.Error(err, "Failed to execute backup job for DevWorkspaces")
203205
}
204206
taskLog.Info("DevWorkspace backup job finished")
@@ -224,43 +226,54 @@ func (r *BackupCronJobReconciler) stopCron(logger logr.Logger) {
224226
}
225227

226228
ctx := r.cron.Stop()
227-
ctx.Done()
229+
<-ctx.Done()
228230

229231
log.Info("Cron scheduler stopped")
230232
}
231233

232234
// executeBackupSync executes the backup job for all DevWorkspaces in the cluster that
233235
// have been stopped in the last N minutes.
234-
func (r *BackupCronJobReconciler) executeBackupSync(ctx context.Context, req ctrl.Request, backUpConfig *controllerv1alpha1.BackupCronJobConfig, logger logr.Logger) error {
236+
func (r *BackupCronJobReconciler) executeBackupSync(ctx context.Context, dwOperatorConfig *controllerv1alpha1.DevWorkspaceOperatorConfig, logger logr.Logger) error {
235237
log := logger.WithName("executeBackupSync")
236238
log.Info("Executing backup sync for all DevWorkspaces")
239+
backUpConfig := dwOperatorConfig.Config.Workspace.BackupCronJob
237240
devWorkspaces := &dw.DevWorkspaceList{}
238241
err := r.List(ctx, devWorkspaces)
239242
if err != nil {
240243
log.Error(err, "Failed to list DevWorkspaces")
241244
return err
242245
}
246+
lastBackupTime := dwOperatorConfig.Status.LastBackupTime
243247
for _, dw := range devWorkspaces.Items {
244-
if !r.wasStoppedInTimeRange(&dw, 30, ctx, logger) {
248+
if !r.wasStoppedSinceLastBackup(&dw, lastBackupTime, ctx, logger) {
245249
log.Info("Skipping backup for DevWorkspace that wasn't stopped recently", "namespace", dw.Namespace, "name", dw.Name)
246250
continue
247251
}
248252
dwID := dw.Status.DevWorkspaceId
249253
log.Info("Found DevWorkspace", "namespace", dw.Namespace, "devworkspace", dw.Name, "id", dwID)
250254

251-
if err := r.createBackupJob(&dw, ctx, req, backUpConfig, logger); err != nil {
255+
if err := r.createBackupJob(&dw, ctx, backUpConfig, logger); err != nil {
252256
log.Error(err, "Failed to create backup Job for DevWorkspace", "id", dwID)
253257
continue
254258
}
255259
log.Info("Backup Job created for DevWorkspace", "id", dwID)
256260

257261
}
262+
if dwOperatorConfig.Status == nil {
263+
dwOperatorConfig.Status = &controllerv1alpha1.OperatorConfigurationStatus{}
264+
}
265+
dwOperatorConfig.Status.LastBackupTime = &metav1.Time{Time: metav1.Now().Time}
266+
err = r.Status().Update(ctx, dwOperatorConfig)
267+
if err != nil {
268+
log.Error(err, "Failed to update DevWorkspaceOperatorConfig status with last backup time")
269+
// Not returning error as the backup jobs were created successfully
270+
}
258271
return nil
259272
}
260273

261-
// wasStoppedInTimeRange checks if the DevWorkspace was stopped in the last N minutes.
262-
func (r *BackupCronJobReconciler) wasStoppedInTimeRange(workspace *dw.DevWorkspace, timeRangeInMinute float64, ctx context.Context, logger logr.Logger) bool {
263-
log := logger.WithName("wasStoppedInTimeRange")
274+
// wasStoppedSinceLastBackup checks if the DevWorkspace was stopped since the last backup time.
275+
func (r *BackupCronJobReconciler) wasStoppedSinceLastBackup(workspace *dw.DevWorkspace, lastBackupTime *metav1.Time, logger logr.Logger) bool {
276+
log := logger.WithName("wasStoppedSinceLastBackup")
264277
if workspace.Status.Phase != dw.DevWorkspaceStatusStopped {
265278
return false
266279
}
@@ -273,11 +286,13 @@ func (r *BackupCronJobReconciler) wasStoppedInTimeRange(workspace *dw.DevWorkspa
273286
lastTimeStopped = condition.LastTransitionTime
274287
}
275288
}
276-
// Calculate the time difference
277289
if !lastTimeStopped.IsZero() {
278-
timeDiff := metav1.Now().Sub(lastTimeStopped.Time)
279-
if timeDiff.Minutes() <= timeRangeInMinute {
280-
log.Info("DevWorkspace was stopped recently", "namespace", workspace.Namespace, "name", workspace.Name)
290+
if lastBackupTime == nil {
291+
// No previous backup, so consider it stopped since last backup
292+
return true
293+
}
294+
if lastTimeStopped.Time.After(lastBackupTime.Time) {
295+
log.Info("DevWorkspace was stopped since last backup", "namespace", workspace.Namespace, "name", workspace.Name)
281296
return true
282297
}
283298
}
@@ -290,7 +305,7 @@ func ptrInt32(i int32) *int32 { return &i }
290305
func ptrBool(b bool) *bool { return &b }
291306

292307
// createBackupJob creates a Kubernetes Job to back up the workspace's PVC data.
293-
func (r *BackupCronJobReconciler) createBackupJob(workspace *dw.DevWorkspace, ctx context.Context, req ctrl.Request, backUpConfig *controllerv1alpha1.BackupCronJobConfig, logger logr.Logger) error {
308+
func (r *BackupCronJobReconciler) createBackupJob(workspace *dw.DevWorkspace, ctx context.Context, backUpConfig *controllerv1alpha1.BackupCronJobConfig, logger logr.Logger) error {
294309
log := logger.WithName("createBackupJob")
295310
dwID := workspace.Status.DevWorkspaceId
296311

@@ -304,8 +319,12 @@ func (r *BackupCronJobReconciler) createBackupJob(workspace *dw.DevWorkspace, ct
304319

305320
job := &batchv1.Job{
306321
ObjectMeta: metav1.ObjectMeta{
307-
GenerateName: "backup-job-",
322+
GenerateName: constants.DevWorkspaceBackupJobNamePrefix,
308323
Namespace: workspace.Namespace,
324+
Labels: map[string]string{
325+
constants.DevWorkspaceIDLabel: dwID,
326+
constants.DevWorkspaceBackupJobLabel: "true",
327+
},
309328
},
310329
Spec: batchv1.JobSpec{
311330
Template: corev1.PodTemplateSpec{
@@ -375,6 +394,9 @@ func (r *BackupCronJobReconciler) createBackupJob(workspace *dw.DevWorkspace, ct
375394
},
376395
},
377396
}
397+
if err := controllerutil.SetControllerReference(workspace, job, r.Scheme); err != nil {
398+
return err
399+
}
378400
err = r.Create(ctx, job)
379401
if err != nil {
380402
log.Error(err, "Failed to create backup Job for DevWorkspace", "devworkspace", workspace.Name)

deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml

Lines changed: 70 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deploy/deployment/kubernetes/combined.yaml

Lines changed: 72 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)