Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 095c979

Browse files
feat: notify users on template deprecation (#15195)
Closes #15117 Notify users when a template has been deprecated.
1 parent bcd68ee commit 095c979

8 files changed

+255
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DELETE FROM notification_templates WHERE id = 'f40fae84-55a2-42cd-99fa-b41c1ca64894';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
INSERT INTO notification_templates
2+
(id, name, title_template, body_template, "group", actions)
3+
VALUES (
4+
'f40fae84-55a2-42cd-99fa-b41c1ca64894',
5+
'Template Deprecated',
6+
E'Template ''{{.Labels.template}}'' has been deprecated',
7+
E'Hello {{.UserName}},\n\n'||
8+
E'The template **{{.Labels.template}}** has been deprecated with the following message:\n\n' ||
9+
E'**{{.Labels.message}}**\n\n' ||
10+
E'New workspaces may not be created from this template. Existing workspaces will continue to function normally.',
11+
'Template Events',
12+
'[
13+
{
14+
"label": "See affected workspaces",
15+
"url": "{{base_url}}/workspaces?filter=owner%3Ame+template%3A{{.Labels.template}}"
16+
},
17+
{
18+
"label": "View template",
19+
"url": "{{base_url}}/templates/{{.Labels.organization}}/{{.Labels.template}}"
20+
}
21+
]'::jsonb
22+
);

coderd/notifications/events.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ var (
3030

3131
// Template-related events.
3232
var (
33-
TemplateTemplateDeleted = uuid.MustParse("29a09665-2a4c-403f-9648-54301670e7be")
33+
TemplateTemplateDeleted = uuid.MustParse("29a09665-2a4c-403f-9648-54301670e7be")
34+
TemplateTemplateDeprecated = uuid.MustParse("f40fae84-55a2-42cd-99fa-b41c1ca64894")
3435

3536
TemplateWorkspaceBuildsFailedReport = uuid.MustParse("34a20db2-e9cc-4a93-b0e4-8569699d7a00")
3637
)

coderd/notifications/notifications_test.go

+14
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,20 @@ func TestNotificationTemplates_Golden(t *testing.T) {
10211021
appName: "Custom Application Name",
10221022
logoURL: "https://custom.application/logo.png",
10231023
},
1024+
{
1025+
name: "TemplateTemplateDeprecated",
1026+
id: notifications.TemplateTemplateDeprecated,
1027+
payload: types.MessagePayload{
1028+
UserName: "Bobby",
1029+
UserEmail: "[email protected]",
1030+
UserUsername: "bobby",
1031+
Labels: map[string]string{
1032+
"template": "alpha",
1033+
"message": "This template has been replaced by beta",
1034+
"organization": "coder",
1035+
},
1036+
},
1037+
},
10241038
}
10251039

10261040
// We must have a test case for every notification_template. This is enforced below:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
2+
3+
Subject: Template 'alpha' has been deprecated
4+
Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48
5+
Date: Fri, 11 Oct 2024 09:03:06 +0000
6+
Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
7+
MIME-Version: 1.0
8+
9+
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
10+
Content-Transfer-Encoding: quoted-printable
11+
Content-Type: text/plain; charset=UTF-8
12+
13+
Hello Bobby,
14+
15+
The template alpha has been deprecated with the following message:
16+
17+
This template has been replaced by beta
18+
19+
New workspaces may not be created from this template. Existing workspaces w=
20+
ill continue to function normally.
21+
22+
23+
See affected workspaces: http://test.com/workspaces?filter=3Downer%3Ame+tem=
24+
plate%3Aalpha
25+
26+
View template: http://test.com/templates/coder/alpha
27+
28+
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
29+
Content-Transfer-Encoding: quoted-printable
30+
Content-Type: text/html; charset=UTF-8
31+
32+
<!doctype html>
33+
<html lang=3D"en">
34+
<head>
35+
<meta charset=3D"UTF-8" />
36+
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
37+
=3D1.0" />
38+
<title>Template 'alpha' has been deprecated</title>
39+
</head>
40+
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
41+
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
42+
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
43+
; background: #f8fafc;">
44+
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
45+
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
46+
n: left; font-size: 14px; line-height: 1.5;">
47+
<div style=3D"text-align: center;">
48+
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
49+
er Logo" style=3D"height: 40px;" />
50+
</div>
51+
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
52+
argin: 8px 0 32px; line-height: 1.5;">
53+
Template 'alpha' has been deprecated
54+
</h1>
55+
<div style=3D"line-height: 1.5;">
56+
<p>Hello Bobby,</p>
57+
58+
<p>The template <strong>alpha</strong> has been deprecated with the followi=
59+
ng message:</p>
60+
61+
<p><strong>This template has been replaced by beta</strong></p>
62+
63+
<p>New workspaces may not be created from this template. Existing workspace=
64+
s will continue to function normally.</p>
65+
</div>
66+
<div style=3D"text-align: center; margin-top: 32px;">
67+
=20
68+
<a href=3D"http://test.com/workspaces?filter=3Downer%3Ame+template%=
69+
3Aalpha" style=3D"display: inline-block; padding: 13px 24px; background-col=
70+
or: #020617; color: #f8fafc; text-decoration: none; border-radius: 8px; mar=
71+
gin: 0 4px;">
72+
See affected workspaces
73+
</a>
74+
=20
75+
<a href=3D"http://test.com/templates/coder/alpha" style=3D"display:=
76+
inline-block; padding: 13px 24px; background-color: #020617; color: #f8faf=
77+
c; text-decoration: none; border-radius: 8px; margin: 0 4px;">
78+
View template
79+
</a>
80+
=20
81+
</div>
82+
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
83+
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
84+
<p>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a =
85+
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
86+
ttp://test.com</a></p>
87+
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
88+
r: #2563eb; text-decoration: none;">Click here to manage your notification =
89+
settings</a></p>
90+
<p><a href=3D"http://test.com/settings/notifications?disabled=3Df40=
91+
fae84-55a2-42cd-99fa-b41c1ca64894" style=3D"color: #2563eb; text-decoration=
92+
: none;">Stop receiving emails like this</a></p>
93+
</div>
94+
</div>
95+
</body>
96+
</html>
97+
98+
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"_version": "1.1",
3+
"msg_id": "00000000-0000-0000-0000-000000000000",
4+
"payload": {
5+
"_version": "1.1",
6+
"notification_name": "Template Deprecated",
7+
"notification_template_id": "00000000-0000-0000-0000-000000000000",
8+
"user_id": "00000000-0000-0000-0000-000000000000",
9+
"user_email": "[email protected]",
10+
"user_name": "Bobby",
11+
"user_username": "bobby",
12+
"actions": [
13+
{
14+
"label": "See affected workspaces",
15+
"url": "http://test.com/workspaces?filter=owner%3Ame+template%3Aalpha"
16+
},
17+
{
18+
"label": "View template",
19+
"url": "http://test.com/templates/coder/alpha"
20+
}
21+
],
22+
"labels": {
23+
"message": "This template has been replaced by beta",
24+
"organization": "coder",
25+
"template": "alpha"
26+
},
27+
"data": null
28+
},
29+
"title": "Template 'alpha' has been deprecated",
30+
"title_markdown": "Template 'alpha' has been deprecated",
31+
"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.",
32+
"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."
33+
}

coderd/templates.go

+42
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,12 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
845845
return
846846
}
847847

848+
if template.Deprecated != updated.Deprecated && updated.Deprecated != "" {
849+
if err := api.notifyUsersOfTemplateDeprecation(ctx, updated); err != nil {
850+
api.Logger.Error(ctx, "failed to notify users of template deprecation", slog.Error(err))
851+
}
852+
}
853+
848854
if updated.UpdatedAt.IsZero() {
849855
aReq.New = template
850856
rw.WriteHeader(http.StatusNotModified)
@@ -855,6 +861,42 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
855861
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(updated))
856862
}
857863

864+
func (api *API) notifyUsersOfTemplateDeprecation(ctx context.Context, template database.Template) error {
865+
workspaces, err := api.Database.GetWorkspaces(ctx, database.GetWorkspacesParams{
866+
TemplateIDs: []uuid.UUID{template.ID},
867+
})
868+
if err != nil {
869+
return xerrors.Errorf("get workspaces by template id: %w", err)
870+
}
871+
872+
users := make(map[uuid.UUID]struct{})
873+
for _, workspace := range workspaces {
874+
users[workspace.OwnerID] = struct{}{}
875+
}
876+
877+
errs := []error{}
878+
879+
for userID := range users {
880+
_, err = api.NotificationsEnqueuer.Enqueue(
881+
//nolint:gocritic // We need the system auth context to be able to send the deprecation notification.
882+
dbauthz.AsSystemRestricted(ctx),
883+
userID,
884+
notifications.TemplateTemplateDeprecated,
885+
map[string]string{
886+
"template": template.Name,
887+
"message": template.Deprecated,
888+
"organization": template.OrganizationName,
889+
},
890+
"notify-users-of-template-deprecation",
891+
)
892+
if err != nil {
893+
errs = append(errs, xerrors.Errorf("enqueue notification: %w", err))
894+
}
895+
}
896+
897+
return errors.Join(errs...)
898+
}
899+
858900
// @Summary Get template DAUs by ID
859901
// @ID get-template-daus-by-id
860902
// @Security CoderSessionToken

enterprise/coderd/templates_test.go

+43-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"net/http"
7+
"slices"
78
"testing"
89
"time"
910

@@ -38,21 +39,36 @@ func TestTemplates(t *testing.T) {
3839
t.Run("Deprecated", func(t *testing.T) {
3940
t.Parallel()
4041

42+
notifyEnq := &testutil.FakeNotificationsEnqueuer{}
4143
owner, user := coderdenttest.New(t, &coderdenttest.Options{
4244
Options: &coderdtest.Options{
4345
IncludeProvisionerDaemon: true,
46+
NotificationsEnqueuer: notifyEnq,
4447
},
4548
LicenseOptions: &coderdenttest.LicenseOptions{
4649
Features: license.Features{
4750
codersdk.FeatureAccessControl: 1,
4851
},
4952
},
5053
})
51-
client, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
54+
client, secondUser := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
55+
otherClient, otherUser := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
56+
5257
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
5358
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
5459
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
5560

61+
_ = coderdtest.CreateWorkspace(t, owner, template.ID)
62+
_ = coderdtest.CreateWorkspace(t, client, template.ID)
63+
64+
// Create another template for testing that users of another template do not
65+
// get a notification.
66+
secondVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
67+
secondTemplate := coderdtest.CreateTemplate(t, client, user.OrganizationID, secondVersion.ID)
68+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, secondVersion.ID)
69+
70+
_ = coderdtest.CreateWorkspace(t, otherClient, secondTemplate.ID)
71+
5672
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
5773
defer cancel()
5874

@@ -65,6 +81,32 @@ func TestTemplates(t *testing.T) {
6581
assert.True(t, updated.Deprecated)
6682
assert.NotEmpty(t, updated.DeprecationMessage)
6783

84+
notifs := []*testutil.Notification{}
85+
for _, notif := range notifyEnq.Sent {
86+
if notif.TemplateID == notifications.TemplateTemplateDeprecated {
87+
notifs = append(notifs, notif)
88+
}
89+
}
90+
require.Equal(t, 2, len(notifs))
91+
92+
expectedSentTo := []string{user.UserID.String(), secondUser.ID.String()}
93+
slices.Sort(expectedSentTo)
94+
95+
sentTo := []string{}
96+
for _, notif := range notifs {
97+
sentTo = append(sentTo, notif.UserID.String())
98+
}
99+
slices.Sort(sentTo)
100+
101+
// Require the notification to have only been sent to the expected users
102+
assert.Equal(t, expectedSentTo, sentTo)
103+
104+
// The previous check should verify this but we're double checking that
105+
// the notification wasn't sent to users not using the template.
106+
for _, notif := range notifs {
107+
assert.NotEqual(t, otherUser.ID, notif.UserID)
108+
}
109+
68110
_, err = client.CreateWorkspace(ctx, user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
69111
TemplateID: template.ID,
70112
Name: "foobar",

0 commit comments

Comments
 (0)