Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-manager-mk2/controllers/timeout_controller_test.go
2498 views
1
// Copyright (c) 2022 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 controllers
6
7
import (
8
"time"
9
10
wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes"
11
workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"
12
"github.com/google/uuid"
13
. "github.com/onsi/ginkgo/v2"
14
. "github.com/onsi/gomega"
15
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16
"k8s.io/apimachinery/pkg/types"
17
"k8s.io/client-go/tools/record"
18
"k8s.io/utils/pointer"
19
"sigs.k8s.io/controller-runtime/pkg/client"
20
"sigs.k8s.io/controller-runtime/pkg/client/fake"
21
"sigs.k8s.io/controller-runtime/pkg/reconcile"
22
// . "github.com/onsi/ginkgo/extensions/table"
23
)
24
25
var _ = Describe("TimeoutController", func() {
26
Context("timeouts", func() {
27
var (
28
now = time.Now()
29
conf = newTestConfig()
30
r *TimeoutReconciler
31
fakeClient client.Client
32
)
33
BeforeEach(func() {
34
var err error
35
// Use a fake client instead of the envtest's k8s client, such that we can add objects
36
// with custom CreationTimestamps and check timeout logic.
37
fakeClient = fake.NewClientBuilder().WithStatusSubresource(&workspacev1.Workspace{}).WithScheme(k8sClient.Scheme()).Build()
38
r, err = NewTimeoutReconciler(fakeClient, record.NewFakeRecorder(100), conf, &fakeMaintenance{enabled: false})
39
Expect(err).ToNot(HaveOccurred())
40
})
41
42
type testCase struct {
43
phase workspacev1.WorkspacePhase
44
lastActivityAgo *time.Duration
45
age time.Duration
46
customTimeout *time.Duration
47
customMaxLifetime *time.Duration
48
update func(ws *workspacev1.Workspace)
49
updateStatus func(ws *workspacev1.Workspace)
50
expectTimeout bool
51
}
52
DescribeTable("workspace timeouts",
53
func(tc testCase) {
54
By("creating a workspace")
55
ws := newWorkspace(uuid.NewString(), "default")
56
ws.CreationTimestamp = metav1.NewTime(now.Add(-tc.age))
57
58
if tc.lastActivityAgo != nil {
59
now := metav1.NewTime(now.Add(-*tc.lastActivityAgo))
60
ws.Status.LastActivity = &now
61
}
62
63
Expect(fakeClient.Create(ctx, ws)).To(Succeed())
64
65
updateObjWithRetries(fakeClient, ws, false, func(ws *workspacev1.Workspace) {
66
if tc.customTimeout != nil {
67
ws.Spec.Timeout.Time = &metav1.Duration{Duration: *tc.customTimeout}
68
}
69
if tc.customMaxLifetime != nil {
70
ws.Spec.Timeout.MaximumLifetime = &metav1.Duration{Duration: *tc.customMaxLifetime}
71
}
72
if tc.update != nil {
73
tc.update(ws)
74
}
75
})
76
updateObjWithRetries(fakeClient, ws, true, func(ws *workspacev1.Workspace) {
77
ws.Status.Phase = tc.phase
78
if tc.updateStatus != nil {
79
tc.updateStatus(ws)
80
}
81
})
82
83
// Run the timeout controller for this workspace.
84
By("running the TimeoutController reconcile()")
85
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: ws.Name, Namespace: ws.Namespace}})
86
Expect(err).ToNot(HaveOccurred())
87
88
if tc.expectTimeout {
89
expectTimeout(fakeClient, ws)
90
} else {
91
expectNoTimeout(fakeClient, ws)
92
}
93
},
94
Entry("should timeout creating workspace", testCase{
95
phase: workspacev1.WorkspacePhaseCreating,
96
age: 10 * time.Hour,
97
expectTimeout: true,
98
}),
99
Entry("shouldn't timeout active workspace", testCase{
100
phase: workspacev1.WorkspacePhaseRunning,
101
lastActivityAgo: pointer.Duration(1 * time.Minute),
102
age: 10 * time.Hour,
103
expectTimeout: false,
104
}),
105
Entry("should timeout inactive workspace", testCase{
106
phase: workspacev1.WorkspacePhaseRunning,
107
lastActivityAgo: pointer.Duration(2 * time.Hour),
108
age: 10 * time.Hour,
109
expectTimeout: true,
110
}),
111
Entry("should timeout inactive workspace with custom timeout", testCase{
112
phase: workspacev1.WorkspacePhaseRunning,
113
// Use a lastActivity that would not trigger the default timeout, but does trigger the custom timeout.
114
lastActivityAgo: pointer.Duration(time.Duration(conf.Timeouts.RegularWorkspace / 2)),
115
customTimeout: pointer.Duration(time.Duration(conf.Timeouts.RegularWorkspace / 3)),
116
age: 10 * time.Hour,
117
expectTimeout: true,
118
}),
119
Entry("should timeout closed workspace", testCase{
120
phase: workspacev1.WorkspacePhaseRunning,
121
updateStatus: func(ws *workspacev1.Workspace) {
122
ws.Status.Conditions = wsk8s.AddUniqueCondition(ws.Status.Conditions, metav1.Condition{
123
Type: string(workspacev1.WorkspaceConditionClosed),
124
LastTransitionTime: metav1.Now(),
125
Status: metav1.ConditionTrue,
126
})
127
},
128
age: 5 * time.Hour,
129
lastActivityAgo: pointer.Duration(10 * time.Minute),
130
expectTimeout: true,
131
}),
132
Entry("should timeout headless workspace", testCase{
133
phase: workspacev1.WorkspacePhaseRunning,
134
update: func(ws *workspacev1.Workspace) {
135
ws.Spec.Type = workspacev1.WorkspaceTypePrebuild
136
},
137
age: 2 * time.Hour,
138
lastActivityAgo: nil,
139
expectTimeout: true,
140
}),
141
Entry("should timeout workspace with no custom lifetime", testCase{
142
phase: workspacev1.WorkspacePhaseRunning,
143
age: 50 * time.Hour,
144
lastActivityAgo: pointer.Duration(1 * time.Minute),
145
expectTimeout: true,
146
}),
147
Entry("should timeout workspace with custom lifetime", testCase{
148
phase: workspacev1.WorkspacePhaseRunning,
149
age: 12 * time.Hour,
150
customMaxLifetime: pointer.Duration(8 * time.Hour),
151
lastActivityAgo: pointer.Duration(1 * time.Minute),
152
expectTimeout: true,
153
}),
154
Entry("should timeout after controller restart if no FirstUserActivity", testCase{
155
phase: workspacev1.WorkspacePhaseRunning,
156
age: 5 * time.Hour,
157
lastActivityAgo: nil, // No last activity recorded yet after controller restart.
158
expectTimeout: true,
159
}),
160
Entry("should timeout eventually with no user activity after controller restart", testCase{
161
phase: workspacev1.WorkspacePhaseRunning,
162
updateStatus: func(ws *workspacev1.Workspace) {
163
ws.Status.Conditions = wsk8s.AddUniqueCondition(ws.Status.Conditions, metav1.Condition{
164
Type: string(workspacev1.WorkspaceConditionFirstUserActivity),
165
Status: metav1.ConditionTrue,
166
LastTransitionTime: metav1.NewTime(now.Add(-5 * time.Hour)),
167
})
168
},
169
age: 5 * time.Hour,
170
lastActivityAgo: nil,
171
expectTimeout: true,
172
}),
173
)
174
})
175
176
Context("reconciliation", func() {
177
var r *TimeoutReconciler
178
BeforeEach(func() {
179
var err error
180
r, err = NewTimeoutReconciler(k8sClient, record.NewFakeRecorder(100), newTestConfig(), &fakeMaintenance{enabled: false})
181
Expect(err).ToNot(HaveOccurred())
182
})
183
184
It("should requeue timeout reconciles", func() {
185
ws := newWorkspace(uuid.NewString(), "default")
186
_ = createWorkspaceExpectPod(ws)
187
188
res, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: ws.Name, Namespace: ws.Namespace}})
189
Expect(err).To(BeNil())
190
Expect(r.reconcileInterval).ToNot(BeZero(), "reconcile interval should be > 0, otherwise events will not requeue")
191
Expect(res.RequeueAfter).To(Equal(r.reconcileInterval))
192
})
193
194
It("should not requeue when resource is not found", func() {
195
res, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: "does-not-exist", Namespace: "default"}})
196
Expect(err).ToNot(HaveOccurred(), "not-found errors should not be returned")
197
Expect(res.Requeue).To(BeFalse())
198
Expect(res.RequeueAfter).To(BeZero())
199
})
200
201
It("should return an error other than not-found", func() {
202
// Create a different error than "not-found", easiest is to provide an empty name which returns an "invalid request".
203
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: "", Namespace: "default"}})
204
Expect(err).To(HaveOccurred(), "should return error and requeue")
205
})
206
})
207
})
208
209
func expectNoTimeout(c client.Client, ws *workspacev1.Workspace) {
210
GinkgoHelper()
211
By("expecting controller to not timeout workspace")
212
Consistently(func(g Gomega) {
213
g.Expect(c.Get(ctx, types.NamespacedName{Name: ws.Name, Namespace: ws.Namespace}, ws)).To(Succeed())
214
g.Expect(wsk8s.GetCondition(ws.Status.Conditions, string(workspacev1.WorkspaceConditionTimeout))).To(BeNil())
215
}, duration, interval).Should(Succeed())
216
}
217
218
func expectTimeout(c client.Client, ws *workspacev1.Workspace) {
219
GinkgoHelper()
220
By("expecting controller to timeout workspace")
221
Eventually(func(g Gomega) {
222
g.Expect(c.Get(ctx, types.NamespacedName{Name: ws.Name, Namespace: ws.Namespace}, ws)).To(Succeed())
223
cond := wsk8s.GetCondition(ws.Status.Conditions, string(workspacev1.WorkspaceConditionTimeout))
224
g.Expect(cond).ToNot(BeNil())
225
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
226
}, timeout, interval).Should(Succeed())
227
}
228
229