Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-daemon/pkg/controller/workspace_controller_test.go
2500 views
1
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package controller
6
7
import (
8
"fmt"
9
"time"
10
11
"github.com/aws/smithy-go/ptr"
12
wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes"
13
csapi "github.com/gitpod-io/gitpod/content-service/api"
14
workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"
15
"github.com/golang/mock/gomock"
16
"github.com/google/uuid"
17
. "github.com/onsi/ginkgo/v2"
18
. "github.com/onsi/gomega"
19
"google.golang.org/protobuf/proto"
20
corev1 "k8s.io/api/core/v1"
21
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22
"k8s.io/apimachinery/pkg/types"
23
"sigs.k8s.io/controller-runtime/pkg/client"
24
)
25
26
const (
27
timeout = time.Second * 20
28
duration = time.Second * 2
29
interval = time.Millisecond * 250
30
workspaceNamespace = "default"
31
)
32
33
var _ = Describe("WorkspaceController", func() {
34
Context("with regular workspace", func() {
35
It("should handle regular content init", func() {
36
name := uuid.NewString()
37
38
mockCtrl := gomock.NewController(GinkgoT())
39
defer mockCtrl.Finish()
40
ops := NewMockWorkspaceOperations(mockCtrl)
41
42
ops.EXPECT().InitWorkspace(gomock.Any(), gomock.Any()).Return(&csapi.InitializerMetrics{}, "", nil).Times(1)
43
workspaceCtrl.operations = ops
44
45
_ = createSecret(fmt.Sprintf("%s-tokens", name), secretsNamespace)
46
ws := newWorkspace(name, workspaceNamespace, workspacev1.WorkspacePhaseCreating)
47
createWorkspace(ws)
48
updateObjWithRetries(k8sClient, ws, true, func(ws *workspacev1.Workspace) {
49
ws.Status.Phase = workspacev1.WorkspacePhaseCreating
50
ws.Status.Conditions = []metav1.Condition{}
51
ws.Status.Runtime = &workspacev1.WorkspaceRuntimeStatus{
52
NodeName: NodeName,
53
}
54
})
55
56
expectConditionEventually(ws, string(workspacev1.WorkspaceConditionContentReady), metav1.ConditionTrue, "InitializationSuccess")
57
})
58
59
It("should handle regular content backup", func() {
60
name := uuid.NewString()
61
62
mockCtrl := gomock.NewController(GinkgoT())
63
defer mockCtrl.Finish()
64
ops := NewMockWorkspaceOperations(mockCtrl)
65
66
gitStatus := &csapi.GitStatus{
67
Branch: "main",
68
LatestCommit: "991300e0cf199116685f25561702a145d40ae462",
69
UncommitedFiles: []string{"git", "pod"},
70
TotalUncommitedFiles: 2,
71
UntrackedFiles: []string{"kumquat"},
72
TotalUntrackedFiles: 1,
73
UnpushedCommits: []string{"df591ed557c9afa8b6bcd1f51809d83d3f48fc43"},
74
TotalUnpushedCommits: 1,
75
}
76
77
ops.EXPECT().BackupWorkspace(gomock.Any(), gomock.Any()).Return(gitStatus, nil).Times(1)
78
ops.EXPECT().DeleteWorkspace(gomock.Any(), gomock.Any())
79
workspaceCtrl.operations = ops
80
81
_ = createSecret(fmt.Sprintf("%s-tokens", name), secretsNamespace)
82
ws := newWorkspace(name, workspaceNamespace, workspacev1.WorkspacePhaseCreating)
83
createWorkspace(ws)
84
markContentReady(ws)
85
86
expectConditionEventually(ws, string(workspacev1.WorkspaceConditionBackupComplete), metav1.ConditionTrue, "BackupComplete")
87
expectGitStatusEventually(ws, gitStatus)
88
})
89
90
It("should report backup failure", func() {
91
name := uuid.NewString()
92
93
mockCtrl := gomock.NewController(GinkgoT())
94
defer mockCtrl.Finish()
95
ops := NewMockWorkspaceOperations(mockCtrl)
96
97
ops.EXPECT().BackupWorkspace(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("BOOM!")).Times(1)
98
ops.EXPECT().DeleteWorkspace(gomock.Any(), gomock.Any())
99
workspaceCtrl.operations = ops
100
101
_ = createSecret(fmt.Sprintf("%s-tokens", name), secretsNamespace)
102
ws := newWorkspace(name, workspaceNamespace, workspacev1.WorkspacePhaseCreating)
103
createWorkspace(ws)
104
markContentReady(ws)
105
106
expectConditionEventually(ws, string(workspacev1.WorkspaceConditionBackupFailure), metav1.ConditionTrue, "BackupFailed")
107
})
108
109
It("should report snapshot url on snapshot", func() {
110
name := uuid.NewString()
111
112
mockCtrl := gomock.NewController(GinkgoT())
113
defer mockCtrl.Finish()
114
ops := NewMockWorkspaceOperations(mockCtrl)
115
116
ops.EXPECT().BackupWorkspace(gomock.Any(), gomock.Any()).Return(nil, nil).Times(1)
117
ops.EXPECT().SnapshotIDs(gomock.Any(), gomock.Any()).Return("snapshotUrl", "snapshotName", nil)
118
ops.EXPECT().DeleteWorkspace(gomock.Any(), gomock.Any()).Return(nil).Times(1)
119
workspaceCtrl.operations = ops
120
121
_ = createSecret(fmt.Sprintf("%s-tokens", name), secretsNamespace)
122
ws := newWorkspace(name, workspaceNamespace, workspacev1.WorkspacePhaseCreating)
123
ws.Spec.Type = workspacev1.WorkspaceTypePrebuild
124
createWorkspace(ws)
125
markContentReady(ws)
126
127
Eventually(func(g Gomega) {
128
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: ws.Name, Namespace: ws.Namespace}, ws)).To(Succeed())
129
g.Expect(ws.Status.Snapshot).ToNot(BeEmpty())
130
}, timeout, interval).Should(Succeed())
131
132
expectConditionEventually(ws, string(workspacev1.WorkspaceConditionBackupComplete), metav1.ConditionTrue, "BackupComplete")
133
})
134
135
})
136
})
137
138
func newWorkspace(name, namespace string, phase workspacev1.WorkspacePhase) *workspacev1.Workspace {
139
GinkgoHelper()
140
initializer := &csapi.WorkspaceInitializer{
141
Spec: &csapi.WorkspaceInitializer_Empty{Empty: &csapi.EmptyInitializer{}},
142
}
143
initializerBytes, err := proto.Marshal(initializer)
144
Expect(err).ToNot(HaveOccurred())
145
146
return &workspacev1.Workspace{
147
TypeMeta: metav1.TypeMeta{
148
APIVersion: "workspace.gitpod.io/v1",
149
Kind: "Workspace",
150
},
151
ObjectMeta: metav1.ObjectMeta{
152
Name: name,
153
Namespace: namespace,
154
Finalizers: []string{workspacev1.GitpodFinalizerName},
155
},
156
Spec: workspacev1.WorkspaceSpec{
157
Ownership: workspacev1.Ownership{
158
Owner: "foobar",
159
WorkspaceID: "cool-workspace",
160
},
161
Type: workspacev1.WorkspaceTypeRegular,
162
Class: "default",
163
Image: workspacev1.WorkspaceImages{
164
Workspace: workspacev1.WorkspaceImage{
165
Ref: ptr.String("alpine:latest"),
166
},
167
IDE: workspacev1.IDEImages{
168
Refs: []string{},
169
},
170
},
171
Ports: []workspacev1.PortSpec{},
172
Initializer: initializerBytes,
173
Admission: workspacev1.AdmissionSpec{
174
Level: workspacev1.AdmissionLevelEveryone,
175
},
176
},
177
}
178
}
179
180
func createWorkspace(ws *workspacev1.Workspace) {
181
GinkgoHelper()
182
By("creating workspace")
183
Expect(k8sClient.Create(ctx, ws)).To(Succeed())
184
}
185
186
func createSecret(name, namespace string) *corev1.Secret {
187
GinkgoHelper()
188
189
By(fmt.Sprintf("creating secret %s", name))
190
secret := &corev1.Secret{
191
ObjectMeta: metav1.ObjectMeta{
192
Name: name,
193
Namespace: namespace,
194
},
195
StringData: map[string]string{
196
"git": "pod",
197
},
198
}
199
200
Expect(k8sClient.Create(ctx, secret)).To(Succeed())
201
Eventually(func() error {
202
return k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, secret)
203
}, timeout, interval).Should(Succeed())
204
205
return secret
206
}
207
208
func updateObjWithRetries[O client.Object](c client.Client, obj O, updateStatus bool, update func(obj O)) {
209
GinkgoHelper()
210
Eventually(func() error {
211
var err error
212
if err = c.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, obj); err != nil {
213
return err
214
}
215
// Apply update.
216
update(obj)
217
if updateStatus {
218
err = c.Status().Update(ctx, obj)
219
} else {
220
err = c.Update(ctx, obj)
221
}
222
return err
223
}, timeout, interval).Should(Succeed())
224
}
225
226
func expectConditionEventually(ws *workspacev1.Workspace, tpe string, status metav1.ConditionStatus, reason string) {
227
GinkgoHelper()
228
By(fmt.Sprintf("expect workspace condition %s to be %s", tpe, status))
229
Eventually(func(g Gomega) {
230
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: ws.Name, Namespace: ws.Namespace}, ws)).To(Succeed())
231
c := wsk8s.GetCondition(ws.Status.Conditions, tpe)
232
g.Expect(c).ToNot(BeNil(), fmt.Sprintf("expected condition %s to be present", tpe))
233
g.Expect(c.Status).To(Equal(status))
234
if reason != "" {
235
g.Expect(c.Reason).To(Equal(reason))
236
}
237
}, timeout, interval).Should(Succeed())
238
}
239
240
func expectGitStatusEventually(ws *workspacev1.Workspace, gitStatus *csapi.GitStatus) {
241
GinkgoHelper()
242
By("expect git status")
243
Eventually(func(g Gomega) {
244
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: ws.Name, Namespace: ws.Namespace}, ws)).To(Succeed())
245
g.Expect(ws.Status.GitStatus.Branch).To(Equal(gitStatus.Branch))
246
g.Expect(ws.Status.GitStatus.LatestCommit).To(Equal(gitStatus.LatestCommit))
247
g.Expect(ws.Status.GitStatus.UncommitedFiles).To(Equal(gitStatus.UncommitedFiles))
248
g.Expect(ws.Status.GitStatus.TotalUncommitedFiles).To(Equal(gitStatus.TotalUncommitedFiles))
249
g.Expect(ws.Status.GitStatus.UntrackedFiles).To(Equal(gitStatus.UntrackedFiles))
250
g.Expect(ws.Status.GitStatus.TotalUntrackedFiles).To(Equal(gitStatus.TotalUntrackedFiles))
251
g.Expect(ws.Status.GitStatus.UnpushedCommits).To(Equal(gitStatus.UnpushedCommits))
252
g.Expect(ws.Status.GitStatus.TotalUnpushedCommits).To(Equal(gitStatus.TotalUnpushedCommits))
253
}, timeout, interval).Should(Succeed())
254
}
255
256
func markContentReady(ws *workspacev1.Workspace) {
257
GinkgoHelper()
258
By("adding content ready condition")
259
updateObjWithRetries(k8sClient, ws, true, func(ws *workspacev1.Workspace) {
260
ws.Status.Phase = workspacev1.WorkspacePhaseStopping
261
ws.Status.Conditions = []metav1.Condition{
262
workspacev1.NewWorkspaceConditionContentReady(metav1.ConditionTrue, "InitializationSuccess", ""),
263
}
264
ws.Status.Runtime = &workspacev1.WorkspaceRuntimeStatus{
265
NodeName: NodeName,
266
}
267
})
268
}
269
270