From 541b852686f7d5c5a84be891412fb92d26ab3413 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 30 Dec 2024 14:36:44 +0000 Subject: [PATCH 01/10] add notification on workspace update --- ...280_workspace_update_notification.down.sql | 1 + ...00280_workspace_update_notification.up.sql | 20 +++++ coderd/notifications/events.go | 1 + coderd/notifications/notifications_test.go | 15 ++++ ...mplateWorkspaceManuallyUpdated.html.golden | 89 +++++++++++++++++++ ...mplateWorkspaceManuallyUpdated.json.golden | 34 +++++++ coderd/workspacebuilds.go | 89 +++++++++++++++++++ 7 files changed, 249 insertions(+) create mode 100644 coderd/database/migrations/000280_workspace_update_notification.down.sql create mode 100644 coderd/database/migrations/000280_workspace_update_notification.up.sql create mode 100644 coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceManuallyUpdated.html.golden create mode 100644 coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceManuallyUpdated.json.golden diff --git a/coderd/database/migrations/000280_workspace_update_notification.down.sql b/coderd/database/migrations/000280_workspace_update_notification.down.sql new file mode 100644 index 0000000000000..5097c0248fe9b --- /dev/null +++ b/coderd/database/migrations/000280_workspace_update_notification.down.sql @@ -0,0 +1 @@ +DELETE FROM notification_templates WHERE id = 'd089fe7b-d5c5-4c0c-aaf5-689859f7d392'; diff --git a/coderd/database/migrations/000280_workspace_update_notification.up.sql b/coderd/database/migrations/000280_workspace_update_notification.up.sql new file mode 100644 index 0000000000000..d1d53c7dada52 --- /dev/null +++ b/coderd/database/migrations/000280_workspace_update_notification.up.sql @@ -0,0 +1,20 @@ +INSERT INTO notification_templates + (id, name, title_template, body_template, "group", actions) +VALUES ( + 'd089fe7b-d5c5-4c0c-aaf5-689859f7d392', + 'Workspace Manually Updated', + E'Workspace ''{{.Labels.workspace}}'' has been manually updated', + E'Hello {{.UserName}},\n\n'|| + E'Your workspace **{{.Labels.workspace}}** has been manually updated to template version **{{.Labels.version}}**.', + 'Workspace Events', + '[ + { + "label": "See workspace", + "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}" + }, + { + "label": "See template version", + "url": "{{base_url}}/templates/{{.Labels.organization}}/{{.Labels.template}}/versions/{{.Labels.version}}" + } + ]'::jsonb +); diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 12aecbaac74ae..754d2e5c7f745 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -8,6 +8,7 @@ import "github.com/google/uuid" // Workspace-related events. var ( TemplateWorkspaceCreated = uuid.MustParse("281fdf73-c6d6-4cbb-8ff5-888baf8a2fff") + TemplateWorkspaceManuallyUpdated = uuid.MustParse("d089fe7b-d5c5-4c0c-aaf5-689859f7d392") TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed") TemplateWorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9") TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0") diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 90cf1a46be28a..616ed364f3809 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -1048,6 +1048,21 @@ func TestNotificationTemplates_Golden(t *testing.T) { }, }, }, + { + name: "TemplateWorkspaceManuallyUpdated", + id: notifications.TemplateWorkspaceManuallyUpdated, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{ + "organization": "bobby-organization", + "workspace": "bobby-workspace", + "template": "bobby-template", + "version": "alpha", + }, + }, + }, } // We must have a test case for every notification_template. This is enforced below: diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceManuallyUpdated.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceManuallyUpdated.html.golden new file mode 100644 index 0000000000000..b8630e64c9b50 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceManuallyUpdated.html.golden @@ -0,0 +1,89 @@ +From: system@coder.com +To: bobby@coder.com +Subject: Workspace 'bobby-workspace' has been manually updated +Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 +Date: Fri, 11 Oct 2024 09:03:06 +0000 +Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hello Bobby, + +Your workspace bobby-workspace has been manually updated to template versio= +n alpha. + + +See workspace: http://test.com/@bobby/bobby-workspace + +See template version: http://test.com/templates/bobby-organization/bobby-te= +mplate/versions/alpha + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + Workspace 'bobby-workspace' has been manually updated + + +
+
+ 3D"Cod= +
+

+ Workspace 'bobby-workspace' has been manually updated +

+
+

Hello Bobby,

+ +

Your workspace bobby-workspace has been manually update= +d to template version alpha.

+
+
+ =20 + + See workspace + + =20 + + See template version + + =20 +
+
+

© 2024 Coder. All rights reserved - h= +ttp://test.com

+

Click here to manage your notification = +settings

+

Stop receiving emails like this

+
+
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceManuallyUpdated.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceManuallyUpdated.json.golden new file mode 100644 index 0000000000000..da9defe42bb75 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceManuallyUpdated.json.golden @@ -0,0 +1,34 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.1", + "notification_name": "Workspace Manually Updated", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "See workspace", + "url": "http://test.com/@bobby/bobby-workspace" + }, + { + "label": "See template version", + "url": "http://test.com/templates/bobby-organization/bobby-template/versions/alpha" + } + ], + "labels": { + "organization": "bobby-organization", + "template": "bobby-template", + "version": "alpha", + "workspace": "bobby-workspace" + }, + "data": null + }, + "title": "Workspace 'bobby-workspace' has been manually updated", + "title_markdown": "Workspace 'bobby-workspace' has been manually updated", + "body": "Hello Bobby,\n\nYour workspace bobby-workspace has been manually updated to template version alpha.", + "body_markdown": "Hello Bobby,\n\nYour workspace **bobby-workspace** has been manually updated to template version **alpha**." +} \ No newline at end of file diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 7eb598a7d4564..4427bf0a117c7 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -27,6 +27,7 @@ import ( "github.com/coder/coder/v2/coderd/database/provisionerjobs" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/provisionerdserver" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -327,6 +328,15 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } + previousWorkspaceBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching latest workspace build", + Detail: err.Error(), + }) + return + } + builder := wsbuilder.New(workspace, database.WorkspaceTransition(createBuild.Transition)). Initiator(apiKey.UserID). RichParameterValues(createBuild.RichParameterValues). @@ -420,6 +430,12 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } + // If this workspace build has a different template version ID to the previous build + // we can assume it has just been updated. + if createBuild.TemplateVersionID != uuid.Nil && createBuild.TemplateVersionID != previousWorkspaceBuild.TemplateVersionID { + api.notifyWorkspaceUpdated(ctx, workspace.ID) + } + api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{ Kind: wspubsub.WorkspaceEventKindStateChange, WorkspaceID: workspace.ID, @@ -428,6 +444,79 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusCreated, apiBuild) } +func (api *API) notifyWorkspaceUpdated(ctx context.Context, workspaceID uuid.UUID) { + log := api.Logger.With(slog.F("workspace_id", workspaceID)) + + workspace, err := api.Database.GetWorkspaceByID(ctx, workspaceID) + if err != nil { + log.Warn(ctx, "failed to fetch workspace for workspace update notification", slog.Error(err)) + return + } + + template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID) + if err != nil { + log.Warn(ctx, "failed to fetch template for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err)) + return + } + + owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID) + if err != nil { + log.Warn(ctx, "failed to fetch user for workspace update notification", slog.F("owner_id", workspace.OwnerID), slog.Error(err)) + return + } + + version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID) + if err != nil { + log.Warn(ctx, "failed to fetch template version for workspace update notification", slog.F("template_version_id", template.ActiveVersionID), slog.Error(err)) + return + } + + build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + log.Warn(ctx, "failed to fetch latest workspace build for workspace update notification", slog.Error(err)) + return + } + + parameters, err := api.Database.GetWorkspaceBuildParameters(ctx, build.ID) + if err != nil { + log.Warn(ctx, "failed to fetch workspace build parameters for workspace update notification", slog.Error(err)) + return + } + + buildParameters := make([]map[string]any, len(parameters)) + for idx, parameter := range parameters { + buildParameters[idx] = map[string]any{ + "name": parameter.Name, + "value": parameter.Value, + } + } + + if _, err := api.NotificationsEnqueuer.EnqueueWithData( + // nolint:gocritic // Need notifier actor to enqueue notifications + dbauthz.AsNotifier(ctx), + workspace.OwnerID, + notifications.TemplateWorkspaceManuallyUpdated, + map[string]string{ + "organization": template.OrganizationName, + "workspace": workspace.Name, + "template": template.Name, + "version": version.Name, + }, + map[string]any{ + "workspace": map[string]any{"id": workspace.ID, "name": workspace.Name}, + "template": map[string]any{"id": template.ID, "name": template.Name}, + "template_version": map[string]any{"id": version.ID, "name": version.Name}, + "owner": map[string]any{"id": owner.ID, "name": owner.Name}, + "parameters": buildParameters, + }, + "api-workspaces-updated", + // Associate this notification with all the related entities + workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID, + ); err != nil { + log.Warn(ctx, "failed to notify of workspace update", slog.Error(err)) + } +} + // @Summary Cancel workspace build // @ID cancel-workspace-build // @Security CoderSessionToken From 835e49cdfa8db64f9804a27cf4ff5d6175e2925c Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 30 Dec 2024 18:42:28 +0000 Subject: [PATCH 02/10] tests: test expectations --- coderd/workspacebuilds_test.go | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index feb748ad29250..43674b308583c 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -27,6 +27,8 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/externalauth" + "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/notifications/notificationstest" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisioner/echo" @@ -560,6 +562,104 @@ func TestWorkspaceBuildResources(t *testing.T) { }) } +func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T) { + t.Parallel() + + t.Run("OnlyOneNotification", func(t *testing.T) { + t.Parallel() + + notify := ¬ificationstest.FakeEnqueuer{} + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: notify}) + first := coderdtest.CreateFirstUser(t, client) + userClient, user := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + + // Create a template with an initial version + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + + // Create a workspace using this template + workspace := coderdtest.CreateWorkspace(t, userClient, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID) + coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) + + // Create a new version of the template + newVersion := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) { + ctvr.TemplateID = template.ID + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, newVersion.ID) + + // Create a workspace build using this new template version + build := coderdtest.CreateWorkspaceBuild(t, userClient, workspace, database.WorkspaceTransitionStart, func(cwbr *codersdk.CreateWorkspaceBuildRequest) { + cwbr.TemplateVersionID = newVersion.ID + }) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID) + coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) + + // Create the workspace build _again_. We are doing this to ensure we only create 1 notification. + build = coderdtest.CreateWorkspaceBuild(t, userClient, workspace, database.WorkspaceTransitionStart, func(cwbr *codersdk.CreateWorkspaceBuildRequest) { + cwbr.TemplateVersionID = newVersion.ID + }) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID) + coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) + + // Ensure we receive only 1 workspace manually updated notification + sent := notify.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceManuallyUpdated)) + require.Len(t, sent, 1) + require.Equal(t, user.ID, sent[0].UserID) + require.Contains(t, sent[0].Targets, template.ID) + require.Contains(t, sent[0].Targets, workspace.ID) + require.Contains(t, sent[0].Targets, workspace.OrganizationID) + require.Contains(t, sent[0].Targets, workspace.OwnerID) + }) + + t.Run("ToCorrectUser", func(t *testing.T) { + t.Parallel() + + notify := ¬ificationstest.FakeEnqueuer{} + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: notify}) + first := coderdtest.CreateFirstUser(t, client) + userClient, user := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + + // Create a template with an initial version + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + + // Create a workspace using this template + workspace := coderdtest.CreateWorkspace(t, userClient, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID) + coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) + + // Create a new version of the template + newVersion := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) { + ctvr.TemplateID = template.ID + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, newVersion.ID) + + // Create a workspace build using this new template version from a different user + ctx := testutil.Context(t, testutil.WaitShort) + build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionID: newVersion.ID, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID) + coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) + + // Ensure we receive only 1 workspace manually updated notification and to the right user + sent := notify.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceManuallyUpdated)) + require.Len(t, sent, 1) + require.Equal(t, user.ID, sent[0].UserID) + require.Contains(t, sent[0].Targets, template.ID) + require.Contains(t, sent[0].Targets, workspace.ID) + require.Contains(t, sent[0].Targets, workspace.OrganizationID) + require.Contains(t, sent[0].Targets, workspace.OwnerID) + }) +} + func assertWorkspaceResource(t *testing.T, actual codersdk.WorkspaceResource, name, aType string, numAgents int) { assert.Equal(t, name, actual.Name) assert.Equal(t, aType, actual.Type) From b43a7e6a25efb085c4897d74798799113067b5ab Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 31 Dec 2024 09:55:41 +0000 Subject: [PATCH 03/10] chore: change error message, add error log --- coderd/workspacebuilds.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 4427bf0a117c7..ab055534978d5 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -330,8 +330,9 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { previousWorkspaceBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + api.Logger.Error(ctx, "failed fetching previous workspace build", slog.F("workspace_id", workspace.ID), slog.Error(err)) httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching latest workspace build", + Message: "Internal error fetching previous workspace build", Detail: err.Error(), }) return From c683985c8837fe85bbaf5c79cb6c82ef30fc55fa Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 31 Dec 2024 14:40:23 +0000 Subject: [PATCH 04/10] chore: apply feedback --- ...00280_workspace_update_notification.up.sql | 16 +++++++-- coderd/notifications/notifications_test.go | 1 + .../smtp/TemplateWorkspaceCreated.html.golden | 4 +-- ...mplateWorkspaceManuallyUpdated.html.golden | 19 +++++----- .../TemplateWorkspaceCreated.json.golden | 2 +- ...mplateWorkspaceManuallyUpdated.json.golden | 9 ++--- coderd/workspacebuilds.go | 36 ++++++++----------- 7 files changed, 47 insertions(+), 40 deletions(-) diff --git a/coderd/database/migrations/000280_workspace_update_notification.up.sql b/coderd/database/migrations/000280_workspace_update_notification.up.sql index d1d53c7dada52..23d2331a323f6 100644 --- a/coderd/database/migrations/000280_workspace_update_notification.up.sql +++ b/coderd/database/migrations/000280_workspace_update_notification.up.sql @@ -5,16 +5,26 @@ VALUES ( 'Workspace Manually Updated', E'Workspace ''{{.Labels.workspace}}'' has been manually updated', E'Hello {{.UserName}},\n\n'|| - E'Your workspace **{{.Labels.workspace}}** has been manually updated to template version **{{.Labels.version}}**.', + E'A new workspace build has been manually created for your workspace **{{.Labels.workspace}}** by **{{.Labels.initiator}}** to update it to version **{{.Labels.version}}** of template **{{.Labels.template}}**.', 'Workspace Events', '[ { - "label": "See workspace", + "label": "View workspace", "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}" }, { - "label": "See template version", + "label": "View template version", "url": "{{base_url}}/templates/{{.Labels.organization}}/{{.Labels.template}}/versions/{{.Labels.version}}" } ]'::jsonb ); + +UPDATE notification_templates +SET + actions = '[ + { + "label": "View workspace", + "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}" + } + ]'::jsonb +WHERE id = '281fdf73-c6d6-4cbb-8ff5-888baf8a2fff'; diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 616ed364f3809..1c4be51974b05 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -1057,6 +1057,7 @@ func TestNotificationTemplates_Golden(t *testing.T) { UserUsername: "bobby", Labels: map[string]string{ "organization": "bobby-organization", + "initiator": "bobby", "workspace": "bobby-workspace", "template": "bobby-template", "version": "alpha", diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceCreated.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceCreated.html.golden index 000b2a71ac77b..9d039ea7f77e9 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceCreated.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceCreated.html.golden @@ -16,7 +16,7 @@ The workspace bobby-workspace has been created from the template bobby-temp= late using version alpha. -See workspace: http://test.com/@bobby/bobby-workspace +View workspace: http://test.com/@bobby/bobby-workspace --bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 Content-Transfer-Encoding: quoted-printable @@ -57,7 +57,7 @@ ng>.

- See workspace + View workspace =20 diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceManuallyUpdated.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceManuallyUpdated.html.golden index b8630e64c9b50..57a9a0d51b7b7 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceManuallyUpdated.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceManuallyUpdated.html.golden @@ -12,14 +12,14 @@ Content-Type: text/plain; charset=UTF-8 Hello Bobby, -Your workspace bobby-workspace has been manually updated to template versio= -n alpha. +A new workspace build has been manually created for your workspace bobby-wo= +rkspace by bobby to update it to version alpha of template bobby-template. -See workspace: http://test.com/@bobby/bobby-workspace +View workspace: http://test.com/@bobby/bobby-workspace -See template version: http://test.com/templates/bobby-organization/bobby-te= -mplate/versions/alpha +View template version: http://test.com/templates/bobby-organization/bobby-t= +emplate/versions/alpha --bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 Content-Transfer-Encoding: quoted-printable @@ -51,22 +51,23 @@ argin: 8px 0 32px; line-height: 1.5;">

Hello Bobby,

-

Your workspace bobby-workspace has been manually update= -d to template version alpha.

+

A new workspace build has been manually created for your workspace bobby-workspace by bobby to update it to versi= +on alpha of template bobby-template.

=20 - See workspace + View workspace =20 - See template version + View template version =20
diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceCreated.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceCreated.json.golden index 46354c4ffeef9..924f299b228b2 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceCreated.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceCreated.json.golden @@ -11,7 +11,7 @@ "user_username": "bobby", "actions": [ { - "label": "See workspace", + "label": "View workspace", "url": "http://test.com/@bobby/bobby-workspace" } ], diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceManuallyUpdated.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceManuallyUpdated.json.golden index da9defe42bb75..7fbda32e194f4 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceManuallyUpdated.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceManuallyUpdated.json.golden @@ -11,15 +11,16 @@ "user_username": "bobby", "actions": [ { - "label": "See workspace", + "label": "View workspace", "url": "http://test.com/@bobby/bobby-workspace" }, { - "label": "See template version", + "label": "View template version", "url": "http://test.com/templates/bobby-organization/bobby-template/versions/alpha" } ], "labels": { + "initiator": "bobby", "organization": "bobby-organization", "template": "bobby-template", "version": "alpha", @@ -29,6 +30,6 @@ }, "title": "Workspace 'bobby-workspace' has been manually updated", "title_markdown": "Workspace 'bobby-workspace' has been manually updated", - "body": "Hello Bobby,\n\nYour workspace bobby-workspace has been manually updated to template version alpha.", - "body_markdown": "Hello Bobby,\n\nYour workspace **bobby-workspace** has been manually updated to template version **alpha**." + "body": "Hello Bobby,\n\nA new workspace build has been manually created for your workspace bobby-workspace by bobby to update it to version alpha of template bobby-template.", + "body_markdown": "Hello Bobby,\n\nA new workspace build has been manually created for your workspace **bobby-workspace** by **bobby** to update it to version **alpha** of template **bobby-template**." } \ No newline at end of file diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index ab055534978d5..8339d66722971 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -434,7 +434,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { // If this workspace build has a different template version ID to the previous build // we can assume it has just been updated. if createBuild.TemplateVersionID != uuid.Nil && createBuild.TemplateVersionID != previousWorkspaceBuild.TemplateVersionID { - api.notifyWorkspaceUpdated(ctx, workspace.ID) + api.notifyWorkspaceUpdated(ctx, apiKey.UserID, workspace, createBuild.RichParameterValues) } api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{ @@ -445,14 +445,13 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusCreated, apiBuild) } -func (api *API) notifyWorkspaceUpdated(ctx context.Context, workspaceID uuid.UUID) { - log := api.Logger.With(slog.F("workspace_id", workspaceID)) - - workspace, err := api.Database.GetWorkspaceByID(ctx, workspaceID) - if err != nil { - log.Warn(ctx, "failed to fetch workspace for workspace update notification", slog.Error(err)) - return - } +func (api *API) notifyWorkspaceUpdated( + ctx context.Context, + initiatorID uuid.UUID, + workspace database.Workspace, + parameters []codersdk.WorkspaceBuildParameter, +) { + log := api.Logger.With(slog.F("workspace_id", workspace.ID)) template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID) if err != nil { @@ -460,27 +459,21 @@ func (api *API) notifyWorkspaceUpdated(ctx context.Context, workspaceID uuid.UUI return } - owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID) + version, err := api.Database.GetTemplateVersionByID(ctx, workspace.TemplateID) if err != nil { - log.Warn(ctx, "failed to fetch user for workspace update notification", slog.F("owner_id", workspace.OwnerID), slog.Error(err)) + log.Warn(ctx, "failed to fetch template version for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err)) return } - version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID) + initiator, err := api.Database.GetUserByID(ctx, initiatorID) if err != nil { - log.Warn(ctx, "failed to fetch template version for workspace update notification", slog.F("template_version_id", template.ActiveVersionID), slog.Error(err)) + log.Warn(ctx, "failed to fetch user for workspace update notification", slog.F("initiator_id", initiatorID), slog.Error(err)) return } - build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) - if err != nil { - log.Warn(ctx, "failed to fetch latest workspace build for workspace update notification", slog.Error(err)) - return - } - - parameters, err := api.Database.GetWorkspaceBuildParameters(ctx, build.ID) + owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID) if err != nil { - log.Warn(ctx, "failed to fetch workspace build parameters for workspace update notification", slog.Error(err)) + log.Warn(ctx, "failed to fetch user for workspace update notification", slog.F("owner_id", workspace.OwnerID), slog.Error(err)) return } @@ -499,6 +492,7 @@ func (api *API) notifyWorkspaceUpdated(ctx context.Context, workspaceID uuid.UUI notifications.TemplateWorkspaceManuallyUpdated, map[string]string{ "organization": template.OrganizationName, + "initiator": initiator.Name, "workspace": workspace.Name, "template": template.Name, "version": version.Name, From b3707a163eb2dc2be296d20e1affa5236fc61e1d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 31 Dec 2024 14:59:14 +0000 Subject: [PATCH 05/10] fix: broken logic --- coderd/workspacebuilds.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 8339d66722971..738ee9be1ba29 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -459,7 +459,7 @@ func (api *API) notifyWorkspaceUpdated( return } - version, err := api.Database.GetTemplateVersionByID(ctx, workspace.TemplateID) + version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID) if err != nil { log.Warn(ctx, "failed to fetch template version for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err)) return From 7f10eccbe1c9765595351b4615619c4cb974776b Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 2 Jan 2025 10:29:57 +0000 Subject: [PATCH 06/10] chore: wrap in transaction --- coderd/workspacebuilds.go | 93 ++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 40 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 738ee9be1ba29..808b0ba423059 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -328,53 +328,66 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - previousWorkspaceBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) - if err != nil && !xerrors.Is(err, sql.ErrNoRows) { - api.Logger.Error(ctx, "failed fetching previous workspace build", slog.F("workspace_id", workspace.ID), slog.Error(err)) - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching previous workspace build", - Detail: err.Error(), - }) - return - } - - builder := wsbuilder.New(workspace, database.WorkspaceTransition(createBuild.Transition)). - Initiator(apiKey.UserID). - RichParameterValues(createBuild.RichParameterValues). - LogLevel(string(createBuild.LogLevel)). - DeploymentValues(api.Options.DeploymentValues) + var ( + previousWorkspaceBuild database.WorkspaceBuild + workspaceBuild *database.WorkspaceBuild + provisionerJob *database.ProvisionerJob + provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow + ) - if createBuild.TemplateVersionID != uuid.Nil { - builder = builder.VersionID(createBuild.TemplateVersionID) - } + err := api.Database.InTx(func(database.Store) error { + var err error - if createBuild.Orphan { - if createBuild.Transition != codersdk.WorkspaceTransitionDelete { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Orphan is only permitted when deleting a workspace.", + previousWorkspaceBuild, err = api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + api.Logger.Error(ctx, "failed fetching previous workspace build", slog.F("workspace_id", workspace.ID), slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching previous workspace build", + Detail: err.Error(), }) - return + return nil + } + + builder := wsbuilder.New(workspace, database.WorkspaceTransition(createBuild.Transition)). + Initiator(apiKey.UserID). + RichParameterValues(createBuild.RichParameterValues). + LogLevel(string(createBuild.LogLevel)). + DeploymentValues(api.Options.DeploymentValues) + + if createBuild.TemplateVersionID != uuid.Nil { + builder = builder.VersionID(createBuild.TemplateVersionID) + } + + if createBuild.Orphan { + if createBuild.Transition != codersdk.WorkspaceTransitionDelete { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Orphan is only permitted when deleting a workspace.", + }) + return nil + } + if len(createBuild.ProvisionerState) > 0 { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.", + }) + return nil + } + builder = builder.Orphan() } if len(createBuild.ProvisionerState) > 0 { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.", - }) - return + builder = builder.State(createBuild.ProvisionerState) } - builder = builder.Orphan() - } - if len(createBuild.ProvisionerState) > 0 { - builder = builder.State(createBuild.ProvisionerState) - } - workspaceBuild, provisionerJob, provisionerDaemons, err := builder.Build( - ctx, - api.Database, - func(action policy.Action, object rbac.Objecter) bool { - return api.Authorize(r, action, object) - }, - audit.WorkspaceBuildBaggageFromRequest(r), - ) + workspaceBuild, provisionerJob, provisionerDaemons, err = builder.Build( + ctx, + api.Database, + func(action policy.Action, object rbac.Objecter) bool { + return api.Authorize(r, action, object) + }, + audit.WorkspaceBuildBaggageFromRequest(r), + ) + + return err + }, nil) var buildErr wsbuilder.BuildError if xerrors.As(err, &buildErr) { var authErr dbauthz.NotAuthorizedError From 1a493fff5496be0e84b48fb45cf618a7f5a938e1 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 2 Jan 2025 10:31:25 +0000 Subject: [PATCH 07/10] chore: remove whitespace between call and return err --- coderd/workspacebuilds.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 808b0ba423059..8874aa280873e 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -385,7 +385,6 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { }, audit.WorkspaceBuildBaggageFromRequest(r), ) - return err }, nil) var buildErr wsbuilder.BuildError From 32adc00526a181a4c328164a0b927a39b23e942b Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 2 Jan 2025 11:21:25 +0000 Subject: [PATCH 08/10] chore: actually use the transaction, oops --- coderd/workspacebuilds.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 8874aa280873e..9434adc8e5c80 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -335,10 +335,10 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow ) - err := api.Database.InTx(func(database.Store) error { + err := api.Database.InTx(func(tx database.Store) error { var err error - previousWorkspaceBuild, err = api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + previousWorkspaceBuild, err = tx.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) if err != nil && !xerrors.Is(err, sql.ErrNoRows) { api.Logger.Error(ctx, "failed fetching previous workspace build", slog.F("workspace_id", workspace.ID), slog.Error(err)) httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -379,7 +379,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { workspaceBuild, provisionerJob, provisionerDaemons, err = builder.Build( ctx, - api.Database, + tx, func(action policy.Action, object rbac.Objecter) bool { return api.Authorize(r, action, object) }, From 90380f7c346cf7e5c009ce46f13fb12653330b67 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 2 Jan 2025 11:37:18 +0000 Subject: [PATCH 09/10] chore: changes --- coderd/workspacebuilds.go | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 9434adc8e5c80..cbb2a113a0c08 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -385,7 +385,17 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { }, audit.WorkspaceBuildBaggageFromRequest(r), ) - return err + if err != nil { + return err + } + + // If this workspace build has a different template version ID to the previous build + // we can assume it has just been updated. + if createBuild.TemplateVersionID != uuid.Nil && createBuild.TemplateVersionID != previousWorkspaceBuild.TemplateVersionID { + api.notifyWorkspaceUpdated(ctx, tx, apiKey.UserID, workspace, createBuild.RichParameterValues) + } + + return nil }, nil) var buildErr wsbuilder.BuildError if xerrors.As(err, &buildErr) { @@ -443,12 +453,6 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - // If this workspace build has a different template version ID to the previous build - // we can assume it has just been updated. - if createBuild.TemplateVersionID != uuid.Nil && createBuild.TemplateVersionID != previousWorkspaceBuild.TemplateVersionID { - api.notifyWorkspaceUpdated(ctx, apiKey.UserID, workspace, createBuild.RichParameterValues) - } - api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{ Kind: wspubsub.WorkspaceEventKindStateChange, WorkspaceID: workspace.ID, @@ -459,31 +463,32 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { func (api *API) notifyWorkspaceUpdated( ctx context.Context, + db database.Store, initiatorID uuid.UUID, workspace database.Workspace, parameters []codersdk.WorkspaceBuildParameter, ) { log := api.Logger.With(slog.F("workspace_id", workspace.ID)) - template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID) + template, err := db.GetTemplateByID(ctx, workspace.TemplateID) if err != nil { log.Warn(ctx, "failed to fetch template for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err)) return } - version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID) + version, err := db.GetTemplateVersionByID(ctx, template.ActiveVersionID) if err != nil { log.Warn(ctx, "failed to fetch template version for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err)) return } - initiator, err := api.Database.GetUserByID(ctx, initiatorID) + initiator, err := db.GetUserByID(ctx, initiatorID) if err != nil { log.Warn(ctx, "failed to fetch user for workspace update notification", slog.F("initiator_id", initiatorID), slog.Error(err)) return } - owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID) + owner, err := db.GetUserByID(ctx, workspace.OwnerID) if err != nil { log.Warn(ctx, "failed to fetch user for workspace update notification", slog.F("owner_id", workspace.OwnerID), slog.Error(err)) return From 71ec1ac108339cde47aaae060fcb1249ba6b574c Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 2 Jan 2025 11:57:46 +0000 Subject: [PATCH 10/10] chore: move prep of builder and notify out of tx --- coderd/workspacebuilds.go | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index cbb2a113a0c08..0a19f7dfdaf0a 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -328,6 +328,12 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } + builder := wsbuilder.New(workspace, database.WorkspaceTransition(createBuild.Transition)). + Initiator(apiKey.UserID). + RichParameterValues(createBuild.RichParameterValues). + LogLevel(string(createBuild.LogLevel)). + DeploymentValues(api.Options.DeploymentValues) + var ( previousWorkspaceBuild database.WorkspaceBuild workspaceBuild *database.WorkspaceBuild @@ -348,12 +354,6 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return nil } - builder := wsbuilder.New(workspace, database.WorkspaceTransition(createBuild.Transition)). - Initiator(apiKey.UserID). - RichParameterValues(createBuild.RichParameterValues). - LogLevel(string(createBuild.LogLevel)). - DeploymentValues(api.Options.DeploymentValues) - if createBuild.TemplateVersionID != uuid.Nil { builder = builder.VersionID(createBuild.TemplateVersionID) } @@ -385,17 +385,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { }, audit.WorkspaceBuildBaggageFromRequest(r), ) - if err != nil { - return err - } - - // If this workspace build has a different template version ID to the previous build - // we can assume it has just been updated. - if createBuild.TemplateVersionID != uuid.Nil && createBuild.TemplateVersionID != previousWorkspaceBuild.TemplateVersionID { - api.notifyWorkspaceUpdated(ctx, tx, apiKey.UserID, workspace, createBuild.RichParameterValues) - } - - return nil + return err }, nil) var buildErr wsbuilder.BuildError if xerrors.As(err, &buildErr) { @@ -453,6 +443,12 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } + // If this workspace build has a different template version ID to the previous build + // we can assume it has just been updated. + if createBuild.TemplateVersionID != uuid.Nil && createBuild.TemplateVersionID != previousWorkspaceBuild.TemplateVersionID { + api.notifyWorkspaceUpdated(ctx, apiKey.UserID, workspace, createBuild.RichParameterValues) + } + api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{ Kind: wspubsub.WorkspaceEventKindStateChange, WorkspaceID: workspace.ID, @@ -463,32 +459,31 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { func (api *API) notifyWorkspaceUpdated( ctx context.Context, - db database.Store, initiatorID uuid.UUID, workspace database.Workspace, parameters []codersdk.WorkspaceBuildParameter, ) { log := api.Logger.With(slog.F("workspace_id", workspace.ID)) - template, err := db.GetTemplateByID(ctx, workspace.TemplateID) + template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID) if err != nil { log.Warn(ctx, "failed to fetch template for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err)) return } - version, err := db.GetTemplateVersionByID(ctx, template.ActiveVersionID) + version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID) if err != nil { log.Warn(ctx, "failed to fetch template version for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err)) return } - initiator, err := db.GetUserByID(ctx, initiatorID) + initiator, err := api.Database.GetUserByID(ctx, initiatorID) if err != nil { log.Warn(ctx, "failed to fetch user for workspace update notification", slog.F("initiator_id", initiatorID), slog.Error(err)) return } - owner, err := db.GetUserByID(ctx, workspace.OwnerID) + owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID) if err != nil { log.Warn(ctx, "failed to fetch user for workspace update notification", slog.F("owner_id", workspace.OwnerID), slog.Error(err)) return