mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd): notify when workspace is marked as dormant (#13868)
This commit is contained in:
@@ -19,6 +19,7 @@ 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"
|
||||
@@ -35,7 +36,6 @@ type Executor struct {
|
||||
log slog.Logger
|
||||
tick <-chan time.Time
|
||||
statsCh chan<- Stats
|
||||
|
||||
// NotificationsEnqueuer handles enqueueing notifications for delivery by SMTP, webhook, etc.
|
||||
notificationsEnqueuer notifications.Enqueuer
|
||||
}
|
||||
@@ -142,13 +142,15 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
|
||||
eg.Go(func() error {
|
||||
err := func() error {
|
||||
var job *database.ProvisionerJob
|
||||
var nextBuild *database.WorkspaceBuild
|
||||
var activeTemplateVersion database.TemplateVersion
|
||||
var ws database.Workspace
|
||||
|
||||
var auditLog *auditParams
|
||||
var didAutoUpdate bool
|
||||
var (
|
||||
job *database.ProvisionerJob
|
||||
auditLog *auditParams
|
||||
dormantNotification *dormancy.WorkspaceDormantNotification
|
||||
nextBuild *database.WorkspaceBuild
|
||||
activeTemplateVersion database.TemplateVersion
|
||||
ws database.Workspace
|
||||
didAutoUpdate bool
|
||||
)
|
||||
err := e.db.InTx(func(tx database.Store) error {
|
||||
var err error
|
||||
|
||||
@@ -246,6 +248,13 @@ 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",
|
||||
}
|
||||
|
||||
log.Info(e.ctx, "dormant workspace",
|
||||
slog.F("last_used_at", ws.LastUsedAt),
|
||||
slog.F("time_til_dormant", templateSchedule.TimeTilDormant),
|
||||
@@ -290,7 +299,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
nextBuildReason = string(nextBuild.Reason)
|
||||
}
|
||||
|
||||
if _, err := e.notificationsEnqueuer.Enqueue(e.ctx, ws.OwnerID, notifications.WorkspaceAutoUpdated,
|
||||
if _, err := e.notificationsEnqueuer.Enqueue(e.ctx, ws.OwnerID, notifications.TemplateWorkspaceAutoUpdated,
|
||||
map[string]string{
|
||||
"name": ws.Name,
|
||||
"initiator": "autobuild",
|
||||
@@ -316,6 +325,16 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
return xerrors.Errorf("post provisioner job to pubsub: %w", err)
|
||||
}
|
||||
}
|
||||
if dormantNotification != nil {
|
||||
_, err = dormancy.NotifyWorkspaceDormant(
|
||||
e.ctx,
|
||||
e.notificationsEnqueuer,
|
||||
*dormantNotification,
|
||||
)
|
||||
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))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/notifications"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
@@ -115,7 +116,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan autobuild.Stats)
|
||||
logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: !tc.expectStart}).Leveled(slog.LevelDebug)
|
||||
enqueuer = testutil.FakeNotificationEnqueuer{}
|
||||
enqueuer = testutil.FakeNotificationsEnqueuer{}
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
@@ -1062,6 +1063,69 @@ func TestExecutorInactiveWorkspace(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotifications(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Dormancy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Setup template with dormancy and create a workspace with it
|
||||
var (
|
||||
ticker = make(chan time.Time)
|
||||
statCh = make(chan autobuild.Stats)
|
||||
notifyEnq = testutil.FakeNotificationsEnqueuer{}
|
||||
timeTilDormant = time.Minute
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
AutobuildStats: statCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
NotificationsEnqueuer: ¬ifyEnq,
|
||||
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
|
||||
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
|
||||
return schedule.TemplateScheduleOptions{
|
||||
UserAutostartEnabled: false,
|
||||
UserAutostopEnabled: true,
|
||||
DefaultTTL: 0,
|
||||
AutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||||
TimeTilDormant: timeTilDormant,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
})
|
||||
admin = coderdtest.CreateFirstUser(t, client)
|
||||
version = coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil)
|
||||
)
|
||||
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID)
|
||||
userClient, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||
workspace := coderdtest.CreateWorkspace(t, userClient, admin.OrganizationID, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||||
|
||||
// Stop workspace
|
||||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||||
|
||||
// Wait for workspace to become dormant
|
||||
ticker <- workspace.LastUsedAt.Add(timeTilDormant * 3)
|
||||
_ = testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, statCh)
|
||||
|
||||
// Check that the workspace is dormant
|
||||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||||
require.NotNil(t, workspace.DormantAt)
|
||||
|
||||
// Check that a notification was enqueued
|
||||
require.Len(t, notifyEnq.Sent, 1)
|
||||
require.Equal(t, notifyEnq.Sent[0].UserID, workspace.OwnerID)
|
||||
require.Equal(t, notifyEnq.Sent[0].TemplateID, notifications.TemplateWorkspaceDormant)
|
||||
require.Contains(t, notifyEnq.Sent[0].Targets, template.ID)
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
||||
func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace {
|
||||
t.Helper()
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
Reference in New Issue
Block a user