Skip to content

Commit b751e7f

Browse files
committed
Add init container for workspace restoration
A new init container is added to the workspace deployment in case user choose to restore the workspace from backup. By setting workspace attribute "controller.devfile.io/restore-workspace" the controller sets a new init container instead of cloning data from git repository. By default an automated path to restore image is used based on cluster settings. However user is capable overwrite that value using another attribute "controller.devfile.io/restore-source-image". The restore container runs a wokspace-recovery.sh script that pull an image using oras an extract files to a /project directory. Signed-off-by: Ales Raszka <araszka@redhat.com>
1 parent cd8852e commit b751e7f

5 files changed

Lines changed: 228 additions & 22 deletions

File tree

controllers/workspace/devworkspace_controller.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"github.com/devfile/devworkspace-operator/pkg/library/home"
4343
kubesync "github.com/devfile/devworkspace-operator/pkg/library/kubernetes"
4444
"github.com/devfile/devworkspace-operator/pkg/library/projects"
45+
"github.com/devfile/devworkspace-operator/pkg/library/restore"
4546
"github.com/devfile/devworkspace-operator/pkg/library/status"
4647
"github.com/devfile/devworkspace-operator/pkg/provision/automount"
4748
"github.com/devfile/devworkspace-operator/pkg/provision/metadata"
@@ -353,21 +354,40 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
353354
if err := projects.ValidateAllProjects(&workspace.Spec.Template); err != nil {
354355
return r.failWorkspace(workspace, fmt.Sprintf("Invalid devfile: %s", err), metrics.ReasonBadRequest, reqLogger, &reconcileStatus), nil
355356
}
356-
// Add init container to clone projects
357-
projectCloneOptions := projects.Options{
358-
Image: workspace.Config.Workspace.ProjectCloneConfig.Image,
359-
Env: env.GetEnvironmentVariablesForProjectClone(workspace),
360-
Resources: workspace.Config.Workspace.ProjectCloneConfig.Resources,
357+
// Add init container to restore workspace from backup if requested
358+
restoreOptions := restore.Options{
359+
Env: env.GetErnvinmentVariablesForProjectRestore(workspace),
361360
}
362-
if workspace.Config.Workspace.ProjectCloneConfig.ImagePullPolicy != "" {
363-
projectCloneOptions.PullPolicy = config.Workspace.ProjectCloneConfig.ImagePullPolicy
361+
if config.Workspace.ImagePullPolicy != "" {
362+
restoreOptions.PullPolicy = corev1.PullPolicy(config.Workspace.ImagePullPolicy)
364363
} else {
365-
projectCloneOptions.PullPolicy = corev1.PullPolicy(config.Workspace.ImagePullPolicy)
364+
restoreOptions.PullPolicy = corev1.PullIfNotPresent
366365
}
367-
if projectClone, err := projects.GetProjectCloneInitContainer(&workspace.Spec.Template, projectCloneOptions, workspace.Config.Routing.ProxyConfig); err != nil {
368-
return r.failWorkspace(workspace, fmt.Sprintf("Failed to set up project-clone init container: %s", err), metrics.ReasonInfrastructureFailure, reqLogger, &reconcileStatus), nil
369-
} else if projectClone != nil {
370-
devfilePodAdditions.InitContainers = append([]corev1.Container{*projectClone}, devfilePodAdditions.InitContainers...)
366+
var workspaceRestoreCreated bool
367+
if workspaceRestore, err := restore.GetWorkspaceRestoreInitContainer(ctx, workspace, clusterAPI.Client, restoreOptions, reqLogger); err != nil {
368+
return r.failWorkspace(workspace, fmt.Sprintf("Failed to set up workspace-restore init container: %s", err), metrics.ReasonInfrastructureFailure, reqLogger, &reconcileStatus), nil
369+
} else if workspaceRestore != nil {
370+
devfilePodAdditions.InitContainers = append([]corev1.Container{*workspaceRestore}, devfilePodAdditions.InitContainers...)
371+
workspaceRestoreCreated = true
372+
}
373+
374+
// Add init container to clone projects only if restore container wasn't created
375+
if !workspaceRestoreCreated {
376+
projectCloneOptions := projects.Options{
377+
Image: workspace.Config.Workspace.ProjectCloneConfig.Image,
378+
Env: env.GetEnvironmentVariablesForProjectClone(workspace),
379+
Resources: workspace.Config.Workspace.ProjectCloneConfig.Resources,
380+
}
381+
if workspace.Config.Workspace.ProjectCloneConfig.ImagePullPolicy != "" {
382+
projectCloneOptions.PullPolicy = config.Workspace.ProjectCloneConfig.ImagePullPolicy
383+
} else {
384+
projectCloneOptions.PullPolicy = corev1.PullPolicy(config.Workspace.ImagePullPolicy)
385+
}
386+
if projectClone, err := projects.GetProjectCloneInitContainer(&workspace.Spec.Template, projectCloneOptions, workspace.Config.Routing.ProxyConfig); err != nil {
387+
return r.failWorkspace(workspace, fmt.Sprintf("Failed to set up project-clone init container: %s", err), metrics.ReasonInfrastructureFailure, reqLogger, &reconcileStatus), nil
388+
} else if projectClone != nil {
389+
devfilePodAdditions.InitContainers = append([]corev1.Container{*projectClone}, devfilePodAdditions.InitContainers...)
390+
}
371391
}
372392

373393
// Inject operator-configured init containers

pkg/constants/attributes.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,24 @@ const (
151151
// of a cloned project. If the bootstrap process is successful, project-clone will automatically remove this attribute
152152
// from the DevWorkspace
153153
BootstrapDevWorkspaceAttribute = "controller.devfile.io/bootstrap-devworkspace"
154+
155+
// WorkspaceRestoreAttribute defines whether workspace restore should be performed when creating a DevWorkspace.
156+
// If this attribute is present, the restore process will be performed during workspace
157+
// initialization before the workspace containers start.
158+
159+
// The backup source is automatically determined from the cluster configuration or can be overridden
160+
// by specifying the WorkspaceRestoreSourceImageAttribute.
161+
WorkspaceRestoreAttribute = "controller.devfile.io/restore-workspace"
162+
163+
// WorkspaceRestoreSourceImageAttribute defines the backup image source to restore from when creating a DevWorkspace.
164+
// The value should be a container image reference containing a workspace backup created by the backup functionality.
165+
// The restore will be performed during workspace initialization before the workspace containers start.
166+
// For example:
167+
//
168+
// spec:
169+
// template:
170+
// attributes:
171+
// controller.devfile.io/restore-source-image: "registry.example.com/backups/my-workspace:20241111-123456"
172+
//
173+
WorkspaceRestoreSourceImageAttribute = "controller.devfile.io/restore-source-image"
154174
)

pkg/library/env/workspaceenv.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,23 @@ func AddCommonEnvironmentVariables(podAdditions *v1alpha1.PodAdditions, clusterD
4949
return nil
5050
}
5151

52+
func GetErnvinmentVariablesForProjectRestore(workspace *common.DevWorkspaceWithConfig) []corev1.EnvVar {
53+
var restoreEnv []corev1.EnvVar
54+
restoreEnv = append(restoreEnv, commonEnvironmentVariables(workspace)...)
55+
restoreEnv = append(restoreEnv, corev1.EnvVar{
56+
Name: devfileConstants.ProjectsRootEnvVar,
57+
Value: constants.DefaultProjectsSourcesRoot,
58+
})
59+
if workspace.Config.Workspace.BackupCronJob.OrasConfig != nil {
60+
restoreEnv = append(restoreEnv, corev1.EnvVar{
61+
Name: "ORAS_EXTRA_ARGS",
62+
Value: workspace.Config.Workspace.BackupCronJob.OrasConfig.ExtraArgs,
63+
})
64+
}
65+
66+
return restoreEnv
67+
}
68+
5269
func GetEnvironmentVariablesForProjectClone(workspace *common.DevWorkspaceWithConfig) []corev1.EnvVar {
5370
var cloneEnv []corev1.EnvVar
5471
cloneEnv = append(cloneEnv, workspace.Config.Workspace.ProjectCloneConfig.Env...)

pkg/library/restore/restore.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//
2+
// Copyright (c) 2019-2025 Red Hat, Inc.
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
// Package restore defines library functions for restoring workspace data from backup images
17+
package restore
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
24+
"github.com/devfile/devworkspace-operator/pkg/common"
25+
devfileConstants "github.com/devfile/devworkspace-operator/pkg/library/constants"
26+
"github.com/devfile/devworkspace-operator/pkg/library/storage"
27+
"github.com/go-logr/logr"
28+
corev1 "k8s.io/api/core/v1"
29+
"sigs.k8s.io/controller-runtime/pkg/client"
30+
31+
"github.com/devfile/devworkspace-operator/internal/images"
32+
"github.com/devfile/devworkspace-operator/pkg/constants"
33+
)
34+
35+
const (
36+
workspaceRestoreContainerName = "workspace-restore"
37+
)
38+
39+
type Options struct {
40+
Image string
41+
PullPolicy corev1.PullPolicy
42+
Resources *corev1.ResourceRequirements
43+
Env []corev1.EnvVar
44+
}
45+
46+
// GetWorkspaceRestoreInitContainer creates an init container that restores workspace data from a backup image.
47+
// The restore container uses the existing workspace-recovery.sh script to extract backup content.
48+
func GetWorkspaceRestoreInitContainer(
49+
ctx context.Context,
50+
workspace *common.DevWorkspaceWithConfig,
51+
k8sClient client.Client,
52+
options Options,
53+
log logr.Logger,
54+
) (*corev1.Container, error) {
55+
wokrspaceTempplate := &workspace.Spec.Template
56+
// Check if restore is requested via workspace attribute
57+
if !wokrspaceTempplate.Attributes.Exists(constants.WorkspaceRestoreAttribute) {
58+
return nil, nil
59+
}
60+
61+
// Get workspace PVC information for mounting into the restore container
62+
pvcName, _, err := storage.GetWorkspacePVCInfo(ctx, workspace.DevWorkspace, workspace.Config, k8sClient, log)
63+
if err != nil {
64+
return nil, fmt.Errorf("failed to resolve workspace PVC info for restore: %w", err)
65+
}
66+
if pvcName == "" {
67+
return nil, fmt.Errorf("no PVC found for workspace %s during restore", workspace.Name)
68+
}
69+
70+
// Determine the source image for restore
71+
var restoreSourceImage string
72+
if wokrspaceTempplate.Attributes.Exists(constants.WorkspaceRestoreSourceImageAttribute) {
73+
// User choose custom image specified in the attribute
74+
restoreSourceImage = wokrspaceTempplate.Attributes.GetString(constants.WorkspaceRestoreSourceImageAttribute, &err)
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to read %s attribute on workspace: %w", constants.WorkspaceRestoreSourceImageAttribute, err)
77+
}
78+
} else {
79+
if workspace.Config.Workspace.BackupCronJob == nil {
80+
return nil, fmt.Errorf("workspace restore requested but backup cron job configuration is missing")
81+
}
82+
if workspace.Config.Workspace.BackupCronJob.Registry == nil || workspace.Config.Workspace.BackupCronJob.Registry.Path == "" {
83+
return nil, fmt.Errorf("workspace restore requested but backup cron job registry is not configured")
84+
}
85+
// Use default backup image location based on workspace info
86+
restoreSourceImage = workspace.Config.Workspace.BackupCronJob.Registry.Path + "/" + workspace.Namespace + "/" + workspace.Name + ":latest"
87+
}
88+
if restoreSourceImage == "" {
89+
return nil, fmt.Errorf("empty value for attribute %s is invalid", constants.WorkspaceRestoreSourceImageAttribute)
90+
}
91+
92+
if !hasContainerComponents(wokrspaceTempplate) {
93+
// Avoid adding restore init container when DevWorkspace does not define any containers
94+
return nil, nil
95+
}
96+
97+
// Use the project backup image which contains the workspace-recovery.sh script
98+
restoreImage := images.GetProjectBackupImage()
99+
100+
// Prepare environment variables for the restore script
101+
env := append(options.Env, []corev1.EnvVar{
102+
{Name: "BACKUP_IMAGE", Value: restoreSourceImage},
103+
}...)
104+
105+
return &corev1.Container{
106+
Name: workspaceRestoreContainerName,
107+
Image: restoreImage,
108+
Command: []string{"/workspace-recovery.sh"},
109+
Args: []string{"--restore"},
110+
Env: env,
111+
VolumeMounts: []corev1.VolumeMount{
112+
{
113+
Name: devfileConstants.ProjectsVolumeName,
114+
MountPath: constants.DefaultProjectsSourcesRoot,
115+
},
116+
},
117+
ImagePullPolicy: options.PullPolicy,
118+
// },
119+
}, nil
120+
}
121+
122+
func hasContainerComponents(workspace *dw.DevWorkspaceTemplateSpec) bool {
123+
for _, component := range workspace.Components {
124+
if component.Container != nil {
125+
return true
126+
}
127+
}
128+
return false
129+
}

project-backup/workspace-recovery.sh

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@
1717
set -euo pipefail
1818

1919
# --- Configuration ---
20-
: "${DEVWORKSPACE_BACKUP_REGISTRY:?Missing DEVWORKSPACE_BACKUP_REGISTRY}"
21-
: "${DEVWORKSPACE_NAMESPACE:?Missing DEVWORKSPACE_NAMESPACE}"
22-
: "${DEVWORKSPACE_NAME:?Missing DEVWORKSPACE_NAME}"
23-
: "${BACKUP_SOURCE_PATH:?Missing BACKUP_SOURCE_PATH}"
2420

25-
BACKUP_IMAGE="${DEVWORKSPACE_BACKUP_REGISTRY}/${DEVWORKSPACE_NAMESPACE}/${DEVWORKSPACE_NAME}:latest"
21+
2622

2723
# --- Functions ---
2824
backup() {
25+
: "${BACKUP_SOURCE_PATH:?Missing BACKUP_SOURCE_PATH}"
26+
: "${DEVWORKSPACE_BACKUP_REGISTRY:?Missing DEVWORKSPACE_BACKUP_REGISTRY}"
27+
: "${DEVWORKSPACE_NAMESPACE:?Missing DEVWORKSPACE_NAMESPACE}"
28+
: "${DEVWORKSPACE_NAME:?Missing DEVWORKSPACE_NAME}"
29+
BACKUP_IMAGE="${DEVWORKSPACE_BACKUP_REGISTRY}/${DEVWORKSPACE_NAMESPACE}/${DEVWORKSPACE_NAME}:latest"
2930
TARBALL_NAME="devworkspace-backup.tar.gz"
3031
cd /tmp
3132
echo "Backing up devworkspace '$DEVWORKSPACE_NAME' in namespace '$DEVWORKSPACE_NAMESPACE' to image '$BACKUP_IMAGE'"
@@ -92,12 +93,31 @@ backup() {
9293
}
9394

9495
restore() {
95-
local container_name="workspace-restore"
96+
: "${PROJECTS_ROOT:?Missing PROJECTS_ROOT}"
97+
98+
echo "Restoring devworkspace from image '$BACKUP_IMAGE' to path '$PROJECTS_ROOT'"
99+
oras_args=(
100+
pull
101+
$BACKUP_IMAGE
102+
--output /tmp
103+
)
104+
105+
if [[ -n "${ORAS_EXTRA_ARGS:-}" ]]; then
106+
extra_args=( ${ORAS_EXTRA_ARGS} )
107+
oras_args+=("${extra_args[@]}")
108+
fi
109+
110+
# Pull the backup tarball from the OCI registry using oras and extract it
111+
oras "${oras_args[@]}"
112+
mkdir /tmp/extracted-backup
113+
tar -xzvf /tmp/devworkspace-backup.tar.gz -C /tmp/extracted-backup
114+
115+
cp -r /tmp/extracted-backup/* "$PROJECTS_ROOT"
116+
117+
rm -f /tmp/devworkspace-backup.tar.gz
118+
rm -rf /tmp/extracted-backup
96119

97-
podman create --name "$container_name" "$BACKUP_IMAGE"
98-
rm -rf "${BACKUP_SOURCE_PATH:?}"/*
99-
podman cp "$container_name":/. "$BACKUP_SOURCE_PATH"
100-
podman rm "$container_name"
120+
echo "Restore completed successfully."
101121
}
102122

103123
usage() {

0 commit comments

Comments
 (0)