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 eaa366d0b..15c0ab9ea 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,39 @@ 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, - } - if workspace.Config.Workspace.ProjectCloneConfig.ImagePullPolicy != "" { - projectCloneOptions.PullPolicy = config.Workspace.ProjectCloneConfig.ImagePullPolicy + if restore.IsWorkspaceRestoreRequested(&workspace.Spec.Template) { + // Add init container to restore workspace from backup if requested + restoreOptions := restore.Options{ + Env: env.GetEnvironmentVariablesForProjectRestore(workspace), + Resources: workspace.Config.Workspace.RestoreConfig.Resources, + } + 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 { - 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...) + // 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), + 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/controllers/workspace/devworkspace_controller_test.go b/controllers/workspace/devworkspace_controller_test.go index 9980a227e..2ed223b12 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,204 @@ 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(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(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") + + }) + 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") + + }) + + }) + 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/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/constants/attributes.go b/pkg/constants/attributes.go index 484c7c885..3261f8438 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 for 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 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: + // + // 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..dd9342b33 100644 --- a/pkg/library/env/workspaceenv.go +++ b/pkg/library/env/workspaceenv.go @@ -49,6 +49,24 @@ func AddCommonEnvironmentVariables(podAdditions *v1alpha1.PodAdditions, clusterD return nil } +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, + 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/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 new file mode 100644 index 000000000..822182fed --- /dev/null +++ b/pkg/library/restore/restore.go @@ -0,0 +1,131 @@ +// +// 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 +// +// 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" + 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" + + "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 +} + +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( + ctx context.Context, + workspace *common.DevWorkspaceWithConfig, + k8sClient client.Client, + options Options, + log logr.Logger, +) (*corev1.Container, error) { + wokrspaceTempplate := &workspace.Spec.Template + + // 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 + 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}, + }...) + + 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, + Resources: *resources, + 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/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 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() {