From fa59b4771cfdfd94abf126e8255772f846abd320 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 19 Jun 2026 16:17:11 +0100 Subject: [PATCH] fix(backup): use configured imagePullPolicy for backup CronJob The backup CronJob controller hardcodes imagePullPolicy to "Always" on backup Job containers. This does not respect the imagePullPolicy setting from DevWorkspaceOperatorConfig, unlike other workspace-related pods. Read imagePullPolicy from the operator config, falling back to "Always" when the field is not explicitly set. Fixes: https://github.com/devfile/devworkspace-operator/issues/1637 Assisted-by: Claude Code Co-Authored-By: Claude Opus 4.6 Signed-off-by: Chris Brown --- .../backupcronjob/backupcronjob_controller.go | 9 ++- .../backupcronjob_controller_test.go | 69 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/controllers/backupcronjob/backupcronjob_controller.go b/controllers/backupcronjob/backupcronjob_controller.go index 7a03c692d..bc292df99 100644 --- a/controllers/backupcronjob/backupcronjob_controller.go +++ b/controllers/backupcronjob/backupcronjob_controller.go @@ -415,7 +415,7 @@ func (r *BackupCronJobReconciler) createBackupJob( {Name: "ORAS_EXTRA_ARGS", Value: orasExtraArgs}, }, Image: images.GetProjectBackupImage(), - ImagePullPolicy: "Always", + ImagePullPolicy: getImagePullPolicy(dwOperatorConfig), Args: []string{ "/workspace-recovery.sh", "--backup", @@ -486,3 +486,10 @@ func (r *BackupCronJobReconciler) createBackupJob( log.Info("Created backup Job for DevWorkspace", "jobName", job.Name, "devworkspace", workspace.Name) return nil } + +func getImagePullPolicy(dwOperatorConfig *controllerv1alpha1.DevWorkspaceOperatorConfig) corev1.PullPolicy { + if dwOperatorConfig.Config.Workspace.ImagePullPolicy != "" { + return corev1.PullPolicy(dwOperatorConfig.Config.Workspace.ImagePullPolicy) + } + return corev1.PullAlways +} diff --git a/controllers/backupcronjob/backupcronjob_controller_test.go b/controllers/backupcronjob/backupcronjob_controller_test.go index d8c7858ed..c520bc769 100644 --- a/controllers/backupcronjob/backupcronjob_controller_test.go +++ b/controllers/backupcronjob/backupcronjob_controller_test.go @@ -426,6 +426,75 @@ var _ = Describe("BackupCronJobReconciler", func() { Expect(*jobList.Items[0].Spec.BackoffLimit).To(Equal(int32(2))) }) + It("creates a Job with configured imagePullPolicy", func() { + enabled := true + schedule := "* * * * *" + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nameNamespace.Name, Namespace: nameNamespace.Namespace}, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ImagePullPolicy: "IfNotPresent", + BackupCronJob: &controllerv1alpha1.BackupCronJobConfig{ + Enable: &enabled, + Schedule: schedule, + Registry: &controllerv1alpha1.RegistryConfig{ + Path: "fake-registry", + }, + }, + }, + }, + } + Expect(fakeClient.Create(ctx, dwoc)).To(Succeed()) + dw := createDevWorkspace("dw-pullpolicy", "ns-a", false, metav1.NewTime(time.Now().Add(-10*time.Minute))) + dw.Status.Phase = dwv2.DevWorkspaceStatusStopped + dw.Status.DevWorkspaceId = "id-pullpolicy" + Expect(fakeClient.Create(ctx, dw)).To(Succeed()) + + pvc := &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: "claim-devworkspace", Namespace: dw.Namespace}} + Expect(fakeClient.Create(ctx, pvc)).To(Succeed()) + + Expect(reconciler.executeBackupSync(ctx, dwoc, log)).To(Succeed()) + + jobList := &batchv1.JobList{} + Expect(fakeClient.List(ctx, jobList, &client.ListOptions{Namespace: dw.Namespace})).To(Succeed()) + Expect(jobList.Items).To(HaveLen(1)) + Expect(jobList.Items[0].Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullIfNotPresent)) + }) + + It("defaults imagePullPolicy to Always when not configured", func() { + enabled := true + schedule := "* * * * *" + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nameNamespace.Name, Namespace: nameNamespace.Namespace}, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + BackupCronJob: &controllerv1alpha1.BackupCronJobConfig{ + Enable: &enabled, + Schedule: schedule, + Registry: &controllerv1alpha1.RegistryConfig{ + Path: "fake-registry", + }, + }, + }, + }, + } + Expect(fakeClient.Create(ctx, dwoc)).To(Succeed()) + dw := createDevWorkspace("dw-default-policy", "ns-a", false, metav1.NewTime(time.Now().Add(-10*time.Minute))) + dw.Status.Phase = dwv2.DevWorkspaceStatusStopped + dw.Status.DevWorkspaceId = "id-default-policy" + Expect(fakeClient.Create(ctx, dw)).To(Succeed()) + + pvc := &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: "claim-devworkspace", Namespace: dw.Namespace}} + Expect(fakeClient.Create(ctx, pvc)).To(Succeed()) + + Expect(reconciler.executeBackupSync(ctx, dwoc, log)).To(Succeed()) + + jobList := &batchv1.JobList{} + Expect(fakeClient.List(ctx, jobList, &client.ListOptions{Namespace: dw.Namespace})).To(Succeed()) + Expect(jobList.Items).To(HaveLen(1)) + Expect(jobList.Items[0].Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullAlways)) + }) + It("does not create a Job when the DevWorkspace was stopped beyond time range", func() { enabled := true schedule := "* * * * *"