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
46 changed files with 699 additions and 495 deletions

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
}