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

Commit0d9615b

Browse files
feat(coderd): notify when workspace is marked as dormant (#13868)
1 parentccb5b4d commit0d9615b

25 files changed

+650
-118
lines changed

‎coderd/autobuild/lifecycle_executor.go

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/coder/coder/v2/coderd/database/dbtime"
2020
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
2121
"github.com/coder/coder/v2/coderd/database/pubsub"
22+
"github.com/coder/coder/v2/coderd/dormancy"
2223
"github.com/coder/coder/v2/coderd/notifications"
2324
"github.com/coder/coder/v2/coderd/schedule"
2425
"github.com/coder/coder/v2/coderd/wsbuilder"
@@ -35,7 +36,6 @@ type Executor struct {
3536
log slog.Logger
3637
tick<-chan time.Time
3738
statsChchan<-Stats
38-
3939
// NotificationsEnqueuer handles enqueueing notifications for delivery by SMTP, webhook, etc.
4040
notificationsEnqueuer notifications.Enqueuer
4141
}
@@ -142,13 +142,15 @@ func (e *Executor) runOnce(t time.Time) Stats {
142142

143143
eg.Go(func()error {
144144
err:=func()error {
145-
varjob*database.ProvisionerJob
146-
varnextBuild*database.WorkspaceBuild
147-
varactiveTemplateVersion database.TemplateVersion
148-
varws database.Workspace
149-
150-
varauditLog*auditParams
151-
vardidAutoUpdatebool
145+
var (
146+
job*database.ProvisionerJob
147+
auditLog*auditParams
148+
dormantNotification*dormancy.WorkspaceDormantNotification
149+
nextBuild*database.WorkspaceBuild
150+
activeTemplateVersion database.TemplateVersion
151+
ws database.Workspace
152+
didAutoUpdatebool
153+
)
152154
err:=e.db.InTx(func(tx database.Store)error {
153155
varerrerror
154156

@@ -246,6 +248,13 @@ func (e *Executor) runOnce(t time.Time) Stats {
246248
returnxerrors.Errorf("update workspace dormant deleting at: %w",err)
247249
}
248250

251+
dormantNotification=&dormancy.WorkspaceDormantNotification{
252+
Workspace:ws,
253+
Initiator:"autobuild",
254+
Reason:"breached the template's threshold for inactivity",
255+
CreatedBy:"lifecycleexecutor",
256+
}
257+
249258
log.Info(e.ctx,"dormant workspace",
250259
slog.F("last_used_at",ws.LastUsedAt),
251260
slog.F("time_til_dormant",templateSchedule.TimeTilDormant),
@@ -290,7 +299,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
290299
nextBuildReason=string(nextBuild.Reason)
291300
}
292301

293-
if_,err:=e.notificationsEnqueuer.Enqueue(e.ctx,ws.OwnerID,notifications.WorkspaceAutoUpdated,
302+
if_,err:=e.notificationsEnqueuer.Enqueue(e.ctx,ws.OwnerID,notifications.TemplateWorkspaceAutoUpdated,
294303
map[string]string{
295304
"name":ws.Name,
296305
"initiator":"autobuild",
@@ -316,6 +325,16 @@ func (e *Executor) runOnce(t time.Time) Stats {
316325
returnxerrors.Errorf("post provisioner job to pubsub: %w",err)
317326
}
318327
}
328+
ifdormantNotification!=nil {
329+
_,err=dormancy.NotifyWorkspaceDormant(
330+
e.ctx,
331+
e.notificationsEnqueuer,
332+
*dormantNotification,
333+
)
334+
iferr!=nil {
335+
log.Warn(e.ctx,"failed to notify of workspace marked as dormant",slog.Error(err),slog.F("workspace_id",dormantNotification.Workspace.ID))
336+
}
337+
}
319338
returnnil
320339
}()
321340
iferr!=nil {

‎coderd/autobuild/lifecycle_executor_test.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/coder/coder/v2/coderd/coderdtest"
1919
"github.com/coder/coder/v2/coderd/database"
2020
"github.com/coder/coder/v2/coderd/database/dbauthz"
21+
"github.com/coder/coder/v2/coderd/notifications"
2122
"github.com/coder/coder/v2/coderd/schedule"
2223
"github.com/coder/coder/v2/coderd/schedule/cron"
2324
"github.com/coder/coder/v2/coderd/util/ptr"
@@ -115,7 +116,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
115116
tickCh=make(chan time.Time)
116117
statsCh=make(chan autobuild.Stats)
117118
logger=slogtest.Make(t,&slogtest.Options{IgnoreErrors:!tc.expectStart}).Leveled(slog.LevelDebug)
118-
enqueuer= testutil.FakeNotificationEnqueuer{}
119+
enqueuer= testutil.FakeNotificationsEnqueuer{}
119120
client=coderdtest.New(t,&coderdtest.Options{
120121
AutobuildTicker:tickCh,
121122
IncludeProvisionerDaemon:true,
@@ -1062,6 +1063,69 @@ func TestExecutorInactiveWorkspace(t *testing.T) {
10621063
})
10631064
}
10641065

1066+
funcTestNotifications(t*testing.T) {
1067+
t.Parallel()
1068+
1069+
t.Run("Dormancy",func(t*testing.T) {
1070+
t.Parallel()
1071+
1072+
// Setup template with dormancy and create a workspace with it
1073+
var (
1074+
ticker=make(chan time.Time)
1075+
statCh=make(chan autobuild.Stats)
1076+
notifyEnq= testutil.FakeNotificationsEnqueuer{}
1077+
timeTilDormant=time.Minute
1078+
client=coderdtest.New(t,&coderdtest.Options{
1079+
AutobuildTicker:ticker,
1080+
AutobuildStats:statCh,
1081+
IncludeProvisionerDaemon:true,
1082+
NotificationsEnqueuer:&notifyEnq,
1083+
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
1084+
GetFn:func(_ context.Context,_ database.Store,_ uuid.UUID) (schedule.TemplateScheduleOptions,error) {
1085+
return schedule.TemplateScheduleOptions{
1086+
UserAutostartEnabled:false,
1087+
UserAutostopEnabled:true,
1088+
DefaultTTL:0,
1089+
AutostopRequirement: schedule.TemplateAutostopRequirement{},
1090+
TimeTilDormant:timeTilDormant,
1091+
},nil
1092+
},
1093+
},
1094+
})
1095+
admin=coderdtest.CreateFirstUser(t,client)
1096+
version=coderdtest.CreateTemplateVersion(t,client,admin.OrganizationID,nil)
1097+
)
1098+
1099+
coderdtest.AwaitTemplateVersionJobCompleted(t,client,version.ID)
1100+
template:=coderdtest.CreateTemplate(t,client,admin.OrganizationID,version.ID)
1101+
userClient,_:=coderdtest.CreateAnotherUser(t,client,admin.OrganizationID)
1102+
workspace:=coderdtest.CreateWorkspace(t,userClient,admin.OrganizationID,template.ID)
1103+
coderdtest.AwaitWorkspaceBuildJobCompleted(t,userClient,workspace.LatestBuild.ID)
1104+
1105+
// Stop workspace
1106+
workspace=coderdtest.MustTransitionWorkspace(t,client,workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop)
1107+
_=coderdtest.AwaitWorkspaceBuildJobCompleted(t,userClient,workspace.LatestBuild.ID)
1108+
1109+
// Wait for workspace to become dormant
1110+
ticker<-workspace.LastUsedAt.Add(timeTilDormant*3)
1111+
_=testutil.RequireRecvCtx(testutil.Context(t,testutil.WaitShort),t,statCh)
1112+
1113+
// Check that the workspace is dormant
1114+
workspace=coderdtest.MustWorkspace(t,client,workspace.ID)
1115+
require.NotNil(t,workspace.DormantAt)
1116+
1117+
// Check that a notification was enqueued
1118+
require.Len(t,notifyEnq.Sent,1)
1119+
require.Equal(t,notifyEnq.Sent[0].UserID,workspace.OwnerID)
1120+
require.Equal(t,notifyEnq.Sent[0].TemplateID,notifications.TemplateWorkspaceDormant)
1121+
require.Contains(t,notifyEnq.Sent[0].Targets,template.ID)
1122+
require.Contains(t,notifyEnq.Sent[0].Targets,workspace.ID)
1123+
require.Contains(t,notifyEnq.Sent[0].Targets,workspace.OrganizationID)
1124+
require.Contains(t,notifyEnq.Sent[0].Targets,workspace.OwnerID)
1125+
require.Equal(t,notifyEnq.Sent[0].Labels["initiator"],"autobuild")
1126+
})
1127+
}
1128+
10651129
funcmustProvisionWorkspace(t*testing.T,client*codersdk.Client,mut...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace {
10661130
t.Helper()
10671131
user:=coderdtest.CreateFirstUser(t,client)

‎coderd/coderdtest/coderdtest.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
242242
}
243243

244244
ifoptions.NotificationsEnqueuer==nil {
245-
options.NotificationsEnqueuer=new(testutil.FakeNotificationEnqueuer)
245+
options.NotificationsEnqueuer=new(testutil.FakeNotificationsEnqueuer)
246246
}
247247

248248
accessControlStore:=&atomic.Pointer[dbauthz.AccessControlStore]{}
@@ -289,6 +289,9 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
289289
options.StatsBatcher=batcher
290290
t.Cleanup(closeBatcher)
291291
}
292+
ifoptions.NotificationsEnqueuer==nil {
293+
options.NotificationsEnqueuer=&testutil.FakeNotificationsEnqueuer{}
294+
}
292295

293296
vartemplateScheduleStore atomic.Pointer[schedule.TemplateScheduleStore]
294297
ifoptions.TemplateScheduleStore==nil {

‎coderd/database/dbauthz/dbauthz.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3555,12 +3555,15 @@ func (q *querier) UpdateWorkspaceTTL(ctx context.Context, arg database.UpdateWor
35553555
returnupdate(q.log,q.auth,fetch,q.db.UpdateWorkspaceTTL)(ctx,arg)
35563556
}
35573557

3558-
func (q*querier)UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context,arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams)error {
3559-
fetch:=func(ctx context.Context,arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) (database.Template,error) {
3560-
returnq.db.GetTemplateByID(ctx,arg.TemplateID)
3558+
func (q*querier)UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context,arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) ([]database.Workspace,error) {
3559+
template,err:=q.db.GetTemplateByID(ctx,arg.TemplateID)
3560+
iferr!=nil {
3561+
returnnil,xerrors.Errorf("get template by id: %w",err)
35613562
}
3562-
3563-
returnfetchAndExec(q.log,q.auth,policy.ActionUpdate,fetch,q.db.UpdateWorkspacesDormantDeletingAtByTemplateID)(ctx,arg)
3563+
iferr:=q.authorizeContext(ctx,policy.ActionUpdate,template);err!=nil {
3564+
returnnil,err
3565+
}
3566+
returnq.db.UpdateWorkspacesDormantDeletingAtByTemplateID(ctx,arg)
35643567
}
35653568

35663569
func (q*querier)UpsertAnnouncementBanners(ctx context.Context,valuestring)error {

‎coderd/database/dbmem/dbmem.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8700,15 +8700,16 @@ func (q *FakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateW
87008700
returnsql.ErrNoRows
87018701
}
87028702

8703-
func (q*FakeQuerier)UpdateWorkspacesDormantDeletingAtByTemplateID(_ context.Context,arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams)error {
8703+
func (q*FakeQuerier)UpdateWorkspacesDormantDeletingAtByTemplateID(_ context.Context,arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams)([]database.Workspace,error) {
87048704
q.mutex.Lock()
87058705
deferq.mutex.Unlock()
87068706

87078707
err:=validateDatabaseType(arg)
87088708
iferr!=nil {
8709-
returnerr
8709+
returnnil,err
87108710
}
87118711

8712+
affectedRows:= []database.Workspace{}
87128713
fori,ws:=rangeq.workspaces {
87138714
ifws.TemplateID!=arg.TemplateID {
87148715
continue
@@ -8733,9 +8734,10 @@ func (q *FakeQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(_ context.Co
87338734
}
87348735
ws.DeletingAt=deletingAt
87358736
q.workspaces[i]=ws
8737+
affectedRows=append(affectedRows,ws)
87368738
}
87378739

8738-
returnnil
8740+
returnaffectedRows,nil
87398741
}
87408742

87418743
func (q*FakeQuerier)UpsertAnnouncementBanners(_ context.Context,datastring)error {

‎coderd/database/dbmetrics/dbmetrics.go

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/database/dbmock/dbmock.go

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
DELETEFROM notification_templates
2+
WHERE
3+
id='0ea69165-ec14-4314-91f1-69566ac3c5a0';
4+
5+
DELETEFROM notification_templates
6+
WHERE
7+
id='51ce2fdf-c9ca-4be1-8d70-628674f9bc42';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
INSERT INTO
2+
notification_templates (
3+
id,
4+
name,
5+
title_template,
6+
body_template,
7+
"group",
8+
actions
9+
)
10+
VALUES (
11+
'0ea69165-ec14-4314-91f1-69566ac3c5a0',
12+
'Workspace Marked as Dormant',
13+
E'Workspace "{{.Labels.name}}" marked as dormant',
14+
E'Hi {{.UserName}}\n\n'|| E'Your workspace **{{.Labels.name}}** has been marked as **dormant**.\n'|| E'The specified reason was "**{{.Labels.reason}} (initiated by: {{ .Labels.initiator }}){{end}}**\n\n'|| E'Dormancy refers to a workspace being unused for a defined length of time, and after it exceeds {{.Labels.dormancyHours}} hours of dormancy might be deleted.\n'|| E'To activate your workspace again, simply use it as normal.',
15+
'Workspace Events',
16+
'[
17+
{
18+
"label": "View workspace",
19+
"url": "{{ base_url }}/@{{.UserName}}/{{.Labels.name}}"
20+
}
21+
]'::jsonb
22+
),
23+
(
24+
'51ce2fdf-c9ca-4be1-8d70-628674f9bc42',
25+
'Workspace Marked for Deletion',
26+
E'Workspace "{{.Labels.name}}" marked for deletion',
27+
E'Hi {{.UserName}}\n\n'|| E'Your workspace **{{.Labels.name}}** has been marked for **deletion** after {{.Labels.dormancyHours}} hours of dormancy.\n'|| E'The specified reason was "**{{.Labels.reason}}{{end}}**\n\n'|| E'Dormancy refers to a workspace being unused for a defined length of time, and after it exceeds {{.Labels.dormancyHours}} hours of dormancy it will be deleted.\n'|| E'To prevent your workspace from being deleted, simply use it as normal.',
28+
'Workspace Events',
29+
'[
30+
{
31+
"label": "View workspace",
32+
"url": "{{ base_url }}/@{{.UserName}}/{{.Labels.name}}"
33+
}
34+
]'::jsonb
35+
);

‎coderd/database/querier.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/database/queries.sql.go

Lines changed: 39 additions & 4 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp