Skip to content

feat: notifications: report failed workspace builds #14571

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

Merged
merged 126 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
126 commits
Select commit Hold shift + click to select a range
9d71a83
just the template
mtojek Sep 5, 2024
7fc0b02
Bobby
mtojek Sep 5, 2024
1ff2a24
golden
mtojek Sep 5, 2024
15ede21
WIP
mtojek Sep 5, 2024
380c81d
generator
mtojek Sep 6, 2024
8aa1d9a
lock
mtojek Sep 6, 2024
fcd6d61
notif data
mtojek Sep 6, 2024
1a941f8
fix
mtojek Sep 6, 2024
be24cbd
fixmes
mtojek Sep 6, 2024
2fc383e
NoopEnqueuer
mtojek Sep 6, 2024
96c4062
fixme helpers
mtojek Sep 6, 2024
b89f484
Run generator
mtojek Sep 6, 2024
c9f99c5
fix
mtojek Sep 6, 2024
a4f4868
TODO
mtojek Sep 6, 2024
630c024
naming
mtojek Sep 6, 2024
9a414f0
fix lint
mtojek Sep 6, 2024
aef1287
TODO
mtojek Sep 9, 2024
f0f6df2
fix dbmock
mtojek Sep 9, 2024
f4e34a7
WIP
mtojek Sep 9, 2024
968bd24
TODOs
mtojek Sep 9, 2024
e771c05
WIP
mtojek Sep 10, 2024
33b1e99
fix
mtojek Sep 10, 2024
f614080
WIP
mtojek Sep 10, 2024
4d7a304
WIP
mtojek Sep 10, 2024
d5c212e
WIP
mtojek Sep 10, 2024
0ab35f1
WIP
mtojek Sep 10, 2024
fc804bd
another WIP
mtojek Sep 10, 2024
c25155d
WIP
mtojek Sep 10, 2024
6eb6794
WIP
mtojek Sep 10, 2024
0afbd56
WIP
mtojek Sep 10, 2024
d0331af
before input data
mtojek Sep 10, 2024
3d65010
Last TODO
mtojek Sep 10, 2024
3490c57
WIP
mtojek Sep 10, 2024
aafed7c
rebuild model
mtojek Sep 10, 2024
78ecccf
Merge branch 'main' into 40-failure-summary
mtojek Sep 10, 2024
2d0299a
WIP
mtojek Sep 10, 2024
38a3a50
WIP
mtojek Sep 10, 2024
4c65295
WIP
mtojek Sep 10, 2024
ef51c9a
WIP
mtojek Sep 10, 2024
007bad6
Fixed
mtojek Sep 10, 2024
73bb5bd
dbauthz
mtojek Sep 11, 2024
7a7bb85
fix
mtojek Sep 11, 2024
e02d271
fix
mtojek Sep 11, 2024
0b21a95
excluded
mtojek Sep 11, 2024
34c44b5
makegen
mtojek Sep 11, 2024
f63a06b
wIP
mtojek Sep 11, 2024
3b60a27
fix
mtojek Sep 11, 2024
774c4b0
fix
mtojek Sep 11, 2024
5d56c4d
makegen
mtojek Sep 11, 2024
e8214e9
dbmem
mtojek Sep 11, 2024
6ba2c29
makegen
mtojek Sep 11, 2024
2962735
last dbmem
mtojek Sep 11, 2024
d48a8ba
fix
mtojek Sep 11, 2024
0bf4748
fix
mtojek Sep 11, 2024
29d4a15
fix
mtojek Sep 11, 2024
a9580a5
wip
mtojek Sep 11, 2024
cf4609d
fix
mtojek Sep 11, 2024
8e8ab49
stub internal test
mtojek Sep 12, 2024
afad1c1
WIP
mtojek Sep 12, 2024
8b49d55
makegen
mtojek Sep 12, 2024
87d40cb
WIP
mtojek Sep 12, 2024
c352822
WIP: tests
mtojek Sep 12, 2024
22b1482
cleanup
mtojek Sep 12, 2024
44b1bba
InitialState_NoBuilds_NoReport
mtojek Sep 12, 2024
121eef5
first semi test
mtojek Sep 12, 2024
bc92fc4
fix: created_by
mtojek Sep 12, 2024
ccc6803
fix: created_by tv
mtojek Sep 12, 2024
f7826d6
fix: build number
mtojek Sep 12, 2024
36644b2
expanding
mtojek Sep 13, 2024
52aafac
fix: snapshot
mtojek Sep 13, 2024
8909bb6
test done
mtojek Sep 13, 2024
0cceede
more tests
mtojek Sep 13, 2024
1f06b86
WIP
mtojek Sep 13, 2024
a1c89ec
tests done
mtojek Sep 13, 2024
34cc4c3
cleanup
mtojek Sep 13, 2024
159e6c7
Merge branch 'main' into 40-failure-summary
mtojek Sep 13, 2024
5911ca9
fix: LockIDNotificationsReportGenerator
mtojek Sep 13, 2024
dda9bf5
Address Mathias' feedback
mtojek Sep 16, 2024
15f83c0
makegen
mtojek Sep 16, 2024
6306407
fmt
mtojek Sep 16, 2024
e258035
more
mtojek Sep 16, 2024
48da1cd
reports
mtojek Sep 16, 2024
309c600
payload
mtojek Sep 16, 2024
8211ee6
nil-omitted
mtojek Sep 16, 2024
a8aea9f
failed-lock
mtojek Sep 16, 2024
da6b0a3
xerrors
mtojek Sep 16, 2024
4afc23b
c=0
mtojek Sep 16, 2024
bf7737d
log-debug
mtojek Sep 16, 2024
102a245
enqueue with data
mtojek Sep 16, 2024
2c9fc46
remove logger
mtojek Sep 16, 2024
cb7ca0e
makegen
mtojek Sep 16, 2024
6582a48
sort
mtojek Sep 16, 2024
c933511
makegen
mtojek Sep 16, 2024
0cdf3ea
GetFailedWorkspaceBuildsByTemplateID
mtojek Sep 16, 2024
6126e6b
usersByIDs
mtojek Sep 16, 2024
1000a50
makegen
mtojek Sep 16, 2024
314f080
fix
mtojek Sep 16, 2024
498b89b
processedUsers
mtojek Sep 17, 2024
f115973
comment
mtojek Sep 17, 2024
01409ca
WIP
mtojek Sep 17, 2024
832bf38
notifications
mtojek Sep 17, 2024
4cacf4d
WIP
mtojek Sep 17, 2024
f23e837
WIP
mtojek Sep 17, 2024
359416c
notifications
mtojek Sep 17, 2024
68ed60f
fmt
mtojek Sep 17, 2024
fdad745
rephrase
mtojek Sep 17, 2024
9ba4c04
WIP
mtojek Sep 17, 2024
8f634a4
WIP
mtojek Sep 17, 2024
5215c92
WIP
mtojek Sep 17, 2024
2edc7d5
fix
mtojek Sep 17, 2024
53f6c63
refactored
mtojek Sep 17, 2024
6405f3c
WIP
mtojek Sep 17, 2024
d96a34a
refactored
mtojek Sep 17, 2024
06a79f4
adjust tests
mtojek Sep 17, 2024
3e60198
Merge branch 'main' into 40-failure-summary
mtojek Sep 17, 2024
0a05a40
duplicate migration
mtojek Sep 17, 2024
65cf4a8
dbauthz
mtojek Sep 17, 2024
898856d
fix
mtojek Sep 17, 2024
a5e42da
makegen
mtojek Sep 17, 2024
72724b3
fix
mtojek Sep 17, 2024
20abd69
fix: migration down
mtojek Sep 17, 2024
b2df714
xerrors.Is
mtojek Sep 17, 2024
65992f6
limit builds
mtojek Sep 17, 2024
af67873
context cancelled
mtojek Sep 17, 2024
6436489
Merge branch 'main' into 40-failure-summary
mtojek Sep 18, 2024
6bd772c
fix migration
mtojek Sep 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/coderd/entitlements"
"github.com/coder/coder/v2/coderd/notifications/reports"
"github.com/coder/coder/v2/coderd/runtimeconfig"
"github.com/coder/pretty"
"github.com/coder/quartz"
Expand Down Expand Up @@ -1018,6 +1019,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.

// nolint:gocritic // TODO: create own role.
notificationsManager.Run(dbauthz.AsSystemRestricted(ctx))

// Run report generator to distribute periodic reports.
notificationReportGenerator := reports.NewReportGenerator(ctx, logger, options.Database, options.NotificationsEnqueuer, quartz.NewReal())
defer notificationReportGenerator.Close()
}

// Wrap the server in middleware that redirects to the access URL if
Expand Down
28 changes: 28 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -1459,6 +1459,13 @@ func (q *querier) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.
return fetchWithPostFilter(q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLinksByUserID)(ctx, userID)
}

func (q *querier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.GetFailedWorkspaceBuildsByTemplateIDRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.GetFailedWorkspaceBuildsByTemplateID(ctx, arg)
}

func (q *querier) GetFileByHashAndCreator(ctx context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) {
file, err := q.db.GetFileByHashAndCreator(ctx, arg)
if err != nil {
Expand Down Expand Up @@ -1628,6 +1635,13 @@ func (q *querier) GetNotificationMessagesByStatus(ctx context.Context, arg datab
return q.db.GetNotificationMessagesByStatus(ctx, arg)
}

func (q *querier) GetNotificationReportGeneratorLogByTemplate(ctx context.Context, arg uuid.UUID) (database.NotificationReportGeneratorLog, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return database.NotificationReportGeneratorLog{}, err
}
return q.db.GetNotificationReportGeneratorLogByTemplate(ctx, arg)
}

func (q *querier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (database.NotificationTemplate, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationTemplate); err != nil {
return database.NotificationTemplate{}, err
Expand Down Expand Up @@ -2510,6 +2524,13 @@ func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuil
return q.db.GetWorkspaceBuildParameters(ctx, workspaceBuildID)
}

func (q *querier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.GetWorkspaceBuildStatsByTemplates(ctx, since)
}

func (q *querier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg database.GetWorkspaceBuildsByWorkspaceIDParams) ([]database.WorkspaceBuild, error) {
if _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID); err != nil {
return nil, err
Expand Down Expand Up @@ -3966,6 +3987,13 @@ func (q *querier) UpsertLogoURL(ctx context.Context, value string) error {
return q.db.UpsertLogoURL(ctx, value)
}

func (q *querier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil {
return err
}
return q.db.UpsertNotificationReportGeneratorLog(ctx, arg)
}

func (q *querier) UpsertNotificationsSettings(ctx context.Context, value string) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return err
Expand Down
22 changes: 22 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2819,6 +2819,28 @@ func (s *MethodTestSuite) TestSystemFunctions() {
Value: "value",
}).Asserts(rbac.ResourceSystem, policy.ActionCreate)
}))
s.Run("GetFailedWorkspaceBuildsByTemplateID", s.Subtest(func(db database.Store, check *expects) {
check.Args(database.GetFailedWorkspaceBuildsByTemplateIDParams{
TemplateID: uuid.New(),
Since: dbtime.Now(),
}).Asserts(rbac.ResourceSystem, policy.ActionRead)
}))
s.Run("GetNotificationReportGeneratorLogByTemplate", s.Subtest(func(db database.Store, check *expects) {
_ = db.UpsertNotificationReportGeneratorLog(context.Background(), database.UpsertNotificationReportGeneratorLogParams{
NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport,
LastGeneratedAt: dbtime.Now(),
})
check.Args(notifications.TemplateWorkspaceBuildsFailedReport).Asserts(rbac.ResourceSystem, policy.ActionRead)
}))
s.Run("GetWorkspaceBuildStatsByTemplates", s.Subtest(func(db database.Store, check *expects) {
check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead)
}))
s.Run("UpsertNotificationReportGeneratorLog", s.Subtest(func(db database.Store, check *expects) {
check.Args(database.UpsertNotificationReportGeneratorLogParams{
NotificationTemplateID: uuid.New(),
LastGeneratedAt: dbtime.Now(),
}).Asserts(rbac.ResourceSystem, policy.ActionCreate)
}))
}

func (s *MethodTestSuite) TestNotifications() {
Expand Down
258 changes: 211 additions & 47 deletions coderd/database/dbmem/dbmem.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,53 +187,54 @@ type data struct {
userLinks []database.UserLink

// New tables
workspaceAgentStats []database.WorkspaceAgentStat
auditLogs []database.AuditLog
cryptoKeys []database.CryptoKey
dbcryptKeys []database.DBCryptKey
files []database.File
externalAuthLinks []database.ExternalAuthLink
gitSSHKey []database.GitSSHKey
groupMembers []database.GroupMemberTable
groups []database.Group
jfrogXRayScans []database.JfrogXrayScan
licenses []database.License
notificationMessages []database.NotificationMessage
notificationPreferences []database.NotificationPreference
oauth2ProviderApps []database.OAuth2ProviderApp
oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret
oauth2ProviderAppCodes []database.OAuth2ProviderAppCode
oauth2ProviderAppTokens []database.OAuth2ProviderAppToken
parameterSchemas []database.ParameterSchema
provisionerDaemons []database.ProvisionerDaemon
provisionerJobLogs []database.ProvisionerJobLog
provisionerJobs []database.ProvisionerJob
provisionerKeys []database.ProvisionerKey
replicas []database.Replica
templateVersions []database.TemplateVersionTable
templateVersionParameters []database.TemplateVersionParameter
templateVersionVariables []database.TemplateVersionVariable
templateVersionWorkspaceTags []database.TemplateVersionWorkspaceTag
templates []database.TemplateTable
templateUsageStats []database.TemplateUsageStat
workspaceAgents []database.WorkspaceAgent
workspaceAgentMetadata []database.WorkspaceAgentMetadatum
workspaceAgentLogs []database.WorkspaceAgentLog
workspaceAgentLogSources []database.WorkspaceAgentLogSource
workspaceAgentScripts []database.WorkspaceAgentScript
workspaceAgentPortShares []database.WorkspaceAgentPortShare
workspaceApps []database.WorkspaceApp
workspaceAppStatsLastInsertID int64
workspaceAppStats []database.WorkspaceAppStat
workspaceBuilds []database.WorkspaceBuild
workspaceBuildParameters []database.WorkspaceBuildParameter
workspaceResourceMetadata []database.WorkspaceResourceMetadatum
workspaceResources []database.WorkspaceResource
workspaces []database.Workspace
workspaceProxies []database.WorkspaceProxy
customRoles []database.CustomRole
provisionerJobTimings []database.ProvisionerJobTiming
runtimeConfig map[string]string
auditLogs []database.AuditLog
cryptoKeys []database.CryptoKey
dbcryptKeys []database.DBCryptKey
files []database.File
externalAuthLinks []database.ExternalAuthLink
gitSSHKey []database.GitSSHKey
groupMembers []database.GroupMemberTable
groups []database.Group
jfrogXRayScans []database.JfrogXrayScan
licenses []database.License
notificationMessages []database.NotificationMessage
notificationPreferences []database.NotificationPreference
notificationReportGeneratorLogs []database.NotificationReportGeneratorLog
oauth2ProviderApps []database.OAuth2ProviderApp
oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret
oauth2ProviderAppCodes []database.OAuth2ProviderAppCode
oauth2ProviderAppTokens []database.OAuth2ProviderAppToken
parameterSchemas []database.ParameterSchema
provisionerDaemons []database.ProvisionerDaemon
provisionerJobLogs []database.ProvisionerJobLog
provisionerJobs []database.ProvisionerJob
provisionerKeys []database.ProvisionerKey
replicas []database.Replica
templateVersions []database.TemplateVersionTable
templateVersionParameters []database.TemplateVersionParameter
templateVersionVariables []database.TemplateVersionVariable
templateVersionWorkspaceTags []database.TemplateVersionWorkspaceTag
templates []database.TemplateTable
templateUsageStats []database.TemplateUsageStat
workspaceAgents []database.WorkspaceAgent
workspaceAgentMetadata []database.WorkspaceAgentMetadatum
workspaceAgentLogs []database.WorkspaceAgentLog
workspaceAgentLogSources []database.WorkspaceAgentLogSource
workspaceAgentPortShares []database.WorkspaceAgentPortShare
workspaceAgentScripts []database.WorkspaceAgentScript
workspaceAgentStats []database.WorkspaceAgentStat
workspaceApps []database.WorkspaceApp
workspaceAppStatsLastInsertID int64
workspaceAppStats []database.WorkspaceAppStat
workspaceBuilds []database.WorkspaceBuild
workspaceBuildParameters []database.WorkspaceBuildParameter
workspaceResourceMetadata []database.WorkspaceResourceMetadatum
workspaceResources []database.WorkspaceResource
workspaces []database.Workspace
workspaceProxies []database.WorkspaceProxy
customRoles []database.CustomRole
provisionerJobTimings []database.ProvisionerJobTiming
runtimeConfig map[string]string
// Locks is a map of lock names. Any keys within the map are currently
// locked.
locks map[int64]struct{}
Expand Down Expand Up @@ -2621,6 +2622,75 @@ func (q *FakeQuerier) GetExternalAuthLinksByUserID(_ context.Context, userID uui
return gals, nil
}

func (q *FakeQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.GetFailedWorkspaceBuildsByTemplateIDRow, error) {
err := validateDatabaseType(arg)
if err != nil {
return nil, err
}

q.mutex.RLock()
defer q.mutex.RUnlock()

workspaceBuildStats := []database.GetFailedWorkspaceBuildsByTemplateIDRow{}
for _, wb := range q.workspaceBuilds {
job, err := q.getProvisionerJobByIDNoLock(ctx, wb.JobID)
if err != nil {
return nil, xerrors.Errorf("get provisioner job by ID: %w", err)
}

if job.JobStatus != database.ProvisionerJobStatusFailed {
continue
}

if !job.CompletedAt.Valid {
continue
}

if wb.CreatedAt.Before(arg.Since) {
continue
}

w, err := q.getWorkspaceByIDNoLock(ctx, wb.WorkspaceID)
if err != nil {
return nil, xerrors.Errorf("get workspace by ID: %w", err)
}

t, err := q.getTemplateByIDNoLock(ctx, w.TemplateID)
if err != nil {
return nil, xerrors.Errorf("get template by ID: %w", err)
}

if t.ID != arg.TemplateID {
continue
}

workspaceOwner, err := q.getUserByIDNoLock(w.OwnerID)
if err != nil {
return nil, xerrors.Errorf("get user by ID: %w", err)
}

templateVersion, err := q.getTemplateVersionByIDNoLock(ctx, wb.TemplateVersionID)
if err != nil {
return nil, xerrors.Errorf("get template version by ID: %w", err)
}

workspaceBuildStats = append(workspaceBuildStats, database.GetFailedWorkspaceBuildsByTemplateIDRow{
WorkspaceName: w.Name,
WorkspaceOwnerUsername: workspaceOwner.Username,
TemplateVersionName: templateVersion.Name,
WorkspaceBuildNumber: wb.BuildNumber,
})
}

sort.Slice(workspaceBuildStats, func(i, j int) bool {
if workspaceBuildStats[i].TemplateVersionName != workspaceBuildStats[j].TemplateVersionName {
return workspaceBuildStats[i].TemplateVersionName < workspaceBuildStats[j].TemplateVersionName
}
return workspaceBuildStats[i].WorkspaceBuildNumber > workspaceBuildStats[j].WorkspaceBuildNumber
})
return workspaceBuildStats, nil
}

func (q *FakeQuerier) GetFileByHashAndCreator(_ context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) {
if err := validateDatabaseType(arg); err != nil {
return database.File{}, err
Expand Down Expand Up @@ -3044,6 +3114,23 @@ func (q *FakeQuerier) GetNotificationMessagesByStatus(_ context.Context, arg dat
return out, nil
}

func (q *FakeQuerier) GetNotificationReportGeneratorLogByTemplate(_ context.Context, templateID uuid.UUID) (database.NotificationReportGeneratorLog, error) {
err := validateDatabaseType(templateID)
if err != nil {
return database.NotificationReportGeneratorLog{}, err
}

q.mutex.RLock()
defer q.mutex.RUnlock()

for _, record := range q.notificationReportGeneratorLogs {
if record.NotificationTemplateID == templateID {
return record, nil
}
}
return database.NotificationReportGeneratorLog{}, sql.ErrNoRows
}

func (*FakeQuerier) GetNotificationTemplateByID(_ context.Context, _ uuid.UUID) (database.NotificationTemplate, error) {
// Not implementing this function because it relies on state in the database which is created with migrations.
// We could consider using code-generation to align the database state and dbmem, but it's not worth it right now.
Expand Down Expand Up @@ -5964,6 +6051,63 @@ func (q *FakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBu
return params, nil
}

func (q *FakeQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()

templateStats := map[uuid.UUID]database.GetWorkspaceBuildStatsByTemplatesRow{}
for _, wb := range q.workspaceBuilds {
job, err := q.getProvisionerJobByIDNoLock(ctx, wb.JobID)
if err != nil {
return nil, xerrors.Errorf("get provisioner job by ID: %w", err)
}

if !job.CompletedAt.Valid {
continue
}

if wb.CreatedAt.Before(since) {
continue
}

w, err := q.getWorkspaceByIDNoLock(ctx, wb.WorkspaceID)
if err != nil {
return nil, xerrors.Errorf("get workspace by ID: %w", err)
}

if _, ok := templateStats[w.TemplateID]; !ok {
t, err := q.getTemplateByIDNoLock(ctx, w.TemplateID)
if err != nil {
return nil, xerrors.Errorf("get template by ID: %w", err)
}

templateStats[w.TemplateID] = database.GetWorkspaceBuildStatsByTemplatesRow{
TemplateID: w.TemplateID,
TemplateName: t.Name,
TemplateDisplayName: t.DisplayName,
TemplateOrganizationID: w.OrganizationID,
}
}

s := templateStats[w.TemplateID]
s.TotalBuilds++
if job.JobStatus == database.ProvisionerJobStatusFailed {
s.FailedBuilds++
}
templateStats[w.TemplateID] = s
}

rows := make([]database.GetWorkspaceBuildStatsByTemplatesRow, 0, len(templateStats))
for _, ts := range templateStats {
rows = append(rows, ts)
}

sort.Slice(rows, func(i, j int) bool {
return rows[i].TemplateName < rows[j].TemplateName
})
return rows, nil
}

func (q *FakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context,
params database.GetWorkspaceBuildsByWorkspaceIDParams,
) ([]database.WorkspaceBuild, error) {
Expand Down Expand Up @@ -9440,6 +9584,26 @@ func (q *FakeQuerier) UpsertLogoURL(_ context.Context, data string) error {
return nil
}

func (q *FakeQuerier) UpsertNotificationReportGeneratorLog(_ context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error {
err := validateDatabaseType(arg)
if err != nil {
return err
}

q.mutex.Lock()
defer q.mutex.Unlock()

for i, record := range q.notificationReportGeneratorLogs {
if arg.NotificationTemplateID == record.NotificationTemplateID {
q.notificationReportGeneratorLogs[i].LastGeneratedAt = arg.LastGeneratedAt
return nil
}
}

q.notificationReportGeneratorLogs = append(q.notificationReportGeneratorLogs, database.NotificationReportGeneratorLog(arg))
return nil
}

func (q *FakeQuerier) UpsertNotificationsSettings(_ context.Context, data string) error {
q.mutex.Lock()
defer q.mutex.Unlock()
Expand Down
Loading
Loading