Path: blob/main/components/ws-daemon/pkg/controller/workspace_controller_test.go
2500 views
// Copyright (c) 2023 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 controller56import (7"fmt"8"time"910"github.com/aws/smithy-go/ptr"11wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes"12csapi "github.com/gitpod-io/gitpod/content-service/api"13workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"14"github.com/golang/mock/gomock"15"github.com/google/uuid"16. "github.com/onsi/ginkgo/v2"17. "github.com/onsi/gomega"18"google.golang.org/protobuf/proto"19corev1 "k8s.io/api/core/v1"20metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"21"k8s.io/apimachinery/pkg/types"22"sigs.k8s.io/controller-runtime/pkg/client"23)2425const (26timeout = time.Second * 2027duration = time.Second * 228interval = time.Millisecond * 25029workspaceNamespace = "default"30)3132var _ = Describe("WorkspaceController", func() {33Context("with regular workspace", func() {34It("should handle regular content init", func() {35name := uuid.NewString()3637mockCtrl := gomock.NewController(GinkgoT())38defer mockCtrl.Finish()39ops := NewMockWorkspaceOperations(mockCtrl)4041ops.EXPECT().InitWorkspace(gomock.Any(), gomock.Any()).Return(&csapi.InitializerMetrics{}, "", nil).Times(1)42workspaceCtrl.operations = ops4344_ = createSecret(fmt.Sprintf("%s-tokens", name), secretsNamespace)45ws := newWorkspace(name, workspaceNamespace, workspacev1.WorkspacePhaseCreating)46createWorkspace(ws)47updateObjWithRetries(k8sClient, ws, true, func(ws *workspacev1.Workspace) {48ws.Status.Phase = workspacev1.WorkspacePhaseCreating49ws.Status.Conditions = []metav1.Condition{}50ws.Status.Runtime = &workspacev1.WorkspaceRuntimeStatus{51NodeName: NodeName,52}53})5455expectConditionEventually(ws, string(workspacev1.WorkspaceConditionContentReady), metav1.ConditionTrue, "InitializationSuccess")56})5758It("should handle regular content backup", func() {59name := uuid.NewString()6061mockCtrl := gomock.NewController(GinkgoT())62defer mockCtrl.Finish()63ops := NewMockWorkspaceOperations(mockCtrl)6465gitStatus := &csapi.GitStatus{66Branch: "main",67LatestCommit: "991300e0cf199116685f25561702a145d40ae462",68UncommitedFiles: []string{"git", "pod"},69TotalUncommitedFiles: 2,70UntrackedFiles: []string{"kumquat"},71TotalUntrackedFiles: 1,72UnpushedCommits: []string{"df591ed557c9afa8b6bcd1f51809d83d3f48fc43"},73TotalUnpushedCommits: 1,74}7576ops.EXPECT().BackupWorkspace(gomock.Any(), gomock.Any()).Return(gitStatus, nil).Times(1)77ops.EXPECT().DeleteWorkspace(gomock.Any(), gomock.Any())78workspaceCtrl.operations = ops7980_ = createSecret(fmt.Sprintf("%s-tokens", name), secretsNamespace)81ws := newWorkspace(name, workspaceNamespace, workspacev1.WorkspacePhaseCreating)82createWorkspace(ws)83markContentReady(ws)8485expectConditionEventually(ws, string(workspacev1.WorkspaceConditionBackupComplete), metav1.ConditionTrue, "BackupComplete")86expectGitStatusEventually(ws, gitStatus)87})8889It("should report backup failure", func() {90name := uuid.NewString()9192mockCtrl := gomock.NewController(GinkgoT())93defer mockCtrl.Finish()94ops := NewMockWorkspaceOperations(mockCtrl)9596ops.EXPECT().BackupWorkspace(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("BOOM!")).Times(1)97ops.EXPECT().DeleteWorkspace(gomock.Any(), gomock.Any())98workspaceCtrl.operations = ops99100_ = createSecret(fmt.Sprintf("%s-tokens", name), secretsNamespace)101ws := newWorkspace(name, workspaceNamespace, workspacev1.WorkspacePhaseCreating)102createWorkspace(ws)103markContentReady(ws)104105expectConditionEventually(ws, string(workspacev1.WorkspaceConditionBackupFailure), metav1.ConditionTrue, "BackupFailed")106})107108It("should report snapshot url on snapshot", func() {109name := uuid.NewString()110111mockCtrl := gomock.NewController(GinkgoT())112defer mockCtrl.Finish()113ops := NewMockWorkspaceOperations(mockCtrl)114115ops.EXPECT().BackupWorkspace(gomock.Any(), gomock.Any()).Return(nil, nil).Times(1)116ops.EXPECT().SnapshotIDs(gomock.Any(), gomock.Any()).Return("snapshotUrl", "snapshotName", nil)117ops.EXPECT().DeleteWorkspace(gomock.Any(), gomock.Any()).Return(nil).Times(1)118workspaceCtrl.operations = ops119120_ = createSecret(fmt.Sprintf("%s-tokens", name), secretsNamespace)121ws := newWorkspace(name, workspaceNamespace, workspacev1.WorkspacePhaseCreating)122ws.Spec.Type = workspacev1.WorkspaceTypePrebuild123createWorkspace(ws)124markContentReady(ws)125126Eventually(func(g Gomega) {127g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: ws.Name, Namespace: ws.Namespace}, ws)).To(Succeed())128g.Expect(ws.Status.Snapshot).ToNot(BeEmpty())129}, timeout, interval).Should(Succeed())130131expectConditionEventually(ws, string(workspacev1.WorkspaceConditionBackupComplete), metav1.ConditionTrue, "BackupComplete")132})133134})135})136137func newWorkspace(name, namespace string, phase workspacev1.WorkspacePhase) *workspacev1.Workspace {138GinkgoHelper()139initializer := &csapi.WorkspaceInitializer{140Spec: &csapi.WorkspaceInitializer_Empty{Empty: &csapi.EmptyInitializer{}},141}142initializerBytes, err := proto.Marshal(initializer)143Expect(err).ToNot(HaveOccurred())144145return &workspacev1.Workspace{146TypeMeta: metav1.TypeMeta{147APIVersion: "workspace.gitpod.io/v1",148Kind: "Workspace",149},150ObjectMeta: metav1.ObjectMeta{151Name: name,152Namespace: namespace,153Finalizers: []string{workspacev1.GitpodFinalizerName},154},155Spec: workspacev1.WorkspaceSpec{156Ownership: workspacev1.Ownership{157Owner: "foobar",158WorkspaceID: "cool-workspace",159},160Type: workspacev1.WorkspaceTypeRegular,161Class: "default",162Image: workspacev1.WorkspaceImages{163Workspace: workspacev1.WorkspaceImage{164Ref: ptr.String("alpine:latest"),165},166IDE: workspacev1.IDEImages{167Refs: []string{},168},169},170Ports: []workspacev1.PortSpec{},171Initializer: initializerBytes,172Admission: workspacev1.AdmissionSpec{173Level: workspacev1.AdmissionLevelEveryone,174},175},176}177}178179func createWorkspace(ws *workspacev1.Workspace) {180GinkgoHelper()181By("creating workspace")182Expect(k8sClient.Create(ctx, ws)).To(Succeed())183}184185func createSecret(name, namespace string) *corev1.Secret {186GinkgoHelper()187188By(fmt.Sprintf("creating secret %s", name))189secret := &corev1.Secret{190ObjectMeta: metav1.ObjectMeta{191Name: name,192Namespace: namespace,193},194StringData: map[string]string{195"git": "pod",196},197}198199Expect(k8sClient.Create(ctx, secret)).To(Succeed())200Eventually(func() error {201return k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, secret)202}, timeout, interval).Should(Succeed())203204return secret205}206207func updateObjWithRetries[O client.Object](c client.Client, obj O, updateStatus bool, update func(obj O)) {208GinkgoHelper()209Eventually(func() error {210var err error211if err = c.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, obj); err != nil {212return err213}214// Apply update.215update(obj)216if updateStatus {217err = c.Status().Update(ctx, obj)218} else {219err = c.Update(ctx, obj)220}221return err222}, timeout, interval).Should(Succeed())223}224225func expectConditionEventually(ws *workspacev1.Workspace, tpe string, status metav1.ConditionStatus, reason string) {226GinkgoHelper()227By(fmt.Sprintf("expect workspace condition %s to be %s", tpe, status))228Eventually(func(g Gomega) {229g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: ws.Name, Namespace: ws.Namespace}, ws)).To(Succeed())230c := wsk8s.GetCondition(ws.Status.Conditions, tpe)231g.Expect(c).ToNot(BeNil(), fmt.Sprintf("expected condition %s to be present", tpe))232g.Expect(c.Status).To(Equal(status))233if reason != "" {234g.Expect(c.Reason).To(Equal(reason))235}236}, timeout, interval).Should(Succeed())237}238239func expectGitStatusEventually(ws *workspacev1.Workspace, gitStatus *csapi.GitStatus) {240GinkgoHelper()241By("expect git status")242Eventually(func(g Gomega) {243g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: ws.Name, Namespace: ws.Namespace}, ws)).To(Succeed())244g.Expect(ws.Status.GitStatus.Branch).To(Equal(gitStatus.Branch))245g.Expect(ws.Status.GitStatus.LatestCommit).To(Equal(gitStatus.LatestCommit))246g.Expect(ws.Status.GitStatus.UncommitedFiles).To(Equal(gitStatus.UncommitedFiles))247g.Expect(ws.Status.GitStatus.TotalUncommitedFiles).To(Equal(gitStatus.TotalUncommitedFiles))248g.Expect(ws.Status.GitStatus.UntrackedFiles).To(Equal(gitStatus.UntrackedFiles))249g.Expect(ws.Status.GitStatus.TotalUntrackedFiles).To(Equal(gitStatus.TotalUntrackedFiles))250g.Expect(ws.Status.GitStatus.UnpushedCommits).To(Equal(gitStatus.UnpushedCommits))251g.Expect(ws.Status.GitStatus.TotalUnpushedCommits).To(Equal(gitStatus.TotalUnpushedCommits))252}, timeout, interval).Should(Succeed())253}254255func markContentReady(ws *workspacev1.Workspace) {256GinkgoHelper()257By("adding content ready condition")258updateObjWithRetries(k8sClient, ws, true, func(ws *workspacev1.Workspace) {259ws.Status.Phase = workspacev1.WorkspacePhaseStopping260ws.Status.Conditions = []metav1.Condition{261workspacev1.NewWorkspaceConditionContentReady(metav1.ConditionTrue, "InitializationSuccess", ""),262}263ws.Status.Runtime = &workspacev1.WorkspaceRuntimeStatus{264NodeName: NodeName,265}266})267}268269270