Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: notify on workspace creation#15934

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
DanielleMaywood merged 6 commits intomainfromdm-notify-on-workspace-creation
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletionscoderd/autobuild/lifecycle_executor_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
Expand All@@ -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)
}
})
}
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
DELETE FROM notification_templates WHERE id = '281fdf73-c6d6-4cbb-8ff5-888baf8a2fff';
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -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
);
1 change: 1 addition & 0 deletionscoderd/notifications/events.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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")
Expand Down
14 changes: 14 additions & 0 deletionscoderd/notifications/notifications_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
27 changes: 25 additions & 2 deletionscoderd/notifications/notificationstest/fake_enqueuer.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
}
Comment on lines +118 to +121
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Nice :-)

}
Original file line numberDiff line numberDiff line change
@@ -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

<!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>Workspace 'bobby-workspace' has been created</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;">
Workspace 'bobby-workspace' has been created
</h1>
<div style=3D"line-height: 1.5;">
<p>Hello Bobby,</p>

<p>The workspace <strong>bobby-workspace</strong> has been created from the=
template <strong>bobby-template</strong> using version <strong>alpha</stro=
ng>.</p>
</div>
<div style=3D"text-align: center; margin-top: 32px;">
=20
<a href=3D"http://test.com/@bobby/bobby-workspace" style=3D"display=
: inline-block; padding: 13px 24px; background-color: #020617; color: #f8fa=
fc; text-decoration: none; border-radius: 8px; margin: 0 4px;">
See workspace
</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>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<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=3D281=
fdf73-c6d6-4cbb-8ff5-888baf8a2fff" style=3D"color: #2563eb; text-decoration=
: none;">Stop receiving emails like this</a></p>
</div>
</div>
</body>
</html>

--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
Original file line numberDiff line numberDiff line change
@@ -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**."
}
60 changes: 60 additions & 0 deletionscoderd/workspaces.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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{
Expand DownExpand Up@@ -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
Expand Down
72 changes: 62 additions & 10 deletionscoderd/workspaces_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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()
Expand DownExpand Up@@ -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) {
Expand DownExpand Up@@ -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) {
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp