feat: notify on successful autoupdate (#13903)

This commit is contained in:
Marcin Tojek
2024-07-18 15:19:12 +02:00
committed by GitHub
parent 44924cd8d8
commit fbd1d7f9a7
8 changed files with 130 additions and 11 deletions
+1 -1
View File
@@ -1066,7 +1066,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
autobuildTicker := time.NewTicker(vals.AutobuildPollInterval.Value())
defer autobuildTicker.Stop()
autobuildExecutor := autobuild.NewExecutor(
ctx, options.Database, options.Pubsub, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, coderAPI.AccessControlStore, logger, autobuildTicker.C)
ctx, options.Database, options.Pubsub, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, coderAPI.AccessControlStore, logger, autobuildTicker.C, options.NotificationsEnqueuer)
autobuildExecutor.Run()
hangDetectorTicker := time.NewTicker(vals.JobHangDetectorInterval.Value())
+45 -3
View File
@@ -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) *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)
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
}
}
_, job, err = builder.Build(e.ctx, tx, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
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))
}
}
if err != nil {
return xerrors.Errorf("transition workspace: %w", err)
}
+26 -7
View File
@@ -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/notiffake"
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/coderd/schedule/cron"
"github.com/coder/coder/v2/coderd/util/ptr"
@@ -79,6 +80,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
compatibleParameters bool
expectStart bool
expectUpdate bool
expectNotification bool
}{
{
name: "Never",
@@ -93,6 +95,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
compatibleParameters: true,
expectStart: true,
expectUpdate: true,
expectNotification: true,
},
{
name: "Always_Incompatible",
@@ -107,17 +110,19 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var (
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
ctx = context.Background()
err error
tickCh = make(chan time.Time)
statsCh = make(chan autobuild.Stats)
logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: !tc.expectStart}).Leveled(slog.LevelDebug)
client = coderdtest.New(t, &coderdtest.Options{
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
ctx = context.Background()
err error
tickCh = make(chan time.Time)
statsCh = make(chan autobuild.Stats)
logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: !tc.expectStart}).Leveled(slog.LevelDebug)
enqueuer = notiffake.FakeNotificationEnqueuer{}
client = coderdtest.New(t, &coderdtest.Options{
AutobuildTicker: tickCh,
IncludeProvisionerDaemon: true,
AutobuildStats: statsCh,
Logger: &logger,
NotificationsEnqueuer: &enqueuer,
})
// Given: we have a user with a workspace that has autostart enabled
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
@@ -195,6 +200,20 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
assert.Equal(t, workspace.LatestBuild.TemplateVersionID, ws.LatestBuild.TemplateVersionID,
"expected workspace build to be using the old template version")
}
if tc.expectNotification {
require.Len(t, enqueuer.Sent, 1)
require.Equal(t, enqueuer.Sent[0].UserID, workspace.OwnerID)
require.Contains(t, enqueuer.Sent[0].Targets, workspace.TemplateID)
require.Contains(t, enqueuer.Sent[0].Targets, workspace.ID)
require.Contains(t, enqueuer.Sent[0].Targets, workspace.OrganizationID)
require.Contains(t, enqueuer.Sent[0].Targets, workspace.OwnerID)
require.Equal(t, newVersion.Name, enqueuer.Sent[0].Labels["template_version_name"])
require.Equal(t, "autobuild", enqueuer.Sent[0].Labels["initiator"])
require.Equal(t, "autostart", enqueuer.Sent[0].Labels["reason"])
} else {
require.Len(t, enqueuer.Sent, 0)
}
})
}
}
+10
View File
@@ -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)
}
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,
}
}
@@ -0,0 +1 @@
DELETE FROM notification_templates WHERE id = 'c34a0c09-0704-4cac-bd1c-0c0146811c2b';
@@ -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);
+1
View File
@@ -9,4 +9,5 @@ import "github.com/google/uuid"
var (
TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed")
WorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9")
WorkspaceAutoUpdated = uuid.MustParse("c34a0c09-0704-4cac-bd1c-0c0146811c2b")
)
@@ -0,0 +1,37 @@
package notiffake
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
}