diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go
index 62bb8b2fd2ed2..c700773028d0a 100644
--- a/coderd/autobuild/lifecycle_executor_test.go
+++ b/coderd/autobuild/lifecycle_executor_test.go
@@ -274,7 +274,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
}
if tc.expectNotification {
- sent := enqueuer.Sent()
+ sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceAutoUpdated))
require.Len(t, sent, 1)
require.Equal(t, sent[0].UserID, workspace.OwnerID)
require.Contains(t, sent[0].Targets, workspace.TemplateID)
@@ -285,7 +285,8 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
require.Equal(t, "autobuild", sent[0].Labels["initiator"])
require.Equal(t, "autostart", sent[0].Labels["reason"])
} else {
- require.Empty(t, enqueuer.Sent())
+ sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceAutoUpdated))
+ require.Empty(t, sent)
}
})
}
diff --git a/coderd/database/migrations/000279_workspace_create_notification.down.sql b/coderd/database/migrations/000279_workspace_create_notification.down.sql
new file mode 100644
index 0000000000000..7780ca466386b
--- /dev/null
+++ b/coderd/database/migrations/000279_workspace_create_notification.down.sql
@@ -0,0 +1 @@
+DELETE FROM notification_templates WHERE id = '281fdf73-c6d6-4cbb-8ff5-888baf8a2fff';
diff --git a/coderd/database/migrations/000279_workspace_create_notification.up.sql b/coderd/database/migrations/000279_workspace_create_notification.up.sql
new file mode 100644
index 0000000000000..ca8678d4bcf5f
--- /dev/null
+++ b/coderd/database/migrations/000279_workspace_create_notification.up.sql
@@ -0,0 +1,16 @@
+INSERT INTO notification_templates
+ (id, name, title_template, body_template, "group", actions)
+VALUES (
+ '281fdf73-c6d6-4cbb-8ff5-888baf8a2fff',
+ 'Workspace Created',
+ E'Workspace ''{{.Labels.workspace}}'' has been created',
+ E'Hello {{.UserName}},\n\n'||
+ E'The workspace **{{.Labels.workspace}}** has been created from the template **{{.Labels.template}}** using version **{{.Labels.version}}**.',
+ 'Workspace Events',
+ '[
+ {
+ "label": "See workspace",
+ "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}"
+ }
+ ]'::jsonb
+);
diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go
index e33a85b523db2..12aecbaac74ae 100644
--- a/coderd/notifications/events.go
+++ b/coderd/notifications/events.go
@@ -7,6 +7,7 @@ import "github.com/google/uuid"
// Workspace-related events.
var (
+ TemplateWorkspaceCreated = uuid.MustParse("281fdf73-c6d6-4cbb-8ff5-888baf8a2fff")
TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed")
TemplateWorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9")
TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0")
diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go
index 22b8c654e631d..90cf1a46be28a 100644
--- a/coderd/notifications/notifications_test.go
+++ b/coderd/notifications/notifications_test.go
@@ -1034,6 +1034,20 @@ func TestNotificationTemplates_Golden(t *testing.T) {
},
},
},
+ {
+ name: "TemplateWorkspaceCreated",
+ id: notifications.TemplateWorkspaceCreated,
+ payload: types.MessagePayload{
+ UserName: "Bobby",
+ UserEmail: "bobby@coder.com",
+ UserUsername: "bobby",
+ Labels: map[string]string{
+ "workspace": "bobby-workspace",
+ "template": "bobby-template",
+ "version": "alpha",
+ },
+ },
+ },
}
// We must have a test case for every notification_template. This is enforced below:
diff --git a/coderd/notifications/notificationstest/fake_enqueuer.go b/coderd/notifications/notificationstest/fake_enqueuer.go
index 023137720998d..b26501cf492eb 100644
--- a/coderd/notifications/notificationstest/fake_enqueuer.go
+++ b/coderd/notifications/notificationstest/fake_enqueuer.go
@@ -92,8 +92,31 @@ func (f *FakeEnqueuer) Clear() {
f.sent = nil
}
-func (f *FakeEnqueuer) Sent() []*FakeNotification {
+func (f *FakeEnqueuer) Sent(matchers ...func(*FakeNotification) bool) []*FakeNotification {
f.mu.Lock()
defer f.mu.Unlock()
- return append([]*FakeNotification{}, f.sent...)
+
+ sent := []*FakeNotification{}
+ for _, notif := range f.sent {
+ // Check this notification matches all given matchers
+ matches := true
+ for _, matcher := range matchers {
+ if !matcher(notif) {
+ matches = false
+ break
+ }
+ }
+
+ if matches {
+ sent = append(sent, notif)
+ }
+ }
+
+ return sent
+}
+
+func WithTemplateID(id uuid.UUID) func(*FakeNotification) bool {
+ return func(n *FakeNotification) bool {
+ return n.TemplateID == id
+ }
}
diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceCreated.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceCreated.html.golden
new file mode 100644
index 0000000000000..000b2a71ac77b
--- /dev/null
+++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceCreated.html.golden
@@ -0,0 +1,80 @@
+From: system@coder.com
+To: bobby@coder.com
+Subject: Workspace 'bobby-workspace' has been created
+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 workspace bobby-workspace has been created from the template bobby-temp=
+late using version alpha.
+
+
+See workspace: http://test.com/@bobby/bobby-workspace
+
+--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/html; charset=UTF-8
+
+
+
+
+
+
+ Workspace 'bobby-workspace' has been created
+
+
+
+
+

+
+
+ Workspace 'bobby-workspace' has been created
+
+
+
Hello Bobby,
+
+
The workspace bobby-workspace has been created from the=
+ template bobby-template using version alpha.
+
+
+
+
+
+
+
+--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceCreated.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceCreated.json.golden
new file mode 100644
index 0000000000000..46354c4ffeef9
--- /dev/null
+++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceCreated.json.golden
@@ -0,0 +1,29 @@
+{
+ "_version": "1.1",
+ "msg_id": "00000000-0000-0000-0000-000000000000",
+ "payload": {
+ "_version": "1.1",
+ "notification_name": "Workspace Created",
+ "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 workspace",
+ "url": "http://test.com/@bobby/bobby-workspace"
+ }
+ ],
+ "labels": {
+ "template": "bobby-template",
+ "version": "alpha",
+ "workspace": "bobby-workspace"
+ },
+ "data": null
+ },
+ "title": "Workspace 'bobby-workspace' has been created",
+ "title_markdown": "Workspace 'bobby-workspace' has been created",
+ "body": "Hello Bobby,\n\nThe workspace bobby-workspace has been created from the template bobby-template using version alpha.",
+ "body_markdown": "Hello Bobby,\n\nThe workspace **bobby-workspace** has been created from the template **bobby-template** using version **alpha**."
+}
\ No newline at end of file
diff --git a/coderd/workspaces.go b/coderd/workspaces.go
index 0234c31888d41..19fb1ec1ce810 100644
--- a/coderd/workspaces.go
+++ b/coderd/workspaces.go
@@ -666,6 +666,8 @@ func createWorkspace(
return err
}, nil)
+ api.notifyWorkspaceCreated(ctx, workspace, req.RichParameterValues)
+
var bldErr wsbuilder.BuildError
if xerrors.As(err, &bldErr) {
httpapi.Write(ctx, rw, bldErr.Status, codersdk.Response{
@@ -735,6 +737,64 @@ func createWorkspace(
httpapi.Write(ctx, rw, http.StatusCreated, w)
}
+func (api *API) notifyWorkspaceCreated(
+ ctx context.Context,
+ workspace database.Workspace,
+ parameters []codersdk.WorkspaceBuildParameter,
+) {
+ log := api.Logger.With(slog.F("workspace_id", workspace.ID))
+
+ template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
+ if err != nil {
+ log.Warn(ctx, "failed to fetch template for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err))
+ return
+ }
+
+ owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID)
+ if err != nil {
+ log.Warn(ctx, "failed to fetch user for workspace creation notification", slog.F("owner_id", workspace.OwnerID), slog.Error(err))
+ return
+ }
+
+ version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID)
+ if err != nil {
+ log.Warn(ctx, "failed to fetch template version for workspace creation notification", slog.F("template_version_id", template.ActiveVersionID), slog.Error(err))
+ return
+ }
+
+ buildParameters := make([]map[string]any, len(parameters))
+ for idx, parameter := range parameters {
+ buildParameters[idx] = map[string]any{
+ "name": parameter.Name,
+ "value": parameter.Value,
+ }
+ }
+
+ if _, err := api.NotificationsEnqueuer.EnqueueWithData(
+ // nolint:gocritic // Need notifier actor to enqueue notifications
+ dbauthz.AsNotifier(ctx),
+ workspace.OwnerID,
+ notifications.TemplateWorkspaceCreated,
+ map[string]string{
+ "workspace": workspace.Name,
+ "template": template.Name,
+ "version": version.Name,
+ },
+ map[string]any{
+ "workspace": map[string]any{"id": workspace.ID, "name": workspace.Name},
+ "template": map[string]any{"id": template.ID, "name": template.Name},
+ "template_version": map[string]any{"id": version.ID, "name": version.Name},
+ "owner": map[string]any{"id": owner.ID, "name": owner.Name},
+ "parameters": buildParameters,
+ },
+ "api-workspaces-create",
+ // Associate this notification with all the related entities
+ workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID,
+ ); err != nil {
+ log.Warn(ctx, "failed to notify of workspace creation", slog.Error(err))
+ }
+}
+
// @Summary Update workspace metadata by ID
// @ID update-workspace-metadata-by-id
// @Security CoderSessionToken
diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go
index 6a2856dcbbe76..d6e365011b929 100644
--- a/coderd/workspaces_test.go
+++ b/coderd/workspaces_test.go
@@ -571,6 +571,59 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
+ t.Run("CreateSendsNotification", func(t *testing.T) {
+ t.Parallel()
+
+ enqueuer := notificationstest.FakeEnqueuer{}
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: &enqueuer})
+ user := coderdtest.CreateFirstUser(t, client)
+ memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
+
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+
+ workspace := coderdtest.CreateWorkspace(t, memberClient, template.ID)
+ coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace.LatestBuild.ID)
+
+ sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceCreated))
+ require.Len(t, sent, 1)
+ require.Equal(t, memberUser.ID, sent[0].UserID)
+ require.Contains(t, sent[0].Targets, template.ID)
+ require.Contains(t, sent[0].Targets, workspace.ID)
+ require.Contains(t, sent[0].Targets, workspace.OrganizationID)
+ require.Contains(t, sent[0].Targets, workspace.OwnerID)
+ })
+
+ t.Run("CreateSendsNotificationToCorrectUser", func(t *testing.T) {
+ t.Parallel()
+
+ enqueuer := notificationstest.FakeEnqueuer{}
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: &enqueuer})
+ user := coderdtest.CreateFirstUser(t, client)
+ _, memberUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
+
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+
+ ctx := testutil.Context(t, testutil.WaitShort)
+ workspace, err := client.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{
+ TemplateID: template.ID,
+ Name: coderdtest.RandomUsername(t),
+ })
+ require.NoError(t, err)
+ coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
+
+ sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceCreated))
+ require.Len(t, sent, 1)
+ require.Equal(t, memberUser.ID, sent[0].UserID)
+ require.Contains(t, sent[0].Targets, template.ID)
+ require.Contains(t, sent[0].Targets, workspace.ID)
+ require.Contains(t, sent[0].Targets, workspace.OrganizationID)
+ require.Contains(t, sent[0].Targets, workspace.OwnerID)
+ })
+
t.Run("CreateWithAuditLogs", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
@@ -3596,15 +3649,14 @@ func TestWorkspaceNotifications(t *testing.T) {
// Then
require.NoError(t, err, "mark workspace as dormant")
- sent := notifyEnq.Sent()
- require.Len(t, sent, 2)
- // notifyEnq.Sent[0] is an event for created user account
- require.Equal(t, sent[1].TemplateID, notifications.TemplateWorkspaceDormant)
- require.Equal(t, sent[1].UserID, workspace.OwnerID)
- require.Contains(t, sent[1].Targets, template.ID)
- require.Contains(t, sent[1].Targets, workspace.ID)
- require.Contains(t, sent[1].Targets, workspace.OrganizationID)
- require.Contains(t, sent[1].Targets, workspace.OwnerID)
+ sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceDormant))
+ require.Len(t, sent, 1)
+ require.Equal(t, sent[0].TemplateID, notifications.TemplateWorkspaceDormant)
+ require.Equal(t, sent[0].UserID, workspace.OwnerID)
+ require.Contains(t, sent[0].Targets, template.ID)
+ require.Contains(t, sent[0].Targets, workspace.ID)
+ require.Contains(t, sent[0].Targets, workspace.OrganizationID)
+ require.Contains(t, sent[0].Targets, workspace.OwnerID)
})
t.Run("InitiatorIsOwner", func(t *testing.T) {
@@ -3635,7 +3687,7 @@ func TestWorkspaceNotifications(t *testing.T) {
// Then
require.NoError(t, err, "mark workspace as dormant")
- require.Len(t, notifyEnq.Sent(), 0)
+ require.Len(t, notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceDormant)), 0)
})
t.Run("ActivateDormantWorkspace", func(t *testing.T) {
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