mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add template delete notification (#14250)
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
INSERT INTO
|
||||
notification_templates (
|
||||
id,
|
||||
name,
|
||||
title_template,
|
||||
body_template,
|
||||
"group",
|
||||
actions
|
||||
)
|
||||
VALUES (
|
||||
'29a09665-2a4c-403f-9648-54301670e7be',
|
||||
'Template Deleted',
|
||||
E'Template "{{.Labels.name}}" deleted',
|
||||
E'Hi {{.UserName}}\n\nThe template **{{.Labels.name}}** was deleted by **{{ .Labels.initiator }}**.',
|
||||
'Template Events',
|
||||
'[
|
||||
{
|
||||
"label": "View templates",
|
||||
"url": "{{ base_url }}/templates"
|
||||
}
|
||||
]'::jsonb
|
||||
);
|
||||
@@ -19,3 +19,8 @@ var (
|
||||
TemplateUserAccountCreated = uuid.MustParse("4e19c0ac-94e1-4532-9515-d1801aa283b2")
|
||||
TemplateUserAccountDeleted = uuid.MustParse("f44d9314-ad03-4bc8-95d0-5cad491da6b6")
|
||||
)
|
||||
|
||||
// Template-related events.
|
||||
var (
|
||||
TemplateTemplateDeleted = uuid.MustParse("29a09665-2a4c-403f-9648-54301670e7be")
|
||||
)
|
||||
|
||||
@@ -740,6 +740,17 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TemplateTemplateDeleted",
|
||||
id: notifications.TemplateTemplateDeleted,
|
||||
payload: types.MessagePayload{
|
||||
UserName: "bobby",
|
||||
Labels: map[string]string{
|
||||
"name": "bobby-template",
|
||||
"initiator": "rob",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
allTemplates, err := enumerateAllTemplates(t)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -12,12 +13,15 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"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/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
@@ -56,6 +60,7 @@ func (api *API) template(rw http.ResponseWriter, r *http.Request) {
|
||||
// @Router /templates/{template} [delete]
|
||||
func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
apiKey = httpmw.APIKey(r)
|
||||
ctx = r.Context()
|
||||
template = httpmw.TemplateParam(r)
|
||||
auditor = *api.Auditor.Load()
|
||||
@@ -101,11 +106,47 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
admins, err := findTemplateAdmins(ctx, api.Database)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching template admins.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, admin := range admins {
|
||||
// Don't send notification to user which initiated the event.
|
||||
if admin.ID == apiKey.UserID {
|
||||
continue
|
||||
}
|
||||
api.notifyTemplateDeleted(ctx, template, apiKey.UserID, admin.ID)
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
|
||||
Message: "Template has been deleted!",
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) notifyTemplateDeleted(ctx context.Context, template database.Template, initiatorID uuid.UUID, receiverID uuid.UUID) {
|
||||
initiator, err := api.Database.GetUserByID(ctx, initiatorID)
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "failed to fetch initiator for template deletion notification", slog.F("initiator_id", initiatorID), slog.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NotificationsEnqueuer.Enqueue(ctx, receiverID, notifications.TemplateTemplateDeleted,
|
||||
map[string]string{
|
||||
"name": template.Name,
|
||||
"initiator": initiator.Username,
|
||||
}, "api-templates-delete",
|
||||
// Associate this notification with all the related entities.
|
||||
template.ID, template.OrganizationID,
|
||||
); err != nil {
|
||||
api.Logger.Warn(ctx, "failed to notify of template deletion", slog.F("deleted_template_id", template.ID), slog.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new template in an organization.
|
||||
// Returns a single template.
|
||||
//
|
||||
@@ -948,3 +989,22 @@ func (api *API) convertTemplate(
|
||||
MaxPortShareLevel: maxPortShareLevel,
|
||||
}
|
||||
}
|
||||
|
||||
// findTemplateAdmins fetches all users with template admin permission including owners.
|
||||
func findTemplateAdmins(ctx context.Context, store database.Store) ([]database.GetUsersRow, error) {
|
||||
// Notice: we can't scrape the user information in parallel as pq
|
||||
// fails with: unexpected describe rows response: 'D'
|
||||
owners, err := store.GetUsers(ctx, database.GetUsersParams{
|
||||
RbacRole: []string{codersdk.RoleOwner},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get owners: %w", err)
|
||||
}
|
||||
templateAdmins, err := store.GetUsers(ctx, database.GetUsersParams{
|
||||
RbacRole: []string{codersdk.RoleTemplateAdmin},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get template admins: %w", err)
|
||||
}
|
||||
return append(owners, templateAdmins...), nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/notifications"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
@@ -1326,3 +1327,98 @@ func TestTemplateMetrics(t *testing.T) {
|
||||
dbtime.Now(), res.Workspaces[0].LastUsedAt, time.Minute,
|
||||
)
|
||||
}
|
||||
|
||||
func TestTemplateNotifications(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("InitiatorIsNotNotified", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given: an initiator
|
||||
var (
|
||||
notifyEnq = &testutil.FakeNotificationsEnqueuer{}
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
NotificationsEnqueuer: notifyEnq,
|
||||
})
|
||||
initiator = coderdtest.CreateFirstUser(t, client)
|
||||
version = coderdtest.CreateTemplateVersion(t, client, initiator.OrganizationID, nil)
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template = coderdtest.CreateTemplate(t, client, initiator.OrganizationID, version.ID)
|
||||
ctx = testutil.Context(t, testutil.WaitLong)
|
||||
)
|
||||
|
||||
// When: the template is deleted by the initiator
|
||||
err := client.DeleteTemplate(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: the delete notification is not sent to the initiator.
|
||||
deleteNotifications := make([]*testutil.Notification, 0)
|
||||
for _, n := range notifyEnq.Sent {
|
||||
if n.TemplateID == notifications.TemplateTemplateDeleted {
|
||||
deleteNotifications = append(deleteNotifications, n)
|
||||
}
|
||||
}
|
||||
require.Len(t, deleteNotifications, 0)
|
||||
})
|
||||
|
||||
t.Run("OnlyOwnersAndAdminsAreNotified", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given: multiple users with different roles
|
||||
var (
|
||||
notifyEnq = &testutil.FakeNotificationsEnqueuer{}
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
NotificationsEnqueuer: notifyEnq,
|
||||
})
|
||||
initiator = coderdtest.CreateFirstUser(t, client)
|
||||
ctx = testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Setup template
|
||||
version = coderdtest.CreateTemplateVersion(t, client, initiator.OrganizationID, nil)
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template = coderdtest.CreateTemplate(t, client, initiator.OrganizationID, version.ID)
|
||||
)
|
||||
|
||||
// Setup users with different roles
|
||||
_, owner := coderdtest.CreateAnotherUser(t, client, initiator.OrganizationID, rbac.RoleOwner())
|
||||
_, tmplAdmin := coderdtest.CreateAnotherUser(t, client, initiator.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
coderdtest.CreateAnotherUser(t, client, initiator.OrganizationID, rbac.RoleMember())
|
||||
coderdtest.CreateAnotherUser(t, client, initiator.OrganizationID, rbac.RoleUserAdmin())
|
||||
coderdtest.CreateAnotherUser(t, client, initiator.OrganizationID, rbac.RoleAuditor())
|
||||
|
||||
// When: the template is deleted by the initiator
|
||||
err := client.DeleteTemplate(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: only owners and template admins should receive the
|
||||
// notification.
|
||||
shouldBeNotified := []uuid.UUID{owner.ID, tmplAdmin.ID}
|
||||
var deleteTemplateNotifications []*testutil.Notification
|
||||
for _, n := range notifyEnq.Sent {
|
||||
if n.TemplateID == notifications.TemplateTemplateDeleted {
|
||||
deleteTemplateNotifications = append(deleteTemplateNotifications, n)
|
||||
}
|
||||
}
|
||||
notifiedUsers := make([]uuid.UUID, 0, len(deleteTemplateNotifications))
|
||||
for _, n := range deleteTemplateNotifications {
|
||||
notifiedUsers = append(notifiedUsers, n.UserID)
|
||||
}
|
||||
require.ElementsMatch(t, shouldBeNotified, notifiedUsers)
|
||||
|
||||
// Validate the notification content
|
||||
for _, n := range deleteTemplateNotifications {
|
||||
require.Equal(t, n.TemplateID, notifications.TemplateTemplateDeleted)
|
||||
require.Contains(t, notifiedUsers, n.UserID)
|
||||
require.Contains(t, n.Targets, template.ID)
|
||||
require.Contains(t, n.Targets, template.OrganizationID)
|
||||
require.Equal(t, n.Labels["name"], template.Name)
|
||||
require.Equal(t, n.Labels["initiator"], coderdtest.FirstUserParams.Username)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3441,7 +3441,7 @@ func TestWorkspaceUsageTracking(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotifications(t *testing.T) {
|
||||
func TestWorkspaceNotifications(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Dormant", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user