Skip to content
Merged
1 change: 1 addition & 0 deletions coderd/database/migrations/000249_email_reports.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM notification_templates WHERE id = 'b02ddd82-4733-4d02-a2d7-c36f3598997d';
20 changes: 20 additions & 0 deletions coderd/database/migrations/000249_email_reports.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions)
VALUES ('bc0d9b9c-6dca-40a7-a209-fb2681e3341a', 'Report: Workspace Builds Failed For Template', E'Workspace builds failed for template "{{.Labels.template_display_name}}"',
E'Hi {{.UserName}},

Template **{{.Labels.template_display_name}}** has failed to build {{.Data.failed_builds}}/{{.Data.total_builds}} times over the last {{.Data.report_frequency}} and may be unstable.

**Report:**
{{range $version := .Data.template_versions}}
**{{$version.template_version_name}}** failed {{$version.failed_count}} time{{if gt $version.failed_count 1}}s{{end}}:
{{range $build := $version.failed_builds}}
* [{{$build.workspace_owner_username}} / {{$build.workspace_name}} / #{{$build.build_number}}]({{base_url}}/@{{$build.workspace_owner_username}}/{{$build.workspace_name}}/builds/{{$build.build_number}})
{{- end}}
{{end}}
We recommend reviewing these issues to ensure future builds are successful.',
'Template Events', '[
{
"label": "View workspaces",
"url": "{{ base_url }}/workspaces?filter=template%3A{{.Labels.template_name}}"
}
]'::jsonb);
2 changes: 2 additions & 0 deletions coderd/notifications/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ var (
// Template-related events.
var (
TemplateTemplateDeleted = uuid.MustParse("29a09665-2a4c-403f-9648-54301670e7be")

TemplateWorkspaceBuildSummary = uuid.MustParse("bc0d9b9c-6dca-40a7-a209-fb2681e3341a")
)
105 changes: 90 additions & 15 deletions coderd/notifications/notifications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import (
"context"
_ "embed"
"encoding/json"
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"sync"
"sync/atomic"
"testing"
Expand Down Expand Up @@ -46,6 +50,9 @@ import (
"github.com/coder/coder/v2/testutil"
)

// updateGoldenFiles is a flag that can be set to update golden files.
var updateGoldenFiles = flag.Bool("update", false, "Update golden files")

func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
Expand Down Expand Up @@ -693,7 +700,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateWorkspaceDeleted",
id: notifications.TemplateWorkspaceDeleted,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"name": "bobby-workspace",
"reason": "autodeleted due to dormancy",
Expand All @@ -705,7 +712,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateWorkspaceAutobuildFailed",
id: notifications.TemplateWorkspaceAutobuildFailed,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"name": "bobby-workspace",
"reason": "autostart",
Expand All @@ -716,7 +723,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateWorkspaceDormant",
id: notifications.TemplateWorkspaceDormant,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"name": "bobby-workspace",
"reason": "breached the template's threshold for inactivity",
Expand All @@ -730,7 +737,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateWorkspaceAutoUpdated",
id: notifications.TemplateWorkspaceAutoUpdated,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"name": "bobby-workspace",
"template_version_name": "1.0",
Expand All @@ -742,7 +749,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateWorkspaceMarkedForDeletion",
id: notifications.TemplateWorkspaceMarkedForDeletion,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"name": "bobby-workspace",
"reason": "template updated to new dormancy policy",
Expand All @@ -755,7 +762,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateUserAccountCreated",
id: notifications.TemplateUserAccountCreated,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"created_account_name": "bobby",
},
Expand All @@ -765,7 +772,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateUserAccountDeleted",
id: notifications.TemplateUserAccountDeleted,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"deleted_account_name": "bobby",
},
Expand All @@ -775,7 +782,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateUserAccountSuspended",
id: notifications.TemplateUserAccountSuspended,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"suspended_account_name": "bobby",
},
Expand All @@ -785,7 +792,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateUserAccountActivated",
id: notifications.TemplateUserAccountActivated,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"activated_account_name": "bobby",
},
Expand All @@ -795,7 +802,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateYourAccountSuspended",
id: notifications.TemplateYourAccountSuspended,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"suspended_account_name": "bobby",
},
Expand All @@ -805,7 +812,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateYourAccountActivated",
id: notifications.TemplateYourAccountActivated,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"activated_account_name": "bobby",
},
Expand All @@ -815,7 +822,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateTemplateDeleted",
id: notifications.TemplateTemplateDeleted,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"name": "bobby-template",
"initiator": "rob",
Expand All @@ -826,7 +833,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
name: "TemplateWorkspaceManualBuildFailed",
id: notifications.TemplateWorkspaceManualBuildFailed,
payload: types.MessagePayload{
UserName: "bobby",
UserName: "Bobby",
Labels: map[string]string{
"name": "bobby-workspace",
"template_name": "bobby-template",
Expand All @@ -837,6 +844,56 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
},
},
},
{
name: "TemplateWorkspaceBuildSummary",
id: notifications.TemplateWorkspaceBuildSummary,
payload: types.MessagePayload{
UserName: "Bobby",
Labels: map[string]string{
"template_name": "bobby-first-template",
"template_display_name": "Bobby First Template",
},
Data: map[string]any{
"failed_builds": 4,
"total_builds": 55,
"report_frequency": "week",
"template_versions": []map[string]any{
{
"template_version_name": "bobby-template-version-1",
"failed_count": 3,
"failed_builds": []map[string]any{
{
"workspace_owner_username": "mtojek",
"workspace_name": "workspace-1",
"build_number": 1234,
},
{
"workspace_owner_username": "johndoe",
"workspace_name": "my-workspace-3",
"build_number": 5678,
},
{
"workspace_owner_username": "jack",
"workspace_name": "workwork",
"build_number": 774,
},
},
},
{
"template_version_name": "bobby-template-version-2",
"failed_count": 1,
"failed_builds": []map[string]any{
{
"workspace_owner_username": "ben",
"workspace_name": "cool-workspace",
"build_number": 8888,
},
},
},
},
},
},
},
}

allTemplates, err := enumerateAllTemplates(t)
Expand Down Expand Up @@ -869,14 +926,32 @@ func TestNotificationTemplatesCanRender(t *testing.T) {
Scan(&titleTmpl, &bodyTmpl)
require.NoError(t, err, "failed to query body template for template:", tc.id)

title, err := render.GoTemplate(titleTmpl, tc.payload, nil)
title, err := render.GoTemplate(titleTmpl, tc.payload, defaultHelpers())
require.NotContainsf(t, title, render.NoValue, "template %q is missing a label value", tc.name)
require.NoError(t, err, "failed to render notification title template")
require.NotEmpty(t, title, "title should not be empty")

body, err := render.GoTemplate(bodyTmpl, tc.payload, nil)
body, err := render.GoTemplate(bodyTmpl, tc.payload, defaultHelpers())
require.NoError(t, err, "failed to render notification body template")
require.NotEmpty(t, body, "body should not be empty")

partialName := strings.Join(strings.Split(t.Name(), "/")[1:], "_")
Copy link
Contributor

Choose a reason for hiding this comment

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

Feels odd to have this in a test which is not explicitly for this purpose.
Can we split this out, rather?

Copy link
Member Author

Choose a reason for hiding this comment

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

ok, let's rely depend on strings.Split

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, sorry you interpreted what I said literally 😆
I meant splitting out the functionality into another test.

goldenFile := filepath.Join("testdata", "rendered-templates", partialName+"-body.md.golden")

if *updateGoldenFiles {
err = os.MkdirAll(filepath.Dir(goldenFile), 0o755)
require.NoError(t, err, "want no error creating golden file directory")
f, err := os.Create(goldenFile)
require.NoError(t, err, "want no error creating golden file")
defer f.Close()
_, err = f.WriteString(body)
require.NoError(t, err, "want no error writing golden file")
return
}

want, err := os.ReadFile(goldenFile)
require.NoError(t, err, "open golden file, run \"make update-golden-files\" and commit the changes")
require.Equal(t, string(want), body, "body should be equal")
})
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Hi Bobby

The template **bobby-template** was deleted by **rob**.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Hi Bobby,
User account **bobby** has been activated.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Hi Bobby,

New user account **bobby** has been created.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Hi Bobby,

User account **bobby** has been deleted.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Hi Bobby,
User account **bobby** has been suspended.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Hi Bobby
Your workspace **bobby-workspace** has been updated automatically to the latest template version (1.0).
Reason for update: **template now includes catnip**
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Hi Bobby
Automatic build of your workspace **bobby-workspace** failed.
The specified reason was "**autostart**".
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Hi Bobby,

Template **Bobby First Template** has failed to build 4/55 times over the last week and may be unstable.

**Report:**

**bobby-template-version-1** failed 3 times:

* [mtojek / workspace-1 / #1234](http://test.com/@mtojek/workspace-1/builds/1234)
* [johndoe / my-workspace-3 / #5678](http://test.com/@johndoe/my-workspace-3/builds/5678)
* [jack / workwork / #774](http://test.com/@jack/workwork/builds/774)

**bobby-template-version-2** failed 1 time:

* [ben / cool-workspace / #8888](http://test.com/@ben/cool-workspace/builds/8888)

We recommend reviewing these issues to ensure future builds are successful.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Hi Bobby

Your workspace **bobby-workspace** was deleted.
The specified reason was "**autodeleted due to dormancy (autobuild)**".
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Hi Bobby

Your workspace **bobby-workspace** has been marked as [**dormant**](https://coder.com/docs/templates/schedule#dormancy-threshold-enterprise) because of breached the template's threshold for inactivity.
Dormant workspaces are [automatically deleted](https://coder.com/docs/templates/schedule#dormancy-auto-deletion-enterprise) after 24 hours of inactivity.
To prevent deletion, use your workspace with the link below.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Hi Bobby,

A manual build of the workspace **bobby-workspace** using the template **bobby-template** failed (version: **bobby-template-version**).
The workspace build was initiated by **joe**.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Hi Bobby

Your workspace **bobby-workspace** has been marked for **deletion** after 24 hours of [dormancy](https://coder.com/docs/templates/schedule#dormancy-auto-deletion-enterprise) because of template updated to new dormancy policy.
To prevent deletion, use your workspace with the link below.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Hi Bobby,
Your account **bobby** has been activated.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Hi Bobby,
Your account **bobby** has been suspended.
1 change: 1 addition & 0 deletions coderd/notifications/types/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ type MessagePayload struct {

Actions []TemplateAction `json:"actions"`
Labels map[string]string `json:"labels"`
Data map[string]any `json:"data,omitempty"`
}
Loading