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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"`
Expand Down
32 changes: 32 additions & 0 deletions apis/controller/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 33 additions & 14 deletions controllers/workspace/devworkspace_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
201 changes: 201 additions & 0 deletions controllers/workspace/devworkspace_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)
Expand Down Expand Up @@ -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",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkwon17 For the purposes of the integration tests, I need a valid backup container somewhere in a registry (the container could be empty with vely low size) to be able start and successfully execute the restore container. What would be the best place to upload it?

To verify the tests works I used my local registry, but that's not an option for running it outside of the localhost.

},
},
},
})
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() {
Expand Down
27 changes: 27 additions & 0 deletions controllers/workspace/testdata/restore-workspace-common.yaml
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading