Skip to content

Commit bfdaefd

Browse files
committed
merge
2 parents f97a385 + 809c611 commit bfdaefd

File tree

90 files changed

+1289
-937
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+1289
-937
lines changed

coderd/autobuild/lifecycle_executor_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
274274
}
275275

276276
if tc.expectNotification {
277-
sent := enqueuer.Sent()
277+
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceAutoUpdated))
278278
require.Len(t, sent, 1)
279279
require.Equal(t, sent[0].UserID, workspace.OwnerID)
280280
require.Contains(t, sent[0].Targets, workspace.TemplateID)
@@ -285,7 +285,8 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
285285
require.Equal(t, "autobuild", sent[0].Labels["initiator"])
286286
require.Equal(t, "autostart", sent[0].Labels["reason"])
287287
} else {
288-
require.Empty(t, enqueuer.Sent())
288+
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceAutoUpdated))
289+
require.Empty(t, sent)
289290
}
290291
})
291292
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DELETE FROM notification_templates WHERE id = '281fdf73-c6d6-4cbb-8ff5-888baf8a2fff';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
INSERT INTO notification_templates
2+
(id, name, title_template, body_template, "group", actions)
3+
VALUES (
4+
'281fdf73-c6d6-4cbb-8ff5-888baf8a2fff',
5+
'Workspace Created',
6+
E'Workspace ''{{.Labels.workspace}}'' has been created',
7+
E'Hello {{.UserName}},\n\n'||
8+
E'The workspace **{{.Labels.workspace}}** has been created from the template **{{.Labels.template}}** using version **{{.Labels.version}}**.',
9+
'Workspace Events',
10+
'[
11+
{
12+
"label": "See workspace",
13+
"url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}"
14+
}
15+
]'::jsonb
16+
);

coderd/notifications/events.go

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import "github.com/google/uuid"
77

88
// Workspace-related events.
99
var (
10+
TemplateWorkspaceCreated = uuid.MustParse("281fdf73-c6d6-4cbb-8ff5-888baf8a2fff")
1011
TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed")
1112
TemplateWorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9")
1213
TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0")

coderd/notifications/notifications_test.go

+14
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,20 @@ func TestNotificationTemplates_Golden(t *testing.T) {
10341034
},
10351035
},
10361036
},
1037+
{
1038+
name: "TemplateWorkspaceCreated",
1039+
id: notifications.TemplateWorkspaceCreated,
1040+
payload: types.MessagePayload{
1041+
UserName: "Bobby",
1042+
UserEmail: "bobby@coder.com",
1043+
UserUsername: "bobby",
1044+
Labels: map[string]string{
1045+
"workspace": "bobby-workspace",
1046+
"template": "bobby-template",
1047+
"version": "alpha",
1048+
},
1049+
},
1050+
},
10371051
}
10381052

10391053
// We must have a test case for every notification_template. This is enforced below:

coderd/notifications/notificationstest/fake_enqueuer.go

+25-2
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,31 @@ func (f *FakeEnqueuer) Clear() {
9292
f.sent = nil
9393
}
9494

95-
func (f *FakeEnqueuer) Sent() []*FakeNotification {
95+
func (f *FakeEnqueuer) Sent(matchers ...func(*FakeNotification) bool) []*FakeNotification {
9696
f.mu.Lock()
9797
defer f.mu.Unlock()
98-
return append([]*FakeNotification{}, f.sent...)
98+
99+
sent := []*FakeNotification{}
100+
for _, notif := range f.sent {
101+
// Check this notification matches all given matchers
102+
matches := true
103+
for _, matcher := range matchers {
104+
if !matcher(notif) {
105+
matches = false
106+
break
107+
}
108+
}
109+
110+
if matches {
111+
sent = append(sent, notif)
112+
}
113+
}
114+
115+
return sent
116+
}
117+
118+
func WithTemplateID(id uuid.UUID) func(*FakeNotification) bool {
119+
return func(n *FakeNotification) bool {
120+
return n.TemplateID == id
121+
}
99122
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
From: system@coder.com
2+
To: bobby@coder.com
3+
Subject: Workspace 'bobby-workspace' has been created
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 workspace bobby-workspace has been created from the template bobby-temp=
16+
late using version alpha.
17+
18+
19+
See workspace: http://test.com/@bobby/bobby-workspace
20+
21+
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
22+
Content-Transfer-Encoding: quoted-printable
23+
Content-Type: text/html; charset=UTF-8
24+
25+
<!doctype html>
26+
<html lang=3D"en">
27+
<head>
28+
<meta charset=3D"UTF-8" />
29+
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
30+
=3D1.0" />
31+
<title>Workspace 'bobby-workspace' has been created</title>
32+
</head>
33+
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
34+
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
35+
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
36+
; background: #f8fafc;">
37+
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
38+
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
39+
n: left; font-size: 14px; line-height: 1.5;">
40+
<div style=3D"text-align: center;">
41+
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
42+
er Logo" style=3D"height: 40px;" />
43+
</div>
44+
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
45+
argin: 8px 0 32px; line-height: 1.5;">
46+
Workspace 'bobby-workspace' has been created
47+
</h1>
48+
<div style=3D"line-height: 1.5;">
49+
<p>Hello Bobby,</p>
50+
51+
<p>The workspace <strong>bobby-workspace</strong> has been created from the=
52+
template <strong>bobby-template</strong> using version <strong>alpha</stro=
53+
ng>.</p>
54+
</div>
55+
<div style=3D"text-align: center; margin-top: 32px;">
56+
=20
57+
<a href=3D"http://test.com/@bobby/bobby-workspace" style=3D"display=
58+
: inline-block; padding: 13px 24px; background-color: #020617; color: #f8fa=
59+
fc; text-decoration: none; border-radius: 8px; margin: 0 4px;">
60+
See workspace
61+
</a>
62+
=20
63+
</div>
64+
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
65+
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
66+
<p>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a =
67+
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
68+
ttp://test.com</a></p>
69+
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
70+
r: #2563eb; text-decoration: none;">Click here to manage your notification =
71+
settings</a></p>
72+
<p><a href=3D"http://test.com/settings/notifications?disabled=3D281=
73+
fdf73-c6d6-4cbb-8ff5-888baf8a2fff" style=3D"color: #2563eb; text-decoration=
74+
: none;">Stop receiving emails like this</a></p>
75+
</div>
76+
</div>
77+
</body>
78+
</html>
79+
80+
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"_version": "1.1",
3+
"msg_id": "00000000-0000-0000-0000-000000000000",
4+
"payload": {
5+
"_version": "1.1",
6+
"notification_name": "Workspace Created",
7+
"notification_template_id": "00000000-0000-0000-0000-000000000000",
8+
"user_id": "00000000-0000-0000-0000-000000000000",
9+
"user_email": "bobby@coder.com",
10+
"user_name": "Bobby",
11+
"user_username": "bobby",
12+
"actions": [
13+
{
14+
"label": "See workspace",
15+
"url": "http://test.com/@bobby/bobby-workspace"
16+
}
17+
],
18+
"labels": {
19+
"template": "bobby-template",
20+
"version": "alpha",
21+
"workspace": "bobby-workspace"
22+
},
23+
"data": null
24+
},
25+
"title": "Workspace 'bobby-workspace' has been created",
26+
"title_markdown": "Workspace 'bobby-workspace' has been created",
27+
"body": "Hello Bobby,\n\nThe workspace bobby-workspace has been created from the template bobby-template using version alpha.",
28+
"body_markdown": "Hello Bobby,\n\nThe workspace **bobby-workspace** has been created from the template **bobby-template** using version **alpha**."
29+
}

coderd/workspaces.go

+60
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,8 @@ func createWorkspace(
666666
return err
667667
}, nil)
668668

669+
api.notifyWorkspaceCreated(ctx, workspace, req.RichParameterValues)
670+
669671
var bldErr wsbuilder.BuildError
670672
if xerrors.As(err, &bldErr) {
671673
httpapi.Write(ctx, rw, bldErr.Status, codersdk.Response{
@@ -735,6 +737,64 @@ func createWorkspace(
735737
httpapi.Write(ctx, rw, http.StatusCreated, w)
736738
}
737739

740+
func (api *API) notifyWorkspaceCreated(
741+
ctx context.Context,
742+
workspace database.Workspace,
743+
parameters []codersdk.WorkspaceBuildParameter,
744+
) {
745+
log := api.Logger.With(slog.F("workspace_id", workspace.ID))
746+
747+
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
748+
if err != nil {
749+
log.Warn(ctx, "failed to fetch template for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err))
750+
return
751+
}
752+
753+
owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID)
754+
if err != nil {
755+
log.Warn(ctx, "failed to fetch user for workspace creation notification", slog.F("owner_id", workspace.OwnerID), slog.Error(err))
756+
return
757+
}
758+
759+
version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID)
760+
if err != nil {
761+
log.Warn(ctx, "failed to fetch template version for workspace creation notification", slog.F("template_version_id", template.ActiveVersionID), slog.Error(err))
762+
return
763+
}
764+
765+
buildParameters := make([]map[string]any, len(parameters))
766+
for idx, parameter := range parameters {
767+
buildParameters[idx] = map[string]any{
768+
"name": parameter.Name,
769+
"value": parameter.Value,
770+
}
771+
}
772+
773+
if _, err := api.NotificationsEnqueuer.EnqueueWithData(
774+
// nolint:gocritic // Need notifier actor to enqueue notifications
775+
dbauthz.AsNotifier(ctx),
776+
workspace.OwnerID,
777+
notifications.TemplateWorkspaceCreated,
778+
map[string]string{
779+
"workspace": workspace.Name,
780+
"template": template.Name,
781+
"version": version.Name,
782+
},
783+
map[string]any{
784+
"workspace": map[string]any{"id": workspace.ID, "name": workspace.Name},
785+
"template": map[string]any{"id": template.ID, "name": template.Name},
786+
"template_version": map[string]any{"id": version.ID, "name": version.Name},
787+
"owner": map[string]any{"id": owner.ID, "name": owner.Name},
788+
"parameters": buildParameters,
789+
},
790+
"api-workspaces-create",
791+
// Associate this notification with all the related entities
792+
workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID,
793+
); err != nil {
794+
log.Warn(ctx, "failed to notify of workspace creation", slog.Error(err))
795+
}
796+
}
797+
738798
// @Summary Update workspace metadata by ID
739799
// @ID update-workspace-metadata-by-id
740800
// @Security CoderSessionToken

coderd/workspaces_test.go

+62-10
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,59 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
571571
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
572572
})
573573

574+
t.Run("CreateSendsNotification", func(t *testing.T) {
575+
t.Parallel()
576+
577+
enqueuer := notificationstest.FakeEnqueuer{}
578+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: &enqueuer})
579+
user := coderdtest.CreateFirstUser(t, client)
580+
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
581+
582+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
583+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
584+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
585+
586+
workspace := coderdtest.CreateWorkspace(t, memberClient, template.ID)
587+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace.LatestBuild.ID)
588+
589+
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceCreated))
590+
require.Len(t, sent, 1)
591+
require.Equal(t, memberUser.ID, sent[0].UserID)
592+
require.Contains(t, sent[0].Targets, template.ID)
593+
require.Contains(t, sent[0].Targets, workspace.ID)
594+
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
595+
require.Contains(t, sent[0].Targets, workspace.OwnerID)
596+
})
597+
598+
t.Run("CreateSendsNotificationToCorrectUser", func(t *testing.T) {
599+
t.Parallel()
600+
601+
enqueuer := notificationstest.FakeEnqueuer{}
602+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: &enqueuer})
603+
user := coderdtest.CreateFirstUser(t, client)
604+
_, memberUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
605+
606+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
607+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
608+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
609+
610+
ctx := testutil.Context(t, testutil.WaitShort)
611+
workspace, err := client.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{
612+
TemplateID: template.ID,
613+
Name: coderdtest.RandomUsername(t),
614+
})
615+
require.NoError(t, err)
616+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
617+
618+
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceCreated))
619+
require.Len(t, sent, 1)
620+
require.Equal(t, memberUser.ID, sent[0].UserID)
621+
require.Contains(t, sent[0].Targets, template.ID)
622+
require.Contains(t, sent[0].Targets, workspace.ID)
623+
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
624+
require.Contains(t, sent[0].Targets, workspace.OwnerID)
625+
})
626+
574627
t.Run("CreateWithAuditLogs", func(t *testing.T) {
575628
t.Parallel()
576629
auditor := audit.NewMock()
@@ -3596,15 +3649,14 @@ func TestWorkspaceNotifications(t *testing.T) {
35963649

35973650
// Then
35983651
require.NoError(t, err, "mark workspace as dormant")
3599-
sent := notifyEnq.Sent()
3600-
require.Len(t, sent, 2)
3601-
// notifyEnq.Sent[0] is an event for created user account
3602-
require.Equal(t, sent[1].TemplateID, notifications.TemplateWorkspaceDormant)
3603-
require.Equal(t, sent[1].UserID, workspace.OwnerID)
3604-
require.Contains(t, sent[1].Targets, template.ID)
3605-
require.Contains(t, sent[1].Targets, workspace.ID)
3606-
require.Contains(t, sent[1].Targets, workspace.OrganizationID)
3607-
require.Contains(t, sent[1].Targets, workspace.OwnerID)
3652+
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceDormant))
3653+
require.Len(t, sent, 1)
3654+
require.Equal(t, sent[0].TemplateID, notifications.TemplateWorkspaceDormant)
3655+
require.Equal(t, sent[0].UserID, workspace.OwnerID)
3656+
require.Contains(t, sent[0].Targets, template.ID)
3657+
require.Contains(t, sent[0].Targets, workspace.ID)
3658+
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
3659+
require.Contains(t, sent[0].Targets, workspace.OwnerID)
36083660
})
36093661

36103662
t.Run("InitiatorIsOwner", func(t *testing.T) {
@@ -3635,7 +3687,7 @@ func TestWorkspaceNotifications(t *testing.T) {
36353687

36363688
// Then
36373689
require.NoError(t, err, "mark workspace as dormant")
3638-
require.Len(t, notifyEnq.Sent(), 0)
3690+
require.Len(t, notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceDormant)), 0)
36393691
})
36403692

36413693
t.Run("ActivateDormantWorkspace", func(t *testing.T) {

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ replace github.com/dlclark/regexp2 => github.com/dlclark/regexp2 v1.7.0
3939

4040
// There are a few minor changes we make to Tailscale that we're slowly upstreaming. Compare here:
4141
// https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main
42-
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20241203112101-49a2c1225cce
42+
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20241218201526-b53d914d625f
4343

4444
// This is replaced to include
4545
// 1. a fix for a data race: c.f. https://github.com/tailscale/wireguard-go/pull/25

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,8 @@ github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM=
223223
github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q=
224224
github.com/coder/ssh v0.0.0-20231128192721-70855dedb788 h1:YoUSJ19E8AtuUFVYBpXuOD6a/zVP3rcxezNsoDseTUw=
225225
github.com/coder/ssh v0.0.0-20231128192721-70855dedb788/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ=
226-
github.com/coder/tailscale v1.1.1-0.20241203112101-49a2c1225cce h1:PzzTHX6p/LHM0NITk8VYH4vnKD5LCyWea6TJznLoTrQ=
227-
github.com/coder/tailscale v1.1.1-0.20241203112101-49a2c1225cce/go.mod h1:sYb47ejBHSeJsD5YYU0Sv/YU+cC8tmzK1nlZ2MDH08A=
226+
github.com/coder/tailscale v1.1.1-0.20241218201526-b53d914d625f h1:CctU+8mmHp/Y/cteK/bMJCUfe7c6gDIy3TJGaHaxrbU=
227+
github.com/coder/tailscale v1.1.1-0.20241218201526-b53d914d625f/go.mod h1:LOne094of6xzi3PdF+WyhPvKjK5zVuGADQ8WP46iIrM=
228228
github.com/coder/terraform-provider-coder v1.0.4 h1:MJldCvykIQzzqBVUDjCJpPyqvKelAAHrtJKfIIx4Qxo=
229229
github.com/coder/terraform-provider-coder v1.0.4/go.mod h1:dQ1e/IccUxnmh/1bXTA3PopSoBkHMyWT6EkdBw8Lx6Y=
230230
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=

0 commit comments

Comments
 (0)