Skip to content

feat: notify template owner on manual build failures #14262

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Prev Previous commit
Next Next commit
Apply dannys suggestions
  • Loading branch information
BrunoQuaresma committed Aug 14, 2024
commit cdf247b5b1a5a1c4e6a6cd9723a920c5ff0e6c5b
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ VALUES (
"url": "{{ base_url }}/@{{.Labels.workspaceUserName}}/{{.Labels.workspaceName}}/builds/{{.Labels.buildNumber}}"
},
{
"label": "View Template",
"label": "View template",
"url": "{{ base_url }}/templates/{{.Labels.name}}"
}
]'::jsonb
Expand Down
38 changes: 1 addition & 37 deletions coderd/provisionerdserver/provisionerdserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,7 @@ func (s *server) notifyTemplateManualBuildFailed(ctx context.Context, workspace
// Associate this notification with all the related entities.
workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID, template.CreatedBy,
); err != nil {
s.Logger.Warn(ctx, "failed to notify of failed template autobuild", slog.F("template_id", template.ID), slog.F("workspace_id", workspace.ID), slog.Error(err))
s.Logger.Warn(ctx, "failed to notify of failed template manual build", slog.F("template_id", template.ID), slog.F("workspace_id", workspace.ID), slog.Error(err))
}
}

Expand Down Expand Up @@ -1615,42 +1615,6 @@ func (s *server) notifyWorkspaceDeleted(ctx context.Context, workspace database.
}
}

func (s *server) notifyTemplateOwnerAboutManualBuildFailure(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:
if build.InitiatorID == workspace.OwnerID {
// Deletions initiated by self should not notify.
return
}

reason = "initiated by user"
case database.BuildReasonAutodelete:
reason = "autodeleted due to dormancy"
initiator = "autobuild"
default:
reason = string(build.Reason)
}
} else {
reason = string(build.Reason)
s.Logger.Warn(ctx, "invalid build reason when sending deletion notification",
slog.F("reason", reason), slog.F("workspace_id", workspace.ID), slog.F("build_id", build.ID))
}

if _, err := s.NotificationsEnqueuer.Enqueue(ctx, workspace.OwnerID, notifications.TemplateWorkspaceDeleted,
map[string]string{
"name": workspace.Name,
"reason": reason,
"initiator": initiator,
}, "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 workspace deletion", slog.Error(err))
}
}
func (s *server) startTrace(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return s.Tracer.Start(ctx, name, append(opts, trace.WithAttributes(
semconv.ServiceNameKey.String("coderd.provisionerd"),
Expand Down
252 changes: 102 additions & 150 deletions coderd/provisionerdserver/provisionerdserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1699,8 +1699,7 @@ func TestNotifications(t *testing.T) {
t.Parallel()

tests := []struct {
name string

name string
buildReason database.BuildReason
shouldNotify bool
}{
Expand Down Expand Up @@ -1808,163 +1807,116 @@ func TestNotifications(t *testing.T) {
t.Run("TemplateManualBuildFailed", func(t *testing.T) {
t.Parallel()

t.Run("NotifyTemplateOwner", func(t *testing.T) {
t.Parallel()

ctx := context.Background()
notifEnq := &testutil.FakeNotificationsEnqueuer{}

// 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{
var (
ctx = context.Background()
notifEnq = &testutil.FakeNotificationsEnqueuer{}
// To avoid spamming the output, ignore log errors. This test is
// designed to check a build failure, which is expected to log errors.
ignoreLogErrors = true
srv, db, ps, pd = setup(t, ignoreLogErrors, &overrides{
notificationEnqueuer: notifEnq,
})
userA = dbgen.User(t, db, database.User{})
userB = dbgen.User(t, db, database.User{})
)

initiator := dbgen.User(t, db, database.User{})
templateOwner := dbgen.User(t, db, database.User{})

template := dbgen.Template(t, db, database.Template{
Name: "template",
Provisioner: database.ProvisionerTypeEcho,
OrganizationID: pd.OrganizationID,
CreatedBy: templateOwner.ID,
})
template, err := db.GetTemplateByID(ctx, template.ID)
require.NoError(t, err)
file := dbgen.File(t, db, database.File{CreatedBy: initiator.ID})
workspace := dbgen.Workspace(t, db, database.Workspace{
TemplateID: template.ID,
OwnerID: initiator.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: database.BuildReasonInitiator,
})
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)
tc := []struct {
name string
owner database.User
initiator database.User
shouldNotify bool
}{
{
name: "InitiatedByOwner",
owner: userA,
initiator: userA,
shouldNotify: false,
},
{
name: "InitiatedBySomeoneElse",
owner: userB,
initiator: userA,
shouldNotify: true,
},
}

_, err = srv.FailJob(ctx, &proto.FailedJob{
JobId: job.ID.String(),
Type: &proto.FailedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{
State: []byte{},
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
// Given: a template created by the owner
template := dbgen.Template(t, db, database.Template{
Name: "template",
Provisioner: database.ProvisionerTypeEcho,
OrganizationID: pd.OrganizationID,
CreatedBy: c.owner.ID,
})
template, err := db.GetTemplateByID(ctx, template.ID)
require.NoError(t, err)
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: pd.OrganizationID,
TemplateID: uuid.NullUUID{
UUID: template.ID,
Valid: true,
},
},
})
require.NoError(t, err)

require.Len(t, notifEnq.Sent, 1)
require.Equal(t, notifEnq.Sent[0].UserID, templateOwner.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, templateOwner.ID)
require.Contains(t, notifEnq.Sent[0].Targets, initiator.ID)
})

t.Run("DoNotNotifyTheInitiator", func(t *testing.T) {
t.Parallel()

ctx := context.Background()
notifEnq := &testutil.FakeNotificationsEnqueuer{}

// 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,
})

initiator := dbgen.User(t, db, database.User{})
templateOwner := initiator
JobID: uuid.New(),
})

template := dbgen.Template(t, db, database.Template{
Name: "template",
Provisioner: database.ProvisionerTypeEcho,
OrganizationID: pd.OrganizationID,
CreatedBy: templateOwner.ID,
})
template, err := db.GetTemplateByID(ctx, template.ID)
require.NoError(t, err)
file := dbgen.File(t, db, database.File{CreatedBy: initiator.ID})
workspace := dbgen.Workspace(t, db, database.Workspace{
TemplateID: template.ID,
OwnerID: initiator.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: database.BuildReasonInitiator,
})
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)
// And: a workspace build initiated manually by a user
workspace := dbgen.Workspace(t, db, database.Workspace{
TemplateID: template.ID,
OwnerID: c.initiator.ID,
OrganizationID: pd.OrganizationID,
})
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: workspace.ID,
TemplateVersionID: version.ID,
InitiatorID: c.initiator.ID,
Transition: database.WorkspaceTransitionDelete,
Reason: database.BuildReasonInitiator,
})
file := dbgen.File(t, db, database.File{CreatedBy: c.initiator.ID})
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{},
// When: the workspace build job fails
_, err = srv.FailJob(ctx, &proto.FailedJob{
JobId: job.ID.String(),
Type: &proto.FailedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{
State: []byte{},
},
},
},
})
require.NoError(t, err)
})
require.NoError(t, err)

require.Len(t, notifEnq.Sent, 0)
})
// Then: send the appropriate notifications
if c.shouldNotify {
require.Len(t, notifEnq.Sent, 1)
require.Equal(t, notifEnq.Sent[0].UserID, c.owner.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, c.owner.ID)
require.Contains(t, notifEnq.Sent[0].Targets, c.initiator.ID)
} else {
require.Len(t, notifEnq.Sent, 0)
}
})
}
})
}

Expand Down
Loading