Skip to content

test (e2e) : Add basic E2E test scenario to verify dev workspace changes are persisted across restarts #1469

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
4 changes: 3 additions & 1 deletion test/e2e/pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
28 changes: 27 additions & 1 deletion test/e2e/pkg/client/devws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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)
}
Expand Down
16 changes: 14 additions & 2 deletions test/e2e/pkg/client/oc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
40 changes: 38 additions & 2 deletions test/e2e/pkg/client/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
123 changes: 123 additions & 0 deletions test/e2e/pkg/tests/devworkspace_restart_tests.go
Original file line number Diff line number Diff line change
@@ -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"))
})
})
4 changes: 2 additions & 2 deletions test/e2e/pkg/tests/devworkspaces_tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@ 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))
}
gomega.Expect(resultOfExecCommand).To(gomega.ContainSubstring("hello dev"))
})

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))
}
Expand Down
17 changes: 17 additions & 0 deletions test/resources/simple-devworkspace-with-project-clone.yaml
Original file line number Diff line number Diff line change
@@ -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"]
Loading