Skip to content

Commit 65992f6

Browse files
committed
limit builds
1 parent b2df714 commit 65992f6

File tree

2 files changed

+127
-9
lines changed

2 files changed

+127
-9
lines changed

coderd/notifications/reports/generator.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat
206206
return nil
207207
}
208208

209+
const workspaceBuildsLimitPerTemplateVersion = 10
210+
209211
func buildDataForReportFailedWorkspaceBuilds(stats database.GetWorkspaceBuildStatsByTemplatesRow, failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow) map[string]any {
210212
// Build notification model for template versions and failed workspace builds.
211213
//
@@ -233,15 +235,19 @@ func buildDataForReportFailedWorkspaceBuilds(stats database.GetWorkspaceBuildSta
233235

234236
tv := templateVersions[c-1]
235237
//nolint:errorlint,forcetypeassert // only this function prepares the notification model
236-
builds := tv["failed_builds"].([]map[string]any)
237-
builds = append(builds, map[string]any{
238-
"workspace_owner_username": failedBuild.WorkspaceOwnerUsername,
239-
"workspace_name": failedBuild.WorkspaceName,
240-
"build_number": failedBuild.WorkspaceBuildNumber,
241-
})
242-
tv["failed_builds"] = builds
243-
//nolint:errorlint,forcetypeassert // only this function prepares the notification model
244238
tv["failed_count"] = tv["failed_count"].(int) + 1
239+
240+
//nolint:errorlint,forcetypeassert // only this function prepares the notification model
241+
builds := tv["failed_builds"].([]map[string]any)
242+
if len(builds) < workspaceBuildsLimitPerTemplateVersion {
243+
// return N last builds to prevent long email reports
244+
builds = append(builds, map[string]any{
245+
"workspace_owner_username": failedBuild.WorkspaceOwnerUsername,
246+
"workspace_name": failedBuild.WorkspaceName,
247+
"build_number": failedBuild.WorkspaceBuildNumber,
248+
})
249+
tv["failed_builds"] = builds
250+
}
245251
templateVersions[c-1] = tv
246252
}
247253

coderd/notifications/reports/generator_internal_test.go

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) {
115115
require.Empty(t, notifEnq.Sent)
116116
})
117117

118-
t.Run("FailedBuilds_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) {
118+
t.Run("FailedBuilds_SecondRun_Report_ThirdRunTooEarly_NoReport_FourthRun_Report", func(t *testing.T) {
119119
t.Parallel()
120120

121121
verifyNotification := func(t *testing.T, recipient database.User, notif *testutil.Notification, tmpl database.Template, failedBuilds, totalBuilds int64, templateVersions []map[string]interface{}) {
@@ -290,6 +290,118 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) {
290290
}
291291
})
292292

293+
t.Run("TooManyFailedBuilds_SecondRun_Report", func(t *testing.T) {
294+
t.Parallel()
295+
296+
verifyNotification := func(t *testing.T, recipient database.User, notif *testutil.Notification, tmpl database.Template, failedBuilds, totalBuilds int64, templateVersions []map[string]interface{}) {
297+
t.Helper()
298+
299+
require.Equal(t, recipient.ID, notif.UserID)
300+
require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notif.TemplateID)
301+
require.Equal(t, tmpl.Name, notif.Labels["template_name"])
302+
require.Equal(t, tmpl.DisplayName, notif.Labels["template_display_name"])
303+
require.Equal(t, failedBuilds, notif.Data["failed_builds"])
304+
require.Equal(t, totalBuilds, notif.Data["total_builds"])
305+
require.Equal(t, "week", notif.Data["report_frequency"])
306+
require.Equal(t, templateVersions, notif.Data["template_versions"])
307+
}
308+
309+
// Setup
310+
ctx, logger, db, ps, notifEnq, clk := setup(t)
311+
312+
// Given
313+
314+
// Organization
315+
org := dbgen.Organization(t, db, database.Organization{})
316+
317+
// Template admins
318+
templateAdmin1 := dbgen.User(t, db, database.User{Username: "template-admin-1", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}})
319+
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin1.ID, OrganizationID: org.ID})
320+
321+
// Regular users
322+
user1 := dbgen.User(t, db, database.User{})
323+
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user1.ID, OrganizationID: org.ID})
324+
325+
// Templates
326+
t1 := dbgen.Template(t, db, database.Template{Name: "template-1", DisplayName: "First Template", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID})
327+
328+
// Template versions
329+
t1v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()})
330+
t1v2 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-2", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()})
331+
332+
// Workspaces
333+
w1 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID})
334+
335+
// When: first run
336+
notifEnq.Clear()
337+
err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk)
338+
339+
// Then
340+
require.NoError(t, err)
341+
require.Empty(t, notifEnq.Sent) // no notifications
342+
343+
// One week later...
344+
clk.Advance(failedWorkspaceBuildsReportFrequency + time.Minute)
345+
now := clk.Now()
346+
347+
// Workspace builds
348+
pj0 := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-24 * time.Hour), Valid: true}})
349+
_ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 777, TemplateVersionID: t1v1.ID, JobID: pj0.ID, CreatedAt: now.Add(-24 * time.Hour), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator})
350+
351+
for i := 1; i <= 23; i++ {
352+
at := now.Add(-time.Duration(i) * time.Hour)
353+
354+
pj1 := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: at, Valid: true}})
355+
_ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: int32(i), TemplateVersionID: t1v1.ID, JobID: pj1.ID, CreatedAt: at, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator})
356+
357+
pj2 := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: at, Valid: true}})
358+
_ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: int32(i) + 100, TemplateVersionID: t1v2.ID, JobID: pj2.ID, CreatedAt: at, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator})
359+
}
360+
361+
// When
362+
notifEnq.Clear()
363+
err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk)
364+
365+
// Then
366+
require.NoError(t, err)
367+
368+
require.Len(t, notifEnq.Sent, 1) // 1 template, 1 template admin
369+
verifyNotification(t, templateAdmin1, notifEnq.Sent[0], t1, 46, 47, []map[string]interface{}{
370+
{
371+
"failed_builds": []map[string]interface{}{
372+
{"build_number": int32(23), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
373+
{"build_number": int32(22), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
374+
{"build_number": int32(21), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
375+
{"build_number": int32(20), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
376+
{"build_number": int32(19), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
377+
{"build_number": int32(18), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
378+
{"build_number": int32(17), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
379+
{"build_number": int32(16), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
380+
{"build_number": int32(15), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
381+
{"build_number": int32(14), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
382+
},
383+
"failed_count": 23,
384+
"template_version_name": t1v1.Name,
385+
},
386+
{
387+
"failed_builds": []map[string]interface{}{
388+
{"build_number": int32(123), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
389+
{"build_number": int32(122), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
390+
{"build_number": int32(121), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
391+
{"build_number": int32(120), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
392+
{"build_number": int32(119), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
393+
{"build_number": int32(118), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
394+
{"build_number": int32(117), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
395+
{"build_number": int32(116), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
396+
{"build_number": int32(115), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
397+
{"build_number": int32(114), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
398+
},
399+
"failed_count": 23,
400+
"template_version_name": t1v2.Name,
401+
},
402+
})
403+
})
404+
293405
t.Run("NoFailedBuilds_NoReport", func(t *testing.T) {
294406
t.Parallel()
295407

0 commit comments

Comments
 (0)