Skip to content

Commit 4d7a304

Browse files
committed
WIP
1 parent f614080 commit 4d7a304

File tree

6 files changed

+116
-30
lines changed

6 files changed

+116
-30
lines changed

coderd/database/dump.sql

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/migrations/000249_email_reports.up.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ CREATE TABLE report_generator_logs
2323
(
2424
user_id uuid NOT NULL,
2525
notification_template_id uuid NOT NULL,
26-
last_generated_at timestamp with time zone,
26+
last_generated_at timestamp with time zone NOT NULL,
2727

2828
PRIMARY KEY (user_id, notification_template_id),
2929
UNIQUE (user_id, notification_template_id)

coderd/database/models.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/notifications/reports/generator.go

Lines changed: 105 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import (
44
"context"
55
"database/sql"
66
"io"
7+
"slices"
8+
"sort"
79
"time"
810

9-
"cdr.dev/slog"
1011
"golang.org/x/xerrors"
1112

13+
"cdr.dev/slog"
14+
15+
"github.com/google/uuid"
16+
1217
"github.com/coder/coder/v2/coderd/database"
1318
"github.com/coder/coder/v2/coderd/database/dbauthz"
1419
"github.com/coder/coder/v2/coderd/database/dbtime"
@@ -91,16 +96,9 @@ func (i *reportGenerator) Close() error {
9196
return nil
9297
}
9398

94-
func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, _ notifications.Enqueuer, clk quartz.Clock) error {
99+
func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) error {
95100
const frequencyDays = 7
96101

97-
templateAdmins, err := db.GetUsers(ctx, database.GetUsersParams{
98-
RbacRole: []string{codersdk.RoleTemplateAdmin},
99-
})
100-
if err != nil {
101-
return xerrors.Errorf("unable to fetch template admins: %w", err)
102-
}
103-
104102
templates, err := db.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{
105103
Deleted: false,
106104
Deprecated: sql.NullBool{Bool: false, Valid: true},
@@ -110,16 +108,69 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat
110108
}
111109

112110
for _, template := range templates {
113-
// 1. Fetch failed builds.
114-
// 2. If failed builds == 0, continue.
115-
// 3. Fetch template RW users.
116-
// 4. For user := range template admins + RW users:
117-
// 1. Check if report is enabled for the person.
118-
// 2. Check `report_generator_log`.
119-
// 3. If sent recently, continue
120-
// 4. Lazy-render the report.
121-
// 5. Send notification
122-
// 6. Upsert into `report_generator_log`.
111+
failedBuilds, err := db.GetFailedWorkspaceBuildsByTemplateID(ctx, database.GetFailedWorkspaceBuildsByTemplateIDParams{
112+
TemplateID: template.ID,
113+
Since: dbtime.Time(clk.Now()).UTC(),
114+
})
115+
if err != nil {
116+
logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", template.ID), slog.Error(err))
117+
continue
118+
}
119+
120+
templateAdmins, err := findTemplateAdmins(ctx, db, template)
121+
if err != nil {
122+
logger.Error(ctx, "unable to find template admins", slog.F("template_id", template.ID), slog.Error(err))
123+
continue
124+
}
125+
126+
for _, templateAdmin := range templateAdmins {
127+
// TODO Check if report is enabled for the person.
128+
// TODO Check `report_generator_log`.
129+
// TODO If sent recently, continue
130+
131+
if len(failedBuilds) == 0 {
132+
err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{
133+
UserID: templateAdmin.ID,
134+
NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport,
135+
LastGeneratedAt: dbtime.Time(clk.Now()).UTC(),
136+
})
137+
if err != nil {
138+
logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", template.ID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err))
139+
continue
140+
}
141+
}
142+
143+
// TODO Lazy-render the report.
144+
reportData := map[string]any{}
145+
146+
templateDisplayName := template.DisplayName
147+
if templateDisplayName == "" {
148+
templateDisplayName = template.Name
149+
}
150+
151+
if _, err := enqueuer.EnqueueData(ctx, templateAdmin.ID, notifications.TemplateWorkspaceBuildsFailedReport,
152+
map[string]string{
153+
"template_name": template.Name,
154+
"template_display_name": templateDisplayName,
155+
},
156+
reportData,
157+
"report_generator",
158+
template.ID, template.OrganizationID,
159+
); err != nil {
160+
logger.Warn(ctx, "failed to send a report with failed workspace builds", slog.Error(err))
161+
}
162+
163+
err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{
164+
UserID: templateAdmin.ID,
165+
NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport,
166+
LastGeneratedAt: dbtime.Time(clk.Now()).UTC(),
167+
})
168+
if err != nil {
169+
logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", template.ID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err))
170+
continue
171+
}
172+
}
173+
123174
}
124175

125176
err = db.DeleteOldReportGeneratorLogs(ctx, frequencyDays)
@@ -128,3 +179,38 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat
128179
}
129180
return nil
130181
}
182+
183+
func findTemplateAdmins(ctx context.Context, db database.Store, template database.Template) ([]database.GetUsersRow, error) {
184+
users, err := db.GetUsers(ctx, database.GetUsersParams{
185+
RbacRole: []string{codersdk.RoleTemplateAdmin},
186+
})
187+
if err != nil {
188+
return nil, xerrors.Errorf("unable to fetch template admins: %w", err)
189+
}
190+
191+
usersByIDs := map[uuid.UUID]database.GetUsersRow{}
192+
var userIDs []uuid.UUID
193+
for _, user := range users {
194+
usersByIDs[user.ID] = user
195+
userIDs = append(userIDs, user.ID)
196+
}
197+
198+
var templateAdmins []database.GetUsersRow
199+
if len(userIDs) > 0 {
200+
orgIDsByMemberIDs, err := db.GetOrganizationIDsByMemberIDs(ctx, userIDs)
201+
if err != nil {
202+
return nil, xerrors.Errorf("unable to fetch organization IDs by member IDs: %w", err)
203+
}
204+
205+
for _, entry := range orgIDsByMemberIDs {
206+
if slices.Contains(entry.OrganizationIDs, template.OrganizationID) {
207+
templateAdmins = append(templateAdmins, usersByIDs[entry.UserID])
208+
}
209+
}
210+
}
211+
sort.Slice(templateAdmins, func(i, j int) bool {
212+
return templateAdmins[i].Username < templateAdmins[j].Username
213+
})
214+
215+
return templateAdmins, nil
216+
}

0 commit comments

Comments
 (0)