feat: allow users to pause prebuilt workspace reconciliation (#18700)

This PR provides two commands:
* `coder prebuilds pause`
* `coder prebuilds resume`

These allow the suspension of all prebuilds activity, intended for use
if prebuilds are misbehaving.
This commit is contained in:
Sas Swart
2025-07-02 17:05:42 +02:00
committed by GitHub
parent 4072d228c5
commit 01163ea57b
42 changed files with 1336 additions and 4 deletions

View File

@ -2304,6 +2304,10 @@ func (q *querier) GetPrebuildMetrics(ctx context.Context) ([]database.GetPrebuil
return q.db.GetPrebuildMetrics(ctx)
}
func (q *querier) GetPrebuildsSettings(ctx context.Context) (string, error) {
return q.db.GetPrebuildsSettings(ctx)
}
func (q *querier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) {
empty := database.GetPresetByIDRow{}
@ -5101,6 +5105,13 @@ func (q *querier) UpsertOAuthSigningKey(ctx context.Context, value string) error
return q.db.UpsertOAuthSigningKey(ctx, value)
}
func (q *querier) UpsertPrebuildsSettings(ctx context.Context, value string) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return err
}
return q.db.UpsertPrebuildsSettings(ctx, value)
}
func (q *querier) UpsertProvisionerDaemon(ctx context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) {
res := rbac.ResourceProvisionerDaemon.InOrg(arg.OrganizationID)
if arg.Tags[provisionersdk.TagScope] == provisionersdk.ScopeUser {

View File

@ -5071,6 +5071,12 @@ func (s *MethodTestSuite) TestPrebuilds() {
check.Args().
Asserts(rbac.ResourceWorkspace.All(), policy.ActionRead)
}))
s.Run("GetPrebuildsSettings", s.Subtest(func(db database.Store, check *expects) {
check.Args().Asserts()
}))
s.Run("UpsertPrebuildsSettings", s.Subtest(func(db database.Store, check *expects) {
check.Args("foo").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
}))
s.Run("CountInProgressPrebuilds", s.Subtest(func(_ database.Store, check *expects) {
check.Args().
Asserts(rbac.ResourceWorkspace.All(), policy.ActionRead).

View File

@ -297,6 +297,7 @@ type data struct {
presets []database.TemplateVersionPreset
presetParameters []database.TemplateVersionPresetParameter
presetPrebuildSchedules []database.TemplateVersionPresetPrebuildSchedule
prebuildsSettings []byte
}
func tryPercentileCont(fs []float64, p float64) float64 {
@ -4277,7 +4278,14 @@ func (*FakeQuerier) GetPrebuildMetrics(_ context.Context) ([]database.GetPrebuil
return make([]database.GetPrebuildMetricsRow, 0), nil
}
func (q *FakeQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) {
func (q *FakeQuerier) GetPrebuildsSettings(_ context.Context) (string, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
return string(slices.Clone(q.prebuildsSettings)), nil
}
func (q *FakeQuerier) GetPresetByID(_ context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -12316,6 +12324,14 @@ func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) err
return nil
}
func (q *FakeQuerier) UpsertPrebuildsSettings(_ context.Context, value string) error {
q.mutex.Lock()
defer q.mutex.Unlock()
q.prebuildsSettings = []byte(value)
return nil
}
func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) {
if err := validateDatabaseType(arg); err != nil {
return database.ProvisionerDaemon{}, err

View File

@ -1103,6 +1103,13 @@ func (m queryMetricsStore) GetPrebuildMetrics(ctx context.Context) ([]database.G
return r0, r1
}
func (m queryMetricsStore) GetPrebuildsSettings(ctx context.Context) (string, error) {
start := time.Now()
r0, r1 := m.s.GetPrebuildsSettings(ctx)
m.queryLatencies.WithLabelValues("GetPrebuildsSettings").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetPresetByID(ctx context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) {
start := time.Now()
r0, r1 := m.s.GetPresetByID(ctx, presetID)
@ -3175,6 +3182,13 @@ func (m queryMetricsStore) UpsertOAuthSigningKey(ctx context.Context, value stri
return r0
}
func (m queryMetricsStore) UpsertPrebuildsSettings(ctx context.Context, value string) error {
start := time.Now()
r0 := m.s.UpsertPrebuildsSettings(ctx, value)
m.queryLatencies.WithLabelValues("UpsertPrebuildsSettings").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) UpsertProvisionerDaemon(ctx context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) {
start := time.Now()
r0, r1 := m.s.UpsertProvisionerDaemon(ctx, arg)

View File

@ -2283,6 +2283,21 @@ func (mr *MockStoreMockRecorder) GetPrebuildMetrics(ctx any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrebuildMetrics", reflect.TypeOf((*MockStore)(nil).GetPrebuildMetrics), ctx)
}
// GetPrebuildsSettings mocks base method.
func (m *MockStore) GetPrebuildsSettings(ctx context.Context) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPrebuildsSettings", ctx)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPrebuildsSettings indicates an expected call of GetPrebuildsSettings.
func (mr *MockStoreMockRecorder) GetPrebuildsSettings(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrebuildsSettings", reflect.TypeOf((*MockStore)(nil).GetPrebuildsSettings), ctx)
}
// GetPresetByID mocks base method.
func (m *MockStore) GetPresetByID(ctx context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) {
m.ctrl.T.Helper()
@ -6719,6 +6734,20 @@ func (mr *MockStoreMockRecorder) UpsertOAuthSigningKey(ctx, value any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertOAuthSigningKey", reflect.TypeOf((*MockStore)(nil).UpsertOAuthSigningKey), ctx, value)
}
// UpsertPrebuildsSettings mocks base method.
func (m *MockStore) UpsertPrebuildsSettings(ctx context.Context, value string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpsertPrebuildsSettings", ctx, value)
ret0, _ := ret[0].(error)
return ret0
}
// UpsertPrebuildsSettings indicates an expected call of UpsertPrebuildsSettings.
func (mr *MockStoreMockRecorder) UpsertPrebuildsSettings(ctx, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertPrebuildsSettings", reflect.TypeOf((*MockStore)(nil).UpsertPrebuildsSettings), ctx, value)
}
// UpsertProvisionerDaemon mocks base method.
func (m *MockStore) UpsertProvisionerDaemon(ctx context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) {
m.ctrl.T.Helper()

View File

@ -242,7 +242,8 @@ CREATE TYPE resource_type AS ENUM (
'idp_sync_settings_group',
'idp_sync_settings_role',
'workspace_agent',
'workspace_app'
'workspace_app',
'prebuilds_settings'
);
CREATE TYPE startup_script_behavior AS ENUM (

View File

@ -0,0 +1 @@
-- No-op, enum values can't be dropped.

View File

@ -0,0 +1,2 @@
ALTER TYPE resource_type
ADD VALUE IF NOT EXISTS 'prebuilds_settings';

View File

@ -1894,6 +1894,7 @@ const (
ResourceTypeIdpSyncSettingsRole ResourceType = "idp_sync_settings_role"
ResourceTypeWorkspaceAgent ResourceType = "workspace_agent"
ResourceTypeWorkspaceApp ResourceType = "workspace_app"
ResourceTypePrebuildsSettings ResourceType = "prebuilds_settings"
)
func (e *ResourceType) Scan(src interface{}) error {
@ -1956,7 +1957,8 @@ func (e ResourceType) Valid() bool {
ResourceTypeIdpSyncSettingsGroup,
ResourceTypeIdpSyncSettingsRole,
ResourceTypeWorkspaceAgent,
ResourceTypeWorkspaceApp:
ResourceTypeWorkspaceApp,
ResourceTypePrebuildsSettings:
return true
}
return false
@ -1988,6 +1990,7 @@ func AllResourceTypeValues() []ResourceType {
ResourceTypeIdpSyncSettingsRole,
ResourceTypeWorkspaceAgent,
ResourceTypeWorkspaceApp,
ResourceTypePrebuildsSettings,
}
}

View File

@ -236,6 +236,7 @@ type sqlcQuerier interface {
GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error)
GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error)
GetPrebuildMetrics(ctx context.Context) ([]GetPrebuildMetricsRow, error)
GetPrebuildsSettings(ctx context.Context) (string, error)
GetPresetByID(ctx context.Context, presetID uuid.UUID) (GetPresetByIDRow, error)
GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (TemplateVersionPreset, error)
GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]TemplateVersionPresetParameter, error)
@ -651,6 +652,7 @@ type sqlcQuerier interface {
UpsertNotificationsSettings(ctx context.Context, value string) error
UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error
UpsertOAuthSigningKey(ctx context.Context, value string) error
UpsertPrebuildsSettings(ctx context.Context, value string) error
UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error)
UpsertRuntimeConfig(ctx context.Context, arg UpsertRuntimeConfigParams) error
UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error)

View File

@ -9604,6 +9604,18 @@ func (q *sqlQuerier) GetOAuthSigningKey(ctx context.Context) (string, error) {
return value, err
}
const getPrebuildsSettings = `-- name: GetPrebuildsSettings :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'prebuilds_settings'), '{}') :: text AS prebuilds_settings
`
func (q *sqlQuerier) GetPrebuildsSettings(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getPrebuildsSettings)
var prebuilds_settings string
err := row.Scan(&prebuilds_settings)
return prebuilds_settings, err
}
const getRuntimeConfig = `-- name: GetRuntimeConfig :one
SELECT value FROM site_configs WHERE site_configs.key = $1
`
@ -9786,6 +9798,16 @@ func (q *sqlQuerier) UpsertOAuthSigningKey(ctx context.Context, value string) er
return err
}
const upsertPrebuildsSettings = `-- name: UpsertPrebuildsSettings :exec
INSERT INTO site_configs (key, value) VALUES ('prebuilds_settings', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'prebuilds_settings'
`
func (q *sqlQuerier) UpsertPrebuildsSettings(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertPrebuildsSettings, value)
return err
}
const upsertRuntimeConfig = `-- name: UpsertRuntimeConfig :exec
INSERT INTO site_configs (key, value) VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET value = $2 WHERE site_configs.key = $1

View File

@ -96,6 +96,15 @@ SELECT
INSERT INTO site_configs (key, value) VALUES ('notifications_settings', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'notifications_settings';
-- name: GetPrebuildsSettings :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'prebuilds_settings'), '{}') :: text AS prebuilds_settings
;
-- name: UpsertPrebuildsSettings :exec
INSERT INTO site_configs (key, value) VALUES ('prebuilds_settings', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'prebuilds_settings';
-- name: GetRuntimeConfig :one
SELECT value FROM site_configs WHERE site_configs.key = $1;

View File

@ -35,6 +35,11 @@ type NotificationsSettings struct {
NotifierPaused bool `db:"notifier_paused" json:"notifier_paused"`
}
type PrebuildsSettings struct {
ID uuid.UUID `db:"id" json:"id"`
ReconciliationPaused bool `db:"reconciliation_paused" json:"reconciliation_paused"`
}
type Actions []policy.Action
func (a *Actions) Scan(src interface{}) error {