Skip to content

Commit 8427ba5

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 9327152 commit 8427ba5

13 files changed

+575
-22
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: 44 additions & 18 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"
@@ -163,8 +164,7 @@ func (r *BackupCronJobReconciler) Reconcile(ctx context.Context, req ctrl.Reques
163164
return ctrl.Result{}, nil
164165
}
165166

166-
backUpConfig := dwOperatorConfig.Config.Workspace.BackupCronJob
167-
r.startCron(ctx, backUpConfig, log)
167+
r.startCron(ctx, dwOperatorConfig, log)
168168

169169
return ctrl.Result{}, nil
170170
}
@@ -180,7 +180,7 @@ func (r *BackupCronJobReconciler) isBackupEnabled(config *controllerv1alpha1.Dev
180180
}
181181

182182
// 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) {
183+
func (r *BackupCronJobReconciler) startCron(ctx context.Context, dwOperatorConfig *controllerv1alpha1.DevWorkspaceOperatorConfig, logger logr.Logger) {
184184
log := logger.WithName("backup cron")
185185
log.Info("Starting backup cron scheduler")
186186

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

195195
// add cronjob task
196+
backUpConfig := dwOperatorConfig.Config.Workspace.BackupCronJob
196197
log.Info("Adding cronjob task", "schedule", backUpConfig.Schedule)
197198
_, err := r.cron.AddFunc(backUpConfig.Schedule, func() {
198199
taskLog := logger.WithName("cronTask")
199200

200201
taskLog.Info("Starting DevWorkspace backup job")
201-
if err := r.executeBackupSync(ctx, req, backUpConfig, logger); err != nil {
202+
if err := r.executeBackupSync(ctx, dwOperatorConfig, logger); err != nil {
202203
taskLog.Error(err, "Failed to execute backup job for DevWorkspaces")
203204
}
204205
taskLog.Info("DevWorkspace backup job finished")
@@ -224,47 +225,63 @@ func (r *BackupCronJobReconciler) stopCron(logger logr.Logger) {
224225
}
225226

226227
ctx := r.cron.Stop()
227-
ctx.Done()
228+
<-ctx.Done()
228229

229230
log.Info("Cron scheduler stopped")
230231
}
231232

232233
// executeBackupSync executes the backup job for all DevWorkspaces in the cluster that
233234
// 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 {
235+
func (r *BackupCronJobReconciler) executeBackupSync(ctx context.Context, dwOperatorConfig *controllerv1alpha1.DevWorkspaceOperatorConfig, logger logr.Logger) error {
235236
log := logger.WithName("executeBackupSync")
236237
log.Info("Executing backup sync for all DevWorkspaces")
238+
backUpConfig := dwOperatorConfig.Config.Workspace.BackupCronJob
237239
devWorkspaces := &dw.DevWorkspaceList{}
238240
err := r.List(ctx, devWorkspaces)
239241
if err != nil {
240242
log.Error(err, "Failed to list DevWorkspaces")
241243
return err
242244
}
245+
var lastBackupTime *metav1.Time
246+
if dwOperatorConfig.Status != nil && dwOperatorConfig.Status.LastBackupTime != nil {
247+
lastBackupTime = dwOperatorConfig.Status.LastBackupTime
248+
}
243249
for _, dw := range devWorkspaces.Items {
244-
if !r.wasStoppedInTimeRange(&dw, 30, ctx, logger) {
250+
if !r.wasStoppedSinceLastBackup(&dw, lastBackupTime, logger) {
245251
log.Info("Skipping backup for DevWorkspace that wasn't stopped recently", "namespace", dw.Namespace, "name", dw.Name)
246252
continue
247253
}
248254
dwID := dw.Status.DevWorkspaceId
249255
log.Info("Found DevWorkspace", "namespace", dw.Namespace, "devworkspace", dw.Name, "id", dwID)
250256

251-
if err := r.createBackupJob(&dw, ctx, req, backUpConfig, logger); err != nil {
257+
if err := r.createBackupJob(&dw, ctx, backUpConfig, logger); err != nil {
252258
log.Error(err, "Failed to create backup Job for DevWorkspace", "id", dwID)
253259
continue
254260
}
255261
log.Info("Backup Job created for DevWorkspace", "id", dwID)
256262

257263
}
264+
origConfig := client.MergeFrom(dwOperatorConfig.DeepCopy())
265+
if dwOperatorConfig.Status == nil {
266+
dwOperatorConfig.Status = &controllerv1alpha1.OperatorConfigurationStatus{}
267+
}
268+
dwOperatorConfig.Status.LastBackupTime = &metav1.Time{Time: metav1.Now().Time}
269+
270+
err = r.Status().Patch(ctx, dwOperatorConfig, origConfig)
271+
if err != nil {
272+
log.Error(err, "Failed to update DevWorkspaceOperatorConfig status with last backup time")
273+
// Not returning error as the backup jobs were created successfully
274+
}
258275
return nil
259276
}
260277

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")
278+
// wasStoppedSinceLastBackup checks if the DevWorkspace was stopped since the last backup time.
279+
func (r *BackupCronJobReconciler) wasStoppedSinceLastBackup(workspace *dw.DevWorkspace, lastBackupTime *metav1.Time, logger logr.Logger) bool {
280+
log := logger.WithName("wasStoppedSinceLastBackup")
264281
if workspace.Status.Phase != dw.DevWorkspaceStatusStopped {
265282
return false
266283
}
267-
log.Info("DevWorkspace is currently stopped, checking if it was stopped recently", "namespace", workspace.Namespace, "name", workspace.Name)
284+
log.Info("DevWorkspace is currently stopped, checking if it was stopped since last backup", "namespace", workspace.Namespace, "name", workspace.Name)
268285
// Check if the workspace was stopped in the last N minutes
269286
if workspace.Status.Conditions != nil {
270287
lastTimeStopped := metav1.Time{}
@@ -273,11 +290,13 @@ func (r *BackupCronJobReconciler) wasStoppedInTimeRange(workspace *dw.DevWorkspa
273290
lastTimeStopped = condition.LastTransitionTime
274291
}
275292
}
276-
// Calculate the time difference
277293
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)
294+
if lastBackupTime == nil {
295+
// No previous backup, so consider it stopped since last backup
296+
return true
297+
}
298+
if lastTimeStopped.Time.After(lastBackupTime.Time) {
299+
log.Info("DevWorkspace was stopped since last backup", "namespace", workspace.Namespace, "name", workspace.Name)
281300
return true
282301
}
283302
}
@@ -290,7 +309,7 @@ func ptrInt32(i int32) *int32 { return &i }
290309
func ptrBool(b bool) *bool { return &b }
291310

292311
// 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 {
312+
func (r *BackupCronJobReconciler) createBackupJob(workspace *dw.DevWorkspace, ctx context.Context, backUpConfig *controllerv1alpha1.BackupCronJobConfig, logger logr.Logger) error {
294313
log := logger.WithName("createBackupJob")
295314
dwID := workspace.Status.DevWorkspaceId
296315

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

305324
job := &batchv1.Job{
306325
ObjectMeta: metav1.ObjectMeta{
307-
GenerateName: "backup-job-",
326+
GenerateName: constants.DevWorkspaceBackupJobNamePrefix,
308327
Namespace: workspace.Namespace,
328+
Labels: map[string]string{
329+
constants.DevWorkspaceIDLabel: dwID,
330+
constants.DevWorkspaceBackupJobLabel: "true",
331+
},
309332
},
310333
Spec: batchv1.JobSpec{
311334
Template: corev1.PodTemplateSpec{
@@ -375,6 +398,9 @@ func (r *BackupCronJobReconciler) createBackupJob(workspace *dw.DevWorkspace, ct
375398
},
376399
},
377400
}
401+
if err := controllerutil.SetControllerReference(workspace, job, r.Scheme); err != nil {
402+
return err
403+
}
378404
err = r.Create(ctx, job)
379405
if err != nil {
380406
log.Error(err, "Failed to create backup Job for DevWorkspace", "devworkspace", workspace.Name)

controllers/backupcronjob/backupcronjob_controller_test.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,20 @@ var _ = Describe("BackupCronJobReconciler", func() {
210210
})
211211

212212
Context("executeBackupSync", func() {
213-
It("creates a Job for a DevWorkspace stopped within last 30 minutes", func() {
213+
It("creates a Job for a DevWorkspace stopped with no previsou backup", func() {
214+
enabled := true
215+
schedule := "* * * * *"
216+
dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{
217+
ObjectMeta: metav1.ObjectMeta{Name: nameNamespace.Name, Namespace: nameNamespace.Namespace},
218+
Config: &controllerv1alpha1.OperatorConfiguration{
219+
Workspace: &controllerv1alpha1.WorkspaceConfig{
220+
BackupCronJob: &controllerv1alpha1.BackupCronJobConfig{
221+
Enable: &enabled,
222+
Schedule: schedule,
223+
},
224+
},
225+
},
226+
}
214227
dw := createDevWorkspace("dw-recent", "ns-a", false, metav1.NewTime(time.Now().Add(-10*time.Minute)))
215228
dw.Status.Phase = dwv2.DevWorkspaceStatusStopped
216229
dw.Status.DevWorkspaceId = "id-recent"
@@ -219,14 +232,31 @@ var _ = Describe("BackupCronJobReconciler", func() {
219232
pvc := &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: "claim-devworkspace", Namespace: dw.Namespace}}
220233
Expect(fakeClient.Create(ctx, pvc)).To(Succeed())
221234

222-
Expect(reconciler.executeBackupSync(ctx, log)).To(Succeed())
235+
Expect(reconciler.executeBackupSync(ctx, dwoc, log)).To(Succeed())
223236

224237
jobList := &batchv1.JobList{}
225238
Expect(fakeClient.List(ctx, jobList, &client.ListOptions{Namespace: dw.Namespace})).To(Succeed())
226239
Expect(jobList.Items).To(HaveLen(1))
227240
})
228241

229242
It("does not create a Job when the DevWorkspace was stopped beyond time range", func() {
243+
enabled := true
244+
schedule := "* * * * *"
245+
lastBackupTime := metav1.NewTime(time.Now().Add(-15 * time.Minute))
246+
dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{
247+
ObjectMeta: metav1.ObjectMeta{Name: nameNamespace.Name, Namespace: nameNamespace.Namespace},
248+
Config: &controllerv1alpha1.OperatorConfiguration{
249+
Workspace: &controllerv1alpha1.WorkspaceConfig{
250+
BackupCronJob: &controllerv1alpha1.BackupCronJobConfig{
251+
Enable: &enabled,
252+
Schedule: schedule,
253+
},
254+
},
255+
},
256+
Status: &controllerv1alpha1.OperatorConfigurationStatus{
257+
LastBackupTime: &lastBackupTime,
258+
},
259+
}
230260
dw := createDevWorkspace("dw-old", "ns-b", false, metav1.NewTime(time.Now().Add(-60*time.Minute)))
231261
dw.Status.Phase = dwv2.DevWorkspaceStatusStopped
232262
dw.Status.DevWorkspaceId = "id-old"
@@ -235,21 +265,34 @@ var _ = Describe("BackupCronJobReconciler", func() {
235265
pvc := &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: "claim-devworkspace", Namespace: dw.Namespace}}
236266
Expect(fakeClient.Create(ctx, pvc)).To(Succeed())
237267

238-
Expect(reconciler.executeBackupSync(ctx, log)).To(Succeed())
268+
Expect(reconciler.executeBackupSync(ctx, dwoc, log)).To(Succeed())
239269

240270
jobList := &batchv1.JobList{}
241271
Expect(fakeClient.List(ctx, jobList, &client.ListOptions{Namespace: dw.Namespace})).To(Succeed())
242272
Expect(jobList.Items).To(HaveLen(0))
243273
})
244274

245275
It("does not create a Job for a running DevWorkspace", func() {
276+
enabled := true
277+
schedule := "* * * * *"
278+
dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{
279+
ObjectMeta: metav1.ObjectMeta{Name: nameNamespace.Name, Namespace: nameNamespace.Namespace},
280+
Config: &controllerv1alpha1.OperatorConfiguration{
281+
Workspace: &controllerv1alpha1.WorkspaceConfig{
282+
BackupCronJob: &controllerv1alpha1.BackupCronJobConfig{
283+
Enable: &enabled,
284+
Schedule: schedule,
285+
},
286+
},
287+
},
288+
}
246289
dw := createDevWorkspace("dw-running", "ns-c", true, metav1.NewTime(time.Now().Add(-5*time.Minute)))
247290
Expect(fakeClient.Create(ctx, dw)).To(Succeed())
248291

249292
pvc := &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: "claim-devworkspace", Namespace: dw.Namespace}}
250293
Expect(fakeClient.Create(ctx, pvc)).To(Succeed())
251294

252-
Expect(reconciler.executeBackupSync(ctx, log)).To(Succeed())
295+
Expect(reconciler.executeBackupSync(ctx, dwoc, log)).To(Succeed())
253296

254297
jobList := &batchv1.JobList{}
255298
Expect(fakeClient.List(ctx, jobList, &client.ListOptions{Namespace: dw.Namespace})).To(Succeed())

0 commit comments

Comments
 (0)