Skip to content

Commit b75a204

Browse files
committed
test (e2e) : Add basic E2E test scenario to verify dev workspace changes are persisted across restarts
Signed-off-by: Rohan Kumar <[email protected]>
1 parent 608bf21 commit b75a204

File tree

7 files changed

+223
-8
lines changed

7 files changed

+223
-8
lines changed

test/e2e/pkg/client/client.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ func NewK8sClientWithKubeConfig(kubeconfigFile string) (*K8sClient, error) {
6767
return nil, err
6868
}
6969

70-
crClient, err := crclient.New(cfg, crclient.Options{})
70+
crClient, err := crclient.New(cfg, crclient.Options{
71+
Scheme: scheme,
72+
})
7173
if err != nil {
7274
return nil, err
7375
}

test/e2e/pkg/client/devws.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,40 @@ package client
1717

1818
import (
1919
"context"
20+
"encoding/json"
2021
"errors"
2122
"log"
2223
"time"
2324

25+
"sigs.k8s.io/controller-runtime/pkg/client"
26+
2427
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
2528
"k8s.io/apimachinery/pkg/types"
2629
)
2730

31+
func (w *K8sClient) UpdateDevWorkspaceStarted(name, namespace string, started bool) error {
32+
patch := map[string]interface{}{
33+
"spec": map[string]interface{}{
34+
"started": started,
35+
},
36+
}
37+
patchData, err := json.Marshal(patch)
38+
if err != nil {
39+
return err
40+
}
41+
42+
target := &dw.DevWorkspace{}
43+
target.ObjectMeta.Name = name
44+
target.ObjectMeta.Namespace = namespace
45+
46+
err = w.crClient.Patch(
47+
context.TODO(),
48+
target,
49+
client.RawPatch(types.MergePatchType, patchData),
50+
)
51+
return err
52+
}
53+
2854
// get workspace current dev workspace status from the Custom Resource object
2955
func (w *K8sClient) GetDevWsStatus(name, namespace string) (*dw.DevWorkspaceStatus, error) {
3056
namespacedName := types.NamespacedName{
@@ -54,7 +80,7 @@ func (w *K8sClient) WaitDevWsStatus(name, namespace string, expectedStatus dw.De
5480
if err != nil {
5581
return false, err
5682
}
57-
log.Printf("Now current status of developer workspace is: %s. Message: %s", currentStatus.Phase, currentStatus.Message)
83+
log.Printf("Now current status of developer workspace %s is: %s. Message: %s", name, currentStatus.Phase, currentStatus.Message)
5884
if currentStatus.Phase == dw.DevWorkspaceStatusFailed {
5985
return false, errors.New("workspace has been failed unexpectedly. Message: " + currentStatus.Message)
6086
}

test/e2e/pkg/client/oc.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,25 @@ func (w *K8sClient) OcApplyWorkspace(namespace string, filePath string) (command
4545
}
4646

4747
// launch 'exec' oc command in the defined pod and container
48-
func (w *K8sClient) ExecCommandInContainer(podName string, namespace, commandInContainer string) (output string, err error) {
48+
func (w *K8sClient) ExecCommandInContainer(podName string, namespace, containerName, commandInContainer string) (output string, err error) {
4949
cmd := exec.Command("bash", "-c", fmt.Sprintf(
50-
"KUBECONFIG=%s oc exec %s -n %s -c restricted-access-container -- %s",
50+
"KUBECONFIG=%s oc exec %s -n %s -c %s -- %s",
5151
w.kubeCfgFile,
5252
podName,
5353
namespace,
54+
containerName,
5455
commandInContainer))
5556
outBytes, err := cmd.CombinedOutput()
5657
return string(outBytes), err
5758
}
59+
60+
func (w *K8sClient) GetLogsForContainer(podName string, namespace, containerName string) (output string, err error) {
61+
cmd := exec.Command("bash", "-c", fmt.Sprintf(
62+
"KUBECONFIG=%s oc logs %s -c %s -n %s",
63+
w.kubeCfgFile,
64+
podName,
65+
containerName,
66+
namespace))
67+
outBytes, err := cmd.CombinedOutput()
68+
return string(outBytes), err
69+
}

test/e2e/pkg/client/pod.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,42 @@ func (w *K8sClient) GetPodNameBySelector(selector, namespace string) (string, er
112112
if len(podList.Items) == 0 {
113113
return "", errors.New(fmt.Sprintf("There is no pod that matches '%s' in namespace %s ", selector, namespace))
114114
}
115-
// we expect just 1 pod in test namespace and return the first value from the list
116-
return podList.Items[0].Name, nil
115+
for _, pod := range podList.Items {
116+
if pod.Status.Phase == v1.PodRunning {
117+
return pod.Name, nil
118+
}
119+
}
120+
121+
return "", fmt.Errorf("no running pod found for selector '%s' in namespace %s", selector, namespace)
122+
}
123+
124+
func (w *K8sClient) WaitForPodContainerToReady(namespace, podName, containerName string) error {
125+
timeout := time.After(6 * time.Minute)
126+
ticker := time.NewTicker(1 * time.Second)
127+
defer ticker.Stop()
128+
129+
for {
130+
select {
131+
case <-timeout:
132+
return fmt.Errorf("timed out waiting for container %s in pod %s to become ready", containerName, podName)
133+
case <-ticker.C:
134+
pod, err := w.Kube().CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{})
135+
if err != nil {
136+
return fmt.Errorf("error waiting for pod %s to become ready : %v", podName, err)
137+
}
138+
if w.IsPodContainerReady(containerName, pod) {
139+
return nil
140+
}
141+
log.Printf("Still waiting for container %s in pod %s to become ready...", containerName, podName)
142+
}
143+
}
144+
}
145+
146+
func (w *K8sClient) IsPodContainerReady(containerName string, pod *v1.Pod) bool {
147+
for _, cs := range pod.Status.ContainerStatuses {
148+
if cs.Name == containerName && cs.Ready {
149+
return true
150+
}
151+
}
152+
return false
117153
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright (c) 2019-2025 Red Hat, Inc.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
package tests
14+
15+
import (
16+
"fmt"
17+
18+
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
19+
"github.com/devfile/devworkspace-operator/test/e2e/pkg/config"
20+
"github.com/onsi/ginkgo/v2"
21+
"github.com/onsi/gomega"
22+
)
23+
24+
var _ = ginkgo.Describe("[Create DevWorkspace and ensure data is persisted during restarts]", func() {
25+
defer ginkgo.GinkgoRecover()
26+
27+
ginkgo.It("Wait DevWorkspace Webhook Server Pod", func() {
28+
controllerLabel := "app.kubernetes.io/name=devworkspace-webhook-server"
29+
30+
deploy, err := config.AdminK8sClient.WaitForPodRunningByLabel(config.OperatorNamespace, controllerLabel)
31+
if err != nil {
32+
ginkgo.Fail(fmt.Sprintf("cannot get the Pod status with label %s: %s", controllerLabel, err.Error()))
33+
return
34+
}
35+
36+
if !deploy {
37+
ginkgo.Fail("Devworkspace webhook didn't start properly")
38+
}
39+
})
40+
41+
ginkgo.It("Add DevWorkspace to cluster and wait running status", func() {
42+
commandResult, err := config.DevK8sClient.OcApplyWorkspace(config.DevWorkspaceNamespace, "test/resources/simple-devworkspace-with-project-clone.yaml")
43+
if err != nil {
44+
ginkgo.Fail(fmt.Sprintf("Failed to create DevWorkspace: %s %s", err.Error(), commandResult))
45+
return
46+
}
47+
48+
deploy, err := config.DevK8sClient.WaitDevWsStatus("code-latest", config.DevWorkspaceNamespace, dw.DevWorkspaceStatusRunning)
49+
if !deploy {
50+
ginkgo.Fail(fmt.Sprintf("DevWorkspace didn't start properly. Error: %s", err))
51+
}
52+
})
53+
54+
var podName string
55+
ginkgo.It("Check that project-clone succeeded as expected", func() {
56+
podSelector := "controller.devfile.io/devworkspace_name=code-latest"
57+
var err error
58+
podName, err = config.AdminK8sClient.GetPodNameBySelector(podSelector, config.DevWorkspaceNamespace)
59+
if err != nil {
60+
ginkgo.Fail(fmt.Sprintf("Can get devworkspace pod by selector. Error: %s", err))
61+
}
62+
resultOfExecCommand, err := config.AdminK8sClient.GetLogsForContainer(podName, config.DevWorkspaceNamespace, "project-clone")
63+
if err != nil {
64+
ginkgo.Fail(fmt.Sprintf("Cannot get logs for project-clone container. Error: `%s`. Exec output: `%s`", err, resultOfExecCommand))
65+
}
66+
gomega.Expect(resultOfExecCommand).To(gomega.ContainSubstring("Cloning project web-nodejs-sample to /projects"))
67+
})
68+
69+
ginkgo.It("Make some changes in DevWorkspace dev container", func() {
70+
resultOfExecCommand, err := config.AdminK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "dev", "bash -c 'echo \"## Modified via e2e test\" >> /projects/web-nodejs-sample/README.md'")
71+
if err != nil {
72+
ginkgo.Fail(fmt.Sprintf("failed to make changes to DevWorkspace container, returned: %s", resultOfExecCommand))
73+
}
74+
})
75+
76+
ginkgo.It("Stop DevWorkspace", func() {
77+
err := config.AdminK8sClient.UpdateDevWorkspaceStarted("code-latest", config.DevWorkspaceNamespace, false)
78+
if err != nil {
79+
ginkgo.Fail(fmt.Sprintf("failed to stop DevWorkspace container, returned: %s", err))
80+
}
81+
deploy, err := config.DevK8sClient.WaitDevWsStatus("code-latest", config.DevWorkspaceNamespace, dw.DevWorkspaceStatusStopped)
82+
if !deploy {
83+
ginkgo.Fail(fmt.Sprintf("DevWorkspace didn't start properly. Error: %s", err))
84+
}
85+
})
86+
87+
ginkgo.It("Start DevWorkspace", func() {
88+
err := config.AdminK8sClient.UpdateDevWorkspaceStarted("code-latest", config.DevWorkspaceNamespace, true)
89+
if err != nil {
90+
ginkgo.Fail(fmt.Sprintf("failed to start DevWorkspace container"))
91+
}
92+
deploy, err := config.DevK8sClient.WaitDevWsStatus("code-latest", config.DevWorkspaceNamespace, dw.DevWorkspaceStatusRunning)
93+
if !deploy {
94+
ginkgo.Fail(fmt.Sprintf("DevWorkspace didn't start properly. Error: %s", err))
95+
}
96+
})
97+
98+
ginkgo.It("Verify changes persist after DevWorkspace restart", func() {
99+
podSelector := "controller.devfile.io/devworkspace_name=code-latest"
100+
podName, err := config.AdminK8sClient.GetPodNameBySelector(podSelector, config.DevWorkspaceNamespace)
101+
if err != nil {
102+
ginkgo.Fail(fmt.Sprintf("Can get devworkspace pod by selector. Error: %s", err))
103+
}
104+
err = config.AdminK8sClient.WaitForPodContainerToReady(config.DevWorkspaceNamespace, podName, "dev")
105+
if err != nil {
106+
ginkgo.Fail(fmt.Sprintf("failed waiting for DevWorkspace container 'dev' to become ready. Error: %s", err))
107+
}
108+
resultOfExecCommand, err := config.AdminK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "dev", "bash -c 'cat /projects/web-nodejs-sample/README.md'")
109+
if err != nil {
110+
ginkgo.Fail(fmt.Sprintf("failed to verify to DevWorkspace container, returned: %s", resultOfExecCommand))
111+
}
112+
gomega.Expect(resultOfExecCommand).To(gomega.ContainSubstring("## Modified via e2e test"))
113+
})
114+
115+
ginkgo.It("Check that project-clone logs mention project already cloned", func() {
116+
resultOfExecCommand, err := config.AdminK8sClient.GetLogsForContainer(podName, config.DevWorkspaceNamespace, "project-clone")
117+
if err != nil {
118+
ginkgo.Fail(fmt.Sprintf("Cannot get logs for project-clone container. Error: `%s`. Exec output: `%s`", err, resultOfExecCommand))
119+
}
120+
gomega.Expect(resultOfExecCommand).To(gomega.ContainSubstring("Project 'web-nodejs-sample' is already cloned and has all remotes configured"))
121+
})
122+
})

test/e2e/pkg/tests/devworkspaces_tests.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ var _ = ginkgo.Describe("[Create OpenShift Web Terminal Workspace]", func() {
6363
if err != nil {
6464
ginkgo.Fail(fmt.Sprintf("Can get web terminal pod by selector. Error: %s", err))
6565
}
66-
resultOfExecCommand, err := config.DevK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "echo hello dev")
66+
resultOfExecCommand, err := config.DevK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "restricted-access-container", "echo hello dev")
6767
if err != nil {
6868
ginkgo.Fail(fmt.Sprintf("Cannot execute command in the devworkspace container. Error: `%s`. Exec output: `%s`", err, resultOfExecCommand))
6969
}
7070
gomega.Expect(resultOfExecCommand).To(gomega.ContainSubstring("hello dev"))
7171
})
7272

7373
ginkgo.It("Check that not pod owner cannot execute a command in the container", func() {
74-
resultOfExecCommand, err := config.AdminK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "echo hello dev")
74+
resultOfExecCommand, err := config.AdminK8sClient.ExecCommandInContainer(podName, config.DevWorkspaceNamespace, "restricted-access-container", "echo hello dev")
7575
if err == nil {
7676
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))
7777
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
kind: DevWorkspace
2+
apiVersion: workspace.devfile.io/v1alpha2
3+
metadata:
4+
name: code-latest
5+
spec:
6+
started: true
7+
template:
8+
projects:
9+
- name: web-nodejs-sample
10+
git:
11+
remotes:
12+
origin: "https://github.com/che-samples/web-nodejs-sample.git"
13+
components:
14+
- name: dev
15+
container:
16+
image: quay.io/wto/web-terminal-tooling:latest
17+
args: ["tail", "-f", "/dev/null"]

0 commit comments

Comments
 (0)