mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: notify owner about failed autobuild (#13891)
This commit is contained in:
@@ -982,12 +982,18 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto.
|
||||
}
|
||||
|
||||
var build database.WorkspaceBuild
|
||||
var workspace database.Workspace
|
||||
err = s.Database.InTx(func(db database.Store) error {
|
||||
build, err = db.GetWorkspaceBuildByID(ctx, input.WorkspaceBuildID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace build: %w", err)
|
||||
}
|
||||
|
||||
workspace, err = db.GetWorkspaceByID(ctx, build.WorkspaceID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace: %w", err)
|
||||
}
|
||||
|
||||
if jobType.WorkspaceBuild.State != nil {
|
||||
err = db.UpdateWorkspaceBuildProvisionerStateByID(ctx, database.UpdateWorkspaceBuildProvisionerStateByIDParams{
|
||||
ID: input.WorkspaceBuildID,
|
||||
@@ -1014,6 +1020,8 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.notifyWorkspaceBuildFailed(ctx, workspace, build)
|
||||
|
||||
err = s.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(build.WorkspaceID), []byte{})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("update workspace: %w", err)
|
||||
@@ -1087,6 +1095,27 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto.
|
||||
return &proto.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *server) notifyWorkspaceBuildFailed(ctx context.Context, workspace database.Workspace, build database.WorkspaceBuild) {
|
||||
var reason string
|
||||
if build.Reason.Valid() && build.Reason == database.BuildReasonInitiator {
|
||||
return // failed workspace build initiated by a user should not notify
|
||||
}
|
||||
reason = string(build.Reason)
|
||||
initiator := "autobuild"
|
||||
|
||||
if _, err := s.NotificationEnqueuer.Enqueue(ctx, workspace.OwnerID, notifications.WorkspaceAutobuildFailed,
|
||||
map[string]string{
|
||||
"name": workspace.Name,
|
||||
"initiator": initiator,
|
||||
"reason": reason,
|
||||
}, "provisionerdserver",
|
||||
// Associate this notification with all the related entities.
|
||||
workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID,
|
||||
); err != nil {
|
||||
s.Logger.Warn(ctx, "failed to notify of failed workspace autobuild", slog.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteJob is triggered by a provision daemon to mark a provisioner job as completed.
|
||||
func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) (*proto.Empty, error) {
|
||||
ctx, span := s.startTrace(ctx, tracing.FuncName())
|
||||
@@ -1523,6 +1552,7 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
|
||||
|
||||
func (s *server) notifyWorkspaceDeleted(ctx context.Context, workspace database.Workspace, build database.WorkspaceBuild) {
|
||||
var reason string
|
||||
initiator := build.InitiatorByUsername
|
||||
if build.Reason.Valid() {
|
||||
switch build.Reason {
|
||||
case database.BuildReasonInitiator:
|
||||
@@ -1534,6 +1564,7 @@ func (s *server) notifyWorkspaceDeleted(ctx context.Context, workspace database.
|
||||
reason = "initiated by user"
|
||||
case database.BuildReasonAutodelete:
|
||||
reason = "autodeleted due to dormancy"
|
||||
initiator = "autobuild"
|
||||
default:
|
||||
reason = string(build.Reason)
|
||||
}
|
||||
@@ -1545,9 +1576,9 @@ func (s *server) notifyWorkspaceDeleted(ctx context.Context, workspace database.
|
||||
|
||||
if _, err := s.NotificationEnqueuer.Enqueue(ctx, workspace.OwnerID, notifications.TemplateWorkspaceDeleted,
|
||||
map[string]string{
|
||||
"name": workspace.Name,
|
||||
"initiatedBy": build.InitiatorByUsername,
|
||||
"reason": reason,
|
||||
"name": workspace.Name,
|
||||
"reason": reason,
|
||||
"initiator": initiator,
|
||||
}, "provisionerdserver",
|
||||
// Associate this notification with all the related entities.
|
||||
workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID,
|
||||
|
||||
@@ -1687,7 +1687,7 @@ func TestNotifications(t *testing.T) {
|
||||
require.Contains(t, notifEnq.sent[0].targets, workspace.OrganizationID)
|
||||
require.Contains(t, notifEnq.sent[0].targets, user.ID)
|
||||
if tc.deletionReason == database.BuildReasonInitiator {
|
||||
require.Equal(t, notifEnq.sent[0].labels["initiatedBy"], initiator.Username)
|
||||
require.Equal(t, initiator.Username, notifEnq.sent[0].labels["initiator"])
|
||||
}
|
||||
} else {
|
||||
require.Len(t, notifEnq.sent, 0)
|
||||
@@ -1695,6 +1695,117 @@ func TestNotifications(t *testing.T) {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Workspace build failed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
buildReason database.BuildReason
|
||||
shouldNotify bool
|
||||
}{
|
||||
{
|
||||
name: "initiated by owner",
|
||||
buildReason: database.BuildReasonInitiator,
|
||||
shouldNotify: false,
|
||||
},
|
||||
{
|
||||
name: "initiated by autostart",
|
||||
buildReason: database.BuildReasonAutostart,
|
||||
shouldNotify: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
notifEnq := &fakeNotificationEnqueuer{}
|
||||
|
||||
// Otherwise `(*Server).FailJob` fails with:
|
||||
// audit log - get build {"error": "sql: no rows in result set"}
|
||||
ignoreLogErrors := true
|
||||
srv, db, ps, pd := setup(t, ignoreLogErrors, &overrides{
|
||||
notificationEnqueuer: notifEnq,
|
||||
})
|
||||
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
initiator := user
|
||||
|
||||
template := dbgen.Template(t, db, database.Template{
|
||||
Name: "template",
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
OrganizationID: pd.OrganizationID,
|
||||
})
|
||||
template, err := db.GetTemplateByID(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
file := dbgen.File(t, db, database.File{CreatedBy: user.ID})
|
||||
workspace := dbgen.Workspace(t, db, database.Workspace{
|
||||
TemplateID: template.ID,
|
||||
OwnerID: user.ID,
|
||||
OrganizationID: pd.OrganizationID,
|
||||
})
|
||||
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
OrganizationID: pd.OrganizationID,
|
||||
TemplateID: uuid.NullUUID{
|
||||
UUID: template.ID,
|
||||
Valid: true,
|
||||
},
|
||||
JobID: uuid.New(),
|
||||
})
|
||||
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||
WorkspaceID: workspace.ID,
|
||||
TemplateVersionID: version.ID,
|
||||
InitiatorID: initiator.ID,
|
||||
Transition: database.WorkspaceTransitionDelete,
|
||||
Reason: tc.buildReason,
|
||||
})
|
||||
job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
|
||||
FileID: file.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{
|
||||
WorkspaceBuildID: build.ID,
|
||||
})),
|
||||
OrganizationID: pd.OrganizationID,
|
||||
})
|
||||
_, err = db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
|
||||
OrganizationID: pd.OrganizationID,
|
||||
WorkerID: uuid.NullUUID{
|
||||
UUID: pd.ID,
|
||||
Valid: true,
|
||||
},
|
||||
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = srv.FailJob(ctx, &proto.FailedJob{
|
||||
JobId: job.ID.String(),
|
||||
Type: &proto.FailedJob_WorkspaceBuild_{
|
||||
WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{
|
||||
State: []byte{},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.shouldNotify {
|
||||
// Validate that the notification was sent and contained the expected values.
|
||||
require.Len(t, notifEnq.sent, 1)
|
||||
require.Equal(t, notifEnq.sent[0].userID, user.ID)
|
||||
require.Contains(t, notifEnq.sent[0].targets, template.ID)
|
||||
require.Contains(t, notifEnq.sent[0].targets, workspace.ID)
|
||||
require.Contains(t, notifEnq.sent[0].targets, workspace.OrganizationID)
|
||||
require.Contains(t, notifEnq.sent[0].targets, user.ID)
|
||||
require.Equal(t, "autobuild", notifEnq.sent[0].labels["initiator"])
|
||||
require.Equal(t, string(tc.buildReason), notifEnq.sent[0].labels["reason"])
|
||||
} else {
|
||||
require.Len(t, notifEnq.sent, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type overrides struct {
|
||||
|
||||
Reference in New Issue
Block a user