Skip to content

Commit fbd1d7f

Browse files
authored
feat: notify on successful autoupdate (#13903)
1 parent 44924cd commit fbd1d7f

File tree

8 files changed

+130
-11
lines changed

8 files changed

+130
-11
lines changed

cli/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1066,7 +1066,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
10661066
autobuildTicker := time.NewTicker(vals.AutobuildPollInterval.Value())
10671067
defer autobuildTicker.Stop()
10681068
autobuildExecutor := autobuild.NewExecutor(
1069-
ctx, options.Database, options.Pubsub, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, coderAPI.AccessControlStore, logger, autobuildTicker.C)
1069+
ctx, options.Database, options.Pubsub, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, coderAPI.AccessControlStore, logger, autobuildTicker.C, options.NotificationsEnqueuer)
10701070
autobuildExecutor.Run()
10711071

10721072
hangDetectorTicker := time.NewTicker(vals.JobHangDetectorInterval.Value())

coderd/autobuild/lifecycle_executor.go

Lines changed: 45 additions & 3 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/notifications"
2223
"github.com/coder/coder/v2/coderd/schedule"
2324
"github.com/coder/coder/v2/coderd/wsbuilder"
2425
)
@@ -34,6 +35,9 @@ type Executor struct {
3435
log slog.Logger
3536
tick <-chan time.Time
3637
statsCh chan<- Stats
38+
39+
// NotificationsEnqueuer handles enqueueing notifications for delivery by SMTP, webhook, etc.
40+
notificationsEnqueuer notifications.Enqueuer
3741
}
3842

3943
// Stats contains information about one run of Executor.
@@ -44,7 +48,7 @@ type Stats struct {
4448
}
4549

4650
// New returns a new wsactions executor.
47-
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], log slog.Logger, tick <-chan time.Time) *Executor {
51+
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], log slog.Logger, tick <-chan time.Time, enqueuer notifications.Enqueuer) *Executor {
4852
le := &Executor{
4953
//nolint:gocritic // Autostart has a limited set of permissions.
5054
ctx: dbauthz.AsAutostart(ctx),
@@ -55,6 +59,7 @@ func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *
5559
log: log.Named("autobuild"),
5660
auditor: auditor,
5761
accessControlStore: acs,
62+
notificationsEnqueuer: enqueuer,
5863
}
5964
return le
6065
}
@@ -138,11 +143,18 @@ func (e *Executor) runOnce(t time.Time) Stats {
138143
eg.Go(func() error {
139144
err := func() error {
140145
var job *database.ProvisionerJob
146+
var nextBuild *database.WorkspaceBuild
147+
var activeTemplateVersion database.TemplateVersion
148+
var ws database.Workspace
149+
141150
var auditLog *auditParams
151+
var didAutoUpdate bool
142152
err := e.db.InTx(func(tx database.Store) error {
153+
var err error
154+
143155
// Re-check eligibility since the first check was outside the
144156
// transaction and the workspace settings may have changed.
145-
ws, err := tx.GetWorkspaceByID(e.ctx, wsID)
157+
ws, err = tx.GetWorkspaceByID(e.ctx, wsID)
146158
if err != nil {
147159
return xerrors.Errorf("get workspace by id: %w", err)
148160
}
@@ -173,6 +185,11 @@ func (e *Executor) runOnce(t time.Time) Stats {
173185
return xerrors.Errorf("get template by ID: %w", err)
174186
}
175187

188+
activeTemplateVersion, err = tx.GetTemplateVersionByID(e.ctx, template.ActiveVersionID)
189+
if err != nil {
190+
return xerrors.Errorf("get active template version by ID: %w", err)
191+
}
192+
176193
accessControl := (*(e.accessControlStore.Load())).GetTemplateAccessControl(template)
177194

178195
nextTransition, reason, err := getNextTransition(user, ws, latestBuild, latestJob, templateSchedule, currentTick)
@@ -195,9 +212,15 @@ func (e *Executor) runOnce(t time.Time) Stats {
195212
useActiveVersion(accessControl, ws) {
196213
log.Debug(e.ctx, "autostarting with active version")
197214
builder = builder.ActiveVersion()
215+
216+
if latestBuild.TemplateVersionID != template.ActiveVersionID {
217+
// control flag to know if the workspace was auto-updated,
218+
// so the lifecycle executor can notify the user
219+
didAutoUpdate = true
220+
}
198221
}
199222

200-
_, job, err = builder.Build(e.ctx, tx, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
223+
nextBuild, job, err = builder.Build(e.ctx, tx, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
201224
if err != nil {
202225
return xerrors.Errorf("build workspace with transition %q: %w", nextTransition, err)
203226
}
@@ -261,6 +284,25 @@ func (e *Executor) runOnce(t time.Time) Stats {
261284
auditLog.Success = err == nil
262285
auditBuild(e.ctx, log, *e.auditor.Load(), *auditLog)
263286
}
287+
if didAutoUpdate && err == nil {
288+
nextBuildReason := ""
289+
if nextBuild != nil {
290+
nextBuildReason = string(nextBuild.Reason)
291+
}
292+
293+
if _, err := e.notificationsEnqueuer.Enqueue(e.ctx, ws.OwnerID, notifications.WorkspaceAutoUpdated,
294+
map[string]string{
295+
"name": ws.Name,
296+
"initiator": "autobuild",
297+
"reason": nextBuildReason,
298+
"template_version_name": activeTemplateVersion.Name,
299+
}, "autobuild",
300+
// Associate this notification with all the related entities.
301+
ws.ID, ws.OwnerID, ws.TemplateID, ws.OrganizationID,
302+
); err != nil {
303+
log.Warn(e.ctx, "failed to notify of autoupdated workspace", slog.Error(err))
304+
}
305+
}
264306
if err != nil {
265307
return xerrors.Errorf("transition workspace: %w", err)
266308
}

coderd/autobuild/lifecycle_executor_test.go

Lines changed: 26 additions & 7 deletions
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/notiffake"
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"
@@ -79,6 +80,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
7980
compatibleParameters bool
8081
expectStart bool
8182
expectUpdate bool
83+
expectNotification bool
8284
}{
8385
{
8486
name: "Never",
@@ -93,6 +95,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
9395
compatibleParameters: true,
9496
expectStart: true,
9597
expectUpdate: true,
98+
expectNotification: true,
9699
},
97100
{
98101
name: "Always_Incompatible",
@@ -107,17 +110,19 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
107110
t.Run(tc.name, func(t *testing.T) {
108111
t.Parallel()
109112
var (
110-
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
111-
ctx = context.Background()
112-
err error
113-
tickCh = make(chan time.Time)
114-
statsCh = make(chan autobuild.Stats)
115-
logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: !tc.expectStart}).Leveled(slog.LevelDebug)
116-
client = coderdtest.New(t, &coderdtest.Options{
113+
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
114+
ctx = context.Background()
115+
err error
116+
tickCh = make(chan time.Time)
117+
statsCh = make(chan autobuild.Stats)
118+
logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: !tc.expectStart}).Leveled(slog.LevelDebug)
119+
enqueuer = notiffake.FakeNotificationEnqueuer{}
120+
client = coderdtest.New(t, &coderdtest.Options{
117121
AutobuildTicker: tickCh,
118122
IncludeProvisionerDaemon: true,
119123
AutobuildStats: statsCh,
120124
Logger: &logger,
125+
NotificationsEnqueuer: &enqueuer,
121126
})
122127
// Given: we have a user with a workspace that has autostart enabled
123128
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
@@ -195,6 +200,20 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
195200
assert.Equal(t, workspace.LatestBuild.TemplateVersionID, ws.LatestBuild.TemplateVersionID,
196201
"expected workspace build to be using the old template version")
197202
}
203+
204+
if tc.expectNotification {
205+
require.Len(t, enqueuer.Sent, 1)
206+
require.Equal(t, enqueuer.Sent[0].UserID, workspace.OwnerID)
207+
require.Contains(t, enqueuer.Sent[0].Targets, workspace.TemplateID)
208+
require.Contains(t, enqueuer.Sent[0].Targets, workspace.ID)
209+
require.Contains(t, enqueuer.Sent[0].Targets, workspace.OrganizationID)
210+
require.Contains(t, enqueuer.Sent[0].Targets, workspace.OwnerID)
211+
require.Equal(t, newVersion.Name, enqueuer.Sent[0].Labels["template_version_name"])
212+
require.Equal(t, "autobuild", enqueuer.Sent[0].Labels["initiator"])
213+
require.Equal(t, "autostart", enqueuer.Sent[0].Labels["reason"])
214+
} else {
215+
require.Len(t, enqueuer.Sent, 0)
216+
}
198217
})
199218
}
200219
}

coderd/coderdtest/coderdtest.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ import (
6464
"github.com/coder/coder/v2/coderd/externalauth"
6565
"github.com/coder/coder/v2/coderd/gitsshkey"
6666
"github.com/coder/coder/v2/coderd/httpmw"
67+
"github.com/coder/coder/v2/coderd/notifications"
68+
"github.com/coder/coder/v2/coderd/notifications/notiffake"
6769
"github.com/coder/coder/v2/coderd/rbac"
6870
"github.com/coder/coder/v2/coderd/schedule"
6971
"github.com/coder/coder/v2/coderd/telemetry"
@@ -154,6 +156,8 @@ type Options struct {
154156
DatabaseRolluper *dbrollup.Rolluper
155157
WorkspaceUsageTrackerFlush chan int
156158
WorkspaceUsageTrackerTick chan time.Time
159+
160+
NotificationsEnqueuer notifications.Enqueuer
157161
}
158162

159163
// New constructs a codersdk client connected to an in-memory API instance.
@@ -238,6 +242,10 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
238242
options.Database, options.Pubsub = dbtestutil.NewDB(t)
239243
}
240244

245+
if options.NotificationsEnqueuer == nil {
246+
options.NotificationsEnqueuer = new(notiffake.FakeNotificationEnqueuer)
247+
}
248+
241249
accessControlStore := &atomic.Pointer[dbauthz.AccessControlStore]{}
242250
var acs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
243251
accessControlStore.Store(&acs)
@@ -305,6 +313,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
305313
accessControlStore,
306314
*options.Logger,
307315
options.AutobuildTicker,
316+
options.NotificationsEnqueuer,
308317
).WithStatsChannel(options.AutobuildStats)
309318
lifecycleExecutor.Run()
310319

@@ -498,6 +507,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
498507
NewTicker: options.NewTicker,
499508
DatabaseRolluper: options.DatabaseRolluper,
500509
WorkspaceUsageTracker: wuTracker,
510+
NotificationsEnqueuer: options.NotificationsEnqueuer,
501511
}
502512
}
503513

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DELETE FROM notification_templates WHERE id = 'c34a0c09-0704-4cac-bd1c-0c0146811c2b';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions)
2+
VALUES ('c34a0c09-0704-4cac-bd1c-0c0146811c2b', 'Workspace updated automatically', E'Workspace "{{.Labels.name}}" updated automatically',
3+
E'Hi {{.UserName}}\n\Your workspace **{{.Labels.name}}** has been updated automatically to the latest template version ({{.Labels.template_version_name}}).',
4+
'Workspace Events', '[
5+
{
6+
"label": "View workspace",
7+
"url": "{{ base_url }}/@{{.UserName}}/{{.Labels.name}}"
8+
}
9+
]'::jsonb);

coderd/notifications/events.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ import "github.com/google/uuid"
99
var (
1010
TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed")
1111
WorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9")
12+
WorkspaceAutoUpdated = uuid.MustParse("c34a0c09-0704-4cac-bd1c-0c0146811c2b")
1213
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package notiffake
2+
3+
import (
4+
"context"
5+
"sync"
6+
7+
"github.com/google/uuid"
8+
)
9+
10+
type FakeNotificationEnqueuer struct {
11+
mu sync.Mutex
12+
13+
Sent []*Notification
14+
}
15+
16+
type Notification struct {
17+
UserID, TemplateID uuid.UUID
18+
Labels map[string]string
19+
CreatedBy string
20+
Targets []uuid.UUID
21+
}
22+
23+
func (f *FakeNotificationEnqueuer) Enqueue(_ context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) {
24+
f.mu.Lock()
25+
defer f.mu.Unlock()
26+
27+
f.Sent = append(f.Sent, &Notification{
28+
UserID: userID,
29+
TemplateID: templateID,
30+
Labels: labels,
31+
CreatedBy: createdBy,
32+
Targets: targets,
33+
})
34+
35+
id := uuid.New()
36+
return &id, nil
37+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy