Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 5 additions & 32 deletions controllers/backupcronjob/backupcronjob_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,11 @@ import (
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
"github.com/devfile/devworkspace-operator/internal/images"
"github.com/devfile/devworkspace-operator/pkg/common"
"github.com/devfile/devworkspace-operator/pkg/conditions"
"github.com/devfile/devworkspace-operator/pkg/config"
wkspConfig "github.com/devfile/devworkspace-operator/pkg/config"
"github.com/devfile/devworkspace-operator/pkg/constants"
"github.com/devfile/devworkspace-operator/pkg/infrastructure"
"github.com/devfile/devworkspace-operator/pkg/provision/storage"
"github.com/devfile/devworkspace-operator/pkg/library/storage"
"github.com/go-logr/logr"
"github.com/robfig/cron/v3"
batchv1 "k8s.io/api/batch/v1"
Expand Down Expand Up @@ -353,14 +351,15 @@ func (r *BackupCronJobReconciler) createBackupJob(
}

// Find a PVC with used by the workspace
pvcName, workspacePath, err := r.getWorkspacePVCName(ctx, workspace, dwOperatorConfig, log)
pvcName, workspacePath, err := storage.GetWorkspacePVCInfo(ctx, workspace, dwOperatorConfig.Config, r.Client, log)
if err != nil {
log.Error(err, "Failed to get workspace PVC name", "devworkspace", workspace.Name)
return err
}
if pvcName == "" {
log.Error(err, "No PVC found for DevWorkspace", "id", dwID)
return err
// No PVC to back up
log.Info("No workspace PVC found, skipping backup", "devworkspace", workspace.Name)
return nil
}

pvc := &corev1.PersistentVolumeClaim{}
Expand Down Expand Up @@ -482,32 +481,6 @@ func (r *BackupCronJobReconciler) createBackupJob(
return nil
}

// getWorkspacePVCName determines the PVC name and workspace path based on the storage provisioner used.
func (r *BackupCronJobReconciler) getWorkspacePVCName(ctx context.Context, workspace *dw.DevWorkspace, dwOperatorConfig *controllerv1alpha1.DevWorkspaceOperatorConfig, log logr.Logger) (string, string, error) {
config, err := wkspConfig.ResolveConfigForWorkspace(workspace, r.Client)

workspaceWithConfig := &common.DevWorkspaceWithConfig{}
workspaceWithConfig.DevWorkspace = workspace
workspaceWithConfig.Config = config

storageProvisioner, err := storage.GetProvisioner(workspaceWithConfig)
if err != nil {
return "", "", err
}
if _, ok := storageProvisioner.(*storage.PerWorkspaceStorageProvisioner); ok {
pvcName := common.PerWorkspacePVCName(workspace.Status.DevWorkspaceId)
return pvcName, constants.DefaultProjectsSourcesRoot, nil

} else if _, ok := storageProvisioner.(*storage.CommonStorageProvisioner); ok {
pvcName := constants.DefaultWorkspacePVCName
if dwOperatorConfig.Config.Workspace.PVCName != "" {
pvcName = dwOperatorConfig.Config.Workspace.PVCName
}
return pvcName, workspace.Status.DevWorkspaceId + constants.DefaultProjectsSourcesRoot, nil
}
return "", "", nil
}

func (r *BackupCronJobReconciler) handleRegistryAuthSecret(ctx context.Context, workspace *dw.DevWorkspace,
dwOperatorConfig *controllerv1alpha1.DevWorkspaceOperatorConfig, log logr.Logger,
) (*corev1.Secret, error) {
Expand Down
55 changes: 55 additions & 0 deletions controllers/backupcronjob/backupcronjob_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,39 @@ var _ = Describe("BackupCronJobReconciler", func() {
Expect(fakeClient.List(ctx, jobList, &client.ListOptions{Namespace: dw.Namespace})).To(Succeed())
Expect(jobList.Items).To(HaveLen(1))
})
It("It doesn't create a Job for a DevWorkspace with no PVC", 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",
},
OrasConfig: &controllerv1alpha1.OrasConfig{
ExtraArgs: "--extra-arg1",
},
},
},
},
}
Expect(fakeClient.Create(ctx, dwoc)).To(Succeed())
dw := createDevWorkspace("dw-recent", "ns-a", false, metav1.NewTime(time.Now().Add(-10*time.Minute)))
dw.Spec.Template.Components = []dwv2.Component{} // No volume component, so no PVC
dw.Status.Phase = dwv2.DevWorkspaceStatusStopped
dw.Status.DevWorkspaceId = "id-recent"
Expect(fakeClient.Create(ctx, dw)).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(0))
})
})
Context("ensureJobRunnerRBAC", func() {
It("creates ServiceAccount for Job runner", func() {
Expand Down Expand Up @@ -907,6 +940,28 @@ func createDevWorkspace(name, namespace string, started bool, lastTransitionTime
},
Spec: dwv2.DevWorkspaceSpec{
Started: started,
Template: dwv2.DevWorkspaceTemplateSpec{
DevWorkspaceTemplateSpecContent: dwv2.DevWorkspaceTemplateSpecContent{
Components: []dwv2.Component{
{
Name: "test-container",
ComponentUnion: dwv2.ComponentUnion{
Container: &dwv2.ContainerComponent{
Container: dwv2.Container{
Image: "test-image:latest",
},
},
Volume: &dwv2.VolumeComponent{
Volume: dwv2.Volume{
Ephemeral: pointer.BoolPtr(true),
Size: "1Mi",
},
},
},
},
},
},
},
},
Status: dwv2.DevWorkspaceStatus{
Conditions: []dwv2.DevWorkspaceCondition{},
Expand Down
69 changes: 69 additions & 0 deletions pkg/library/storage/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// Copyright (c) 2019-2025 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package storage

import (
"context"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
"github.com/devfile/devworkspace-operator/pkg/common"
"github.com/devfile/devworkspace-operator/pkg/constants"
"github.com/devfile/devworkspace-operator/pkg/provision/storage"
"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// GetWorkspacePVCInfo determines the PVC name and workspace path based on the storage provisioner used.
// This function can be used by both backup and restore operations to ensure consistent PVC resolution logic.
//
// Returns:
// - pvcName: The name of the PVC that stores workspace data
// - workspacePath: The path within the PVC where workspace data is stored
// - error: Any error that occurred during PVC resolution
func GetWorkspacePVCInfo(
ctx context.Context,
workspace *dw.DevWorkspace,
config *controllerv1alpha1.OperatorConfiguration,
k8sClient client.Client,
log logr.Logger,
) (pvcName string, workspacePath string, err error) {
workspaceWithConfig := &common.DevWorkspaceWithConfig{}
workspaceWithConfig.DevWorkspace = workspace
workspaceWithConfig.Config = config

storageProvisioner, err := storage.GetProvisioner(workspaceWithConfig)
if err != nil {
return "", "", err
}
if !storageProvisioner.NeedsStorage(&workspace.Spec.Template) {
// No storage provisioned for this workspace
return "", "", nil
}

if _, ok := storageProvisioner.(*storage.PerWorkspaceStorageProvisioner); ok {
pvcName := common.PerWorkspacePVCName(workspace.Status.DevWorkspaceId)
return pvcName, constants.DefaultProjectsSourcesRoot, nil

} else if _, ok := storageProvisioner.(*storage.CommonStorageProvisioner); ok {
pvcName := constants.DefaultWorkspacePVCName
if config.Workspace != nil && config.Workspace.PVCName != "" {
pvcName = config.Workspace.PVCName
}
return pvcName, workspace.Status.DevWorkspaceId + constants.DefaultProjectsSourcesRoot, nil
}
return "", "", nil
}
Loading