mirror of https://github.com/coder/coder.git
feat: add inactivity cleanup and failure cleanup configuration fields to Template Schedule Form (#7402)
* added workspace actions entitlement * added workspace actions experiment * added new route for template enterprise meta * removing new route; repurposing old * add new fields to get endpoints * removed workspace actions experiment * added logic to enterprise template store * added new form fields * feature flagged new fields * fix validation * fixed submit btn * fix tests * changed ttl defaults * added FE tests * added BE tests * fixed lint * adjusted comment language * fixing unstaged changes check * fix test * Update coderd/database/migrations/000122_add_template_cleanup_ttls.down.sql Co-authored-by: Dean Sheather <dean@deansheather.com> * Update coderd/database/migrations/000122_add_template_cleanup_ttls.up.sql Co-authored-by: Dean Sheather <dean@deansheather.com> --------- Co-authored-by: Dean Sheather <dean@deansheather.com>
This commit is contained in:
parent
3632ac8c01
commit
5ffa6dae50
|
@ -34,6 +34,7 @@
|
|||
"Dsts",
|
||||
"embeddedpostgres",
|
||||
"enablements",
|
||||
"enterprisemeta",
|
||||
"errgroup",
|
||||
"eventsourcemock",
|
||||
"Failf",
|
||||
|
|
|
@ -6719,10 +6719,18 @@ const docTemplate = `{
|
|||
"description": "DisplayName is the displayed name of the template.",
|
||||
"type": "string"
|
||||
},
|
||||
"failure_ttl_ms": {
|
||||
"description": "FailureTTLMillis allows optionally specifying the max lifetime before Coder\nstops all resources for failed workspaces created from this template.",
|
||||
"type": "integer"
|
||||
},
|
||||
"icon": {
|
||||
"description": "Icon is a relative path or external URL that specifies\nan icon to be displayed in the dashboard.",
|
||||
"type": "string"
|
||||
},
|
||||
"inactivity_ttl_ms": {
|
||||
"description": "InactivityTTLMillis allows optionally specifying the max lifetime before Coder\ndeletes inactive workspaces created from this template.",
|
||||
"type": "integer"
|
||||
},
|
||||
"max_ttl_ms": {
|
||||
"description": "MaxTTLMillis allows optionally specifying the max lifetime for\nworkspaces created from this template.",
|
||||
"type": "integer"
|
||||
|
@ -8698,6 +8706,10 @@ const docTemplate = `{
|
|||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"failure_ttl_ms": {
|
||||
"description": "FailureTTLMillis and InactivityTTLMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.",
|
||||
"type": "integer"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -8705,6 +8717,9 @@ const docTemplate = `{
|
|||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"inactivity_ttl_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_ttl_ms": {
|
||||
"description": "MaxTTLMillis is an enterprise feature. It's value is only used if your\nlicense is entitled to use the advanced template scheduling feature.",
|
||||
"type": "integer"
|
||||
|
|
|
@ -5978,10 +5978,18 @@
|
|||
"description": "DisplayName is the displayed name of the template.",
|
||||
"type": "string"
|
||||
},
|
||||
"failure_ttl_ms": {
|
||||
"description": "FailureTTLMillis allows optionally specifying the max lifetime before Coder\nstops all resources for failed workspaces created from this template.",
|
||||
"type": "integer"
|
||||
},
|
||||
"icon": {
|
||||
"description": "Icon is a relative path or external URL that specifies\nan icon to be displayed in the dashboard.",
|
||||
"type": "string"
|
||||
},
|
||||
"inactivity_ttl_ms": {
|
||||
"description": "InactivityTTLMillis allows optionally specifying the max lifetime before Coder\ndeletes inactive workspaces created from this template.",
|
||||
"type": "integer"
|
||||
},
|
||||
"max_ttl_ms": {
|
||||
"description": "MaxTTLMillis allows optionally specifying the max lifetime for\nworkspaces created from this template.",
|
||||
"type": "integer"
|
||||
|
@ -7816,6 +7824,10 @@
|
|||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"failure_ttl_ms": {
|
||||
"description": "FailureTTLMillis and InactivityTTLMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.",
|
||||
"type": "integer"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -7823,6 +7835,9 @@
|
|||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"inactivity_ttl_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_ttl_ms": {
|
||||
"description": "MaxTTLMillis is an enterprise feature. It's value is only used if your\nlicense is entitled to use the advanced template scheduling feature.",
|
||||
"type": "integer"
|
||||
|
|
|
@ -1945,6 +1945,8 @@ func (q *fakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database
|
|||
tpl.UpdatedAt = database.Now()
|
||||
tpl.DefaultTTL = arg.DefaultTTL
|
||||
tpl.MaxTTL = arg.MaxTTL
|
||||
tpl.FailureTTL = arg.FailureTTL
|
||||
tpl.InactivityTTL = arg.InactivityTTL
|
||||
q.templates[idx] = tpl
|
||||
return tpl.DeepCopy(), nil
|
||||
}
|
||||
|
|
|
@ -478,7 +478,9 @@ CREATE TABLE templates (
|
|||
allow_user_cancel_workspace_jobs boolean DEFAULT true NOT NULL,
|
||||
max_ttl bigint DEFAULT '0'::bigint NOT NULL,
|
||||
allow_user_autostart boolean DEFAULT true NOT NULL,
|
||||
allow_user_autostop boolean DEFAULT true NOT NULL
|
||||
allow_user_autostop boolean DEFAULT true NOT NULL,
|
||||
failure_ttl bigint DEFAULT 0 NOT NULL,
|
||||
inactivity_ttl bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN;
|
||||
ALTER TABLE ONLY templates DROP COLUMN IF EXISTS failure_ttl;
|
||||
ALTER TABLE ONLY templates DROP COLUMN IF EXISTS inactivity_ttl;
|
||||
COMMIT;
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN;
|
||||
ALTER TABLE ONLY templates ADD COLUMN IF NOT EXISTS failure_ttl BIGINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE ONLY templates ADD COLUMN IF NOT EXISTS inactivity_ttl BIGINT NOT NULL DEFAULT 0;
|
||||
COMMIT;
|
|
@ -80,6 +80,8 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
|
|||
&i.MaxTTL,
|
||||
&i.AllowUserAutostart,
|
||||
&i.AllowUserAutostop,
|
||||
&i.FailureTTL,
|
||||
&i.InactivityTTL,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1436,7 +1436,9 @@ type Template struct {
|
|||
// Allow users to specify an autostart schedule for workspaces (enterprise).
|
||||
AllowUserAutostart bool `db:"allow_user_autostart" json:"allow_user_autostart"`
|
||||
// Allow users to specify custom autostop values for workspaces (enterprise).
|
||||
AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"`
|
||||
AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"`
|
||||
FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"`
|
||||
InactivityTTL int64 `db:"inactivity_ttl" json:"inactivity_ttl"`
|
||||
}
|
||||
|
||||
type TemplateVersion struct {
|
||||
|
|
|
@ -3490,7 +3490,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
|
||||
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, inactivity_ttl
|
||||
FROM
|
||||
templates
|
||||
WHERE
|
||||
|
@ -3522,13 +3522,15 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
|||
&i.MaxTTL,
|
||||
&i.AllowUserAutostart,
|
||||
&i.AllowUserAutostop,
|
||||
&i.FailureTTL,
|
||||
&i.InactivityTTL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
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
|
||||
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, inactivity_ttl
|
||||
FROM
|
||||
templates
|
||||
WHERE
|
||||
|
@ -3568,12 +3570,14 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
|||
&i.MaxTTL,
|
||||
&i.AllowUserAutostart,
|
||||
&i.AllowUserAutostop,
|
||||
&i.FailureTTL,
|
||||
&i.InactivityTTL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
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 FROM 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, inactivity_ttl FROM templates
|
||||
ORDER BY (name, id) ASC
|
||||
`
|
||||
|
||||
|
@ -3606,6 +3610,8 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
|||
&i.MaxTTL,
|
||||
&i.AllowUserAutostart,
|
||||
&i.AllowUserAutostop,
|
||||
&i.FailureTTL,
|
||||
&i.InactivityTTL,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -3622,7 +3628,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
|
||||
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, inactivity_ttl
|
||||
FROM
|
||||
templates
|
||||
WHERE
|
||||
|
@ -3692,6 +3698,8 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
|
|||
&i.MaxTTL,
|
||||
&i.AllowUserAutostart,
|
||||
&i.AllowUserAutostop,
|
||||
&i.FailureTTL,
|
||||
&i.InactivityTTL,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -3725,7 +3733,7 @@ INSERT INTO
|
|||
allow_user_cancel_workspace_jobs
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING 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
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING 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, inactivity_ttl
|
||||
`
|
||||
|
||||
type InsertTemplateParams struct {
|
||||
|
@ -3783,6 +3791,8 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
|
|||
&i.MaxTTL,
|
||||
&i.AllowUserAutostart,
|
||||
&i.AllowUserAutostop,
|
||||
&i.FailureTTL,
|
||||
&i.InactivityTTL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -3796,7 +3806,7 @@ SET
|
|||
WHERE
|
||||
id = $3
|
||||
RETURNING
|
||||
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
|
||||
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, inactivity_ttl
|
||||
`
|
||||
|
||||
type UpdateTemplateACLByIDParams struct {
|
||||
|
@ -3828,6 +3838,8 @@ func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTempla
|
|||
&i.MaxTTL,
|
||||
&i.AllowUserAutostart,
|
||||
&i.AllowUserAutostop,
|
||||
&i.FailureTTL,
|
||||
&i.InactivityTTL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -3887,7 +3899,7 @@ SET
|
|||
WHERE
|
||||
id = $1
|
||||
RETURNING
|
||||
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
|
||||
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, inactivity_ttl
|
||||
`
|
||||
|
||||
type UpdateTemplateMetaByIDParams struct {
|
||||
|
@ -3931,6 +3943,8 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
|
|||
&i.MaxTTL,
|
||||
&i.AllowUserAutostart,
|
||||
&i.AllowUserAutostop,
|
||||
&i.FailureTTL,
|
||||
&i.InactivityTTL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -3943,11 +3957,13 @@ SET
|
|||
allow_user_autostart = $3,
|
||||
allow_user_autostop = $4,
|
||||
default_ttl = $5,
|
||||
max_ttl = $6
|
||||
max_ttl = $6,
|
||||
failure_ttl = $7,
|
||||
inactivity_ttl = $8
|
||||
WHERE
|
||||
id = $1
|
||||
RETURNING
|
||||
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
|
||||
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, inactivity_ttl
|
||||
`
|
||||
|
||||
type UpdateTemplateScheduleByIDParams struct {
|
||||
|
@ -3957,6 +3973,8 @@ type UpdateTemplateScheduleByIDParams struct {
|
|||
AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"`
|
||||
DefaultTTL int64 `db:"default_ttl" json:"default_ttl"`
|
||||
MaxTTL int64 `db:"max_ttl" json:"max_ttl"`
|
||||
FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"`
|
||||
InactivityTTL int64 `db:"inactivity_ttl" json:"inactivity_ttl"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) (Template, error) {
|
||||
|
@ -3967,6 +3985,8 @@ func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateT
|
|||
arg.AllowUserAutostop,
|
||||
arg.DefaultTTL,
|
||||
arg.MaxTTL,
|
||||
arg.FailureTTL,
|
||||
arg.InactivityTTL,
|
||||
)
|
||||
var i Template
|
||||
err := row.Scan(
|
||||
|
@ -3989,6 +4009,8 @@ func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateT
|
|||
&i.MaxTTL,
|
||||
&i.AllowUserAutostart,
|
||||
&i.AllowUserAutostop,
|
||||
&i.FailureTTL,
|
||||
&i.InactivityTTL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
@ -118,7 +118,9 @@ SET
|
|||
allow_user_autostart = $3,
|
||||
allow_user_autostop = $4,
|
||||
default_ttl = $5,
|
||||
max_ttl = $6
|
||||
max_ttl = $6,
|
||||
failure_ttl = $7,
|
||||
inactivity_ttl = $8
|
||||
WHERE
|
||||
id = $1
|
||||
RETURNING
|
||||
|
|
|
@ -51,6 +51,8 @@ overrides:
|
|||
template_max_ttl: TemplateMaxTTL
|
||||
motd_file: MOTDFile
|
||||
uuid: UUID
|
||||
failure_ttl: FailureTTL
|
||||
inactivity_ttl: InactivityTTL
|
||||
|
||||
sql:
|
||||
- schema: "./dump.sql"
|
||||
|
|
|
@ -18,6 +18,10 @@ type TemplateScheduleOptions struct {
|
|||
//
|
||||
// If set, users cannot disable automatic workspace shutdown.
|
||||
MaxTTL time.Duration `json:"max_ttl"`
|
||||
// If FailureTTL is set, all failed workspaces will be stopped automatically after this time has elapsed.
|
||||
FailureTTL time.Duration `json:"failure_ttl"`
|
||||
// If InactivityTTL is set, all inactive workspaces will be deleted automatically after this time has elapsed.
|
||||
InactivityTTL time.Duration `json:"inactivity_ttl"`
|
||||
}
|
||||
|
||||
// TemplateScheduleStore provides an interface for retrieving template
|
||||
|
@ -47,9 +51,11 @@ func (*agplTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context
|
|||
UserAutostartEnabled: true,
|
||||
UserAutostopEnabled: true,
|
||||
DefaultTTL: time.Duration(tpl.DefaultTTL),
|
||||
// Disregard the value in the database, since MaxTTL is an enterprise
|
||||
// feature.
|
||||
MaxTTL: 0,
|
||||
// Disregard the values in the database, since MaxTTL, FailureTTL, and InactivityTTL are enterprise
|
||||
// features.
|
||||
MaxTTL: 0,
|
||||
FailureTTL: 0,
|
||||
InactivityTTL: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -68,5 +74,7 @@ func (*agplTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context
|
|||
AllowUserAutostart: tpl.AllowUserAutostart,
|
||||
AllowUserAutostop: tpl.AllowUserAutostop,
|
||||
MaxTTL: tpl.MaxTTL,
|
||||
FailureTTL: tpl.FailureTTL,
|
||||
InactivityTTL: tpl.InactivityTTL,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -476,6 +476,12 @@ 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.FailureTTLMillis < 0 {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "failure_ttl_ms", Detail: "Must be a positive integer."})
|
||||
}
|
||||
if req.InactivityTTLMillis < 0 {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "inactivity_ttl_ms", Detail: "Must be a positive integer."})
|
||||
}
|
||||
|
||||
if len(validErrs) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
|
@ -495,18 +501,14 @@ 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() &&
|
||||
req.MaxTTLMillis == time.Duration(template.MaxTTL).Milliseconds() {
|
||||
req.MaxTTLMillis == time.Duration(template.MaxTTL).Milliseconds() &&
|
||||
req.FailureTTLMillis == time.Duration(template.FailureTTL).Milliseconds() &&
|
||||
req.InactivityTTLMillis == time.Duration(template.InactivityTTL).Milliseconds() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update template metadata -- empty fields are not overwritten,
|
||||
// except for display_name, description, icon, and default_ttl.
|
||||
// These exceptions are required to clear content of these fields with UI.
|
||||
// Users should not be able to clear the template name in the UI
|
||||
name := req.Name
|
||||
displayName := req.DisplayName
|
||||
desc := req.Description
|
||||
icon := req.Icon
|
||||
|
||||
if name == "" {
|
||||
name = template.Name
|
||||
}
|
||||
|
@ -516,9 +518,9 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||
ID: template.ID,
|
||||
UpdatedAt: database.Now(),
|
||||
Name: name,
|
||||
DisplayName: displayName,
|
||||
Description: desc,
|
||||
Icon: icon,
|
||||
DisplayName: req.DisplayName,
|
||||
Description: req.Description,
|
||||
Icon: req.Icon,
|
||||
AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -527,8 +529,13 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||
|
||||
defaultTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond
|
||||
maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond
|
||||
failureTTL := time.Duration(req.FailureTTLMillis) * time.Millisecond
|
||||
inactivityTTL := time.Duration(req.InactivityTTLMillis) * time.Millisecond
|
||||
|
||||
if defaultTTL != time.Duration(template.DefaultTTL) ||
|
||||
maxTTL != time.Duration(template.MaxTTL) ||
|
||||
failureTTL != time.Duration(template.FailureTTL) ||
|
||||
inactivityTTL != time.Duration(template.InactivityTTL) ||
|
||||
req.AllowUserAutostart != template.AllowUserAutostart ||
|
||||
req.AllowUserAutostop != template.AllowUserAutostop {
|
||||
updated, err = (*api.TemplateScheduleStore.Load()).SetTemplateScheduleOptions(ctx, tx, updated, schedule.TemplateScheduleOptions{
|
||||
|
@ -539,6 +546,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||
UserAutostopEnabled: req.AllowUserAutostop,
|
||||
DefaultTTL: defaultTTL,
|
||||
MaxTTL: maxTTL,
|
||||
FailureTTL: failureTTL,
|
||||
InactivityTTL: inactivityTTL,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set template schedule options: %w", err)
|
||||
|
@ -678,5 +687,7 @@ func (api *API) convertTemplate(
|
|||
AllowUserAutostart: template.AllowUserAutostart,
|
||||
AllowUserAutostop: template.AllowUserAutostop,
|
||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
||||
FailureTTLMillis: time.Duration(template.FailureTTL).Milliseconds(),
|
||||
InactivityTTLMillis: time.Duration(template.InactivityTTL).Milliseconds(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -604,6 +604,90 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||
})
|
||||
})
|
||||
|
||||
t.Run("CleanupTTLs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
failureTTL = 7 * 24 * time.Hour
|
||||
inactivityTTL = 180 * 24 * time.Hour
|
||||
)
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var setCalled int64
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
|
||||
SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) {
|
||||
if atomic.AddInt64(&setCalled, 1) == 2 {
|
||||
require.Equal(t, failureTTL, options.FailureTTL)
|
||||
require.Equal(t, inactivityTTL, options.InactivityTTL)
|
||||
}
|
||||
template.FailureTTL = int64(options.FailureTTL)
|
||||
template.InactivityTTL = int64(options.InactivityTTL)
|
||||
return template, nil
|
||||
},
|
||||
},
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.FailureTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds())
|
||||
ctr.InactivityTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds())
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||
Name: template.Name,
|
||||
DisplayName: template.DisplayName,
|
||||
Description: template.Description,
|
||||
Icon: template.Icon,
|
||||
DefaultTTLMillis: 0,
|
||||
MaxTTLMillis: 0,
|
||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
||||
FailureTTLMillis: failureTTL.Milliseconds(),
|
||||
InactivityTTLMillis: inactivityTTL.Milliseconds(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EqualValues(t, 2, atomic.LoadInt64(&setCalled))
|
||||
require.Equal(t, failureTTL.Milliseconds(), got.FailureTTLMillis)
|
||||
require.Equal(t, inactivityTTL.Milliseconds(), got.InactivityTTLMillis)
|
||||
})
|
||||
|
||||
t.Run("IgnoredUnlicensed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.FailureTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds())
|
||||
ctr.InactivityTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds())
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||
Name: template.Name,
|
||||
DisplayName: template.DisplayName,
|
||||
Description: template.Description,
|
||||
Icon: template.Icon,
|
||||
DefaultTTLMillis: template.DefaultTTLMillis,
|
||||
MaxTTLMillis: template.MaxTTLMillis,
|
||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
||||
FailureTTLMillis: failureTTL.Milliseconds(),
|
||||
InactivityTTLMillis: inactivityTTL.Milliseconds(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, got.FailureTTLMillis)
|
||||
require.Zero(t, got.InactivityTTLMillis)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("AllowUserScheduling", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
@ -46,7 +46,6 @@ const (
|
|||
FeatureAppearance FeatureName = "appearance"
|
||||
FeatureAdvancedTemplateScheduling FeatureName = "advanced_template_scheduling"
|
||||
FeatureWorkspaceProxy FeatureName = "workspace_proxy"
|
||||
FeatureWorkspaceActions FeatureName = "workspace_actions"
|
||||
)
|
||||
|
||||
// FeatureNames must be kept in-sync with the Feature enum above.
|
||||
|
@ -62,7 +61,6 @@ var FeatureNames = []FeatureName{
|
|||
FeatureAppearance,
|
||||
FeatureAdvancedTemplateScheduling,
|
||||
FeatureWorkspaceProxy,
|
||||
FeatureWorkspaceActions,
|
||||
}
|
||||
|
||||
// Humanize returns the feature name in a human-readable format.
|
||||
|
|
|
@ -106,6 +106,13 @@ type CreateTemplateRequest struct {
|
|||
// false, the DefaultTTL will always be used. This can only be disabled when
|
||||
// using an enterprise license.
|
||||
AllowUserAutostop *bool `json:"allow_user_autostop"`
|
||||
|
||||
// FailureTTLMillis allows optionally specifying the max lifetime before Coder
|
||||
// stops all resources for failed workspaces created from this template.
|
||||
FailureTTLMillis *int64 `json:"failure_ttl_ms,omitempty"`
|
||||
// InactivityTTLMillis allows optionally specifying the max lifetime before Coder
|
||||
// deletes inactive workspaces created from this template.
|
||||
InactivityTTLMillis *int64 `json:"inactivity_ttl_ms,omitempty"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceRequest provides options for creating a new workspace.
|
||||
|
|
|
@ -40,6 +40,12 @@ type Template struct {
|
|||
AllowUserAutostart bool `json:"allow_user_autostart"`
|
||||
AllowUserAutostop bool `json:"allow_user_autostop"`
|
||||
AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs"`
|
||||
|
||||
// FailureTTLMillis and InactivityTTLMillis are enterprise-only. Their
|
||||
// values are used if your license is entitled to use the advanced
|
||||
// template scheduling feature.
|
||||
FailureTTLMillis int64 `json:"failure_ttl_ms"`
|
||||
InactivityTTLMillis int64 `json:"inactivity_ttl_ms"`
|
||||
}
|
||||
|
||||
type TransitionStats struct {
|
||||
|
@ -95,6 +101,8 @@ type UpdateTemplateMeta struct {
|
|||
AllowUserAutostart bool `json:"allow_user_autostart,omitempty"`
|
||||
AllowUserAutostop bool `json:"allow_user_autostop,omitempty"`
|
||||
AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"`
|
||||
FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"`
|
||||
InactivityTTLMillis int64 `json:"inactivity_ttl_ms,omitempty"`
|
||||
}
|
||||
|
||||
type TemplateExample struct {
|
||||
|
|
|
@ -9,18 +9,18 @@ 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> |
|
||||
| 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>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></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> |
|
||||
| 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>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</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>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>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>git_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>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>false</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</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>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>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_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>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>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>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> |
|
||||
| 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>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></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> |
|
||||
| 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>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</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>inactivity_ttl</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>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>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>git_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>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>false</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</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>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>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_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>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>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>wildcard_hostname</td><td>true</td></tr></tbody></table> |
|
||||
|
||||
<!-- End generated by 'make docs/admin/audit-logs.md'. -->
|
||||
|
||||
|
|
|
@ -1333,7 +1333,9 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
"default_ttl_ms": 0,
|
||||
"description": "string",
|
||||
"display_name": "string",
|
||||
"failure_ttl_ms": 0,
|
||||
"icon": "string",
|
||||
"inactivity_ttl_ms": 0,
|
||||
"max_ttl_ms": 0,
|
||||
"name": "string",
|
||||
"parameter_values": [
|
||||
|
@ -1359,7 +1361,9 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
| `default_ttl_ms` | integer | false | | Default ttl ms allows optionally specifying the default TTL for all workspaces created from this template. |
|
||||
| `description` | string | false | | Description is a description of what the template contains. It must be less than 128 bytes. |
|
||||
| `display_name` | string | false | | Display name is the displayed name of the 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. |
|
||||
| `inactivity_ttl_ms` | integer | false | | Inactivity ttl ms allows optionally specifying the max lifetime before Coder deletes inactive workspaces created from this template. |
|
||||
| `max_ttl_ms` | integer | false | | Max ttl ms allows optionally specifying the max lifetime for workspaces created from this template. |
|
||||
| `name` | string | true | | Name is the name of the template. |
|
||||
| `parameter_values` | array of [codersdk.CreateParameterRequest](#codersdkcreateparameterrequest) | false | | Parameter values is a structure used to create a new parameter value for a scope.] |
|
||||
|
@ -3866,8 +3870,10 @@ Parameter represents a set value for the scope.
|
|||
"default_ttl_ms": 0,
|
||||
"description": "string",
|
||||
"display_name": "string",
|
||||
"failure_ttl_ms": 0,
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"inactivity_ttl_ms": 0,
|
||||
"max_ttl_ms": 0,
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
|
@ -3892,8 +3898,10 @@ Parameter represents a set value for the scope.
|
|||
| `default_ttl_ms` | integer | false | | |
|
||||
| `description` | string | false | | |
|
||||
| `display_name` | string | false | | |
|
||||
| `failure_ttl_ms` | integer | false | | Failure ttl ms and InactivityTTLMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. |
|
||||
| `icon` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `inactivity_ttl_ms` | integer | false | | |
|
||||
| `max_ttl_ms` | integer | false | | Max ttl ms is an enterprise feature. It's value is only used if your license is entitled to use the advanced template scheduling feature. |
|
||||
| `name` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
|
|
|
@ -47,8 +47,10 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
|||
"default_ttl_ms": 0,
|
||||
"description": "string",
|
||||
"display_name": "string",
|
||||
"failure_ttl_ms": 0,
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"inactivity_ttl_ms": 0,
|
||||
"max_ttl_ms": 0,
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
|
@ -86,8 +88,10 @@ Status Code **200**
|
|||
| `» default_ttl_ms` | integer | false | | |
|
||||
| `» description` | string | false | | |
|
||||
| `» display_name` | string | false | | |
|
||||
| `» failure_ttl_ms` | integer | false | | Failure ttl ms and InactivityTTLMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. |
|
||||
| `» icon` | string | false | | |
|
||||
| `» id` | string(uuid) | false | | |
|
||||
| `» inactivity_ttl_ms` | integer | false | | |
|
||||
| `» max_ttl_ms` | integer | false | | Max ttl ms is an enterprise feature. It's value is only used if your license is entitled to use the advanced template scheduling feature. |
|
||||
| `» name` | string | false | | |
|
||||
| `» organization_id` | string(uuid) | false | | |
|
||||
|
@ -126,7 +130,9 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
|
|||
"default_ttl_ms": 0,
|
||||
"description": "string",
|
||||
"display_name": "string",
|
||||
"failure_ttl_ms": 0,
|
||||
"icon": "string",
|
||||
"inactivity_ttl_ms": 0,
|
||||
"max_ttl_ms": 0,
|
||||
"name": "string",
|
||||
"parameter_values": [
|
||||
|
@ -176,8 +182,10 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
|
|||
"default_ttl_ms": 0,
|
||||
"description": "string",
|
||||
"display_name": "string",
|
||||
"failure_ttl_ms": 0,
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"inactivity_ttl_ms": 0,
|
||||
"max_ttl_ms": 0,
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
|
@ -301,8 +309,10 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
|||
"default_ttl_ms": 0,
|
||||
"description": "string",
|
||||
"display_name": "string",
|
||||
"failure_ttl_ms": 0,
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"inactivity_ttl_ms": 0,
|
||||
"max_ttl_ms": 0,
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
|
@ -628,8 +638,10 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \
|
|||
"default_ttl_ms": 0,
|
||||
"description": "string",
|
||||
"display_name": "string",
|
||||
"failure_ttl_ms": 0,
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"inactivity_ttl_ms": 0,
|
||||
"max_ttl_ms": 0,
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
|
@ -736,8 +748,10 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
|
|||
"default_ttl_ms": 0,
|
||||
"description": "string",
|
||||
"display_name": "string",
|
||||
"failure_ttl_ms": 0,
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"inactivity_ttl_ms": 0,
|
||||
"max_ttl_ms": 0,
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
|
|
|
@ -75,6 +75,8 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
|||
"allow_user_autostop": ActionTrack,
|
||||
"allow_user_cancel_workspace_jobs": ActionTrack,
|
||||
"max_ttl": ActionTrack,
|
||||
"failure_ttl": ActionTrack,
|
||||
"inactivity_ttl": ActionTrack,
|
||||
},
|
||||
&database.TemplateVersion{}: {
|
||||
"id": ActionTrack,
|
||||
|
|
|
@ -326,7 +326,6 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
|||
codersdk.FeatureExternalProvisionerDaemons: true,
|
||||
codersdk.FeatureAdvancedTemplateScheduling: true,
|
||||
codersdk.FeatureWorkspaceProxy: true,
|
||||
codersdk.FeatureWorkspaceActions: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -54,7 +54,6 @@ func TestEntitlements(t *testing.T) {
|
|||
codersdk.FeatureExternalProvisionerDaemons: 1,
|
||||
codersdk.FeatureAdvancedTemplateScheduling: 1,
|
||||
codersdk.FeatureWorkspaceProxy: 1,
|
||||
codersdk.FeatureWorkspaceActions: 1,
|
||||
},
|
||||
GraceAt: time.Now().Add(59 * 24 * time.Hour),
|
||||
})
|
||||
|
|
|
@ -324,12 +324,16 @@ func (*enterpriseTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.C
|
|||
UserAutostopEnabled: tpl.AllowUserAutostop,
|
||||
DefaultTTL: time.Duration(tpl.DefaultTTL),
|
||||
MaxTTL: time.Duration(tpl.MaxTTL),
|
||||
FailureTTL: time.Duration(tpl.FailureTTL),
|
||||
InactivityTTL: time.Duration(tpl.InactivityTTL),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (*enterpriseTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, tpl database.Template, opts schedule.TemplateScheduleOptions) (database.Template, error) {
|
||||
if int64(opts.DefaultTTL) == tpl.DefaultTTL &&
|
||||
int64(opts.MaxTTL) == tpl.MaxTTL &&
|
||||
int64(opts.FailureTTL) == tpl.FailureTTL &&
|
||||
int64(opts.InactivityTTL) == tpl.InactivityTTL &&
|
||||
opts.UserAutostartEnabled == tpl.AllowUserAutostart &&
|
||||
opts.UserAutostopEnabled == tpl.AllowUserAutostop {
|
||||
// Avoid updating the UpdatedAt timestamp if nothing will be changed.
|
||||
|
@ -343,6 +347,8 @@ func (*enterpriseTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.C
|
|||
AllowUserAutostop: opts.UserAutostopEnabled,
|
||||
DefaultTTL: int64(opts.DefaultTTL),
|
||||
MaxTTL: int64(opts.MaxTTL),
|
||||
FailureTTL: int64(opts.FailureTTL),
|
||||
InactivityTTL: int64(opts.InactivityTTL),
|
||||
})
|
||||
if err != nil {
|
||||
return database.Template{}, xerrors.Errorf("update template schedule: %w", err)
|
||||
|
|
|
@ -194,6 +194,8 @@ export interface CreateTemplateRequest {
|
|||
readonly allow_user_cancel_workspace_jobs?: boolean
|
||||
readonly allow_user_autostart?: boolean
|
||||
readonly allow_user_autostop?: boolean
|
||||
readonly failure_ttl_ms?: number
|
||||
readonly inactivity_ttl_ms?: number
|
||||
}
|
||||
|
||||
// From codersdk/templateversions.go
|
||||
|
@ -841,6 +843,8 @@ export interface Template {
|
|||
readonly allow_user_autostart: boolean
|
||||
readonly allow_user_autostop: boolean
|
||||
readonly allow_user_cancel_workspace_jobs: boolean
|
||||
readonly failure_ttl_ms: number
|
||||
readonly inactivity_ttl_ms: number
|
||||
}
|
||||
|
||||
// From codersdk/templates.go
|
||||
|
@ -1010,6 +1014,8 @@ export interface UpdateTemplateMeta {
|
|||
readonly allow_user_autostart?: boolean
|
||||
readonly allow_user_autostop?: boolean
|
||||
readonly allow_user_cancel_workspace_jobs?: boolean
|
||||
readonly failure_ttl_ms?: number
|
||||
readonly inactivity_ttl_ms?: number
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
|
@ -1376,7 +1382,6 @@ export type FeatureName =
|
|||
| "scim"
|
||||
| "template_rbac"
|
||||
| "user_limit"
|
||||
| "workspace_actions"
|
||||
| "workspace_proxy"
|
||||
export const FeatureNames: FeatureName[] = [
|
||||
"advanced_template_scheduling",
|
||||
|
@ -1389,7 +1394,6 @@ export const FeatureNames: FeatureName[] = [
|
|||
"scim",
|
||||
"template_rbac",
|
||||
"user_limit",
|
||||
"workspace_actions",
|
||||
"workspace_proxy",
|
||||
]
|
||||
|
||||
|
|
|
@ -19,6 +19,12 @@
|
|||
"maxTTLHelperText_zero": "Workspaces may run indefinitely.",
|
||||
"maxTTLHelperText_one": "Workspaces must stop within 1 hour of starting.",
|
||||
"maxTTLHelperText_other": "Workspaces must stop within {{count}} hours of starting.",
|
||||
"failureTTLHelperText_zero": "Coder will not automatically stop failed workspaces",
|
||||
"failureTTLHelperText_one": "Coder will automatically stop failed workspaces after {{count}} day.",
|
||||
"failureTTLHelperText_other": "Coder will automatically stop failed workspaces after {{count}} days.",
|
||||
"inactivityTTLHelperText_zero": "Coder will not automatically delete inactive workspaces",
|
||||
"inactivityTTLHelperText_one": "Coder will automatically delete inactive workspaces after {{count}} day.",
|
||||
"inactivityTTLHelperText_other": "Coder will automatically delete inactive workspaces after {{count}} days.",
|
||||
"allowUserCancelWorkspaceJobsLabel": "Allow users to cancel in-progress workspace jobs.",
|
||||
"allowUserCancelWorkspaceJobsNotice": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases.",
|
||||
"allowUsersCancelHelperText": "If checked, users may be able to corrupt their workspace.",
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
import TextField from "@material-ui/core/TextField"
|
||||
import { Template, UpdateTemplateMeta } from "api/typesGenerated"
|
||||
import { FormikTouched, useFormik } from "formik"
|
||||
import { FC } from "react"
|
||||
import { FC, ChangeEvent } from "react"
|
||||
import { getFormHelpers } from "utils/formUtils"
|
||||
import * as Yup from "yup"
|
||||
import i18next from "i18next"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Maybe } from "components/Conditionals/Maybe"
|
||||
import { FormSection, HorizontalForm, FormFooter } from "components/Form/Form"
|
||||
import {
|
||||
FormSection,
|
||||
HorizontalForm,
|
||||
FormFooter,
|
||||
FormFields,
|
||||
} from "components/Form/Form"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import Checkbox from "@material-ui/core/Checkbox"
|
||||
import FormControlLabel from "@material-ui/core/FormControlLabel"
|
||||
import Switch from "@material-ui/core/Switch"
|
||||
|
||||
const TTLHelperText = ({
|
||||
ttl,
|
||||
|
@ -32,6 +39,14 @@ const TTLHelperText = ({
|
|||
|
||||
const MAX_TTL_DAYS = 7
|
||||
const MS_HOUR_CONVERSION = 3600000
|
||||
const MS_DAY_CONVERSION = 86400000
|
||||
const FAILURE_CLEANUP_DEFAULT = 7
|
||||
const INACTIVITY_CLEANUP_DEFAULT = 180
|
||||
|
||||
export interface TemplateScheduleFormValues extends UpdateTemplateMeta {
|
||||
failure_cleanup_enabled: boolean
|
||||
inactivity_cleanup_enabled: boolean
|
||||
}
|
||||
|
||||
export const getValidationSchema = (): Yup.AnyObjectSchema =>
|
||||
Yup.object({
|
||||
|
@ -49,6 +64,36 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
|
|||
24 * MAX_TTL_DAYS /* 7 days in hours */,
|
||||
i18next.t("maxTTLMaxError", { ns: "templateSettingsPage" }),
|
||||
),
|
||||
failure_ttl_ms: Yup.number()
|
||||
.integer()
|
||||
.min(0, "Failure cleanup days must not be less than 0.")
|
||||
.test(
|
||||
"positive-if-enabled",
|
||||
"Failure cleanup days must be greater than zero when enabled.",
|
||||
function (value) {
|
||||
const parent = this.parent as TemplateScheduleFormValues
|
||||
if (parent.failure_cleanup_enabled) {
|
||||
return Boolean(value)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
),
|
||||
inactivity_ttl_ms: Yup.number()
|
||||
.integer()
|
||||
.min(0, "Inactivity cleanup days must not be less than 0.")
|
||||
.test(
|
||||
"positive-if-enabled",
|
||||
"Inactivity cleanup days must be greater than zero when enabled.",
|
||||
function (value) {
|
||||
const parent = this.parent as TemplateScheduleFormValues
|
||||
if (parent.inactivity_cleanup_enabled) {
|
||||
return Boolean(value)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
),
|
||||
allow_user_autostart: Yup.boolean(),
|
||||
allow_user_autostop: Yup.boolean(),
|
||||
})
|
||||
|
@ -60,6 +105,7 @@ export interface TemplateScheduleForm {
|
|||
isSubmitting: boolean
|
||||
error?: unknown
|
||||
allowAdvancedScheduling: boolean
|
||||
allowWorkspaceActions: boolean
|
||||
// Helpful to show field errors on Storybook
|
||||
initialTouched?: FormikTouched<UpdateTemplateMeta>
|
||||
}
|
||||
|
@ -70,22 +116,34 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
onCancel,
|
||||
error,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
isSubmitting,
|
||||
initialTouched,
|
||||
}) => {
|
||||
const { t: commonT } = useTranslation("common")
|
||||
const validationSchema = getValidationSchema()
|
||||
const form = useFormik<UpdateTemplateMeta>({
|
||||
const form = useFormik<TemplateScheduleFormValues>({
|
||||
initialValues: {
|
||||
// on display, convert from ms => hours
|
||||
default_ttl_ms: template.default_ttl_ms / MS_HOUR_CONVERSION,
|
||||
// the API ignores this value, but to avoid tripping up validation set
|
||||
// the API ignores these values, but to avoid tripping up validation set
|
||||
// it to zero if the user can't set the field.
|
||||
max_ttl_ms: allowAdvancedScheduling
|
||||
? template.max_ttl_ms / MS_HOUR_CONVERSION
|
||||
: 0,
|
||||
failure_ttl_ms: allowAdvancedScheduling
|
||||
? template.failure_ttl_ms / MS_DAY_CONVERSION
|
||||
: 0,
|
||||
inactivity_ttl_ms: allowAdvancedScheduling
|
||||
? template.inactivity_ttl_ms / MS_DAY_CONVERSION
|
||||
: 0,
|
||||
|
||||
allow_user_autostart: template.allow_user_autostart,
|
||||
allow_user_autostop: template.allow_user_autostop,
|
||||
failure_cleanup_enabled:
|
||||
allowAdvancedScheduling && Boolean(template.failure_ttl_ms),
|
||||
inactivity_cleanup_enabled:
|
||||
allowAdvancedScheduling && Boolean(template.inactivity_ttl_ms),
|
||||
},
|
||||
validationSchema,
|
||||
onSubmit: (formData) => {
|
||||
|
@ -97,16 +155,64 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
max_ttl_ms: formData.max_ttl_ms
|
||||
? formData.max_ttl_ms * MS_HOUR_CONVERSION
|
||||
: undefined,
|
||||
failure_ttl_ms: formData.failure_ttl_ms
|
||||
? formData.failure_ttl_ms * MS_DAY_CONVERSION
|
||||
: undefined,
|
||||
inactivity_ttl_ms: formData.inactivity_ttl_ms
|
||||
? formData.inactivity_ttl_ms * MS_DAY_CONVERSION
|
||||
: undefined,
|
||||
|
||||
allow_user_autostart: formData.allow_user_autostart,
|
||||
allow_user_autostop: formData.allow_user_autostop,
|
||||
})
|
||||
},
|
||||
initialTouched,
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers<UpdateTemplateMeta>(form, error)
|
||||
const getFieldHelpers = getFormHelpers<TemplateScheduleFormValues>(
|
||||
form,
|
||||
error,
|
||||
)
|
||||
const { t } = useTranslation("templateSettingsPage")
|
||||
const styles = useStyles()
|
||||
|
||||
const handleToggleFailureCleanup = async (e: ChangeEvent) => {
|
||||
form.handleChange(e)
|
||||
if (!form.values.failure_cleanup_enabled) {
|
||||
// fill failure_ttl_ms with defaults
|
||||
await form.setValues({
|
||||
...form.values,
|
||||
failure_cleanup_enabled: true,
|
||||
failure_ttl_ms: FAILURE_CLEANUP_DEFAULT,
|
||||
})
|
||||
} else {
|
||||
// clear failure_ttl_ms
|
||||
await form.setValues({
|
||||
...form.values,
|
||||
failure_cleanup_enabled: false,
|
||||
failure_ttl_ms: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleInactivityCleanup = async (e: ChangeEvent) => {
|
||||
form.handleChange(e)
|
||||
if (!form.values.inactivity_cleanup_enabled) {
|
||||
// fill inactivity_ttl_ms with defaults
|
||||
await form.setValues({
|
||||
...form.values,
|
||||
inactivity_cleanup_enabled: true,
|
||||
inactivity_ttl_ms: INACTIVITY_CLEANUP_DEFAULT,
|
||||
})
|
||||
} else {
|
||||
// clear inactivity_ttl_ms
|
||||
await form.setValues({
|
||||
...form.values,
|
||||
inactivity_cleanup_enabled: false,
|
||||
inactivity_ttl_ms: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<HorizontalForm
|
||||
onSubmit={form.handleSubmit}
|
||||
|
@ -215,8 +321,85 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
</Stack>
|
||||
</Stack>
|
||||
</FormSection>
|
||||
|
||||
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
|
||||
{allowAdvancedScheduling && allowWorkspaceActions && (
|
||||
<>
|
||||
<FormSection
|
||||
title="Failure Cleanup"
|
||||
description="When enabled, Coder will automatically stop workspaces that are in a failed state after a specified number of days."
|
||||
>
|
||||
<FormFields>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="failureCleanupEnabled"
|
||||
checked={form.values.failure_cleanup_enabled}
|
||||
onChange={handleToggleFailureCleanup}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Enable Failure Cleanup"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"failure_ttl_ms",
|
||||
<TTLHelperText
|
||||
translationName="failureTTLHelperText"
|
||||
ttl={form.values.failure_ttl_ms}
|
||||
/>,
|
||||
)}
|
||||
disabled={isSubmitting || !form.values.failure_cleanup_enabled}
|
||||
fullWidth
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label="Time until cleanup (days)"
|
||||
variant="outlined"
|
||||
type="number"
|
||||
aria-label="Failure Cleanup"
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
title="Inactivity Cleanup"
|
||||
description="When enabled, Coder will automatically delete workspaces that are in an inactive state after a specified number of days."
|
||||
>
|
||||
<FormFields>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="inactivityCleanupEnabled"
|
||||
checked={form.values.inactivity_cleanup_enabled}
|
||||
onChange={handleToggleInactivityCleanup}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Enable Inactivity Cleanup"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"inactivity_ttl_ms",
|
||||
<TTLHelperText
|
||||
translationName="inactivityTTLHelperText"
|
||||
ttl={form.values.inactivity_ttl_ms}
|
||||
/>,
|
||||
)}
|
||||
disabled={
|
||||
isSubmitting || !form.values.inactivity_cleanup_enabled
|
||||
}
|
||||
fullWidth
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label="Time until cleanup (days)"
|
||||
variant="outlined"
|
||||
type="number"
|
||||
aria-label="Inactivity Cleanup"
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
</>
|
||||
)}
|
||||
<FormFooter
|
||||
onCancel={onCancel}
|
||||
isLoading={isSubmitting}
|
||||
submitDisabled={!form.isValid || !form.dirty}
|
||||
/>
|
||||
</HorizontalForm>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@ import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter
|
|||
import {
|
||||
MockEntitlementsWithScheduling,
|
||||
MockTemplate,
|
||||
} from "../../../testHelpers/entities"
|
||||
} from "testHelpers/entities"
|
||||
import {
|
||||
renderWithTemplateSettingsLayout,
|
||||
waitForLoaderToBeRemoved,
|
||||
} from "../../../testHelpers/renderHelpers"
|
||||
} from "testHelpers/renderHelpers"
|
||||
import { getValidationSchema } from "./TemplateScheduleForm"
|
||||
import TemplateSchedulePage from "./TemplateSchedulePage"
|
||||
import i18next from "i18next"
|
||||
|
@ -20,6 +20,8 @@ const { t } = i18next
|
|||
const validFormValues = {
|
||||
default_ttl_ms: 1,
|
||||
max_ttl_ms: 2,
|
||||
failure_ttl_ms: 7,
|
||||
inactivity_ttl_ms: 180,
|
||||
}
|
||||
|
||||
const renderTemplateSchedulePage = async () => {
|
||||
|
@ -33,9 +35,13 @@ const renderTemplateSchedulePage = async () => {
|
|||
const fillAndSubmitForm = async ({
|
||||
default_ttl_ms,
|
||||
max_ttl_ms,
|
||||
failure_ttl_ms,
|
||||
inactivity_ttl_ms,
|
||||
}: {
|
||||
default_ttl_ms: number
|
||||
max_ttl_ms: number
|
||||
failure_ttl_ms: number
|
||||
inactivity_ttl_ms: number
|
||||
}) => {
|
||||
const user = userEvent.setup()
|
||||
const defaultTtlLabel = t("defaultTtlLabel", { ns: "templateSettingsPage" })
|
||||
|
@ -48,6 +54,16 @@ const fillAndSubmitForm = async ({
|
|||
await user.clear(maxTtlField)
|
||||
await user.type(maxTtlField, max_ttl_ms.toString())
|
||||
|
||||
const failureTtlField = screen.getByRole("checkbox", {
|
||||
name: /Failure Cleanup/i,
|
||||
})
|
||||
await user.type(failureTtlField, failure_ttl_ms.toString())
|
||||
|
||||
const inactivityTtlField = screen.getByRole("checkbox", {
|
||||
name: /Inactivity Cleanup/i,
|
||||
})
|
||||
await user.type(inactivityTtlField, inactivity_ttl_ms.toString())
|
||||
|
||||
const submitButton = await screen.findByText(
|
||||
FooterFormLanguage.defaultSubmitLabel,
|
||||
)
|
||||
|
@ -59,6 +75,9 @@ describe("TemplateSchedulePage", () => {
|
|||
jest
|
||||
.spyOn(API, "getEntitlements")
|
||||
.mockResolvedValue(MockEntitlementsWithScheduling)
|
||||
|
||||
// remove when https://github.com/coder/coder/milestone/19 is completed.
|
||||
jest.spyOn(API, "getExperiments").mockResolvedValue(["workspace_actions"])
|
||||
})
|
||||
|
||||
it("succeeds", async () => {
|
||||
|
@ -71,7 +90,7 @@ describe("TemplateSchedulePage", () => {
|
|||
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1))
|
||||
})
|
||||
|
||||
test("ttl is converted to and from hours", async () => {
|
||||
test("default and max ttl is converted to and from hours", async () => {
|
||||
await renderTemplateSchedulePage()
|
||||
|
||||
jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
|
||||
|
@ -92,7 +111,28 @@ describe("TemplateSchedulePage", () => {
|
|||
)
|
||||
})
|
||||
|
||||
it("allows a ttl of 7 days", () => {
|
||||
test("failure and inactivity ttl converted to and from days", async () => {
|
||||
await renderTemplateSchedulePage()
|
||||
|
||||
jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
|
||||
...MockTemplate,
|
||||
...validFormValues,
|
||||
})
|
||||
|
||||
await fillAndSubmitForm(validFormValues)
|
||||
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1))
|
||||
await waitFor(() =>
|
||||
expect(API.updateTemplateMeta).toBeCalledWith(
|
||||
"test-template",
|
||||
expect.objectContaining({
|
||||
failure_ttl_ms: validFormValues.failure_ttl_ms * 86400000,
|
||||
inactivity_ttl_ms: validFormValues.inactivity_ttl_ms * 86400000,
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it("allows a default ttl of 7 days", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
default_ttl_ms: 24 * 7,
|
||||
|
@ -101,7 +141,7 @@ describe("TemplateSchedulePage", () => {
|
|||
expect(validate).not.toThrowError()
|
||||
})
|
||||
|
||||
it("allows ttl of 0", () => {
|
||||
it("allows default ttl of 0", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
default_ttl_ms: 0,
|
||||
|
@ -110,7 +150,7 @@ describe("TemplateSchedulePage", () => {
|
|||
expect(validate).not.toThrowError()
|
||||
})
|
||||
|
||||
it("disallows a ttl of 7 days + 1 hour", () => {
|
||||
it("disallows a default ttl of 7 days + 1 hour", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
default_ttl_ms: 24 * 7 + 1,
|
||||
|
@ -120,4 +160,62 @@ describe("TemplateSchedulePage", () => {
|
|||
t("defaultTTLMaxError", { ns: "templateSettingsPage" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("allows a failure ttl of 7 days", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
failure_ttl_ms: 86400000 * 7,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).not.toThrowError()
|
||||
})
|
||||
|
||||
it("allows failure ttl of 0", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
failure_ttl_ms: 0,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).not.toThrowError()
|
||||
})
|
||||
|
||||
it("disallows a negative failure ttl", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
failure_ttl_ms: -1,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).toThrowError(
|
||||
"Failure cleanup days must not be less than 0.",
|
||||
)
|
||||
})
|
||||
|
||||
it("allows an inactivity ttl of 7 days", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
inactivity_ttl_ms: 86400000 * 7,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).not.toThrowError()
|
||||
})
|
||||
|
||||
it("allows an inactivity ttl of 0", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
inactivity_ttl_ms: 0,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).not.toThrowError()
|
||||
})
|
||||
|
||||
it("disallows a negative inactivity ttl", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
inactivity_ttl_ms: -1,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).toThrowError(
|
||||
"Inactivity cleanup days must not be less than 0.",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -14,9 +14,13 @@ const TemplateSchedulePage: FC = () => {
|
|||
const { template: templateName } = useParams() as { template: string }
|
||||
const navigate = useNavigate()
|
||||
const { template } = useTemplateSettingsContext()
|
||||
const { entitlements } = useDashboard()
|
||||
const { entitlements, experiments } = useDashboard()
|
||||
const allowAdvancedScheduling =
|
||||
entitlements.features["advanced_template_scheduling"].enabled
|
||||
// This check can be removed when https://github.com/coder/coder/milestone/19
|
||||
// is merged up
|
||||
const allowWorkspaceActions = experiments.includes("workspace_actions")
|
||||
|
||||
const {
|
||||
mutate: updateTemplate,
|
||||
isLoading: isSubmitting,
|
||||
|
@ -37,6 +41,7 @@ const TemplateSchedulePage: FC = () => {
|
|||
</Helmet>
|
||||
<TemplateSchedulePageView
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
allowWorkspaceActions={allowWorkspaceActions}
|
||||
isSubmitting={isSubmitting}
|
||||
template={template}
|
||||
submitError={submitError}
|
||||
|
|
|
@ -11,6 +11,7 @@ export default {
|
|||
component: TemplateSchedulePageView,
|
||||
args: {
|
||||
allowAdvancedScheduling: true,
|
||||
allowWorkspaceActions: true,
|
||||
template: MockTemplate,
|
||||
onSubmit: action("onSubmit"),
|
||||
onCancel: action("cancel"),
|
||||
|
|
|
@ -12,6 +12,7 @@ export interface TemplateSchedulePageViewProps {
|
|||
submitError?: unknown
|
||||
initialTouched?: ComponentProps<typeof TemplateScheduleForm>["initialTouched"]
|
||||
allowAdvancedScheduling: boolean
|
||||
allowWorkspaceActions: boolean
|
||||
}
|
||||
|
||||
export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
||||
|
@ -20,6 +21,7 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
|||
onSubmit,
|
||||
isSubmitting,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
submitError,
|
||||
initialTouched,
|
||||
}) => {
|
||||
|
@ -33,6 +35,7 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
|||
|
||||
<TemplateScheduleForm
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
allowWorkspaceActions={allowWorkspaceActions}
|
||||
initialTouched={initialTouched}
|
||||
isSubmitting={isSubmitting}
|
||||
template={template}
|
||||
|
|
|
@ -341,6 +341,8 @@ export const MockTemplate: TypesGen.Template = {
|
|||
created_by_name: "test_creator",
|
||||
icon: "/icon/code.svg",
|
||||
allow_user_cancel_workspace_jobs: true,
|
||||
failure_ttl_ms: 0,
|
||||
inactivity_ttl_ms: 0,
|
||||
allow_user_autostart: false,
|
||||
allow_user_autostop: false,
|
||||
}
|
||||
|
@ -1340,7 +1342,7 @@ export const MockEntitlementsWithScheduling: TypesGen.Entitlements = {
|
|||
}),
|
||||
}
|
||||
|
||||
export const MockExperiments: TypesGen.Experiment[] = []
|
||||
export const MockExperiments: TypesGen.Experiment[] = ["workspace_actions"]
|
||||
|
||||
export const MockAuditLog: TypesGen.AuditLog = {
|
||||
id: "fbd2116a-8961-4954-87ae-e4575bd29ce0",
|
||||
|
|
Loading…
Reference in New Issue