From 1df372ce2eede6d1882cdf20650d397a87d9c682 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Wed, 16 Jul 2025 11:57:56 +0530 Subject: [PATCH] test (e2e) : Add basic E2E test scenario to verify dev workspace changes are persisted across restarts Signed-off-by: Rohan Kumar --- test/e2e/pkg/client/client.go | 4 +- test/e2e/pkg/client/devws.go | 28 +++- test/e2e/pkg/client/oc.go | 16 ++- test/e2e/pkg/client/pod.go | 40 +++++- .../pkg/tests/devworkspace_restart_tests.go | 123 ++++++++++++++++++ test/e2e/pkg/tests/devworkspaces_tests.go | 4 +- ...imple-devworkspace-with-project-clone.yaml | 17 +++ 7 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 test/e2e/pkg/tests/devworkspace_restart_tests.go create mode 100644 test/resources/simple-devworkspace-with-project-clone.yaml diff --git a/test/e2e/pkg/client/client.go b/test/e2e/pkg/client/client.go index 1c7d81f9f..da92bd118 100644 --- a/test/e2e/pkg/client/client.go +++ b/test/e2e/pkg/client/client.go @@ -67,7 +67,9 @@ func NewK8sClientWithKubeConfig(kubeconfigFile string) (*K8sClient, error) { return nil, err } - crClient, err := crclient.New(cfg, crclient.Options{}) + crClient, err := crclient.New(cfg, crclient.Options{ + Scheme: scheme, + }) if err != nil { return nil, err } diff --git a/test/e2e/pkg/client/devws.go b/test/e2e/pkg/client/devws.go index eb80a7a9e..ca8859b55 100644 --- a/test/e2e/pkg/client/devws.go +++ b/test/e2e/pkg/client/devws.go @@ -17,14 +17,40 @@ package client import ( "context" + "encoding/json" "errors" "log" "time" + "sigs.k8s.io/controller-runtime/pkg/client" + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "k8s.io/apimachinery/pkg/types" ) +func (w *K8sClient) UpdateDevWorkspaceStarted(name, namespace string, started bool) error { + patch := map[string]interface{}{ + "spec": map[string]interface{}{ + "started": started, + }, + } + patchData, err := json.Marshal(patch) + if err != nil { + return err + } + + target := &dw.DevWorkspace{} + target.ObjectMeta.Name = name + target.ObjectMeta.Namespace = namespace + + err = w.crClient.Patch( + context.TODO(), + target, + client.RawPatch(types.MergePatchType, patchData), + ) + return err +} + // get workspace current dev workspace status from the Custom Resource object func (w *K8sClient) GetDevWsStatus(name, namespace string) (*dw.DevWorkspaceStatus, error) { namespacedName := types.NamespacedName{ @@ -54,7 +80,7 @@ func (w *K8sClient) WaitDevWsStatus(name, namespace string, expectedStatus dw.De if err != nil { return false, err } - log.Printf("Now current status of developer workspace is: %s. Message: %s", currentStatus.Phase, currentStatus.Message) + log.Printf("Now current status of developer workspace %s is: %s. Message: %s", name, currentStatus.Phase, currentStatus.Message) if currentStatus.Phase == dw.DevWorkspaceStatusFailed { return false, errors.New("workspace has been failed unexpectedly. Message: " + currentStatus.Message) } diff --git a/test/e2e/pkg/client/oc.go b/test/e2e/pkg/client/oc.go index 14397558f..419d99283 100644 --- a/test/e2e/pkg/client/oc.go +++ b/test/e2e/pkg/client/oc.go @@ -45,13 +45,25 @@ func (w *K8sClient) OcApplyWorkspace(namespace string, filePath string) (command } // launch 'exec' oc command in the defined pod and container -func (w *K8sClient) ExecCommandInContainer(podName string, namespace, commandInContainer string) (output string, err error) { +func (w *K8sClient) ExecCommandInContainer(podName string, namespace, containerName, commandInContainer string) (output string, err error) { cmd := exec.Command("bash", "-c", fmt.Sprintf( - "KUBECONFIG=%s oc exec %s -n %s -c restricted-access-container -- %s", + "KUBECONFIG=%s oc exec %s -n %s -c %s -- %s", w.kubeCfgFile, podName, namespace, + containerName, commandInContainer)) outBytes, err := cmd.CombinedOutput() return string(outBytes), err } + +func (w *K8sClient) GetLogsForContainer(podName string, namespace, containerName string) (output string, err error) { + cmd := exec.Command("bash", "-c", fmt.Sprintf( + "KUBECONFIG=%s oc logs %s -c %s -n %s", + w.kubeCfgFile, + podName, + containerName, + namespace)) + outBytes, err := cmd.CombinedOutput() + return string(outBytes), err +} diff --git a/test/e2e/pkg/client/pod.go b/test/e2e/pkg/client/pod.go index 1a91b4337..9c87aee6e 100644 --- a/test/e2e/pkg/client/pod.go +++ b/test/e2e/pkg/client/pod.go @@ -112,6 +112,42 @@ func (w *K8sClient) GetPodNameBySelector(selector, namespace string) (string, er if len(podList.Items) == 0 { return "", errors.New(fmt.Sprintf("There is no pod that matches '%s' in namespace %s ", selector, namespace)) } - // we expect just 1 pod in test namespace and return the first value from the list - return podList.Items[0].Name, nil + for _, pod := range podList.Items { + if pod.Status.Phase == v1.PodRunning && pod.DeletionTimestamp == nil { + return pod.Name, nil + } + } + + return "", fmt.Errorf("no running pod found for selector '%s' in namespace %s", selector, namespace) +} + +func (w *K8sClient) WaitForPodContainerToReady(namespace, podName, containerName string) error { + timeout := time.After(6 * time.Minute) + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-timeout: + return fmt.Errorf("timed out waiting for container %s in pod %s to become ready", containerName, podName) + case <-ticker.C: + pod, err := w.Kube().CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("error waiting for pod %s to become ready : %v", podName, err) + } + if w.IsPodContainerReady(containerName, pod) { + return nil + } + log.Printf("Still waiting for container %s in pod %s to become ready...", containerName, podName) + } + } +} + +func (w *K8sClient) IsPodContainerReady(containerName string, pod *v1.Pod) bool { + for _, cs := range pod.Status.ContainerStatuses { + if cs.Name == containerName && cs.Ready { + return true + } + } + return false } diff --git a/test/e2e/pkg/tests/devworkspace_restart_tests.go b/test/e2e/pkg/tests/devworkspace_restart_tests.go new file mode 100644 index 000000000..2e45eee40 --- /dev/null +++ b/test/e2e/pkg/tests/devworkspace_restart_tests.go @@ -0,0 +1,123 @@ +// Copyright (c) 2019-2025 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package tests + +import ( + "fmt" + + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/devworkspace-operator/test/e2e/pkg/config" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +var _ = ginkgo.Describe("[Create DevWorkspace and ensure data is persisted during restarts]", func() { + defer ginkgo.GinkgoRecover() + + ginkgo.It("Wait DevWorkspace Webhook Server Pod", func() { + controllerLabel := "app.kubernetes.io/name=devworkspace-webhook-server" + + deploy, err := config.AdminK8sClient.WaitForPodRunningByLabel(config.OperatorNamespace, controllerLabel) + if err != nil { + ginkgo.Fail(fmt.Sprintf("cannot get the DevWorkspace Webhook Server Pod status with label %s: %s", controllerLabel, err.Error())) + return + } + + if !deploy { + ginkgo.Fail("Devworkspace webhook didn't start properly") + } + }) + + ginkgo.It("Add DevWorkspace to cluster and wait running status", func() { + commandResult, err := config.DevK8sClient.OcApplyWorkspace(config.DevWorkspaceNamespace, "test/resources/simple-devworkspace-with-project-clone.yaml") + if err != nil { + ginkgo.Fail(fmt.Sprintf("Failed to create DevWorkspace: %s %s", err.Error(), commandResult)) + return + } + + deploy, err := config.DevK8sClient.WaitDevWsStatus("code-latest", config.DevWorkspaceNamespace, dw.DevWorkspaceStatusRunning) + if !deploy { + ginkgo.Fail(fmt.Sprintf("DevWorkspace didn't start properly. Error: %s", err)) + } + }) + + var podName string + ginkgo.It("Check that project-clone succeeded as expected", func() { + var err error + podSelector := "controller.devfile.io/devworkspace_name=code-latest" + podName, err = config.AdminK8sClient.GetPodNameBySelector(podSelector, config.DevWorkspaceNamespace) + if err != nil { + ginkgo.Fail(fmt.Sprintf("Can get devworkspace pod by selector. Error: %s", err)) + } + resultOfExecCommand, err := config.AdminK8sClient.GetLogsForContainer(podName, config.DevWorkspaceNamespace, "project-clone") + if err != nil { + ginkgo.Fail(fmt.Sprintf("Cannot get logs for project-clone container. Error: `%s`. Exec output: `%s`", err, resultOfExecCommand)) + } + gomega.Expect(resultOfExecCommand).To(gomega.ContainSubstring("Cloning project web-nodejs-sample to /projects")) + }) + + ginkgo.It("Make some changes in DevWorkspace dev container", func() { + resultOfExecCommand, err := config.AdminK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "dev", "bash -c 'echo \"## Modified via e2e test\" >> /projects/web-nodejs-sample/README.md'") + if err != nil { + ginkgo.Fail(fmt.Sprintf("failed to make changes to DevWorkspace container, returned: %s", resultOfExecCommand)) + } + }) + + ginkgo.It("Stop DevWorkspace", func() { + err := config.AdminK8sClient.UpdateDevWorkspaceStarted("code-latest", config.DevWorkspaceNamespace, false) + if err != nil { + ginkgo.Fail(fmt.Sprintf("failed to stop DevWorkspace container, returned: %s", err)) + } + deploy, err := config.DevK8sClient.WaitDevWsStatus("code-latest", config.DevWorkspaceNamespace, dw.DevWorkspaceStatusStopped) + if !deploy { + ginkgo.Fail(fmt.Sprintf("DevWorkspace didn't start properly. Error: %s", err)) + } + }) + + ginkgo.It("Start DevWorkspace", func() { + err := config.AdminK8sClient.UpdateDevWorkspaceStarted("code-latest", config.DevWorkspaceNamespace, true) + if err != nil { + ginkgo.Fail(fmt.Sprintf("failed to start DevWorkspace container")) + } + deploy, err := config.DevK8sClient.WaitDevWsStatus("code-latest", config.DevWorkspaceNamespace, dw.DevWorkspaceStatusRunning) + if !deploy { + ginkgo.Fail(fmt.Sprintf("DevWorkspace didn't start properly. Error: %s", err)) + } + }) + + ginkgo.It("Verify changes persist after DevWorkspace restart", func() { + podSelector := "controller.devfile.io/devworkspace_name=code-latest" + var err error + podName, err = config.AdminK8sClient.GetPodNameBySelector(podSelector, config.DevWorkspaceNamespace) + if err != nil { + ginkgo.Fail(fmt.Sprintf("Can get devworkspace pod by selector. Error: %s", err)) + } + err = config.AdminK8sClient.WaitForPodContainerToReady(config.DevWorkspaceNamespace, podName, "dev") + if err != nil { + ginkgo.Fail(fmt.Sprintf("failed waiting for DevWorkspace container 'dev' to become ready. Error: %s", err)) + } + resultOfExecCommand, err := config.AdminK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "dev", "bash -c 'cat /projects/web-nodejs-sample/README.md'") + if err != nil { + ginkgo.Fail(fmt.Sprintf("failed to verify to DevWorkspace container, returned: %s", resultOfExecCommand)) + } + gomega.Expect(resultOfExecCommand).To(gomega.ContainSubstring("## Modified via e2e test")) + }) + + ginkgo.It("Check that project-clone logs mention project already cloned", func() { + resultOfExecCommand, err := config.AdminK8sClient.GetLogsForContainer(podName, config.DevWorkspaceNamespace, "project-clone") + if err != nil { + ginkgo.Fail(fmt.Sprintf("Cannot get logs for project-clone container. Error: `%s`. Exec output: `%s`", err, resultOfExecCommand)) + } + gomega.Expect(resultOfExecCommand).To(gomega.ContainSubstring("Project 'web-nodejs-sample' is already cloned and has all remotes configured")) + }) +}) diff --git a/test/e2e/pkg/tests/devworkspaces_tests.go b/test/e2e/pkg/tests/devworkspaces_tests.go index c07946eac..1c1b36cf8 100644 --- a/test/e2e/pkg/tests/devworkspaces_tests.go +++ b/test/e2e/pkg/tests/devworkspaces_tests.go @@ -63,7 +63,7 @@ var _ = ginkgo.Describe("[Create OpenShift Web Terminal Workspace]", func() { if err != nil { ginkgo.Fail(fmt.Sprintf("Can get web terminal pod by selector. Error: %s", err)) } - resultOfExecCommand, err := config.DevK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "echo hello dev") + resultOfExecCommand, err := config.DevK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "restricted-access-container", "echo hello dev") if err != nil { ginkgo.Fail(fmt.Sprintf("Cannot execute command in the devworkspace container. Error: `%s`. Exec output: `%s`", err, resultOfExecCommand)) } @@ -71,7 +71,7 @@ var _ = ginkgo.Describe("[Create OpenShift Web Terminal Workspace]", func() { }) ginkgo.It("Check that not pod owner cannot execute a command in the container", func() { - resultOfExecCommand, err := config.AdminK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "echo hello dev") + resultOfExecCommand, err := config.AdminK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "restricted-access-container", "echo hello dev") if err == nil { ginkgo.Fail(fmt.Sprintf("Admin is not supposed to be able to exec into test terminal but exec is executed successfully and returned: %s", resultOfExecCommand)) } diff --git a/test/resources/simple-devworkspace-with-project-clone.yaml b/test/resources/simple-devworkspace-with-project-clone.yaml new file mode 100644 index 000000000..39b81cbfe --- /dev/null +++ b/test/resources/simple-devworkspace-with-project-clone.yaml @@ -0,0 +1,17 @@ +kind: DevWorkspace +apiVersion: workspace.devfile.io/v1alpha2 +metadata: + name: code-latest +spec: + started: true + template: + projects: + - name: web-nodejs-sample + git: + remotes: + origin: "https://github.com/che-samples/web-nodejs-sample.git" + components: + - name: dev + container: + image: quay.io/wto/web-terminal-tooling:latest + args: ["tail", "-f", "/dev/null"] \ No newline at end of file