feat: notify users on template deprecation (#15195)

Closes https://github.com/coder/coder/issues/15117

Notify users when a template has been deprecated.
This commit is contained in:
Danielle Maywood
2024-10-24 13:12:12 +01:00
committed by GitHub
parent bcd68ee249
commit 095c9797c9
8 changed files with 255 additions and 2 deletions
@@ -0,0 +1 @@
DELETE FROM notification_templates WHERE id = 'f40fae84-55a2-42cd-99fa-b41c1ca64894';
@@ -0,0 +1,22 @@
INSERT INTO notification_templates
(id, name, title_template, body_template, "group", actions)
VALUES (
'f40fae84-55a2-42cd-99fa-b41c1ca64894',
'Template Deprecated',
E'Template ''{{.Labels.template}}'' has been deprecated',
E'Hello {{.UserName}},\n\n'||
E'The template **{{.Labels.template}}** has been deprecated with the following message:\n\n' ||
E'**{{.Labels.message}}**\n\n' ||
E'New workspaces may not be created from this template. Existing workspaces will continue to function normally.',
'Template Events',
'[
{
"label": "See affected workspaces",
"url": "{{base_url}}/workspaces?filter=owner%3Ame+template%3A{{.Labels.template}}"
},
{
"label": "View template",
"url": "{{base_url}}/templates/{{.Labels.organization}}/{{.Labels.template}}"
}
]'::jsonb
);
+2 -1
View File
@@ -30,7 +30,8 @@ var (
// Template-related events.
var (
TemplateTemplateDeleted = uuid.MustParse("29a09665-2a4c-403f-9648-54301670e7be")
TemplateTemplateDeleted = uuid.MustParse("29a09665-2a4c-403f-9648-54301670e7be")
TemplateTemplateDeprecated = uuid.MustParse("f40fae84-55a2-42cd-99fa-b41c1ca64894")
TemplateWorkspaceBuildsFailedReport = uuid.MustParse("34a20db2-e9cc-4a93-b0e4-8569699d7a00")
)
@@ -1021,6 +1021,20 @@ func TestNotificationTemplates_Golden(t *testing.T) {
appName: "Custom Application Name",
logoURL: "https://custom.application/logo.png",
},
{
name: "TemplateTemplateDeprecated",
id: notifications.TemplateTemplateDeprecated,
payload: types.MessagePayload{
UserName: "Bobby",
UserEmail: "bobby@coder.com",
UserUsername: "bobby",
Labels: map[string]string{
"template": "alpha",
"message": "This template has been replaced by beta",
"organization": "coder",
},
},
},
}
// We must have a test case for every notification_template. This is enforced below:
@@ -0,0 +1,98 @@
From: system@coder.com
To: bobby@coder.com
Subject: Template 'alpha' has been deprecated
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,
The template alpha has been deprecated with the following message:
This template has been replaced by beta
New workspaces may not be created from this template. Existing workspaces w=
ill continue to function normally.
See affected workspaces: http://test.com/workspaces?filter=3Downer%3Ame+tem=
plate%3Aalpha
View template: http://test.com/templates/coder/alpha
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8
<!doctype html>
<html lang=3D"en">
<head>
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>Template 'alpha' has been deprecated</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
; background: #f8fafc;">
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
n: left; font-size: 14px; line-height: 1.5;">
<div style=3D"text-align: center;">
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
Template 'alpha' has been deprecated
</h1>
<div style=3D"line-height: 1.5;">
<p>Hello Bobby,</p>
<p>The template <strong>alpha</strong> has been deprecated with the followi=
ng message:</p>
<p><strong>This template has been replaced by beta</strong></p>
<p>New workspaces may not be created from this template. Existing workspace=
s will continue to function normally.</p>
</div>
<div style=3D"text-align: center; margin-top: 32px;">
=20
<a href=3D"http://test.com/workspaces?filter=3Downer%3Ame+template%=
3Aalpha" style=3D"display: inline-block; padding: 13px 24px; background-col=
or: #020617; color: #f8fafc; text-decoration: none; border-radius: 8px; mar=
gin: 0 4px;">
See affected workspaces
</a>
=20
<a href=3D"http://test.com/templates/coder/alpha" style=3D"display:=
inline-block; padding: 13px 24px; background-color: #020617; color: #f8faf=
c; text-decoration: none; border-radius: 8px; margin: 0 4px;">
View template
</a>
=20
</div>
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
<p>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a =
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
ttp://test.com</a></p>
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
r: #2563eb; text-decoration: none;">Click here to manage your notification =
settings</a></p>
<p><a href=3D"http://test.com/settings/notifications?disabled=3Df40=
fae84-55a2-42cd-99fa-b41c1ca64894" style=3D"color: #2563eb; text-decoration=
: none;">Stop receiving emails like this</a></p>
</div>
</div>
</body>
</html>
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
@@ -0,0 +1,33 @@
{
"_version": "1.1",
"msg_id": "00000000-0000-0000-0000-000000000000",
"payload": {
"_version": "1.1",
"notification_name": "Template Deprecated",
"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 affected workspaces",
"url": "http://test.com/workspaces?filter=owner%3Ame+template%3Aalpha"
},
{
"label": "View template",
"url": "http://test.com/templates/coder/alpha"
}
],
"labels": {
"message": "This template has been replaced by beta",
"organization": "coder",
"template": "alpha"
},
"data": null
},
"title": "Template 'alpha' has been deprecated",
"title_markdown": "Template 'alpha' has been deprecated",
"body": "Hello Bobby,\n\nThe template alpha has been deprecated with the following message:\n\nThis template has been replaced by beta\n\nNew workspaces may not be created from this template. Existing workspaces will continue to function normally.",
"body_markdown": "Hello Bobby,\n\nThe template **alpha** has been deprecated with the following message:\n\n**This template has been replaced by beta**\n\nNew workspaces may not be created from this template. Existing workspaces will continue to function normally."
}
+42
View File
@@ -845,6 +845,12 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
return
}
if template.Deprecated != updated.Deprecated && updated.Deprecated != "" {
if err := api.notifyUsersOfTemplateDeprecation(ctx, updated); err != nil {
api.Logger.Error(ctx, "failed to notify users of template deprecation", slog.Error(err))
}
}
if updated.UpdatedAt.IsZero() {
aReq.New = template
rw.WriteHeader(http.StatusNotModified)
@@ -855,6 +861,42 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(updated))
}
func (api *API) notifyUsersOfTemplateDeprecation(ctx context.Context, template database.Template) error {
workspaces, err := api.Database.GetWorkspaces(ctx, database.GetWorkspacesParams{
TemplateIDs: []uuid.UUID{template.ID},
})
if err != nil {
return xerrors.Errorf("get workspaces by template id: %w", err)
}
users := make(map[uuid.UUID]struct{})
for _, workspace := range workspaces {
users[workspace.OwnerID] = struct{}{}
}
errs := []error{}
for userID := range users {
_, err = api.NotificationsEnqueuer.Enqueue(
//nolint:gocritic // We need the system auth context to be able to send the deprecation notification.
dbauthz.AsSystemRestricted(ctx),
userID,
notifications.TemplateTemplateDeprecated,
map[string]string{
"template": template.Name,
"message": template.Deprecated,
"organization": template.OrganizationName,
},
"notify-users-of-template-deprecation",
)
if err != nil {
errs = append(errs, xerrors.Errorf("enqueue notification: %w", err))
}
}
return errors.Join(errs...)
}
// @Summary Get template DAUs by ID
// @ID get-template-daus-by-id
// @Security CoderSessionToken
+43 -1
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"net/http"
"slices"
"testing"
"time"
@@ -38,9 +39,11 @@ func TestTemplates(t *testing.T) {
t.Run("Deprecated", func(t *testing.T) {
t.Parallel()
notifyEnq := &testutil.FakeNotificationsEnqueuer{}
owner, user := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
IncludeProvisionerDaemon: true,
NotificationsEnqueuer: notifyEnq,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
@@ -48,11 +51,24 @@ func TestTemplates(t *testing.T) {
},
},
})
client, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
client, secondUser := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
otherClient, otherUser := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
_ = coderdtest.CreateWorkspace(t, owner, template.ID)
_ = coderdtest.CreateWorkspace(t, client, template.ID)
// Create another template for testing that users of another template do not
// get a notification.
secondVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
secondTemplate := coderdtest.CreateTemplate(t, client, user.OrganizationID, secondVersion.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, secondVersion.ID)
_ = coderdtest.CreateWorkspace(t, otherClient, secondTemplate.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
@@ -65,6 +81,32 @@ func TestTemplates(t *testing.T) {
assert.True(t, updated.Deprecated)
assert.NotEmpty(t, updated.DeprecationMessage)
notifs := []*testutil.Notification{}
for _, notif := range notifyEnq.Sent {
if notif.TemplateID == notifications.TemplateTemplateDeprecated {
notifs = append(notifs, notif)
}
}
require.Equal(t, 2, len(notifs))
expectedSentTo := []string{user.UserID.String(), secondUser.ID.String()}
slices.Sort(expectedSentTo)
sentTo := []string{}
for _, notif := range notifs {
sentTo = append(sentTo, notif.UserID.String())
}
slices.Sort(sentTo)
// Require the notification to have only been sent to the expected users
assert.Equal(t, expectedSentTo, sentTo)
// The previous check should verify this but we're double checking that
// the notification wasn't sent to users not using the template.
for _, notif := range notifs {
assert.NotEqual(t, otherUser.ID, notif.UserID)
}
_, err = client.CreateWorkspace(ctx, user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "foobar",