diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 72c1c2bee7334..e969ba780bcd3 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -447,15 +447,15 @@ USER QUIET HOURS SCHEDULE OPTIONS: Allow users to set quiet hours schedules each day for workspaces to avoid workspaces stopping during the day due to template max TTL. - --default-quiet-hours-schedule string, $CODER_QUIET_HOURS_DEFAULT_SCHEDULE + --default-quiet-hours-schedule string, $CODER_QUIET_HOURS_DEFAULT_SCHEDULE (default: CRON_TZ=UTC 0 0 * * *) The default daily cron schedule applied to users that haven't set a custom quiet hours schedule themselves. The quiet hours schedule determines when workspaces will be force stopped due to the template's - max TTL, and will round the max TTL up to be within the user's quiet - hours window (or default). The format is the same as the standard cron - format, but the day-of-month, month and day-of-week must be *. Only - one hour and minute can be specified (ranges or comma separated values - are not supported). + autostop requirement, and will round the max deadline up to be within + the user's quiet hours window (or default). The format is the same as + the standard cron format, but the day-of-month, month and day-of-week + must be *. Only one hour and minute can be specified (ranges or comma + separated values are not supported). ⚠️ DANGEROUS OPTIONS: --dangerous-allow-path-app-sharing bool, $CODER_DANGEROUS_ALLOW_PATH_APP_SHARING diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 2346f73d112d1..5b5a48dfaffeb 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -450,10 +450,10 @@ wgtunnelHost: "" userQuietHoursSchedule: # The default daily cron schedule applied to users that haven't set a custom quiet # hours schedule themselves. The quiet hours schedule determines when workspaces - # will be force stopped due to the template's max TTL, and will round the max TTL - # up to be within the user's quiet hours window (or default). The format is the - # same as the standard cron format, but the day-of-month, month and day-of-week - # must be *. Only one hour and minute can be specified (ranges or comma separated - # values are not supported). - # (default: , type: string) - defaultQuietHoursSchedule: "" + # will be force stopped due to the template's autostop requirement, and will round + # the max deadline up to be within the user's quiet hours window (or default). The + # format is the same as the standard cron format, but the day-of-month, month and + # day-of-week must be *. Only one hour and minute can be specified (ranges or + # comma separated values are not supported). + # (default: CRON_TZ=UTC 0 0 * * *, type: string) + defaultQuietHoursSchedule: CRON_TZ=UTC 0 0 * * * diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index b9d66219a7229..1cdbea6ee2e3c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8031,7 +8031,7 @@ const docTemplate = `{ ] }, "autostop_requirement": { - "description": "AutostopRequirement allows optionally specifying the autostop requirement\nfor workspaces created from this template. This is an enterprise feature.", + "description": "AutostopRequirement allows optionally specifying the autostop requirement\nfor workspaces created from this template. This is an enterprise feature.\nOnly one of MaxTTLMillis or AutostopRequirement can be specified.", "allOf": [ { "$ref": "#/definitions/codersdk.TemplateAutostopRequirement" @@ -8071,7 +8071,7 @@ const docTemplate = `{ "type": "string" }, "max_ttl_ms": { - "description": "TODO(@dean): remove max_ttl once autostop_requirement is matured", + "description": "TODO(@dean): remove max_ttl once autostop_requirement is matured\nOnly one of MaxTTLMillis or AutostopRequirement can be specified.", "type": "integer" }, "name": { @@ -8805,7 +8805,6 @@ const docTemplate = `{ "workspace_actions", "tailnet_pg_coordinator", "single_tailnet", - "template_autostop_requirement", "deployment_health_page", "template_update_policies" ], @@ -8814,7 +8813,6 @@ const docTemplate = `{ "ExperimentWorkspaceActions", "ExperimentTailnetPGCoordinator", "ExperimentSingleTailnet", - "ExperimentTemplateAutostopRequirement", "ExperimentDeploymentHealthPage", "ExperimentTemplateUpdatePolicies" ] @@ -10378,6 +10376,10 @@ const docTemplate = `{ "updated_at": { "type": "string", "format": "date-time" + }, + "use_max_ttl": { + "description": "UseMaxTTL picks whether to use the deprecated max TTL for the template or\nthe new autostop requirement.", + "type": "boolean" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index bc1d67918912c..68cd8ed8257f7 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7152,7 +7152,7 @@ ] }, "autostop_requirement": { - "description": "AutostopRequirement allows optionally specifying the autostop requirement\nfor workspaces created from this template. This is an enterprise feature.", + "description": "AutostopRequirement allows optionally specifying the autostop requirement\nfor workspaces created from this template. This is an enterprise feature.\nOnly one of MaxTTLMillis or AutostopRequirement can be specified.", "allOf": [ { "$ref": "#/definitions/codersdk.TemplateAutostopRequirement" @@ -7192,7 +7192,7 @@ "type": "string" }, "max_ttl_ms": { - "description": "TODO(@dean): remove max_ttl once autostop_requirement is matured", + "description": "TODO(@dean): remove max_ttl once autostop_requirement is matured\nOnly one of MaxTTLMillis or AutostopRequirement can be specified.", "type": "integer" }, "name": { @@ -7885,7 +7885,6 @@ "workspace_actions", "tailnet_pg_coordinator", "single_tailnet", - "template_autostop_requirement", "deployment_health_page", "template_update_policies" ], @@ -7894,7 +7893,6 @@ "ExperimentWorkspaceActions", "ExperimentTailnetPGCoordinator", "ExperimentSingleTailnet", - "ExperimentTemplateAutostopRequirement", "ExperimentDeploymentHealthPage", "ExperimentTemplateUpdatePolicies" ] @@ -9374,6 +9372,10 @@ "updated_at": { "type": "string", "format": "date-time" + }, + "use_max_ttl": { + "description": "UseMaxTTL picks whether to use the deprecated max TTL for the template or\nthe new autostop requirement.", + "type": "boolean" } } }, diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 6ea5344294a18..2e85bda1ffcbd 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -6158,6 +6158,7 @@ func (q *FakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database tpl.AllowUserAutostop = arg.AllowUserAutostop tpl.UpdatedAt = dbtime.Now() tpl.DefaultTTL = arg.DefaultTTL + tpl.UseMaxTtl = arg.UseMaxTtl tpl.MaxTTL = arg.MaxTTL tpl.AutostopRequirementDaysOfWeek = arg.AutostopRequirementDaysOfWeek tpl.AutostopRequirementWeeks = arg.AutostopRequirementWeeks diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index b555970c5884e..b2f03e01830c8 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -814,7 +814,8 @@ CREATE TABLE templates ( autostop_requirement_weeks bigint DEFAULT 0 NOT NULL, autostart_block_days_of_week smallint DEFAULT 0 NOT NULL, require_active_version boolean DEFAULT false NOT NULL, - deprecated text DEFAULT ''::text NOT NULL + deprecated text DEFAULT ''::text NOT NULL, + use_max_ttl boolean DEFAULT false NOT NULL ); COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.'; @@ -863,6 +864,7 @@ CREATE VIEW template_with_users AS templates.autostart_block_days_of_week, templates.require_active_version, templates.deprecated, + templates.use_max_ttl, COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url, COALESCE(visible_users.username, ''::text) AS created_by_username FROM (public.templates diff --git a/coderd/database/migrations/000180_template_use_max_ttl.down.sql b/coderd/database/migrations/000180_template_use_max_ttl.down.sql new file mode 100644 index 0000000000000..5d2359e7814d9 --- /dev/null +++ b/coderd/database/migrations/000180_template_use_max_ttl.down.sql @@ -0,0 +1,19 @@ +DROP VIEW template_with_users; + +ALTER TABLE templates DROP COLUMN use_max_ttl; + +CREATE VIEW + template_with_users +AS + SELECT + templates.*, + coalesce(visible_users.avatar_url, '') AS created_by_avatar_url, + coalesce(visible_users.username, '') AS created_by_username + FROM + templates + LEFT JOIN + visible_users + ON + templates.created_by = visible_users.id; + +COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.'; diff --git a/coderd/database/migrations/000180_template_use_max_ttl.up.sql b/coderd/database/migrations/000180_template_use_max_ttl.up.sql new file mode 100644 index 0000000000000..811875de63f81 --- /dev/null +++ b/coderd/database/migrations/000180_template_use_max_ttl.up.sql @@ -0,0 +1,28 @@ +-- Add column with default true, so existing templates will function as usual +ALTER TABLE templates ADD COLUMN use_max_ttl boolean NOT NULL DEFAULT true; + +-- Find any templates with autostop_requirement_days_of_week set and set them to +-- use_max_ttl = false +UPDATE templates SET use_max_ttl = false WHERE autostop_requirement_days_of_week != 0; + +-- Alter column to default false, because we want autostop_requirement to be the +-- default from now on +ALTER TABLE templates ALTER COLUMN use_max_ttl SET DEFAULT false; + +DROP VIEW template_with_users; + +CREATE VIEW + template_with_users +AS + SELECT + templates.*, + coalesce(visible_users.avatar_url, '') AS created_by_avatar_url, + coalesce(visible_users.username, '') AS created_by_username + FROM + templates + LEFT JOIN + visible_users + ON + templates.created_by = visible_users.id; + +COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.'; diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 945b02c744f5c..81375e66c88c5 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -89,6 +89,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate &i.AutostartBlockDaysOfWeek, &i.RequireActiveVersion, &i.Deprecated, + &i.UseMaxTtl, &i.CreatedByAvatarURL, &i.CreatedByUsername, ); err != nil { diff --git a/coderd/database/models.go b/coderd/database/models.go index 1fd9f241fa417..1a6c5bed8d1a7 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1973,6 +1973,7 @@ type Template struct { AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"` RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"` Deprecated string `db:"deprecated" json:"deprecated"` + UseMaxTtl bool `db:"use_max_ttl" json:"use_max_ttl"` CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"` CreatedByUsername string `db:"created_by_username" json:"created_by_username"` } @@ -2014,6 +2015,7 @@ type TemplateTable struct { RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"` // If set to a non empty string, the template will no longer be able to be used. The message will be displayed to the user. Deprecated string `db:"deprecated" json:"deprecated"` + UseMaxTtl bool `db:"use_max_ttl" json:"use_max_ttl"` } // Joins in the username + avatar url of the created by user. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ceaff55ef64aa..e0c4c7ce6d26b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5354,7 +5354,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem const getTemplateByID = `-- name: GetTemplateByID :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, created_by_avatar_url, created_by_username + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, created_by_avatar_url, created_by_username FROM template_with_users WHERE @@ -5394,6 +5394,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat &i.AutostartBlockDaysOfWeek, &i.RequireActiveVersion, &i.Deprecated, + &i.UseMaxTtl, &i.CreatedByAvatarURL, &i.CreatedByUsername, ) @@ -5402,7 +5403,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, created_by_avatar_url, created_by_username + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, created_by_avatar_url, created_by_username FROM template_with_users AS templates WHERE @@ -5450,6 +5451,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G &i.AutostartBlockDaysOfWeek, &i.RequireActiveVersion, &i.Deprecated, + &i.UseMaxTtl, &i.CreatedByAvatarURL, &i.CreatedByUsername, ) @@ -5457,7 +5459,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G } const getTemplates = `-- name: GetTemplates :many -SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, created_by_avatar_url, created_by_username FROM template_with_users AS templates +SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, created_by_avatar_url, created_by_username FROM template_with_users AS templates ORDER BY (name, id) ASC ` @@ -5498,6 +5500,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { &i.AutostartBlockDaysOfWeek, &i.RequireActiveVersion, &i.Deprecated, + &i.UseMaxTtl, &i.CreatedByAvatarURL, &i.CreatedByUsername, ); err != nil { @@ -5516,7 +5519,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, created_by_avatar_url, created_by_username + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, created_by_avatar_url, created_by_username FROM template_with_users AS templates WHERE @@ -5607,6 +5610,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate &i.AutostartBlockDaysOfWeek, &i.RequireActiveVersion, &i.Deprecated, + &i.UseMaxTtl, &i.CreatedByAvatarURL, &i.CreatedByUsername, ); err != nil { @@ -5811,13 +5815,14 @@ SET allow_user_autostart = $3, allow_user_autostop = $4, default_ttl = $5, - max_ttl = $6, - autostop_requirement_days_of_week = $7, - autostop_requirement_weeks = $8, - autostart_block_days_of_week = $9, - failure_ttl = $10, - time_til_dormant = $11, - time_til_dormant_autodelete = $12 + use_max_ttl = $6, + max_ttl = $7, + autostop_requirement_days_of_week = $8, + autostop_requirement_weeks = $9, + autostart_block_days_of_week = $10, + failure_ttl = $11, + time_til_dormant = $12, + time_til_dormant_autodelete = $13 WHERE id = $1 ` @@ -5828,6 +5833,7 @@ type UpdateTemplateScheduleByIDParams struct { AllowUserAutostart bool `db:"allow_user_autostart" json:"allow_user_autostart"` AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"` DefaultTTL int64 `db:"default_ttl" json:"default_ttl"` + UseMaxTtl bool `db:"use_max_ttl" json:"use_max_ttl"` MaxTTL int64 `db:"max_ttl" json:"max_ttl"` AutostopRequirementDaysOfWeek int16 `db:"autostop_requirement_days_of_week" json:"autostop_requirement_days_of_week"` AutostopRequirementWeeks int64 `db:"autostop_requirement_weeks" json:"autostop_requirement_weeks"` @@ -5844,6 +5850,7 @@ func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateT arg.AllowUserAutostart, arg.AllowUserAutostop, arg.DefaultTTL, + arg.UseMaxTtl, arg.MaxTTL, arg.AutostopRequirementDaysOfWeek, arg.AutostopRequirementWeeks, diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index 87d827b0daea6..af8c3fe80f420 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -128,13 +128,14 @@ SET allow_user_autostart = $3, allow_user_autostop = $4, default_ttl = $5, - max_ttl = $6, - autostop_requirement_days_of_week = $7, - autostop_requirement_weeks = $8, - autostart_block_days_of_week = $9, - failure_ttl = $10, - time_til_dormant = $11, - time_til_dormant_autodelete = $12 + use_max_ttl = $6, + max_ttl = $7, + autostop_requirement_days_of_week = $8, + autostop_requirement_weeks = $9, + autostart_block_days_of_week = $10, + failure_ttl = $11, + time_til_dormant = $12, + time_til_dormant_autodelete = $13 WHERE id = $1 ; diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index beae639e627ed..3aff6d4d804d3 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1113,11 +1113,11 @@ func TestCompleteJob(t *testing.T) { var store schedule.TemplateScheduleStore = schedule.MockTemplateScheduleStore{ GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) { return schedule.TemplateScheduleOptions{ - UserAutostartEnabled: false, - UserAutostopEnabled: c.templateAllowAutostop, - DefaultTTL: c.templateDefaultTTL, - MaxTTL: c.templateMaxTTL, - UseAutostopRequirement: false, + UserAutostartEnabled: false, + UserAutostopEnabled: c.templateAllowAutostop, + DefaultTTL: c.templateDefaultTTL, + MaxTTL: c.templateMaxTTL, + UseMaxTTL: true, }, nil }, } @@ -1333,11 +1333,11 @@ func TestCompleteJob(t *testing.T) { var templateScheduleStore schedule.TemplateScheduleStore = schedule.MockTemplateScheduleStore{ GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) { return schedule.TemplateScheduleOptions{ - UserAutostartEnabled: false, - UserAutostopEnabled: true, - DefaultTTL: 0, - UseAutostopRequirement: true, - AutostopRequirement: c.templateAutostopRequirement, + UserAutostartEnabled: false, + UserAutostopEnabled: true, + DefaultTTL: 0, + UseMaxTTL: false, + AutostopRequirement: c.templateAutostopRequirement, }, nil }, } diff --git a/coderd/schedule/autostop.go b/coderd/schedule/autostop.go index 8cf645b52f5de..6ba3e27dfa65d 100644 --- a/coderd/schedule/autostop.go +++ b/coderd/schedule/autostop.go @@ -112,13 +112,12 @@ func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (Aut // Use the old algorithm for calculating max_deadline if the instance isn't // configured or entitled to use the new feature flag yet. - // TODO(@dean): remove this once the feature flag is enabled for all - if !templateSchedule.UseAutostopRequirement && templateSchedule.MaxTTL > 0 { + if templateSchedule.UseMaxTTL && templateSchedule.MaxTTL > 0 { autostop.MaxDeadline = now.Add(templateSchedule.MaxTTL) } - // TODO(@dean): remove extra conditional - if templateSchedule.UseAutostopRequirement && templateSchedule.AutostopRequirement.DaysOfWeek != 0 { + // Otherwise, use the autostop_requirement algorithm. + if !templateSchedule.UseMaxTTL && templateSchedule.AutostopRequirement.DaysOfWeek != 0 { // The template has a autostop requirement, so determine the max deadline // of this workspace build. @@ -130,8 +129,8 @@ func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (Aut } // If the schedule is nil, that means the deployment isn't entitled to - // use quiet hours or the default schedule has not been set. In this - // case, do not set a max deadline on the workspace. + // use quiet hours. In this case, do not set a max deadline on the + // workspace. if userQuietHoursSchedule.Schedule != nil { loc := userQuietHoursSchedule.Schedule.Location() now := now.In(loc) diff --git a/coderd/schedule/autostop_test.go b/coderd/schedule/autostop_test.go index cafe2b413eaed..5ea665726a7c4 100644 --- a/coderd/schedule/autostop_test.go +++ b/coderd/schedule/autostop_test.go @@ -415,12 +415,12 @@ func TestCalculateAutoStop(t *testing.T) { templateScheduleStore := schedule.MockTemplateScheduleStore{ GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) { return schedule.TemplateScheduleOptions{ - UserAutostartEnabled: false, - UserAutostopEnabled: c.templateAllowAutostop, - DefaultTTL: c.templateDefaultTTL, - MaxTTL: c.templateMaxTTL, - UseAutostopRequirement: !c.useMaxTTL, - AutostopRequirement: c.templateAutostopRequirement, + UserAutostartEnabled: false, + UserAutostopEnabled: c.templateAllowAutostop, + DefaultTTL: c.templateDefaultTTL, + MaxTTL: c.templateMaxTTL, + UseMaxTTL: c.useMaxTTL, + AutostopRequirement: c.templateAutostopRequirement, }, nil }, } diff --git a/coderd/schedule/template.go b/coderd/schedule/template.go index fc274d9a7d8ba..4872b89ef9d83 100644 --- a/coderd/schedule/template.go +++ b/coderd/schedule/template.go @@ -117,14 +117,12 @@ type TemplateScheduleOptions struct { UserAutostartEnabled bool `json:"user_autostart_enabled"` UserAutostopEnabled bool `json:"user_autostop_enabled"` DefaultTTL time.Duration `json:"default_ttl"` - // TODO(@dean): remove MaxTTL once autostop_requirement is matured and the - // default - MaxTTL time.Duration `json:"max_ttl"` - // UseAutostopRequirement dictates whether the autostop requirement should - // be used instead of MaxTTL. This is governed by the feature flag and - // licensing. + MaxTTL time.Duration `json:"max_ttl"` + // UseMaxTTL dictates whether the max_ttl should be used instead of + // autostop_requirement for this template. This is governed by the template + // and licensing. // TODO(@dean): remove this when we remove max_tll - UseAutostopRequirement bool + UseMaxTTL bool // AutostopRequirement dictates when the workspace must be restarted. This // used to be handled by MaxTTL. AutostopRequirement TemplateAutostopRequirement `json:"autostop_requirement"` @@ -185,8 +183,8 @@ func (*agplTemplateScheduleStore) Get(ctx context.Context, db database.Store, te DefaultTTL: time.Duration(tpl.DefaultTTL), // Disregard the values in the database, since AutostopRequirement, // FailureTTL, TimeTilDormant, and TimeTilDormantAutoDelete are enterprise features. - UseAutostopRequirement: false, - MaxTTL: 0, + UseMaxTTL: false, + MaxTTL: 0, AutostartRequirement: TemplateAutostartRequirement{ // Default to allowing all days for AGPL DaysOfWeek: 0b01111111, @@ -220,6 +218,7 @@ func (*agplTemplateScheduleStore) Set(ctx context.Context, db database.Store, tp DefaultTTL: int64(opts.DefaultTTL), // Don't allow changing these settings, but keep the value in the DB (to // avoid clearing settings if the license has an issue). + UseMaxTtl: tpl.UseMaxTtl, MaxTTL: tpl.MaxTTL, AutostopRequirementDaysOfWeek: tpl.AutostopRequirementDaysOfWeek, AutostopRequirementWeeks: tpl.AutostopRequirementWeeks, diff --git a/coderd/templates.go b/coderd/templates.go index 9ac8c2d85d5aa..5e6d9644a782f 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -223,8 +223,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque } var ( - defaultTTL time.Duration - // TODO(@dean): remove max_ttl once autostop_requirement is ready + defaultTTL time.Duration maxTTL time.Duration autostopRequirementDaysOfWeek []string autostartRequirementDaysOfWeek []string @@ -285,6 +284,9 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque if createTemplate.MaxTTLMillis != nil { maxTTL = time.Duration(*createTemplate.MaxTTLMillis) * time.Millisecond } + if maxTTL != 0 && len(autostopRequirementDaysOfWeek) > 0 { + validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.days_of_week", Detail: "Cannot be set if max_ttl_ms is set."}) + } if autostopRequirementWeeks < 0 { validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: "Must be a positive integer."}) } @@ -364,6 +366,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque dbTemplate, err = (*api.TemplateScheduleStore.Load()).Set(ctx, tx, dbTemplate, schedule.TemplateScheduleOptions{ UserAutostartEnabled: allowUserAutostart, UserAutostopEnabled: allowUserAutostop, + UseMaxTTL: maxTTL > 0, DefaultTTL: defaultTTL, MaxTTL: maxTTL, // Some of these values are enterprise-only, but the @@ -568,6 +571,10 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { if req.MaxTTLMillis != 0 && req.DefaultTTLMillis > req.MaxTTLMillis { validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be less than or equal to max_ttl_ms if max_ttl_ms is set."}) } + if req.MaxTTLMillis != 0 && req.AutostopRequirement != nil && len(req.AutostopRequirement.DaysOfWeek) > 0 { + validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.days_of_week", Detail: "Cannot be set if max_ttl_ms is set."}) + } + useMaxTTL := req.MaxTTLMillis > 0 if req.AutostopRequirement == nil { req.AutostopRequirement = &codersdk.TemplateAutostopRequirement{ DaysOfWeek: codersdk.BitmapToWeekdays(scheduleOpts.AutostopRequirement.DaysOfWeek), @@ -641,6 +648,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { req.AllowUserAutostop == template.AllowUserAutostop && req.AllowUserCancelWorkspaceJobs == template.AllowUserCancelWorkspaceJobs && req.DefaultTTLMillis == time.Duration(template.DefaultTTL).Milliseconds() && + useMaxTTL == scheduleOpts.UseMaxTTL && req.MaxTTLMillis == time.Duration(template.MaxTTL).Milliseconds() && autostopRequirementDaysOfWeekParsed == scheduleOpts.AutostopRequirement.DaysOfWeek && autostartRequirementDaysOfWeekParsed == scheduleOpts.AutostartRequirement.DaysOfWeek && @@ -695,6 +703,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { timeTilDormantAutoDelete := time.Duration(req.TimeTilDormantAutoDeleteMillis) * time.Millisecond if defaultTTL != time.Duration(template.DefaultTTL) || + useMaxTTL != scheduleOpts.UseMaxTTL || maxTTL != time.Duration(template.MaxTTL) || autostopRequirementDaysOfWeekParsed != scheduleOpts.AutostopRequirement.DaysOfWeek || autostartRequirementDaysOfWeekParsed != scheduleOpts.AutostartRequirement.DaysOfWeek || @@ -711,6 +720,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { UserAutostartEnabled: req.AllowUserAutostart, UserAutostopEnabled: req.AllowUserAutostop, DefaultTTL: defaultTTL, + UseMaxTTL: useMaxTTL, MaxTTL: maxTTL, AutostopRequirement: schedule.TemplateAutostopRequirement{ DaysOfWeek: autostopRequirementDaysOfWeekParsed, @@ -859,6 +869,7 @@ func (api *API) convertTemplate( Description: template.Description, Icon: template.Icon, DefaultTTLMillis: time.Duration(template.DefaultTTL).Milliseconds(), + UseMaxTTL: template.UseMaxTtl, MaxTTLMillis: time.Duration(template.MaxTTL).Milliseconds(), CreatedByID: template.CreatedBy, CreatedByName: template.CreatedByUsername, diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 47154d1511ff0..fc3e8d07c3c97 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -268,6 +268,7 @@ func TestPostTemplateByOrganization(t *testing.T) { AllowUserAutostart: options.UserAutostartEnabled, AllowUserAutostop: options.UserAutostopEnabled, DefaultTTL: int64(options.DefaultTTL), + UseMaxTtl: options.UseMaxTTL, MaxTTL: int64(options.MaxTTL), AutostopRequirementDaysOfWeek: int16(options.AutostopRequirement.DaysOfWeek), AutostopRequirementWeeks: options.AutostopRequirement.Weeks, @@ -296,6 +297,7 @@ func TestPostTemplateByOrganization(t *testing.T) { }) require.NoError(t, err) + require.False(t, got.UseMaxTTL) // default require.EqualValues(t, 1, atomic.LoadInt64(&setCalled)) require.Empty(t, got.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 1, got.AutostopRequirement.Weeks) @@ -318,6 +320,7 @@ func TestPostTemplateByOrganization(t *testing.T) { AllowUserAutostart: options.UserAutostartEnabled, AllowUserAutostop: options.UserAutostopEnabled, DefaultTTL: int64(options.DefaultTTL), + UseMaxTtl: options.UseMaxTTL, MaxTTL: int64(options.MaxTTL), AutostopRequirementDaysOfWeek: int16(options.AutostopRequirement.DaysOfWeek), AutostopRequirementWeeks: options.AutostopRequirement.Weeks, @@ -351,11 +354,13 @@ func TestPostTemplateByOrganization(t *testing.T) { require.NoError(t, err) require.EqualValues(t, 1, atomic.LoadInt64(&setCalled)) + require.False(t, got.UseMaxTTL) require.Equal(t, []string{"friday", "saturday"}, got.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 2, got.AutostopRequirement.Weeks) got, err = client.Template(ctx, got.ID) require.NoError(t, err) + require.False(t, got.UseMaxTTL) require.Equal(t, []string{"friday", "saturday"}, got.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 2, got.AutostopRequirement.Weeks) }) @@ -380,10 +385,36 @@ func TestPostTemplateByOrganization(t *testing.T) { }) require.NoError(t, err) // ignored and use AGPL defaults + require.False(t, got.UseMaxTTL) require.Empty(t, got.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 1, got.AutostopRequirement.Weeks) }) }) + + t.Run("BothMaxTTLAndAutostopRequirement", func(t *testing.T) { + t.Parallel() + + // Fake template schedule store is unneeded for this test since the + // route fails before it is called. + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{ + Name: "testing", + VersionID: version.ID, + MaxTTLMillis: ptr.Ref(24 * time.Hour.Milliseconds()), + AutostopRequirement: &codersdk.TemplateAutostopRequirement{ + DaysOfWeek: []string{"friday", "saturday"}, + Weeks: 2, + }, + }) + require.Error(t, err) + require.ErrorContains(t, err, "max_ttl_ms") + }) } func TestTemplatesByOrganization(t *testing.T) { @@ -677,6 +708,7 @@ func TestPatchTemplateMeta(t *testing.T) { AllowUserAutostop: options.UserAutostopEnabled, DefaultTTL: int64(options.DefaultTTL), MaxTTL: int64(options.MaxTTL), + UseMaxTtl: options.UseMaxTTL, AutostopRequirementDaysOfWeek: int16(options.AutostopRequirement.DaysOfWeek), AutostopRequirementWeeks: options.AutostopRequirement.Weeks, FailureTTL: int64(options.FailureTTL), @@ -1073,6 +1105,7 @@ func TestPatchTemplateMeta(t *testing.T) { AllowUserAutostart: options.UserAutostartEnabled, AllowUserAutostop: options.UserAutostopEnabled, DefaultTTL: int64(options.DefaultTTL), + UseMaxTtl: options.UseMaxTTL, MaxTTL: int64(options.MaxTTL), AutostopRequirementDaysOfWeek: int16(options.AutostopRequirement.DaysOfWeek), AutostopRequirementWeeks: options.AutostopRequirement.Weeks, @@ -1144,6 +1177,7 @@ func TestPatchTemplateMeta(t *testing.T) { AllowUserAutostart: options.UserAutostartEnabled, AllowUserAutostop: options.UserAutostopEnabled, DefaultTTL: int64(options.DefaultTTL), + UseMaxTtl: options.UseMaxTTL, MaxTTL: int64(options.MaxTTL), AutostopRequirementDaysOfWeek: int16(options.AutostopRequirement.DaysOfWeek), AutostopRequirementWeeks: options.AutostopRequirement.Weeks, @@ -1238,6 +1272,38 @@ func TestPatchTemplateMeta(t *testing.T) { require.False(t, template.Deprecated) }) }) + + t.Run("BothMaxTTLAndAutostopRequirement", func(t *testing.T) { + t.Parallel() + + // Fake template schedule store is unneeded for this test since the + // route fails before it is called. + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + req := codersdk.UpdateTemplateMeta{ + Name: template.Name, + DisplayName: template.DisplayName, + Description: template.Description, + Icon: template.Icon, + AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, + DefaultTTLMillis: time.Hour.Milliseconds(), + MaxTTLMillis: time.Hour.Milliseconds(), + AutostopRequirement: &codersdk.TemplateAutostopRequirement{ + DaysOfWeek: []string{"monday"}, + Weeks: 2, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err := client.UpdateTemplateMeta(ctx, template.ID, req) + require.Error(t, err) + require.ErrorContains(t, err, "max_ttl_ms") + }) } func TestDeleteTemplate(t *testing.T) { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index b41428b26a3ae..238780b1ec1b6 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -431,7 +431,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req } maxTTL := templateSchedule.MaxTTL - if templateSchedule.UseAutostopRequirement { + if !templateSchedule.UseMaxTTL { // If we're using autostop requirements, there isn't a max TTL. maxTTL = 0 } @@ -787,7 +787,7 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { } maxTTL := templateSchedule.MaxTTL - if templateSchedule.UseAutostopRequirement { + if !templateSchedule.UseMaxTTL { // If we're using autostop requirements, there isn't a max TTL. maxTTL = 0 } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 5bc3d30a9a206..682775cf31778 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -35,22 +35,21 @@ const ( type FeatureName string const ( - FeatureUserLimit FeatureName = "user_limit" - FeatureAuditLog FeatureName = "audit_log" - FeatureBrowserOnly FeatureName = "browser_only" - FeatureSCIM FeatureName = "scim" - FeatureTemplateRBAC FeatureName = "template_rbac" - FeatureUserRoleManagement FeatureName = "user_role_management" - FeatureHighAvailability FeatureName = "high_availability" - FeatureMultipleExternalAuth FeatureName = "multiple_external_auth" - FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons" - FeatureAppearance FeatureName = "appearance" - FeatureAdvancedTemplateScheduling FeatureName = "advanced_template_scheduling" - FeatureWorkspaceProxy FeatureName = "workspace_proxy" - FeatureExternalTokenEncryption FeatureName = "external_token_encryption" - FeatureTemplateAutostopRequirement FeatureName = "template_autostop_requirement" - FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions" - FeatureAccessControl FeatureName = "access_control" + FeatureUserLimit FeatureName = "user_limit" + FeatureAuditLog FeatureName = "audit_log" + FeatureBrowserOnly FeatureName = "browser_only" + FeatureSCIM FeatureName = "scim" + FeatureTemplateRBAC FeatureName = "template_rbac" + FeatureUserRoleManagement FeatureName = "user_role_management" + FeatureHighAvailability FeatureName = "high_availability" + FeatureMultipleExternalAuth FeatureName = "multiple_external_auth" + FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons" + FeatureAppearance FeatureName = "appearance" + FeatureAdvancedTemplateScheduling FeatureName = "advanced_template_scheduling" + FeatureWorkspaceProxy FeatureName = "workspace_proxy" + FeatureExternalTokenEncryption FeatureName = "external_token_encryption" + FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions" + FeatureAccessControl FeatureName = "access_control" ) // FeatureNames must be kept in-sync with the Feature enum above. @@ -65,11 +64,9 @@ var FeatureNames = []FeatureName{ FeatureExternalProvisionerDaemons, FeatureAppearance, FeatureAdvancedTemplateScheduling, - FeatureTemplateAutostopRequirement, FeatureWorkspaceProxy, FeatureUserRoleManagement, FeatureExternalTokenEncryption, - FeatureTemplateAutostopRequirement, FeatureWorkspaceBatchActions, FeatureAccessControl, } @@ -1816,10 +1813,10 @@ Write out the current server config as YAML to stdout.`, }, { Name: "Default Quiet Hours Schedule", - Description: "The default daily cron schedule applied to users that haven't set a custom quiet hours schedule themselves. The quiet hours schedule determines when workspaces will be force stopped due to the template's max TTL, and will round the max TTL up to be within the user's quiet hours window (or default). The format is the same as the standard cron format, but the day-of-month, month and day-of-week must be *. Only one hour and minute can be specified (ranges or comma separated values are not supported).", + Description: "The default daily cron schedule applied to users that haven't set a custom quiet hours schedule themselves. The quiet hours schedule determines when workspaces will be force stopped due to the template's autostop requirement, and will round the max deadline up to be within the user's quiet hours window (or default). The format is the same as the standard cron format, but the day-of-month, month and day-of-week must be *. Only one hour and minute can be specified (ranges or comma separated values are not supported).", Flag: "default-quiet-hours-schedule", Env: "CODER_QUIET_HOURS_DEFAULT_SCHEDULE", - Default: "", + Default: "CRON_TZ=UTC 0 0 * * *", Value: &c.UserQuietHoursSchedule.DefaultSchedule, Group: &deploymentGroupUserQuietHoursSchedule, YAML: "defaultQuietHoursSchedule", @@ -2071,20 +2068,6 @@ const ( // single tailnet for each agent. ExperimentSingleTailnet Experiment = "single_tailnet" - // ExperimentTemplateAutostopRequirement allows template admins to have more - // control over when workspaces created on a template are required to - // stop, and allows users to ensure these restarts never happen during their - // business hours. - // - // This will replace the MaxTTL setting on templates. - // - // Enables: - // - User quiet hours schedule settings - // - Template autostop requirement settings - // - Changes the max_deadline algorithm to use autostop requirement and user - // quiet hours instead of max_ttl. - ExperimentTemplateAutostopRequirement Experiment = "template_autostop_requirement" - // Deployment health page ExperimentDeploymentHealthPage Experiment = "deployment_health_page" diff --git a/codersdk/organizations.go b/codersdk/organizations.go index cc206180f81ae..5efe728ad03a7 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -85,9 +85,11 @@ type CreateTemplateRequest struct { // for all workspaces created from this template. DefaultTTLMillis *int64 `json:"default_ttl_ms,omitempty"` // TODO(@dean): remove max_ttl once autostop_requirement is matured + // Only one of MaxTTLMillis or AutostopRequirement can be specified. MaxTTLMillis *int64 `json:"max_ttl_ms,omitempty"` // AutostopRequirement allows optionally specifying the autostop requirement // for workspaces created from this template. This is an enterprise feature. + // Only one of MaxTTLMillis or AutostopRequirement can be specified. AutostopRequirement *TemplateAutostopRequirement `json:"autostop_requirement,omitempty"` // AutostartRequirement allows optionally specifying the autostart allowed days // for workspaces created from this template. This is an enterprise feature. diff --git a/codersdk/templates.go b/codersdk/templates.go index 3ebe2359c8600..8164843ad0c66 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -31,6 +31,9 @@ type Template struct { DeprecationMessage string `json:"deprecation_message"` Icon string `json:"icon"` DefaultTTLMillis int64 `json:"default_ttl_ms"` + // UseMaxTTL picks whether to use the deprecated max TTL for the template or + // the new autostop requirement. + UseMaxTTL bool `json:"use_max_ttl"` // TODO(@dean): remove max_ttl once autostop_requirement is matured MaxTTLMillis int64 `json:"max_ttl_ms"` // AutostopRequirement and AutostartRequirement are enterprise features. Its @@ -206,10 +209,12 @@ type UpdateTemplateMeta struct { Icon string `json:"icon,omitempty"` DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"` // TODO(@dean): remove max_ttl once autostop_requirement is matured + // Only one of MaxTTLMillis or AutostopRequirement can be specified. MaxTTLMillis int64 `json:"max_ttl_ms,omitempty"` // AutostopRequirement and AutostartRequirement can only be set if your license // includes the advanced template scheduling feature. If you attempt to set this // value while unlicensed, it will be ignored. + // Only one of MaxTTLMillis or AutostopRequirement can be specified. AutostopRequirement *TemplateAutostopRequirement `json:"autostop_requirement,omitempty"` AutostartRequirement *TemplateAutostartRequirement `json:"autostart_requirement,omitempty"` AllowUserAutostart bool `json:"allow_user_autostart,omitempty"` diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 50848cc833d45..71d233e8f9546 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -8,20 +8,20 @@ We track the following resources: -| Resource | | -| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| -| AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| -| Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| -| GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| -| HealthSettings
|
FieldTracked
dismissed_healthcheckstrue
idfalse
| -| License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| -| Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| -| TemplateVersion
create, write |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| -| User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
theme_preferencefalse
updated_atfalse
usernametrue
| -| Workspace
create, write, delete |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
idtrue
last_used_atfalse
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| -| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| -| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| +| Resource | | +| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| +| AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| +| Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| +| GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| +| HealthSettings
|
FieldTracked
dismissed_healthcheckstrue
idfalse
| +| License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| +| Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
use_max_ttltrue
user_acltrue
| +| TemplateVersion
create, write |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| +| User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
theme_preferencefalse
updated_atfalse
usernametrue
| +| Workspace
create, write, delete |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
idtrue
last_used_atfalse
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| +| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| diff --git a/docs/api/schemas.md b/docs/api/schemas.md index b1e2030f7961c..118952b50fccd 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1607,7 +1607,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `allow_user_autostop` | boolean | false | | Allow user autostop allows users to set a custom workspace TTL to use in place of the template's DefaultTTL field. By default this is true. If false, the DefaultTTL will always be used. This can only be disabled when using an enterprise license. | | `allow_user_cancel_workspace_jobs` | boolean | false | | Allow users to cancel in-progress workspace jobs. \*bool as the default value is "true". | | `autostart_requirement` | [codersdk.TemplateAutostartRequirement](#codersdktemplateautostartrequirement) | false | | Autostart requirement allows optionally specifying the autostart allowed days for workspaces created from this template. This is an enterprise feature. | -| `autostop_requirement` | [codersdk.TemplateAutostopRequirement](#codersdktemplateautostoprequirement) | false | | Autostop requirement allows optionally specifying the autostop requirement for workspaces created from this template. This is an enterprise feature. | +| `autostop_requirement` | [codersdk.TemplateAutostopRequirement](#codersdktemplateautostoprequirement) | false | | Autostop requirement allows optionally specifying the autostop requirement for workspaces created from this template. This is an enterprise feature. Only one of MaxTTLMillis or AutostopRequirement can be specified. | | `default_ttl_ms` | integer | false | | Default ttl ms allows optionally specifying the default TTL for all workspaces created from this template. | | `delete_ttl_ms` | integer | false | | Delete ttl ms allows optionally specifying the max lifetime before Coder permanently deletes dormant workspaces created from this template. | | `description` | string | false | | Description is a description of what the template contains. It must be less than 128 bytes. | @@ -1616,7 +1616,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `dormant_ttl_ms` | integer | false | | Dormant ttl ms allows optionally specifying the max lifetime before Coder locks inactive workspaces created from this template. | | `failure_ttl_ms` | integer | false | | Failure ttl ms allows optionally specifying the max lifetime before Coder stops all resources for failed workspaces created from this template. | | `icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | -| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured | +| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured Only one of MaxTTLMillis or AutostopRequirement can be specified. | | `name` | string | true | | Name is the name of the template. | | `require_active_version` | boolean | false | | Require active version mandates that workspaces are built with the active template version. | | `template_version_id` | string | true | | Template version ID is an in-progress or completed job to use as an initial version of the template. | @@ -2867,15 +2867,14 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in #### Enumerated Values -| Value | -| ------------------------------- | -| `moons` | -| `workspace_actions` | -| `tailnet_pg_coordinator` | -| `single_tailnet` | -| `template_autostop_requirement` | -| `deployment_health_page` | -| `template_update_policies` | +| Value | +| -------------------------- | +| `moons` | +| `workspace_actions` | +| `tailnet_pg_coordinator` | +| `single_tailnet` | +| `deployment_health_page` | +| `template_update_policies` | ## codersdk.ExternalAuth @@ -4501,7 +4500,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_max_ttl": true } ``` @@ -4536,6 +4536,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `time_til_dormant_autodelete_ms` | integer | false | | | | `time_til_dormant_ms` | integer | false | | | | `updated_at` | string | false | | | +| `use_max_ttl` | boolean | false | | Use max ttl picks whether to use the deprecated max TTL for the template or the new autostop requirement. | #### Enumerated Values diff --git a/docs/api/templates.md b/docs/api/templates.md index 0fcfd2a8b3cc5..63420f76a0d35 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -66,7 +66,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_max_ttl": true } ] ``` @@ -118,6 +119,7 @@ Status Code **200** | `» time_til_dormant_autodelete_ms` | integer | false | | | | `» time_til_dormant_ms` | integer | false | | | | `» updated_at` | string(date-time) | false | | | +| `» use_max_ttl` | boolean | false | | Use max ttl picks whether to use the deprecated max TTL for the template or the new autostop requirement. | #### Enumerated Values @@ -223,7 +225,8 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_max_ttl": true } ``` @@ -361,7 +364,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_max_ttl": true } ``` @@ -675,7 +679,8 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \ "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_max_ttl": true } ``` @@ -796,7 +801,8 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \ "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_max_ttl": true } ``` diff --git a/docs/cli/server.md b/docs/cli/server.md index 2b700e09568f8..d0a084883af3d 100644 --- a/docs/cli/server.md +++ b/docs/cli/server.md @@ -179,8 +179,9 @@ Addresses for STUN servers to establish P2P connections. It's recommended to hav | Type | string | | Environment | $CODER_QUIET_HOURS_DEFAULT_SCHEDULE | | YAML | userQuietHoursSchedule.defaultQuietHoursSchedule | +| Default | CRON_TZ=UTC 0 0 \* \* \* | -The default daily cron schedule applied to users that haven't set a custom quiet hours schedule themselves. The quiet hours schedule determines when workspaces will be force stopped due to the template's max TTL, and will round the max TTL up to be within the user's quiet hours window (or default). The format is the same as the standard cron format, but the day-of-month, month and day-of-week must be \*. Only one hour and minute can be specified (ranges or comma separated values are not supported). +The default daily cron schedule applied to users that haven't set a custom quiet hours schedule themselves. The quiet hours schedule determines when workspaces will be force stopped due to the template's autostop requirement, and will round the max deadline up to be within the user's quiet hours window (or default). The format is the same as the standard cron format, but the day-of-month, month and day-of-week must be \*. Only one hour and minute can be specified (ranges or comma separated values are not supported). ### --disable-owner-workspace-access diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index fc5257110cf82..0f5e5eef01dc5 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -71,6 +71,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "icon": ActionTrack, "default_ttl": ActionTrack, "max_ttl": ActionTrack, + "use_max_ttl": ActionTrack, "autostart_block_days_of_week": ActionTrack, "autostop_requirement_days_of_week": ActionTrack, "autostop_requirement_weeks": ActionTrack, diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 6997e74260317..230d07dbda6ed 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -448,15 +448,15 @@ USER QUIET HOURS SCHEDULE OPTIONS: Allow users to set quiet hours schedules each day for workspaces to avoid workspaces stopping during the day due to template max TTL. - --default-quiet-hours-schedule string, $CODER_QUIET_HOURS_DEFAULT_SCHEDULE + --default-quiet-hours-schedule string, $CODER_QUIET_HOURS_DEFAULT_SCHEDULE (default: CRON_TZ=UTC 0 0 * * *) The default daily cron schedule applied to users that haven't set a custom quiet hours schedule themselves. The quiet hours schedule determines when workspaces will be force stopped due to the template's - max TTL, and will round the max TTL up to be within the user's quiet - hours window (or default). The format is the same as the standard cron - format, but the day-of-month, month and day-of-week must be *. Only - one hour and minute can be specified (ranges or comma separated values - are not supported). + autostop requirement, and will round the max deadline up to be within + the user's quiet hours window (or default). The format is the same as + the standard cron format, but the day-of-month, month and day-of-week + must be *. Only one hour and minute can be specified (ranges or comma + separated values are not supported). ⚠️ DANGEROUS OPTIONS: --dangerous-allow-path-app-sharing bool, $CODER_DANGEROUS_ALLOW_PATH_APP_SHARING diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 32e96ec25975e..a4ac8733a6f79 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -492,12 +492,9 @@ func (api *API) updateEntitlements(ctx context.Context) error { codersdk.FeatureExternalTokenEncryption: len(api.ExternalTokenEncryption) > 0, codersdk.FeatureExternalProvisionerDaemons: true, codersdk.FeatureAdvancedTemplateScheduling: true, - // FeatureTemplateAutostopRequirement depends on - // FeatureAdvancedTemplateScheduling. - codersdk.FeatureTemplateAutostopRequirement: api.AGPL.Experiments.Enabled(codersdk.ExperimentTemplateAutostopRequirement) && api.DefaultQuietHoursSchedule != "", - codersdk.FeatureWorkspaceProxy: true, - codersdk.FeatureUserRoleManagement: true, - codersdk.FeatureAccessControl: true, + codersdk.FeatureWorkspaceProxy: true, + codersdk.FeatureUserRoleManagement: true, + codersdk.FeatureAccessControl: true, }) if err != nil { return err @@ -516,18 +513,6 @@ func (api *API) updateEntitlements(ctx context.Context) error { return nil } - if entitlements.Features[codersdk.FeatureTemplateAutostopRequirement].Enabled && !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled { - api.entitlements.Errors = []string{ - `Your license is entitled to the feature "template autostop ` + - `requirement" (and you have it enabled by setting the ` + - "default quiet hours schedule), but you are not entitled to " + - `the dependency feature "advanced template scheduling". ` + - "Please contact support for a new license.", - } - api.Logger.Error(ctx, "license is entitled to template autostop requirement but not advanced template scheduling") - return nil - } - featureChanged := func(featureName codersdk.FeatureName) (initial, changed, enabled bool) { if api.entitlements.Features == nil { return true, false, entitlements.Features[featureName].Enabled @@ -579,21 +564,11 @@ func (api *API) updateEntitlements(ctx context.Context) error { templateStore := schedule.NewEnterpriseTemplateScheduleStore(api.AGPL.UserQuietHoursScheduleStore) templateStoreInterface := agplschedule.TemplateScheduleStore(templateStore) api.AGPL.TemplateScheduleStore.Store(&templateStoreInterface) - } else { - templateStore := agplschedule.NewAGPLTemplateScheduleStore() - api.AGPL.TemplateScheduleStore.Store(&templateStore) - } - } - if initial, changed, enabled := featureChanged(codersdk.FeatureTemplateAutostopRequirement); shouldUpdate(initial, changed, enabled) { - if enabled { - templateStore := *(api.AGPL.TemplateScheduleStore.Load()) - enterpriseTemplateStore, ok := templateStore.(*schedule.EnterpriseTemplateScheduleStore) - if !ok { - api.Logger.Error(ctx, "unable to set up enterprise template schedule store, template autostop requirements will not be applied to workspace builds") + if api.DefaultQuietHoursSchedule == "" { + api.Logger.Warn(ctx, "template autostop requirement will default to UTC midnight as the default user quiet hours schedule. Set a custom default quiet hours schedule using CODER_QUIET_HOURS_DEFAULT_SCHEDULE to avoid this warning") + api.DefaultQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" } - enterpriseTemplateStore.UseAutostopRequirement.Store(true) - quietHoursStore, err := schedule.NewEnterpriseUserQuietHoursScheduleStore(api.DefaultQuietHoursSchedule) if err != nil { api.Logger.Error(ctx, "unable to set up enterprise user quiet hours schedule store, template autostop requirements will not be applied to workspace builds", slog.Error(err)) @@ -601,16 +576,8 @@ func (api *API) updateEntitlements(ctx context.Context) error { api.AGPL.UserQuietHoursScheduleStore.Store(&quietHoursStore) } } else { - if api.DefaultQuietHoursSchedule != "" { - api.Logger.Warn(ctx, "template autostop requirements are not enabled (due to setting default quiet hours schedule) as your license is not entitled to this feature") - } - - templateStore := *(api.AGPL.TemplateScheduleStore.Load()) - enterpriseTemplateStore, ok := templateStore.(*schedule.EnterpriseTemplateScheduleStore) - if ok { - enterpriseTemplateStore.UseAutostopRequirement.Store(false) - } - + templateStore := agplschedule.NewAGPLTemplateScheduleStore() + api.AGPL.TemplateScheduleStore.Store(&templateStore) quietHoursStore := agplschedule.NewAGPLUserQuietHoursScheduleStore() api.AGPL.UserQuietHoursScheduleStore.Store(&quietHoursStore) } diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index 3ec1f87a6da43..75900fd06d9bc 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -47,19 +47,18 @@ func init() { type Options struct { *coderdtest.Options - AuditLogging bool - BrowserOnly bool - EntitlementsUpdateInterval time.Duration - SCIMAPIKey []byte - UserWorkspaceQuota int - ProxyHealthInterval time.Duration - LicenseOptions *LicenseOptions - NoDefaultQuietHoursSchedule bool - DontAddLicense bool - DontAddFirstUser bool - ReplicaSyncUpdateInterval time.Duration - ExternalTokenEncryption []dbcrypt.Cipher - ProvisionerDaemonPSK string + AuditLogging bool + BrowserOnly bool + EntitlementsUpdateInterval time.Duration + SCIMAPIKey []byte + UserWorkspaceQuota int + ProxyHealthInterval time.Duration + LicenseOptions *LicenseOptions + DontAddLicense bool + DontAddFirstUser bool + ReplicaSyncUpdateInterval time.Duration + ExternalTokenEncryption []dbcrypt.Cipher + ProvisionerDaemonPSK string } // New constructs a codersdk client connected to an in-memory Enterprise API instance. @@ -86,10 +85,6 @@ func NewWithAPI(t *testing.T, options *Options) ( } require.False(t, options.DontAddFirstUser && !options.DontAddLicense, "DontAddFirstUser requires DontAddLicense") setHandler, cancelFunc, serverURL, oop := coderdtest.NewOptions(t, options.Options) - if !options.NoDefaultQuietHoursSchedule && oop.DeploymentValues.UserQuietHoursSchedule.DefaultSchedule.Value() == "" { - err := oop.DeploymentValues.UserQuietHoursSchedule.DefaultSchedule.Set("0 0 * * *") - require.NoError(t, err) - } coderAPI, err := coderd.New(context.Background(), &coderd.Options{ RBAC: true, AuditLogging: options.AuditLogging, diff --git a/enterprise/coderd/schedule/template.go b/enterprise/coderd/schedule/template.go index 26b0c78c6bfc9..69f868dfaa599 100644 --- a/enterprise/coderd/schedule/template.go +++ b/enterprise/coderd/schedule/template.go @@ -2,6 +2,7 @@ package schedule import ( "context" + "database/sql" "sync/atomic" "time" @@ -21,12 +22,6 @@ import ( // EnterpriseTemplateScheduleStore provides an agpl.TemplateScheduleStore that // has all fields implemented for enterprise customers. type EnterpriseTemplateScheduleStore struct { - // UseAutostopRequirement decides whether the AutostopRequirement field - // should be used instead of the MaxTTL field for determining the max - // deadline of a workspace build. This value is determined by a feature - // flag, licensing, and whether a default user quiet hours schedule is set. - UseAutostopRequirement atomic.Bool - // UserQuietHoursScheduleStore is used when recalculating build deadlines on // update. UserQuietHoursScheduleStore *atomic.Pointer[agpl.UserQuietHoursScheduleStore] @@ -51,7 +46,7 @@ func (s *EnterpriseTemplateScheduleStore) now() time.Time { } // Get implements agpl.TemplateScheduleStore. -func (s *EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.Store, templateID uuid.UUID) (agpl.TemplateScheduleOptions, error) { +func (*EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.Store, templateID uuid.UUID) (agpl.TemplateScheduleOptions, error) { ctx, span := tracing.StartSpan(ctx) defer span.End() @@ -77,11 +72,11 @@ func (s *EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.S } return agpl.TemplateScheduleOptions{ - UserAutostartEnabled: tpl.AllowUserAutostart, - UserAutostopEnabled: tpl.AllowUserAutostop, - DefaultTTL: time.Duration(tpl.DefaultTTL), - MaxTTL: time.Duration(tpl.MaxTTL), - UseAutostopRequirement: s.UseAutostopRequirement.Load(), + UserAutostartEnabled: tpl.AllowUserAutostart, + UserAutostopEnabled: tpl.AllowUserAutostop, + DefaultTTL: time.Duration(tpl.DefaultTTL), + MaxTTL: time.Duration(tpl.MaxTTL), + UseMaxTTL: tpl.UseMaxTtl, AutostopRequirement: agpl.TemplateAutostopRequirement{ DaysOfWeek: uint8(tpl.AutostopRequirementDaysOfWeek), Weeks: tpl.AutostopRequirementWeeks, @@ -108,6 +103,7 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S } if int64(opts.DefaultTTL) == tpl.DefaultTTL && + opts.UseMaxTTL != tpl.UseMaxTtl && int64(opts.MaxTTL) == tpl.MaxTTL && int16(opts.AutostopRequirement.DaysOfWeek) == tpl.AutostopRequirementDaysOfWeek && opts.AutostartRequirement.DaysOfWeek == tpl.AutostartAllowedDays() && @@ -142,6 +138,7 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S AllowUserAutostart: opts.UserAutostartEnabled, AllowUserAutostop: opts.UserAutostopEnabled, DefaultTTL: int64(opts.DefaultTTL), + UseMaxTtl: opts.UseMaxTTL, MaxTTL: int64(opts.MaxTTL), AutostopRequirementDaysOfWeek: int16(opts.AutostopRequirement.DaysOfWeek), AutostopRequirementWeeks: opts.AutostopRequirement.Weeks, @@ -184,7 +181,6 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S } } - // TODO: update all workspace max_deadlines to be within new bounds template, err = tx.GetTemplateByID(ctx, tpl.ID) if err != nil { return xerrors.Errorf("get updated template schedule: %w", err) @@ -192,11 +188,9 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S // Recalculate max_deadline and deadline for all running workspace // builds on this template. - if s.UseAutostopRequirement.Load() { - err = s.updateWorkspaceBuilds(ctx, tx, template) - if err != nil { - return xerrors.Errorf("update workspace builds: %w", err) - } + err = s.updateWorkspaceBuilds(ctx, tx, template) + if err != nil { + return xerrors.Errorf("update workspace builds: %w", err) } return nil @@ -218,6 +212,9 @@ func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuilds(ctx context.Cont ctx = dbauthz.AsSystemRestricted(ctx) builds, err := db.GetActiveWorkspaceBuildsByTemplateID(ctx, template.ID) + if xerrors.Is(err, sql.ErrNoRows) { + return nil + } if err != nil { return xerrors.Errorf("get active workspace builds: %w", err) } diff --git a/enterprise/coderd/schedule/template_test.go b/enterprise/coderd/schedule/template_test.go index 1fdbd6ba241dd..5045048061de8 100644 --- a/enterprise/coderd/schedule/template_test.go +++ b/enterprise/coderd/schedule/template_test.go @@ -214,16 +214,15 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) { // Set the template policy. templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr) - templateScheduleStore.UseAutostopRequirement.Store(true) templateScheduleStore.TimeNowFn = func() time.Time { return c.now } _, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{ - UserAutostartEnabled: false, - UserAutostopEnabled: false, - DefaultTTL: 0, - MaxTTL: 0, - UseAutostopRequirement: true, + UserAutostartEnabled: false, + UserAutostopEnabled: false, + DefaultTTL: 0, + MaxTTL: 0, + UseMaxTTL: false, AutostopRequirement: agplschedule.TemplateAutostopRequirement{ // Every day DaysOfWeek: 0b01111111, @@ -498,16 +497,15 @@ func TestTemplateUpdateBuildDeadlinesSkip(t *testing.T) { // Set the template policy. templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr) - templateScheduleStore.UseAutostopRequirement.Store(true) templateScheduleStore.TimeNowFn = func() time.Time { return now } _, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{ - UserAutostartEnabled: false, - UserAutostopEnabled: false, - DefaultTTL: 0, - MaxTTL: 0, - UseAutostopRequirement: true, + UserAutostartEnabled: false, + UserAutostopEnabled: false, + DefaultTTL: 0, + MaxTTL: 0, + UseMaxTTL: false, AutostopRequirement: agplschedule.TemplateAutostopRequirement{ // Every day DaysOfWeek: 0b01111111, diff --git a/enterprise/coderd/users.go b/enterprise/coderd/users.go index 6398a93c95e85..00c3ecaf0ab4b 100644 --- a/enterprise/coderd/users.go +++ b/enterprise/coderd/users.go @@ -13,26 +13,20 @@ import ( func (api *API) autostopRequirementEnabledMW(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - // The experiment must be enabled. - if !api.AGPL.Experiments.Enabled(codersdk.ExperimentTemplateAutostopRequirement) { - httpapi.RouteNotFound(rw) - return - } - // Entitlement must be enabled. api.entitlementsMu.RLock() - entitled := api.entitlements.Features[codersdk.FeatureTemplateAutostopRequirement].Entitlement != codersdk.EntitlementNotEntitled - enabled := api.entitlements.Features[codersdk.FeatureTemplateAutostopRequirement].Enabled + entitled := api.entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Entitlement != codersdk.EntitlementNotEntitled + enabled := api.entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled api.entitlementsMu.RUnlock() if !entitled { httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{ - Message: "Template autostop requirement is an Enterprise feature. Contact sales!", + Message: "Advanced template scheduling (and user quiet hours schedule) is an Enterprise feature. Contact sales!", }) return } if !enabled { httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{ - Message: "Template autostop requirement feature is not enabled. Please specify a default user quiet hours schedule to use this feature.", + Message: "Advanced template scheduling (and user quiet hours schedule) is not enabled.", }) return } diff --git a/enterprise/coderd/users_test.go b/enterprise/coderd/users_test.go index de7865f7d3666..40c06fbf339bf 100644 --- a/enterprise/coderd/users_test.go +++ b/enterprise/coderd/users_test.go @@ -18,6 +18,26 @@ import ( func TestUserQuietHours(t *testing.T) { t.Parallel() + t.Run("DefaultToUTC", func(t *testing.T) { + t.Parallel() + + adminClient, adminUser := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAdvancedTemplateScheduling: 1, + }, + }, + }) + + client, user := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID) + ctx := testutil.Context(t, testutil.WaitLong) + res, err := client.UserQuietHoursSchedule(ctx, user.ID.String()) + require.NoError(t, err) + require.Equal(t, "UTC", res.Timezone) + require.Equal(t, "00:00", res.Time) + require.Equal(t, "CRON_TZ=UTC 0 0 * * *", res.RawSchedule) + }) + t.Run("OK", func(t *testing.T) { t.Parallel() @@ -35,7 +55,6 @@ func TestUserQuietHours(t *testing.T) { dv := coderdtest.DeploymentValues(t) dv.UserQuietHoursSchedule.DefaultSchedule.Set(defaultQuietHoursSchedule) - dv.Experiments.Set(string(codersdk.ExperimentTemplateAutostopRequirement)) adminClient, adminUser := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ @@ -43,8 +62,7 @@ func TestUserQuietHours(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureAdvancedTemplateScheduling: 1, - codersdk.FeatureTemplateAutostopRequirement: 1, + codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) @@ -137,7 +155,6 @@ func TestUserQuietHours(t *testing.T) { dv := coderdtest.DeploymentValues(t) dv.UserQuietHoursSchedule.DefaultSchedule.Set("CRON_TZ=America/Chicago 0 0 * * *") - dv.Experiments.Set(string(codersdk.ExperimentTemplateAutostopRequirement)) client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ @@ -145,9 +162,8 @@ func TestUserQuietHours(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureAdvancedTemplateScheduling: 1, // Not entitled. - // codersdk.FeatureTemplateAutostopRequirement: 1, + // codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) @@ -160,61 +176,4 @@ func TestUserQuietHours(t *testing.T) { require.ErrorAs(t, err, &sdkErr) require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) }) - - t.Run("NotEnabled", func(t *testing.T) { - t.Parallel() - - dv := coderdtest.DeploymentValues(t) - dv.UserQuietHoursSchedule.DefaultSchedule.Set("") - dv.Experiments.Set(string(codersdk.ExperimentTemplateAutostopRequirement)) - - client, user := coderdenttest.New(t, &coderdenttest.Options{ - NoDefaultQuietHoursSchedule: true, - Options: &coderdtest.Options{ - DeploymentValues: dv, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{ - codersdk.FeatureAdvancedTemplateScheduling: 1, - codersdk.FeatureTemplateAutostopRequirement: 1, - }, - }, - }) - - ctx := testutil.Context(t, testutil.WaitLong) - //nolint:gocritic // We want to test the lack of feature, not RBAC. - _, err := client.UserQuietHoursSchedule(ctx, user.UserID.String()) - require.Error(t, err) - var sdkErr *codersdk.Error - require.ErrorAs(t, err, &sdkErr) - require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) - }) - - t.Run("NoFeatureFlag", func(t *testing.T) { - t.Parallel() - - dv := coderdtest.DeploymentValues(t) - dv.UserQuietHoursSchedule.DefaultSchedule.Set("CRON_TZ=America/Chicago 0 0 * * *") - dv.UserQuietHoursSchedule.DefaultSchedule.Set("") - - client, user := coderdenttest.New(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - DeploymentValues: dv, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{ - codersdk.FeatureAdvancedTemplateScheduling: 1, - codersdk.FeatureTemplateAutostopRequirement: 1, - }, - }, - }) - - ctx := testutil.Context(t, testutil.WaitLong) - //nolint:gocritic // We want to test the lack of feature, not RBAC. - _, err := client.UserQuietHoursSchedule(ctx, user.UserID.String()) - require.Error(t, err) - var sdkErr *codersdk.Error - require.ErrorAs(t, err, &sdkErr) - require.Equal(t, http.StatusNotFound, sdkErr.StatusCode()) - }) } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ca98e4eaebc43..c8887832c3f19 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -958,6 +958,7 @@ export interface Template { readonly deprecation_message: string; readonly icon: string; readonly default_ttl_ms: number; + readonly use_max_ttl: boolean; readonly max_ttl_ms: number; readonly autostop_requirement: TemplateAutostopRequirement; readonly autostart_requirement: TemplateAutostartRequirement; @@ -1776,7 +1777,6 @@ export type Experiment = | "moons" | "single_tailnet" | "tailnet_pg_coordinator" - | "template_autostop_requirement" | "template_update_policies" | "workspace_actions"; export const Experiments: Experiment[] = [ @@ -1784,7 +1784,6 @@ export const Experiments: Experiment[] = [ "moons", "single_tailnet", "tailnet_pg_coordinator", - "template_autostop_requirement", "template_update_policies", "workspace_actions", ]; @@ -1801,7 +1800,6 @@ export type FeatureName = | "high_availability" | "multiple_external_auth" | "scim" - | "template_autostop_requirement" | "template_rbac" | "user_limit" | "user_role_management" @@ -1818,7 +1816,6 @@ export const FeatureNames: FeatureName[] = [ "high_availability", "multiple_external_auth", "scim", - "template_autostop_requirement", "template_rbac", "user_limit", "user_role_management", diff --git a/site/src/components/Badges/Badges.tsx b/site/src/components/Badges/Badges.tsx index 2e4f9a2ea1c3e..e1ca731f749bf 100644 --- a/site/src/components/Badges/Badges.tsx +++ b/site/src/components/Badges/Badges.tsx @@ -122,6 +122,23 @@ export const AlphaBadge: FC = () => { ); }; +export const DeprecatedBadge: FC = () => { + return ( + + Deprecated + + ); +}; + export const Badges: FC = ({ children }) => { return ( = ({ children, title, description, classes = {}, alpha = false }) => { +> = ({ + children, + title, + description, + classes = {}, + alpha = false, + deprecated = false, +}) => { const { direction } = useContext(FormContext); const theme = useTheme(); @@ -121,6 +129,7 @@ export const FormSection: FC< > {title} {alpha && } + {deprecated && }
{description}
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index 1f94711d23ebe..be2d80b23e5bb 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -60,6 +60,7 @@ export interface CreateTemplateData { description: string; icon: string; default_ttl_hours: number; + use_max_ttl: boolean; max_ttl_hours: number; autostart_requirement_days_of_week: TemplateAutostartRequirementDaysValue[]; autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue; @@ -110,6 +111,7 @@ const defaultInitialValues: CreateTemplateData = { // // The maximum value is 30 days but we default to 7 days as it's a much more // sensible value for most teams. + use_max_ttl: false, // autostop_requirement is default max_ttl_hours: 24 * 7, // autostop_requirement is an enterprise-only feature, and the server ignores // the value if you are not licensed. We hide the form value based on @@ -145,6 +147,8 @@ const getInitialValues = ({ initialValues = { ...initialValues, max_ttl_hours: 0, + autostop_requirement_days_of_week: "off", + autostop_requirement_weeks: 1, }; } @@ -202,7 +206,6 @@ export type CreateTemplateFormProps = ( logs?: ProvisionerJobLog[]; allowAdvancedScheduling: boolean; allowDisableEveryoneAccess: boolean; - allowAutostopRequirement: boolean; }; export const CreateTemplateForm: FC = (props) => { @@ -216,7 +219,6 @@ export const CreateTemplateForm: FC = (props) => { logs, allowAdvancedScheduling, allowDisableEveryoneAccess, - allowAutostopRequirement, } = props; const form = useFormik({ initialValues: getInitialValues({ @@ -262,6 +264,27 @@ export const CreateTemplateForm: FC = (props) => { } }, [autostop_requirement_days_of_week, setFieldValue]); + const handleToggleUseMaxTTL = async () => { + const val = !form.values.use_max_ttl; + if (val) { + // set max_ttl to 1, set autostop_requirement to empty + await form.setValues({ + ...form.values, + use_max_ttl: val, + max_ttl_hours: 1, + autostop_requirement_days_of_week: "off", + autostop_requirement_weeks: 1, + }); + } else { + // set max_ttl to 0 + await form.setValues({ + ...form.values, + use_max_ttl: val, + max_ttl_hours: 0, + }); + } + }; + return ( {/* General info */} @@ -349,78 +372,107 @@ export const CreateTemplateForm: FC = (props) => { label="Default autostop (hours)" type="number" /> +
- {!allowAutostopRequirement && ( - - ) : ( - <> - You need an enterprise license to use it.{" "} - Learn more. - - ), - )} - disabled={isSubmitting || !allowAdvancedScheduling} - fullWidth - label="Max lifetime (hours)" - type="number" - /> - )} + + , + )} + disabled={ + isSubmitting || + form.values.use_max_ttl || + !allowAdvancedScheduling + } + fullWidth + select + value={form.values.autostop_requirement_days_of_week} + label="Days with required stop" + > + + Off + + + Daily + + + Saturday + + + Sunday + + + + , + )} + disabled={ + isSubmitting || + form.values.use_max_ttl || + !allowAdvancedScheduling || + !["saturday", "sunday"].includes( + form.values.autostop_requirement_days_of_week || "", + ) + } + fullWidth + inputProps={{ min: 1, max: 16, step: 1 }} + label="Weeks between required stops" + type="number" + /> - {allowAutostopRequirement && ( - - , - )} - disabled={isSubmitting} - fullWidth - select - value={form.values.autostop_requirement_days_of_week} - label="Days with required stop" - > - - Off - - - Daily - - - Saturday - - - Sunday - - - - , - )} - disabled={ - isSubmitting || - !["saturday", "sunday"].includes( - form.values.autostop_requirement_days_of_week || "", - ) - } - fullWidth - inputProps={{ min: 1, max: 16, step: 1 }} - label="Weeks between required stops" - type="number" + + + + + + Use a max lifetime instead of a required autostop schedule. + + + Use a maximum lifetime for workspaces created from this + template instead of an autostop requirement as configured + above. + + - )} + + + ) : ( + <> + You need an enterprise license to use it.{" "} + Learn more. + + ), + )} + disabled={ + isSubmitting || + !form.values.use_max_ttl || + !allowAdvancedScheduling + } + fullWidth + label="Max lifetime (hours)" + type="number" + /> + @@ -482,8 +534,8 @@ export const CreateTemplateForm: FC = (props) => { Workspaces will always use the default TTL if this is set. - Regardless of this setting, workspaces can only stay on for - the max TTL. + Regardless of this setting, workspaces will still stop due to + the autostop requirement policy. diff --git a/site/src/pages/CreateTemplatePage/utils.ts b/site/src/pages/CreateTemplatePage/utils.ts index fdeb1046d3869..986373bc0902e 100644 --- a/site/src/pages/CreateTemplatePage/utils.ts +++ b/site/src/pages/CreateTemplatePage/utils.ts @@ -12,30 +12,45 @@ const provisioner: ProvisionerType = typeof (window as any).playwright !== "undefined" ? "echo" : "terraform"; export const newTemplate = (formData: CreateTemplateData) => { - const { - default_ttl_hours, + let { max_ttl_hours, - parameter_values_by_name, - allow_everyone_group_access, - autostart_requirement_days_of_week, autostop_requirement_days_of_week, autostop_requirement_weeks, - ...safeTemplateData } = formData; + const safeTemplateData = { + name: formData.name, + display_name: formData.display_name, + description: formData.description, + icon: formData.icon, + use_max_ttl: formData.use_max_ttl, + allow_user_autostart: formData.allow_user_autostart, + allow_user_autostop: formData.allow_user_autostop, + allow_user_cancel_workspace_jobs: formData.allow_user_cancel_workspace_jobs, + user_variable_values: formData.user_variable_values, + allow_everyone_group_access: formData.allow_everyone_group_access, + }; + + if (formData.use_max_ttl) { + autostop_requirement_days_of_week = "off"; + autostop_requirement_weeks = 1; + } else { + max_ttl_hours = 0; + } + return { ...safeTemplateData, disable_everyone_group_access: !formData.allow_everyone_group_access, default_ttl_ms: formData.default_ttl_hours * 60 * 60 * 1000, // Convert hours to ms - max_ttl_ms: formData.max_ttl_hours * 60 * 60 * 1000, // Convert hours to ms + max_ttl_ms: max_ttl_hours * 60 * 60 * 1000, // Convert hours to ms autostop_requirement: { days_of_week: calculateAutostopRequirementDaysValue( - formData.autostop_requirement_days_of_week, + autostop_requirement_days_of_week, ), - weeks: formData.autostop_requirement_weeks, + weeks: autostop_requirement_weeks, }, autostart_requirement: { - days_of_week: autostart_requirement_days_of_week, + days_of_week: formData.autostart_requirement_days_of_week, }, require_active_version: false, }; @@ -48,13 +63,10 @@ export const getFormPermissions = (entitlements: Entitlements) => { // means no one can access. const allowDisableEveryoneAccess = entitlements.features["template_rbac"].enabled; - const allowAutostopRequirement = - entitlements.features["template_autostop_requirement"].enabled; return { allowAdvancedScheduling, allowDisableEveryoneAccess, - allowAutostopRequirement, }; }; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx index 912cc070ccb79..457e4501711f8 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx @@ -55,7 +55,6 @@ export interface TemplateScheduleForm { error?: unknown; allowAdvancedScheduling: boolean; allowWorkspaceActions: boolean; - allowAutostopRequirement: boolean; // Helpful to show field errors on Storybook initialTouched?: FormikTouched; } @@ -67,7 +66,6 @@ export const TemplateScheduleForm: FC = ({ error, allowAdvancedScheduling, allowWorkspaceActions, - allowAutostopRequirement, isSubmitting, initialTouched, }) => { @@ -78,6 +76,10 @@ export const TemplateScheduleForm: FC = ({ default_ttl_ms: template.default_ttl_ms / MS_HOUR_CONVERSION, // the API ignores these values, but to avoid tripping up validation set // it to zero if the user can't set the field. + use_max_ttl: + template.use_max_ttl === undefined + ? template.max_ttl_ms > 0 + : template.use_max_ttl, max_ttl_ms: allowAdvancedScheduling ? template.max_ttl_ms / MS_HOUR_CONVERSION : 0, @@ -91,12 +93,12 @@ export const TemplateScheduleForm: FC = ({ ? template.time_til_dormant_autodelete_ms / MS_DAY_CONVERSION : 0, - autostop_requirement_days_of_week: allowAutostopRequirement + autostop_requirement_days_of_week: allowAdvancedScheduling ? convertAutostopRequirementDaysValue( template.autostop_requirement.days_of_week, ) : "off", - autostop_requirement_weeks: allowAutostopRequirement + autostop_requirement_weeks: allowAdvancedScheduling ? template.autostop_requirement.weeks > 0 ? template.autostop_requirement.weeks : 1 @@ -205,9 +207,10 @@ export const TemplateScheduleForm: FC = ({ default_ttl_ms: form.values.default_ttl_ms ? form.values.default_ttl_ms * MS_HOUR_CONVERSION : undefined, - max_ttl_ms: form.values.max_ttl_ms - ? form.values.max_ttl_ms * MS_HOUR_CONVERSION - : undefined, + max_ttl_ms: + form.values.max_ttl_ms && form.values.use_max_ttl + ? form.values.max_ttl_ms * MS_HOUR_CONVERSION + : undefined, failure_ttl_ms: form.values.failure_ttl_ms ? form.values.failure_ttl_ms * MS_DAY_CONVERSION : undefined, @@ -218,12 +221,14 @@ export const TemplateScheduleForm: FC = ({ ? form.values.time_til_dormant_autodelete_ms * MS_DAY_CONVERSION : undefined, - autostop_requirement: { - days_of_week: calculateAutostopRequirementDaysValue( - form.values.autostop_requirement_days_of_week, - ), - weeks: autostop_requirement_weeks, - }, + autostop_requirement: form.values.use_max_ttl + ? undefined + : { + days_of_week: calculateAutostopRequirementDaysValue( + form.values.autostop_requirement_days_of_week, + ), + weeks: autostop_requirement_weeks, + }, autostart_requirement: { days_of_week: form.values.autostart_requirement_days_of_week, }, @@ -317,6 +322,27 @@ export const TemplateScheduleForm: FC = ({ } }; + const handleToggleUseMaxTTL = async () => { + const val = !form.values.use_max_ttl; + if (val) { + // set max_ttl to 1, set autostop_requirement to empty + await form.setValues({ + ...form.values, + use_max_ttl: val, + max_ttl_ms: 1, + autostop_requirement_days_of_week: "off", + autostop_requirement_weeks: 1, + }); + } else { + // set max_ttl to 0 + await form.setValues({ + ...form.values, + use_max_ttl: val, + max_ttl_ms: 0, + }); + } + }; + return ( = ({ label="Default autostop (hours)" type="number" /> + + - {!allowAutostopRequirement && ( - - ) : ( - <> - You need an enterprise license to use it{" "} - Learn more. - - ), - )} - disabled={isSubmitting || !allowAdvancedScheduling} - fullWidth - inputProps={{ min: 0, step: 1 }} - label="Max lifetime (hours)" - type="number" - /> - )} + + + , + )} + disabled={isSubmitting || form.values.use_max_ttl} + fullWidth + select + value={form.values.autostop_requirement_days_of_week} + label="Days with required stop" + > + + Off + + + Daily + + + Saturday + + + Sunday + + + + , + )} + disabled={ + isSubmitting || + form.values.use_max_ttl || + !["saturday", "sunday"].includes( + form.values.autostop_requirement_days_of_week || "", + ) + } + fullWidth + inputProps={{ min: 1, max: 16, step: 1 }} + label="Weeks between required stops" + type="number" + /> - {allowAutostopRequirement && ( - - - , - )} - disabled={isSubmitting} - fullWidth - select - value={form.values.autostop_requirement_days_of_week} - label="Days with required stop" - > - - Off - - - Daily - - - Saturday - - - Sunday - - - - , - )} - disabled={ - isSubmitting || - !["saturday", "sunday"].includes( - form.values.autostop_requirement_days_of_week || "", - ) + + + + + } + label={ + + + Use a max lifetime instead of a required autostop schedule. + + + Use a maximum lifetime for workspaces created from this + template instead of an autostop requirement as configured + above. + + } - fullWidth - inputProps={{ min: 1, max: 16, step: 1 }} - label="Weeks between required stops" - type="number" /> - - )} + + + ) : ( + <> + You need an enterprise license to use it{" "} + Learn more. + + ), + )} + disabled={ + isSubmitting || + !form.values.use_max_ttl || + !allowAdvancedScheduling + } + fullWidth + inputProps={{ min: 0, step: 1 }} + label="Max lifetime (hours)" + type="number" + /> + + { // This check can be removed when https://github.com/coder/coder/milestone/19 // is merged up const allowWorkspaceActions = experiments.includes("workspace_actions"); - const allowAutostopRequirement = - entitlements.features["template_autostop_requirement"].enabled; const { clearLocal } = useLocalStorage(); const { @@ -55,7 +53,6 @@ const TemplateSchedulePage: FC = () => { ["initialTouched"]; allowAdvancedScheduling: boolean; allowWorkspaceActions: boolean; - allowAutostopRequirement: boolean; } export const TemplateSchedulePageView: FC = ({ @@ -24,7 +23,6 @@ export const TemplateSchedulePageView: FC = ({ isSubmitting, allowAdvancedScheduling, allowWorkspaceActions, - allowAutostopRequirement, submitError, initialTouched, }) => { @@ -37,7 +35,6 @@ export const TemplateSchedulePageView: FC = ({ { + use_max_ttl: boolean; autostart_requirement_days_of_week: TemplateAutostartRequirementDaysValue[]; autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue; autostop_requirement_weeks: number; diff --git a/site/src/pages/UserSettingsPage/Sidebar.tsx b/site/src/pages/UserSettingsPage/Sidebar.tsx index 4cf6cad3dde80..a67d7b2bed171 100644 --- a/site/src/pages/UserSettingsPage/Sidebar.tsx +++ b/site/src/pages/UserSettingsPage/Sidebar.tsx @@ -7,13 +7,13 @@ import ScheduleIcon from "@mui/icons-material/EditCalendarOutlined"; import SecurityIcon from "@mui/icons-material/LockOutlined"; import type { User } from "api/typesGenerated"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; -import { useDashboard } from "components/Dashboard/DashboardProvider"; import { Sidebar as BaseSidebar, SidebarHeader, SidebarNavItem, } from "components/Sidebar/Sidebar"; import { GitIcon } from "components/Icons/GitIcon"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; interface SidebarProps { user: User; @@ -21,8 +21,8 @@ interface SidebarProps { export const Sidebar: FC = ({ user }) => { const { entitlements } = useDashboard(); - const allowAutostopRequirement = - entitlements.features.template_autostop_requirement.enabled; + const showSchedulePage = + entitlements.features.advanced_template_scheduling.enabled; return ( @@ -39,7 +39,7 @@ export const Sidebar: FC = ({ user }) => { Appearance - {allowAutostopRequirement && ( + {showSchedulePage && ( Schedule diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index a73e6ee02524a..14b475a6d1377 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -445,9 +445,10 @@ export const MockTemplate: TypesGen.Template = { }, description: "This is a test description.", default_ttl_ms: 24 * 60 * 60 * 1000, - max_ttl_ms: 2 * 24 * 60 * 60 * 1000, + use_max_ttl: false, + max_ttl_ms: 0, autostop_requirement: { - days_of_week: [], + days_of_week: ["sunday"], weeks: 1, }, autostart_requirement: {