1
1
package schedule_test
2
2
3
3
import (
4
+ "context"
4
5
"database/sql"
5
6
"encoding/json"
6
7
"fmt"
@@ -17,14 +18,18 @@ import (
17
18
18
19
"github.com/coder/coder/v2/coderd/database"
19
20
"github.com/coder/coder/v2/coderd/database/dbauthz"
21
+ "github.com/coder/coder/v2/coderd/database/dbfake"
20
22
"github.com/coder/coder/v2/coderd/database/dbgen"
21
23
"github.com/coder/coder/v2/coderd/database/dbtestutil"
22
24
"github.com/coder/coder/v2/coderd/database/dbtime"
23
25
"github.com/coder/coder/v2/coderd/notifications"
24
26
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
25
27
agplschedule "github.com/coder/coder/v2/coderd/schedule"
28
+ "github.com/coder/coder/v2/coderd/schedule/cron"
29
+ "github.com/coder/coder/v2/codersdk"
26
30
"github.com/coder/coder/v2/cryptorand"
27
31
"github.com/coder/coder/v2/enterprise/coderd/schedule"
32
+ "github.com/coder/coder/v2/provisionersdk/proto"
28
33
"github.com/coder/coder/v2/testutil"
29
34
"github.com/coder/quartz"
30
35
)
@@ -979,6 +984,252 @@ func TestTemplateTTL(t *testing.T) {
979
984
})
980
985
}
981
986
987
+ func TestTemplateUpdatePrebuilds (t * testing.T ) {
988
+ t .Parallel ()
989
+
990
+ // Dormant auto-delete configured to 10 hours
991
+ dormantAutoDelete := 10 * time .Hour
992
+
993
+ // TTL configured to 8 hours
994
+ ttl := 8 * time .Hour
995
+
996
+ // Autostop configuration set to everyday at midnight
997
+ autostopWeekdays , err := codersdk .WeekdaysToBitmap (codersdk .AllDaysOfWeek )
998
+ require .NoError (t , err )
999
+
1000
+ // Autostart configuration set to everyday at midnight
1001
+ autostartSchedule , err := cron .Weekly ("CRON_TZ=UTC 0 0 * * *" )
1002
+ require .NoError (t , err )
1003
+ autostartWeekdays , err := codersdk .WeekdaysToBitmap (codersdk .AllDaysOfWeek )
1004
+ require .NoError (t , err )
1005
+
1006
+ cases := []struct {
1007
+ name string
1008
+ templateSchedule agplschedule.TemplateScheduleOptions
1009
+ workspaceUpdate func (* testing.T , context.Context , database.Store , time.Time , database.ClaimPrebuiltWorkspaceRow )
1010
+ assertWorkspace func (* testing.T , context.Context , database.Store , time.Time , bool , database.Workspace )
1011
+ }{
1012
+ {
1013
+ name : "TemplateDormantAutoDeleteUpdatePrebuildAfterClaim" ,
1014
+ templateSchedule : agplschedule.TemplateScheduleOptions {
1015
+ // Template level TimeTilDormantAutodelete set to 10 hours
1016
+ TimeTilDormantAutoDelete : dormantAutoDelete ,
1017
+ },
1018
+ workspaceUpdate : func (t * testing.T , ctx context.Context , db database.Store , now time.Time ,
1019
+ workspace database.ClaimPrebuiltWorkspaceRow ,
1020
+ ) {
1021
+ // When: the workspace is marked dormant
1022
+ dormantWorkspace , err := db .UpdateWorkspaceDormantDeletingAt (ctx , database.UpdateWorkspaceDormantDeletingAtParams {
1023
+ ID : workspace .ID ,
1024
+ DormantAt : sql.NullTime {
1025
+ Time : now ,
1026
+ Valid : true ,
1027
+ },
1028
+ })
1029
+ require .NoError (t , err )
1030
+ require .NotNil (t , dormantWorkspace .DormantAt )
1031
+ },
1032
+ assertWorkspace : func (t * testing.T , ctx context.Context , db database.Store , now time.Time ,
1033
+ isPrebuild bool , workspace database.Workspace ,
1034
+ ) {
1035
+ if isPrebuild {
1036
+ // The unclaimed prebuild should have an empty DormantAt and DeletingAt
1037
+ require .True (t , workspace .DormantAt .Time .IsZero ())
1038
+ require .True (t , workspace .DeletingAt .Time .IsZero ())
1039
+ } else {
1040
+ // The claimed workspace should have its DormantAt and DeletingAt updated
1041
+ require .False (t , workspace .DormantAt .Time .IsZero ())
1042
+ require .False (t , workspace .DeletingAt .Time .IsZero ())
1043
+ require .WithinDuration (t , now .UTC (), workspace .DormantAt .Time .UTC (), time .Second )
1044
+ require .WithinDuration (t , now .Add (dormantAutoDelete ).UTC (), workspace .DeletingAt .Time .UTC (), time .Second )
1045
+ }
1046
+ },
1047
+ },
1048
+ {
1049
+ name : "TemplateTTLUpdatePrebuildAfterClaim" ,
1050
+ templateSchedule : agplschedule.TemplateScheduleOptions {
1051
+ // Template level TTL can only be set if autostop is disabled for users
1052
+ DefaultTTL : ttl ,
1053
+ UserAutostopEnabled : false ,
1054
+ },
1055
+ workspaceUpdate : func (t * testing.T , ctx context.Context , db database.Store , now time.Time ,
1056
+ workspace database.ClaimPrebuiltWorkspaceRow ) {
1057
+ },
1058
+ assertWorkspace : func (t * testing.T , ctx context.Context , db database.Store , now time.Time ,
1059
+ isPrebuild bool , workspace database.Workspace ,
1060
+ ) {
1061
+ if isPrebuild {
1062
+ // The unclaimed prebuild should have an empty TTL
1063
+ require .Equal (t , sql.NullInt64 {}, workspace .Ttl )
1064
+ } else {
1065
+ // The claimed workspace should have its TTL updated
1066
+ require .Equal (t , sql.NullInt64 {Int64 : int64 (ttl ), Valid : true }, workspace .Ttl )
1067
+ }
1068
+ },
1069
+ },
1070
+ {
1071
+ name : "TemplateAutostopUpdatePrebuildAfterClaim" ,
1072
+ templateSchedule : agplschedule.TemplateScheduleOptions {
1073
+ // Template level Autostop set for everyday
1074
+ AutostopRequirement : agplschedule.TemplateAutostopRequirement {
1075
+ DaysOfWeek : autostopWeekdays ,
1076
+ Weeks : 0 ,
1077
+ },
1078
+ },
1079
+ workspaceUpdate : func (t * testing.T , ctx context.Context , db database.Store , now time.Time ,
1080
+ workspace database.ClaimPrebuiltWorkspaceRow ) {
1081
+ },
1082
+ assertWorkspace : func (t * testing.T , ctx context.Context , db database.Store , now time.Time , isPrebuild bool , workspace database.Workspace ) {
1083
+ if isPrebuild {
1084
+ // The unclaimed prebuild should have an empty MaxDeadline
1085
+ prebuildBuild , err := db .GetLatestWorkspaceBuildByWorkspaceID (ctx , workspace .ID )
1086
+ require .NoError (t , err )
1087
+ require .True (t , prebuildBuild .MaxDeadline .IsZero ())
1088
+ } else {
1089
+ // The claimed workspace should have its MaxDeadline updated
1090
+ workspaceBuild , err := db .GetLatestWorkspaceBuildByWorkspaceID (ctx , workspace .ID )
1091
+ require .NoError (t , err )
1092
+ require .False (t , workspaceBuild .MaxDeadline .IsZero ())
1093
+ }
1094
+ },
1095
+ },
1096
+ {
1097
+ name : "TemplateAutostartUpdatePrebuildAfterClaim" ,
1098
+ templateSchedule : agplschedule.TemplateScheduleOptions {
1099
+ // Template level Autostart set for everyday
1100
+ UserAutostartEnabled : true ,
1101
+ AutostartRequirement : agplschedule.TemplateAutostartRequirement {
1102
+ DaysOfWeek : autostartWeekdays ,
1103
+ },
1104
+ },
1105
+ workspaceUpdate : func (t * testing.T , ctx context.Context , db database.Store , now time.Time , workspace database.ClaimPrebuiltWorkspaceRow ) {
1106
+ // To compute NextStartAt, the workspace must have a valid autostart schedule
1107
+ err = db .UpdateWorkspaceAutostart (ctx , database.UpdateWorkspaceAutostartParams {
1108
+ ID : workspace .ID ,
1109
+ AutostartSchedule : sql.NullString {
1110
+ String : autostartSchedule .String (),
1111
+ Valid : true ,
1112
+ },
1113
+ })
1114
+ require .NoError (t , err )
1115
+ },
1116
+ assertWorkspace : func (t * testing.T , ctx context.Context , db database.Store , now time.Time , isPrebuild bool , workspace database.Workspace ) {
1117
+ if isPrebuild {
1118
+ // The unclaimed prebuild should have an empty NextStartAt
1119
+ require .True (t , workspace .NextStartAt .Time .IsZero ())
1120
+ } else {
1121
+ // The claimed workspace should have its NextStartAt updated
1122
+ require .False (t , workspace .NextStartAt .Time .IsZero ())
1123
+ }
1124
+ },
1125
+ },
1126
+ }
1127
+
1128
+ for _ , tc := range cases {
1129
+ tc := tc
1130
+ t .Run (tc .name , func (t * testing.T ) {
1131
+ t .Parallel ()
1132
+
1133
+ clock := quartz .NewMock (t )
1134
+ clock .Set (dbtime .Now ())
1135
+
1136
+ // Setup
1137
+ var (
1138
+ logger = slogtest .Make (t , & slogtest.Options {IgnoreErrors : true }).Leveled (slog .LevelDebug )
1139
+ db , _ = dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1140
+ ctx = testutil .Context (t , testutil .WaitLong )
1141
+ user = dbgen .User (t , db , database.User {})
1142
+ )
1143
+
1144
+ // Setup the template schedule store
1145
+ notifyEnq := notifications .NewNoopEnqueuer ()
1146
+ const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
1147
+ userQuietHoursStore , err := schedule .NewEnterpriseUserQuietHoursScheduleStore (userQuietHoursSchedule , true )
1148
+ require .NoError (t , err )
1149
+ userQuietHoursStorePtr := & atomic.Pointer [agplschedule.UserQuietHoursScheduleStore ]{}
1150
+ userQuietHoursStorePtr .Store (& userQuietHoursStore )
1151
+ templateScheduleStore := schedule .NewEnterpriseTemplateScheduleStore (userQuietHoursStorePtr , notifyEnq , logger , clock )
1152
+
1153
+ // Given: a template and a template version with preset and a prebuilt workspace
1154
+ presetID := uuid .New ()
1155
+ org := dbfake .Organization (t , db ).Do ()
1156
+ tv := dbfake .TemplateVersion (t , db ).Seed (database.TemplateVersion {
1157
+ OrganizationID : org .Org .ID ,
1158
+ CreatedBy : user .ID ,
1159
+ }).Preset (database.TemplateVersionPreset {
1160
+ ID : presetID ,
1161
+ DesiredInstances : sql.NullInt32 {
1162
+ Int32 : 1 ,
1163
+ Valid : true ,
1164
+ },
1165
+ }).Do ()
1166
+ workspaceBuild := dbfake .WorkspaceBuild (t , db , database.WorkspaceTable {
1167
+ OwnerID : database .PrebuildsSystemUserID ,
1168
+ TemplateID : tv .Template .ID ,
1169
+ OrganizationID : tv .Template .OrganizationID ,
1170
+ }).Seed (database.WorkspaceBuild {
1171
+ TemplateVersionID : tv .TemplateVersion .ID ,
1172
+ TemplateVersionPresetID : uuid.NullUUID {
1173
+ UUID : presetID ,
1174
+ Valid : true ,
1175
+ },
1176
+ }).WithAgent (func (agent []* proto.Agent ) []* proto.Agent {
1177
+ return agent
1178
+ }).Do ()
1179
+
1180
+ // Mark the prebuilt workspace's agent as ready so the prebuild can be claimed
1181
+ // nolint:gocritic
1182
+ agentCtx := dbauthz .AsSystemRestricted (testutil .Context (t , testutil .WaitLong ))
1183
+ agent , err := db .GetWorkspaceAgentAndLatestBuildByAuthToken (agentCtx , uuid .MustParse (workspaceBuild .AgentToken ))
1184
+ require .NoError (t , err )
1185
+ err = db .UpdateWorkspaceAgentLifecycleStateByID (agentCtx , database.UpdateWorkspaceAgentLifecycleStateByIDParams {
1186
+ ID : agent .WorkspaceAgent .ID ,
1187
+ LifecycleState : database .WorkspaceAgentLifecycleStateReady ,
1188
+ })
1189
+ require .NoError (t , err )
1190
+
1191
+ // Given: a prebuilt workspace
1192
+ prebuild , err := db .GetWorkspaceByID (ctx , workspaceBuild .Workspace .ID )
1193
+ require .NoError (t , err )
1194
+ tc .assertWorkspace (t , ctx , db , clock .Now (), true , prebuild )
1195
+
1196
+ // When: the template schedule is updated
1197
+ _ , err = templateScheduleStore .Set (ctx , db , tv .Template , tc .templateSchedule )
1198
+ require .NoError (t , err )
1199
+
1200
+ // Then: lifecycle parameters must remain unset while the prebuild is unclaimed
1201
+ prebuild , err = db .GetWorkspaceByID (ctx , workspaceBuild .Workspace .ID )
1202
+ require .NoError (t , err )
1203
+ tc .assertWorkspace (t , ctx , db , clock .Now (), true , prebuild )
1204
+
1205
+ // Given: the prebuilt workspace is claimed by a user
1206
+ claimedWorkspace := dbgen .ClaimPrebuild (
1207
+ t , db ,
1208
+ clock .Now (),
1209
+ user .ID ,
1210
+ "claimedWorkspace-autostop" ,
1211
+ presetID ,
1212
+ sql.NullString {},
1213
+ sql.NullTime {},
1214
+ sql.NullInt64 {})
1215
+ require .Equal (t , prebuild .ID , claimedWorkspace .ID )
1216
+
1217
+ // Given: the workspace level configurations are properly set in order to ensure the
1218
+ // lifecycle parameters are updated
1219
+ tc .workspaceUpdate (t , ctx , db , clock .Now (), claimedWorkspace )
1220
+
1221
+ // When: the template schedule is updated
1222
+ _ , err = templateScheduleStore .Set (ctx , db , tv .Template , tc .templateSchedule )
1223
+ require .NoError (t , err )
1224
+
1225
+ // Then: the workspace should have its lifecycle parameters updated
1226
+ workspace , err := db .GetWorkspaceByID (ctx , claimedWorkspace .ID )
1227
+ require .NoError (t , err )
1228
+ tc .assertWorkspace (t , ctx , db , clock .Now (), false , workspace )
1229
+ })
1230
+ }
1231
+ }
1232
+
982
1233
func must [V any ](v V , err error ) V {
983
1234
if err != nil {
984
1235
panic (err )
0 commit comments