77"regexp"
88"strings"
99"testing"
10+ "time"
1011
1112"github.com/google/uuid"
1213"github.com/stretchr/testify/assert"
@@ -17,6 +18,7 @@ import (
1718"github.com/coder/coder/v2/coderd/coderdtest"
1819"github.com/coder/coder/v2/coderd/database"
1920"github.com/coder/coder/v2/coderd/database/dbauthz"
21+ "github.com/coder/coder/v2/coderd/database/dbfake"
2022"github.com/coder/coder/v2/coderd/database/dbtestutil"
2123"github.com/coder/coder/v2/coderd/externalauth"
2224"github.com/coder/coder/v2/coderd/rbac"
@@ -27,6 +29,7 @@ import (
2729"github.com/coder/coder/v2/provisionersdk"
2830"github.com/coder/coder/v2/provisionersdk/proto"
2931"github.com/coder/coder/v2/testutil"
32+ "github.com/coder/quartz"
3033)
3134
3235func TestTemplateVersion (t * testing.T ) {
@@ -1194,6 +1197,220 @@ func TestPatchActiveTemplateVersion(t *testing.T) {
11941197require .Len (t ,auditor .AuditLogs (),6 )
11951198assert .Equal (t ,database .AuditActionWrite ,auditor .AuditLogs ()[5 ].Action )
11961199})
1200+
1201+ t .Run ("CancelPendingPrebuilds" ,func (t * testing.T ) {
1202+ t .Parallel ()
1203+
1204+ for _ ,tt := range []struct {
1205+ name string
1206+ setupBuild func (
1207+ t * testing.T ,
1208+ db database.Store ,
1209+ client * codersdk.Client ,
1210+ orgID uuid.UUID ,
1211+ templateID uuid.UUID ,
1212+ ) dbfake.WorkspaceResponse
1213+ previouslyCanceled bool
1214+ previouslyCompleted bool
1215+ shouldCancel bool
1216+ }{
1217+ // Should cancel pending prebuild-related jobs from the previous template version
1218+ {
1219+ name :"CancelsPendingPrebuildJobFromPreviousVersion" ,
1220+ // Given: a pending prebuild job
1221+ setupBuild :func (t * testing.T ,db database.Store ,client * codersdk.Client ,orgID uuid.UUID ,templateID uuid.UUID ) dbfake.WorkspaceResponse {
1222+ return dbfake .WorkspaceBuild (t ,db , database.WorkspaceTable {
1223+ OwnerID :database .PrebuildsSystemUserID ,
1224+ OrganizationID :orgID ,
1225+ TemplateID :templateID ,
1226+ }).Pending ().Seed (database.WorkspaceBuild {
1227+ InitiatorID :database .PrebuildsSystemUserID ,
1228+ }).Do ()
1229+ },
1230+ previouslyCanceled :false ,
1231+ previouslyCompleted :false ,
1232+ shouldCancel :true ,
1233+ },
1234+ // Should not cancel pending prebuild-related jobs of a different template
1235+ {
1236+ name :"DoesNotCancelPrebuildJobDifferentTemplate" ,
1237+ // Given: a pending prebuild job belonging to a different template
1238+ setupBuild :func (t * testing.T ,db database.Store ,client * codersdk.Client ,orgID uuid.UUID ,templateID uuid.UUID ) dbfake.WorkspaceResponse {
1239+ return dbfake .WorkspaceBuild (t ,db , database.WorkspaceTable {
1240+ OwnerID :database .PrebuildsSystemUserID ,
1241+ OrganizationID :orgID ,
1242+ TemplateID :uuid .Nil ,
1243+ }).Pending ().Seed (database.WorkspaceBuild {
1244+ InitiatorID :database .PrebuildsSystemUserID ,
1245+ }).Do ()
1246+ },
1247+ previouslyCanceled :false ,
1248+ previouslyCompleted :false ,
1249+ shouldCancel :false ,
1250+ },
1251+ // Should not cancel pending user workspace build jobs
1252+ {
1253+ name :"DoesNotCancelUserWorkspaceJob" ,
1254+ // Given: a pending user workspace build job
1255+ setupBuild :func (t * testing.T ,db database.Store ,client * codersdk.Client ,orgID uuid.UUID ,templateID uuid.UUID ) dbfake.WorkspaceResponse {
1256+ _ ,member := coderdtest .CreateAnotherUser (t ,client ,orgID ,rbac .RoleMember ())
1257+ return dbfake .WorkspaceBuild (t ,db , database.WorkspaceTable {
1258+ OwnerID :member .ID ,
1259+ OrganizationID :orgID ,
1260+ TemplateID :uuid .Nil ,
1261+ }).Pending ().Seed (database.WorkspaceBuild {
1262+ InitiatorID :member .ID ,
1263+ }).Do ()
1264+ },
1265+ previouslyCanceled :false ,
1266+ previouslyCompleted :false ,
1267+ shouldCancel :false ,
1268+ },
1269+ // Should not cancel pending prebuild-related jobs with a delete transition
1270+ {
1271+ name :"DoesNotCancelPrebuildJobDeleteTransition" ,
1272+ // Given: a pending prebuild job with a delete transition
1273+ setupBuild :func (t * testing.T ,db database.Store ,client * codersdk.Client ,orgID uuid.UUID ,templateID uuid.UUID ) dbfake.WorkspaceResponse {
1274+ return dbfake .WorkspaceBuild (t ,db , database.WorkspaceTable {
1275+ OwnerID :database .PrebuildsSystemUserID ,
1276+ OrganizationID :orgID ,
1277+ TemplateID :templateID ,
1278+ }).Pending ().Seed (database.WorkspaceBuild {
1279+ InitiatorID :database .PrebuildsSystemUserID ,
1280+ Transition :database .WorkspaceTransitionDelete ,
1281+ }).Do ()
1282+ },
1283+ previouslyCanceled :false ,
1284+ previouslyCompleted :false ,
1285+ shouldCancel :false ,
1286+ },
1287+ // Should not cancel prebuild-related jobs already being processed by a provisioner
1288+ {
1289+ name :"DoesNotCancelRunningPrebuildJob" ,
1290+ // Given: a running prebuild job
1291+ setupBuild :func (t * testing.T ,db database.Store ,client * codersdk.Client ,orgID uuid.UUID ,templateID uuid.UUID ) dbfake.WorkspaceResponse {
1292+ return dbfake .WorkspaceBuild (t ,db , database.WorkspaceTable {
1293+ OwnerID :database .PrebuildsSystemUserID ,
1294+ OrganizationID :orgID ,
1295+ TemplateID :templateID ,
1296+ }).Starting ().Seed (database.WorkspaceBuild {
1297+ InitiatorID :database .PrebuildsSystemUserID ,
1298+ }).Do ()
1299+ },
1300+ previouslyCanceled :false ,
1301+ previouslyCompleted :false ,
1302+ shouldCancel :false ,
1303+ },
1304+ // Should not cancel canceled prebuild-related jobs
1305+ {
1306+ name :"DoesNotCancelCanceledPrebuildJob" ,
1307+ // Given: a canceled prebuild job
1308+ setupBuild :func (t * testing.T ,db database.Store ,client * codersdk.Client ,orgID uuid.UUID ,templateID uuid.UUID ) dbfake.WorkspaceResponse {
1309+ return dbfake .WorkspaceBuild (t ,db , database.WorkspaceTable {
1310+ OwnerID :database .PrebuildsSystemUserID ,
1311+ OrganizationID :orgID ,
1312+ TemplateID :templateID ,
1313+ }).Canceled ().Seed (database.WorkspaceBuild {
1314+ InitiatorID :database .PrebuildsSystemUserID ,
1315+ }).Do ()
1316+ },
1317+ shouldCancel :false ,
1318+ previouslyCanceled :true ,
1319+ previouslyCompleted :true ,
1320+ },
1321+ // Should not cancel completed prebuild-related jobs
1322+ {
1323+ name :"DoesNotCancelCompletedPrebuildJob" ,
1324+ // Given: a completed prebuild job
1325+ setupBuild :func (t * testing.T ,db database.Store ,client * codersdk.Client ,orgID uuid.UUID ,templateID uuid.UUID ) dbfake.WorkspaceResponse {
1326+ return dbfake .WorkspaceBuild (t ,db , database.WorkspaceTable {
1327+ OwnerID :database .PrebuildsSystemUserID ,
1328+ OrganizationID :orgID ,
1329+ TemplateID :templateID ,
1330+ }).Seed (database.WorkspaceBuild {
1331+ InitiatorID :database .PrebuildsSystemUserID ,
1332+ }).Do ()
1333+ },
1334+ shouldCancel :false ,
1335+ previouslyCanceled :false ,
1336+ previouslyCompleted :true ,
1337+ },
1338+ } {
1339+ t .Run (tt .name ,func (t * testing.T ) {
1340+ t .Parallel ()
1341+
1342+ // Set the clock to Monday, January 1st, 2024 at 8:00 AM UTC to keep the test deterministic
1343+ clock := quartz .NewMock (t )
1344+ clock .Set (time .Date (2024 ,1 ,1 ,8 ,0 ,0 ,0 ,time .UTC ))
1345+
1346+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitLong )
1347+ defer cancel ()
1348+
1349+ // Setup
1350+ db ,ps := dbtestutil .NewDB (t )
1351+ client ,_ ,_ := coderdtest .NewWithAPI (t ,& coderdtest.Options {
1352+ // Explicitly not including provisioner daemons, as we don't want the jobs to be processed
1353+ // Jobs operations will be simulated via the database model
1354+ IncludeProvisionerDaemon :false ,
1355+ Database :db ,
1356+ Pubsub :ps ,
1357+ Clock :clock ,
1358+ })
1359+ owner := coderdtest .CreateFirstUser (t ,client )
1360+
1361+ // Given: a template with an initial version
1362+ templateVersion := dbfake .TemplateVersion (t ,db ).Seed (database.TemplateVersion {
1363+ OrganizationID :owner .OrganizationID ,
1364+ CreatedBy :owner .UserID ,
1365+ }).Do ()
1366+ templateID := templateVersion .Template .ID
1367+
1368+ // Given: a workspace, workspace build and respective provisioner job
1369+ workspace := tt .setupBuild (t ,db ,client ,owner .OrganizationID ,templateID )
1370+
1371+ // Given: a new template version
1372+ newTemplateVersion := dbfake .TemplateVersion (t ,db ).Seed (database.TemplateVersion {
1373+ OrganizationID :owner .OrganizationID ,
1374+ CreatedBy :owner .UserID ,
1375+ TemplateID : uuid.NullUUID {
1376+ UUID :templateID ,
1377+ Valid :true ,
1378+ },
1379+ }).SkipCreateTemplate ().Do ()
1380+
1381+ // When: a new active version is promoted
1382+ err := client .UpdateActiveTemplateVersion (ctx ,templateID , codersdk.UpdateActiveTemplateVersion {
1383+ ID :newTemplateVersion .TemplateVersion .ID ,
1384+ })
1385+ require .NoError (t ,err )
1386+
1387+ // Then: the template's active version should be updated
1388+ updatedTemplate ,err := db .GetTemplateByID (ctx ,templateID )
1389+ require .NoError (t ,err )
1390+ require .Equal (t ,newTemplateVersion .TemplateVersion .ID ,updatedTemplate .ActiveVersionID )
1391+
1392+ if tt .shouldCancel {
1393+ // Then: the prebuild related jobs from the previous version should be canceled
1394+ cancelledJob ,err := db .GetProvisionerJobByID (ctx ,workspace .Build .JobID )
1395+ require .NoError (t ,err )
1396+ require .Equal (t ,clock .Now ().UTC (),cancelledJob .CanceledAt .Time .UTC ())
1397+ require .Equal (t ,clock .Now ().UTC (),cancelledJob .CompletedAt .Time .UTC ())
1398+ require .Equal (t ,database .ProvisionerJobStatusCanceled ,cancelledJob .JobStatus )
1399+ }else {
1400+ // Then: the provisioner job should not be canceled
1401+ job ,err := db .GetProvisionerJobByID (ctx ,workspace .Build .JobID )
1402+ require .NoError (t ,err )
1403+ if ! tt .previouslyCanceled {
1404+ require .Zero (t ,job .CanceledAt .Time .UTC ())
1405+ require .NotEqual (t ,database .ProvisionerJobStatusCanceled ,job .JobStatus )
1406+ }
1407+ if ! tt .previouslyCompleted {
1408+ require .Zero (t ,job .CompletedAt .Time .UTC ())
1409+ }
1410+ }
1411+ })
1412+ }
1413+ })
11971414}
11981415
11991416func TestTemplateVersionDryRun (t * testing.T ) {