From fcb3e5d0e2c106a65813aa6481338ccff39f515f Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 6 May 2025 20:44:49 +0000 Subject: [PATCH 01/47] fix: limit prebuild failure cost --- cli/testdata/server-config.yaml.golden | 4 + coderd/apidoc/docs.go | 4 + coderd/apidoc/swagger.json | 4 + coderd/database/dbauthz/dbauthz.go | 9 + coderd/database/dbauthz/dbauthz_test.go | 5 + coderd/database/dbmem/dbmem.go | 4 + coderd/database/dbmetrics/querymetrics.go | 7 + coderd/database/dbmock/dbmock.go | 15 + coderd/database/querier.go | 9 + coderd/database/querier_test.go | 310 ++++++++++++++++++ coderd/database/queries.sql.go | 65 ++++ coderd/database/queries/prebuilds.sql | 36 ++ coderd/prebuilds/global_snapshot.go | 29 +- coderd/prebuilds/preset_snapshot_test.go | 18 +- codersdk/deployment.go | 17 + docs/reference/api/general.md | 1 + docs/reference/api/schemas.md | 14 +- enterprise/coderd/prebuilds/reconcile.go | 19 +- enterprise/coderd/prebuilds/reconcile_test.go | 96 ++++++ site/src/api/typesGenerated.ts | 1 + 20 files changed, 644 insertions(+), 23 deletions(-) diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index fc76a6c2ec8a0..13bb147063750 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -704,3 +704,7 @@ workspace_prebuilds: # backoff. # (default: 1h0m0s, type: duration) reconciliation_backoff_lookback_period: 1h0m0s + # Maximum number of consecutive failed prebuilds before a preset hits the hard + # limit. FailureHardLimit is disabled when set to zero. + # (default: 3, type: int) + failure_hard_limit: 3 diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index fb5ae20e448c8..c4bf5e1a2dac5 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -14239,6 +14239,10 @@ const docTemplate = `{ "codersdk.PrebuildsConfig": { "type": "object", "properties": { + "failure_hard_limit": { + "description": "FailureHardLimit defines the maximum number of consecutive failed prebuild attempts allowed\nbefore a preset is considered to be in a hard limit state. When a preset hits this limit,\nno new prebuilds will be created until the limit is reset.\nFailureHardLimit is disabled when set to zero.", + "type": "integer" + }, "reconciliation_backoff_interval": { "description": "ReconciliationBackoffInterval specifies the amount of time to increase the backoff interval\nwhen errors occur during reconciliation.", "type": "integer" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 8420c9ea0f812..34797c8fc1275 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -12891,6 +12891,10 @@ "codersdk.PrebuildsConfig": { "type": "object", "properties": { + "failure_hard_limit": { + "description": "FailureHardLimit defines the maximum number of consecutive failed prebuild attempts allowed\nbefore a preset is considered to be in a hard limit state. When a preset hits this limit,\nno new prebuilds will be created until the limit is reset.\nFailureHardLimit is disabled when set to zero.", + "type": "integer" + }, "reconciliation_backoff_interval": { "description": "ReconciliationBackoffInterval specifies the amount of time to increase the backoff interval\nwhen errors occur during reconciliation.", "type": "integer" diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 2ed230dd7a8f3..9dbc5c76c3443 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2234,6 +2234,15 @@ func (q *querier) GetPresetParametersByTemplateVersionID(ctx context.Context, ar return q.db.GetPresetParametersByTemplateVersionID(ctx, args) } +func (q *querier) GetPresetsAtFailureLimit(ctx context.Context, hardLimit int64) ([]database.GetPresetsAtFailureLimitRow, error) { + // GetPresetsAtFailureLimit returns a list of template version presets that have reached the hard failure limit. + // Request the same authorization permissions as GetPresetsBackoff, since the methods are similar. + if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { + return nil, err + } + return q.db.GetPresetsAtFailureLimit(ctx, hardLimit) +} + func (q *querier) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]database.GetPresetsBackoffRow, error) { // GetPresetsBackoff returns a list of template version presets along with metadata such as the number of failed prebuilds. if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 6dc9a32f03943..e151306a9b08d 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4857,6 +4857,11 @@ func (s *MethodTestSuite) TestPrebuilds() { Asserts(rbac.ResourceWorkspace.All(), policy.ActionRead). ErrorsWithInMemDB(dbmem.ErrUnimplemented) })) + s.Run("GetPresetsAtFailureLimit", s.Subtest(func(_ database.Store, check *expects) { + check.Args(int64(0)). + Asserts(rbac.ResourceTemplate.All(), policy.ActionViewInsights). + ErrorsWithInMemDB(dbmem.ErrUnimplemented) + })) s.Run("GetPresetsBackoff", s.Subtest(func(_ database.Store, check *expects) { check.Args(time.Time{}). Asserts(rbac.ResourceTemplate.All(), policy.ActionViewInsights). diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 6bae4455a89ef..592901a3fbcd2 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -4362,6 +4362,10 @@ func (q *FakeQuerier) GetPresetParametersByTemplateVersionID(_ context.Context, return parameters, nil } +func (q *FakeQuerier) GetPresetsAtFailureLimit(ctx context.Context, hardLimit int64) ([]database.GetPresetsAtFailureLimitRow, error) { + return nil, ErrUnimplemented +} + func (*FakeQuerier) GetPresetsBackoff(_ context.Context, _ time.Time) ([]database.GetPresetsBackoffRow, error) { return nil, ErrUnimplemented } diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 128e741da1d76..d28f5ed79e66d 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1145,6 +1145,13 @@ func (m queryMetricsStore) GetPresetParametersByTemplateVersionID(ctx context.Co return r0, r1 } +func (m queryMetricsStore) GetPresetsAtFailureLimit(ctx context.Context, hardLimit int64) ([]database.GetPresetsAtFailureLimitRow, error) { + start := time.Now() + r0, r1 := m.s.GetPresetsAtFailureLimit(ctx, hardLimit) + m.queryLatencies.WithLabelValues("GetPresetsAtFailureLimit").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]database.GetPresetsBackoffRow, error) { start := time.Now() r0, r1 := m.s.GetPresetsBackoff(ctx, lookback) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 17b263dfb2e07..6308e50e84c9c 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2343,6 +2343,21 @@ func (mr *MockStoreMockRecorder) GetPresetParametersByTemplateVersionID(ctx, tem return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetParametersByTemplateVersionID", reflect.TypeOf((*MockStore)(nil).GetPresetParametersByTemplateVersionID), ctx, templateVersionID) } +// GetPresetsAtFailureLimit mocks base method. +func (m *MockStore) GetPresetsAtFailureLimit(ctx context.Context, hardLimit int64) ([]database.GetPresetsAtFailureLimitRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPresetsAtFailureLimit", ctx, hardLimit) + ret0, _ := ret[0].([]database.GetPresetsAtFailureLimitRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPresetsAtFailureLimit indicates an expected call of GetPresetsAtFailureLimit. +func (mr *MockStoreMockRecorder) GetPresetsAtFailureLimit(ctx, hardLimit any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetsAtFailureLimit", reflect.TypeOf((*MockStore)(nil).GetPresetsAtFailureLimit), ctx, hardLimit) +} + // GetPresetsBackoff mocks base method. func (m *MockStore) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]database.GetPresetsBackoffRow, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index d0f74ee609724..60042af669738 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -242,6 +242,15 @@ type sqlcQuerier interface { GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (TemplateVersionPreset, error) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]TemplateVersionPresetParameter, error) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPresetParameter, error) + // GetPresetsAtFailureLimit groups workspace builds by preset ID. + // Each preset is associated with exactly one template version ID. + // For each preset, the query checks the last hard_limit builds. + // If all of them failed, the preset is considered to have hit the hard failure limit. + // The query returns a list of preset IDs that have reached this failure threshold. + // Only active template versions with configured presets are considered. + // For each preset, check the last hard_limit builds. + // If all of them failed, the preset is considered to have hit the hard failure limit. + GetPresetsAtFailureLimit(ctx context.Context, hardLimit int64) ([]GetPresetsAtFailureLimitRow, error) // GetPresetsBackoff groups workspace builds by preset ID. // Each preset is associated with exactly one template version ID. // For each group, the query checks up to N of the most recent jobs that occurred within the diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index b2cc20c4894d5..f91e830705686 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -4401,6 +4401,316 @@ func TestGetPresetsBackoff(t *testing.T) { }) } +func TestGetPresetsAtFailureLimit(t *testing.T) { + t.Parallel() + if !dbtestutil.WillUsePostgres() { + t.SkipNow() + } + + now := dbtime.Now() + hourBefore := now.Add(-time.Hour) + orgID := uuid.New() + userID := uuid.New() + + findPresetByTmplVersionID := func(hardLimitedPresets []database.GetPresetsAtFailureLimitRow, tmplVersionID uuid.UUID) *database.GetPresetsAtFailureLimitRow { + for _, preset := range hardLimitedPresets { + if preset.TemplateVersionID == tmplVersionID { + return &preset + } + } + + return nil + } + + testCases := []struct { + name string + // true - build is successful + // false - build is unsuccessful + buildSuccesses []bool + hardLimit int64 + expHitHardLimit bool + }{ + { + name: "failed build", + buildSuccesses: []bool{false}, + hardLimit: 1, + expHitHardLimit: true, + }, + { + name: "2 failed builds", + buildSuccesses: []bool{false, false}, + hardLimit: 1, + expHitHardLimit: true, + }, + { + name: "successful build", + buildSuccesses: []bool{true}, + hardLimit: 1, + expHitHardLimit: false, + }, + { + name: "last build is failed", + buildSuccesses: []bool{true, true, false}, + hardLimit: 1, + expHitHardLimit: true, + }, + { + name: "last build is successful", + buildSuccesses: []bool{false, false, true}, + hardLimit: 1, + expHitHardLimit: false, + }, + { + name: "last 3 builds are failed - hard limit is reached", + buildSuccesses: []bool{true, true, false, false, false}, + hardLimit: 3, + expHitHardLimit: true, + }, + { + name: "1 out of 3 last build is successful - hard limit is NOT reached", + buildSuccesses: []bool{false, false, true, false, false}, + hardLimit: 3, + expHitHardLimit: false, + }, + // hardLimit set to zero, implicitly disables the hard limit. + { + name: "despite 5 failed builds, the hard limit is not reached because it's disabled.", + buildSuccesses: []bool{false, false, false, false, false}, + hardLimit: 0, + expHitHardLimit: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitShort) + dbgen.Organization(t, db, database.Organization{ + ID: orgID, + }) + dbgen.User(t, db, database.User{ + ID: userID, + }) + + tmpl := createTemplate(t, db, orgID, userID) + tmplV1 := createTmplVersionAndPreset(t, db, tmpl, tmpl.ActiveVersionID, now, nil) + for idx, buildSuccess := range tc.buildSuccesses { + createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV1, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: !buildSuccess, + createdAt: hourBefore.Add(time.Duration(idx) * time.Second), + }) + } + + hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, tc.hardLimit) + require.NoError(t, err) + + if !tc.expHitHardLimit { + require.Len(t, hardLimitedPresets, 0) + return + } + + require.Len(t, hardLimitedPresets, 1) + hardLimitedPreset := hardLimitedPresets[0] + require.Equal(t, hardLimitedPreset.TemplateVersionID, tmpl.ActiveVersionID) + require.Equal(t, hardLimitedPreset.PresetID, tmplV1.preset.ID) + }) + } + + t.Run("Ignore Inactive Version", func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitShort) + dbgen.Organization(t, db, database.Organization{ + ID: orgID, + }) + dbgen.User(t, db, database.User{ + ID: userID, + }) + + tmpl := createTemplate(t, db, orgID, userID) + tmplV1 := createTmplVersionAndPreset(t, db, tmpl, uuid.New(), now, nil) + createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV1, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + + // Active Version + tmplV2 := createTmplVersionAndPreset(t, db, tmpl, tmpl.ActiveVersionID, now, nil) + createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV2, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV2, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + + hardLimit := int64(1) + hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, hardLimit) + require.NoError(t, err) + + require.Len(t, hardLimitedPresets, 1) + hardLimitedPreset := hardLimitedPresets[0] + require.Equal(t, hardLimitedPreset.TemplateVersionID, tmpl.ActiveVersionID) + require.Equal(t, hardLimitedPreset.PresetID, tmplV2.preset.ID) + }) + + t.Run("Multiple Templates", func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitShort) + dbgen.Organization(t, db, database.Organization{ + ID: orgID, + }) + dbgen.User(t, db, database.User{ + ID: userID, + }) + + tmpl1 := createTemplate(t, db, orgID, userID) + tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil) + createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + + tmpl2 := createTemplate(t, db, orgID, userID) + tmpl2V1 := createTmplVersionAndPreset(t, db, tmpl2, tmpl2.ActiveVersionID, now, nil) + createPrebuiltWorkspace(ctx, t, db, tmpl2, tmpl2V1, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + + hardLimit := int64(1) + hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, hardLimit) + + require.NoError(t, err) + + require.Len(t, hardLimitedPresets, 2) + { + hardLimitedPreset := findPresetByTmplVersionID(hardLimitedPresets, tmpl1.ActiveVersionID) + require.Equal(t, hardLimitedPreset.TemplateVersionID, tmpl1.ActiveVersionID) + require.Equal(t, hardLimitedPreset.PresetID, tmpl1V1.preset.ID) + } + { + hardLimitedPreset := findPresetByTmplVersionID(hardLimitedPresets, tmpl2.ActiveVersionID) + require.Equal(t, hardLimitedPreset.TemplateVersionID, tmpl2.ActiveVersionID) + require.Equal(t, hardLimitedPreset.PresetID, tmpl2V1.preset.ID) + } + }) + + t.Run("Multiple Templates, Versions and Workspace Builds", func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitShort) + dbgen.Organization(t, db, database.Organization{ + ID: orgID, + }) + dbgen.User(t, db, database.User{ + ID: userID, + }) + + tmpl1 := createTemplate(t, db, orgID, userID) + tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil) + createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + + tmpl2 := createTemplate(t, db, orgID, userID) + tmpl2V1 := createTmplVersionAndPreset(t, db, tmpl2, tmpl2.ActiveVersionID, now, nil) + createPrebuiltWorkspace(ctx, t, db, tmpl2, tmpl2V1, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + createPrebuiltWorkspace(ctx, t, db, tmpl2, tmpl2V1, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + + tmpl3 := createTemplate(t, db, orgID, userID) + tmpl3V1 := createTmplVersionAndPreset(t, db, tmpl3, uuid.New(), now, nil) + createPrebuiltWorkspace(ctx, t, db, tmpl3, tmpl3V1, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + + tmpl3V2 := createTmplVersionAndPreset(t, db, tmpl3, tmpl3.ActiveVersionID, now, nil) + createPrebuiltWorkspace(ctx, t, db, tmpl3, tmpl3V2, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + createPrebuiltWorkspace(ctx, t, db, tmpl3, tmpl3V2, orgID, now, &createPrebuiltWorkspaceOpts{ + failedJob: true, + }) + + hardLimit := int64(2) + hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, hardLimit) + require.NoError(t, err) + + require.Len(t, hardLimitedPresets, 3) + { + hardLimitedPreset := findPresetByTmplVersionID(hardLimitedPresets, tmpl1.ActiveVersionID) + require.Equal(t, hardLimitedPreset.TemplateVersionID, tmpl1.ActiveVersionID) + require.Equal(t, hardLimitedPreset.PresetID, tmpl1V1.preset.ID) + } + { + hardLimitedPreset := findPresetByTmplVersionID(hardLimitedPresets, tmpl2.ActiveVersionID) + require.Equal(t, hardLimitedPreset.TemplateVersionID, tmpl2.ActiveVersionID) + require.Equal(t, hardLimitedPreset.PresetID, tmpl2V1.preset.ID) + } + { + hardLimitedPreset := findPresetByTmplVersionID(hardLimitedPresets, tmpl3.ActiveVersionID) + require.Equal(t, hardLimitedPreset.TemplateVersionID, tmpl3.ActiveVersionID) + require.Equal(t, hardLimitedPreset.PresetID, tmpl3V2.preset.ID) + } + }) + + t.Run("No Workspace Builds", func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitShort) + dbgen.Organization(t, db, database.Organization{ + ID: orgID, + }) + dbgen.User(t, db, database.User{ + ID: userID, + }) + + tmpl1 := createTemplate(t, db, orgID, userID) + tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil) + _ = tmpl1V1 + + hardLimit := int64(1) + hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, hardLimit) + require.NoError(t, err) + require.Nil(t, hardLimitedPresets) + }) + + t.Run("No Failed Workspace Builds", func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitShort) + dbgen.Organization(t, db, database.Organization{ + ID: orgID, + }) + dbgen.User(t, db, database.User{ + ID: userID, + }) + + tmpl1 := createTemplate(t, db, orgID, userID) + tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil) + successfulJobOpts := createPrebuiltWorkspaceOpts{} + createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &successfulJobOpts) + createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &successfulJobOpts) + createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &successfulJobOpts) + + hardLimit := int64(1) + hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, hardLimit) + require.NoError(t, err) + require.Nil(t, hardLimitedPresets) + }) +} + func requireUsersMatch(t testing.TB, expected []database.User, found []database.GetUsersRow, msg string) { t.Helper() require.ElementsMatch(t, expected, database.ConvertUserRows(found), msg) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index cd5b297c85e07..3ae7eb65273a2 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6286,6 +6286,71 @@ func (q *sqlQuerier) GetPrebuildMetrics(ctx context.Context) ([]GetPrebuildMetri return items, nil } +const getPresetsAtFailureLimit = `-- name: GetPresetsAtFailureLimit :many +WITH filtered_builds AS ( + -- Only select builds which are for prebuild creations + SELECT wlb.template_version_id, wlb.created_at, tvp.id AS preset_id, wlb.job_status, tvp.desired_instances + FROM template_version_presets tvp + INNER JOIN workspace_latest_builds wlb ON wlb.template_version_preset_id = tvp.id + INNER JOIN workspaces w ON wlb.workspace_id = w.id + INNER JOIN template_versions tv ON wlb.template_version_id = tv.id + INNER JOIN templates t ON tv.template_id = t.id AND t.active_version_id = tv.id + WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration. + AND wlb.transition = 'start'::workspace_transition + AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0' +), +time_sorted_builds AS ( + -- Group builds by preset, then sort each group by created_at. + SELECT fb.template_version_id, fb.created_at, fb.preset_id, fb.job_status, fb.desired_instances, + ROW_NUMBER() OVER (PARTITION BY fb.preset_id ORDER BY fb.created_at DESC) as rn + FROM filtered_builds fb +) +SELECT + tsb.template_version_id, + tsb.preset_id +FROM time_sorted_builds tsb +WHERE tsb.rn <= $1::bigint + AND tsb.job_status = 'failed'::provisioner_job_status +GROUP BY tsb.template_version_id, tsb.preset_id +HAVING COUNT(*) = $1::bigint +` + +type GetPresetsAtFailureLimitRow struct { + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + PresetID uuid.UUID `db:"preset_id" json:"preset_id"` +} + +// GetPresetsAtFailureLimit groups workspace builds by preset ID. +// Each preset is associated with exactly one template version ID. +// For each preset, the query checks the last hard_limit builds. +// If all of them failed, the preset is considered to have hit the hard failure limit. +// The query returns a list of preset IDs that have reached this failure threshold. +// Only active template versions with configured presets are considered. +// For each preset, check the last hard_limit builds. +// If all of them failed, the preset is considered to have hit the hard failure limit. +func (q *sqlQuerier) GetPresetsAtFailureLimit(ctx context.Context, hardLimit int64) ([]GetPresetsAtFailureLimitRow, error) { + rows, err := q.db.QueryContext(ctx, getPresetsAtFailureLimit, hardLimit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetPresetsAtFailureLimitRow + for rows.Next() { + var i GetPresetsAtFailureLimitRow + if err := rows.Scan(&i.TemplateVersionID, &i.PresetID); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getPresetsBackoff = `-- name: GetPresetsBackoff :many WITH filtered_builds AS ( -- Only select builds which are for prebuild creations diff --git a/coderd/database/queries/prebuilds.sql b/coderd/database/queries/prebuilds.sql index 1d3a827c98586..8cd1feb544dd2 100644 --- a/coderd/database/queries/prebuilds.sql +++ b/coderd/database/queries/prebuilds.sql @@ -125,6 +125,42 @@ WHERE tsb.rn <= tsb.desired_instances -- Fetch the last N builds, where N is the AND created_at >= @lookback::timestamptz GROUP BY tsb.template_version_id, tsb.preset_id, fc.num_failed; +-- GetPresetsAtFailureLimit groups workspace builds by preset ID. +-- Each preset is associated with exactly one template version ID. +-- For each preset, the query checks the last hard_limit builds. +-- If all of them failed, the preset is considered to have hit the hard failure limit. +-- The query returns a list of preset IDs that have reached this failure threshold. +-- Only active template versions with configured presets are considered. +-- name: GetPresetsAtFailureLimit :many +WITH filtered_builds AS ( + -- Only select builds which are for prebuild creations + SELECT wlb.template_version_id, wlb.created_at, tvp.id AS preset_id, wlb.job_status, tvp.desired_instances + FROM template_version_presets tvp + INNER JOIN workspace_latest_builds wlb ON wlb.template_version_preset_id = tvp.id + INNER JOIN workspaces w ON wlb.workspace_id = w.id + INNER JOIN template_versions tv ON wlb.template_version_id = tv.id + INNER JOIN templates t ON tv.template_id = t.id AND t.active_version_id = tv.id + WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration. + AND wlb.transition = 'start'::workspace_transition + AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0' +), +time_sorted_builds AS ( + -- Group builds by preset, then sort each group by created_at. + SELECT fb.template_version_id, fb.created_at, fb.preset_id, fb.job_status, fb.desired_instances, + ROW_NUMBER() OVER (PARTITION BY fb.preset_id ORDER BY fb.created_at DESC) as rn + FROM filtered_builds fb +) +SELECT + tsb.template_version_id, + tsb.preset_id +FROM time_sorted_builds tsb +-- For each preset, check the last hard_limit builds. +-- If all of them failed, the preset is considered to have hit the hard failure limit. +WHERE tsb.rn <= @hard_limit::bigint + AND tsb.job_status = 'failed'::provisioner_job_status +GROUP BY tsb.template_version_id, tsb.preset_id +HAVING COUNT(*) = @hard_limit::bigint; + -- name: GetPrebuildMetrics :many SELECT t.name as template_name, diff --git a/coderd/prebuilds/global_snapshot.go b/coderd/prebuilds/global_snapshot.go index 0cf3fa3facc3a..53828e27f59df 100644 --- a/coderd/prebuilds/global_snapshot.go +++ b/coderd/prebuilds/global_snapshot.go @@ -10,10 +10,11 @@ import ( // GlobalSnapshot represents a full point-in-time snapshot of state relating to prebuilds across all templates. type GlobalSnapshot struct { - Presets []database.GetTemplatePresetsWithPrebuildsRow - RunningPrebuilds []database.GetRunningPrebuiltWorkspacesRow - PrebuildsInProgress []database.CountInProgressPrebuildsRow - Backoffs []database.GetPresetsBackoffRow + Presets []database.GetTemplatePresetsWithPrebuildsRow + RunningPrebuilds []database.GetRunningPrebuiltWorkspacesRow + PrebuildsInProgress []database.CountInProgressPrebuildsRow + Backoffs []database.GetPresetsBackoffRow + HardLimitedPresetMap map[uuid.UUID]database.GetPresetsAtFailureLimitRow } func NewGlobalSnapshot( @@ -21,12 +22,19 @@ func NewGlobalSnapshot( runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, prebuildsInProgress []database.CountInProgressPrebuildsRow, backoffs []database.GetPresetsBackoffRow, + hardLimitedPresets []database.GetPresetsAtFailureLimitRow, ) GlobalSnapshot { + hardLimitedPresetMap := make(map[uuid.UUID]database.GetPresetsAtFailureLimitRow, len(hardLimitedPresets)) + for _, preset := range hardLimitedPresets { + hardLimitedPresetMap[preset.PresetID] = preset + } + return GlobalSnapshot{ - Presets: presets, - RunningPrebuilds: runningPrebuilds, - PrebuildsInProgress: prebuildsInProgress, - Backoffs: backoffs, + Presets: presets, + RunningPrebuilds: runningPrebuilds, + PrebuildsInProgress: prebuildsInProgress, + Backoffs: backoffs, + HardLimitedPresetMap: hardLimitedPresetMap, } } @@ -64,3 +72,8 @@ func (s GlobalSnapshot) FilterByPreset(presetID uuid.UUID) (*PresetSnapshot, err Backoff: backoffPtr, }, nil } + +func (s GlobalSnapshot) IsHardLimited(presetID uuid.UUID) bool { + _, ok := s.HardLimitedPresetMap[presetID] + return ok +} diff --git a/coderd/prebuilds/preset_snapshot_test.go b/coderd/prebuilds/preset_snapshot_test.go index a5acb40e5311f..2febf1d13ec91 100644 --- a/coderd/prebuilds/preset_snapshot_test.go +++ b/coderd/prebuilds/preset_snapshot_test.go @@ -73,7 +73,7 @@ func TestNoPrebuilds(t *testing.T) { preset(true, 0, current), } - snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil) + snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil, nil) ps, err := snapshot.FilterByPreset(current.presetID) require.NoError(t, err) @@ -98,7 +98,7 @@ func TestNetNew(t *testing.T) { preset(true, 1, current), } - snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil) + snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil, nil) ps, err := snapshot.FilterByPreset(current.presetID) require.NoError(t, err) @@ -138,7 +138,7 @@ func TestOutdatedPrebuilds(t *testing.T) { var inProgress []database.CountInProgressPrebuildsRow // WHEN: calculating the outdated preset's state. - snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil) + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil, nil) ps, err := snapshot.FilterByPreset(outdated.presetID) require.NoError(t, err) @@ -200,7 +200,7 @@ func TestDeleteOutdatedPrebuilds(t *testing.T) { } // WHEN: calculating the outdated preset's state. - snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil) + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil, nil) ps, err := snapshot.FilterByPreset(outdated.presetID) require.NoError(t, err) @@ -442,7 +442,7 @@ func TestInProgressActions(t *testing.T) { } // WHEN: calculating the current preset's state. - snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil) + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil, nil) ps, err := snapshot.FilterByPreset(current.presetID) require.NoError(t, err) @@ -485,7 +485,7 @@ func TestExtraneous(t *testing.T) { var inProgress []database.CountInProgressPrebuildsRow // WHEN: calculating the current preset's state. - snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil) + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil, nil) ps, err := snapshot.FilterByPreset(current.presetID) require.NoError(t, err) @@ -525,7 +525,7 @@ func TestDeprecated(t *testing.T) { var inProgress []database.CountInProgressPrebuildsRow // WHEN: calculating the current preset's state. - snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil) + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil, nil) ps, err := snapshot.FilterByPreset(current.presetID) require.NoError(t, err) @@ -576,7 +576,7 @@ func TestLatestBuildFailed(t *testing.T) { } // WHEN: calculating the current preset's state. - snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, backoffs) + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, backoffs, nil) psCurrent, err := snapshot.FilterByPreset(current.presetID) require.NoError(t, err) @@ -669,7 +669,7 @@ func TestMultiplePresetsPerTemplateVersion(t *testing.T) { }, } - snapshot := prebuilds.NewGlobalSnapshot(presets, nil, inProgress, nil) + snapshot := prebuilds.NewGlobalSnapshot(presets, nil, inProgress, nil, nil) // Nothing has to be created for preset 1. { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 0741bf9e3844a..63e14f61d584b 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -807,6 +807,12 @@ type PrebuildsConfig struct { // ReconciliationBackoffLookback determines the time window to look back when calculating // the number of failed prebuilds, which influences the backoff strategy. ReconciliationBackoffLookback serpent.Duration `json:"reconciliation_backoff_lookback" typescript:",notnull"` + + // FailureHardLimit defines the maximum number of consecutive failed prebuild attempts allowed + // before a preset is considered to be in a hard limit state. When a preset hits this limit, + // no new prebuilds will be created until the limit is reset. + // FailureHardLimit is disabled when set to zero. + FailureHardLimit serpent.Int64 `json:"failure_hard_limit" typescript:"failure_hard_limit"` } const ( @@ -3086,6 +3092,17 @@ Write out the current server config as YAML to stdout.`, Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"), Hidden: true, }, + { + Name: "Failure Hard Limit", + Description: "Maximum number of consecutive failed prebuilds before a preset hits the hard limit. FailureHardLimit is disabled when set to zero.", + Flag: "workspace-prebuilds-failure-hard-limit", + Env: "CODER_WORKSPACE_PREBUILDS_FAILURE_HARD_LIMIT", + Value: &c.Prebuilds.FailureHardLimit, + Default: "3", + Group: &deploymentGroupPrebuilds, + YAML: "failure_hard_limit", + Hidden: true, + }, } return opts diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index c14c317066a39..12454145569bb 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -533,6 +533,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "wildcard_access_url": "string", "workspace_hostname_suffix": "string", "workspace_prebuilds": { + "failure_hard_limit": 0, "reconciliation_backoff_interval": 0, "reconciliation_backoff_lookback": 0, "reconciliation_interval": 0 diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 6ca005b4ec69c..c7e0130922990 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -2672,6 +2672,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "wildcard_access_url": "string", "workspace_hostname_suffix": "string", "workspace_prebuilds": { + "failure_hard_limit": 0, "reconciliation_backoff_interval": 0, "reconciliation_backoff_lookback": 0, "reconciliation_interval": 0 @@ -3170,6 +3171,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "wildcard_access_url": "string", "workspace_hostname_suffix": "string", "workspace_prebuilds": { + "failure_hard_limit": 0, "reconciliation_backoff_interval": 0, "reconciliation_backoff_lookback": 0, "reconciliation_interval": 0 @@ -5229,6 +5231,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ```json { + "failure_hard_limit": 0, "reconciliation_backoff_interval": 0, "reconciliation_backoff_lookback": 0, "reconciliation_interval": 0 @@ -5237,11 +5240,12 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ### Properties -| Name | Type | Required | Restrictions | Description | -|-----------------------------------|---------|----------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `reconciliation_backoff_interval` | integer | false | | Reconciliation backoff interval specifies the amount of time to increase the backoff interval when errors occur during reconciliation. | -| `reconciliation_backoff_lookback` | integer | false | | Reconciliation backoff lookback determines the time window to look back when calculating the number of failed prebuilds, which influences the backoff strategy. | -| `reconciliation_interval` | integer | false | | Reconciliation interval defines how often the workspace prebuilds state should be reconciled. | +| Name | Type | Required | Restrictions | Description | +|-----------------------------------|---------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `failure_hard_limit` | integer | false | | Failure hard limit defines the maximum number of consecutive failed prebuild attempts allowed before a preset is considered to be in a hard limit state. When a preset hits this limit, no new prebuilds will be created until the limit is reset. FailureHardLimit is disabled when set to zero. | +| `reconciliation_backoff_interval` | integer | false | | Reconciliation backoff interval specifies the amount of time to increase the backoff interval when errors occur during reconciliation. | +| `reconciliation_backoff_lookback` | integer | false | | Reconciliation backoff lookback determines the time window to look back when calculating the number of failed prebuilds, which influences the backoff strategy. | +| `reconciliation_interval` | integer | false | | Reconciliation interval defines how often the workspace prebuilds state should be reconciled. | ## codersdk.Preset diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index c31da695637ba..98c7c97d852bc 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -243,6 +243,11 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error { var eg errgroup.Group // Reconcile presets in parallel. Each preset in its own goroutine. for _, preset := range snapshot.Presets { + if snapshot.IsHardLimited(preset.ID) { + logger.Debug(ctx, "skipping hard limited preset", slog.F("preset_id", preset.ID), slog.F("name", preset.Name)) + continue + } + ps, err := snapshot.FilterByPreset(preset.ID) if err != nil { logger.Warn(ctx, "failed to find preset snapshot", slog.Error(err), slog.F("preset_id", preset.ID.String())) @@ -292,6 +297,7 @@ func (c *StoreReconciler) SnapshotState(ctx context.Context, store database.Stor if len(presetsWithPrebuilds) == 0 { return nil } + allRunningPrebuilds, err := db.GetRunningPrebuiltWorkspaces(ctx) if err != nil { return xerrors.Errorf("failed to get running prebuilds: %w", err) @@ -307,7 +313,18 @@ func (c *StoreReconciler) SnapshotState(ctx context.Context, store database.Stor return xerrors.Errorf("failed to get backoffs for presets: %w", err) } - state = prebuilds.NewGlobalSnapshot(presetsWithPrebuilds, allRunningPrebuilds, allPrebuildsInProgress, presetsBackoff) + hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, c.cfg.FailureHardLimit.Value()) + if err != nil { + return xerrors.Errorf("failed to get hard limited presets: %w", err) + } + + state = prebuilds.NewGlobalSnapshot( + presetsWithPrebuilds, + allRunningPrebuilds, + allPrebuildsInProgress, + presetsBackoff, + hardLimitedPresets, + ) return nil }, &database.TxOptions{ Isolation: sql.LevelRepeatableRead, // This mirrors the MVCC snapshotting Postgres does when using CTEs diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index a1666134a7965..516db373cba88 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -645,6 +645,102 @@ func TestDeletionOfPrebuiltWorkspaceWithInvalidPreset(t *testing.T) { require.Equal(t, database.WorkspaceTransitionDelete, builds[0].Transition) } +func TestSkippingHardLimitedPresets(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires postgres") + } + + // Test cases verify the behavior of prebuild creation depending on configured failure limits + testCases := []struct { + name string + hardLimit int64 + isHardLimitHit bool + }{ + { + name: "hard limit is hit - skip creation of prebuilt workspace", + hardLimit: 1, + isHardLimitHit: true, + }, + { + name: "hard limit is not hit - try to create prebuilt workspace again", + hardLimit: 2, + isHardLimitHit: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + templateDeleted := false + + clock := quartz.NewMock(t) + ctx := testutil.Context(t, testutil.WaitShort) + cfg := codersdk.PrebuildsConfig{ + FailureHardLimit: serpent.Int64(tc.hardLimit), + } + logger := slogtest.Make( + t, &slogtest.Options{IgnoreErrors: true}, + ).Leveled(slog.LevelDebug) + db, pubSub := dbtestutil.NewDB(t) + controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, prometheus.NewRegistry()) + + // Set up test environment with a template, version, and preset + ownerID := uuid.New() + dbgen.User(t, db, database.User{ + ID: ownerID, + }) + org, template := setupTestDBTemplate(t, db, ownerID, templateDeleted) + templateVersionID := setupTestDBTemplateVersion(ctx, t, clock, db, pubSub, org.ID, ownerID, template.ID) + preset := setupTestDBPreset(t, db, templateVersionID, 1, uuid.New().String()) + + // Create a failed prebuild workspace that counts toward the hard failure limit. + prebuiltWorkspace := setupTestDBPrebuild( + t, + clock, + db, + pubSub, + database.WorkspaceTransitionStart, + database.ProvisionerJobStatusFailed, + org.ID, + preset, + template.ID, + templateVersionID, + ) + _ = prebuiltWorkspace + + // Verify initial state: one failed workspace exists + workspaces, err := db.GetWorkspacesByTemplateID(ctx, template.ID) + require.NoError(t, err) + workspaceCount := len(workspaces) + require.Equal(t, 1, workspaceCount) + + // Advance clock to bypass backoff mechanisms + clock.Advance(time.Second).MustWait(ctx) + + // Trigger reconciliation to attempt creating a new prebuild + // The outcome depends on whether the hard limit has been reached + require.NoError(t, controller.ReconcileAll(ctx)) + + // Verify the final state after reconciliation + workspaces, err = db.GetWorkspacesByTemplateID(ctx, template.ID) + require.NoError(t, err) + + if tc.isHardLimitHit { + // When hard limit is reached, no new workspace should be created + workspaceCount = len(workspaces) + require.Equal(t, 1, workspaceCount) + } else { + // When hard limit is not reached, a new workspace should be created + workspaceCount = len(workspaces) + require.Equal(t, 2, workspaceCount) + } + }) + } +} + func TestRunLoop(t *testing.T) { t.Parallel() diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b1fcb296de4e8..fe7530f0daad0 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1761,6 +1761,7 @@ export interface PrebuildsConfig { readonly reconciliation_interval: number; readonly reconciliation_backoff_interval: number; readonly reconciliation_backoff_lookback: number; + readonly failure_hard_limit: number; } // From codersdk/presets.go From 701e2a81309c04326a206edea372a492600d9e07 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Mon, 12 May 2025 15:56:42 +0000 Subject: [PATCH 02/47] refactor: CR's fixes --- coderd/database/querier_test.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index f91e830705686..f161d27b9a978 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -4545,8 +4545,7 @@ func TestGetPresetsAtFailureLimit(t *testing.T) { failedJob: true, }) - hardLimit := int64(1) - hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, hardLimit) + hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, 1) require.NoError(t, err) require.Len(t, hardLimitedPresets, 1) @@ -4579,8 +4578,7 @@ func TestGetPresetsAtFailureLimit(t *testing.T) { failedJob: true, }) - hardLimit := int64(1) - hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, hardLimit) + hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, 1) require.NoError(t, err) @@ -4679,8 +4677,7 @@ func TestGetPresetsAtFailureLimit(t *testing.T) { tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil) _ = tmpl1V1 - hardLimit := int64(1) - hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, hardLimit) + hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, 1) require.NoError(t, err) require.Nil(t, hardLimitedPresets) }) @@ -4704,8 +4701,7 @@ func TestGetPresetsAtFailureLimit(t *testing.T) { createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &successfulJobOpts) createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &successfulJobOpts) - hardLimit := int64(1) - hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, hardLimit) + hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, 1) require.NoError(t, err) require.Nil(t, hardLimitedPresets) }) From 0fdd096951a7a9219f9be05f4a8b827766dd59e9 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Mon, 12 May 2025 19:07:06 +0000 Subject: [PATCH 03/47] refactor: CR's fixes --- coderd/prebuilds/global_snapshot.go | 43 +++++++++++------------- coderd/prebuilds/preset_snapshot.go | 9 ++--- enterprise/coderd/prebuilds/reconcile.go | 10 +++--- 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/coderd/prebuilds/global_snapshot.go b/coderd/prebuilds/global_snapshot.go index 53828e27f59df..9110f57574e7b 100644 --- a/coderd/prebuilds/global_snapshot.go +++ b/coderd/prebuilds/global_snapshot.go @@ -10,11 +10,11 @@ import ( // GlobalSnapshot represents a full point-in-time snapshot of state relating to prebuilds across all templates. type GlobalSnapshot struct { - Presets []database.GetTemplatePresetsWithPrebuildsRow - RunningPrebuilds []database.GetRunningPrebuiltWorkspacesRow - PrebuildsInProgress []database.CountInProgressPrebuildsRow - Backoffs []database.GetPresetsBackoffRow - HardLimitedPresetMap map[uuid.UUID]database.GetPresetsAtFailureLimitRow + Presets []database.GetTemplatePresetsWithPrebuildsRow + RunningPrebuilds []database.GetRunningPrebuiltWorkspacesRow + PrebuildsInProgress []database.CountInProgressPrebuildsRow + Backoffs []database.GetPresetsBackoffRow + HardLimitedPresets []database.GetPresetsAtFailureLimitRow } func NewGlobalSnapshot( @@ -24,17 +24,12 @@ func NewGlobalSnapshot( backoffs []database.GetPresetsBackoffRow, hardLimitedPresets []database.GetPresetsAtFailureLimitRow, ) GlobalSnapshot { - hardLimitedPresetMap := make(map[uuid.UUID]database.GetPresetsAtFailureLimitRow, len(hardLimitedPresets)) - for _, preset := range hardLimitedPresets { - hardLimitedPresetMap[preset.PresetID] = preset - } - return GlobalSnapshot{ - Presets: presets, - RunningPrebuilds: runningPrebuilds, - PrebuildsInProgress: prebuildsInProgress, - Backoffs: backoffs, - HardLimitedPresetMap: hardLimitedPresetMap, + Presets: presets, + RunningPrebuilds: runningPrebuilds, + PrebuildsInProgress: prebuildsInProgress, + Backoffs: backoffs, + HardLimitedPresets: hardLimitedPresets, } } @@ -65,15 +60,15 @@ func (s GlobalSnapshot) FilterByPreset(presetID uuid.UUID) (*PresetSnapshot, err backoffPtr = &backoff } + _, isHardLimited := slice.Find(s.HardLimitedPresets, func(row database.GetPresetsAtFailureLimitRow) bool { + return row.PresetID == preset.ID + }) + return &PresetSnapshot{ - Preset: preset, - Running: running, - InProgress: inProgress, - Backoff: backoffPtr, + Preset: preset, + Running: running, + InProgress: inProgress, + Backoff: backoffPtr, + IsHardLimited: isHardLimited, }, nil } - -func (s GlobalSnapshot) IsHardLimited(presetID uuid.UUID) bool { - _, ok := s.HardLimitedPresetMap[presetID] - return ok -} diff --git a/coderd/prebuilds/preset_snapshot.go b/coderd/prebuilds/preset_snapshot.go index 8441a350187d2..40e77de5ab3e3 100644 --- a/coderd/prebuilds/preset_snapshot.go +++ b/coderd/prebuilds/preset_snapshot.go @@ -32,10 +32,11 @@ const ( // It contains the raw data needed to calculate the current state of a preset's prebuilds, // including running prebuilds, in-progress builds, and backoff information. type PresetSnapshot struct { - Preset database.GetTemplatePresetsWithPrebuildsRow - Running []database.GetRunningPrebuiltWorkspacesRow - InProgress []database.CountInProgressPrebuildsRow - Backoff *database.GetPresetsBackoffRow + Preset database.GetTemplatePresetsWithPrebuildsRow + Running []database.GetRunningPrebuiltWorkspacesRow + InProgress []database.CountInProgressPrebuildsRow + Backoff *database.GetPresetsBackoffRow + IsHardLimited bool } // ReconciliationState represents the processed state of a preset's prebuilds, diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 98c7c97d852bc..33c7c58cc5d33 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -243,11 +243,6 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error { var eg errgroup.Group // Reconcile presets in parallel. Each preset in its own goroutine. for _, preset := range snapshot.Presets { - if snapshot.IsHardLimited(preset.ID) { - logger.Debug(ctx, "skipping hard limited preset", slog.F("preset_id", preset.ID), slog.F("name", preset.Name)) - continue - } - ps, err := snapshot.FilterByPreset(preset.ID) if err != nil { logger.Warn(ctx, "failed to find preset snapshot", slog.Error(err), slog.F("preset_id", preset.ID.String())) @@ -345,6 +340,11 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres slog.F("preset_name", ps.Preset.Name), ) + if ps.IsHardLimited { + logger.Debug(ctx, "skipping hard limited preset", slog.F("preset_id", ps.Preset.ID), slog.F("name", ps.Preset.Name)) + return nil + } + state := ps.CalculateState() actions, err := c.CalculateActions(ctx, ps) if err != nil { From 13c0e2809f7638730d37e423e474aad61c72e7f3 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Mon, 12 May 2025 19:32:05 +0000 Subject: [PATCH 04/47] refactor: CR's fixes --- enterprise/coderd/prebuilds/reconcile_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index 516db373cba88..6e8e8536b296b 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -697,7 +697,7 @@ func TestSkippingHardLimitedPresets(t *testing.T) { preset := setupTestDBPreset(t, db, templateVersionID, 1, uuid.New().String()) // Create a failed prebuild workspace that counts toward the hard failure limit. - prebuiltWorkspace := setupTestDBPrebuild( + setupTestDBPrebuild( t, clock, db, @@ -709,7 +709,6 @@ func TestSkippingHardLimitedPresets(t *testing.T) { template.ID, templateVersionID, ) - _ = prebuiltWorkspace // Verify initial state: one failed workspace exists workspaces, err := db.GetWorkspacesByTemplateID(ctx, template.ID) From 29e9cffdaeae8b4d8e8163f978197c22bb47975b Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Mon, 12 May 2025 19:36:55 +0000 Subject: [PATCH 05/47] refactor: CR's fixes --- enterprise/coderd/prebuilds/reconcile_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index 6e8e8536b296b..ee05037bd30fb 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -729,12 +729,10 @@ func TestSkippingHardLimitedPresets(t *testing.T) { if tc.isHardLimitHit { // When hard limit is reached, no new workspace should be created - workspaceCount = len(workspaces) - require.Equal(t, 1, workspaceCount) + require.Equal(t, 1, len(workspaces)) } else { // When hard limit is not reached, a new workspace should be created - workspaceCount = len(workspaces) - require.Equal(t, 2, workspaceCount) + require.Equal(t, 2, len(workspaces)) } }) } From 5c60065db5b1a21de87a30aa0039a38139fd166e Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Wed, 14 May 2025 16:15:54 +0000 Subject: [PATCH 06/47] feat: add notification for prebuilds failure --- ...ebuild_failure_limit_notification.down.sql | 1 + ...prebuild_failure_limit_notification.up.sql | 10 +++++ coderd/notifications/events.go | 1 + enterprise/coderd/prebuilds/reconcile.go | 45 ++++++++++++++++++- 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 coderd/database/migrations/000325_prebuild_failure_limit_notification.down.sql create mode 100644 coderd/database/migrations/000325_prebuild_failure_limit_notification.up.sql diff --git a/coderd/database/migrations/000325_prebuild_failure_limit_notification.down.sql b/coderd/database/migrations/000325_prebuild_failure_limit_notification.down.sql new file mode 100644 index 0000000000000..40697c7bbc3d2 --- /dev/null +++ b/coderd/database/migrations/000325_prebuild_failure_limit_notification.down.sql @@ -0,0 +1 @@ +DELETE FROM notification_templates WHERE id = '414d9331-c1fc-4761-b40c-d1f4702279eb'; diff --git a/coderd/database/migrations/000325_prebuild_failure_limit_notification.up.sql b/coderd/database/migrations/000325_prebuild_failure_limit_notification.up.sql new file mode 100644 index 0000000000000..e701e05a2741c --- /dev/null +++ b/coderd/database/migrations/000325_prebuild_failure_limit_notification.up.sql @@ -0,0 +1,10 @@ +INSERT INTO notification_templates +(id, name, title_template, body_template, "group", actions) +VALUES ('414d9331-c1fc-4761-b40c-d1f4702279eb', + 'Prebuild Failure Limit Reached', + E'There is a problem creating prebuilt workspaces for the preset', + $$ + The number of failed prebuilds has reached the hard limit for template **{{ .Labels.template }}** and preset **{{ .Labels.preset }}** + $$, + 'Template Events', + '[]'::jsonb); diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 35d9925055da5..17cf2c1452ef6 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -40,6 +40,7 @@ var ( TemplateWorkspaceBuildsFailedReport = uuid.MustParse("34a20db2-e9cc-4a93-b0e4-8569699d7a00") TemplateWorkspaceResourceReplaced = uuid.MustParse("89d9745a-816e-4695-a17f-3d0a229e2b8d") + PrebuildFailureLimitReached = uuid.MustParse("414d9331-c1fc-4761-b40c-d1f4702279eb") ) // Notification-related events. diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index ab38a7074cfb3..7361c470851df 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -362,7 +362,50 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres ) if ps.IsHardLimited { - logger.Debug(ctx, "skipping hard limited preset", slog.F("preset_id", ps.Preset.ID), slog.F("name", ps.Preset.Name)) + logger.Warn(ctx, "skipping hard limited preset", slog.F("preset_id", ps.Preset.ID), slog.F("name", ps.Preset.Name)) + + // TODO: rename ctx? + // nolint:gocritic // Necessary to query all the required data. + ctx := dbauthz.AsSystemRestricted(ctx) + + // TODO(yevhenii): move into separate function + // Send notification to template admins. + if c.notifEnq == nil { + c.logger.Warn(ctx, "notification enqueuer not set, cannot send resource replacement notification(s)") + return nil + } + + // TODO(yevhenii): remove owner from the list + templateAdmins, err := c.store.GetUsers(ctx, database.GetUsersParams{ + RbacRole: []string{codersdk.RoleTemplateAdmin, codersdk.RoleOwner}, + }) + if err != nil { + return xerrors.Errorf("fetch template admins: %w", err) + } + + for _, templateAdmin := range templateAdmins { + if _, err := c.notifEnq.EnqueueWithData(ctx, templateAdmin.ID, notifications.PrebuildFailureLimitReached, + map[string]string{ + "org": ps.Preset.OrganizationName, + "template": ps.Preset.TemplateName, + "template_version": ps.Preset.TemplateVersionName, + "preset": ps.Preset.Name, + }, + map[string]any{}, + "prebuilds_reconciler", + // Associate this notification with all the related entities. + ps.Preset.TemplateID, ps.Preset.TemplateVersionID, ps.Preset.ID, + ); err != nil { + c.logger.Error(ctx, + "failed to send notification", + slog.Error(err), + slog.F("template_admin_id", templateAdmin.ID.String()), + ) + + continue + } + } + return nil } From b0248c3cdb075898df94d898a06ca5e6129a1499 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Thu, 15 May 2025 18:53:09 +0000 Subject: [PATCH 07/47] test: fix tests --- enterprise/coderd/prebuilds/reconcile_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index 81063feb120ee..fd235557de7ec 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -689,7 +689,7 @@ func TestSkippingHardLimitedPresets(t *testing.T) { t, &slogtest.Options{IgnoreErrors: true}, ).Leveled(slog.LevelDebug) db, pubSub := dbtestutil.NewDB(t) - controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, prometheus.NewRegistry()) + controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer()) // Set up test environment with a template, version, and preset ownerID := uuid.New() From 08b18b09c5535a77f04cb5b70db48b8f03db829d Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Thu, 15 May 2025 20:22:14 +0000 Subject: [PATCH 08/47] test: add a notification test --- enterprise/coderd/prebuilds/reconcile_test.go | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index fd235557de7ec..9391410df781d 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -689,7 +689,13 @@ func TestSkippingHardLimitedPresets(t *testing.T) { t, &slogtest.Options{IgnoreErrors: true}, ).Leveled(slog.LevelDebug) db, pubSub := dbtestutil.NewDB(t) - controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer()) + fakeEnqueuer := newFakeEnqueuer() + controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, prometheus.NewRegistry(), fakeEnqueuer) + + // Template admin to receive a notification. + templateAdmin := dbgen.User(t, db, database.User{ + RBACRoles: []string{codersdk.RoleTemplateAdmin}, + }) // Set up test environment with a template, version, and preset ownerID := uuid.New() @@ -731,13 +737,28 @@ func TestSkippingHardLimitedPresets(t *testing.T) { workspaces, err = db.GetWorkspacesByTemplateID(ctx, template.ID) require.NoError(t, err) - if tc.isHardLimitHit { - // When hard limit is reached, no new workspace should be created - require.Equal(t, 1, len(workspaces)) - } else { + if !tc.isHardLimitHit { // When hard limit is not reached, a new workspace should be created require.Equal(t, 2, len(workspaces)) + return } + + // When hard limit is reached, no new workspace should be created + require.Equal(t, 1, len(workspaces)) + + // When hard limit is reached, a notification should be sent. + matching := fakeEnqueuer.Sent(func(notification *notificationstest.FakeNotification) bool { + if !assert.Equal(t, notification.TemplateID, notifications.PrebuildFailureLimitReached, "unexpected template") { + return false + } + + if !assert.Equal(t, templateAdmin.ID, notification.UserID, "unexpected receiver") { + return false + } + + return true + }) + require.Len(t, matching, 1) }) } } From 0fd347acf5d42142982458784258f0a717cc2a61 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Fri, 16 May 2025 15:19:15 +0000 Subject: [PATCH 09/47] fix: add prebuild_status migration --- .../000326_add_status_to_template_presets.down.sql | 5 +++++ .../000326_add_status_to_template_presets.up.sql | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 coderd/database/migrations/000326_add_status_to_template_presets.down.sql create mode 100644 coderd/database/migrations/000326_add_status_to_template_presets.up.sql diff --git a/coderd/database/migrations/000326_add_status_to_template_presets.down.sql b/coderd/database/migrations/000326_add_status_to_template_presets.down.sql new file mode 100644 index 0000000000000..8fe04f99cae33 --- /dev/null +++ b/coderd/database/migrations/000326_add_status_to_template_presets.down.sql @@ -0,0 +1,5 @@ +-- Remove the column from the table first (must happen before dropping the enum type) +ALTER TABLE template_version_presets DROP COLUMN prebuild_status; + +-- Then drop the enum type +DROP TYPE prebuild_status; diff --git a/coderd/database/migrations/000326_add_status_to_template_presets.up.sql b/coderd/database/migrations/000326_add_status_to_template_presets.up.sql new file mode 100644 index 0000000000000..617d645b95785 --- /dev/null +++ b/coderd/database/migrations/000326_add_status_to_template_presets.up.sql @@ -0,0 +1,7 @@ +CREATE TYPE prebuild_status AS ENUM ( + 'normal', -- Prebuilds are working as expected; this is the default, healthy state. + 'hard_limited', -- Prebuilds have failed repeatedly and hit the configured hard failure limit; won't be retried anymore. + 'validation_failed' -- Prebuilds failed due to a non-retryable validation error (e.g. template misconfiguration); won't be retried. +); + +ALTER TABLE template_version_presets ADD COLUMN prebuild_status prebuild_status DEFAULT 'normal'; From 08981a4b506f3573e7773cc35f3b3c2f1823f5b5 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Fri, 16 May 2025 15:26:27 +0000 Subject: [PATCH 10/47] generate.sh --- coderd/database/dump.sql | 9 ++++- coderd/database/models.go | 74 +++++++++++++++++++++++++++++++--- coderd/database/queries.sql.go | 29 +++++++------ 3 files changed, 93 insertions(+), 19 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index a03ea910f937c..72bd3f9cfca8d 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -148,6 +148,12 @@ CREATE TYPE port_share_protocol AS ENUM ( 'https' ); +CREATE TYPE prebuild_status AS ENUM ( + 'normal', + 'hard_limited', + 'validation_failed' +); + CREATE TYPE provisioner_daemon_status AS ENUM ( 'offline', 'idle', @@ -1434,7 +1440,8 @@ CREATE TABLE template_version_presets ( name text NOT NULL, created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, desired_instances integer, - invalidate_after_secs integer DEFAULT 0 + invalidate_after_secs integer DEFAULT 0, + prebuild_status prebuild_status DEFAULT 'normal'::prebuild_status ); CREATE TABLE template_version_terraform_values ( diff --git a/coderd/database/models.go b/coderd/database/models.go index 3944d56268eaf..ec545d9cab3d5 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1285,6 +1285,67 @@ func AllPortShareProtocolValues() []PortShareProtocol { } } +type PrebuildStatus string + +const ( + PrebuildStatusNormal PrebuildStatus = "normal" + PrebuildStatusHardLimited PrebuildStatus = "hard_limited" + PrebuildStatusValidationFailed PrebuildStatus = "validation_failed" +) + +func (e *PrebuildStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = PrebuildStatus(s) + case string: + *e = PrebuildStatus(s) + default: + return fmt.Errorf("unsupported scan type for PrebuildStatus: %T", src) + } + return nil +} + +type NullPrebuildStatus struct { + PrebuildStatus PrebuildStatus `json:"prebuild_status"` + Valid bool `json:"valid"` // Valid is true if PrebuildStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullPrebuildStatus) Scan(value interface{}) error { + if value == nil { + ns.PrebuildStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.PrebuildStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullPrebuildStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.PrebuildStatus), nil +} + +func (e PrebuildStatus) Valid() bool { + switch e { + case PrebuildStatusNormal, + PrebuildStatusHardLimited, + PrebuildStatusValidationFailed: + return true + } + return false +} + +func AllPrebuildStatusValues() []PrebuildStatus { + return []PrebuildStatus{ + PrebuildStatusNormal, + PrebuildStatusHardLimited, + PrebuildStatusValidationFailed, + } +} + // The status of a provisioner daemon. type ProvisionerDaemonStatus string @@ -3187,12 +3248,13 @@ type TemplateVersionParameter struct { } type TemplateVersionPreset struct { - ID uuid.UUID `db:"id" json:"id"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - Name string `db:"name" json:"name"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` - InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"` + ID uuid.UUID `db:"id" json:"id"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + Name string `db:"name" json:"name"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` + InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"` + PrebuildStatus NullPrebuildStatus `db:"prebuild_status" json:"prebuild_status"` } type TemplateVersionPresetParameter struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index df1f605f0efdf..182c70fbd6e14 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6570,21 +6570,22 @@ func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templa } const getPresetByID = `-- name: GetPresetByID :one -SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, tv.template_id, tv.organization_id FROM +SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, tvp.prebuild_status, tv.template_id, tv.organization_id FROM template_version_presets tvp INNER JOIN template_versions tv ON tvp.template_version_id = tv.id WHERE tvp.id = $1 ` type GetPresetByIDRow struct { - ID uuid.UUID `db:"id" json:"id"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - Name string `db:"name" json:"name"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` - InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"` - TemplateID uuid.NullUUID `db:"template_id" json:"template_id"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + ID uuid.UUID `db:"id" json:"id"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + Name string `db:"name" json:"name"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` + InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"` + PrebuildStatus NullPrebuildStatus `db:"prebuild_status" json:"prebuild_status"` + TemplateID uuid.NullUUID `db:"template_id" json:"template_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` } func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (GetPresetByIDRow, error) { @@ -6597,6 +6598,7 @@ func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (Get &i.CreatedAt, &i.DesiredInstances, &i.InvalidateAfterSecs, + &i.PrebuildStatus, &i.TemplateID, &i.OrganizationID, ) @@ -6605,7 +6607,7 @@ func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (Get const getPresetByWorkspaceBuildID = `-- name: GetPresetByWorkspaceBuildID :one SELECT - template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at, template_version_presets.desired_instances, template_version_presets.invalidate_after_secs + template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at, template_version_presets.desired_instances, template_version_presets.invalidate_after_secs, template_version_presets.prebuild_status FROM template_version_presets INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id @@ -6623,6 +6625,7 @@ func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceB &i.CreatedAt, &i.DesiredInstances, &i.InvalidateAfterSecs, + &i.PrebuildStatus, ) return i, err } @@ -6704,7 +6707,7 @@ func (q *sqlQuerier) GetPresetParametersByTemplateVersionID(ctx context.Context, const getPresetsByTemplateVersionID = `-- name: GetPresetsByTemplateVersionID :many SELECT - id, template_version_id, name, created_at, desired_instances, invalidate_after_secs + id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status FROM template_version_presets WHERE @@ -6727,6 +6730,7 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template &i.CreatedAt, &i.DesiredInstances, &i.InvalidateAfterSecs, + &i.PrebuildStatus, ); err != nil { return nil, err } @@ -6757,7 +6761,7 @@ VALUES ( $4, $5, $6 -) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs +) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status ` type InsertPresetParams struct { @@ -6786,6 +6790,7 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) ( &i.CreatedAt, &i.DesiredInstances, &i.InvalidateAfterSecs, + &i.PrebuildStatus, ) return i, err } From 0e4600cc84a3d9afb465ebc3625807ee1f7f1bad Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Fri, 16 May 2025 16:47:50 +0000 Subject: [PATCH 11/47] fix: using prebuild status --- coderd/database/dbauthz/dbauthz.go | 4 + coderd/database/dbmem/dbmem.go | 9 ++ coderd/database/dbmetrics/querymetrics.go | 7 ++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 41 +++++-- coderd/database/queries/prebuilds.sql | 1 + coderd/database/queries/presets.sql | 5 + enterprise/coderd/prebuilds/reconcile.go | 101 +++++++++++------- enterprise/coderd/prebuilds/reconcile_test.go | 5 + 9 files changed, 127 insertions(+), 47 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 631e7524bc279..6bd644b162a05 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -4178,6 +4178,10 @@ func (q *querier) UpdateOrganizationDeletedByID(ctx context.Context, arg databas return deleteQ(q.log, q.auth, q.db.GetOrganizationByID, deleteF)(ctx, arg.ID) } +func (q *querier) UpdatePrebuildStatus(ctx context.Context, arg database.UpdatePrebuildStatusParams) error { + panic("not implemented") +} + func (q *querier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerDaemon); err != nil { return err diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 2c08cfe493887..df35b59f64625 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -10867,6 +10867,15 @@ func (q *FakeQuerier) UpdateOrganizationDeletedByID(_ context.Context, arg datab return sql.ErrNoRows } +func (q *FakeQuerier) UpdatePrebuildStatus(ctx context.Context, arg database.UpdatePrebuildStatusParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + panic("not implemented") +} + func (q *FakeQuerier) UpdateProvisionerDaemonLastSeenAt(_ context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { err := validateDatabaseType(arg) if err != nil { diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 4619a8ecb1c16..cabc6686842d5 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2685,6 +2685,13 @@ func (m queryMetricsStore) UpdateOrganizationDeletedByID(ctx context.Context, ar return r0 } +func (m queryMetricsStore) UpdatePrebuildStatus(ctx context.Context, arg database.UpdatePrebuildStatusParams) error { + start := time.Now() + r0 := m.s.UpdatePrebuildStatus(ctx, arg) + m.queryLatencies.WithLabelValues("UpdatePrebuildStatus").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { start := time.Now() r0 := m.s.UpdateProvisionerDaemonLastSeenAt(ctx, arg) diff --git a/coderd/database/querier.go b/coderd/database/querier.go index e3c53be115f84..a7ea91ef86169 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -572,6 +572,7 @@ type sqlcQuerier interface { UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error) UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) UpdateOrganizationDeletedByID(ctx context.Context, arg UpdateOrganizationDeletedByIDParams) error + UpdatePrebuildStatus(ctx context.Context, arg UpdatePrebuildStatusParams) error UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 182c70fbd6e14..52c0a754f8b5b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6507,6 +6507,7 @@ SELECT tvp.id, tvp.name, tvp.desired_instances AS desired_instances, + tvp.prebuild_status, t.deleted, t.deprecated != '' AS deprecated FROM templates t @@ -6518,17 +6519,18 @@ WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a pre ` type GetTemplatePresetsWithPrebuildsRow struct { - TemplateID uuid.UUID `db:"template_id" json:"template_id"` - TemplateName string `db:"template_name" json:"template_name"` - OrganizationName string `db:"organization_name" json:"organization_name"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - TemplateVersionName string `db:"template_version_name" json:"template_version_name"` - UsingActiveVersion bool `db:"using_active_version" json:"using_active_version"` - ID uuid.UUID `db:"id" json:"id"` - Name string `db:"name" json:"name"` - DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` - Deleted bool `db:"deleted" json:"deleted"` - Deprecated bool `db:"deprecated" json:"deprecated"` + TemplateID uuid.UUID `db:"template_id" json:"template_id"` + TemplateName string `db:"template_name" json:"template_name"` + OrganizationName string `db:"organization_name" json:"organization_name"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + TemplateVersionName string `db:"template_version_name" json:"template_version_name"` + UsingActiveVersion bool `db:"using_active_version" json:"using_active_version"` + ID uuid.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` + PrebuildStatus NullPrebuildStatus `db:"prebuild_status" json:"prebuild_status"` + Deleted bool `db:"deleted" json:"deleted"` + Deprecated bool `db:"deprecated" json:"deprecated"` } // GetTemplatePresetsWithPrebuilds retrieves template versions with configured presets and prebuilds. @@ -6553,6 +6555,7 @@ func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templa &i.ID, &i.Name, &i.DesiredInstances, + &i.PrebuildStatus, &i.Deleted, &i.Deprecated, ); err != nil { @@ -6839,6 +6842,22 @@ func (q *sqlQuerier) InsertPresetParameters(ctx context.Context, arg InsertPrese return items, nil } +const updatePrebuildStatus = `-- name: UpdatePrebuildStatus :exec +UPDATE template_version_presets +SET prebuild_status = $1 +WHERE id = $2 +` + +type UpdatePrebuildStatusParams struct { + Status NullPrebuildStatus `db:"status" json:"status"` + PresetID uuid.UUID `db:"preset_id" json:"preset_id"` +} + +func (q *sqlQuerier) UpdatePrebuildStatus(ctx context.Context, arg UpdatePrebuildStatusParams) error { + _, err := q.db.ExecContext(ctx, updatePrebuildStatus, arg.Status, arg.PresetID) + return err +} + const deleteOldProvisionerDaemons = `-- name: DeleteOldProvisionerDaemons :exec DELETE FROM provisioner_daemons WHERE ( (created_at < (NOW() - INTERVAL '7 days') AND last_seen_at IS NULL) OR diff --git a/coderd/database/queries/prebuilds.sql b/coderd/database/queries/prebuilds.sql index 8cd1feb544dd2..249a30413c539 100644 --- a/coderd/database/queries/prebuilds.sql +++ b/coderd/database/queries/prebuilds.sql @@ -33,6 +33,7 @@ SELECT tvp.id, tvp.name, tvp.desired_instances AS desired_instances, + tvp.prebuild_status, t.deleted, t.deprecated != '' AS deprecated FROM templates t diff --git a/coderd/database/queries/presets.sql b/coderd/database/queries/presets.sql index 6d5646a285b4a..e721db18a7f93 100644 --- a/coderd/database/queries/presets.sql +++ b/coderd/database/queries/presets.sql @@ -25,6 +25,11 @@ SELECT unnest(@values :: TEXT[]) RETURNING *; +-- name: UpdatePrebuildStatus :exec +UPDATE template_version_presets +SET prebuild_status = @status +WHERE id = @preset_id; + -- name: GetPresetsByTemplateVersionID :many SELECT * diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 7361c470851df..49e63b407d588 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -361,49 +361,32 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres slog.F("preset_name", ps.Preset.Name), ) - if ps.IsHardLimited { + // If the preset was previously hard-limited, log it and exit early. + if ps.Preset.PrebuildStatus.PrebuildStatus == database.PrebuildStatusHardLimited { logger.Warn(ctx, "skipping hard limited preset", slog.F("preset_id", ps.Preset.ID), slog.F("name", ps.Preset.Name)) + return nil + } - // TODO: rename ctx? - // nolint:gocritic // Necessary to query all the required data. - ctx := dbauthz.AsSystemRestricted(ctx) - - // TODO(yevhenii): move into separate function - // Send notification to template admins. - if c.notifEnq == nil { - c.logger.Warn(ctx, "notification enqueuer not set, cannot send resource replacement notification(s)") - return nil - } + // If the preset reached the hard failure limit for the first time during this iteration: + // - Mark it as hard-limited in the database + // - Send notifications to template admins + if ps.IsHardLimited { + logger.Warn(ctx, "skipping hard limited preset", slog.F("preset_id", ps.Preset.ID), slog.F("name", ps.Preset.Name)) - // TODO(yevhenii): remove owner from the list - templateAdmins, err := c.store.GetUsers(ctx, database.GetUsersParams{ - RbacRole: []string{codersdk.RoleTemplateAdmin, codersdk.RoleOwner}, + err := c.store.UpdatePrebuildStatus(ctx, database.UpdatePrebuildStatusParams{ + Status: database.NullPrebuildStatus{ + PrebuildStatus: database.PrebuildStatusHardLimited, + Valid: true, + }, + PresetID: ps.Preset.ID, }) if err != nil { - return xerrors.Errorf("fetch template admins: %w", err) + return err } - for _, templateAdmin := range templateAdmins { - if _, err := c.notifEnq.EnqueueWithData(ctx, templateAdmin.ID, notifications.PrebuildFailureLimitReached, - map[string]string{ - "org": ps.Preset.OrganizationName, - "template": ps.Preset.TemplateName, - "template_version": ps.Preset.TemplateVersionName, - "preset": ps.Preset.Name, - }, - map[string]any{}, - "prebuilds_reconciler", - // Associate this notification with all the related entities. - ps.Preset.TemplateID, ps.Preset.TemplateVersionID, ps.Preset.ID, - ); err != nil { - c.logger.Error(ctx, - "failed to send notification", - slog.Error(err), - slog.F("template_admin_id", templateAdmin.ID.String()), - ) - - continue - } + err = c.notifyPrebuildFailureLimitReached(ctx, ps) + if err != nil { + return err } return nil @@ -502,6 +485,52 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres } } +func (c *StoreReconciler) notifyPrebuildFailureLimitReached(ctx context.Context, ps prebuilds.PresetSnapshot) error { + // TODO: rename ctx? + // nolint:gocritic // Necessary to query all the required data. + ctx = dbauthz.AsSystemRestricted(ctx) + + // TODO(yevhenii): move into separate function + // Send notification to template admins. + if c.notifEnq == nil { + c.logger.Warn(ctx, "notification enqueuer not set, cannot send resource replacement notification(s)") + return nil + } + + // TODO(yevhenii): remove owner from the list + templateAdmins, err := c.store.GetUsers(ctx, database.GetUsersParams{ + RbacRole: []string{codersdk.RoleTemplateAdmin, codersdk.RoleOwner}, + }) + if err != nil { + return xerrors.Errorf("fetch template admins: %w", err) + } + + for _, templateAdmin := range templateAdmins { + if _, err := c.notifEnq.EnqueueWithData(ctx, templateAdmin.ID, notifications.PrebuildFailureLimitReached, + map[string]string{ + "org": ps.Preset.OrganizationName, + "template": ps.Preset.TemplateName, + "template_version": ps.Preset.TemplateVersionName, + "preset": ps.Preset.Name, + }, + map[string]any{}, + "prebuilds_reconciler", + // Associate this notification with all the related entities. + ps.Preset.TemplateID, ps.Preset.TemplateVersionID, ps.Preset.ID, + ); err != nil { + c.logger.Error(ctx, + "failed to send notification", + slog.Error(err), + slog.F("template_admin_id", templateAdmin.ID.String()), + ) + + continue + } + } + + return nil +} + func (c *StoreReconciler) CalculateActions(ctx context.Context, snapshot prebuilds.PresetSnapshot) (*prebuilds.ReconciliationActions, error) { if ctx.Err() != nil { return nil, ctx.Err() diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index 9391410df781d..e75c19801a303 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -733,6 +733,11 @@ func TestSkippingHardLimitedPresets(t *testing.T) { // The outcome depends on whether the hard limit has been reached require.NoError(t, controller.ReconcileAll(ctx)) + // These two additional calls to ReconcileAll should not trigger any notifications. + // A notification is only sent once. + require.NoError(t, controller.ReconcileAll(ctx)) + require.NoError(t, controller.ReconcileAll(ctx)) + // Verify the final state after reconciliation workspaces, err = db.GetWorkspacesByTemplateID(ctx, template.ID) require.NoError(t, err) From 107879bd61439097bb57c9657fd1bde1f5cd7efd Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Fri, 16 May 2025 17:41:14 +0000 Subject: [PATCH 12/47] fix: migrations numbers --- ...wn.sql => 000328_prebuild_failure_limit_notification.down.sql} | 0 ...n.up.sql => 000328_prebuild_failure_limit_notification.up.sql} | 0 ...ts.down.sql => 000329_add_status_to_template_presets.down.sql} | 0 ...resets.up.sql => 000329_add_status_to_template_presets.up.sql} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000325_prebuild_failure_limit_notification.down.sql => 000328_prebuild_failure_limit_notification.down.sql} (100%) rename coderd/database/migrations/{000325_prebuild_failure_limit_notification.up.sql => 000328_prebuild_failure_limit_notification.up.sql} (100%) rename coderd/database/migrations/{000326_add_status_to_template_presets.down.sql => 000329_add_status_to_template_presets.down.sql} (100%) rename coderd/database/migrations/{000326_add_status_to_template_presets.up.sql => 000329_add_status_to_template_presets.up.sql} (100%) diff --git a/coderd/database/migrations/000325_prebuild_failure_limit_notification.down.sql b/coderd/database/migrations/000328_prebuild_failure_limit_notification.down.sql similarity index 100% rename from coderd/database/migrations/000325_prebuild_failure_limit_notification.down.sql rename to coderd/database/migrations/000328_prebuild_failure_limit_notification.down.sql diff --git a/coderd/database/migrations/000325_prebuild_failure_limit_notification.up.sql b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql similarity index 100% rename from coderd/database/migrations/000325_prebuild_failure_limit_notification.up.sql rename to coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql diff --git a/coderd/database/migrations/000326_add_status_to_template_presets.down.sql b/coderd/database/migrations/000329_add_status_to_template_presets.down.sql similarity index 100% rename from coderd/database/migrations/000326_add_status_to_template_presets.down.sql rename to coderd/database/migrations/000329_add_status_to_template_presets.down.sql diff --git a/coderd/database/migrations/000326_add_status_to_template_presets.up.sql b/coderd/database/migrations/000329_add_status_to_template_presets.up.sql similarity index 100% rename from coderd/database/migrations/000326_add_status_to_template_presets.up.sql rename to coderd/database/migrations/000329_add_status_to_template_presets.up.sql From 2dea5ab6423402dd5ddb1c265620caa9165c3ad2 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Fri, 16 May 2025 17:46:41 +0000 Subject: [PATCH 13/47] make gen --- coderd/database/dbmock/dbmock.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 97df732ecbf27..3b2c84c531146 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -5691,6 +5691,20 @@ func (mr *MockStoreMockRecorder) UpdateOrganizationDeletedByID(ctx, arg any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOrganizationDeletedByID", reflect.TypeOf((*MockStore)(nil).UpdateOrganizationDeletedByID), ctx, arg) } +// UpdatePrebuildStatus mocks base method. +func (m *MockStore) UpdatePrebuildStatus(ctx context.Context, arg database.UpdatePrebuildStatusParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatePrebuildStatus", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdatePrebuildStatus indicates an expected call of UpdatePrebuildStatus. +func (mr *MockStoreMockRecorder) UpdatePrebuildStatus(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePrebuildStatus", reflect.TypeOf((*MockStore)(nil).UpdatePrebuildStatus), ctx, arg) +} + // UpdateProvisionerDaemonLastSeenAt mocks base method. func (m *MockStore) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { m.ctrl.T.Helper() From e252093000975323987461c542d4259343152a4d Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Fri, 16 May 2025 18:03:15 +0000 Subject: [PATCH 14/47] make gen/golden-files --- coderd/notifications/notifications_test.go | 16 +++++ .../PrebuildFailureLimitReached.html.golden | 72 +++++++++++++++++++ .../PrebuildFailureLimitReached.json.golden | 26 +++++++ 3 files changed, 114 insertions(+) create mode 100644 coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden create mode 100644 coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 8f8a3c82441e0..fab87af41deb9 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -1250,6 +1250,22 @@ func TestNotificationTemplates_Golden(t *testing.T) { }, }, }, + { + name: "PrebuildFailureLimitReached", + id: notifications.PrebuildFailureLimitReached, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{ + "org": "cern", + "template": "docker", + "template_version": "angry_torvalds", + "preset": "particle-accelerator", + }, + Data: map[string]any{}, + }, + }, } // We must have a test case for every notification_template. This is enforced below: diff --git a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden new file mode 100644 index 0000000000000..5d53d7a4e676d --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden @@ -0,0 +1,72 @@ +From: system@coder.com +To: bobby@coder.com +Subject: There is a problem creating prebuilt workspaces for the preset +Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 +Date: Fri, 11 Oct 2024 09:03:06 +0000 +Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +The number of failed prebuilds has reached the hard limit for template **do= +cker** and preset **particle-accelerator** + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + There is a problem creating prebuilt workspaces for the preset</= +title> + </head> + <body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-= +ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel= +l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617= +; background: #f8fafc;"> + <div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde= +r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig= +n: left; font-size: 14px; line-height: 1.5;"> + <div style=3D"text-align: center;"> + <img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod= +er Logo" style=3D"height: 40px;" /> + </div> + <h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m= +argin: 8px 0 32px; line-height: 1.5;"> + There is a problem creating prebuilt workspaces for the preset + </h1> + <div style=3D"line-height: 1.5;"> + <p>Hi Bobby,</p> + <pre><code> The number of failed prebuilds has reached the hard li= +mit for template **docker** and preset **particle-accelerator** +</code></pre> + </div> + <div style=3D"text-align: center; margin-top: 32px;"> + =20 + </div> + <div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz= +e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;"> + <p>© 2024 Coder. All rights reserved - <a = +href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h= +ttp://test.com</a></p> + <p><a href=3D"http://test.com/settings/notifications" style=3D"colo= +r: #2563eb; text-decoration: none;">Click here to manage your notification = +settings</a></p> + <p><a href=3D"http://test.com/settings/notifications?disabled=3D414= +d9331-c1fc-4761-b40c-d1f4702279eb" style=3D"color: #2563eb; text-decoration= +: none;">Stop receiving emails like this</a></p> + </div> + </div> + </body> +</html> + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden new file mode 100644 index 0000000000000..e29c082600e70 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden @@ -0,0 +1,26 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.2", + "notification_name": "Prebuild Failure Limit Reached", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [], + "labels": { + "org": "cern", + "preset": "particle-accelerator", + "template": "docker", + "template_version": "angry_torvalds" + }, + "data": {}, + "targets": null + }, + "title": "There is a problem creating prebuilt workspaces for the preset", + "title_markdown": "There is a problem creating prebuilt workspaces for the preset", + "body": "The number of failed prebuilds has reached the hard limit for template **docker** and preset **particle-accelerator**", + "body_markdown": "\n\t\t\tThe number of failed prebuilds has reached the hard limit for template **docker** and preset **particle-accelerator**\n\t\t" +} \ No newline at end of file From 906ceb9f4df8b4f0ec1692b3172e0bd22b63fbf1 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Fri, 16 May 2025 20:27:46 +0000 Subject: [PATCH 15/47] fix: fix dbauthz --- coderd/database/dbauthz/dbauthz.go | 16 +++++++++++++++- coderd/database/dbauthz/dbauthz_test.go | 25 +++++++++++++++++++++++++ coderd/database/dbmem/dbmem.go | 2 +- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 6bd644b162a05..e7a45aa67809f 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -4179,7 +4179,21 @@ func (q *querier) UpdateOrganizationDeletedByID(ctx context.Context, arg databas } func (q *querier) UpdatePrebuildStatus(ctx context.Context, arg database.UpdatePrebuildStatusParams) error { - panic("not implemented") + preset, err := q.db.GetPresetByID(ctx, arg.PresetID) + if err != nil { + return err + } + + object := rbac.ResourceTemplate. + WithID(preset.TemplateID.UUID). + InOrg(preset.OrganizationID) + + err = q.authorizeContext(ctx, policy.ActionUpdate, object) + if err != nil { + return err + } + + return q.db.UpdatePrebuildStatus(ctx, arg) } func (q *querier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 807a5266f5d89..131354d72b713 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4943,6 +4943,31 @@ func (s *MethodTestSuite) TestPrebuilds() { OrganizationID: org.ID, }) })) + s.Run("UpdatePrebuildStatus", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + user := dbgen.User(s.T(), db, database.User{}) + template := dbgen.Template(s.T(), db, database.Template{ + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{ + UUID: template.ID, + Valid: true, + }, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + preset := dbgen.Preset(s.T(), db, database.InsertPresetParams{ + TemplateVersionID: templateVersion.ID, + }) + req := database.UpdatePrebuildStatusParams{ + PresetID: preset.ID, + } + check.Args(req). + Asserts(rbac.ResourceTemplate.WithID(template.ID).InOrg(org.ID), policy.ActionUpdate). + ErrorsWithInMemDB(dbmem.ErrUnimplemented) + })) } func (s *MethodTestSuite) TestOAuth2ProviderApps() { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index df35b59f64625..5dbded6354711 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -10873,7 +10873,7 @@ func (q *FakeQuerier) UpdatePrebuildStatus(ctx context.Context, arg database.Upd return err } - panic("not implemented") + return ErrUnimplemented } func (q *FakeQuerier) UpdateProvisionerDaemonLastSeenAt(_ context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { From 13fb1af4ef8c70c4403da773e6c8b2119641afe6 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Fri, 16 May 2025 20:58:26 +0000 Subject: [PATCH 16/47] fix: minor fix --- .../000328_prebuild_failure_limit_notification.up.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql index e701e05a2741c..79c1334e81a03 100644 --- a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql +++ b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql @@ -4,7 +4,7 @@ VALUES ('414d9331-c1fc-4761-b40c-d1f4702279eb', 'Prebuild Failure Limit Reached', E'There is a problem creating prebuilt workspaces for the preset', $$ - The number of failed prebuilds has reached the hard limit for template **{{ .Labels.template }}** and preset **{{ .Labels.preset }}** - $$, +The number of failed prebuilds has reached the hard limit for template **{{ .Labels.template }}** and preset **{{ .Labels.preset }}**. +$$, 'Template Events', '[]'::jsonb); From 108ea0a764e08b945c69c055de977a67f6760773 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Mon, 19 May 2025 12:25:40 +0000 Subject: [PATCH 17/47] test: fix tests --- coderd/database/dbauthz/dbauthz_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 0a5e102c12cb4..5e2d49766880e 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4942,6 +4942,10 @@ func (s *MethodTestSuite) TestPrebuilds() { }, InvalidateAfterSecs: preset.InvalidateAfterSecs, OrganizationID: org.ID, + PrebuildStatus: database.NullPrebuildStatus{ + PrebuildStatus: database.PrebuildStatusNormal, + Valid: true, + }, }) })) s.Run("UpdatePrebuildStatus", s.Subtest(func(db database.Store, check *expects) { From 7cd27994b16d113769c55c86bd0155ea7bc4f51d Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Mon, 19 May 2025 13:18:00 +0000 Subject: [PATCH 18/47] test: fix dbmem --- coderd/database/dbauthz/dbauthz_test.go | 3 +-- coderd/database/dbmem/dbmem.go | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 5e2d49766880e..2fc014e8a320c 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4970,8 +4970,7 @@ func (s *MethodTestSuite) TestPrebuilds() { PresetID: preset.ID, } check.Args(req). - Asserts(rbac.ResourceTemplate.WithID(template.ID).InOrg(org.ID), policy.ActionUpdate). - ErrorsWithInMemDB(dbmem.ErrUnimplemented) + Asserts(rbac.ResourceTemplate.WithID(template.ID).InOrg(org.ID), policy.ActionUpdate) })) } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index dbfb1ce93bff9..e74dc4a86d904 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -4303,6 +4303,7 @@ func (q *FakeQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (da CreatedAt: preset.CreatedAt, DesiredInstances: preset.DesiredInstances, InvalidateAfterSecs: preset.InvalidateAfterSecs, + PrebuildStatus: preset.PrebuildStatus, TemplateID: tv.TemplateID, OrganizationID: tv.OrganizationID, }, nil @@ -9041,6 +9042,10 @@ func (q *FakeQuerier) InsertPreset(_ context.Context, arg database.InsertPresetP Int32: 0, Valid: true, }, + PrebuildStatus: database.NullPrebuildStatus{ + PrebuildStatus: database.PrebuildStatusNormal, + Valid: true, + }, } q.presets = append(q.presets, preset) return preset, nil @@ -10875,7 +10880,17 @@ func (q *FakeQuerier) UpdatePrebuildStatus(ctx context.Context, arg database.Upd return err } - return ErrUnimplemented + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, preset := range q.presets { + if preset.ID == arg.PresetID { + preset.PrebuildStatus = arg.Status + return nil + } + } + + return xerrors.Errorf("preset %v does not exist", arg.PresetID) } func (q *FakeQuerier) UpdateProvisionerDaemonLastSeenAt(_ context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { From f9148e273fcfa8c031c2dd28b82633b6637b913f Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Mon, 19 May 2025 14:58:06 +0000 Subject: [PATCH 19/47] make gen/golden-files --- .../smtp/PrebuildFailureLimitReached.html.golden | 10 +++++----- .../webhook/PrebuildFailureLimitReached.json.golden | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden index 5d53d7a4e676d..57c7bbef6d3d4 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden @@ -12,8 +12,8 @@ Content-Type: text/plain; charset=UTF-8 Hi Bobby, -The number of failed prebuilds has reached the hard limit for template **do= -cker** and preset **particle-accelerator** +The number of failed prebuilds has reached the hard limit for template dock= +er and preset particle-accelerator. --bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 @@ -46,9 +46,9 @@ argin: 8px 0 32px; line-height: 1.5;"> </h1> <div style=3D"line-height: 1.5;"> <p>Hi Bobby,</p> - <pre><code> The number of failed prebuilds has reached the hard li= -mit for template **docker** and preset **particle-accelerator** -</code></pre> + <p>The number of failed prebuilds has reached the hard limit for te= +mplate <strong>docker</strong> and preset <strong>particle-accelerator</str= +ong>.</p> </div> <div style=3D"text-align: center; margin-top: 32px;"> =20 diff --git a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden index e29c082600e70..fc50ba056c8da 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden @@ -21,6 +21,6 @@ }, "title": "There is a problem creating prebuilt workspaces for the preset", "title_markdown": "There is a problem creating prebuilt workspaces for the preset", - "body": "The number of failed prebuilds has reached the hard limit for template **docker** and preset **particle-accelerator**", - "body_markdown": "\n\t\t\tThe number of failed prebuilds has reached the hard limit for template **docker** and preset **particle-accelerator**\n\t\t" + "body": "The number of failed prebuilds has reached the hard limit for template docker and preset particle-accelerator.", + "body_markdown": "\nThe number of failed prebuilds has reached the hard limit for template **docker** and preset **particle-accelerator**.\n" } \ No newline at end of file From 315acfa93d8ec5e0b59f9e9e3e487f6907d51558 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Mon, 19 May 2025 15:00:20 +0000 Subject: [PATCH 20/47] Remove outdated TODOs --- enterprise/coderd/prebuilds/reconcile.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 49e63b407d588..94c6f0f559095 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -486,11 +486,9 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres } func (c *StoreReconciler) notifyPrebuildFailureLimitReached(ctx context.Context, ps prebuilds.PresetSnapshot) error { - // TODO: rename ctx? // nolint:gocritic // Necessary to query all the required data. ctx = dbauthz.AsSystemRestricted(ctx) - // TODO(yevhenii): move into separate function // Send notification to template admins. if c.notifEnq == nil { c.logger.Warn(ctx, "notification enqueuer not set, cannot send resource replacement notification(s)") From 1679ac90ebd0f21b22653abc13c4a7c50db21759 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Mon, 19 May 2025 15:57:54 +0000 Subject: [PATCH 21/47] fix: enhance notification template --- .../000328_prebuild_failure_limit_notification.up.sql | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql index 79c1334e81a03..175e79c8d88a7 100644 --- a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql +++ b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql @@ -7,4 +7,13 @@ VALUES ('414d9331-c1fc-4761-b40c-d1f4702279eb', The number of failed prebuilds has reached the hard limit for template **{{ .Labels.template }}** and preset **{{ .Labels.preset }}**. $$, 'Template Events', - '[]'::jsonb); + '[ + { + "label": "View failed workspaces", + "url": "{{base_url}}/workspaces?filter=owner:prebuilds+status:failed+template:{{.Labels.template}}&page=1" + }, + { + "label": "View template version", + "url": "{{base_url}}/templates/{{.Labels.org}}/{{.Labels.template}}/versions/{{.Labels.template_version}}" + } + ]'::jsonb); From c47ddaac2d0de2e91fc25c03f5d350bc88b7ee1c Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Mon, 19 May 2025 16:12:12 +0000 Subject: [PATCH 22/47] make gen/golden-files --- .../PrebuildFailureLimitReached.html.golden | 20 +++++++++++++++++++ .../PrebuildFailureLimitReached.json.golden | 11 +++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden index 57c7bbef6d3d4..bf971688efac8 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden @@ -16,6 +16,12 @@ The number of failed prebuilds has reached the hard limit for template dock= er and preset particle-accelerator. +View failed workspaces: http://test.com/workspaces?filter=3Downer:prebuilds= ++status:failed+template:docker&page=3D1 + +View template version: http://test.com/templates/cern/docker/versions/angry= +_torvalds + --bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 Content-Transfer-Encoding: quoted-printable Content-Type: text/html; charset=UTF-8 @@ -52,6 +58,20 @@ ong>.</p> </div> <div style=3D"text-align: center; margin-top: 32px;"> =20 + <a href=3D"http://test.com/workspaces?filter=3Downer:prebuilds+stat= +us:failed+template:docker&page=3D1" style=3D"display: inline-block; padding= +: 13px 24px; background-color: #020617; color: #f8fafc; text-decoration: no= +ne; border-radius: 8px; margin: 0 4px;"> + View failed workspaces + </a> + =20 + <a href=3D"http://test.com/templates/cern/docker/versions/angry_tor= +valds" style=3D"display: inline-block; padding: 13px 24px; background-color= +: #020617; color: #f8fafc; text-decoration: none; border-radius: 8px; margi= +n: 0 4px;"> + View template version + </a> + =20 </div> <div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz= e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;"> diff --git a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden index fc50ba056c8da..80140d16abae0 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden @@ -9,7 +9,16 @@ "user_email": "bobby@coder.com", "user_name": "Bobby", "user_username": "bobby", - "actions": [], + "actions": [ + { + "label": "View failed workspaces", + "url": "http://test.com/workspaces?filter=owner:prebuilds+status:failed+template:docker\u0026page=1" + }, + { + "label": "View template version", + "url": "http://test.com/templates/cern/docker/versions/angry_torvalds" + } + ], "labels": { "org": "cern", "preset": "particle-accelerator", From c19f28a7bf353d49bd7e1485e2f651a588b9c0e3 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Mon, 19 May 2025 16:25:16 +0000 Subject: [PATCH 23/47] refactor: minor improvements --- coderd/database/queries.sql.go | 3 +++ coderd/database/queries/prebuilds.sql | 1 + enterprise/coderd/prebuilds/reconcile.go | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3c211609d8d6b..d9f064ed0a6d5 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6503,6 +6503,7 @@ const getTemplatePresetsWithPrebuilds = `-- name: GetTemplatePresetsWithPrebuild SELECT t.id AS template_id, t.name AS template_name, + o.id AS organization_id, o.name AS organization_name, tv.id AS template_version_id, tv.name AS template_version_name, @@ -6525,6 +6526,7 @@ WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a pre type GetTemplatePresetsWithPrebuildsRow struct { TemplateID uuid.UUID `db:"template_id" json:"template_id"` TemplateName string `db:"template_name" json:"template_name"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` OrganizationName string `db:"organization_name" json:"organization_name"` TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` TemplateVersionName string `db:"template_version_name" json:"template_version_name"` @@ -6552,6 +6554,7 @@ func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templa if err := rows.Scan( &i.TemplateID, &i.TemplateName, + &i.OrganizationID, &i.OrganizationName, &i.TemplateVersionID, &i.TemplateVersionName, diff --git a/coderd/database/queries/prebuilds.sql b/coderd/database/queries/prebuilds.sql index af9595c2d43ee..9cd4321afec23 100644 --- a/coderd/database/queries/prebuilds.sql +++ b/coderd/database/queries/prebuilds.sql @@ -27,6 +27,7 @@ RETURNING w.id, w.name; SELECT t.id AS template_id, t.name AS template_name, + o.id AS organization_id, o.name AS organization_name, tv.id AS template_version_id, tv.name AS template_version_name, diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 94c6f0f559095..8f806a65f58bc 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -514,7 +514,7 @@ func (c *StoreReconciler) notifyPrebuildFailureLimitReached(ctx context.Context, map[string]any{}, "prebuilds_reconciler", // Associate this notification with all the related entities. - ps.Preset.TemplateID, ps.Preset.TemplateVersionID, ps.Preset.ID, + ps.Preset.TemplateID, ps.Preset.TemplateVersionID, ps.Preset.ID, ps.Preset.OrganizationID, ); err != nil { c.logger.Error(ctx, "failed to send notification", From b18730fb1b8fb65c94c75c400dc2b6b443978872 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Mon, 19 May 2025 16:49:32 +0000 Subject: [PATCH 24/47] fix: minor fix in template --- .../000328_prebuild_failure_limit_notification.up.sql | 2 +- .../smtp/PrebuildFailureLimitReached.html.golden | 8 ++++---- .../webhook/PrebuildFailureLimitReached.json.golden | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql index 175e79c8d88a7..19d608d6a3628 100644 --- a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql +++ b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql @@ -10,7 +10,7 @@ $$, '[ { "label": "View failed workspaces", - "url": "{{base_url}}/workspaces?filter=owner:prebuilds+status:failed+template:{{.Labels.template}}&page=1" + "url": "{{base_url}}/workspaces?filter=owner:prebuilds+status:failed+template:{{.Labels.template}}" }, { "label": "View template version", diff --git a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden index bf971688efac8..1d5acd9fca83d 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden @@ -17,7 +17,7 @@ er and preset particle-accelerator. View failed workspaces: http://test.com/workspaces?filter=3Downer:prebuilds= -+status:failed+template:docker&page=3D1 ++status:failed+template:docker View template version: http://test.com/templates/cern/docker/versions/angry= _torvalds @@ -59,9 +59,9 @@ ong>.</p> <div style=3D"text-align: center; margin-top: 32px;"> =20 <a href=3D"http://test.com/workspaces?filter=3Downer:prebuilds+stat= -us:failed+template:docker&page=3D1" style=3D"display: inline-block; padding= -: 13px 24px; background-color: #020617; color: #f8fafc; text-decoration: no= -ne; border-radius: 8px; margin: 0 4px;"> +us:failed+template:docker" style=3D"display: inline-block; padding: 13px 24= +px; background-color: #020617; color: #f8fafc; text-decoration: none; borde= +r-radius: 8px; margin: 0 4px;"> View failed workspaces </a> =20 diff --git a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden index 80140d16abae0..e6ee9f22619f9 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden @@ -12,7 +12,7 @@ "actions": [ { "label": "View failed workspaces", - "url": "http://test.com/workspaces?filter=owner:prebuilds+status:failed+template:docker\u0026page=1" + "url": "http://test.com/workspaces?filter=owner:prebuilds+status:failed+template:docker" }, { "label": "View template version", From 37173b0a86927f640ece1f5fd133c01aef3d1709 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Mon, 19 May 2025 19:05:16 +0000 Subject: [PATCH 25/47] fix: minor improvement to template --- .../000328_prebuild_failure_limit_notification.up.sql | 2 ++ .../smtp/PrebuildFailureLimitReached.html.golden | 6 ++++++ .../webhook/PrebuildFailureLimitReached.json.golden | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql index 19d608d6a3628..a8cb254ed9f31 100644 --- a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql +++ b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql @@ -5,6 +5,8 @@ VALUES ('414d9331-c1fc-4761-b40c-d1f4702279eb', E'There is a problem creating prebuilt workspaces for the preset', $$ The number of failed prebuilds has reached the hard limit for template **{{ .Labels.template }}** and preset **{{ .Labels.preset }}**. + +To resume prebuilds, fix the underlying issue and upload a new template version. $$, 'Template Events', '[ diff --git a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden index 1d5acd9fca83d..99bef49a6782a 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden @@ -15,6 +15,9 @@ Hi Bobby, The number of failed prebuilds has reached the hard limit for template dock= er and preset particle-accelerator. +To resume prebuilds, fix the underlying issue and upload a new template ver= +sion. + View failed workspaces: http://test.com/workspaces?filter=3Downer:prebuilds= +status:failed+template:docker @@ -55,6 +58,9 @@ argin: 8px 0 32px; line-height: 1.5;"> <p>The number of failed prebuilds has reached the hard limit for te= mplate <strong>docker</strong> and preset <strong>particle-accelerator</str= ong>.</p> + +<p>To resume prebuilds, fix the underlying issue and upload a new template = +version.</p> </div> <div style=3D"text-align: center; margin-top: 32px;"> =20 diff --git a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden index e6ee9f22619f9..362c2703f17b5 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden @@ -30,6 +30,6 @@ }, "title": "There is a problem creating prebuilt workspaces for the preset", "title_markdown": "There is a problem creating prebuilt workspaces for the preset", - "body": "The number of failed prebuilds has reached the hard limit for template docker and preset particle-accelerator.", - "body_markdown": "\nThe number of failed prebuilds has reached the hard limit for template **docker** and preset **particle-accelerator**.\n" + "body": "The number of failed prebuilds has reached the hard limit for template docker and preset particle-accelerator.\n\nTo resume prebuilds, fix the underlying issue and upload a new template version.", + "body_markdown": "\nThe number of failed prebuilds has reached the hard limit for template **docker** and preset **particle-accelerator**.\n\nTo resume prebuilds, fix the underlying issue and upload a new template version.\n" } \ No newline at end of file From 0e3cc40d08ae549c1570e74e6c32b4a3a96c661e Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Mon, 19 May 2025 19:29:06 +0000 Subject: [PATCH 26/47] fix: minor fixes --- enterprise/coderd/prebuilds/reconcile.go | 2 +- enterprise/coderd/prebuilds/reconcile_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 8f806a65f58bc..27a496299908d 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -491,7 +491,7 @@ func (c *StoreReconciler) notifyPrebuildFailureLimitReached(ctx context.Context, // Send notification to template admins. if c.notifEnq == nil { - c.logger.Warn(ctx, "notification enqueuer not set, cannot send resource replacement notification(s)") + c.logger.Warn(ctx, "notification enqueuer not set, cannot send prebuild is hard limited notification(s)") return nil } diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index c2991753bc238..4dc45bd798e8c 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -758,7 +758,7 @@ func TestSkippingHardLimitedPresets(t *testing.T) { // When hard limit is reached, a notification should be sent. matching := fakeEnqueuer.Sent(func(notification *notificationstest.FakeNotification) bool { - if !assert.Equal(t, notification.TemplateID, notifications.PrebuildFailureLimitReached, "unexpected template") { + if !assert.Equal(t, notifications.PrebuildFailureLimitReached, notification.TemplateID, "unexpected template") { return false } From 784adbad4f1119287011799a255c382911a39405 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Mon, 19 May 2025 21:17:33 +0000 Subject: [PATCH 27/47] fix: fix TODO --- enterprise/coderd/prebuilds/reconcile.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 27a496299908d..a27ebd485152b 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -495,9 +495,8 @@ func (c *StoreReconciler) notifyPrebuildFailureLimitReached(ctx context.Context, return nil } - // TODO(yevhenii): remove owner from the list templateAdmins, err := c.store.GetUsers(ctx, database.GetUsersParams{ - RbacRole: []string{codersdk.RoleTemplateAdmin, codersdk.RoleOwner}, + RbacRole: []string{codersdk.RoleTemplateAdmin}, }) if err != nil { return xerrors.Errorf("fetch template admins: %w", err) From 50bd9b46a3b33a4ff1dc6674eef025b2cbac0528 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com> Date: Tue, 20 May 2025 13:50:16 +0000 Subject: [PATCH 28/47] refactor: CR's fixes --- ...8_prebuild_failure_limit_notification.up.sql | 4 ++-- .../PrebuildFailureLimitReached.html.golden | 17 ++++++++--------- .../PrebuildFailureLimitReached.json.golden | 8 ++++---- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql index a8cb254ed9f31..a875e0233341d 100644 --- a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql +++ b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql @@ -2,9 +2,9 @@ INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) VALUES ('414d9331-c1fc-4761-b40c-d1f4702279eb', 'Prebuild Failure Limit Reached', - E'There is a problem creating prebuilt workspaces for the preset', + E'There is a problem creating prebuilt workspaces', $$ -The number of failed prebuilds has reached the hard limit for template **{{ .Labels.template }}** and preset **{{ .Labels.preset }}**. +The number of failed prebuild attempts has reached the hard limit for template **{{ .Labels.template }}** and preset **{{ .Labels.preset }}**. To resume prebuilds, fix the underlying issue and upload a new template version. $$, diff --git a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden index 99bef49a6782a..e648419c09fcb 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden @@ -1,6 +1,6 @@ From: system@coder.com To: bobby@coder.com -Subject: There is a problem creating prebuilt workspaces for the preset +Subject: There is a problem creating prebuilt workspaces Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 Date: Fri, 11 Oct 2024 09:03:06 +0000 Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 @@ -12,8 +12,8 @@ Content-Type: text/plain; charset=UTF-8 Hi Bobby, -The number of failed prebuilds has reached the hard limit for template dock= -er and preset particle-accelerator. +The number of failed prebuild attempts has reached the hard limit for templ= +ate docker and preset particle-accelerator. To resume prebuilds, fix the underlying issue and upload a new template ver= sion. @@ -35,8 +35,7 @@ Content-Type: text/html; charset=UTF-8 <meta charset=3D"UTF-8" /> <meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale= =3D1.0" /> - <title>There is a problem creating prebuilt workspaces for the preset</= -title> + <title>There is a problem creating prebuilt workspaces

- There is a problem creating prebuilt workspaces for the preset + There is a problem creating prebuilt workspaces

Hi Bobby,

-

The number of failed prebuilds has reached the hard limit for te= -mplate docker and preset particle-accelerator.

+

The number of failed prebuild attempts has reached the hard limi= +t for template docker and preset particle-accelera= +tor.

To resume prebuilds, fix the underlying issue and upload a new template = version.

diff --git a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden index 362c2703f17b5..914a37c1eee2e 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden @@ -28,8 +28,8 @@ "data": {}, "targets": null }, - "title": "There is a problem creating prebuilt workspaces for the preset", - "title_markdown": "There is a problem creating prebuilt workspaces for the preset", - "body": "The number of failed prebuilds has reached the hard limit for template docker and preset particle-accelerator.\n\nTo resume prebuilds, fix the underlying issue and upload a new template version.", - "body_markdown": "\nThe number of failed prebuilds has reached the hard limit for template **docker** and preset **particle-accelerator**.\n\nTo resume prebuilds, fix the underlying issue and upload a new template version.\n" + "title": "There is a problem creating prebuilt workspaces", + "title_markdown": "There is a problem creating prebuilt workspaces", + "body": "The number of failed prebuild attempts has reached the hard limit for template docker and preset particle-accelerator.\n\nTo resume prebuilds, fix the underlying issue and upload a new template version.", + "body_markdown": "\nThe number of failed prebuild attempts has reached the hard limit for template **docker** and preset **particle-accelerator**.\n\nTo resume prebuilds, fix the underlying issue and upload a new template version.\n" } \ No newline at end of file From 8a33ac8505ebbb6da89ed1d18dc094ff2562278b Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 14:07:05 +0000 Subject: [PATCH 29/47] refactor: CR's fixes --- .../000328_prebuild_failure_limit_notification.up.sql | 2 +- .../smtp/PrebuildFailureLimitReached.html.golden | 6 +++--- .../webhook/PrebuildFailureLimitReached.json.golden | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql index a875e0233341d..f2da2f8528478 100644 --- a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql +++ b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql @@ -11,7 +11,7 @@ $$, 'Template Events', '[ { - "label": "View failed workspaces", + "label": "View failed prebuilt workspaces", "url": "{{base_url}}/workspaces?filter=owner:prebuilds+status:failed+template:{{.Labels.template}}" }, { diff --git a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden index e648419c09fcb..cad83b5207e8c 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden @@ -19,8 +19,8 @@ To resume prebuilds, fix the underlying issue and upload a new template ver= sion. -View failed workspaces: http://test.com/workspaces?filter=3Downer:prebuilds= -+status:failed+template:docker +View failed prebuilt workspaces: http://test.com/workspaces?filter=3Downer:= +prebuilds+status:failed+template:docker View template version: http://test.com/templates/cern/docker/versions/angry= _torvalds @@ -67,7 +67,7 @@ version.

us:failed+template:docker" style=3D"display: inline-block; padding: 13px 24= px; background-color: #020617; color: #f8fafc; text-decoration: none; borde= r-radius: 8px; margin: 0 4px;"> - View failed workspaces + View failed prebuilt workspaces =20 Date: Tue, 20 May 2025 15:30:10 +0000 Subject: [PATCH 30/47] refactor: use not null for prebuild_status --- coderd/database/dbmem/dbmem.go | 5 +- coderd/database/dump.sql | 2 +- ...0329_add_status_to_template_presets.up.sql | 2 +- coderd/database/models.go | 14 +++--- coderd/database/queries.sql.go | 48 +++++++++---------- enterprise/coderd/prebuilds/reconcile.go | 7 +-- 6 files changed, 36 insertions(+), 42 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 6ffe5bfcc98dc..b909809b8e758 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9049,10 +9049,7 @@ func (q *FakeQuerier) InsertPreset(_ context.Context, arg database.InsertPresetP Int32: 0, Valid: true, }, - PrebuildStatus: database.NullPrebuildStatus{ - PrebuildStatus: database.PrebuildStatusNormal, - Valid: true, - }, + PrebuildStatus: database.PrebuildStatusNormal, } q.presets = append(q.presets, preset) return preset, nil diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index aa6675fcd985b..c44e572211ed7 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1446,7 +1446,7 @@ CREATE TABLE template_version_presets ( created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, desired_instances integer, invalidate_after_secs integer DEFAULT 0, - prebuild_status prebuild_status DEFAULT 'normal'::prebuild_status + prebuild_status prebuild_status DEFAULT 'normal'::prebuild_status NOT NULL ); CREATE TABLE template_version_terraform_values ( diff --git a/coderd/database/migrations/000329_add_status_to_template_presets.up.sql b/coderd/database/migrations/000329_add_status_to_template_presets.up.sql index 617d645b95785..7a1fa1c3af0f5 100644 --- a/coderd/database/migrations/000329_add_status_to_template_presets.up.sql +++ b/coderd/database/migrations/000329_add_status_to_template_presets.up.sql @@ -4,4 +4,4 @@ CREATE TYPE prebuild_status AS ENUM ( 'validation_failed' -- Prebuilds failed due to a non-retryable validation error (e.g. template misconfiguration); won't be retried. ); -ALTER TABLE template_version_presets ADD COLUMN prebuild_status prebuild_status DEFAULT 'normal'; +ALTER TABLE template_version_presets ADD COLUMN prebuild_status prebuild_status NOT NULL DEFAULT 'normal'::prebuild_status; diff --git a/coderd/database/models.go b/coderd/database/models.go index 3cecd1d64cd17..163263b0bef6f 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3309,13 +3309,13 @@ type TemplateVersionParameter struct { } type TemplateVersionPreset struct { - ID uuid.UUID `db:"id" json:"id"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - Name string `db:"name" json:"name"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` - InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"` - PrebuildStatus NullPrebuildStatus `db:"prebuild_status" json:"prebuild_status"` + ID uuid.UUID `db:"id" json:"id"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + Name string `db:"name" json:"name"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` + InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"` + PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"` } type TemplateVersionPresetParameter struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index d9f064ed0a6d5..51c9f07318b45 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6524,19 +6524,19 @@ WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a pre ` type GetTemplatePresetsWithPrebuildsRow struct { - TemplateID uuid.UUID `db:"template_id" json:"template_id"` - TemplateName string `db:"template_name" json:"template_name"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - OrganizationName string `db:"organization_name" json:"organization_name"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - TemplateVersionName string `db:"template_version_name" json:"template_version_name"` - UsingActiveVersion bool `db:"using_active_version" json:"using_active_version"` - ID uuid.UUID `db:"id" json:"id"` - Name string `db:"name" json:"name"` - DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` - PrebuildStatus NullPrebuildStatus `db:"prebuild_status" json:"prebuild_status"` - Deleted bool `db:"deleted" json:"deleted"` - Deprecated bool `db:"deprecated" json:"deprecated"` + TemplateID uuid.UUID `db:"template_id" json:"template_id"` + TemplateName string `db:"template_name" json:"template_name"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + OrganizationName string `db:"organization_name" json:"organization_name"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + TemplateVersionName string `db:"template_version_name" json:"template_version_name"` + UsingActiveVersion bool `db:"using_active_version" json:"using_active_version"` + ID uuid.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` + PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"` + Deleted bool `db:"deleted" json:"deleted"` + Deprecated bool `db:"deprecated" json:"deprecated"` } // GetTemplatePresetsWithPrebuilds retrieves template versions with configured presets and prebuilds. @@ -6587,15 +6587,15 @@ WHERE tvp.id = $1 ` type GetPresetByIDRow struct { - ID uuid.UUID `db:"id" json:"id"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - Name string `db:"name" json:"name"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` - InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"` - PrebuildStatus NullPrebuildStatus `db:"prebuild_status" json:"prebuild_status"` - TemplateID uuid.NullUUID `db:"template_id" json:"template_id"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + ID uuid.UUID `db:"id" json:"id"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + Name string `db:"name" json:"name"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"` + InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"` + PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"` + TemplateID uuid.NullUUID `db:"template_id" json:"template_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` } func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (GetPresetByIDRow, error) { @@ -6856,8 +6856,8 @@ WHERE id = $2 ` type UpdatePrebuildStatusParams struct { - Status NullPrebuildStatus `db:"status" json:"status"` - PresetID uuid.UUID `db:"preset_id" json:"preset_id"` + Status PrebuildStatus `db:"status" json:"status"` + PresetID uuid.UUID `db:"preset_id" json:"preset_id"` } func (q *sqlQuerier) UpdatePrebuildStatus(ctx context.Context, arg UpdatePrebuildStatusParams) error { diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index a27ebd485152b..4850fcc926eee 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -362,7 +362,7 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres ) // If the preset was previously hard-limited, log it and exit early. - if ps.Preset.PrebuildStatus.PrebuildStatus == database.PrebuildStatusHardLimited { + if ps.Preset.PrebuildStatus == database.PrebuildStatusHardLimited { logger.Warn(ctx, "skipping hard limited preset", slog.F("preset_id", ps.Preset.ID), slog.F("name", ps.Preset.Name)) return nil } @@ -374,10 +374,7 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres logger.Warn(ctx, "skipping hard limited preset", slog.F("preset_id", ps.Preset.ID), slog.F("name", ps.Preset.Name)) err := c.store.UpdatePrebuildStatus(ctx, database.UpdatePrebuildStatusParams{ - Status: database.NullPrebuildStatus{ - PrebuildStatus: database.PrebuildStatusHardLimited, - Valid: true, - }, + Status: database.PrebuildStatusHardLimited, PresetID: ps.Preset.ID, }) if err != nil { From e667dad0ef9e77ede4ab7d3db40747996498159f Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 15:35:41 +0000 Subject: [PATCH 31/47] refactor: fix dbauthz test --- coderd/database/dbauthz/dbauthz_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 2fc014e8a320c..e7e2aa10ebce7 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4942,10 +4942,7 @@ func (s *MethodTestSuite) TestPrebuilds() { }, InvalidateAfterSecs: preset.InvalidateAfterSecs, OrganizationID: org.ID, - PrebuildStatus: database.NullPrebuildStatus{ - PrebuildStatus: database.PrebuildStatusNormal, - Valid: true, - }, + PrebuildStatus: database.PrebuildStatusNormal, }) })) s.Run("UpdatePrebuildStatus", s.Subtest(func(db database.Store, check *expects) { @@ -4968,6 +4965,7 @@ func (s *MethodTestSuite) TestPrebuilds() { }) req := database.UpdatePrebuildStatusParams{ PresetID: preset.ID, + Status: database.PrebuildStatusNormal, } check.Args(req). Asserts(rbac.ResourceTemplate.WithID(template.ID).InOrg(org.ID), policy.ActionUpdate) From 8a517025e744c34c0621acc300263adb1c8c9f65 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 16:39:13 +0000 Subject: [PATCH 32/47] refactor: use healthy enum option instead of normal --- coderd/database/dbauthz/dbauthz_test.go | 4 ++-- coderd/database/dbmem/dbmem.go | 2 +- coderd/database/dump.sql | 4 ++-- .../migrations/000329_add_status_to_template_presets.up.sql | 4 ++-- coderd/database/models.go | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index e7e2aa10ebce7..5eace0d50d0eb 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4942,7 +4942,7 @@ func (s *MethodTestSuite) TestPrebuilds() { }, InvalidateAfterSecs: preset.InvalidateAfterSecs, OrganizationID: org.ID, - PrebuildStatus: database.PrebuildStatusNormal, + PrebuildStatus: database.PrebuildStatusHealthy, }) })) s.Run("UpdatePrebuildStatus", s.Subtest(func(db database.Store, check *expects) { @@ -4965,7 +4965,7 @@ func (s *MethodTestSuite) TestPrebuilds() { }) req := database.UpdatePrebuildStatusParams{ PresetID: preset.ID, - Status: database.PrebuildStatusNormal, + Status: database.PrebuildStatusHealthy, } check.Args(req). Asserts(rbac.ResourceTemplate.WithID(template.ID).InOrg(org.ID), policy.ActionUpdate) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index b909809b8e758..9432ee8c46453 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9049,7 +9049,7 @@ func (q *FakeQuerier) InsertPreset(_ context.Context, arg database.InsertPresetP Int32: 0, Valid: true, }, - PrebuildStatus: database.PrebuildStatusNormal, + PrebuildStatus: database.PrebuildStatusHealthy, } q.presets = append(q.presets, preset) return preset, nil diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index c44e572211ed7..ec196405df2d3 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -154,7 +154,7 @@ CREATE TYPE port_share_protocol AS ENUM ( ); CREATE TYPE prebuild_status AS ENUM ( - 'normal', + 'healthy', 'hard_limited', 'validation_failed' ); @@ -1446,7 +1446,7 @@ CREATE TABLE template_version_presets ( created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, desired_instances integer, invalidate_after_secs integer DEFAULT 0, - prebuild_status prebuild_status DEFAULT 'normal'::prebuild_status NOT NULL + prebuild_status prebuild_status DEFAULT 'healthy'::prebuild_status NOT NULL ); CREATE TABLE template_version_terraform_values ( diff --git a/coderd/database/migrations/000329_add_status_to_template_presets.up.sql b/coderd/database/migrations/000329_add_status_to_template_presets.up.sql index 7a1fa1c3af0f5..019a246f73a87 100644 --- a/coderd/database/migrations/000329_add_status_to_template_presets.up.sql +++ b/coderd/database/migrations/000329_add_status_to_template_presets.up.sql @@ -1,7 +1,7 @@ CREATE TYPE prebuild_status AS ENUM ( - 'normal', -- Prebuilds are working as expected; this is the default, healthy state. + 'healthy', -- Prebuilds are working as expected; this is the default, healthy state. 'hard_limited', -- Prebuilds have failed repeatedly and hit the configured hard failure limit; won't be retried anymore. 'validation_failed' -- Prebuilds failed due to a non-retryable validation error (e.g. template misconfiguration); won't be retried. ); -ALTER TABLE template_version_presets ADD COLUMN prebuild_status prebuild_status NOT NULL DEFAULT 'normal'::prebuild_status; +ALTER TABLE template_version_presets ADD COLUMN prebuild_status prebuild_status NOT NULL DEFAULT 'healthy'::prebuild_status; diff --git a/coderd/database/models.go b/coderd/database/models.go index 163263b0bef6f..d5047f6bbe65f 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1346,7 +1346,7 @@ func AllPortShareProtocolValues() []PortShareProtocol { type PrebuildStatus string const ( - PrebuildStatusNormal PrebuildStatus = "normal" + PrebuildStatusHealthy PrebuildStatus = "healthy" PrebuildStatusHardLimited PrebuildStatus = "hard_limited" PrebuildStatusValidationFailed PrebuildStatus = "validation_failed" ) @@ -1388,7 +1388,7 @@ func (ns NullPrebuildStatus) Value() (driver.Value, error) { func (e PrebuildStatus) Valid() bool { switch e { - case PrebuildStatusNormal, + case PrebuildStatusHealthy, PrebuildStatusHardLimited, PrebuildStatusValidationFailed: return true @@ -1398,7 +1398,7 @@ func (e PrebuildStatus) Valid() bool { func AllPrebuildStatusValues() []PrebuildStatus { return []PrebuildStatus{ - PrebuildStatusNormal, + PrebuildStatusHealthy, PrebuildStatusHardLimited, PrebuildStatusValidationFailed, } From a793e1803f40fcfe2eb0bc1af909e79fc59457e8 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 16:49:04 +0000 Subject: [PATCH 33/47] refactor: minor refactoring in tests --- coderd/database/querier_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index f161d27b9a978..5bafa58796b7a 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -4123,8 +4123,7 @@ func TestGetPresetsBackoff(t *testing.T) { }) tmpl1 := createTemplate(t, db, orgID, userID) - tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil) - _ = tmpl1V1 + createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil) backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-time.Hour)) require.NoError(t, err) @@ -4674,8 +4673,7 @@ func TestGetPresetsAtFailureLimit(t *testing.T) { }) tmpl1 := createTemplate(t, db, orgID, userID) - tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil) - _ = tmpl1V1 + createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil) hardLimitedPresets, err := db.GetPresetsAtFailureLimit(ctx, 1) require.NoError(t, err) From a0fb69c904a0f85991551815b4783393393d2175 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 17:05:54 +0000 Subject: [PATCH 34/47] refactor: rename DB method --- coderd/database/dbauthz/dbauthz.go | 106 +++++++++++----------- coderd/database/dbauthz/dbauthz_test.go | 4 +- coderd/database/dbmem/dbmem.go | 38 ++++---- coderd/database/dbmetrics/querymetrics.go | 6 +- coderd/database/dbmock/dbmock.go | 12 +-- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 8 +- coderd/database/queries/presets.sql | 2 +- enterprise/coderd/prebuilds/reconcile.go | 2 +- 9 files changed, 90 insertions(+), 90 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index e7a45aa67809f..908b1aad2399b 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -482,9 +482,9 @@ func As(ctx context.Context, actor rbac.Subject) context.Context { // running the insertFunc. The insertFunc is expected to return the object that // was inserted. func insert[ - ObjectType any, - ArgumentType any, - Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ObjectType any, +ArgumentType any, +Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -495,9 +495,9 @@ func insert[ } func insertWithAction[ - ObjectType any, - ArgumentType any, - Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ObjectType any, +ArgumentType any, +Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -524,10 +524,10 @@ func insertWithAction[ } func deleteQ[ - ObjectType rbac.Objecter, - ArgumentType any, - Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), - Delete func(ctx context.Context, arg ArgumentType) error, +ObjectType rbac.Objecter, +ArgumentType any, +Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), +Delete func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -539,10 +539,10 @@ func deleteQ[ } func updateWithReturn[ - ObjectType rbac.Objecter, - ArgumentType any, - Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), - UpdateQuery func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ObjectType rbac.Objecter, +ArgumentType any, +Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), +UpdateQuery func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -553,10 +553,10 @@ func updateWithReturn[ } func update[ - ObjectType rbac.Objecter, - ArgumentType any, - Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), - Exec func(ctx context.Context, arg ArgumentType) error, +ObjectType rbac.Objecter, +ArgumentType any, +Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), +Exec func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -574,9 +574,9 @@ func update[ // user cannot read the resource. This is because the resource details are // required to run a proper authorization check. func fetchWithAction[ - ArgumentType any, - ObjectType rbac.Objecter, - DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ArgumentType any, +ObjectType rbac.Objecter, +DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -607,9 +607,9 @@ func fetchWithAction[ } func fetch[ - ArgumentType any, - ObjectType rbac.Objecter, - DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ArgumentType any, +ObjectType rbac.Objecter, +DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -622,10 +622,10 @@ func fetch[ // from SQL 'exec' functions which only return an error. // See fetchAndQuery for more information. func fetchAndExec[ - ObjectType rbac.Objecter, - ArgumentType any, - Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), - Exec func(ctx context.Context, arg ArgumentType) error, +ObjectType rbac.Objecter, +ArgumentType any, +Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), +Exec func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -648,10 +648,10 @@ func fetchAndExec[ // **before** the query runs. The returns from the fetch are only used to // assert rbac. The final return of this function comes from the Query function. func fetchAndQuery[ - ObjectType rbac.Objecter, - ArgumentType any, - Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), - Query func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ObjectType rbac.Objecter, +ArgumentType any, +Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), +Query func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -685,9 +685,9 @@ func fetchAndQuery[ // fetchWithPostFilter is like fetch, but works with lists of objects. // SQL filters are much more optimal. func fetchWithPostFilter[ - ArgumentType any, - ObjectType rbac.Objecter, - DatabaseFunc func(ctx context.Context, arg ArgumentType) ([]ObjectType, error), +ArgumentType any, +ObjectType rbac.Objecter, +DatabaseFunc func(ctx context.Context, arg ArgumentType) ([]ObjectType, error), ]( authorizer rbac.Authorizer, action policy.Action, @@ -1074,6 +1074,24 @@ func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole) return nil } +func (q *querier) UpdatePresetPrebuildStatus(ctx context.Context, arg database.UpdatePresetPrebuildStatusParams) error { + preset, err := q.db.GetPresetByID(ctx, arg.PresetID) + if err != nil { + return err + } + + object := rbac.ResourceTemplate. + WithID(preset.TemplateID.UUID). + InOrg(preset.OrganizationID) + + err = q.authorizeContext(ctx, policy.ActionUpdate, object) + if err != nil { + return err + } + + return q.db.UpdatePresetPrebuildStatus(ctx, arg) +} + func (q *querier) AcquireLock(ctx context.Context, id int64) error { return q.db.AcquireLock(ctx, id) } @@ -4178,24 +4196,6 @@ func (q *querier) UpdateOrganizationDeletedByID(ctx context.Context, arg databas return deleteQ(q.log, q.auth, q.db.GetOrganizationByID, deleteF)(ctx, arg.ID) } -func (q *querier) UpdatePrebuildStatus(ctx context.Context, arg database.UpdatePrebuildStatusParams) error { - preset, err := q.db.GetPresetByID(ctx, arg.PresetID) - if err != nil { - return err - } - - object := rbac.ResourceTemplate. - WithID(preset.TemplateID.UUID). - InOrg(preset.OrganizationID) - - err = q.authorizeContext(ctx, policy.ActionUpdate, object) - if err != nil { - return err - } - - return q.db.UpdatePrebuildStatus(ctx, arg) -} - func (q *querier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerDaemon); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 5eace0d50d0eb..1e23026a3b422 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4945,7 +4945,7 @@ func (s *MethodTestSuite) TestPrebuilds() { PrebuildStatus: database.PrebuildStatusHealthy, }) })) - s.Run("UpdatePrebuildStatus", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpdatePresetPrebuildStatus", s.Subtest(func(db database.Store, check *expects) { org := dbgen.Organization(s.T(), db, database.Organization{}) user := dbgen.User(s.T(), db, database.User{}) template := dbgen.Template(s.T(), db, database.Template{ @@ -4963,7 +4963,7 @@ func (s *MethodTestSuite) TestPrebuilds() { preset := dbgen.Preset(s.T(), db, database.InsertPresetParams{ TemplateVersionID: templateVersion.ID, }) - req := database.UpdatePrebuildStatusParams{ + req := database.UpdatePresetPrebuildStatusParams{ PresetID: preset.ID, Status: database.PrebuildStatusHealthy, } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 9432ee8c46453..1a8a5d22497b6 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1386,6 +1386,25 @@ func isDeprecated(template database.Template) bool { return template.Deprecated != "" } +func (q *FakeQuerier) UpdatePresetPrebuildStatus(ctx context.Context, arg database.UpdatePresetPrebuildStatusParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, preset := range q.presets { + if preset.ID == arg.PresetID { + preset.PrebuildStatus = arg.Status + return nil + } + } + + return xerrors.Errorf("preset %v does not exist", arg.PresetID) +} + func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error { return xerrors.New("AcquireLock must only be called within a transaction") } @@ -10878,25 +10897,6 @@ func (q *FakeQuerier) UpdateOrganizationDeletedByID(_ context.Context, arg datab return sql.ErrNoRows } -func (q *FakeQuerier) UpdatePrebuildStatus(ctx context.Context, arg database.UpdatePrebuildStatusParams) error { - err := validateDatabaseType(arg) - if err != nil { - return err - } - - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, preset := range q.presets { - if preset.ID == arg.PresetID { - preset.PrebuildStatus = arg.Status - return nil - } - } - - return xerrors.Errorf("preset %v does not exist", arg.PresetID) -} - func (q *FakeQuerier) UpdateProvisionerDaemonLastSeenAt(_ context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { err := validateDatabaseType(arg) if err != nil { diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index cabc6686842d5..d86185c2ae1c3 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2685,10 +2685,10 @@ func (m queryMetricsStore) UpdateOrganizationDeletedByID(ctx context.Context, ar return r0 } -func (m queryMetricsStore) UpdatePrebuildStatus(ctx context.Context, arg database.UpdatePrebuildStatusParams) error { +func (m queryMetricsStore) UpdatePresetPrebuildStatus(ctx context.Context, arg database.UpdatePresetPrebuildStatusParams) error { start := time.Now() - r0 := m.s.UpdatePrebuildStatus(ctx, arg) - m.queryLatencies.WithLabelValues("UpdatePrebuildStatus").Observe(time.Since(start).Seconds()) + r0 := m.s.UpdatePresetPrebuildStatus(ctx, arg) + m.queryLatencies.WithLabelValues("UpdatePresetPrebuildStatus").Observe(time.Since(start).Seconds()) return r0 } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 3b2c84c531146..ee51462b4ba31 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -5691,18 +5691,18 @@ func (mr *MockStoreMockRecorder) UpdateOrganizationDeletedByID(ctx, arg any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOrganizationDeletedByID", reflect.TypeOf((*MockStore)(nil).UpdateOrganizationDeletedByID), ctx, arg) } -// UpdatePrebuildStatus mocks base method. -func (m *MockStore) UpdatePrebuildStatus(ctx context.Context, arg database.UpdatePrebuildStatusParams) error { +// UpdatePresetPrebuildStatus mocks base method. +func (m *MockStore) UpdatePresetPrebuildStatus(ctx context.Context, arg database.UpdatePresetPrebuildStatusParams) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdatePrebuildStatus", ctx, arg) + ret := m.ctrl.Call(m, "UpdatePresetPrebuildStatus", ctx, arg) ret0, _ := ret[0].(error) return ret0 } -// UpdatePrebuildStatus indicates an expected call of UpdatePrebuildStatus. -func (mr *MockStoreMockRecorder) UpdatePrebuildStatus(ctx, arg any) *gomock.Call { +// UpdatePresetPrebuildStatus indicates an expected call of UpdatePresetPrebuildStatus. +func (mr *MockStoreMockRecorder) UpdatePresetPrebuildStatus(ctx, arg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePrebuildStatus", reflect.TypeOf((*MockStore)(nil).UpdatePrebuildStatus), ctx, arg) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePresetPrebuildStatus", reflect.TypeOf((*MockStore)(nil).UpdatePresetPrebuildStatus), ctx, arg) } // UpdateProvisionerDaemonLastSeenAt mocks base method. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index a7ea91ef86169..f9e38bb5a30e1 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -572,7 +572,7 @@ type sqlcQuerier interface { UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error) UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) UpdateOrganizationDeletedByID(ctx context.Context, arg UpdateOrganizationDeletedByIDParams) error - UpdatePrebuildStatus(ctx context.Context, arg UpdatePrebuildStatusParams) error + UpdatePresetPrebuildStatus(ctx context.Context, arg UpdatePresetPrebuildStatusParams) error UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 51c9f07318b45..9cddefef64ddc 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6849,19 +6849,19 @@ func (q *sqlQuerier) InsertPresetParameters(ctx context.Context, arg InsertPrese return items, nil } -const updatePrebuildStatus = `-- name: UpdatePrebuildStatus :exec +const updatePresetPrebuildStatus = `-- name: UpdatePresetPrebuildStatus :exec UPDATE template_version_presets SET prebuild_status = $1 WHERE id = $2 ` -type UpdatePrebuildStatusParams struct { +type UpdatePresetPrebuildStatusParams struct { Status PrebuildStatus `db:"status" json:"status"` PresetID uuid.UUID `db:"preset_id" json:"preset_id"` } -func (q *sqlQuerier) UpdatePrebuildStatus(ctx context.Context, arg UpdatePrebuildStatusParams) error { - _, err := q.db.ExecContext(ctx, updatePrebuildStatus, arg.Status, arg.PresetID) +func (q *sqlQuerier) UpdatePresetPrebuildStatus(ctx context.Context, arg UpdatePresetPrebuildStatusParams) error { + _, err := q.db.ExecContext(ctx, updatePresetPrebuildStatus, arg.Status, arg.PresetID) return err } diff --git a/coderd/database/queries/presets.sql b/coderd/database/queries/presets.sql index e721db18a7f93..2fb6722bc2c33 100644 --- a/coderd/database/queries/presets.sql +++ b/coderd/database/queries/presets.sql @@ -25,7 +25,7 @@ SELECT unnest(@values :: TEXT[]) RETURNING *; --- name: UpdatePrebuildStatus :exec +-- name: UpdatePresetPrebuildStatus :exec UPDATE template_version_presets SET prebuild_status = @status WHERE id = @preset_id; diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 4850fcc926eee..3b651daf3cdf0 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -373,7 +373,7 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres if ps.IsHardLimited { logger.Warn(ctx, "skipping hard limited preset", slog.F("preset_id", ps.Preset.ID), slog.F("name", ps.Preset.Name)) - err := c.store.UpdatePrebuildStatus(ctx, database.UpdatePrebuildStatusParams{ + err := c.store.UpdatePresetPrebuildStatus(ctx, database.UpdatePresetPrebuildStatusParams{ Status: database.PrebuildStatusHardLimited, PresetID: ps.Preset.ID, }) From c6f209caed7389c61e032c74a4f4b2da418b9270 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 18:04:35 +0000 Subject: [PATCH 35/47] refactor: make fmt --- coderd/database/dbauthz/dbauthz.go | 70 +++++++++++++++--------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 908b1aad2399b..66260bd326f09 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -482,9 +482,9 @@ func As(ctx context.Context, actor rbac.Subject) context.Context { // running the insertFunc. The insertFunc is expected to return the object that // was inserted. func insert[ -ObjectType any, -ArgumentType any, -Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ObjectType any, + ArgumentType any, + Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -495,9 +495,9 @@ Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), } func insertWithAction[ -ObjectType any, -ArgumentType any, -Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ObjectType any, + ArgumentType any, + Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -524,10 +524,10 @@ Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), } func deleteQ[ -ObjectType rbac.Objecter, -ArgumentType any, -Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), -Delete func(ctx context.Context, arg ArgumentType) error, + ObjectType rbac.Objecter, + ArgumentType any, + Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), + Delete func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -539,10 +539,10 @@ Delete func(ctx context.Context, arg ArgumentType) error, } func updateWithReturn[ -ObjectType rbac.Objecter, -ArgumentType any, -Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), -UpdateQuery func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ObjectType rbac.Objecter, + ArgumentType any, + Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), + UpdateQuery func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -553,10 +553,10 @@ UpdateQuery func(ctx context.Context, arg ArgumentType) (ObjectType, error), } func update[ -ObjectType rbac.Objecter, -ArgumentType any, -Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), -Exec func(ctx context.Context, arg ArgumentType) error, + ObjectType rbac.Objecter, + ArgumentType any, + Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), + Exec func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -574,9 +574,9 @@ Exec func(ctx context.Context, arg ArgumentType) error, // user cannot read the resource. This is because the resource details are // required to run a proper authorization check. func fetchWithAction[ -ArgumentType any, -ObjectType rbac.Objecter, -DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ArgumentType any, + ObjectType rbac.Objecter, + DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -607,9 +607,9 @@ DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), } func fetch[ -ArgumentType any, -ObjectType rbac.Objecter, -DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ArgumentType any, + ObjectType rbac.Objecter, + DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -622,10 +622,10 @@ DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), // from SQL 'exec' functions which only return an error. // See fetchAndQuery for more information. func fetchAndExec[ -ObjectType rbac.Objecter, -ArgumentType any, -Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), -Exec func(ctx context.Context, arg ArgumentType) error, + ObjectType rbac.Objecter, + ArgumentType any, + Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), + Exec func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -648,10 +648,10 @@ Exec func(ctx context.Context, arg ArgumentType) error, // **before** the query runs. The returns from the fetch are only used to // assert rbac. The final return of this function comes from the Query function. func fetchAndQuery[ -ObjectType rbac.Objecter, -ArgumentType any, -Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), -Query func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ObjectType rbac.Objecter, + ArgumentType any, + Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), + Query func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -685,9 +685,9 @@ Query func(ctx context.Context, arg ArgumentType) (ObjectType, error), // fetchWithPostFilter is like fetch, but works with lists of objects. // SQL filters are much more optimal. func fetchWithPostFilter[ -ArgumentType any, -ObjectType rbac.Objecter, -DatabaseFunc func(ctx context.Context, arg ArgumentType) ([]ObjectType, error), + ArgumentType any, + ObjectType rbac.Objecter, + DatabaseFunc func(ctx context.Context, arg ArgumentType) ([]ObjectType, error), ]( authorizer rbac.Authorizer, action policy.Action, From 9dccf3ecac4c2f7b2fa0066680e9e80642ab1acb Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 18:21:14 +0000 Subject: [PATCH 36/47] refactor: make gen + reorder methods --- coderd/database/dbauthz/dbauthz.go | 106 ++++++++++++++--------------- coderd/database/dbmem/dbmem.go | 38 +++++------ 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 66260bd326f09..d15cc3442339d 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -482,9 +482,9 @@ func As(ctx context.Context, actor rbac.Subject) context.Context { // running the insertFunc. The insertFunc is expected to return the object that // was inserted. func insert[ - ObjectType any, - ArgumentType any, - Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ObjectType any, +ArgumentType any, +Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -495,9 +495,9 @@ func insert[ } func insertWithAction[ - ObjectType any, - ArgumentType any, - Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ObjectType any, +ArgumentType any, +Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -524,10 +524,10 @@ func insertWithAction[ } func deleteQ[ - ObjectType rbac.Objecter, - ArgumentType any, - Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), - Delete func(ctx context.Context, arg ArgumentType) error, +ObjectType rbac.Objecter, +ArgumentType any, +Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), +Delete func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -539,10 +539,10 @@ func deleteQ[ } func updateWithReturn[ - ObjectType rbac.Objecter, - ArgumentType any, - Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), - UpdateQuery func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ObjectType rbac.Objecter, +ArgumentType any, +Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), +UpdateQuery func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -553,10 +553,10 @@ func updateWithReturn[ } func update[ - ObjectType rbac.Objecter, - ArgumentType any, - Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), - Exec func(ctx context.Context, arg ArgumentType) error, +ObjectType rbac.Objecter, +ArgumentType any, +Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), +Exec func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -574,9 +574,9 @@ func update[ // user cannot read the resource. This is because the resource details are // required to run a proper authorization check. func fetchWithAction[ - ArgumentType any, - ObjectType rbac.Objecter, - DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ArgumentType any, +ObjectType rbac.Objecter, +DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -607,9 +607,9 @@ func fetchWithAction[ } func fetch[ - ArgumentType any, - ObjectType rbac.Objecter, - DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ArgumentType any, +ObjectType rbac.Objecter, +DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -622,10 +622,10 @@ func fetch[ // from SQL 'exec' functions which only return an error. // See fetchAndQuery for more information. func fetchAndExec[ - ObjectType rbac.Objecter, - ArgumentType any, - Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), - Exec func(ctx context.Context, arg ArgumentType) error, +ObjectType rbac.Objecter, +ArgumentType any, +Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), +Exec func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -648,10 +648,10 @@ func fetchAndExec[ // **before** the query runs. The returns from the fetch are only used to // assert rbac. The final return of this function comes from the Query function. func fetchAndQuery[ - ObjectType rbac.Objecter, - ArgumentType any, - Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), - Query func(ctx context.Context, arg ArgumentType) (ObjectType, error), +ObjectType rbac.Objecter, +ArgumentType any, +Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), +Query func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -685,9 +685,9 @@ func fetchAndQuery[ // fetchWithPostFilter is like fetch, but works with lists of objects. // SQL filters are much more optimal. func fetchWithPostFilter[ - ArgumentType any, - ObjectType rbac.Objecter, - DatabaseFunc func(ctx context.Context, arg ArgumentType) ([]ObjectType, error), +ArgumentType any, +ObjectType rbac.Objecter, +DatabaseFunc func(ctx context.Context, arg ArgumentType) ([]ObjectType, error), ]( authorizer rbac.Authorizer, action policy.Action, @@ -1074,24 +1074,6 @@ func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole) return nil } -func (q *querier) UpdatePresetPrebuildStatus(ctx context.Context, arg database.UpdatePresetPrebuildStatusParams) error { - preset, err := q.db.GetPresetByID(ctx, arg.PresetID) - if err != nil { - return err - } - - object := rbac.ResourceTemplate. - WithID(preset.TemplateID.UUID). - InOrg(preset.OrganizationID) - - err = q.authorizeContext(ctx, policy.ActionUpdate, object) - if err != nil { - return err - } - - return q.db.UpdatePresetPrebuildStatus(ctx, arg) -} - func (q *querier) AcquireLock(ctx context.Context, id int64) error { return q.db.AcquireLock(ctx, id) } @@ -4196,6 +4178,24 @@ func (q *querier) UpdateOrganizationDeletedByID(ctx context.Context, arg databas return deleteQ(q.log, q.auth, q.db.GetOrganizationByID, deleteF)(ctx, arg.ID) } +func (q *querier) UpdatePresetPrebuildStatus(ctx context.Context, arg database.UpdatePresetPrebuildStatusParams) error { + preset, err := q.db.GetPresetByID(ctx, arg.PresetID) + if err != nil { + return err + } + + object := rbac.ResourceTemplate. + WithID(preset.TemplateID.UUID). + InOrg(preset.OrganizationID) + + err = q.authorizeContext(ctx, policy.ActionUpdate, object) + if err != nil { + return err + } + + return q.db.UpdatePresetPrebuildStatus(ctx, arg) +} + func (q *querier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerDaemon); err != nil { return err diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 1a8a5d22497b6..c244fbb9b7415 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1386,25 +1386,6 @@ func isDeprecated(template database.Template) bool { return template.Deprecated != "" } -func (q *FakeQuerier) UpdatePresetPrebuildStatus(ctx context.Context, arg database.UpdatePresetPrebuildStatusParams) error { - err := validateDatabaseType(arg) - if err != nil { - return err - } - - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, preset := range q.presets { - if preset.ID == arg.PresetID { - preset.PrebuildStatus = arg.Status - return nil - } - } - - return xerrors.Errorf("preset %v does not exist", arg.PresetID) -} - func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error { return xerrors.New("AcquireLock must only be called within a transaction") } @@ -10897,6 +10878,25 @@ func (q *FakeQuerier) UpdateOrganizationDeletedByID(_ context.Context, arg datab return sql.ErrNoRows } +func (q *FakeQuerier) UpdatePresetPrebuildStatus(ctx context.Context, arg database.UpdatePresetPrebuildStatusParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, preset := range q.presets { + if preset.ID == arg.PresetID { + preset.PrebuildStatus = arg.Status + return nil + } + } + + return xerrors.Errorf("preset %v does not exist", arg.PresetID) +} + func (q *FakeQuerier) UpdateProvisionerDaemonLastSeenAt(_ context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { err := validateDatabaseType(arg) if err != nil { From c19ae04a74d03563433c853ec7b2dcf9af3aa141 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 18:29:27 +0000 Subject: [PATCH 37/47] refactor: make fmt --- coderd/database/dbauthz/dbauthz.go | 70 +++++++++++++++--------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index d15cc3442339d..bf74a7205d9a9 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -482,9 +482,9 @@ func As(ctx context.Context, actor rbac.Subject) context.Context { // running the insertFunc. The insertFunc is expected to return the object that // was inserted. func insert[ -ObjectType any, -ArgumentType any, -Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ObjectType any, + ArgumentType any, + Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -495,9 +495,9 @@ Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), } func insertWithAction[ -ObjectType any, -ArgumentType any, -Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ObjectType any, + ArgumentType any, + Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -524,10 +524,10 @@ Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), } func deleteQ[ -ObjectType rbac.Objecter, -ArgumentType any, -Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), -Delete func(ctx context.Context, arg ArgumentType) error, + ObjectType rbac.Objecter, + ArgumentType any, + Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), + Delete func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -539,10 +539,10 @@ Delete func(ctx context.Context, arg ArgumentType) error, } func updateWithReturn[ -ObjectType rbac.Objecter, -ArgumentType any, -Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), -UpdateQuery func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ObjectType rbac.Objecter, + ArgumentType any, + Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), + UpdateQuery func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -553,10 +553,10 @@ UpdateQuery func(ctx context.Context, arg ArgumentType) (ObjectType, error), } func update[ -ObjectType rbac.Objecter, -ArgumentType any, -Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), -Exec func(ctx context.Context, arg ArgumentType) error, + ObjectType rbac.Objecter, + ArgumentType any, + Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), + Exec func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -574,9 +574,9 @@ Exec func(ctx context.Context, arg ArgumentType) error, // user cannot read the resource. This is because the resource details are // required to run a proper authorization check. func fetchWithAction[ -ArgumentType any, -ObjectType rbac.Objecter, -DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ArgumentType any, + ObjectType rbac.Objecter, + DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -607,9 +607,9 @@ DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), } func fetch[ -ArgumentType any, -ObjectType rbac.Objecter, -DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ArgumentType any, + ObjectType rbac.Objecter, + DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -622,10 +622,10 @@ DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), // from SQL 'exec' functions which only return an error. // See fetchAndQuery for more information. func fetchAndExec[ -ObjectType rbac.Objecter, -ArgumentType any, -Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), -Exec func(ctx context.Context, arg ArgumentType) error, + ObjectType rbac.Objecter, + ArgumentType any, + Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), + Exec func(ctx context.Context, arg ArgumentType) error, ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -648,10 +648,10 @@ Exec func(ctx context.Context, arg ArgumentType) error, // **before** the query runs. The returns from the fetch are only used to // assert rbac. The final return of this function comes from the Query function. func fetchAndQuery[ -ObjectType rbac.Objecter, -ArgumentType any, -Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), -Query func(ctx context.Context, arg ArgumentType) (ObjectType, error), + ObjectType rbac.Objecter, + ArgumentType any, + Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error), + Query func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, @@ -685,9 +685,9 @@ Query func(ctx context.Context, arg ArgumentType) (ObjectType, error), // fetchWithPostFilter is like fetch, but works with lists of objects. // SQL filters are much more optimal. func fetchWithPostFilter[ -ArgumentType any, -ObjectType rbac.Objecter, -DatabaseFunc func(ctx context.Context, arg ArgumentType) ([]ObjectType, error), + ArgumentType any, + ObjectType rbac.Objecter, + DatabaseFunc func(ctx context.Context, arg ArgumentType) ([]ObjectType, error), ]( authorizer rbac.Authorizer, action policy.Action, From 4b145ccc071e20dc4b7c01192a0e9e1c68f53aa1 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 18:45:07 +0000 Subject: [PATCH 38/47] refactor: CR's fixes --- codersdk/deployment.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 63e14f61d584b..c8f1edb76b15b 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3094,7 +3094,7 @@ Write out the current server config as YAML to stdout.`, }, { Name: "Failure Hard Limit", - Description: "Maximum number of consecutive failed prebuilds before a preset hits the hard limit. FailureHardLimit is disabled when set to zero.", + Description: "Maximum number of consecutive failed prebuilds before a preset hits the hard limit; disabled when set to zero.", Flag: "workspace-prebuilds-failure-hard-limit", Env: "CODER_WORKSPACE_PREBUILDS_FAILURE_HARD_LIMIT", Value: &c.Prebuilds.FailureHardLimit, From 80f36775bd2de62146a16e1dfda084768a92e3a0 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 19:29:53 +0000 Subject: [PATCH 39/47] refactor: make gen --- cli/testdata/server-config.yaml.golden | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 13bb147063750..bf09f8c7781a1 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -705,6 +705,6 @@ workspace_prebuilds: # (default: 1h0m0s, type: duration) reconciliation_backoff_lookback_period: 1h0m0s # Maximum number of consecutive failed prebuilds before a preset hits the hard - # limit. FailureHardLimit is disabled when set to zero. + # limit; disabled when set to zero. # (default: 3, type: int) failure_hard_limit: 3 From 2144d13d56c22d4397ab234c513360a81eed962c Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 19:33:02 +0000 Subject: [PATCH 40/47] refactor: CR's fixes --- enterprise/coderd/prebuilds/reconcile.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 3b651daf3cdf0..257e28f11921a 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -363,7 +363,7 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres // If the preset was previously hard-limited, log it and exit early. if ps.Preset.PrebuildStatus == database.PrebuildStatusHardLimited { - logger.Warn(ctx, "skipping hard limited preset", slog.F("preset_id", ps.Preset.ID), slog.F("name", ps.Preset.Name)) + logger.Warn(ctx, "skipping hard limited preset") return nil } @@ -371,7 +371,7 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres // - Mark it as hard-limited in the database // - Send notifications to template admins if ps.IsHardLimited { - logger.Warn(ctx, "skipping hard limited preset", slog.F("preset_id", ps.Preset.ID), slog.F("name", ps.Preset.Name)) + logger.Warn(ctx, "skipping hard limited preset") err := c.store.UpdatePresetPrebuildStatus(ctx, database.UpdatePresetPrebuildStatusParams{ Status: database.PrebuildStatusHardLimited, From 79725b304eb61d82ca9f76731dde45b5057675bf Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 19:47:18 +0000 Subject: [PATCH 41/47] refactor: CR's fixes --- enterprise/coderd/prebuilds/reconcile.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 257e28f11921a..48a5fa701fce7 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -378,12 +378,13 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres PresetID: ps.Preset.ID, }) if err != nil { - return err + return xerrors.Errorf("can't update preset prebuild status: %w", err) } err = c.notifyPrebuildFailureLimitReached(ctx, ps) if err != nil { - return err + logger.Error(ctx, "failed to notify that number of prebuild failures reached the limit", slog.Error(err)) + return nil } return nil @@ -392,16 +393,13 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres state := ps.CalculateState() actions, err := c.CalculateActions(ctx, ps) if err != nil { - logger.Error(ctx, "failed to calculate actions for preset", slog.Error(err), slog.F("preset_id", ps.Preset.ID)) + logger.Error(ctx, "failed to calculate actions for preset", slog.Error(err)) return nil } // Nothing has to be done. if !ps.Preset.UsingActiveVersion && actions.IsNoop() { - logger.Debug(ctx, "skipping reconciliation for preset - nothing has to be done", - slog.F("template_id", ps.Preset.TemplateID.String()), slog.F("template_name", ps.Preset.TemplateName), - slog.F("template_version_id", ps.Preset.TemplateVersionID.String()), slog.F("template_version_name", ps.Preset.TemplateVersionName), - slog.F("preset_id", ps.Preset.ID.String()), slog.F("preset_name", ps.Preset.Name)) + logger.Debug(ctx, "skipping reconciliation for preset - nothing has to be done") return nil } From 7e8b4b6cbc269fb9ed0bbf9c57133488f2fa00d8 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 20:21:49 +0000 Subject: [PATCH 42/47] refactor: improve comments for test --- enterprise/coderd/prebuilds/reconcile_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index 4dc45bd798e8c..4619d1e2ea238 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -688,7 +688,8 @@ func TestSkippingHardLimitedPresets(t *testing.T) { clock := quartz.NewMock(t) ctx := testutil.Context(t, testutil.WaitShort) cfg := codersdk.PrebuildsConfig{ - FailureHardLimit: serpent.Int64(tc.hardLimit), + FailureHardLimit: serpent.Int64(tc.hardLimit), + ReconciliationBackoffInterval: 0, } logger := slogtest.Make( t, &slogtest.Options{IgnoreErrors: true}, @@ -731,8 +732,9 @@ func TestSkippingHardLimitedPresets(t *testing.T) { workspaceCount := len(workspaces) require.Equal(t, 1, workspaceCount) - // Advance clock to bypass backoff mechanisms - clock.Advance(time.Second).MustWait(ctx) + // We simulate a failed prebuild in the test; Consequently, the backoff mechanism is triggered when ReconcileAll is called. + // Even though ReconciliationBackoffInterval is set to zero, we still need to advance the clock by at least one nanosecond. + clock.Advance(time.Nanosecond).MustWait(ctx) // Trigger reconciliation to attempt creating a new prebuild // The outcome depends on whether the hard limit has been reached From ab5acfb5508293fa9c8e044cba043edb2faab843 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Tue, 20 May 2025 20:28:33 +0000 Subject: [PATCH 43/47] refactor: CR's fixes --- enterprise/coderd/prebuilds/reconcile_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index 4619d1e2ea238..de95aaba256e6 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -661,7 +661,7 @@ func TestSkippingHardLimitedPresets(t *testing.T) { t.Skip("This test requires postgres") } - // Test cases verify the behavior of prebuild creation depending on configured failure limits + // Test cases verify the behavior of prebuild creation depending on configured failure limits. testCases := []struct { name string hardLimit int64 @@ -703,7 +703,7 @@ func TestSkippingHardLimitedPresets(t *testing.T) { RBACRoles: []string{codersdk.RoleTemplateAdmin}, }) - // Set up test environment with a template, version, and preset + // Set up test environment with a template, version, and preset. ownerID := uuid.New() dbgen.User(t, db, database.User{ ID: ownerID, @@ -726,7 +726,7 @@ func TestSkippingHardLimitedPresets(t *testing.T) { templateVersionID, ) - // Verify initial state: one failed workspace exists + // Verify initial state: one failed workspace exists. workspaces, err := db.GetWorkspacesByTemplateID(ctx, template.ID) require.NoError(t, err) workspaceCount := len(workspaces) @@ -736,8 +736,8 @@ func TestSkippingHardLimitedPresets(t *testing.T) { // Even though ReconciliationBackoffInterval is set to zero, we still need to advance the clock by at least one nanosecond. clock.Advance(time.Nanosecond).MustWait(ctx) - // Trigger reconciliation to attempt creating a new prebuild - // The outcome depends on whether the hard limit has been reached + // Trigger reconciliation to attempt creating a new prebuild. + // The outcome depends on whether the hard limit has been reached. require.NoError(t, controller.ReconcileAll(ctx)) // These two additional calls to ReconcileAll should not trigger any notifications. @@ -745,17 +745,17 @@ func TestSkippingHardLimitedPresets(t *testing.T) { require.NoError(t, controller.ReconcileAll(ctx)) require.NoError(t, controller.ReconcileAll(ctx)) - // Verify the final state after reconciliation + // Verify the final state after reconciliation. workspaces, err = db.GetWorkspacesByTemplateID(ctx, template.ID) require.NoError(t, err) if !tc.isHardLimitHit { - // When hard limit is not reached, a new workspace should be created + // When hard limit is not reached, a new workspace should be created. require.Equal(t, 2, len(workspaces)) return } - // When hard limit is reached, no new workspace should be created + // When hard limit is reached, no new workspace should be created. require.Equal(t, 1, len(workspaces)) // When hard limit is reached, a notification should be sent. From 7c09465f02a8be2636adc23401fdb5b3c8fc5577 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Wed, 21 May 2025 12:13:03 +0000 Subject: [PATCH 44/47] refactor: CR's fixes --- enterprise/coderd/prebuilds/reconcile_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index de95aaba256e6..f52a77ca500b9 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -748,15 +748,19 @@ func TestSkippingHardLimitedPresets(t *testing.T) { // Verify the final state after reconciliation. workspaces, err = db.GetWorkspacesByTemplateID(ctx, template.ID) require.NoError(t, err) + updatedPreset, err := db.GetPresetByID(ctx, preset.ID) + require.NoError(t, err) if !tc.isHardLimitHit { // When hard limit is not reached, a new workspace should be created. require.Equal(t, 2, len(workspaces)) + require.Equal(t, database.PrebuildStatusHealthy, updatedPreset.PrebuildStatus) return } // When hard limit is reached, no new workspace should be created. require.Equal(t, 1, len(workspaces)) + require.Equal(t, database.PrebuildStatusHardLimited, updatedPreset.PrebuildStatus) // When hard limit is reached, a notification should be sent. matching := fakeEnqueuer.Sent(func(notification *notificationstest.FakeNotification) bool { From b7a34c5d5257272f190b34068389fc4d5c21b95e Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Wed, 21 May 2025 18:35:09 +0000 Subject: [PATCH 45/47] refactor: improve notification template --- ...328_prebuild_failure_limit_notification.up.sql | 4 ++++ .../smtp/PrebuildFailureLimitReached.html.golden | 15 +++++++++++++++ .../PrebuildFailureLimitReached.json.golden | 4 ++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql index f2da2f8528478..403bd667abd28 100644 --- a/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql +++ b/coderd/database/migrations/000328_prebuild_failure_limit_notification.up.sql @@ -7,6 +7,10 @@ VALUES ('414d9331-c1fc-4761-b40c-d1f4702279eb', The number of failed prebuild attempts has reached the hard limit for template **{{ .Labels.template }}** and preset **{{ .Labels.preset }}**. To resume prebuilds, fix the underlying issue and upload a new template version. + +Refer to the documentation for more details: +- [Troubleshooting templates](https://coder.com/docs/admin/templates/troubleshooting) +- [Troubleshooting of prebuilt workspaces](https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces#administration-and-troubleshooting) $$, 'Template Events', '[ diff --git a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden index cad83b5207e8c..69f13b86ca71c 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/PrebuildFailureLimitReached.html.golden @@ -18,6 +18,14 @@ ate docker and preset particle-accelerator. To resume prebuilds, fix the underlying issue and upload a new template ver= sion. +Refer to the documentation for more details: + +Troubleshooting templates (https://coder.com/docs/admin/templates/troublesh= +ooting) +Troubleshooting of prebuilt workspaces (https://coder.com/docs/admin/templa= +tes/extending-templates/prebuilt-workspaces#administration-and-troubleshoot= +ing) + View failed prebuilt workspaces: http://test.com/workspaces?filter=3Downer:= prebuilds+status:failed+template:docker @@ -60,6 +68,13 @@ tor
.

To resume prebuilds, fix the underlying issue and upload a new template = version.

+ +

Refer to the documentation for more details:
+-
Troubl= +eshooting templates
+- Troubleshooting of pre= +built workspaces

=20 diff --git a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden index 706546a84d179..0a6e262ff7512 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/PrebuildFailureLimitReached.json.golden @@ -30,6 +30,6 @@ }, "title": "There is a problem creating prebuilt workspaces", "title_markdown": "There is a problem creating prebuilt workspaces", - "body": "The number of failed prebuild attempts has reached the hard limit for template docker and preset particle-accelerator.\n\nTo resume prebuilds, fix the underlying issue and upload a new template version.", - "body_markdown": "\nThe number of failed prebuild attempts has reached the hard limit for template **docker** and preset **particle-accelerator**.\n\nTo resume prebuilds, fix the underlying issue and upload a new template version.\n" + "body": "The number of failed prebuild attempts has reached the hard limit for template docker and preset particle-accelerator.\n\nTo resume prebuilds, fix the underlying issue and upload a new template version.\n\nRefer to the documentation for more details:\n\nTroubleshooting templates (https://coder.com/docs/admin/templates/troubleshooting)\nTroubleshooting of prebuilt workspaces (https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces#administration-and-troubleshooting)", + "body_markdown": "\nThe number of failed prebuild attempts has reached the hard limit for template **docker** and preset **particle-accelerator**.\n\nTo resume prebuilds, fix the underlying issue and upload a new template version.\n\nRefer to the documentation for more details:\n- [Troubleshooting templates](https://coder.com/docs/admin/templates/troubleshooting)\n- [Troubleshooting of prebuilt workspaces](https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces#administration-and-troubleshooting)\n" } \ No newline at end of file From e1e141d5713c407433d48923b34f3987aab003d1 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Wed, 21 May 2025 18:55:06 +0000 Subject: [PATCH 46/47] refactor: CR's fixes --- coderd/notifications/events.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 17cf2c1452ef6..0e88361b56f68 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -40,7 +40,11 @@ var ( TemplateWorkspaceBuildsFailedReport = uuid.MustParse("34a20db2-e9cc-4a93-b0e4-8569699d7a00") TemplateWorkspaceResourceReplaced = uuid.MustParse("89d9745a-816e-4695-a17f-3d0a229e2b8d") - PrebuildFailureLimitReached = uuid.MustParse("414d9331-c1fc-4761-b40c-d1f4702279eb") +) + +// Prebuilds-related events +var ( + PrebuildFailureLimitReached = uuid.MustParse("414d9331-c1fc-4761-b40c-d1f4702279eb") ) // Notification-related events. From 354aeb408f205a15b1157aaa8aaf6d2d6cd0ce64 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Wed, 21 May 2025 18:56:35 +0000 Subject: [PATCH 47/47] refactor: CR's fixes --- enterprise/coderd/prebuilds/reconcile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 48a5fa701fce7..7796e43777951 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -378,7 +378,7 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres PresetID: ps.Preset.ID, }) if err != nil { - return xerrors.Errorf("can't update preset prebuild status: %w", err) + return xerrors.Errorf("failed to update preset prebuild status: %w", err) } err = c.notifyPrebuildFailureLimitReached(ctx, ps)