@@ -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})
0 commit comments