Skip to content

Commit 6de5937

Browse files
authored
feat: notifications: report failed workspace builds (#14571)
1 parent 1e5438e commit 6de5937

29 files changed

+1545
-55
lines changed

cli/server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import (
5656
"cdr.dev/slog"
5757
"cdr.dev/slog/sloggers/sloghuman"
5858
"github.com/coder/coder/v2/coderd/entitlements"
59+
"github.com/coder/coder/v2/coderd/notifications/reports"
5960
"github.com/coder/coder/v2/coderd/runtimeconfig"
6061
"github.com/coder/pretty"
6162
"github.com/coder/quartz"
@@ -1018,6 +1019,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
10181019

10191020
// nolint:gocritic // TODO: create own role.
10201021
notificationsManager.Run(dbauthz.AsSystemRestricted(ctx))
1022+
1023+
// Run report generator to distribute periodic reports.
1024+
notificationReportGenerator := reports.NewReportGenerator(ctx, logger, options.Database, options.NotificationsEnqueuer, quartz.NewReal())
1025+
defer notificationReportGenerator.Close()
10211026
}
10221027

10231028
// Wrap the server in middleware that redirects to the access URL if

coderd/database/dbauthz/dbauthz.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,6 +1459,13 @@ func (q *querier) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.
14591459
return fetchWithPostFilter(q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLinksByUserID)(ctx, userID)
14601460
}
14611461

1462+
func (q *querier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.GetFailedWorkspaceBuildsByTemplateIDRow, error) {
1463+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
1464+
return nil, err
1465+
}
1466+
return q.db.GetFailedWorkspaceBuildsByTemplateID(ctx, arg)
1467+
}
1468+
14621469
func (q *querier) GetFileByHashAndCreator(ctx context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) {
14631470
file, err := q.db.GetFileByHashAndCreator(ctx, arg)
14641471
if err != nil {
@@ -1628,6 +1635,13 @@ func (q *querier) GetNotificationMessagesByStatus(ctx context.Context, arg datab
16281635
return q.db.GetNotificationMessagesByStatus(ctx, arg)
16291636
}
16301637

1638+
func (q *querier) GetNotificationReportGeneratorLogByTemplate(ctx context.Context, arg uuid.UUID) (database.NotificationReportGeneratorLog, error) {
1639+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
1640+
return database.NotificationReportGeneratorLog{}, err
1641+
}
1642+
return q.db.GetNotificationReportGeneratorLogByTemplate(ctx, arg)
1643+
}
1644+
16311645
func (q *querier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (database.NotificationTemplate, error) {
16321646
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationTemplate); err != nil {
16331647
return database.NotificationTemplate{}, err
@@ -2510,6 +2524,13 @@ func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuil
25102524
return q.db.GetWorkspaceBuildParameters(ctx, workspaceBuildID)
25112525
}
25122526

2527+
func (q *querier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
2528+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
2529+
return nil, err
2530+
}
2531+
return q.db.GetWorkspaceBuildStatsByTemplates(ctx, since)
2532+
}
2533+
25132534
func (q *querier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg database.GetWorkspaceBuildsByWorkspaceIDParams) ([]database.WorkspaceBuild, error) {
25142535
if _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID); err != nil {
25152536
return nil, err
@@ -3966,6 +3987,13 @@ func (q *querier) UpsertLogoURL(ctx context.Context, value string) error {
39663987
return q.db.UpsertLogoURL(ctx, value)
39673988
}
39683989

3990+
func (q *querier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error {
3991+
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil {
3992+
return err
3993+
}
3994+
return q.db.UpsertNotificationReportGeneratorLog(ctx, arg)
3995+
}
3996+
39693997
func (q *querier) UpsertNotificationsSettings(ctx context.Context, value string) error {
39703998
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
39713999
return err

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2819,6 +2819,28 @@ func (s *MethodTestSuite) TestSystemFunctions() {
28192819
Value: "value",
28202820
}).Asserts(rbac.ResourceSystem, policy.ActionCreate)
28212821
}))
2822+
s.Run("GetFailedWorkspaceBuildsByTemplateID", s.Subtest(func(db database.Store, check *expects) {
2823+
check.Args(database.GetFailedWorkspaceBuildsByTemplateIDParams{
2824+
TemplateID: uuid.New(),
2825+
Since: dbtime.Now(),
2826+
}).Asserts(rbac.ResourceSystem, policy.ActionRead)
2827+
}))
2828+
s.Run("GetNotificationReportGeneratorLogByTemplate", s.Subtest(func(db database.Store, check *expects) {
2829+
_ = db.UpsertNotificationReportGeneratorLog(context.Background(), database.UpsertNotificationReportGeneratorLogParams{
2830+
NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport,
2831+
LastGeneratedAt: dbtime.Now(),
2832+
})
2833+
check.Args(notifications.TemplateWorkspaceBuildsFailedReport).Asserts(rbac.ResourceSystem, policy.ActionRead)
2834+
}))
2835+
s.Run("GetWorkspaceBuildStatsByTemplates", s.Subtest(func(db database.Store, check *expects) {
2836+
check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead)
2837+
}))
2838+
s.Run("UpsertNotificationReportGeneratorLog", s.Subtest(func(db database.Store, check *expects) {
2839+
check.Args(database.UpsertNotificationReportGeneratorLogParams{
2840+
NotificationTemplateID: uuid.New(),
2841+
LastGeneratedAt: dbtime.Now(),
2842+
}).Asserts(rbac.ResourceSystem, policy.ActionCreate)
2843+
}))
28222844
}
28232845

28242846
func (s *MethodTestSuite) TestNotifications() {

coderd/database/dbmem/dbmem.go

Lines changed: 211 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -187,53 +187,54 @@ type data struct {
187187
userLinks []database.UserLink
188188

189189
// New tables
190-
workspaceAgentStats []database.WorkspaceAgentStat
191-
auditLogs []database.AuditLog
192-
cryptoKeys []database.CryptoKey
193-
dbcryptKeys []database.DBCryptKey
194-
files []database.File
195-
externalAuthLinks []database.ExternalAuthLink
196-
gitSSHKey []database.GitSSHKey
197-
groupMembers []database.GroupMemberTable
198-
groups []database.Group
199-
jfrogXRayScans []database.JfrogXrayScan
200-
licenses []database.License
201-
notificationMessages []database.NotificationMessage
202-
notificationPreferences []database.NotificationPreference
203-
oauth2ProviderApps []database.OAuth2ProviderApp
204-
oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret
205-
oauth2ProviderAppCodes []database.OAuth2ProviderAppCode
206-
oauth2ProviderAppTokens []database.OAuth2ProviderAppToken
207-
parameterSchemas []database.ParameterSchema
208-
provisionerDaemons []database.ProvisionerDaemon
209-
provisionerJobLogs []database.ProvisionerJobLog
210-
provisionerJobs []database.ProvisionerJob
211-
provisionerKeys []database.ProvisionerKey
212-
replicas []database.Replica
213-
templateVersions []database.TemplateVersionTable
214-
templateVersionParameters []database.TemplateVersionParameter
215-
templateVersionVariables []database.TemplateVersionVariable
216-
templateVersionWorkspaceTags []database.TemplateVersionWorkspaceTag
217-
templates []database.TemplateTable
218-
templateUsageStats []database.TemplateUsageStat
219-
workspaceAgents []database.WorkspaceAgent
220-
workspaceAgentMetadata []database.WorkspaceAgentMetadatum
221-
workspaceAgentLogs []database.WorkspaceAgentLog
222-
workspaceAgentLogSources []database.WorkspaceAgentLogSource
223-
workspaceAgentScripts []database.WorkspaceAgentScript
224-
workspaceAgentPortShares []database.WorkspaceAgentPortShare
225-
workspaceApps []database.WorkspaceApp
226-
workspaceAppStatsLastInsertID int64
227-
workspaceAppStats []database.WorkspaceAppStat
228-
workspaceBuilds []database.WorkspaceBuild
229-
workspaceBuildParameters []database.WorkspaceBuildParameter
230-
workspaceResourceMetadata []database.WorkspaceResourceMetadatum
231-
workspaceResources []database.WorkspaceResource
232-
workspaces []database.Workspace
233-
workspaceProxies []database.WorkspaceProxy
234-
customRoles []database.CustomRole
235-
provisionerJobTimings []database.ProvisionerJobTiming
236-
runtimeConfig map[string]string
190+
auditLogs []database.AuditLog
191+
cryptoKeys []database.CryptoKey
192+
dbcryptKeys []database.DBCryptKey
193+
files []database.File
194+
externalAuthLinks []database.ExternalAuthLink
195+
gitSSHKey []database.GitSSHKey
196+
groupMembers []database.GroupMemberTable
197+
groups []database.Group
198+
jfrogXRayScans []database.JfrogXrayScan
199+
licenses []database.License
200+
notificationMessages []database.NotificationMessage
201+
notificationPreferences []database.NotificationPreference
202+
notificationReportGeneratorLogs []database.NotificationReportGeneratorLog
203+
oauth2ProviderApps []database.OAuth2ProviderApp
204+
oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret
205+
oauth2ProviderAppCodes []database.OAuth2ProviderAppCode
206+
oauth2ProviderAppTokens []database.OAuth2ProviderAppToken
207+
parameterSchemas []database.ParameterSchema
208+
provisionerDaemons []database.ProvisionerDaemon
209+
provisionerJobLogs []database.ProvisionerJobLog
210+
provisionerJobs []database.ProvisionerJob
211+
provisionerKeys []database.ProvisionerKey
212+
replicas []database.Replica
213+
templateVersions []database.TemplateVersionTable
214+
templateVersionParameters []database.TemplateVersionParameter
215+
templateVersionVariables []database.TemplateVersionVariable
216+
templateVersionWorkspaceTags []database.TemplateVersionWorkspaceTag
217+
templates []database.TemplateTable
218+
templateUsageStats []database.TemplateUsageStat
219+
workspaceAgents []database.WorkspaceAgent
220+
workspaceAgentMetadata []database.WorkspaceAgentMetadatum
221+
workspaceAgentLogs []database.WorkspaceAgentLog
222+
workspaceAgentLogSources []database.WorkspaceAgentLogSource
223+
workspaceAgentPortShares []database.WorkspaceAgentPortShare
224+
workspaceAgentScripts []database.WorkspaceAgentScript
225+
workspaceAgentStats []database.WorkspaceAgentStat
226+
workspaceApps []database.WorkspaceApp
227+
workspaceAppStatsLastInsertID int64
228+
workspaceAppStats []database.WorkspaceAppStat
229+
workspaceBuilds []database.WorkspaceBuild
230+
workspaceBuildParameters []database.WorkspaceBuildParameter
231+
workspaceResourceMetadata []database.WorkspaceResourceMetadatum
232+
workspaceResources []database.WorkspaceResource
233+
workspaces []database.Workspace
234+
workspaceProxies []database.WorkspaceProxy
235+
customRoles []database.CustomRole
236+
provisionerJobTimings []database.ProvisionerJobTiming
237+
runtimeConfig map[string]string
237238
// Locks is a map of lock names. Any keys within the map are currently
238239
// locked.
239240
locks map[int64]struct{}
@@ -2621,6 +2622,75 @@ func (q *FakeQuerier) GetExternalAuthLinksByUserID(_ context.Context, userID uui
26212622
return gals, nil
26222623
}
26232624

2625+
func (q *FakeQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.GetFailedWorkspaceBuildsByTemplateIDRow, error) {
2626+
err := validateDatabaseType(arg)
2627+
if err != nil {
2628+
return nil, err
2629+
}
2630+
2631+
q.mutex.RLock()
2632+
defer q.mutex.RUnlock()
2633+
2634+
workspaceBuildStats := []database.GetFailedWorkspaceBuildsByTemplateIDRow{}
2635+
for _, wb := range q.workspaceBuilds {
2636+
job, err := q.getProvisionerJobByIDNoLock(ctx, wb.JobID)
2637+
if err != nil {
2638+
return nil, xerrors.Errorf("get provisioner job by ID: %w", err)
2639+
}
2640+
2641+
if job.JobStatus != database.ProvisionerJobStatusFailed {
2642+
continue
2643+
}
2644+
2645+
if !job.CompletedAt.Valid {
2646+
continue
2647+
}
2648+
2649+
if wb.CreatedAt.Before(arg.Since) {
2650+
continue
2651+
}
2652+
2653+
w, err := q.getWorkspaceByIDNoLock(ctx, wb.WorkspaceID)
2654+
if err != nil {
2655+
return nil, xerrors.Errorf("get workspace by ID: %w", err)
2656+
}
2657+
2658+
t, err := q.getTemplateByIDNoLock(ctx, w.TemplateID)
2659+
if err != nil {
2660+
return nil, xerrors.Errorf("get template by ID: %w", err)
2661+
}
2662+
2663+
if t.ID != arg.TemplateID {
2664+
continue
2665+
}
2666+
2667+
workspaceOwner, err := q.getUserByIDNoLock(w.OwnerID)
2668+
if err != nil {
2669+
return nil, xerrors.Errorf("get user by ID: %w", err)
2670+
}
2671+
2672+
templateVersion, err := q.getTemplateVersionByIDNoLock(ctx, wb.TemplateVersionID)
2673+
if err != nil {
2674+
return nil, xerrors.Errorf("get template version by ID: %w", err)
2675+
}
2676+
2677+
workspaceBuildStats = append(workspaceBuildStats, database.GetFailedWorkspaceBuildsByTemplateIDRow{
2678+
WorkspaceName: w.Name,
2679+
WorkspaceOwnerUsername: workspaceOwner.Username,
2680+
TemplateVersionName: templateVersion.Name,
2681+
WorkspaceBuildNumber: wb.BuildNumber,
2682+
})
2683+
}
2684+
2685+
sort.Slice(workspaceBuildStats, func(i, j int) bool {
2686+
if workspaceBuildStats[i].TemplateVersionName != workspaceBuildStats[j].TemplateVersionName {
2687+
return workspaceBuildStats[i].TemplateVersionName < workspaceBuildStats[j].TemplateVersionName
2688+
}
2689+
return workspaceBuildStats[i].WorkspaceBuildNumber > workspaceBuildStats[j].WorkspaceBuildNumber
2690+
})
2691+
return workspaceBuildStats, nil
2692+
}
2693+
26242694
func (q *FakeQuerier) GetFileByHashAndCreator(_ context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) {
26252695
if err := validateDatabaseType(arg); err != nil {
26262696
return database.File{}, err
@@ -3044,6 +3114,23 @@ func (q *FakeQuerier) GetNotificationMessagesByStatus(_ context.Context, arg dat
30443114
return out, nil
30453115
}
30463116

3117+
func (q *FakeQuerier) GetNotificationReportGeneratorLogByTemplate(_ context.Context, templateID uuid.UUID) (database.NotificationReportGeneratorLog, error) {
3118+
err := validateDatabaseType(templateID)
3119+
if err != nil {
3120+
return database.NotificationReportGeneratorLog{}, err
3121+
}
3122+
3123+
q.mutex.RLock()
3124+
defer q.mutex.RUnlock()
3125+
3126+
for _, record := range q.notificationReportGeneratorLogs {
3127+
if record.NotificationTemplateID == templateID {
3128+
return record, nil
3129+
}
3130+
}
3131+
return database.NotificationReportGeneratorLog{}, sql.ErrNoRows
3132+
}
3133+
30473134
func (*FakeQuerier) GetNotificationTemplateByID(_ context.Context, _ uuid.UUID) (database.NotificationTemplate, error) {
30483135
// Not implementing this function because it relies on state in the database which is created with migrations.
30493136
// We could consider using code-generation to align the database state and dbmem, but it's not worth it right now.
@@ -5964,6 +6051,63 @@ func (q *FakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBu
59646051
return params, nil
59656052
}
59666053

6054+
func (q *FakeQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
6055+
q.mutex.RLock()
6056+
defer q.mutex.RUnlock()
6057+
6058+
templateStats := map[uuid.UUID]database.GetWorkspaceBuildStatsByTemplatesRow{}
6059+
for _, wb := range q.workspaceBuilds {
6060+
job, err := q.getProvisionerJobByIDNoLock(ctx, wb.JobID)
6061+
if err != nil {
6062+
return nil, xerrors.Errorf("get provisioner job by ID: %w", err)
6063+
}
6064+
6065+
if !job.CompletedAt.Valid {
6066+
continue
6067+
}
6068+
6069+
if wb.CreatedAt.Before(since) {
6070+
continue
6071+
}
6072+
6073+
w, err := q.getWorkspaceByIDNoLock(ctx, wb.WorkspaceID)
6074+
if err != nil {
6075+
return nil, xerrors.Errorf("get workspace by ID: %w", err)
6076+
}
6077+
6078+
if _, ok := templateStats[w.TemplateID]; !ok {
6079+
t, err := q.getTemplateByIDNoLock(ctx, w.TemplateID)
6080+
if err != nil {
6081+
return nil, xerrors.Errorf("get template by ID: %w", err)
6082+
}
6083+
6084+
templateStats[w.TemplateID] = database.GetWorkspaceBuildStatsByTemplatesRow{
6085+
TemplateID: w.TemplateID,
6086+
TemplateName: t.Name,
6087+
TemplateDisplayName: t.DisplayName,
6088+
TemplateOrganizationID: w.OrganizationID,
6089+
}
6090+
}
6091+
6092+
s := templateStats[w.TemplateID]
6093+
s.TotalBuilds++
6094+
if job.JobStatus == database.ProvisionerJobStatusFailed {
6095+
s.FailedBuilds++
6096+
}
6097+
templateStats[w.TemplateID] = s
6098+
}
6099+
6100+
rows := make([]database.GetWorkspaceBuildStatsByTemplatesRow, 0, len(templateStats))
6101+
for _, ts := range templateStats {
6102+
rows = append(rows, ts)
6103+
}
6104+
6105+
sort.Slice(rows, func(i, j int) bool {
6106+
return rows[i].TemplateName < rows[j].TemplateName
6107+
})
6108+
return rows, nil
6109+
}
6110+
59676111
func (q *FakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context,
59686112
params database.GetWorkspaceBuildsByWorkspaceIDParams,
59696113
) ([]database.WorkspaceBuild, error) {
@@ -9440,6 +9584,26 @@ func (q *FakeQuerier) UpsertLogoURL(_ context.Context, data string) error {
94409584
return nil
94419585
}
94429586

9587+
func (q *FakeQuerier) UpsertNotificationReportGeneratorLog(_ context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error {
9588+
err := validateDatabaseType(arg)
9589+
if err != nil {
9590+
return err
9591+
}
9592+
9593+
q.mutex.Lock()
9594+
defer q.mutex.Unlock()
9595+
9596+
for i, record := range q.notificationReportGeneratorLogs {
9597+
if arg.NotificationTemplateID == record.NotificationTemplateID {
9598+
q.notificationReportGeneratorLogs[i].LastGeneratedAt = arg.LastGeneratedAt
9599+
return nil
9600+
}
9601+
}
9602+
9603+
q.notificationReportGeneratorLogs = append(q.notificationReportGeneratorLogs, database.NotificationReportGeneratorLog(arg))
9604+
return nil
9605+
}
9606+
94439607
func (q *FakeQuerier) UpsertNotificationsSettings(_ context.Context, data string) error {
94449608
q.mutex.Lock()
94459609
defer q.mutex.Unlock()

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