mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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
|
||||
);
|
||||
@@ -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:
|
||||
|
||||
Vendored
+98
@@ -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>© 2024 Coder. All rights reserved - <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--
|
||||
Vendored
+33
@@ -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."
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user