Skip to content

Commit e5fe838

Browse files
committed
Add devworkspace tests to cover stopping DevWorkspaces
Signed-off-by: Angel Misevski <[email protected]>
1 parent 84684ac commit e5fe838

File tree

2 files changed

+256
-0
lines changed

2 files changed

+256
-0
lines changed

controllers/workspace/devworkspace_controller_test.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,23 @@ import (
1818
"net/http"
1919
"os"
2020
"path/filepath"
21+
"time"
2122

2223
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
2324
controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
2425
workspacecontroller "github.com/devfile/devworkspace-operator/controllers/workspace"
2526
"github.com/devfile/devworkspace-operator/controllers/workspace/internal/testutil"
2627
"github.com/devfile/devworkspace-operator/pkg/common"
2728
"github.com/devfile/devworkspace-operator/pkg/conditions"
29+
"github.com/devfile/devworkspace-operator/pkg/config"
2830
"github.com/devfile/devworkspace-operator/pkg/constants"
2931
. "github.com/onsi/ginkgo/v2"
3032
. "github.com/onsi/gomega"
3133
appsv1 "k8s.io/api/apps/v1"
3234
corev1 "k8s.io/api/core/v1"
35+
rbacv1 "k8s.io/api/rbac/v1"
36+
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
37+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3338
"k8s.io/apimachinery/pkg/types"
3439
"sigs.k8s.io/controller-runtime/pkg/client"
3540
"sigs.k8s.io/yaml"
@@ -593,4 +598,208 @@ var _ = Describe("DevWorkspace Controller", func() {
593598
})
594599
})
595600

601+
Context("Stopping DevWorkspaces", func() {
602+
const testURL = "test-url"
603+
604+
BeforeEach(func() {
605+
workspacecontroller.SetupHttpClientsForTesting(&http.Client{
606+
Transport: &testutil.TestRoundTripper{
607+
Data: map[string]testutil.TestResponse{
608+
fmt.Sprintf("%s/healthz", testURL): {
609+
StatusCode: http.StatusOK,
610+
},
611+
},
612+
},
613+
})
614+
createStartedDevWorkspace("test-devworkspace.yaml")
615+
})
616+
617+
AfterEach(func() {
618+
deleteDevWorkspace(devWorkspaceName)
619+
workspacecontroller.SetupHttpClientsForTesting(getBasicTestHttpClient())
620+
})
621+
622+
It("Stops workspaces and scales deployment to zero", func() {
623+
devworkspace := &dw.DevWorkspace{}
624+
625+
By("Setting DevWorkspace's .spec.started to false")
626+
Eventually(func() error {
627+
devworkspace = getExistingDevWorkspace()
628+
devworkspace.Spec.Started = false
629+
return k8sClient.Update(ctx, devworkspace)
630+
}, timeout, interval).Should(Succeed(), "Update DevWorkspace to have .spec.started = false")
631+
632+
By("Adds devworkspace-started annotation to false on DevWorkspaceRouting")
633+
Eventually(func() (string, error) {
634+
dwr := &controllerv1alpha1.DevWorkspaceRouting{}
635+
if err := k8sClient.Get(ctx, types.NamespacedName{
636+
Name: common.DevWorkspaceRoutingName(devworkspace.Status.DevWorkspaceId),
637+
Namespace: testNamespace,
638+
}, dwr); err != nil {
639+
return "", err
640+
}
641+
annotation, ok := dwr.Annotations[constants.DevWorkspaceStartedStatusAnnotation]
642+
if !ok {
643+
return "", fmt.Errorf("%s annotation not present", constants.DevWorkspaceStartedStatusAnnotation)
644+
}
645+
return annotation, nil
646+
}, timeout, interval).Should(Equal("false"), "DevWorkspace Routing should get `devworkspace-started: false` annotation")
647+
648+
By("Checking that workspace deployment is scaled to zero")
649+
Eventually(func() (replicas int32, err error) {
650+
deploy := &appsv1.Deployment{}
651+
if err := k8sClient.Get(ctx, types.NamespacedName{
652+
Name: common.DeploymentName(devworkspace.Status.DevWorkspaceId),
653+
Namespace: testNamespace,
654+
}, deploy); err != nil {
655+
return -1, err
656+
}
657+
return *deploy.Spec.Replicas, nil
658+
}, timeout, interval).Should(Equal(int32(0)), "Workspace deployment was not scaled to zero")
659+
660+
By("Setting DevWorkspace's deployment replicas to zero")
661+
scaleDeploymentToZero(common.DeploymentName(devworkspace.Status.DevWorkspaceId))
662+
663+
currDW := &dw.DevWorkspace{}
664+
Eventually(func() (dw.DevWorkspacePhase, error) {
665+
if err := k8sClient.Get(ctx, types.NamespacedName{
666+
Name: devworkspace.Name,
667+
Namespace: devworkspace.Namespace,
668+
}, currDW); err != nil {
669+
return "", err
670+
}
671+
GinkgoWriter.Printf("Waiting for DevWorkspace to enter Stopped phase -- Phase: %s, Message %s\n", currDW.Status.Phase, currDW.Status.Message)
672+
return currDW.Status.Phase, nil
673+
}, timeout, interval).Should(Equal(dw.DevWorkspaceStatusStopped), "Workspace did not enter Stopped phase before timeout")
674+
675+
Expect(currDW.Status.Message).Should(Equal("Stopped"))
676+
startedCondition := conditions.GetConditionByType(currDW.Status.Conditions, conditions.Started)
677+
Expect(startedCondition).Should(Not(BeNil()), "Workspace should have Started condition")
678+
Expect(startedCondition.Status).Should(Equal(corev1.ConditionFalse), "Workspace Started condition should have status=false")
679+
Expect(startedCondition.Message).Should(Equal("Workspace is stopped"))
680+
})
681+
682+
It("Stops workspaces and deletes resources when cleanup option is enabled", func() {
683+
boolTrue := true
684+
config.SetGlobalConfigForTesting(&controllerv1alpha1.OperatorConfiguration{
685+
Workspace: &controllerv1alpha1.WorkspaceConfig{
686+
CleanupOnStop: &boolTrue,
687+
},
688+
})
689+
defer config.SetGlobalConfigForTesting(nil)
690+
devworkspace := &dw.DevWorkspace{}
691+
692+
By("Setting DevWorkspace's .spec.started to false")
693+
Eventually(func() error {
694+
devworkspace = getExistingDevWorkspace()
695+
devworkspace.Spec.Started = false
696+
return k8sClient.Update(ctx, devworkspace)
697+
}, timeout, interval).Should(Succeed(), "Update DevWorkspace to have .spec.started = false")
698+
workspaceId := devworkspace.Status.DevWorkspaceId
699+
700+
By("Checking that workspace owned objects are deleted")
701+
objects := []client.Object{
702+
&appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: common.DeploymentName(workspaceId)}},
703+
&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: common.MetadataConfigMapName(workspaceId)}},
704+
&controllerv1alpha1.DevWorkspaceRouting{ObjectMeta: metav1.ObjectMeta{Name: common.DevWorkspaceRoutingName(workspaceId)}},
705+
}
706+
for _, obj := range objects {
707+
Eventually(func() error {
708+
err := k8sClient.Get(ctx, types.NamespacedName{
709+
Name: obj.GetName(),
710+
Namespace: testNamespace,
711+
}, obj)
712+
switch {
713+
case err == nil:
714+
return fmt.Errorf("Object exists")
715+
case k8sErrors.IsNotFound(err):
716+
return nil
717+
default:
718+
return err
719+
}
720+
}, timeout, interval).Should(Succeed(), "DevWorkspace-owned %s should be deleted", obj.GetObjectKind().GroupVersionKind().Kind)
721+
}
722+
723+
currDW := &dw.DevWorkspace{}
724+
Eventually(func() (dw.DevWorkspacePhase, error) {
725+
if err := k8sClient.Get(ctx, types.NamespacedName{
726+
Name: devworkspace.Name,
727+
Namespace: devworkspace.Namespace,
728+
}, currDW); err != nil {
729+
return "", err
730+
}
731+
GinkgoWriter.Printf("Waiting for DevWorkspace to enter Stopped phase -- Phase: %s, Message %s\n", currDW.Status.Phase, currDW.Status.Message)
732+
return currDW.Status.Phase, nil
733+
}, timeout, interval).Should(Equal(dw.DevWorkspaceStatusStopped), "Workspace did not enter Stopped phase before timeout")
734+
735+
Expect(currDW.Status.Message).Should(Equal("Stopped"))
736+
startedCondition := conditions.GetConditionByType(currDW.Status.Conditions, conditions.Started)
737+
Expect(startedCondition).Should(Not(BeNil()), "Workspace should have Started condition")
738+
Expect(startedCondition.Status).Should(Equal(corev1.ConditionFalse), "Workspace Started condition should have status=false")
739+
Expect(startedCondition.Message).Should(Equal("Workspace is stopped"))
740+
})
741+
742+
It("Stops failing workspaces with debug annotation after timeout", func() {
743+
devworkspace := &dw.DevWorkspace{}
744+
failTime := metav1.Time{Time: clock.Now().Add(-20 * time.Second)}
745+
746+
By("Set debug start annotation on DevWorkspace")
747+
Eventually(func() error {
748+
devworkspace = getExistingDevWorkspace()
749+
if devworkspace.Annotations == nil {
750+
devworkspace.Annotations = map[string]string{}
751+
}
752+
devworkspace.Annotations[constants.DevWorkspaceDebugStartAnnotation] = "true"
753+
return k8sClient.Update(ctx, devworkspace)
754+
}, timeout, interval).Should(Succeed(), "Should be able to set failing status on DevWorkspace")
755+
756+
config.SetGlobalConfigForTesting(&controllerv1alpha1.OperatorConfiguration{
757+
Workspace: &controllerv1alpha1.WorkspaceConfig{
758+
ProgressTimeout: "1s",
759+
},
760+
})
761+
defer config.SetGlobalConfigForTesting(nil)
762+
763+
By("Setting failing phase on workspace directly")
764+
Eventually(func() error {
765+
devworkspace = getExistingDevWorkspace()
766+
devworkspace.Status.Phase = "Failing"
767+
devworkspace.Status.Conditions = append(devworkspace.Status.Conditions, dw.DevWorkspaceCondition{
768+
Type: dw.DevWorkspaceFailedStart,
769+
LastTransitionTime: failTime,
770+
Status: corev1.ConditionTrue,
771+
Message: "testing failed condition",
772+
})
773+
return k8sClient.Status().Update(ctx, devworkspace)
774+
}, timeout, interval).Should(Succeed(), "Should be able to set failing status on DevWorkspace")
775+
776+
currDW := &dw.DevWorkspace{}
777+
Eventually(func() (started bool, err error) {
778+
if err := k8sClient.Get(ctx, namespacedName(devworkspace.Name, devworkspace.Namespace), currDW); err != nil {
779+
return false, err
780+
}
781+
return currDW.Spec.Started, nil
782+
}, timeout, interval).Should(BeFalse(), "DevWorkspace should have spec.started = false")
783+
})
784+
785+
It("Stops failing workspaces", func() {
786+
devworkspace := &dw.DevWorkspace{}
787+
788+
By("Setting failing phase on workspace directly")
789+
Eventually(func() error {
790+
devworkspace = getExistingDevWorkspace()
791+
devworkspace.Status.Phase = "Failing"
792+
return k8sClient.Status().Update(ctx, devworkspace)
793+
}, timeout, interval).Should(Succeed(), "Should be able to set failing status on DevWorkspace")
794+
795+
currDW := &dw.DevWorkspace{}
796+
Eventually(func() (started bool, err error) {
797+
if err := k8sClient.Get(ctx, namespacedName(devworkspace.Name, devworkspace.Namespace), currDW); err != nil {
798+
return false, err
799+
}
800+
return currDW.Spec.Started, nil
801+
}, timeout, interval).Should(BeFalse(), "DevWorkspace should have spec.started = false")
802+
})
803+
804+
})
596805
})

controllers/workspace/util_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,16 @@ import (
3030
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
3131
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3232
"k8s.io/apimachinery/pkg/types"
33+
kubeclock "k8s.io/apimachinery/pkg/util/clock"
3334
)
3435

3536
const (
3637
timeout = 10 * time.Second
3738
interval = 250 * time.Millisecond
3839
)
3940

41+
var clock kubeclock.Clock = &kubeclock.RealClock{}
42+
4043
func createDevWorkspace(fromFile string) {
4144
By("Loading DevWorkspace from test file")
4245
devworkspace := &dw.DevWorkspace{}
@@ -54,6 +57,31 @@ func createDevWorkspace(fromFile string) {
5457
}, 10*time.Second, 250*time.Millisecond).Should(BeTrue())
5558
}
5659

60+
func createStartedDevWorkspace(fromFile string) {
61+
createDevWorkspace(fromFile)
62+
devworkspace := getExistingDevWorkspace()
63+
workspaceID := devworkspace.Status.DevWorkspaceId
64+
65+
By("Manually making Routing ready to continue")
66+
markRoutingReady("test-url", common.DevWorkspaceRoutingName(workspaceID))
67+
68+
By("Setting the deployment to have 1 ready replica")
69+
markDeploymentReady(common.DeploymentName(workspaceID))
70+
71+
currDW := &dw.DevWorkspace{}
72+
Eventually(func() (dw.DevWorkspacePhase, error) {
73+
err := k8sClient.Get(ctx, types.NamespacedName{
74+
Name: devworkspace.Name,
75+
Namespace: devworkspace.Namespace,
76+
}, currDW)
77+
if err != nil {
78+
return "", err
79+
}
80+
GinkgoWriter.Printf("Waiting for DevWorkspace to enter running phase -- Phase: %s, Message %s\n", currDW.Status.Phase, currDW.Status.Message)
81+
return currDW.Status.Phase, nil
82+
}, timeout, interval).Should(Equal(dw.DevWorkspaceStatusRunning), "Workspace did not enter Running phase before timeout")
83+
}
84+
5785
func getExistingDevWorkspace() *dw.DevWorkspace {
5886
By("Getting existing DevWorkspace")
5987
devworkspace := &dw.DevWorkspace{}
@@ -162,6 +190,25 @@ func markDeploymentReady(deploymentName string) {
162190
}, 30*time.Second, 250*time.Millisecond).Should(Succeed(), "Update Deployment to have 1 ready replica")
163191
}
164192

193+
func scaleDeploymentToZero(deploymentName string) {
194+
namespacedName := types.NamespacedName{
195+
Name: deploymentName,
196+
Namespace: testNamespace,
197+
}
198+
deploy := &appsv1.Deployment{}
199+
Eventually(func() error {
200+
err := k8sClient.Get(ctx, namespacedName, deploy)
201+
if err != nil {
202+
return err
203+
}
204+
deploy.Status.ReadyReplicas = 0
205+
deploy.Status.Replicas = 0
206+
deploy.Status.AvailableReplicas = 0
207+
deploy.Status.UpdatedReplicas = 0
208+
return k8sClient.Status().Update(ctx, deploy)
209+
}, 30*time.Second, 250*time.Millisecond).Should(Succeed(), "Update Deployment to have 1 ready replica")
210+
}
211+
165212
func devworkspaceOwnerRef(wksp *dw.DevWorkspace) metav1.OwnerReference {
166213
boolTrue := true
167214
return metav1.OwnerReference{

0 commit comments

Comments
 (0)