From 52800325e62b984356045357910fdc528facfce9 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 19 Jun 2026 16:18:52 +0100 Subject: [PATCH] fix(backup): apply configured podSecurityContext to backup CronJob The backup CronJob controller does not set podSecurityContext on the backup Job pod spec, even though DevWorkspaceOperatorConfig documents that podSecurityContext applies to all workspace-related pods. This can cause permission or SELinux failures when the backup container reads workspace PVC data on clusters with a custom podSecurityContext. Apply the configured podSecurityContext from DevWorkspaceOperatorConfig to the backup Job pod template, consistent with workspace deployments and PVC cleanup jobs. Fixes: https://github.com/devfile/devworkspace-operator/issues/1636 Assisted-by: Claude Code Co-Authored-By: Claude Opus 4.6 Signed-off-by: Chris Brown --- .../backupcronjob/backupcronjob_controller.go | 1 + .../backupcronjob_controller_test.go | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/controllers/backupcronjob/backupcronjob_controller.go b/controllers/backupcronjob/backupcronjob_controller.go index 7a03c692d..31809199a 100644 --- a/controllers/backupcronjob/backupcronjob_controller.go +++ b/controllers/backupcronjob/backupcronjob_controller.go @@ -400,6 +400,7 @@ func (r *BackupCronJobReconciler) createBackupJob( Spec: corev1.PodSpec{ ServiceAccountName: JobRunnerSAName + "-" + workspace.Status.DevWorkspaceId, RestartPolicy: corev1.RestartPolicyNever, + SecurityContext: dwOperatorConfig.Config.Workspace.PodSecurityContext, Containers: []corev1.Container{ { Name: "backup-workspace", diff --git a/controllers/backupcronjob/backupcronjob_controller_test.go b/controllers/backupcronjob/backupcronjob_controller_test.go index d8c7858ed..7287b777e 100644 --- a/controllers/backupcronjob/backupcronjob_controller_test.go +++ b/controllers/backupcronjob/backupcronjob_controller_test.go @@ -426,6 +426,80 @@ var _ = Describe("BackupCronJobReconciler", func() { Expect(*jobList.Items[0].Spec.BackoffLimit).To(Equal(int32(2))) }) + It("creates a Job with configured podSecurityContext", func() { + enabled := true + schedule := "* * * * *" + fsGroupChangeOnRootMismatch := corev1.FSGroupChangeOnRootMismatch + customPodSecurityContext := &corev1.PodSecurityContext{ + FSGroupChangePolicy: &fsGroupChangeOnRootMismatch, + SELinuxOptions: &corev1.SELinuxOptions{Type: "spc_t"}, + } + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nameNamespace.Name, Namespace: nameNamespace.Namespace}, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PodSecurityContext: customPodSecurityContext, + BackupCronJob: &controllerv1alpha1.BackupCronJobConfig{ + Enable: &enabled, + Schedule: schedule, + Registry: &controllerv1alpha1.RegistryConfig{ + Path: "fake-registry", + }, + }, + }, + }, + } + Expect(fakeClient.Create(ctx, dwoc)).To(Succeed()) + dw := createDevWorkspace("dw-secctx", "ns-a", false, metav1.NewTime(time.Now().Add(-10*time.Minute))) + dw.Status.Phase = dwv2.DevWorkspaceStatusStopped + dw.Status.DevWorkspaceId = "id-secctx" + 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.SecurityContext).To(Equal(customPodSecurityContext)) + }) + + It("does not set podSecurityContext 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-no-secctx", "ns-a", false, metav1.NewTime(time.Now().Add(-10*time.Minute))) + dw.Status.Phase = dwv2.DevWorkspaceStatusStopped + dw.Status.DevWorkspaceId = "id-no-secctx" + 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.SecurityContext).To(BeNil()) + }) + It("does not create a Job when the DevWorkspace was stopped beyond time range", func() { enabled := true schedule := "* * * * *"