- Notifications
You must be signed in to change notification settings - Fork928
feat: notify on successful autoupdate#13903
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
5fa194c
8b8b00b
b5fa318
3754f79
3cb5184
c2ebada
b601c22
acf9394
99b1802
8c7b06e
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/notifications" | ||
"github.com/coder/coder/v2/coderd/schedule" | ||
"github.com/coder/coder/v2/coderd/wsbuilder" | ||
) | ||
@@ -34,6 +35,9 @@ 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 | ||
} | ||
// Stats contains information about one run of Executor. | ||
@@ -44,7 +48,7 @@ type Stats struct { | ||
} | ||
// New returns a new wsactions executor. | ||
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 { | ||
le := &Executor{ | ||
//nolint:gocritic // Autostart has a limited set of permissions. | ||
ctx: dbauthz.AsAutostart(ctx), | ||
@@ -55,6 +59,7 @@ func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss * | ||
log: log.Named("autobuild"), | ||
auditor: auditor, | ||
accessControlStore: acs, | ||
notificationsEnqueuer: enqueuer, | ||
} | ||
return le | ||
} | ||
@@ -138,11 +143,18 @@ 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 | ||
err := e.db.InTx(func(tx database.Store) error { | ||
var err error | ||
// Re-check eligibility since the first check was outside the | ||
// transaction and the workspace settings may have changed. | ||
ws, err = tx.GetWorkspaceByID(e.ctx, wsID) | ||
if err != nil { | ||
return xerrors.Errorf("get workspace by id: %w", err) | ||
} | ||
@@ -173,6 +185,11 @@ func (e *Executor) runOnce(t time.Time) Stats { | ||
return xerrors.Errorf("get template by ID: %w", err) | ||
} | ||
activeTemplateVersion, err = tx.GetTemplateVersionByID(e.ctx, template.ActiveVersionID) | ||
if err != nil { | ||
return xerrors.Errorf("get active template version by ID: %w", err) | ||
} | ||
accessControl := (*(e.accessControlStore.Load())).GetTemplateAccessControl(template) | ||
nextTransition, reason, err := getNextTransition(user, ws, latestBuild, latestJob, templateSchedule, currentTick) | ||
@@ -195,9 +212,15 @@ func (e *Executor) runOnce(t time.Time) Stats { | ||
useActiveVersion(accessControl, ws) { | ||
log.Debug(e.ctx, "autostarting with active version") | ||
builder = builder.ActiveVersion() | ||
if latestBuild.TemplateVersionID != template.ActiveVersionID { | ||
// control flag to know if the workspace was auto-updated, | ||
// so the lifecycle executor can notify the user | ||
didAutoUpdate = true | ||
} | ||
} | ||
nextBuild, job, err = builder.Build(e.ctx, tx, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"}) | ||
if err != nil { | ||
return xerrors.Errorf("build workspace with transition %q: %w", nextTransition, err) | ||
} | ||
@@ -261,6 +284,25 @@ func (e *Executor) runOnce(t time.Time) Stats { | ||
auditLog.Success = err == nil | ||
auditBuild(e.ctx, log, *e.auditor.Load(), *auditLog) | ||
} | ||
if didAutoUpdate && err == nil { | ||
nextBuildReason := "" | ||
if nextBuild != nil { | ||
nextBuildReason = string(nextBuild.Reason) | ||
} | ||
if _, err := e.notificationsEnqueuer.Enqueue(e.ctx, ws.OwnerID, notifications.WorkspaceAutoUpdated, | ||
map[string]string{ | ||
"name": ws.Name, | ||
"initiator": "autobuild", | ||
"reason": nextBuildReason, | ||
"template_version_name": activeTemplateVersion.Name, | ||
}, "autobuild", | ||
// Associate this notification with all the related entities. | ||
ws.ID, ws.OwnerID, ws.TemplateID, ws.OrganizationID, | ||
); err != nil { | ||
log.Warn(e.ctx, "failed to notify of autoupdated workspace", slog.Error(err)) | ||
mtojek marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
} | ||
} | ||
if err != nil { | ||
return xerrors.Errorf("transition workspace: %w", err) | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -64,6 +64,8 @@ import ( | ||
"github.com/coder/coder/v2/coderd/externalauth" | ||
"github.com/coder/coder/v2/coderd/gitsshkey" | ||
"github.com/coder/coder/v2/coderd/httpmw" | ||
"github.com/coder/coder/v2/coderd/notifications" | ||
"github.com/coder/coder/v2/coderd/notifications/notiffake" | ||
"github.com/coder/coder/v2/coderd/rbac" | ||
"github.com/coder/coder/v2/coderd/schedule" | ||
"github.com/coder/coder/v2/coderd/telemetry" | ||
@@ -154,6 +156,8 @@ type Options struct { | ||
DatabaseRolluper *dbrollup.Rolluper | ||
WorkspaceUsageTrackerFlush chan int | ||
WorkspaceUsageTrackerTick chan time.Time | ||
NotificationsEnqueuer notifications.Enqueuer | ||
} | ||
// 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 | ||
options.Database, options.Pubsub = dbtestutil.NewDB(t) | ||
} | ||
if options.NotificationsEnqueuer == nil { | ||
options.NotificationsEnqueuer = new(notiffake.FakeNotificationEnqueuer) | ||
mtojek marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
} | ||
accessControlStore := &atomic.Pointer[dbauthz.AccessControlStore]{} | ||
var acs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{} | ||
accessControlStore.Store(&acs) | ||
@@ -305,6 +313,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can | ||
accessControlStore, | ||
*options.Logger, | ||
options.AutobuildTicker, | ||
options.NotificationsEnqueuer, | ||
).WithStatsChannel(options.AutobuildStats) | ||
lifecycleExecutor.Run() | ||
@@ -498,6 +507,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can | ||
NewTicker: options.NewTicker, | ||
DatabaseRolluper: options.DatabaseRolluper, | ||
WorkspaceUsageTracker: wuTracker, | ||
NotificationsEnqueuer: options.NotificationsEnqueuer, | ||
} | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
DELETE FROM notification_templates WHERE id = 'c34a0c09-0704-4cac-bd1c-0c0146811c2b'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) | ||
VALUES ('c34a0c09-0704-4cac-bd1c-0c0146811c2b', 'Workspace updated automatically', E'Workspace "{{.Labels.name}}" updated automatically', | ||
E'Hi {{.UserName}}\n\Your workspace **{{.Labels.name}}** has been updated automatically to the latest template version ({{.Labels.template_version_name}}).', | ||
'Workspace Events', '[ | ||
{ | ||
"label": "View workspace", | ||
"url": "{{ base_url }}/@{{.UserName}}/{{.Labels.name}}" | ||
} | ||
]'::jsonb); |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. In the other PR, we did this a bit differently. We added this fake helper into the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Yes, I'm aware of it. This is an alternative approach, and I'm happy to leave it or switch to the other form.@dannykopping any preference? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I prefer names which are explicit and clear, so I prefer MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I will wait with merging this PR until Bruno merges#13868, then adjust it. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package notiffake | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Nit: I like splitting this out but I think the name is too "cute". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Alright, I decided to merge it as is, and refactor/unify once#13868 is merged. | ||
import ( | ||
"context" | ||
"sync" | ||
"github.com/google/uuid" | ||
) | ||
type FakeNotificationEnqueuer struct { | ||
mu sync.Mutex | ||
Sent []*Notification | ||
} | ||
type Notification struct { | ||
UserID, TemplateID uuid.UUID | ||
Labels map[string]string | ||
CreatedBy string | ||
Targets []uuid.UUID | ||
} | ||
func (f *FakeNotificationEnqueuer) Enqueue(_ context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { | ||
f.mu.Lock() | ||
defer f.mu.Unlock() | ||
f.Sent = append(f.Sent, &Notification{ | ||
UserID: userID, | ||
TemplateID: templateID, | ||
Labels: labels, | ||
CreatedBy: createdBy, | ||
Targets: targets, | ||
}) | ||
id := uuid.New() | ||
return &id, nil | ||
} |
Uh oh!
There was an error while loading.Please reload this page.