@@ -3,6 +3,7 @@ package prebuilds_test
3
3
import (
4
4
"context"
5
5
"database/sql"
6
+ "errors"
6
7
"strings"
7
8
"sync/atomic"
8
9
"testing"
@@ -27,6 +28,32 @@ import (
27
28
"github.com/coder/coder/v2/testutil"
28
29
)
29
30
31
+ type errorStore struct {
32
+ claimingErr error
33
+
34
+ database.Store
35
+ }
36
+
37
+ func newErrorStore (db database.Store ,claimingErr error )* errorStore {
38
+ return & errorStore {
39
+ Store :db ,
40
+ claimingErr :claimingErr ,
41
+ }
42
+ }
43
+
44
+ func (es * errorStore )InTx (fn func (store database.Store )error ,opts * database.TxOptions )error {
45
+ // Pass failure store down into transaction store.
46
+ return es .Store .InTx (func (store database.Store )error {
47
+ newES := newErrorStore (store ,es .claimingErr )
48
+
49
+ return fn (newES )
50
+ },opts )
51
+ }
52
+
53
+ func (es * errorStore )ClaimPrebuiltWorkspace (ctx context.Context ,arg database.ClaimPrebuiltWorkspaceParams ) (database.ClaimPrebuiltWorkspaceRow ,error ) {
54
+ return database.ClaimPrebuiltWorkspaceRow {},es .claimingErr
55
+ }
56
+
30
57
type storeSpy struct {
31
58
database.Store
32
59
@@ -66,6 +93,171 @@ func (m *storeSpy) ClaimPrebuiltWorkspace(ctx context.Context, arg database.Clai
66
93
return result ,err
67
94
}
68
95
96
+ func TestClaimPrebuild_CheckDifferentErrors (t * testing.T ) {
97
+ t .Parallel ()
98
+
99
+ if ! dbtestutil .WillUsePostgres () {
100
+ t .Skip ("This test requires postgres" )
101
+ }
102
+
103
+ const (
104
+ desiredInstances = 1
105
+ presetCount = 2
106
+
107
+ expectedPrebuildsCount = desiredInstances * presetCount
108
+ )
109
+
110
+ cases := map [string ]struct {
111
+ claimingErr error
112
+ checkFn func (
113
+ t * testing.T ,
114
+ ctx context.Context ,
115
+ store database.Store ,
116
+ userClient * codersdk.Client ,
117
+ user codersdk.User ,
118
+ templateVersionID uuid.UUID ,
119
+ presetID uuid.UUID ,
120
+ )
121
+ }{
122
+ "ErrNoClaimablePrebuiltWorkspaces is returned" : {
123
+ claimingErr :agplprebuilds .ErrNoClaimablePrebuiltWorkspaces ,
124
+ checkFn :func (
125
+ t * testing.T ,
126
+ ctx context.Context ,
127
+ store database.Store ,
128
+ userClient * codersdk.Client ,
129
+ user codersdk.User ,
130
+ templateVersionID uuid.UUID ,
131
+ presetID uuid.UUID ,
132
+ ) {
133
+ // When: a user creates a new workspace with a preset for which prebuilds are configured.
134
+ workspaceName := strings .ReplaceAll (testutil .GetRandomName (t ),"_" ,"-" )
135
+ userWorkspace ,err := userClient .CreateUserWorkspace (ctx ,user .Username , codersdk.CreateWorkspaceRequest {
136
+ TemplateVersionID :templateVersionID ,
137
+ Name :workspaceName ,
138
+ TemplateVersionPresetID :presetID ,
139
+ })
140
+
141
+ require .NoError (t ,err )
142
+ coderdtest .AwaitWorkspaceBuildJobCompleted (t ,userClient ,userWorkspace .LatestBuild .ID )
143
+
144
+ // Then: the number of running prebuilds hasn't changed because claiming prebuild is failed and we fallback to creating new workspace.
145
+ currentPrebuilds ,err := store .GetRunningPrebuiltWorkspaces (ctx )
146
+ require .NoError (t ,err )
147
+ require .Equal (t ,expectedPrebuildsCount ,len (currentPrebuilds ))
148
+ },
149
+ },
150
+ "unexpected error during claim is returned" : {
151
+ claimingErr :errors .New ("unexpected error during claim" ),
152
+ checkFn :func (
153
+ t * testing.T ,
154
+ ctx context.Context ,
155
+ store database.Store ,
156
+ userClient * codersdk.Client ,
157
+ user codersdk.User ,
158
+ templateVersionID uuid.UUID ,
159
+ presetID uuid.UUID ,
160
+ ) {
161
+ // When: a user creates a new workspace with a preset for which prebuilds are configured.
162
+ workspaceName := strings .ReplaceAll (testutil .GetRandomName (t ),"_" ,"-" )
163
+ _ ,err := userClient .CreateUserWorkspace (ctx ,user .Username , codersdk.CreateWorkspaceRequest {
164
+ TemplateVersionID :templateVersionID ,
165
+ Name :workspaceName ,
166
+ TemplateVersionPresetID :presetID ,
167
+ })
168
+
169
+ // Then: unexpected error happened and was propagated all the way to the caller
170
+ require .Error (t ,err )
171
+ require .ErrorContains (t ,err ,"unexpected error during claim" )
172
+ },
173
+ },
174
+ }
175
+
176
+ for name ,tc := range cases {
177
+ t .Run (name ,func (t * testing.T ) {
178
+ t .Parallel ()
179
+
180
+ // Setup.
181
+ ctx := testutil .Context (t ,testutil .WaitMedium )
182
+ db ,pubsub := dbtestutil .NewDB (t )
183
+ failureStore := newErrorStore (db ,tc .claimingErr )
184
+
185
+ logger := testutil .Logger (t )
186
+ client ,_ ,api ,owner := coderdenttest .NewWithAPI (t ,& coderdenttest.Options {
187
+ Options :& coderdtest.Options {
188
+ IncludeProvisionerDaemon :true ,
189
+ Database :failureStore ,
190
+ Pubsub :pubsub ,
191
+ },
192
+
193
+ EntitlementsUpdateInterval :time .Second ,
194
+ })
195
+
196
+ reconciler := prebuilds .NewStoreReconciler (failureStore ,pubsub , codersdk.PrebuildsConfig {},logger ,quartz .NewMock (t ))
197
+ var claimer agplprebuilds.Claimer = & prebuilds.EnterpriseClaimer {}
198
+ api .AGPL .PrebuildsClaimer .Store (& claimer )
199
+
200
+ version := coderdtest .CreateTemplateVersion (t ,client ,owner .OrganizationID ,templateWithAgentAndPresetsWithPrebuilds (desiredInstances ))
201
+ _ = coderdtest .AwaitTemplateVersionJobCompleted (t ,client ,version .ID )
202
+ coderdtest .CreateTemplate (t ,client ,owner .OrganizationID ,version .ID )
203
+ presets ,err := client .TemplateVersionPresets (ctx ,version .ID )
204
+ require .NoError (t ,err )
205
+ require .Len (t ,presets ,presetCount )
206
+
207
+ userClient ,user := coderdtest .CreateAnotherUser (t ,client ,owner .OrganizationID ,rbac .RoleMember ())
208
+
209
+ ctx = dbauthz .AsPrebuildsOrchestrator (ctx )
210
+
211
+ // Given: the reconciliation state is snapshot.
212
+ state ,err := reconciler .SnapshotState (ctx ,failureStore )
213
+ require .NoError (t ,err )
214
+ require .Len (t ,state .Presets ,presetCount )
215
+
216
+ // When: a reconciliation is setup for each preset.
217
+ for _ ,preset := range presets {
218
+ ps ,err := state .FilterByPreset (preset .ID )
219
+ require .NoError (t ,err )
220
+ require .NotNil (t ,ps )
221
+ actions ,err := reconciler .CalculateActions (ctx ,* ps )
222
+ require .NoError (t ,err )
223
+ require .NotNil (t ,actions )
224
+
225
+ require .NoError (t ,reconciler .ReconcilePreset (ctx ,* ps ))
226
+ }
227
+
228
+ // Given: a set of running, eligible prebuilds eventually starts up.
229
+ runningPrebuilds := make (map [uuid.UUID ]database.GetRunningPrebuiltWorkspacesRow ,desiredInstances * presetCount )
230
+ require .Eventually (t ,func ()bool {
231
+ rows ,err := failureStore .GetRunningPrebuiltWorkspaces (ctx )
232
+ require .NoError (t ,err )
233
+
234
+ for _ ,row := range rows {
235
+ runningPrebuilds [row .CurrentPresetID .UUID ]= row
236
+
237
+ agents ,err := db .GetWorkspaceAgentsInLatestBuildByWorkspaceID (ctx ,row .ID )
238
+ require .NoError (t ,err )
239
+
240
+ // Workspaces are eligible once its agent is marked "ready".
241
+ for _ ,agent := range agents {
242
+ require .NoError (t ,db .UpdateWorkspaceAgentLifecycleStateByID (ctx , database.UpdateWorkspaceAgentLifecycleStateByIDParams {
243
+ ID :agent .ID ,
244
+ LifecycleState :database .WorkspaceAgentLifecycleStateReady ,
245
+ StartedAt : sql.NullTime {Time :time .Now ().Add (time .Hour ),Valid :true },
246
+ ReadyAt : sql.NullTime {Time :time .Now ().Add (- 1 * time .Hour ),Valid :true },
247
+ }))
248
+ }
249
+ }
250
+
251
+ t .Logf ("found %d running prebuilds so far, want %d" ,len (runningPrebuilds ),expectedPrebuildsCount )
252
+
253
+ return len (runningPrebuilds )== expectedPrebuildsCount
254
+ },testutil .WaitSuperLong ,testutil .IntervalSlow )
255
+
256
+ tc .checkFn (t ,ctx ,failureStore ,userClient ,user ,version .ID ,presets [0 ].ID )
257
+ })
258
+ }
259
+ }
260
+
69
261
func TestClaimPrebuild (t * testing.T ) {
70
262
t .Parallel ()
71
263