Skip to content

fix: fix dormancy notifications #14029

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
39 changes: 22 additions & 17 deletions coderd/autobuild/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/dormancy"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/coderd/wsbuilder"
Expand Down Expand Up @@ -145,10 +144,11 @@ func (e *Executor) runOnce(t time.Time) Stats {
var (
job *database.ProvisionerJob
auditLog *auditParams
dormantNotification *dormancy.WorkspaceDormantNotification
shouldNotifyDormancy bool
nextBuild *database.WorkspaceBuild
activeTemplateVersion database.TemplateVersion
ws database.Workspace
tmpl database.Template
didAutoUpdate bool
)
err := e.db.InTx(func(tx database.Store) error {
Expand Down Expand Up @@ -182,17 +182,17 @@ func (e *Executor) runOnce(t time.Time) Stats {
return xerrors.Errorf("get template scheduling options: %w", err)
}

template, err := tx.GetTemplateByID(e.ctx, ws.TemplateID)
tmpl, err = tx.GetTemplateByID(e.ctx, ws.TemplateID)
if err != nil {
return xerrors.Errorf("get template by ID: %w", err)
}

activeTemplateVersion, err = tx.GetTemplateVersionByID(e.ctx, template.ActiveVersionID)
activeTemplateVersion, err = tx.GetTemplateVersionByID(e.ctx, tmpl.ActiveVersionID)
if err != nil {
return xerrors.Errorf("get active template version by ID: %w", err)
}

accessControl := (*(e.accessControlStore.Load())).GetTemplateAccessControl(template)
accessControl := (*(e.accessControlStore.Load())).GetTemplateAccessControl(tmpl)

nextTransition, reason, err := getNextTransition(user, ws, latestBuild, latestJob, templateSchedule, currentTick)
if err != nil {
Expand All @@ -215,7 +215,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
log.Debug(e.ctx, "autostarting with active version")
builder = builder.ActiveVersion()

if latestBuild.TemplateVersionID != template.ActiveVersionID {
if latestBuild.TemplateVersionID != tmpl.ActiveVersionID {
// control flag to know if the workspace was auto-updated,
// so the lifecycle executor can notify the user
didAutoUpdate = true
Expand Down Expand Up @@ -248,12 +248,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
return xerrors.Errorf("update workspace dormant deleting at: %w", err)
}

dormantNotification = &dormancy.WorkspaceDormantNotification{
Workspace: ws,
Initiator: "autobuild",
Reason: "breached the template's threshold for inactivity",
CreatedBy: "lifecycleexecutor",
}
shouldNotifyDormancy = true

log.Info(e.ctx, "dormant workspace",
slog.F("last_used_at", ws.LastUsedAt),
Expand Down Expand Up @@ -325,14 +320,24 @@ func (e *Executor) runOnce(t time.Time) Stats {
return xerrors.Errorf("post provisioner job to pubsub: %w", err)
}
}
if dormantNotification != nil {
_, err = dormancy.NotifyWorkspaceDormant(
if shouldNotifyDormancy {
_, err = e.notificationsEnqueuer.Enqueue(
e.ctx,
e.notificationsEnqueuer,
*dormantNotification,
ws.OwnerID,
notifications.TemplateWorkspaceDormant,
map[string]string{
"name": ws.Name,
"reason": "inactivity exceeded the dormancy threshold",
"timeTilDormant": time.Duration(tmpl.TimeTilDormant).String(),
},
"lifecycle_executor",
ws.ID,
ws.OwnerID,
ws.TemplateID,
ws.OrganizationID,
)
if err != nil {
log.Warn(e.ctx, "failed to notify of workspace marked as dormant", slog.Error(err), slog.F("workspace_id", dormantNotification.Workspace.ID))
log.Warn(e.ctx, "failed to notify of workspace marked as dormant", slog.Error(err), slog.F("workspace_id", ws.ID))
}
}
return nil
Expand Down
1 change: 0 additions & 1 deletion coderd/autobuild/lifecycle_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1122,7 +1122,6 @@ func TestNotifications(t *testing.T) {
require.Contains(t, notifyEnq.Sent[0].Targets, workspace.ID)
require.Contains(t, notifyEnq.Sent[0].Targets, workspace.OrganizationID)
require.Contains(t, notifyEnq.Sent[0].Targets, workspace.OwnerID)
require.Equal(t, notifyEnq.Sent[0].Labels["initiator"], "autobuild")
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
UPDATE notification_templates
SET
body_template = E'Hi {{.UserName}}\n\n' ||
E'Your workspace **{{.Labels.name}}** has been marked as [**dormant**](https://coder.com/docs/templates/schedule#dormancy-threshold-enterprise) because of {{.Labels.reason}}.\n' ||
E'Dormant workspaces are [automatically deleted](https://coder.com/docs/templates/schedule#dormancy-auto-deletion-enterprise) after {{.Labels.timeTilDormant}} of inactivity.\n' ||
E'To prevent deletion, use your workspace with the link below.'
WHERE
id = '0ea69165-ec14-4314-91f1-69566ac3c5a0';

UPDATE notification_templates
SET
body_template = E'Hi {{.UserName}}\n\n' ||
E'Your workspace **{{.Labels.name}}** has been marked for **deletion** after {{.Labels.timeTilDormant}} of [dormancy](https://coder.com/docs/templates/schedule#dormancy-auto-deletion-enterprise) because of {{.Labels.reason}}.\n' ||
E'To prevent deletion, use your workspace with the link below.'
WHERE
id = '51ce2fdf-c9ca-4be1-8d70-628674f9bc42';
1 change: 0 additions & 1 deletion coderd/database/queries/notifications.sql
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,3 @@ WHERE id IN

-- name: GetNotificationMessagesByStatus :many
SELECT * FROM notification_messages WHERE status = @status LIMIT sqlc.arg('limit')::int;

75 changes: 0 additions & 75 deletions coderd/dormancy/notifications.go

This file was deleted.

102 changes: 102 additions & 0 deletions coderd/notifications/notifications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/notifications/dispatch"
"github.com/coder/coder/v2/coderd/notifications/render"
"github.com/coder/coder/v2/coderd/notifications/types"
"github.com/coder/coder/v2/coderd/util/syncmap"
"github.com/coder/coder/v2/codersdk"
Expand Down Expand Up @@ -603,6 +604,107 @@ func TestNotifierPaused(t *testing.T) {
}, testutil.WaitShort, testutil.IntervalFast)
}

func TestNotifcationTemplatesBody(t *testing.T) {
t.Parallel()

if !dbtestutil.WillUsePostgres() {
t.Skip("This test requires postgres; it relies on the notification templates added by migrations in the database")
}

tests := []struct {
name string
id uuid.UUID
payload types.MessagePayload
}{
{
name: "TemplateWorkspaceDeleted",
id: notifications.TemplateWorkspaceDeleted,
payload: types.MessagePayload{
UserName: "bobby",
Labels: map[string]string{
"name": "bobby-workspace",
"reason": "autodeleted due to dormancy",
"initiator": "autobuild",
},
},
},
{
name: "TemplateWorkspaceAutobuildFailed",
id: notifications.TemplateWorkspaceAutobuildFailed,
payload: types.MessagePayload{
UserName: "bobby",
Labels: map[string]string{
"name": "bobby-workspace",
"reason": "autostart",
},
},
},
{
name: "TemplateWorkspaceDormant",
id: notifications.TemplateWorkspaceDormant,
payload: types.MessagePayload{
UserName: "bobby",
Labels: map[string]string{
"name": "bobby-workspace",
"reason": "breached the template's threshold for inactivity",
"initiator": "autobuild",
"dormancyHours": "24",
},
},
},
{
name: "TemplateWorkspaceAutoUpdated",
id: notifications.TemplateWorkspaceAutoUpdated,
payload: types.MessagePayload{
UserName: "bobby",
Labels: map[string]string{
"name": "bobby-workspace",
"template_version_name": "1.0",
},
},
},
{
name: "TemplateWorkspaceMarkedForDeletion",
id: notifications.TemplateWorkspaceMarkedForDeletion,
payload: types.MessagePayload{
UserName: "bobby",
Labels: map[string]string{
"name": "bobby-workspace",
"reason": "template updated to new dormancy policy",
"dormancyHours": "24",
},
},
},
}

for _, tc := range tests {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

_, _, sql := dbtestutil.NewDBWithSQLDB(t)

var (
titleTmpl string
bodyTmpl string
)
err := sql.
QueryRow("SELECT title_template, body_template FROM notification_templates WHERE id = $1 LIMIT 1", tc.id).
Scan(&titleTmpl, &bodyTmpl)
require.NoError(t, err, "failed to query body template for template:", tc.id)

title, err := render.GoTemplate(titleTmpl, tc.payload, nil)
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)
require.NoError(t, err, "failed to render notification body template")
require.NotEmpty(t, body, "body should not be empty")
})
}
}

type fakeHandler struct {
mu sync.RWMutex
succeeded, failed []string
Expand Down
6 changes: 2 additions & 4 deletions coderd/provisionerdserver/provisionerdserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1101,13 +1101,11 @@ func (s *server) notifyWorkspaceBuildFailed(ctx context.Context, workspace datab
return // failed workspace build initiated by a user should not notify
}
reason = string(build.Reason)
initiator := "autobuild"

if _, err := s.NotificationsEnqueuer.Enqueue(ctx, workspace.OwnerID, notifications.TemplateWorkspaceAutobuildFailed,
map[string]string{
"name": workspace.Name,
"initiator": initiator,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I found out that TemplateWorkspaceAutobuildFailed was not using the initiator label in the template body so I just removed it.

"reason": reason,
"name": workspace.Name,
"reason": reason,
}, "provisionerdserver",
// Associate this notification with all the related entities.
workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID,
Expand Down
1 change: 0 additions & 1 deletion coderd/provisionerdserver/provisionerdserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1797,7 +1797,6 @@ func TestNotifications(t *testing.T) {
require.Contains(t, notifEnq.Sent[0].Targets, workspace.ID)
require.Contains(t, notifEnq.Sent[0].Targets, workspace.OrganizationID)
require.Contains(t, notifEnq.Sent[0].Targets, user.ID)
require.Equal(t, "autobuild", notifEnq.Sent[0].Labels["initiator"])
require.Equal(t, string(tc.buildReason), notifEnq.Sent[0].Labels["reason"])
} else {
require.Len(t, notifEnq.Sent, 0)
Expand Down
Loading
Loading