diff --git a/coderd/database/migrations/000270_template_deprecation_notification.down.sql b/coderd/database/migrations/000270_template_deprecation_notification.down.sql
new file mode 100644
index 0000000000000..b3f9abc0133bd
--- /dev/null
+++ b/coderd/database/migrations/000270_template_deprecation_notification.down.sql
@@ -0,0 +1 @@
+DELETE FROM notification_templates WHERE id = 'f40fae84-55a2-42cd-99fa-b41c1ca64894';
diff --git a/coderd/database/migrations/000270_template_deprecation_notification.up.sql b/coderd/database/migrations/000270_template_deprecation_notification.up.sql
new file mode 100644
index 0000000000000..e98f852c8b4e1
--- /dev/null
+++ b/coderd/database/migrations/000270_template_deprecation_notification.up.sql
@@ -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
+);
diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go
index c2e0f442e0623..e33a85b523db2 100644
--- a/coderd/notifications/events.go
+++ b/coderd/notifications/events.go
@@ -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")
)
diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go
index 4a6978b5024fe..86ed14fe90957 100644
--- a/coderd/notifications/notifications_test.go
+++ b/coderd/notifications/notifications_test.go
@@ -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:
diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateTemplateDeprecated.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTemplateDeprecated.html.golden
new file mode 100644
index 0000000000000..1393acc4bc60a
--- /dev/null
+++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTemplateDeprecated.html.golden
@@ -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
+
+
+
+
+
+
+ Template 'alpha' has been deprecated
+
+
+
+
+

+
+
+ Template 'alpha' has been deprecated
+
+
+
Hello Bobby,
+
+
The template alpha has been deprecated with the followi=
+ng message:
+
+
This template has been replaced by beta
+
+
New workspaces may not be created from this template. Existing workspace=
+s will continue to function normally.
+
+
+
+
+
+
+
+--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateTemplateDeprecated.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTemplateDeprecated.json.golden
new file mode 100644
index 0000000000000..c4202271c5257
--- /dev/null
+++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTemplateDeprecated.json.golden
@@ -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."
+}
\ No newline at end of file
diff --git a/coderd/templates.go b/coderd/templates.go
index 907a4d1265836..cbc6eb784d2e4 100644
--- a/coderd/templates.go
+++ b/coderd/templates.go
@@ -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
diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go
index 5d9cb8ee9fa35..cde01553e349c 100644
--- a/enterprise/coderd/templates_test.go
+++ b/enterprise/coderd/templates_test.go
@@ -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",
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy