From 9a876c18093c0c6b32f612237ae62626582ca2fe Mon Sep 17 00:00:00 2001 From: Ales Raszka Date: Wed, 14 Jan 2026 12:52:47 +0100 Subject: [PATCH 1/4] 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 --- .../workspace/devworkspace_controller.go | 44 ++++-- pkg/constants/attributes.go | 20 +++ pkg/library/env/workspaceenv.go | 17 +++ pkg/library/restore/restore.go | 129 ++++++++++++++++++ project-backup/workspace-recovery.sh | 40 ++++-- 5 files changed, 228 insertions(+), 22 deletions(-) create mode 100644 pkg/library/restore/restore.go diff --git a/controllers/workspace/devworkspace_controller.go b/controllers/workspace/devworkspace_controller.go index eaa366d0b..108cc1136 100644 --- a/controllers/workspace/devworkspace_controller.go +++ b/controllers/workspace/devworkspace_controller.go @@ -42,6 +42,7 @@ import ( "github.com/devfile/devworkspace-operator/pkg/library/home" kubesync "github.com/devfile/devworkspace-operator/pkg/library/kubernetes" "github.com/devfile/devworkspace-operator/pkg/library/projects" + "github.com/devfile/devworkspace-operator/pkg/library/restore" "github.com/devfile/devworkspace-operator/pkg/library/status" "github.com/devfile/devworkspace-operator/pkg/provision/automount" "github.com/devfile/devworkspace-operator/pkg/provision/metadata" @@ -353,21 +354,40 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request if err := projects.ValidateAllProjects(&workspace.Spec.Template); err != nil { return r.failWorkspace(workspace, fmt.Sprintf("Invalid devfile: %s", err), metrics.ReasonBadRequest, reqLogger, &reconcileStatus), nil } - // Add init container to clone projects - projectCloneOptions := projects.Options{ - Image: workspace.Config.Workspace.ProjectCloneConfig.Image, - Env: env.GetEnvironmentVariablesForProjectClone(workspace), - Resources: workspace.Config.Workspace.ProjectCloneConfig.Resources, + // Add init container to restore workspace from backup if requested + restoreOptions := restore.Options{ + Env: env.GetErnvinmentVariablesForProjectRestore(workspace), } - if workspace.Config.Workspace.ProjectCloneConfig.ImagePullPolicy != "" { - projectCloneOptions.PullPolicy = config.Workspace.ProjectCloneConfig.ImagePullPolicy + if config.Workspace.ImagePullPolicy != "" { + restoreOptions.PullPolicy = corev1.PullPolicy(config.Workspace.ImagePullPolicy) } else { - projectCloneOptions.PullPolicy = corev1.PullPolicy(config.Workspace.ImagePullPolicy) + restoreOptions.PullPolicy = corev1.PullIfNotPresent } - if projectClone, err := projects.GetProjectCloneInitContainer(&workspace.Spec.Template, projectCloneOptions, workspace.Config.Routing.ProxyConfig); err != nil { - return r.failWorkspace(workspace, fmt.Sprintf("Failed to set up project-clone init container: %s", err), metrics.ReasonInfrastructureFailure, reqLogger, &reconcileStatus), nil - } else if projectClone != nil { - devfilePodAdditions.InitContainers = append([]corev1.Container{*projectClone}, devfilePodAdditions.InitContainers...) + var workspaceRestoreCreated bool + if workspaceRestore, err := restore.GetWorkspaceRestoreInitContainer(ctx, workspace, clusterAPI.Client, restoreOptions, reqLogger); err != nil { + return r.failWorkspace(workspace, fmt.Sprintf("Failed to set up workspace-restore init container: %s", err), metrics.ReasonInfrastructureFailure, reqLogger, &reconcileStatus), nil + } else if workspaceRestore != nil { + devfilePodAdditions.InitContainers = append([]corev1.Container{*workspaceRestore}, devfilePodAdditions.InitContainers...) + workspaceRestoreCreated = true + } + + // Add init container to clone projects only if restore container wasn't created + if !workspaceRestoreCreated { + projectCloneOptions := projects.Options{ + Image: workspace.Config.Workspace.ProjectCloneConfig.Image, + Env: env.GetEnvironmentVariablesForProjectClone(workspace), + Resources: workspace.Config.Workspace.ProjectCloneConfig.Resources, + } + if workspace.Config.Workspace.ProjectCloneConfig.ImagePullPolicy != "" { + projectCloneOptions.PullPolicy = config.Workspace.ProjectCloneConfig.ImagePullPolicy + } else { + projectCloneOptions.PullPolicy = corev1.PullPolicy(config.Workspace.ImagePullPolicy) + } + if projectClone, err := projects.GetProjectCloneInitContainer(&workspace.Spec.Template, projectCloneOptions, workspace.Config.Routing.ProxyConfig); err != nil { + return r.failWorkspace(workspace, fmt.Sprintf("Failed to set up project-clone init container: %s", err), metrics.ReasonInfrastructureFailure, reqLogger, &reconcileStatus), nil + } else if projectClone != nil { + devfilePodAdditions.InitContainers = append([]corev1.Container{*projectClone}, devfilePodAdditions.InitContainers...) + } } // Inject operator-configured init containers diff --git a/pkg/constants/attributes.go b/pkg/constants/attributes.go index 484c7c885..3ff2d41fa 100644 --- a/pkg/constants/attributes.go +++ b/pkg/constants/attributes.go @@ -151,4 +151,24 @@ const ( // of a cloned project. If the bootstrap process is successful, project-clone will automatically remove this attribute // from the DevWorkspace BootstrapDevWorkspaceAttribute = "controller.devfile.io/bootstrap-devworkspace" + + // WorkspaceRestoreAttribute defines whether workspace restore should be performed when creating a DevWorkspace. + // If this attribute is present, the restore process will be performed during workspace + // initialization before the workspace containers start. + + // The backup source is automatically determined from the cluster configuration or can be overridden + // by specifying the WorkspaceRestoreSourceImageAttribute. + WorkspaceRestoreAttribute = "controller.devfile.io/restore-workspace" + + // WorkspaceRestoreSourceImageAttribute defines the backup image source to restore from when creating a DevWorkspace. + // The value should be a container image reference containing a workspace backup created by the backup functionality. + // The restore will be performed during workspace initialization before the workspace containers start. + // For example: + // + // spec: + // template: + // attributes: + // controller.devfile.io/restore-source-image: "registry.example.com/backups/my-workspace:20241111-123456" + // + WorkspaceRestoreSourceImageAttribute = "controller.devfile.io/restore-source-image" ) diff --git a/pkg/library/env/workspaceenv.go b/pkg/library/env/workspaceenv.go index 6adf0ab24..0879a130a 100644 --- a/pkg/library/env/workspaceenv.go +++ b/pkg/library/env/workspaceenv.go @@ -49,6 +49,23 @@ func AddCommonEnvironmentVariables(podAdditions *v1alpha1.PodAdditions, clusterD return nil } +func GetErnvinmentVariablesForProjectRestore(workspace *common.DevWorkspaceWithConfig) []corev1.EnvVar { + var restoreEnv []corev1.EnvVar + restoreEnv = append(restoreEnv, commonEnvironmentVariables(workspace)...) + restoreEnv = append(restoreEnv, corev1.EnvVar{ + Name: devfileConstants.ProjectsRootEnvVar, + Value: constants.DefaultProjectsSourcesRoot, + }) + if workspace.Config.Workspace.BackupCronJob.OrasConfig != nil { + restoreEnv = append(restoreEnv, corev1.EnvVar{ + Name: "ORAS_EXTRA_ARGS", + Value: workspace.Config.Workspace.BackupCronJob.OrasConfig.ExtraArgs, + }) + } + + return restoreEnv +} + func GetEnvironmentVariablesForProjectClone(workspace *common.DevWorkspaceWithConfig) []corev1.EnvVar { var cloneEnv []corev1.EnvVar cloneEnv = append(cloneEnv, workspace.Config.Workspace.ProjectCloneConfig.Env...) diff --git a/pkg/library/restore/restore.go b/pkg/library/restore/restore.go new file mode 100644 index 000000000..c3cae8ee6 --- /dev/null +++ b/pkg/library/restore/restore.go @@ -0,0 +1,129 @@ +// +// 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 restore defines library functions for restoring workspace data from backup images +package restore + +import ( + "context" + "fmt" + + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/devworkspace-operator/pkg/common" + devfileConstants "github.com/devfile/devworkspace-operator/pkg/library/constants" + "github.com/devfile/devworkspace-operator/pkg/library/storage" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/devfile/devworkspace-operator/internal/images" + "github.com/devfile/devworkspace-operator/pkg/constants" +) + +const ( + workspaceRestoreContainerName = "workspace-restore" +) + +type Options struct { + Image string + PullPolicy corev1.PullPolicy + Resources *corev1.ResourceRequirements + Env []corev1.EnvVar +} + +// GetWorkspaceRestoreInitContainer creates an init container that restores workspace data from a backup image. +// The restore container uses the existing workspace-recovery.sh script to extract backup content. +func GetWorkspaceRestoreInitContainer( + ctx context.Context, + workspace *common.DevWorkspaceWithConfig, + k8sClient client.Client, + options Options, + log logr.Logger, +) (*corev1.Container, error) { + wokrspaceTempplate := &workspace.Spec.Template + // Check if restore is requested via workspace attribute + if !wokrspaceTempplate.Attributes.Exists(constants.WorkspaceRestoreAttribute) { + return nil, nil + } + + // Get workspace PVC information for mounting into the restore container + pvcName, _, err := storage.GetWorkspacePVCInfo(ctx, workspace.DevWorkspace, workspace.Config, k8sClient, log) + if err != nil { + return nil, fmt.Errorf("failed to resolve workspace PVC info for restore: %w", err) + } + if pvcName == "" { + return nil, fmt.Errorf("no PVC found for workspace %s during restore", workspace.Name) + } + + // Determine the source image for restore + var restoreSourceImage string + if wokrspaceTempplate.Attributes.Exists(constants.WorkspaceRestoreSourceImageAttribute) { + // User choose custom image specified in the attribute + restoreSourceImage = wokrspaceTempplate.Attributes.GetString(constants.WorkspaceRestoreSourceImageAttribute, &err) + if err != nil { + return nil, fmt.Errorf("failed to read %s attribute on workspace: %w", constants.WorkspaceRestoreSourceImageAttribute, err) + } + } else { + if workspace.Config.Workspace.BackupCronJob == nil { + return nil, fmt.Errorf("workspace restore requested but backup cron job configuration is missing") + } + if workspace.Config.Workspace.BackupCronJob.Registry == nil || workspace.Config.Workspace.BackupCronJob.Registry.Path == "" { + return nil, fmt.Errorf("workspace restore requested but backup cron job registry is not configured") + } + // Use default backup image location based on workspace info + restoreSourceImage = workspace.Config.Workspace.BackupCronJob.Registry.Path + "/" + workspace.Namespace + "/" + workspace.Name + ":latest" + } + if restoreSourceImage == "" { + return nil, fmt.Errorf("empty value for attribute %s is invalid", constants.WorkspaceRestoreSourceImageAttribute) + } + + if !hasContainerComponents(wokrspaceTempplate) { + // Avoid adding restore init container when DevWorkspace does not define any containers + return nil, nil + } + + // Use the project backup image which contains the workspace-recovery.sh script + restoreImage := images.GetProjectBackupImage() + + // Prepare environment variables for the restore script + env := append(options.Env, []corev1.EnvVar{ + {Name: "BACKUP_IMAGE", Value: restoreSourceImage}, + }...) + + return &corev1.Container{ + Name: workspaceRestoreContainerName, + Image: restoreImage, + Command: []string{"/workspace-recovery.sh"}, + Args: []string{"--restore"}, + Env: env, + VolumeMounts: []corev1.VolumeMount{ + { + Name: devfileConstants.ProjectsVolumeName, + MountPath: constants.DefaultProjectsSourcesRoot, + }, + }, + ImagePullPolicy: options.PullPolicy, + // }, + }, nil +} + +func hasContainerComponents(workspace *dw.DevWorkspaceTemplateSpec) bool { + for _, component := range workspace.Components { + if component.Container != nil { + return true + } + } + return false +} diff --git a/project-backup/workspace-recovery.sh b/project-backup/workspace-recovery.sh index 440176f62..214767bb7 100644 --- a/project-backup/workspace-recovery.sh +++ b/project-backup/workspace-recovery.sh @@ -17,15 +17,16 @@ set -euo pipefail # --- Configuration --- -: "${DEVWORKSPACE_BACKUP_REGISTRY:?Missing DEVWORKSPACE_BACKUP_REGISTRY}" -: "${DEVWORKSPACE_NAMESPACE:?Missing DEVWORKSPACE_NAMESPACE}" -: "${DEVWORKSPACE_NAME:?Missing DEVWORKSPACE_NAME}" -: "${BACKUP_SOURCE_PATH:?Missing BACKUP_SOURCE_PATH}" -BACKUP_IMAGE="${DEVWORKSPACE_BACKUP_REGISTRY}/${DEVWORKSPACE_NAMESPACE}/${DEVWORKSPACE_NAME}:latest" + # --- Functions --- backup() { + : "${BACKUP_SOURCE_PATH:?Missing BACKUP_SOURCE_PATH}" + : "${DEVWORKSPACE_BACKUP_REGISTRY:?Missing DEVWORKSPACE_BACKUP_REGISTRY}" + : "${DEVWORKSPACE_NAMESPACE:?Missing DEVWORKSPACE_NAMESPACE}" + : "${DEVWORKSPACE_NAME:?Missing DEVWORKSPACE_NAME}" + BACKUP_IMAGE="${DEVWORKSPACE_BACKUP_REGISTRY}/${DEVWORKSPACE_NAMESPACE}/${DEVWORKSPACE_NAME}:latest" TARBALL_NAME="devworkspace-backup.tar.gz" cd /tmp echo "Backing up devworkspace '$DEVWORKSPACE_NAME' in namespace '$DEVWORKSPACE_NAMESPACE' to image '$BACKUP_IMAGE'" @@ -92,12 +93,31 @@ backup() { } restore() { - local container_name="workspace-restore" + : "${PROJECTS_ROOT:?Missing PROJECTS_ROOT}" + + echo "Restoring devworkspace from image '$BACKUP_IMAGE' to path '$PROJECTS_ROOT'" + oras_args=( + pull + $BACKUP_IMAGE + --output /tmp + ) + + if [[ -n "${ORAS_EXTRA_ARGS:-}" ]]; then + extra_args=( ${ORAS_EXTRA_ARGS} ) + oras_args+=("${extra_args[@]}") + fi + + # Pull the backup tarball from the OCI registry using oras and extract it + oras "${oras_args[@]}" + mkdir /tmp/extracted-backup + tar -xzvf /tmp/devworkspace-backup.tar.gz -C /tmp/extracted-backup + + cp -r /tmp/extracted-backup/* "$PROJECTS_ROOT" + + rm -f /tmp/devworkspace-backup.tar.gz + rm -rf /tmp/extracted-backup - podman create --name "$container_name" "$BACKUP_IMAGE" - rm -rf "${BACKUP_SOURCE_PATH:?}"/* - podman cp "$container_name":/. "$BACKUP_SOURCE_PATH" - podman rm "$container_name" + echo "Restore completed successfully." } usage() { From 214b762b10197d6498443016a0f8efbcc6d8a0f7 Mon Sep 17 00:00:00 2001 From: Ales Raszka Date: Fri, 16 Jan 2026 09:51:32 +0100 Subject: [PATCH 2/4] test: Add integration tests for restore feature A new tests that verifies the workspace is created from a backup. It checks if a deployment is ready and if it contains a new restore init container with proper configuration. There are 2 tests - one focused on common pvc and other that have per-workspace storage. Signed-off-by: Ales Raszka --- .../workspace/devworkspace_controller_test.go | 152 ++++++++++++++++++ .../testdata/restore-workspace-common.yaml | 27 ++++ .../restore-workspace-perworkspace.yaml | 30 ++++ pkg/library/projects/clone.go | 4 +- pkg/library/restore/restore.go | 4 +- 5 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 controllers/workspace/testdata/restore-workspace-common.yaml create mode 100644 controllers/workspace/testdata/restore-workspace-perworkspace.yaml diff --git a/controllers/workspace/devworkspace_controller_test.go b/controllers/workspace/devworkspace_controller_test.go index 9980a227e..57f010bc8 100644 --- a/controllers/workspace/devworkspace_controller_test.go +++ b/controllers/workspace/devworkspace_controller_test.go @@ -28,6 +28,8 @@ import ( "github.com/devfile/devworkspace-operator/pkg/conditions" "github.com/devfile/devworkspace-operator/pkg/config" "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/devfile/devworkspace-operator/pkg/library/projects" + "github.com/devfile/devworkspace-operator/pkg/library/restore" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" @@ -36,6 +38,7 @@ import ( rbacv1 "k8s.io/api/rbac/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" ) @@ -1024,6 +1027,155 @@ var _ = Describe("DevWorkspace Controller", func() { }) }) + Context("Workspace Restore", func() { + const testURL = "test-url" + + BeforeEach(func() { + workspacecontroller.SetupHttpClientsForTesting(&http.Client{ + Transport: &testutil.TestRoundTripper{ + Data: map[string]testutil.TestResponse{ + fmt.Sprintf("%s/healthz", testURL): { + StatusCode: http.StatusOK, + }, + }, + }, + }) + }) + + AfterEach(func() { + deleteDevWorkspace(devWorkspaceName) + workspacecontroller.SetupHttpClientsForTesting(getBasicTestHttpClient()) + }) + + It("Restores workspace from backup with common PVC", func() { + config.SetGlobalConfigForTesting(&controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + BackupCronJob: &controllerv1alpha1.BackupCronJobConfig{ + Enable: ptr.To[bool](true), + Registry: &controllerv1alpha1.RegistryConfig{ + Path: "localhost:5000", + }, + }, + }, + }) + defer config.SetGlobalConfigForTesting(nil) + By("Reading DevWorkspace with restore configuration from testdata file") + createDevWorkspace(devWorkspaceName, "restore-workspace-common.yaml") + devworkspace := getExistingDevWorkspace(devWorkspaceName) + workspaceID := devworkspace.Status.DevWorkspaceId + + By("Waiting for DevWorkspaceRouting to be created") + dwr := &controllerv1alpha1.DevWorkspaceRouting{} + dwrName := common.DevWorkspaceRoutingName(workspaceID) + Eventually(func() error { + return k8sClient.Get(ctx, namespacedName(dwrName, testNamespace), dwr) + }, timeout, interval).Should(Succeed(), "DevWorkspaceRouting should be created") + + By("Manually making Routing ready to continue") + markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID)) + + By("Setting the deployment to have 1 ready replica") + markDeploymentReady(common.DeploymentName(devworkspace.Status.DevWorkspaceId)) + + deployment := &appsv1.Deployment{} + err := k8sClient.Get(ctx, namespacedName(devworkspace.Status.DevWorkspaceId, devworkspace.Namespace), deployment) + Expect(err).ToNot(HaveOccurred(), "Failed to get DevWorkspace deployment") + + initContainers := deployment.Spec.Template.Spec.InitContainers + Expect(len(initContainers)).To(BeNumerically(">", 0), "No initContainers found in deployment") + + var restoreInitContainer corev1.Container + var cloneInitContainer corev1.Container + for _, container := range initContainers { + if container.Name == restore.WorkspaceRestoreContainerName { + restoreInitContainer = container + } + if container.Name == projects.ProjectClonerContainerName { + cloneInitContainer = container + } + } + // Expect(initContainers).To(BeEmpty(), "Init containers should be present in deployment") + Expect(cloneInitContainer.Name).To(BeEmpty(), "Project clone init container should be omitted when restoring from backup") + Expect(restoreInitContainer).ToNot(BeNil(), "Workspace restore init container should not be nil") + Expect(restoreInitContainer.Name).To(Equal(restore.WorkspaceRestoreContainerName), "Workspace restore init container should be present in deployment") + + Expect(restoreInitContainer.Command).To(Equal([]string{"/workspace-recovery.sh"}), "Restore init container should have correct command") + Expect(restoreInitContainer.Args).To(Equal([]string{"--restore"}), "Restore init container should have correct args") + Expect(restoreInitContainer.VolumeMounts).To(ContainElement(corev1.VolumeMount{ + Name: "claim-devworkspace", // PVC name for common storage + MountPath: constants.DefaultProjectsSourcesRoot, + ReadOnly: false, + SubPath: workspaceID + "/projects", // Dynamic workspace ID + projects + SubPathExpr: "", + }), "Restore init container should have workspace storage volume mounted at correct path") + + }) + It("Restores workspace from backup with per-workspace PVC", func() { + config.SetGlobalConfigForTesting(&controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + BackupCronJob: &controllerv1alpha1.BackupCronJobConfig{ + Enable: ptr.To[bool](true), + Registry: &controllerv1alpha1.RegistryConfig{ + Path: "localhost:5000", + }, + }, + }, + }) + defer config.SetGlobalConfigForTesting(nil) + By("Reading DevWorkspace with restore configuration from testdata file") + createDevWorkspace(devWorkspaceName, "restore-workspace-perworkspace.yaml") + devworkspace := getExistingDevWorkspace(devWorkspaceName) + workspaceID := devworkspace.Status.DevWorkspaceId + + By("Waiting for DevWorkspaceRouting to be created") + dwr := &controllerv1alpha1.DevWorkspaceRouting{} + dwrName := common.DevWorkspaceRoutingName(workspaceID) + Eventually(func() error { + return k8sClient.Get(ctx, namespacedName(dwrName, testNamespace), dwr) + }, timeout, interval).Should(Succeed(), "DevWorkspaceRouting should be created") + + By("Manually making Routing ready to continue") + markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID)) + + By("Setting the deployment to have 1 ready replica") + markDeploymentReady(common.DeploymentName(devworkspace.Status.DevWorkspaceId)) + + deployment := &appsv1.Deployment{} + err := k8sClient.Get(ctx, namespacedName(devworkspace.Status.DevWorkspaceId, devworkspace.Namespace), deployment) + Expect(err).ToNot(HaveOccurred(), "Failed to get DevWorkspace deployment") + + initContainers := deployment.Spec.Template.Spec.InitContainers + Expect(len(initContainers)).To(BeNumerically(">", 0), "No initContainers found in deployment") + + var restoreInitContainer corev1.Container + var cloneInitContainer corev1.Container + for _, container := range initContainers { + if container.Name == restore.WorkspaceRestoreContainerName { + restoreInitContainer = container + } + if container.Name == projects.ProjectClonerContainerName { + cloneInitContainer = container + } + } + // Expect(initContainers).To(BeEmpty(), "Init containers should be present in deployment") + Expect(cloneInitContainer.Name).To(BeEmpty(), "Project clone init container should be omitted when restoring from backup") + Expect(restoreInitContainer).ToNot(BeNil(), "Workspace restore init container should not be nil") + Expect(restoreInitContainer.Name).To(Equal(restore.WorkspaceRestoreContainerName), "Workspace restore init container should be present in deployment") + + Expect(restoreInitContainer.Command).To(Equal([]string{"/workspace-recovery.sh"}), "Restore init container should have correct command") + Expect(restoreInitContainer.Args).To(Equal([]string{"--restore"}), "Restore init container should have correct args") + Expect(restoreInitContainer.VolumeMounts).To(ContainElement(corev1.VolumeMount{ + Name: common.PerWorkspacePVCName(workspaceID), + MountPath: constants.DefaultProjectsSourcesRoot, + ReadOnly: false, + SubPath: "projects", + SubPathExpr: "", + }), "Restore init container should have workspace storage volume mounted at correct path") + + }) + + }) + Context("Edge cases", func() { It("Allows Kubernetes and Container components to share same target port on endpoint", func() { diff --git a/controllers/workspace/testdata/restore-workspace-common.yaml b/controllers/workspace/testdata/restore-workspace-common.yaml new file mode 100644 index 000000000..732650878 --- /dev/null +++ b/controllers/workspace/testdata/restore-workspace-common.yaml @@ -0,0 +1,27 @@ +kind: DevWorkspace +apiVersion: workspace.devfile.io/v1alpha2 +metadata: + labels: + controller.devfile.io/creator: "" +spec: + started: true + routingClass: 'basic' + template: + attributes: + controller.devfile.io/storage-type: common + controller.devfile.io/restore-workspace: 'true' + projects: + - name: web-nodejs-sample + git: + remotes: + origin: "https://github.com/che-samples/web-nodejs-sample.git" + components: + - name: web-terminal + container: + image: quay.io/wto/web-terminal-tooling:latest + memoryLimit: 512Mi + mountSources: true + command: + - "tail" + - "-f" + - "/dev/null" diff --git a/controllers/workspace/testdata/restore-workspace-perworkspace.yaml b/controllers/workspace/testdata/restore-workspace-perworkspace.yaml new file mode 100644 index 000000000..29b8ebc2b --- /dev/null +++ b/controllers/workspace/testdata/restore-workspace-perworkspace.yaml @@ -0,0 +1,30 @@ +kind: DevWorkspace +apiVersion: workspace.devfile.io/v1alpha2 +metadata: + labels: + controller.devfile.io/creator: "" +spec: + started: true + routingClass: 'basic' + template: + attributes: + controller.devfile.io/storage-type: per-workspace + controller.devfile.io/restore-workspace: 'true' + projects: + - name: web-nodejs-sample + git: + remotes: + origin: "https://github.com/che-samples/web-nodejs-sample.git" + components: + - volume: + size: 1Gi + name: projects + - name: web-terminal + container: + image: quay.io/wto/web-terminal-tooling:latest + memoryLimit: 512Mi + mountSources: true + command: + - "tail" + - "-f" + - "/dev/null" diff --git a/pkg/library/projects/clone.go b/pkg/library/projects/clone.go index c1e4d9de1..f07e6fdeb 100644 --- a/pkg/library/projects/clone.go +++ b/pkg/library/projects/clone.go @@ -31,7 +31,7 @@ import ( ) const ( - projectClonerContainerName = "project-clone" + ProjectClonerContainerName = "project-clone" ) type Options struct { @@ -118,7 +118,7 @@ func GetProjectCloneInitContainer(workspace *dw.DevWorkspaceTemplateSpec, option } return &corev1.Container{ - Name: projectClonerContainerName, + Name: ProjectClonerContainerName, Image: cloneImage, Env: options.Env, Resources: *resources, diff --git a/pkg/library/restore/restore.go b/pkg/library/restore/restore.go index c3cae8ee6..6fe739596 100644 --- a/pkg/library/restore/restore.go +++ b/pkg/library/restore/restore.go @@ -33,7 +33,7 @@ import ( ) const ( - workspaceRestoreContainerName = "workspace-restore" + WorkspaceRestoreContainerName = "workspace-restore" ) type Options struct { @@ -103,7 +103,7 @@ func GetWorkspaceRestoreInitContainer( }...) return &corev1.Container{ - Name: workspaceRestoreContainerName, + Name: WorkspaceRestoreContainerName, Image: restoreImage, Command: []string{"/workspace-recovery.sh"}, Args: []string{"--restore"}, From 728f1076e16f5ca13eb4120eadd88fe4045fe0b0 Mon Sep 17 00:00:00 2001 From: Ales Raszka Date: Fri, 16 Jan 2026 11:20:50 +0100 Subject: [PATCH 3/4] Move restore container condition to controller level The condition whether an workspace should be restored from workspace was in the restore module itself. This make a reading a code more difficult. Now the condition is checked in the controller itself and restore container is only added when enabled. This commit also fixes few minor changes based on the code review comments: - Licence header - Attribute validation - Add a test for disabled workspace recovery - Typos Signed-off-by: Ales Raszka --- .../workspace/devworkspace_controller.go | 34 ++++++------ .../workspace/devworkspace_controller_test.go | 53 ++++++++++++++++++- pkg/constants/attributes.go | 4 +- pkg/library/env/workspaceenv.go | 2 +- pkg/library/restore/restore.go | 27 ++++------ pkg/library/storage/storage.go | 2 +- 6 files changed, 82 insertions(+), 40 deletions(-) diff --git a/controllers/workspace/devworkspace_controller.go b/controllers/workspace/devworkspace_controller.go index 108cc1136..835edae6a 100644 --- a/controllers/workspace/devworkspace_controller.go +++ b/controllers/workspace/devworkspace_controller.go @@ -354,25 +354,23 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request if err := projects.ValidateAllProjects(&workspace.Spec.Template); err != nil { return r.failWorkspace(workspace, fmt.Sprintf("Invalid devfile: %s", err), metrics.ReasonBadRequest, reqLogger, &reconcileStatus), nil } - // Add init container to restore workspace from backup if requested - restoreOptions := restore.Options{ - Env: env.GetErnvinmentVariablesForProjectRestore(workspace), - } - if config.Workspace.ImagePullPolicy != "" { - restoreOptions.PullPolicy = corev1.PullPolicy(config.Workspace.ImagePullPolicy) + if restore.IsWorkspaceRestoreRequested(&workspace.Spec.Template) { + // Add init container to restore workspace from backup if requested + restoreOptions := restore.Options{ + Env: env.GetEnvironmentVariablesForProjectRestore(workspace), + } + if config.Workspace.ImagePullPolicy != "" { + restoreOptions.PullPolicy = corev1.PullPolicy(config.Workspace.ImagePullPolicy) + } else { + restoreOptions.PullPolicy = corev1.PullIfNotPresent + } + if workspaceRestore, err := restore.GetWorkspaceRestoreInitContainer(ctx, workspace, clusterAPI.Client, restoreOptions, reqLogger); err != nil { + return r.failWorkspace(workspace, fmt.Sprintf("Failed to set up workspace-restore init container: %s", err), metrics.ReasonInfrastructureFailure, reqLogger, &reconcileStatus), nil + } else if workspaceRestore != nil { + devfilePodAdditions.InitContainers = append([]corev1.Container{*workspaceRestore}, devfilePodAdditions.InitContainers...) + } } else { - restoreOptions.PullPolicy = corev1.PullIfNotPresent - } - var workspaceRestoreCreated bool - if workspaceRestore, err := restore.GetWorkspaceRestoreInitContainer(ctx, workspace, clusterAPI.Client, restoreOptions, reqLogger); err != nil { - return r.failWorkspace(workspace, fmt.Sprintf("Failed to set up workspace-restore init container: %s", err), metrics.ReasonInfrastructureFailure, reqLogger, &reconcileStatus), nil - } else if workspaceRestore != nil { - devfilePodAdditions.InitContainers = append([]corev1.Container{*workspaceRestore}, devfilePodAdditions.InitContainers...) - workspaceRestoreCreated = true - } - - // Add init container to clone projects only if restore container wasn't created - if !workspaceRestoreCreated { + // Add init container to clone projects only if restore container wasn't created projectCloneOptions := projects.Options{ Image: workspace.Config.Workspace.ProjectCloneConfig.Image, Env: env.GetEnvironmentVariablesForProjectClone(workspace), diff --git a/controllers/workspace/devworkspace_controller_test.go b/controllers/workspace/devworkspace_controller_test.go index 57f010bc8..2ed223b12 100644 --- a/controllers/workspace/devworkspace_controller_test.go +++ b/controllers/workspace/devworkspace_controller_test.go @@ -1094,7 +1094,6 @@ var _ = Describe("DevWorkspace Controller", func() { cloneInitContainer = container } } - // Expect(initContainers).To(BeEmpty(), "Init containers should be present in deployment") Expect(cloneInitContainer.Name).To(BeEmpty(), "Project clone init container should be omitted when restoring from backup") Expect(restoreInitContainer).ToNot(BeNil(), "Workspace restore init container should not be nil") Expect(restoreInitContainer.Name).To(Equal(restore.WorkspaceRestoreContainerName), "Workspace restore init container should be present in deployment") @@ -1157,7 +1156,6 @@ var _ = Describe("DevWorkspace Controller", func() { cloneInitContainer = container } } - // Expect(initContainers).To(BeEmpty(), "Init containers should be present in deployment") Expect(cloneInitContainer.Name).To(BeEmpty(), "Project clone init container should be omitted when restoring from backup") Expect(restoreInitContainer).ToNot(BeNil(), "Workspace restore init container should not be nil") Expect(restoreInitContainer.Name).To(Equal(restore.WorkspaceRestoreContainerName), "Workspace restore init container should be present in deployment") @@ -1173,6 +1171,57 @@ var _ = Describe("DevWorkspace Controller", func() { }), "Restore init container should have workspace storage volume mounted at correct path") }) + It("Doesn't restore workspace from backup if restore is disabled", func() { + config.SetGlobalConfigForTesting(&controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + BackupCronJob: &controllerv1alpha1.BackupCronJobConfig{ + Enable: ptr.To[bool](true), + Registry: &controllerv1alpha1.RegistryConfig{ + Path: "localhost:5000", + }, + }, + }, + }) + defer config.SetGlobalConfigForTesting(nil) + By("Reading DevWorkspace with restore configuration from testdata file") + createDevWorkspace(devWorkspaceName, "restore-workspace-disabled.yaml") + devworkspace := getExistingDevWorkspace(devWorkspaceName) + workspaceID := devworkspace.Status.DevWorkspaceId + + By("Waiting for DevWorkspaceRouting to be created") + dwr := &controllerv1alpha1.DevWorkspaceRouting{} + dwrName := common.DevWorkspaceRoutingName(workspaceID) + Eventually(func() error { + return k8sClient.Get(ctx, namespacedName(dwrName, testNamespace), dwr) + }, timeout, interval).Should(Succeed(), "DevWorkspaceRouting should be created") + + By("Manually making Routing ready to continue") + markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID)) + + By("Setting the deployment to have 1 ready replica") + markDeploymentReady(common.DeploymentName(devworkspace.Status.DevWorkspaceId)) + + deployment := &appsv1.Deployment{} + err := k8sClient.Get(ctx, namespacedName(devworkspace.Status.DevWorkspaceId, devworkspace.Namespace), deployment) + Expect(err).ToNot(HaveOccurred(), "Failed to get DevWorkspace deployment") + + initContainers := deployment.Spec.Template.Spec.InitContainers + Expect(len(initContainers)).To(BeNumerically(">", 0), "No initContainers found in deployment") + + var restoreInitContainer corev1.Container + var cloneInitContainer corev1.Container + for _, container := range initContainers { + if container.Name == restore.WorkspaceRestoreContainerName { + restoreInitContainer = container + } + if container.Name == projects.ProjectClonerContainerName { + cloneInitContainer = container + } + } + Expect(restoreInitContainer.Name).To(BeEmpty(), "Workspace restore init container should be omitted when restore is disabled") + Expect(cloneInitContainer).ToNot(BeNil(), "Project clone init container should not be nil") + + }) }) diff --git a/pkg/constants/attributes.go b/pkg/constants/attributes.go index 3ff2d41fa..3261f8438 100644 --- a/pkg/constants/attributes.go +++ b/pkg/constants/attributes.go @@ -152,7 +152,7 @@ const ( // from the DevWorkspace BootstrapDevWorkspaceAttribute = "controller.devfile.io/bootstrap-devworkspace" - // WorkspaceRestoreAttribute defines whether workspace restore should be performed when creating a DevWorkspace. + // WorkspaceRestoreAttribute defines whether workspace restore should be performed for a DevWorkspace. // If this attribute is present, the restore process will be performed during workspace // initialization before the workspace containers start. @@ -160,7 +160,7 @@ const ( // by specifying the WorkspaceRestoreSourceImageAttribute. WorkspaceRestoreAttribute = "controller.devfile.io/restore-workspace" - // WorkspaceRestoreSourceImageAttribute defines the backup image source to restore from when creating a DevWorkspace. + // WorkspaceRestoreSourceImageAttribute defines the backup image source to restore from for a DevWorkspace. // The value should be a container image reference containing a workspace backup created by the backup functionality. // The restore will be performed during workspace initialization before the workspace containers start. // For example: diff --git a/pkg/library/env/workspaceenv.go b/pkg/library/env/workspaceenv.go index 0879a130a..96533e6d4 100644 --- a/pkg/library/env/workspaceenv.go +++ b/pkg/library/env/workspaceenv.go @@ -49,7 +49,7 @@ func AddCommonEnvironmentVariables(podAdditions *v1alpha1.PodAdditions, clusterD return nil } -func GetErnvinmentVariablesForProjectRestore(workspace *common.DevWorkspaceWithConfig) []corev1.EnvVar { +func GetEnvironmentVariablesForProjectRestore(workspace *common.DevWorkspaceWithConfig) []corev1.EnvVar { var restoreEnv []corev1.EnvVar restoreEnv = append(restoreEnv, commonEnvironmentVariables(workspace)...) restoreEnv = append(restoreEnv, corev1.EnvVar{ diff --git a/pkg/library/restore/restore.go b/pkg/library/restore/restore.go index 6fe739596..340f1678f 100644 --- a/pkg/library/restore/restore.go +++ b/pkg/library/restore/restore.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 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 @@ -23,7 +23,6 @@ import ( dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/devworkspace-operator/pkg/common" devfileConstants "github.com/devfile/devworkspace-operator/pkg/library/constants" - "github.com/devfile/devworkspace-operator/pkg/library/storage" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -43,6 +42,15 @@ type Options struct { Env []corev1.EnvVar } +func IsWorkspaceRestoreRequested(workspace *dw.DevWorkspaceTemplateSpec) bool { + if !workspace.Attributes.Exists(constants.WorkspaceRestoreAttribute) { + return false + } + enableRecovery := workspace.Attributes.GetBoolean(constants.WorkspaceRestoreAttribute, nil) + return enableRecovery + +} + // GetWorkspaceRestoreInitContainer creates an init container that restores workspace data from a backup image. // The restore container uses the existing workspace-recovery.sh script to extract backup content. func GetWorkspaceRestoreInitContainer( @@ -53,21 +61,9 @@ func GetWorkspaceRestoreInitContainer( log logr.Logger, ) (*corev1.Container, error) { wokrspaceTempplate := &workspace.Spec.Template - // Check if restore is requested via workspace attribute - if !wokrspaceTempplate.Attributes.Exists(constants.WorkspaceRestoreAttribute) { - return nil, nil - } - - // Get workspace PVC information for mounting into the restore container - pvcName, _, err := storage.GetWorkspacePVCInfo(ctx, workspace.DevWorkspace, workspace.Config, k8sClient, log) - if err != nil { - return nil, fmt.Errorf("failed to resolve workspace PVC info for restore: %w", err) - } - if pvcName == "" { - return nil, fmt.Errorf("no PVC found for workspace %s during restore", workspace.Name) - } // Determine the source image for restore + var err error var restoreSourceImage string if wokrspaceTempplate.Attributes.Exists(constants.WorkspaceRestoreSourceImageAttribute) { // User choose custom image specified in the attribute @@ -115,7 +111,6 @@ func GetWorkspaceRestoreInitContainer( }, }, ImagePullPolicy: options.PullPolicy, - // }, }, nil } diff --git a/pkg/library/storage/storage.go b/pkg/library/storage/storage.go index f32aecb91..1dbf9b61d 100644 --- a/pkg/library/storage/storage.go +++ b/pkg/library/storage/storage.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 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 From 5480c1c2eac56f80d22d3c04c886a3c0f9ed380a Mon Sep 17 00:00:00 2001 From: Ales Raszka Date: Fri, 16 Jan 2026 11:33:56 +0100 Subject: [PATCH 4/4] Add resources configuration to restore container A new config is added to control the restore container. Default values are set for the new init container. It can be changed by user in the config. The config uses same logic as the project clone container config. Signed-off-by: Ales Raszka --- .../devworkspaceoperatorconfig_types.go | 13 + .../v1alpha1/zz_generated.deepcopy.go | 32 +++ .../workspace/devworkspace_controller.go | 3 +- ...evfile.io_devworkspaceoperatorconfigs.yaml | 218 +++++++++++++++++ deploy/deployment/kubernetes/combined.yaml | 231 ++++++++++++++++++ ...r.devfile.io.CustomResourceDefinition.yaml | 231 ++++++++++++++++++ deploy/deployment/openshift/combined.yaml | 231 ++++++++++++++++++ ...r.devfile.io.CustomResourceDefinition.yaml | 231 ++++++++++++++++++ ...evfile.io_devworkspaceoperatorconfigs.yaml | 231 ++++++++++++++++++ pkg/config/defaults.go | 12 + pkg/config/sync.go | 20 ++ pkg/library/env/workspaceenv.go | 1 + pkg/library/restore/restore.go | 17 +- 13 files changed, 1465 insertions(+), 6 deletions(-) diff --git a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go index bdbcdf377..becde2b39 100644 --- a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go +++ b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go @@ -144,6 +144,9 @@ type WorkspaceConfig struct { // ProjectCloneConfig defines configuration related to the project clone init container // that is used to clone git projects into the DevWorkspace. ProjectCloneConfig *ProjectCloneConfig `json:"projectClone,omitempty"` + // RestoreConfig defines configuration related to the workspace restore init container + // that is used to restore workspace data from a backup image. + RestoreConfig *RestoreConfig `json:"restore,omitempty"` // ImagePullPolicy defines the imagePullPolicy used for containers in a DevWorkspace // For additional information, see Kubernetes documentation for imagePullPolicy. If // not specified, the default value of "Always" is used. @@ -376,6 +379,16 @@ type ProjectCloneConfig struct { Env []corev1.EnvVar `json:"env,omitempty"` } +type RestoreConfig struct { + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + // Resources defines the resource (cpu, memory) limits and requests for the restore + // container. To explicitly not specify a limit or request, define the resource + // quantity as zero ('0') + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + // Env allows defining additional environment variables for the restore container. + Env []corev1.EnvVar `json:"env,omitempty"` +} + type ConfigmapReference struct { // Name is the name of the configmap Name string `json:"name"` diff --git a/apis/controller/v1alpha1/zz_generated.deepcopy.go b/apis/controller/v1alpha1/zz_generated.deepcopy.go index 00f941bcb..fac1c4ed7 100644 --- a/apis/controller/v1alpha1/zz_generated.deepcopy.go +++ b/apis/controller/v1alpha1/zz_generated.deepcopy.go @@ -676,6 +676,33 @@ func (in *RegistryConfig) DeepCopy() *RegistryConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RestoreConfig) DeepCopyInto(out *RestoreConfig) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreConfig. +func (in *RestoreConfig) DeepCopy() *RestoreConfig { + if in == nil { + return nil + } + out := new(RestoreConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RoutingConfig) DeepCopyInto(out *RoutingConfig) { *out = *in @@ -808,6 +835,11 @@ func (in *WorkspaceConfig) DeepCopyInto(out *WorkspaceConfig) { *out = new(ProjectCloneConfig) (*in).DeepCopyInto(*out) } + if in.RestoreConfig != nil { + in, out := &in.RestoreConfig, &out.RestoreConfig + *out = new(RestoreConfig) + (*in).DeepCopyInto(*out) + } if in.ServiceAccount != nil { in, out := &in.ServiceAccount, &out.ServiceAccount *out = new(ServiceAccountConfig) diff --git a/controllers/workspace/devworkspace_controller.go b/controllers/workspace/devworkspace_controller.go index 835edae6a..15c0ab9ea 100644 --- a/controllers/workspace/devworkspace_controller.go +++ b/controllers/workspace/devworkspace_controller.go @@ -357,7 +357,8 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request if restore.IsWorkspaceRestoreRequested(&workspace.Spec.Template) { // Add init container to restore workspace from backup if requested restoreOptions := restore.Options{ - Env: env.GetEnvironmentVariablesForProjectRestore(workspace), + Env: env.GetEnvironmentVariablesForProjectRestore(workspace), + Resources: workspace.Config.Workspace.RestoreConfig.Resources, } if config.Workspace.ImagePullPolicy != "" { restoreOptions.PullPolicy = corev1.PullPolicy(config.Workspace.ImagePullPolicy) diff --git a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml index dceb20d7a..44c5e6c1a 100644 --- a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -4507,6 +4507,224 @@ spec: maxLength: 63 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string + restore: + description: |- + RestoreConfig defines configuration related to the workspace restore init container + that is used to restore workspace data from a backup image. + properties: + env: + description: Env allows defining additional environment variables for the restore container. + items: + description: EnvVar represents an environment variable present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull a container image + type: string + resources: + description: |- + Resources defines the resource (cpu, memory) limits and requests for the restore + container. To explicitly not specify a limit or request, define the resource + quantity as zero ('0') + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object runtimeClassName: description: RuntimeClassName defines the spec.runtimeClassName for DevWorkspace pods created by the DevWorkspace Operator. type: string diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index bab3c69ee..0dd0588aa 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -4727,6 +4727,237 @@ spec: maxLength: 63 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string + restore: + description: |- + RestoreConfig defines configuration related to the workspace restore init container + that is used to restore workspace data from a backup image. + properties: + env: + description: Env allows defining additional environment variables + for the restore container. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + resources: + description: |- + Resources defines the resource (cpu, memory) limits and requests for the restore + container. To explicitly not specify a limit or request, define the resource + quantity as zero ('0') + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object runtimeClassName: description: RuntimeClassName defines the spec.runtimeClassName for DevWorkspace pods created by the DevWorkspace Operator. diff --git a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index dd5a8b46e..a00ea68d0 100644 --- a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -4727,6 +4727,237 @@ spec: maxLength: 63 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string + restore: + description: |- + RestoreConfig defines configuration related to the workspace restore init container + that is used to restore workspace data from a backup image. + properties: + env: + description: Env allows defining additional environment variables + for the restore container. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + resources: + description: |- + Resources defines the resource (cpu, memory) limits and requests for the restore + container. To explicitly not specify a limit or request, define the resource + quantity as zero ('0') + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object runtimeClassName: description: RuntimeClassName defines the spec.runtimeClassName for DevWorkspace pods created by the DevWorkspace Operator. diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index 5bc6c6adb..f08754342 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -4727,6 +4727,237 @@ spec: maxLength: 63 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string + restore: + description: |- + RestoreConfig defines configuration related to the workspace restore init container + that is used to restore workspace data from a backup image. + properties: + env: + description: Env allows defining additional environment variables + for the restore container. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + resources: + description: |- + Resources defines the resource (cpu, memory) limits and requests for the restore + container. To explicitly not specify a limit or request, define the resource + quantity as zero ('0') + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object runtimeClassName: description: RuntimeClassName defines the spec.runtimeClassName for DevWorkspace pods created by the DevWorkspace Operator. diff --git a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index dd5a8b46e..a00ea68d0 100644 --- a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -4727,6 +4727,237 @@ spec: maxLength: 63 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string + restore: + description: |- + RestoreConfig defines configuration related to the workspace restore init container + that is used to restore workspace data from a backup image. + properties: + env: + description: Env allows defining additional environment variables + for the restore container. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + resources: + description: |- + Resources defines the resource (cpu, memory) limits and requests for the restore + container. To explicitly not specify a limit or request, define the resource + quantity as zero ('0') + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object runtimeClassName: description: RuntimeClassName defines the spec.runtimeClassName for DevWorkspace pods created by the DevWorkspace Operator. diff --git a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml index 1953c1886..053712db0 100644 --- a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -4725,6 +4725,237 @@ spec: maxLength: 63 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string + restore: + description: |- + RestoreConfig defines configuration related to the workspace restore init container + that is used to restore workspace data from a backup image. + properties: + env: + description: Env allows defining additional environment variables + for the restore container. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + resources: + description: |- + Resources defines the resource (cpu, memory) limits and requests for the restore + container. To explicitly not specify a limit or request, define the resource + quantity as zero ('0') + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object runtimeClassName: description: RuntimeClassName defines the spec.runtimeClassName for DevWorkspace pods created by the DevWorkspace Operator. diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index ab2f7b26a..c8862670d 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -68,6 +68,18 @@ var defaultConfig = &v1alpha1.OperatorConfiguration{ }, }, }, + RestoreConfig: &v1alpha1.RestoreConfig{ + Resources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1Gi"), + corev1.ResourceCPU: resource.MustParse("500m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("128Mi"), + corev1.ResourceCPU: resource.MustParse("100m"), + }, + }, + }, DefaultContainerResources: &corev1.ResourceRequirements{ Limits: corev1.ResourceList{ corev1.ResourceMemory: resource.MustParse("128Mi"), diff --git a/pkg/config/sync.go b/pkg/config/sync.go index e65222034..d8a5e5173 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -398,6 +398,26 @@ func mergeConfig(from, to *controller.OperatorConfiguration) { to.Workspace.ProjectCloneConfig.Env = from.Workspace.ProjectCloneConfig.Env } } + if from.Workspace.RestoreConfig != nil { + if to.Workspace.RestoreConfig == nil { + to.Workspace.RestoreConfig = &controller.RestoreConfig{} + } + if from.Workspace.RestoreConfig.ImagePullPolicy != "" { + to.Workspace.RestoreConfig.ImagePullPolicy = from.Workspace.RestoreConfig.ImagePullPolicy + } + if from.Workspace.RestoreConfig.Resources != nil { + if to.Workspace.RestoreConfig.Resources == nil { + to.Workspace.RestoreConfig.Resources = &corev1.ResourceRequirements{} + } + to.Workspace.RestoreConfig.Resources = mergeResources(from.Workspace.RestoreConfig.Resources, to.Workspace.RestoreConfig.Resources) + } + + // Overwrite env instead of trying to merge, don't want to bother merging lists when + // the default is empty + if from.Workspace.RestoreConfig.Env != nil { + to.Workspace.RestoreConfig.Env = from.Workspace.RestoreConfig.Env + } + } if from.Workspace.DefaultContainerResources != nil { if to.Workspace.DefaultContainerResources == nil { to.Workspace.DefaultContainerResources = &corev1.ResourceRequirements{} diff --git a/pkg/library/env/workspaceenv.go b/pkg/library/env/workspaceenv.go index 96533e6d4..dd9342b33 100644 --- a/pkg/library/env/workspaceenv.go +++ b/pkg/library/env/workspaceenv.go @@ -51,6 +51,7 @@ func AddCommonEnvironmentVariables(podAdditions *v1alpha1.PodAdditions, clusterD func GetEnvironmentVariablesForProjectRestore(workspace *common.DevWorkspaceWithConfig) []corev1.EnvVar { var restoreEnv []corev1.EnvVar + restoreEnv = append(restoreEnv, workspace.Config.Workspace.RestoreConfig.Env...) restoreEnv = append(restoreEnv, commonEnvironmentVariables(workspace)...) restoreEnv = append(restoreEnv, corev1.EnvVar{ Name: devfileConstants.ProjectsRootEnvVar, diff --git a/pkg/library/restore/restore.go b/pkg/library/restore/restore.go index 340f1678f..822182fed 100644 --- a/pkg/library/restore/restore.go +++ b/pkg/library/restore/restore.go @@ -23,6 +23,7 @@ import ( dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/devworkspace-operator/pkg/common" devfileConstants "github.com/devfile/devworkspace-operator/pkg/library/constants" + dwResources "github.com/devfile/devworkspace-operator/pkg/library/resources" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -98,12 +99,18 @@ func GetWorkspaceRestoreInitContainer( {Name: "BACKUP_IMAGE", Value: restoreSourceImage}, }...) + resources := dwResources.FilterResources(options.Resources) + if err := dwResources.ValidateResources(resources); err != nil { + return nil, fmt.Errorf("invalid resources for project clone container: %w", err) + } + return &corev1.Container{ - Name: WorkspaceRestoreContainerName, - Image: restoreImage, - Command: []string{"/workspace-recovery.sh"}, - Args: []string{"--restore"}, - Env: env, + Name: WorkspaceRestoreContainerName, + Image: restoreImage, + Command: []string{"/workspace-recovery.sh"}, + Args: []string{"--restore"}, + Env: env, + Resources: *resources, VolumeMounts: []corev1.VolumeMount{ { Name: devfileConstants.ProjectsVolumeName,