Path: blob/main/components/node-labeler/cmd/run_test.go
2498 views
// Copyright (c) 2025 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package cmd56import (7"context"8"path/filepath"9"testing"10"time"1112"github.com/aws/smithy-go/ptr"13"github.com/golang/mock/gomock"14"github.com/google/uuid"15. "github.com/onsi/ginkgo/v2"16. "github.com/onsi/gomega"17"google.golang.org/protobuf/proto"18corev1 "k8s.io/api/core/v1"19metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"20"k8s.io/apimachinery/pkg/types"21clientgoscheme "k8s.io/client-go/kubernetes/scheme"22ctrl "sigs.k8s.io/controller-runtime"23"sigs.k8s.io/controller-runtime/pkg/client"24"sigs.k8s.io/controller-runtime/pkg/envtest"25logf "sigs.k8s.io/controller-runtime/pkg/log"26"sigs.k8s.io/controller-runtime/pkg/log/zap"2728"github.com/gitpod-io/gitpod/common-go/util"29csapi "github.com/gitpod-io/gitpod/content-service/api"30workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"31)3233const (34timeout = time.Second * 1035duration = time.Second * 236interval = time.Millisecond * 25037workspaceNamespace = "default"38secretsNamespace = "workspace-secrets"39)4041var (42k8sClient client.Client43testEnv *envtest.Environment44mock_ctrl *gomock.Controller45ctx context.Context46cancel context.CancelFunc47nodeScaledownCtrl *NodeScaledownAnnotationController48NodeName = "cool-ws-node"49)5051func TestAPIs(t *testing.T) {52mock_ctrl = gomock.NewController(t)53RegisterFailHandler(Fail)54RunSpecs(t, "Controller Suite")55}5657var _ = Describe("NodeScaledownAnnotationController", func() {58It("should remove scale-down-disabled when last workspace is removed", func() {59ws1 := newWorkspace(uuid.NewString(), workspaceNamespace, NodeName, workspacev1.WorkspacePhaseRunning)60ws2 := newWorkspace(uuid.NewString(), workspaceNamespace, NodeName, workspacev1.WorkspacePhaseRunning)61ws1.Status.Runtime = nil62ws2.Status.Runtime = nil63createWorkspace(ws1)64createWorkspace(ws2)6566By("Assigning nodes to workspaces")67updateObjWithRetries(k8sClient, ws1, true, func(ws *workspacev1.Workspace) {68ws.Status.Conditions = []metav1.Condition{}69ws.Status.Runtime = &workspacev1.WorkspaceRuntimeStatus{70NodeName: NodeName,71}72})7374updateObjWithRetries(k8sClient, ws2, true, func(ws *workspacev1.Workspace) {75ws.Status.Conditions = []metav1.Condition{}76ws.Status.Runtime = &workspacev1.WorkspaceRuntimeStatus{77NodeName: NodeName,78}79})8081By("Verifying node annotation")82Eventually(func(g Gomega) {83var node corev1.Node84g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: NodeName}, &node)).To(Succeed())85g.Expect(node.Annotations).To(HaveKeyWithValue("cluster-autoscaler.kubernetes.io/scale-down-disabled", "true"))86}, timeout, interval).Should(Succeed())8788By("Deleting workspaces")89Expect(k8sClient.Delete(ctx, ws1)).To(Succeed())90Expect(k8sClient.Delete(ctx, ws2)).To(Succeed())9192By("Verifying final state")93Eventually(func(g Gomega) {94var node corev1.Node95g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: NodeName}, &node)).To(Succeed())96g.Expect(node.Annotations).ToNot(HaveKey("cluster-autoscaler.kubernetes.io/scale-down-disabled"))97}, timeout, interval).Should(Succeed())98})99})100101var _ = BeforeSuite(func() {102logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))103104crdPath := filepath.Join("..", "crd")105if !util.InLeewayBuild() {106crdPath = filepath.Join("..", "..", "ws-manager-mk2", "config", "crd", "bases")107}108109By("bootstrapping test environment")110testEnv = &envtest.Environment{111ControlPlaneStartTimeout: 1 * time.Minute,112ControlPlaneStopTimeout: 1 * time.Minute,113CRDDirectoryPaths: []string{crdPath},114ErrorIfCRDPathMissing: true,115}116117cfg, err := testEnv.Start()118Expect(err).NotTo(HaveOccurred())119Expect(cfg).NotTo(BeNil())120121err = workspacev1.AddToScheme(clientgoscheme.Scheme)122Expect(err).NotTo(HaveOccurred())123124//+kubebuilder:scaffold:scheme125126k8sClient, err = client.New(cfg, client.Options{Scheme: clientgoscheme.Scheme})127Expect(err).NotTo(HaveOccurred())128Expect(k8sClient).NotTo(BeNil())129130k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{131Scheme: clientgoscheme.Scheme,132})133Expect(err).ToNot(HaveOccurred())134ctx, cancel = context.WithCancel(context.Background())135136By("Creating default ws node")137node := &corev1.Node{138ObjectMeta: metav1.ObjectMeta{139Name: NodeName,140},141}142Expect(k8sClient.Create(ctx, node)).To(Succeed())143144err = k8sManager.GetFieldIndexer().IndexField(context.Background(),145&workspacev1.Workspace{},146"status.runtime.nodeName",147func(o client.Object) []string {148ws := o.(*workspacev1.Workspace)149if ws.Status.Runtime == nil {150return nil151}152return []string{ws.Status.Runtime.NodeName}153})154Expect(err).ToNot(HaveOccurred())155156By("Setting up controllers")157nodeScaledownCtrl, err = NewNodeScaledownAnnotationController(k8sManager.GetClient())158Expect(err).NotTo(HaveOccurred())159Expect(nodeScaledownCtrl.SetupWithManager(k8sManager)).To(Succeed())160161_ = createNamespace(secretsNamespace)162163By("Starting the manager")164go func() {165defer GinkgoRecover()166err = k8sManager.Start(ctx)167Expect(err).ToNot(HaveOccurred(), "failed to run manager")168}()169170By("Waiting for controllers to be ready")171DeferCleanup(cancel)172173// Wait for controllers to be ready174Eventually(func() bool {175return k8sManager.GetCache().WaitForCacheSync(ctx)176}, time.Second*10, time.Millisecond*100).Should(BeTrue())177})178179var _ = AfterSuite(func() {180if cancel != nil {181cancel()182}183By("tearing down the test environment")184err := testEnv.Stop()185Expect(err).NotTo(HaveOccurred())186})187188func newWorkspace(name, namespace, nodeName string, phase workspacev1.WorkspacePhase) *workspacev1.Workspace {189GinkgoHelper()190initializer := &csapi.WorkspaceInitializer{191Spec: &csapi.WorkspaceInitializer_Empty{Empty: &csapi.EmptyInitializer{}},192}193initializerBytes, err := proto.Marshal(initializer)194Expect(err).ToNot(HaveOccurred())195196return &workspacev1.Workspace{197Status: workspacev1.WorkspaceStatus{198Phase: phase,199Runtime: &workspacev1.WorkspaceRuntimeStatus{200NodeName: nodeName,201},202Conditions: []metav1.Condition{},203},204TypeMeta: metav1.TypeMeta{205APIVersion: "workspace.gitpod.io/v1",206Kind: "Workspace",207},208ObjectMeta: metav1.ObjectMeta{209Name: name,210Namespace: namespace,211},212Spec: workspacev1.WorkspaceSpec{213Ownership: workspacev1.Ownership{214Owner: "foobar",215WorkspaceID: "cool-workspace",216},217Type: workspacev1.WorkspaceTypeRegular,218Class: "default",219Image: workspacev1.WorkspaceImages{220Workspace: workspacev1.WorkspaceImage{221Ref: ptr.String("alpine:latest"),222},223IDE: workspacev1.IDEImages{224Refs: []string{},225},226},227Ports: []workspacev1.PortSpec{},228Initializer: initializerBytes,229Admission: workspacev1.AdmissionSpec{230Level: workspacev1.AdmissionLevelEveryone,231},232},233}234}235236func createWorkspace(ws *workspacev1.Workspace) {237GinkgoHelper()238By("creating workspace")239Expect(k8sClient.Create(ctx, ws)).To(Succeed())240}241242func updateObjWithRetries[O client.Object](c client.Client, obj O, updateStatus bool, update func(obj O)) {243GinkgoHelper()244Eventually(func() error {245var err error246if err = c.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, obj); err != nil {247return err248}249// Apply update.250update(obj)251if updateStatus {252err = c.Status().Update(ctx, obj)253} else {254err = c.Update(ctx, obj)255}256return err257}, timeout, interval).Should(Succeed())258}259260func createNamespace(name string) *corev1.Namespace {261GinkgoHelper()262263namespace := &corev1.Namespace{264ObjectMeta: metav1.ObjectMeta{265Name: name,266},267}268269Expect(k8sClient.Create(ctx, namespace)).To(Succeed())270return namespace271}272273274