@@ -27,6 +27,7 @@ import (
27
27
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
28
28
"github.com/coder/coder/v2/coderd/httpapi"
29
29
"github.com/coder/coder/v2/coderd/httpmw"
30
+ "github.com/coder/coder/v2/coderd/notifications"
30
31
"github.com/coder/coder/v2/coderd/provisionerdserver"
31
32
"github.com/coder/coder/v2/coderd/rbac"
32
33
"github.com/coder/coder/v2/coderd/rbac/policy"
@@ -333,37 +334,59 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
333
334
LogLevel (string (createBuild .LogLevel )).
334
335
DeploymentValues (api .Options .DeploymentValues )
335
336
336
- if createBuild .TemplateVersionID != uuid .Nil {
337
- builder = builder .VersionID (createBuild .TemplateVersionID )
338
- }
337
+ var (
338
+ previousWorkspaceBuild database.WorkspaceBuild
339
+ workspaceBuild * database.WorkspaceBuild
340
+ provisionerJob * database.ProvisionerJob
341
+ provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
342
+ )
339
343
340
- if createBuild .Orphan {
341
- if createBuild .Transition != codersdk .WorkspaceTransitionDelete {
342
- httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
343
- Message : "Orphan is only permitted when deleting a workspace." ,
344
+ err := api .Database .InTx (func (tx database.Store ) error {
345
+ var err error
346
+
347
+ previousWorkspaceBuild , err = tx .GetLatestWorkspaceBuildByWorkspaceID (ctx , workspace .ID )
348
+ if err != nil && ! xerrors .Is (err , sql .ErrNoRows ) {
349
+ api .Logger .Error (ctx , "failed fetching previous workspace build" , slog .F ("workspace_id" , workspace .ID ), slog .Error (err ))
350
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
351
+ Message : "Internal error fetching previous workspace build" ,
352
+ Detail : err .Error (),
344
353
})
345
- return
354
+ return nil
355
+ }
356
+
357
+ if createBuild .TemplateVersionID != uuid .Nil {
358
+ builder = builder .VersionID (createBuild .TemplateVersionID )
359
+ }
360
+
361
+ if createBuild .Orphan {
362
+ if createBuild .Transition != codersdk .WorkspaceTransitionDelete {
363
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
364
+ Message : "Orphan is only permitted when deleting a workspace." ,
365
+ })
366
+ return nil
367
+ }
368
+ if len (createBuild .ProvisionerState ) > 0 {
369
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
370
+ Message : "ProvisionerState cannot be set alongside Orphan since state intent is unclear." ,
371
+ })
372
+ return nil
373
+ }
374
+ builder = builder .Orphan ()
346
375
}
347
376
if len (createBuild .ProvisionerState ) > 0 {
348
- httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
349
- Message : "ProvisionerState cannot be set alongside Orphan since state intent is unclear." ,
350
- })
351
- return
377
+ builder = builder .State (createBuild .ProvisionerState )
352
378
}
353
- builder = builder .Orphan ()
354
- }
355
- if len (createBuild .ProvisionerState ) > 0 {
356
- builder = builder .State (createBuild .ProvisionerState )
357
- }
358
379
359
- workspaceBuild , provisionerJob , provisionerDaemons , err := builder .Build (
360
- ctx ,
361
- api .Database ,
362
- func (action policy.Action , object rbac.Objecter ) bool {
363
- return api .Authorize (r , action , object )
364
- },
365
- audit .WorkspaceBuildBaggageFromRequest (r ),
366
- )
380
+ workspaceBuild , provisionerJob , provisionerDaemons , err = builder .Build (
381
+ ctx ,
382
+ tx ,
383
+ func (action policy.Action , object rbac.Objecter ) bool {
384
+ return api .Authorize (r , action , object )
385
+ },
386
+ audit .WorkspaceBuildBaggageFromRequest (r ),
387
+ )
388
+ return err
389
+ }, nil )
367
390
var buildErr wsbuilder.BuildError
368
391
if xerrors .As (err , & buildErr ) {
369
392
var authErr dbauthz.NotAuthorizedError
@@ -420,6 +443,12 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
420
443
return
421
444
}
422
445
446
+ // If this workspace build has a different template version ID to the previous build
447
+ // we can assume it has just been updated.
448
+ if createBuild .TemplateVersionID != uuid .Nil && createBuild .TemplateVersionID != previousWorkspaceBuild .TemplateVersionID {
449
+ api .notifyWorkspaceUpdated (ctx , apiKey .UserID , workspace , createBuild .RichParameterValues )
450
+ }
451
+
423
452
api .publishWorkspaceUpdate (ctx , workspace .OwnerID , wspubsub.WorkspaceEvent {
424
453
Kind : wspubsub .WorkspaceEventKindStateChange ,
425
454
WorkspaceID : workspace .ID ,
@@ -428,6 +457,73 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
428
457
httpapi .Write (ctx , rw , http .StatusCreated , apiBuild )
429
458
}
430
459
460
+ func (api * API ) notifyWorkspaceUpdated (
461
+ ctx context.Context ,
462
+ initiatorID uuid.UUID ,
463
+ workspace database.Workspace ,
464
+ parameters []codersdk.WorkspaceBuildParameter ,
465
+ ) {
466
+ log := api .Logger .With (slog .F ("workspace_id" , workspace .ID ))
467
+
468
+ template , err := api .Database .GetTemplateByID (ctx , workspace .TemplateID )
469
+ if err != nil {
470
+ log .Warn (ctx , "failed to fetch template for workspace creation notification" , slog .F ("template_id" , workspace .TemplateID ), slog .Error (err ))
471
+ return
472
+ }
473
+
474
+ version , err := api .Database .GetTemplateVersionByID (ctx , template .ActiveVersionID )
475
+ if err != nil {
476
+ log .Warn (ctx , "failed to fetch template version for workspace creation notification" , slog .F ("template_id" , workspace .TemplateID ), slog .Error (err ))
477
+ return
478
+ }
479
+
480
+ initiator , err := api .Database .GetUserByID (ctx , initiatorID )
481
+ if err != nil {
482
+ log .Warn (ctx , "failed to fetch user for workspace update notification" , slog .F ("initiator_id" , initiatorID ), slog .Error (err ))
483
+ return
484
+ }
485
+
486
+ owner , err := api .Database .GetUserByID (ctx , workspace .OwnerID )
487
+ if err != nil {
488
+ log .Warn (ctx , "failed to fetch user for workspace update notification" , slog .F ("owner_id" , workspace .OwnerID ), slog .Error (err ))
489
+ return
490
+ }
491
+
492
+ buildParameters := make ([]map [string ]any , len (parameters ))
493
+ for idx , parameter := range parameters {
494
+ buildParameters [idx ] = map [string ]any {
495
+ "name" : parameter .Name ,
496
+ "value" : parameter .Value ,
497
+ }
498
+ }
499
+
500
+ if _ , err := api .NotificationsEnqueuer .EnqueueWithData (
501
+ // nolint:gocritic // Need notifier actor to enqueue notifications
502
+ dbauthz .AsNotifier (ctx ),
503
+ workspace .OwnerID ,
504
+ notifications .TemplateWorkspaceManuallyUpdated ,
505
+ map [string ]string {
506
+ "organization" : template .OrganizationName ,
507
+ "initiator" : initiator .Name ,
508
+ "workspace" : workspace .Name ,
509
+ "template" : template .Name ,
510
+ "version" : version .Name ,
511
+ },
512
+ map [string ]any {
513
+ "workspace" : map [string ]any {"id" : workspace .ID , "name" : workspace .Name },
514
+ "template" : map [string ]any {"id" : template .ID , "name" : template .Name },
515
+ "template_version" : map [string ]any {"id" : version .ID , "name" : version .Name },
516
+ "owner" : map [string ]any {"id" : owner .ID , "name" : owner .Name },
517
+ "parameters" : buildParameters ,
518
+ },
519
+ "api-workspaces-updated" ,
520
+ // Associate this notification with all the related entities
521
+ workspace .ID , workspace .OwnerID , workspace .TemplateID , workspace .OrganizationID ,
522
+ ); err != nil {
523
+ log .Warn (ctx , "failed to notify of workspace update" , slog .Error (err ))
524
+ }
525
+ }
526
+
431
527
// @Summary Cancel workspace build
432
528
// @ID cancel-workspace-build
433
529
// @Security CoderSessionToken
0 commit comments