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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy