feat: allow templates to specify max_ttl or autostop_requirement (#10920)

This commit is contained in:
Dean Sheather 2023-12-15 00:27:56 -08:00 committed by GitHub
parent 30f032d282
commit b36071c6bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 699 additions and 495 deletions

View File

@ -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

View File

@ -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: <unset>, 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 * * *

10
coderd/apidoc/docs.go generated
View File

@ -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"
}
}
},

View File

@ -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"
}
}
},

View File

@ -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

View File

@ -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

View File

@ -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.';

View File

@ -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.';

View File

@ -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 {

View File

@ -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.

View File

@ -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,

View File

@ -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
;

View File

@ -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
},
}

View File

@ -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)

View File

@ -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
},
}

View File

@ -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,

View File

@ -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,

View File

@ -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) {

View File

@ -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
}

View File

@ -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"

View File

@ -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.

View File

@ -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"`

View File

@ -8,20 +8,20 @@ We track the following resources:
<!-- Code generated by 'make docs/admin/audit-logs.md'. DO NOT EDIT -->
| <b>Resource<b> | |
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
| <b>Resource<b> | |
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_max_ttl</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
<!-- End generated by 'make docs/admin/audit-logs.md'. -->

25
docs/api/schemas.md generated
View File

@ -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

16
docs/api/templates.md generated
View File

@ -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
}
```

3
docs/cli/server.md generated
View File

@ -179,8 +179,9 @@ Addresses for STUN servers to establish P2P connections. It's recommended to hav
| Type | <code>string</code> |
| Environment | <code>$CODER_QUIET_HOURS_DEFAULT_SCHEDULE</code> |
| YAML | <code>userQuietHoursSchedule.defaultQuietHoursSchedule</code> |
| Default | <code>CRON_TZ=UTC 0 0 \* \* \*</code> |
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

View File

@ -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,

View File

@ -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

View File

@ -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)
}

View File

@ -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,

View File

@ -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)
}

View File

@ -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,

View File

@ -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
}

View File

@ -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())
})
}

View File

@ -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",

View File

@ -122,6 +122,23 @@ export const AlphaBadge: FC = () => {
);
};
export const DeprecatedBadge: FC = () => {
return (
<span
css={[
styles.badge,
{
border: `1px solid ${colors.orange[600]}`,
backgroundColor: colors.orange[950],
color: colors.orange[50],
},
]}
>
Deprecated
</span>
);
};
export const Badges: FC<PropsWithChildren> = ({ children }) => {
return (
<Stack

View File

@ -6,7 +6,7 @@ import {
type PropsWithChildren,
useContext,
} from "react";
import { AlphaBadge } from "components/Badges/Badges";
import { AlphaBadge, DeprecatedBadge } from "components/Badges/Badges";
import { Stack } from "components/Stack/Stack";
import {
FormFooter as BaseFormFooter,
@ -77,8 +77,16 @@ export const FormSection: FC<
infoTitle?: string;
};
alpha?: boolean;
deprecated?: boolean;
}
> = ({ 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 && <AlphaBadge />}
{deprecated && <DeprecatedBadge />}
</h2>
<div css={styles.formSectionInfoDescription}>{description}</div>
</div>

View File

@ -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<CreateTemplateFormProps> = (props) => {
@ -216,7 +219,6 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
logs,
allowAdvancedScheduling,
allowDisableEveryoneAccess,
allowAutostopRequirement,
} = props;
const form = useFormik<CreateTemplateData>({
initialValues: getInitialValues({
@ -262,6 +264,27 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (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 (
<HorizontalForm onSubmit={form.handleSubmit}>
{/* General info */}
@ -349,78 +372,107 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
label="Default autostop (hours)"
type="number"
/>
{!allowAutostopRequirement && (
<TextField
{...getFieldHelpers(
"max_ttl_hours",
allowAdvancedScheduling ? (
<MaxTTLHelperText ttl={form.values.max_ttl_hours} />
) : (
<>
You need an enterprise license to use it.{" "}
<Link href={docs("/enterprise")}>Learn more</Link>.
</>
),
)}
disabled={isSubmitting || !allowAdvancedScheduling}
fullWidth
label="Max lifetime (hours)"
type="number"
/>
)}
</Stack>
{allowAutostopRequirement && (
<Stack direction="row" css={styles.ttlFields}>
<TextField
{...getFieldHelpers(
"autostop_requirement_days_of_week",
<AutostopRequirementDaysHelperText
days={form.values.autostop_requirement_days_of_week}
/>,
)}
disabled={isSubmitting}
fullWidth
select
value={form.values.autostop_requirement_days_of_week}
label="Days with required stop"
>
<MenuItem key="off" value="off">
Off
</MenuItem>
<MenuItem key="daily" value="daily">
Daily
</MenuItem>
<MenuItem key="saturday" value="saturday">
Saturday
</MenuItem>
<MenuItem key="sunday" value="sunday">
Sunday
</MenuItem>
</TextField>
<Stack direction="row" css={styles.ttlFields}>
<TextField
{...getFieldHelpers(
"autostop_requirement_days_of_week",
<AutostopRequirementDaysHelperText
days={form.values.autostop_requirement_days_of_week}
/>,
)}
disabled={
isSubmitting ||
form.values.use_max_ttl ||
!allowAdvancedScheduling
}
fullWidth
select
value={form.values.autostop_requirement_days_of_week}
label="Days with required stop"
>
<MenuItem key="off" value="off">
Off
</MenuItem>
<MenuItem key="daily" value="daily">
Daily
</MenuItem>
<MenuItem key="saturday" value="saturday">
Saturday
</MenuItem>
<MenuItem key="sunday" value="sunday">
Sunday
</MenuItem>
</TextField>
<TextField
{...getFieldHelpers(
"autostop_requirement_weeks",
<AutostopRequirementWeeksHelperText
days={form.values.autostop_requirement_days_of_week}
weeks={form.values.autostop_requirement_weeks}
/>,
)}
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"
<TextField
{...getFieldHelpers(
"autostop_requirement_weeks",
<AutostopRequirementWeeksHelperText
days={form.values.autostop_requirement_days_of_week}
weeks={form.values.autostop_requirement_weeks}
/>,
)}
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"
/>
</Stack>
<Stack direction="column">
<Stack direction="row" alignItems="center">
<Checkbox
id="use_max_ttl"
size="small"
disabled={isSubmitting || !allowAdvancedScheduling}
onChange={handleToggleUseMaxTTL}
name="use_max_ttl"
checked={form.values.use_max_ttl}
/>
<Stack spacing={0.5}>
<strong>
Use a max lifetime instead of a required autostop schedule.
</strong>
<span css={styles.optionHelperText}>
Use a maximum lifetime for workspaces created from this
template instead of an autostop requirement as configured
above.
</span>
</Stack>
</Stack>
)}
<TextField
{...getFieldHelpers(
"max_ttl_hours",
allowAdvancedScheduling ? (
<MaxTTLHelperText ttl={form.values.max_ttl_hours} />
) : (
<>
You need an enterprise license to use it.{" "}
<Link href={docs("/enterprise")}>Learn more</Link>.
</>
),
)}
disabled={
isSubmitting ||
!form.values.use_max_ttl ||
!allowAdvancedScheduling
}
fullWidth
label="Max lifetime (hours)"
type="number"
/>
</Stack>
<Stack direction="column">
<Stack direction="row" alignItems="center">
@ -482,8 +534,8 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
</strong>
<span css={styles.optionHelperText}>
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.
</span>
</Stack>
</Stack>

View File

@ -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,
};
};

View File

@ -55,7 +55,6 @@ export interface TemplateScheduleForm {
error?: unknown;
allowAdvancedScheduling: boolean;
allowWorkspaceActions: boolean;
allowAutostopRequirement: boolean;
// Helpful to show field errors on Storybook
initialTouched?: FormikTouched<UpdateTemplateMeta>;
}
@ -67,7 +66,6 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
error,
allowAdvancedScheduling,
allowWorkspaceActions,
allowAutostopRequirement,
isSubmitting,
initialTouched,
}) => {
@ -78,6 +76,10 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
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<TemplateScheduleForm> = ({
? 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<TemplateScheduleForm> = ({
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<TemplateScheduleForm> = ({
? 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<TemplateScheduleForm> = ({
}
};
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 (
<HorizontalForm
onSubmit={form.handleSubmit}
@ -338,85 +364,126 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
label="Default autostop (hours)"
type="number"
/>
{!allowAutostopRequirement && (
<TextField
{...getFieldHelpers(
"max_ttl_ms",
allowAdvancedScheduling ? (
<MaxTTLHelperText ttl={form.values.max_ttl_ms} />
) : (
<>
You need an enterprise license to use it{" "}
<Link href={docs("/enterprise")}>Learn more</Link>.
</>
),
)}
disabled={isSubmitting || !allowAdvancedScheduling}
fullWidth
inputProps={{ min: 0, step: 1 }}
label="Max lifetime (hours)"
type="number"
/>
)}
</Stack>
</FormSection>
{allowAutostopRequirement && (
<FormSection
title="Autostop Requirement"
description="Define when workspaces created from this template are stopped periodically to enforce template updates and ensure idle workspaces are stopped."
>
<Stack direction="row" css={styles.ttlFields}>
<TextField
{...getFieldHelpers(
"autostop_requirement_days_of_week",
<AutostopRequirementDaysHelperText
days={form.values.autostop_requirement_days_of_week}
/>,
)}
disabled={isSubmitting}
fullWidth
select
value={form.values.autostop_requirement_days_of_week}
label="Days with required stop"
>
<MenuItem key="off" value="off">
Off
</MenuItem>
<MenuItem key="daily" value="daily">
Daily
</MenuItem>
<MenuItem key="saturday" value="saturday">
Saturday
</MenuItem>
<MenuItem key="sunday" value="sunday">
Sunday
</MenuItem>
</TextField>
<FormSection
title="Autostop Requirement"
description="Define when workspaces created from this template are stopped periodically to enforce template updates and ensure idle workspaces are stopped."
>
<Stack direction="row" css={styles.ttlFields}>
<TextField
{...getFieldHelpers(
"autostop_requirement_days_of_week",
<AutostopRequirementDaysHelperText
days={form.values.autostop_requirement_days_of_week}
/>,
)}
disabled={isSubmitting || form.values.use_max_ttl}
fullWidth
select
value={form.values.autostop_requirement_days_of_week}
label="Days with required stop"
>
<MenuItem key="off" value="off">
Off
</MenuItem>
<MenuItem key="daily" value="daily">
Daily
</MenuItem>
<MenuItem key="saturday" value="saturday">
Saturday
</MenuItem>
<MenuItem key="sunday" value="sunday">
Sunday
</MenuItem>
</TextField>
<TextField
{...getFieldHelpers(
"autostop_requirement_weeks",
<AutostopRequirementWeeksHelperText
days={form.values.autostop_requirement_days_of_week}
weeks={form.values.autostop_requirement_weeks}
/>,
)}
disabled={
isSubmitting ||
!["saturday", "sunday"].includes(
form.values.autostop_requirement_days_of_week || "",
)
<TextField
{...getFieldHelpers(
"autostop_requirement_weeks",
<AutostopRequirementWeeksHelperText
days={form.values.autostop_requirement_days_of_week}
weeks={form.values.autostop_requirement_weeks}
/>,
)}
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"
/>
</Stack>
</FormSection>
<FormSection
title="Max Lifetime"
description="Define the maximum lifetime for workspaces created from this template."
deprecated
>
<Stack direction="column">
<Stack direction="row" alignItems="center">
<FormControlLabel
control={
<Checkbox
id="use_max_ttl"
size="small"
disabled={isSubmitting || !allowAdvancedScheduling}
onChange={handleToggleUseMaxTTL}
name="use_max_ttl"
checked={form.values.use_max_ttl}
/>
}
label={
<Stack spacing={0.5}>
<strong>
Use a max lifetime instead of a required autostop schedule.
</strong>
<span
css={{
fontSize: 12,
color: theme.palette.text.secondary,
}}
>
Use a maximum lifetime for workspaces created from this
template instead of an autostop requirement as configured
above.
</span>
</Stack>
}
fullWidth
inputProps={{ min: 1, max: 16, step: 1 }}
label="Weeks between required stops"
type="number"
/>
</Stack>
</FormSection>
)}
<TextField
{...getFieldHelpers(
"max_ttl_ms",
allowAdvancedScheduling ? (
<MaxTTLHelperText ttl={form.values.max_ttl_ms} />
) : (
<>
You need an enterprise license to use it{" "}
<Link href={docs("/enterprise")}>Learn more</Link>.
</>
),
)}
disabled={
isSubmitting ||
!form.values.use_max_ttl ||
!allowAdvancedScheduling
}
fullWidth
inputProps={{ min: 0, step: 1 }}
label="Max lifetime (hours)"
type="number"
/>
</Stack>
</FormSection>
<FormSection
title="Allow users scheduling"

View File

@ -15,6 +15,7 @@ import TemplateSchedulePage from "./TemplateSchedulePage";
const validFormValues: TemplateScheduleFormValues = {
default_ttl_ms: 1,
use_max_ttl: true,
max_ttl_ms: 2,
failure_ttl_ms: 7,
time_til_dormant_ms: 180,
@ -73,8 +74,12 @@ const fillAndSubmitForm = async ({
}
if (max_ttl_ms) {
const useMaxTtlCheckbox = screen.getByRole("checkbox", {
name: /Use a max lifetime/i,
});
const maxTtlField = await screen.findByLabelText("Max lifetime (hours)");
await user.click(useMaxTtlCheckbox);
await user.clear(maxTtlField);
await user.type(maxTtlField, max_ttl_ms.toString());
}

View File

@ -24,8 +24,6 @@ const TemplateSchedulePage: FC = () => {
// 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 = () => {
<TemplateSchedulePageView
allowAdvancedScheduling={allowAdvancedScheduling}
allowWorkspaceActions={allowWorkspaceActions}
allowAutostopRequirement={allowAutostopRequirement}
isSubmitting={isSubmitting}
template={template}
submitError={submitError}

View File

@ -14,7 +14,6 @@ export interface TemplateSchedulePageViewProps {
>["initialTouched"];
allowAdvancedScheduling: boolean;
allowWorkspaceActions: boolean;
allowAutostopRequirement: boolean;
}
export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
@ -24,7 +23,6 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
isSubmitting,
allowAdvancedScheduling,
allowWorkspaceActions,
allowAutostopRequirement,
submitError,
initialTouched,
}) => {
@ -37,7 +35,6 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
<TemplateScheduleForm
allowAdvancedScheduling={allowAdvancedScheduling}
allowWorkspaceActions={allowWorkspaceActions}
allowAutostopRequirement={allowAutostopRequirement}
initialTouched={initialTouched}
isSubmitting={isSubmitting}
template={template}

View File

@ -10,6 +10,7 @@ export interface TemplateScheduleFormValues
UpdateTemplateMeta,
"autostop_requirement" | "autostart_requirement"
> {
use_max_ttl: boolean;
autostart_requirement_days_of_week: TemplateAutostartRequirementDaysValue[];
autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue;
autostop_requirement_weeks: number;

View File

@ -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<SidebarProps> = ({ user }) => {
const { entitlements } = useDashboard();
const allowAutostopRequirement =
entitlements.features.template_autostop_requirement.enabled;
const showSchedulePage =
entitlements.features.advanced_template_scheduling.enabled;
return (
<BaseSidebar>
@ -39,7 +39,7 @@ export const Sidebar: FC<SidebarProps> = ({ user }) => {
<SidebarNavItem href="appearance" icon={AppearanceIcon}>
Appearance
</SidebarNavItem>
{allowAutostopRequirement && (
{showSchedulePage && (
<SidebarNavItem href="schedule" icon={ScheduleIcon}>
Schedule
</SidebarNavItem>

View File

@ -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: {