From 24e8c1b3c110879f2d3c48b9413d61f7f82718a7 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 23 Aug 2024 15:53:50 +0200 Subject: [PATCH 01/15] Manual build failed --- .../000248_notifications_manual_build_failed.down.sql | 1 + .../000248_notifications_manual_build_failed.up.sql | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 coderd/database/migrations/000248_notifications_manual_build_failed.down.sql create mode 100644 coderd/database/migrations/000248_notifications_manual_build_failed.up.sql diff --git a/coderd/database/migrations/000248_notifications_manual_build_failed.down.sql b/coderd/database/migrations/000248_notifications_manual_build_failed.down.sql new file mode 100644 index 0000000000000..0689bb3d3c462 --- /dev/null +++ b/coderd/database/migrations/000248_notifications_manual_build_failed.down.sql @@ -0,0 +1 @@ +DELETE FROM notification_templates WHERE id = '2faeee0f-26cb-4e96-821c-85ccb9f71513'; diff --git a/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql b/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql new file mode 100644 index 0000000000000..a990d1329ded1 --- /dev/null +++ b/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql @@ -0,0 +1,9 @@ +INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) +VALUES ('2faeee0f-26cb-4e96-821c-85ccb9f71513', 'Workspace Manual Build Failed', E'Workspace "{{.Labels.name}}" manual build failed', + E'Hi {{.UserName}},\nA manual build of the workspace **{{.Labels.name}}** using the template **{{.Labels.template_name}}** failed.\nThe workspace build was initiated by "**{{.Labels.initiator}}**".', + 'Workspace Events', '[ + { + "label": "View workspace", + "url": "{{ base_url }}/@{{.Labels.workspace_owner_username}}/{{.Labels.name}}" + } + ]'::jsonb); From 0782614a6e93ba05aed150e16f3731e2e3dad20e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 23 Aug 2024 15:58:16 +0200 Subject: [PATCH 02/15] fix --- .../migrations/000248_notifications_manual_build_failed.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql b/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql index a990d1329ded1..1a30f8d49b9a9 100644 --- a/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql +++ b/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql @@ -1,6 +1,6 @@ INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) VALUES ('2faeee0f-26cb-4e96-821c-85ccb9f71513', 'Workspace Manual Build Failed', E'Workspace "{{.Labels.name}}" manual build failed', - E'Hi {{.UserName}},\nA manual build of the workspace **{{.Labels.name}}** using the template **{{.Labels.template_name}}** failed.\nThe workspace build was initiated by "**{{.Labels.initiator}}**".', + E'Hi {{.UserName}},\nA manual build of the workspace **{{.Labels.name}}** using the template **{{.Labels.template_name}}** failed.\nThe workspace build was initiated by **{{.Labels.initiator}}**.', 'Workspace Events', '[ { "label": "View workspace", From 2342d526ae5871e09b96a7b2b6af6bc851fb387e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 26 Aug 2024 14:14:34 +0200 Subject: [PATCH 03/15] WIP --- coderd/notifications/events.go | 1 + coderd/notifications/notifications_test.go | 13 ++++++ .../provisionerdserver/provisionerdserver.go | 40 ++++++++++++++++++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index ee143465bfe6b..6ba88c239edc8 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -12,6 +12,7 @@ var ( TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0") TemplateWorkspaceAutoUpdated = uuid.MustParse("c34a0c09-0704-4cac-bd1c-0c0146811c2b") TemplateWorkspaceMarkedForDeletion = uuid.MustParse("51ce2fdf-c9ca-4be1-8d70-628674f9bc42") + TemplateWorkspaceManualBuildFailed = uuid.MustParse("2faeee0f-26cb-4e96-821c-85ccb9f71513") ) // Account-related events. diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 20487e8ffe2f4..69ff984b36aad 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -736,6 +736,19 @@ func TestNotificationTemplatesCanRender(t *testing.T) { }, }, }, + { + name: "TemplateWorkspaceManualBuildFailed", + id: notifications.TemplateWorkspaceManualBuildFailed, + payload: types.MessagePayload{ + UserName: "bobby", + Labels: map[string]string{ + "name": "bobby-workspace", + "template_name": "bobby-template", + "initiator": "joe", + "workspace_owner_username": "mrbobby", + }, + }, + }, { name: "TemplateUserAccountCreated", id: notifications.TemplateUserAccountCreated, diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 9a1a1d45a5b85..7ca49110c1266 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1098,7 +1098,8 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto. func (s *server) notifyWorkspaceBuildFailed(ctx context.Context, workspace database.Workspace, build database.WorkspaceBuild) { var reason string if build.Reason.Valid() && build.Reason == database.BuildReasonInitiator { - return // failed workspace build initiated by a user should not notify + s.notifyWorkspaceManualBuildFailed(ctx, workspace, build) + return } reason = string(build.Reason) @@ -1114,6 +1115,43 @@ func (s *server) notifyWorkspaceBuildFailed(ctx context.Context, workspace datab } } +func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace database.Workspace, build database.WorkspaceBuild) { + templateAdmins, err := s.Database.GetUsers(ctx, database.GetUsersParams{ + RbacRole: []string{codersdk.RoleTemplateAdmin}, + }) + if err != nil { + s.Logger.Error(ctx, "unable to fetch template admins", slog.Error(err)) + return + } + + template, err := s.Database.GetTemplateByID(ctx, workspace.TemplateID) + if err != nil { + s.Logger.Error(ctx, "unable to fetch template", slog.Error(err)) + return + } + + workspaceOwner, err := s.Database.GetUserByID(ctx, workspace.OwnerID) + if err != nil { + s.Logger.Error(ctx, "unable to fetch workspace owner", slog.Error(err)) + return + } + + for _, templateAdmin := range templateAdmins { + if _, err := s.NotificationsEnqueuer.Enqueue(ctx, templateAdmin.ID, notifications.TemplateWorkspaceManualBuildFailed, + map[string]string{ + "name": workspace.Name, + "template_name": template.Name, + "initiator": build.InitiatorByUsername, + "workspace_owner_username": workspaceOwner.Name, + }, "provisionerdserver", + // Associate this notification with all the related entities. + workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID, + ); err != nil { + s.Logger.Warn(ctx, "failed to notify of failed workspace autobuild", slog.Error(err)) + } + } +} + // CompleteJob is triggered by a provision daemon to mark a provisioner job as completed. func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) (*proto.Empty, error) { ctx, span := s.startTrace(ctx, tracing.FuncName()) From 127aa472996c7f55df02667108d0b4820f9ceae1 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 26 Aug 2024 14:39:48 +0200 Subject: [PATCH 04/15] simple test --- .../provisionerdserver_test.go | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 79c1b00ac78ee..90b7072ef4260 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1804,6 +1804,88 @@ func TestNotifications(t *testing.T) { }) } }) + + t.Run("Manual build failed, template admins notified", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + notifEnq := &testutil.FakeNotificationsEnqueuer{} + + // Otherwise `(*Server).FailJob` fails with: + // audit log - get build {"error": "sql: no rows in result set"} + ignoreLogErrors := true + srv, db, ps, pd := setup(t, ignoreLogErrors, &overrides{ + notificationEnqueuer: notifEnq, + }) + + templateAdmin := dbgen.User(t, db, database.User{RBACRoles: []string{codersdk.RoleTemplateAdmin}}) + user := dbgen.User(t, db, database.User{}) + initiator := user + + template := dbgen.Template(t, db, database.Template{ + Name: "template", + Provisioner: database.ProvisionerTypeEcho, + OrganizationID: pd.OrganizationID, + }) + template, err := db.GetTemplateByID(ctx, template.ID) + require.NoError(t, err) + file := dbgen.File(t, db, database.File{CreatedBy: user.ID}) + workspace := dbgen.Workspace(t, db, database.Workspace{ + TemplateID: template.ID, + OwnerID: user.ID, + OrganizationID: pd.OrganizationID, + }) + version := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + OrganizationID: pd.OrganizationID, + TemplateID: uuid.NullUUID{ + UUID: template.ID, + Valid: true, + }, + JobID: uuid.New(), + }) + build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + TemplateVersionID: version.ID, + InitiatorID: initiator.ID, + Transition: database.WorkspaceTransitionDelete, + Reason: database.BuildReasonInitiator, + }) + job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + FileID: file.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: build.ID, + })), + OrganizationID: pd.OrganizationID, + }) + _, err = db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{ + OrganizationID: pd.OrganizationID, + WorkerID: uuid.NullUUID{ + UUID: pd.ID, + Valid: true, + }, + Types: []database.ProvisionerType{database.ProvisionerTypeEcho}, + }) + require.NoError(t, err) + + _, err = srv.FailJob(ctx, &proto.FailedJob{ + JobId: job.ID.String(), + Type: &proto.FailedJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{ + State: []byte{}, + }, + }, + }) + require.NoError(t, err) + + require.Len(t, notifEnq.Sent, 1) + require.Equal(t, notifEnq.Sent[0].UserID, templateAdmin.ID) + require.Equal(t, notifEnq.Sent[0].TemplateID, notifications.TemplateWorkspaceManualBuildFailed) + require.Contains(t, notifEnq.Sent[0].Targets, template.ID) + require.Contains(t, notifEnq.Sent[0].Targets, workspace.ID) + require.Contains(t, notifEnq.Sent[0].Targets, workspace.OrganizationID) + require.Contains(t, notifEnq.Sent[0].Targets, user.ID) + }) } type overrides struct { From e9737eb4f47716e41ef4fc5c69d3371d611b6c6a Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 26 Aug 2024 14:45:16 +0200 Subject: [PATCH 05/15] fix --- .../provisionerdserver_test.go | 65 +++++-------------- 1 file changed, 17 insertions(+), 48 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 90b7072ef4260..d277af7dd2b01 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1738,8 +1738,6 @@ func TestNotifications(t *testing.T) { Provisioner: database.ProvisionerTypeEcho, OrganizationID: pd.OrganizationID, }) - template, err := db.GetTemplateByID(ctx, template.ID) - require.NoError(t, err) file := dbgen.File(t, db, database.File{CreatedBy: user.ID}) workspace := dbgen.Workspace(t, db, database.Workspace{ TemplateID: template.ID, @@ -1769,7 +1767,7 @@ func TestNotifications(t *testing.T) { })), OrganizationID: pd.OrganizationID, }) - _, err = db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{ + _, err := db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{ OrganizationID: pd.OrganizationID, WorkerID: uuid.NullUUID{ UUID: pd.ID, @@ -1809,75 +1807,46 @@ func TestNotifications(t *testing.T) { t.Parallel() ctx := context.Background() - notifEnq := &testutil.FakeNotificationsEnqueuer{} - // Otherwise `(*Server).FailJob` fails with: - // audit log - get build {"error": "sql: no rows in result set"} - ignoreLogErrors := true - srv, db, ps, pd := setup(t, ignoreLogErrors, &overrides{ - notificationEnqueuer: notifEnq, - }) + // given + notifEnq := &testutil.FakeNotificationsEnqueuer{} + srv, db, ps, pd := setup(t, true /* ignoreLogErrors */, &overrides{notificationEnqueuer: notifEnq}) templateAdmin := dbgen.User(t, db, database.User{RBACRoles: []string{codersdk.RoleTemplateAdmin}}) user := dbgen.User(t, db, database.User{}) - initiator := user template := dbgen.Template(t, db, database.Template{ - Name: "template", - Provisioner: database.ProvisionerTypeEcho, - OrganizationID: pd.OrganizationID, + Name: "template", Provisioner: database.ProvisionerTypeEcho, OrganizationID: pd.OrganizationID, }) - template, err := db.GetTemplateByID(ctx, template.ID) - require.NoError(t, err) - file := dbgen.File(t, db, database.File{CreatedBy: user.ID}) workspace := dbgen.Workspace(t, db, database.Workspace{ - TemplateID: template.ID, - OwnerID: user.ID, - OrganizationID: pd.OrganizationID, + TemplateID: template.ID, OwnerID: user.ID, OrganizationID: pd.OrganizationID, }) version := dbgen.TemplateVersion(t, db, database.TemplateVersion{ - OrganizationID: pd.OrganizationID, - TemplateID: uuid.NullUUID{ - UUID: template.ID, - Valid: true, - }, - JobID: uuid.New(), + OrganizationID: pd.OrganizationID, TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, JobID: uuid.New(), }) build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - WorkspaceID: workspace.ID, - TemplateVersionID: version.ID, - InitiatorID: initiator.ID, - Transition: database.WorkspaceTransitionDelete, - Reason: database.BuildReasonInitiator, + WorkspaceID: workspace.ID, TemplateVersionID: version.ID, InitiatorID: user.ID, Transition: database.WorkspaceTransitionDelete, Reason: database.BuildReasonInitiator, }) job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - FileID: file.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: build.ID, - })), + FileID: dbgen.File(t, db, database.File{CreatedBy: user.ID}).ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{WorkspaceBuildID: build.ID})), OrganizationID: pd.OrganizationID, }) - _, err = db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{ + _, err := db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{ OrganizationID: pd.OrganizationID, - WorkerID: uuid.NullUUID{ - UUID: pd.ID, - Valid: true, - }, - Types: []database.ProvisionerType{database.ProvisionerTypeEcho}, + WorkerID: uuid.NullUUID{UUID: pd.ID, Valid: true}, + Types: []database.ProvisionerType{database.ProvisionerTypeEcho}, }) require.NoError(t, err) + // when _, err = srv.FailJob(ctx, &proto.FailedJob{ - JobId: job.ID.String(), - Type: &proto.FailedJob_WorkspaceBuild_{ - WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{ - State: []byte{}, - }, - }, + JobId: job.ID.String(), Type: &proto.FailedJob_WorkspaceBuild_{WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{State: []byte{}}}, }) require.NoError(t, err) + // then require.Len(t, notifEnq.Sent, 1) require.Equal(t, notifEnq.Sent[0].UserID, templateAdmin.ID) require.Equal(t, notifEnq.Sent[0].TemplateID, notifications.TemplateWorkspaceManualBuildFailed) From 0d889b5d5c87788970cfa69d6b4c6f1c4b2f9d87 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 26 Aug 2024 14:53:57 +0200 Subject: [PATCH 06/15] fix --- coderd/notifications/notifications_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 2eb2cd6b10e8a..93b0dc3b8112d 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -814,6 +814,19 @@ func TestNotificationTemplatesCanRender(t *testing.T) { }, }, }, + { + name: "TemplateWorkspaceManualBuildFailed", + id: notifications.TemplateWorkspaceManualBuildFailed, + payload: types.MessagePayload{ + UserName: "bobby", + Labels: map[string]string{ + "name": "bobby-workspace", + "template_name": "bobby-template", + "initiator": "joe", + "workspace_owner_username": "mrbobby", + }, + }, + }, } allTemplates, err := enumerateAllTemplates(t) From 015ec3c631910b1c41cd99a5304c99e67f2e0fe1 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 26 Aug 2024 15:22:22 +0200 Subject: [PATCH 07/15] double n --- .../migrations/000248_notifications_manual_build_failed.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql b/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql index 1a30f8d49b9a9..f6eb765f788dc 100644 --- a/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql +++ b/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql @@ -1,6 +1,6 @@ INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) VALUES ('2faeee0f-26cb-4e96-821c-85ccb9f71513', 'Workspace Manual Build Failed', E'Workspace "{{.Labels.name}}" manual build failed', - E'Hi {{.UserName}},\nA manual build of the workspace **{{.Labels.name}}** using the template **{{.Labels.template_name}}** failed.\nThe workspace build was initiated by **{{.Labels.initiator}}**.', + E'Hi {{.UserName}},\n\nA manual build of the workspace **{{.Labels.name}}** using the template **{{.Labels.template_name}}** failed.\nThe workspace build was initiated by **{{.Labels.initiator}}**.', 'Workspace Events', '[ { "label": "View workspace", From 6a0b9f7eb6d9bf4807511cd559fefc0b3a3ab037 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 26 Aug 2024 15:39:54 +0200 Subject: [PATCH 08/15] expose template version --- ...248_notifications_manual_build_failed.up.sql | 2 +- coderd/notifications/notifications_test.go | 1 + coderd/provisionerdserver/provisionerdserver.go | 9 ++++++++- .../provisionerdserver_test.go | 17 +++++++++++------ 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql b/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql index f6eb765f788dc..81a86b95eb7a1 100644 --- a/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql +++ b/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql @@ -1,6 +1,6 @@ INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) VALUES ('2faeee0f-26cb-4e96-821c-85ccb9f71513', 'Workspace Manual Build Failed', E'Workspace "{{.Labels.name}}" manual build failed', - E'Hi {{.UserName}},\n\nA manual build of the workspace **{{.Labels.name}}** using the template **{{.Labels.template_name}}** failed.\nThe workspace build was initiated by **{{.Labels.initiator}}**.', + E'Hi {{.UserName}},\n\nA manual build of the workspace **{{.Labels.name}}** using the template **{{.Labels.template_name}}** failed (version: **{{.Labels.template_version_name}}**).\nThe workspace build was initiated by **{{.Labels.initiator}}**.', 'Workspace Events', '[ { "label": "View workspace", diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 93b0dc3b8112d..cde2a64331df5 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -822,6 +822,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) { Labels: map[string]string{ "name": "bobby-workspace", "template_name": "bobby-template", + "template_version_name": "bobby-template-version", "initiator": "joe", "workspace_owner_username": "mrbobby", }, diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 7ca49110c1266..d409836e8b6c3 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1130,6 +1130,12 @@ func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace return } + templateVersion, err := s.Database.GetTemplateVersionByID(ctx, build.TemplateVersionID) + if err != nil { + s.Logger.Error(ctx, "unable to fetch template version", slog.Error(err)) + return + } + workspaceOwner, err := s.Database.GetUserByID(ctx, workspace.OwnerID) if err != nil { s.Logger.Error(ctx, "unable to fetch workspace owner", slog.Error(err)) @@ -1141,8 +1147,9 @@ func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace map[string]string{ "name": workspace.Name, "template_name": template.Name, + "template_version_name": templateVersion.Name, "initiator": build.InitiatorByUsername, - "workspace_owner_username": workspaceOwner.Name, + "workspace_owner_username": workspaceOwner.Username, }, "provisionerdserver", // Associate this notification with all the related entities. workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index d277af7dd2b01..b89b6fb482f90 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1848,12 +1848,17 @@ func TestNotifications(t *testing.T) { // then require.Len(t, notifEnq.Sent, 1) - require.Equal(t, notifEnq.Sent[0].UserID, templateAdmin.ID) - require.Equal(t, notifEnq.Sent[0].TemplateID, notifications.TemplateWorkspaceManualBuildFailed) - require.Contains(t, notifEnq.Sent[0].Targets, template.ID) - require.Contains(t, notifEnq.Sent[0].Targets, workspace.ID) - require.Contains(t, notifEnq.Sent[0].Targets, workspace.OrganizationID) - require.Contains(t, notifEnq.Sent[0].Targets, user.ID) + assert.Equal(t, notifEnq.Sent[0].UserID, templateAdmin.ID) + assert.Equal(t, notifEnq.Sent[0].TemplateID, notifications.TemplateWorkspaceManualBuildFailed) + assert.Contains(t, notifEnq.Sent[0].Targets, template.ID) + assert.Contains(t, notifEnq.Sent[0].Targets, workspace.ID) + assert.Contains(t, notifEnq.Sent[0].Targets, workspace.OrganizationID) + assert.Contains(t, notifEnq.Sent[0].Targets, user.ID) + assert.Equal(t, workspace.Name, notifEnq.Sent[0].Labels["name"]) + assert.Equal(t, template.Name, notifEnq.Sent[0].Labels["template_name"]) + assert.Equal(t, version.Name, notifEnq.Sent[0].Labels["template_version_name"]) + assert.Equal(t, user.Username, notifEnq.Sent[0].Labels["initiator"]) + assert.Equal(t, user.Username, notifEnq.Sent[0].Labels["workspace_owner_username"]) }) } From f03cbc091dcfa83e080c58bbee352dd34c2530bc Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 27 Aug 2024 10:37:35 +0200 Subject: [PATCH 09/15] fix: logging --- coderd/provisionerdserver/provisionerdserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index d409836e8b6c3..a44a7da5e4bb8 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1154,7 +1154,7 @@ func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace // Associate this notification with all the related entities. workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID, ); err != nil { - s.Logger.Warn(ctx, "failed to notify of failed workspace autobuild", slog.Error(err)) + s.Logger.Warn(ctx, "failed to notify of failed workspace manual build", slog.Error(err)) } } } From 30f380910ddaa1cee465692c1b6da881f74b0cc5 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 27 Aug 2024 11:35:39 +0200 Subject: [PATCH 10/15] build number --- .../000248_notifications_manual_build_failed.up.sql | 4 ++-- coderd/notifications/notifications_test.go | 1 + coderd/provisionerdserver/provisionerdserver.go | 1 + coderd/provisionerdserver/provisionerdserver_test.go | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql b/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql index 81a86b95eb7a1..df227666f0fb1 100644 --- a/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql +++ b/coderd/database/migrations/000248_notifications_manual_build_failed.up.sql @@ -3,7 +3,7 @@ VALUES ('2faeee0f-26cb-4e96-821c-85ccb9f71513', 'Workspace Manual Build Failed', E'Hi {{.UserName}},\n\nA manual build of the workspace **{{.Labels.name}}** using the template **{{.Labels.template_name}}** failed (version: **{{.Labels.template_version_name}}**).\nThe workspace build was initiated by **{{.Labels.initiator}}**.', 'Workspace Events', '[ { - "label": "View workspace", - "url": "{{ base_url }}/@{{.Labels.workspace_owner_username}}/{{.Labels.name}}" + "label": "View build", + "url": "{{ base_url }}/@{{.Labels.workspace_owner_username}}/{{.Labels.name}}/builds/{{.Labels.workspace_build_number}}" } ]'::jsonb); diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index cde2a64331df5..91e71462b429f 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -825,6 +825,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) { "template_version_name": "bobby-template-version", "initiator": "joe", "workspace_owner_username": "mrbobby", + "workspace_build_number": "3", }, }, }, diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index a44a7da5e4bb8..0683cf694f2ac 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1150,6 +1150,7 @@ func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace "template_version_name": templateVersion.Name, "initiator": build.InitiatorByUsername, "workspace_owner_username": workspaceOwner.Username, + "workspace_build_number": strconv.Itoa(int(build.BuildNumber)), }, "provisionerdserver", // Associate this notification with all the related entities. workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index b89b6fb482f90..48ba66c87395b 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "io" "net/url" + "strconv" "strings" "sync" "sync/atomic" @@ -1859,6 +1860,7 @@ func TestNotifications(t *testing.T) { assert.Equal(t, version.Name, notifEnq.Sent[0].Labels["template_version_name"]) assert.Equal(t, user.Username, notifEnq.Sent[0].Labels["initiator"]) assert.Equal(t, user.Username, notifEnq.Sent[0].Labels["workspace_owner_username"]) + assert.Equal(t, strconv.Itoa(int(build.BuildNumber)), notifEnq.Sent[0].Labels["workspace_build_number"]) }) } From c2f038cc681937b2815e13ae2b2f84e4e19ad8cc Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 27 Aug 2024 12:06:49 +0200 Subject: [PATCH 11/15] prepare --- .../provisionerdserver/provisionerdserver.go | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 0683cf694f2ac..a2d2acd5e452c 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1116,29 +1116,9 @@ func (s *server) notifyWorkspaceBuildFailed(ctx context.Context, workspace datab } func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace database.Workspace, build database.WorkspaceBuild) { - templateAdmins, err := s.Database.GetUsers(ctx, database.GetUsersParams{ - RbacRole: []string{codersdk.RoleTemplateAdmin}, - }) - if err != nil { - s.Logger.Error(ctx, "unable to fetch template admins", slog.Error(err)) - return - } - - template, err := s.Database.GetTemplateByID(ctx, workspace.TemplateID) + templateAdmins, template, templateVersion, workspaceOwner, err := s.prepareForNotifyWorkspaceManualBuildFailed(ctx, workspace, build) if err != nil { - s.Logger.Error(ctx, "unable to fetch template", slog.Error(err)) - return - } - - templateVersion, err := s.Database.GetTemplateVersionByID(ctx, build.TemplateVersionID) - if err != nil { - s.Logger.Error(ctx, "unable to fetch template version", slog.Error(err)) - return - } - - workspaceOwner, err := s.Database.GetUserByID(ctx, workspace.OwnerID) - if err != nil { - s.Logger.Error(ctx, "unable to fetch workspace owner", slog.Error(err)) + s.Logger.Error(ctx, "unable to collect data for manual build failed notification", slog.Error(err)) return } @@ -1160,6 +1140,32 @@ func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace } } +func (s *server) prepareForNotifyWorkspaceManualBuildFailed(ctx context.Context, workspace database.Workspace, build database.WorkspaceBuild) ([]database.GetUsersRow, + database.Template, database.TemplateVersion, database.User, error) { + templateAdmins, err := s.Database.GetUsers(ctx, database.GetUsersParams{ + RbacRole: []string{codersdk.RoleTemplateAdmin}, + }) + if err != nil { + return nil, database.Template{}, database.TemplateVersion{}, database.User{}, xerrors.Errorf("unable to fetch template admins: %w", err) + } + + template, err := s.Database.GetTemplateByID(ctx, workspace.TemplateID) + if err != nil { + return nil, database.Template{}, database.TemplateVersion{}, database.User{}, xerrors.Errorf("unable to fetch template: %w", err) + } + + templateVersion, err := s.Database.GetTemplateVersionByID(ctx, build.TemplateVersionID) + if err != nil { + return nil, database.Template{}, database.TemplateVersion{}, database.User{}, xerrors.Errorf("unable to fetch template version: %w", err) + } + + workspaceOwner, err := s.Database.GetUserByID(ctx, workspace.OwnerID) + if err != nil { + return nil, database.Template{}, database.TemplateVersion{}, database.User{}, xerrors.Errorf("unable to fetch workspace owner: %w", err) + } + return templateAdmins, template, templateVersion, workspaceOwner, nil +} + // CompleteJob is triggered by a provision daemon to mark a provisioner job as completed. func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) (*proto.Empty, error) { ctx, span := s.startTrace(ctx, tracing.FuncName()) From 2381213257dec3e3f154fdd57b54ef63b91b4a8f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 27 Aug 2024 12:12:57 +0200 Subject: [PATCH 12/15] fmt --- coderd/provisionerdserver/provisionerdserver.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index a2d2acd5e452c..65fba00cdd220 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1141,7 +1141,8 @@ func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace } func (s *server) prepareForNotifyWorkspaceManualBuildFailed(ctx context.Context, workspace database.Workspace, build database.WorkspaceBuild) ([]database.GetUsersRow, - database.Template, database.TemplateVersion, database.User, error) { + database.Template, database.TemplateVersion, database.User, error +) { templateAdmins, err := s.Database.GetUsers(ctx, database.GetUsersParams{ RbacRole: []string{codersdk.RoleTemplateAdmin}, }) From 51b880c4e38749928c07cf25c730dbd1c47fe15f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 27 Aug 2024 12:14:24 +0200 Subject: [PATCH 13/15] fmt --- coderd/provisionerdserver/provisionerdserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 65fba00cdd220..7b502c3ecc2d2 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1141,7 +1141,7 @@ func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace } func (s *server) prepareForNotifyWorkspaceManualBuildFailed(ctx context.Context, workspace database.Workspace, build database.WorkspaceBuild) ([]database.GetUsersRow, - database.Template, database.TemplateVersion, database.User, error + database.Template, database.TemplateVersion, database.User, error, ) { templateAdmins, err := s.Database.GetUsers(ctx, database.GetUsersParams{ RbacRole: []string{codersdk.RoleTemplateAdmin}, From e4f6f29d2175ffe63cd6719af7fc68f0fa6341c3 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 27 Aug 2024 15:20:38 +0200 Subject: [PATCH 14/15] test --- .../provisionerdserver/provisionerdserver.go | 27 ++++++++++++++++++- .../provisionerdserver_test.go | 3 +++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 7b502c3ecc2d2..f1c02e87053b9 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "reflect" + "sort" "strconv" "strings" "sync/atomic" @@ -1143,13 +1144,37 @@ func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace func (s *server) prepareForNotifyWorkspaceManualBuildFailed(ctx context.Context, workspace database.Workspace, build database.WorkspaceBuild) ([]database.GetUsersRow, database.Template, database.TemplateVersion, database.User, error, ) { - templateAdmins, err := s.Database.GetUsers(ctx, database.GetUsersParams{ + users, err := s.Database.GetUsers(ctx, database.GetUsersParams{ RbacRole: []string{codersdk.RoleTemplateAdmin}, }) if err != nil { return nil, database.Template{}, database.TemplateVersion{}, database.User{}, xerrors.Errorf("unable to fetch template admins: %w", err) } + usersByIDs := map[uuid.UUID]database.GetUsersRow{} + var userIDs []uuid.UUID + for _, user := range users { + usersByIDs[user.ID] = user + userIDs = append(userIDs, user.ID) + } + + var templateAdmins []database.GetUsersRow + if len(userIDs) > 0 { + orgIDsByMemberIDs, err := s.Database.GetOrganizationIDsByMemberIDs(ctx, userIDs) + if err != nil { + return nil, database.Template{}, database.TemplateVersion{}, database.User{}, xerrors.Errorf("unable to fetch organization IDs by member IDs: %w", err) + } + + for _, entry := range orgIDsByMemberIDs { + if slices.Contains(entry.OrganizationIDs, workspace.OrganizationID) { + templateAdmins = append(templateAdmins, usersByIDs[entry.UserID]) + } + } + } + sort.Slice(templateAdmins, func(i, j int) bool { + return templateAdmins[i].Username < templateAdmins[j].Username + }) + template, err := s.Database.GetTemplateByID(ctx, workspace.TemplateID) if err != nil { return nil, database.Template{}, database.TemplateVersion{}, database.User{}, xerrors.Errorf("unable to fetch template: %w", err) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 48ba66c87395b..9028af0be3b9b 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1814,7 +1814,10 @@ func TestNotifications(t *testing.T) { srv, db, ps, pd := setup(t, true /* ignoreLogErrors */, &overrides{notificationEnqueuer: notifEnq}) templateAdmin := dbgen.User(t, db, database.User{RBACRoles: []string{codersdk.RoleTemplateAdmin}}) + _ /* other template admin, should not receive notification */ = dbgen.User(t, db, database.User{RBACRoles: []string{codersdk.RoleTemplateAdmin}}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin.ID, OrganizationID: pd.OrganizationID}) user := dbgen.User(t, db, database.User{}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user.ID, OrganizationID: pd.OrganizationID}) template := dbgen.Template(t, db, database.Template{ Name: "template", Provisioner: database.ProvisionerTypeEcho, OrganizationID: pd.OrganizationID, From 53f6eab82a600d516a10011f2fd32b31db7ed7b3 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 27 Aug 2024 16:24:16 +0200 Subject: [PATCH 15/15] comment --- coderd/provisionerdserver/provisionerdserver.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index f1c02e87053b9..8644e26ec9972 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1141,6 +1141,9 @@ func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace } } +// prepareForNotifyWorkspaceManualBuildFailed collects data required to build notifications for template admins. +// The template `notifications.TemplateWorkspaceManualBuildFailed` is quite detailed as it requires information about the template, +// template version, workspace, workspace build, etc. func (s *server) prepareForNotifyWorkspaceManualBuildFailed(ctx context.Context, workspace database.Workspace, build database.WorkspaceBuild) ([]database.GetUsersRow, database.Template, database.TemplateVersion, database.User, error, ) {