diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 4ea7036e01129..8ffccd35d8526 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6684,7 +6684,11 @@ const docTemplate = `{ "type": "string" }, "inactivity_ttl_ms": { - "description": "InactivityTTLMillis allows optionally specifying the max lifetime before Coder\ndeletes inactive workspaces created from this template.", + "description": "InactivityTTLMillis allows optionally specifying the max lifetime before Coder\nlocks inactive workspaces created from this template.", + "type": "integer" + }, + "locked_ttl_ms": { + "description": "LockedTTL allows optionally specifying the max lifetime before Coder\npermanently deletes locked workspaces created from this template.", "type": "integer" }, "max_ttl_ms": { @@ -8500,7 +8504,7 @@ const docTemplate = `{ "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.", + "description": "FailureTTLMillis, InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.", "type": "integer" }, "icon": { @@ -8513,6 +8517,9 @@ const docTemplate = `{ "inactivity_ttl_ms": { "type": "integer" }, + "locked_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" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index eea32d65ade16..4a72c4c6ddfaf 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5955,7 +5955,11 @@ "type": "string" }, "inactivity_ttl_ms": { - "description": "InactivityTTLMillis allows optionally specifying the max lifetime before Coder\ndeletes inactive workspaces created from this template.", + "description": "InactivityTTLMillis allows optionally specifying the max lifetime before Coder\nlocks inactive workspaces created from this template.", + "type": "integer" + }, + "locked_ttl_ms": { + "description": "LockedTTL allows optionally specifying the max lifetime before Coder\npermanently deletes locked workspaces created from this template.", "type": "integer" }, "max_ttl_ms": { @@ -7657,7 +7661,7 @@ "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.", + "description": "FailureTTLMillis, InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.", "type": "integer" }, "icon": { @@ -7670,6 +7674,9 @@ "inactivity_ttl_ms": { "type": "integer" }, + "locked_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" diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index be75ba87f647c..eaa8d5f5a2903 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -494,7 +494,8 @@ CREATE TABLE templates ( allow_user_autostart 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 + inactivity_ttl bigint DEFAULT 0 NOT NULL, + locked_ttl bigint DEFAULT 0 NOT NULL ); COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.'; diff --git a/coderd/database/migrations/000128_template_locked_ttl.down.sql b/coderd/database/migrations/000128_template_locked_ttl.down.sql new file mode 100644 index 0000000000000..71beb28ebe2f9 --- /dev/null +++ b/coderd/database/migrations/000128_template_locked_ttl.down.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE templates DROP COLUMN locked_ttl; +COMMIT; diff --git a/coderd/database/migrations/000128_template_locked_ttl.up.sql b/coderd/database/migrations/000128_template_locked_ttl.up.sql new file mode 100644 index 0000000000000..0f51a424fe115 --- /dev/null +++ b/coderd/database/migrations/000128_template_locked_ttl.up.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE templates ADD COLUMN locked_ttl BIGINT NOT NULL DEFAULT 0; +COMMIT; diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 8a105ec52ca02..83aeca7e06f3e 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -54,7 +54,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate pq.Array(arg.IDs), ) if err != nil { - return nil, err + return nil, xerrors.Errorf("query context: %w", err) } defer rows.Close() var items []Template @@ -82,16 +82,17 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate &i.AllowUserAutostop, &i.FailureTTL, &i.InactivityTTL, + &i.LockedTTL, ); err != nil { - return nil, err + return nil, xerrors.Errorf("scan: %w", err) } items = append(items, i) } if err := rows.Close(); err != nil { - return nil, err + return nil, xerrors.Errorf("close: %w", err) } if err := rows.Err(); err != nil { - return nil, err + return nil, xerrors.Errorf("rows err: %w", err) } return items, nil } diff --git a/coderd/database/models.go b/coderd/database/models.go index c6bb681d5c00f..84562c2fe4539 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1561,6 +1561,7 @@ type Template struct { 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"` + LockedTTL int64 `db:"locked_ttl" json:"locked_ttl"` } type TemplateVersion struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index bab3b845b24d9..b40c5a9c1a614 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3240,7 +3240,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, inactivity_ttl + 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, locked_ttl FROM templates WHERE @@ -3274,13 +3274,14 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat &i.AllowUserAutostop, &i.FailureTTL, &i.InactivityTTL, + &i.LockedTTL, ) 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, failure_ttl, inactivity_ttl + 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, locked_ttl FROM templates WHERE @@ -3322,12 +3323,13 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G &i.AllowUserAutostop, &i.FailureTTL, &i.InactivityTTL, + &i.LockedTTL, ) 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, failure_ttl, inactivity_ttl 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, locked_ttl FROM templates ORDER BY (name, id) ASC ` @@ -3362,6 +3364,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { &i.AllowUserAutostop, &i.FailureTTL, &i.InactivityTTL, + &i.LockedTTL, ); err != nil { return nil, err } @@ -3378,7 +3381,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, inactivity_ttl + 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, locked_ttl FROM templates WHERE @@ -3450,6 +3453,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate &i.AllowUserAutostop, &i.FailureTTL, &i.InactivityTTL, + &i.LockedTTL, ); err != nil { return nil, err } @@ -3483,7 +3487,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, failure_ttl, inactivity_ttl + ($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, locked_ttl ` type InsertTemplateParams struct { @@ -3543,6 +3547,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam &i.AllowUserAutostop, &i.FailureTTL, &i.InactivityTTL, + &i.LockedTTL, ) return i, err } @@ -3556,7 +3561,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, failure_ttl, inactivity_ttl + 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, locked_ttl ` type UpdateTemplateACLByIDParams struct { @@ -3590,6 +3595,7 @@ func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTempla &i.AllowUserAutostop, &i.FailureTTL, &i.InactivityTTL, + &i.LockedTTL, ) return i, err } @@ -3649,7 +3655,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, failure_ttl, inactivity_ttl + 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, locked_ttl ` type UpdateTemplateMetaByIDParams struct { @@ -3695,6 +3701,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl &i.AllowUserAutostop, &i.FailureTTL, &i.InactivityTTL, + &i.LockedTTL, ) return i, err } @@ -3709,11 +3716,12 @@ SET default_ttl = $5, max_ttl = $6, failure_ttl = $7, - inactivity_ttl = $8 + inactivity_ttl = $8, + locked_ttl = $9 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, failure_ttl, inactivity_ttl + 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, locked_ttl ` type UpdateTemplateScheduleByIDParams struct { @@ -3725,6 +3733,7 @@ type UpdateTemplateScheduleByIDParams struct { MaxTTL int64 `db:"max_ttl" json:"max_ttl"` FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"` InactivityTTL int64 `db:"inactivity_ttl" json:"inactivity_ttl"` + LockedTTL int64 `db:"locked_ttl" json:"locked_ttl"` } func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) (Template, error) { @@ -3737,6 +3746,7 @@ func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateT arg.MaxTTL, arg.FailureTTL, arg.InactivityTTL, + arg.LockedTTL, ) var i Template err := row.Scan( @@ -3761,6 +3771,7 @@ func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateT &i.AllowUserAutostop, &i.FailureTTL, &i.InactivityTTL, + &i.LockedTTL, ) return i, err } diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index d9eb13b0f6ac3..3750b2fa76fd7 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -120,7 +120,8 @@ SET default_ttl = $5, max_ttl = $6, failure_ttl = $7, - inactivity_ttl = $8 + inactivity_ttl = $8, + locked_ttl = $9 WHERE id = $1 RETURNING diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index 1eee41678458d..964706cf0ad06 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -56,6 +56,7 @@ overrides: failure_ttl: FailureTTL inactivity_ttl: InactivityTTL eof: EOF + locked_ttl: LockedTTL sql: - schema: "./dump.sql" diff --git a/coderd/schedule/template.go b/coderd/schedule/template.go index 084218cb56a42..12e87aac16527 100644 --- a/coderd/schedule/template.go +++ b/coderd/schedule/template.go @@ -18,10 +18,12 @@ 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 dictates the duration after which failed workspaces will be stopped automatically. FailureTTL time.Duration `json:"failure_ttl"` - // If InactivityTTL is set, all inactive workspaces will be deleted automatically after this time has elapsed. + // InactivityTTL dictates the duration after which inactive workspaces will be locked. InactivityTTL time.Duration `json:"inactivity_ttl"` + // LockedTTL dictates the duration after which locked workspaces will be permanently deleted. + LockedTTL time.Duration `json:"locked_ttl"` } // TemplateScheduleStore provides an interface for retrieving template @@ -51,11 +53,12 @@ func (*agplTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context UserAutostartEnabled: true, UserAutostopEnabled: true, DefaultTTL: time.Duration(tpl.DefaultTTL), - // Disregard the values in the database, since MaxTTL, FailureTTL, and InactivityTTL are enterprise + // Disregard the values in the database, since MaxTTL, FailureTTL, InactivityTTL, and LockedTTL are enterprise // features. MaxTTL: 0, FailureTTL: 0, InactivityTTL: 0, + LockedTTL: 0, }, nil } @@ -76,5 +79,6 @@ func (*agplTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context MaxTTL: tpl.MaxTTL, FailureTTL: tpl.FailureTTL, InactivityTTL: tpl.InactivityTTL, + LockedTTL: tpl.LockedTTL, }) } diff --git a/coderd/templates.go b/coderd/templates.go index 9aefb174172ca..c8a06f3b88a8f 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -497,6 +497,12 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { if req.InactivityTTLMillis < 0 { validErrs = append(validErrs, codersdk.ValidationError{Field: "inactivity_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 req.LockedTTLMillis < 0 { + validErrs = append(validErrs, codersdk.ValidationError{Field: "locked_ttl_ms", Detail: "Must be a positive integer."}) + } if len(validErrs) > 0 { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ @@ -518,7 +524,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { req.DefaultTTLMillis == time.Duration(template.DefaultTTL).Milliseconds() && req.MaxTTLMillis == time.Duration(template.MaxTTL).Milliseconds() && req.FailureTTLMillis == time.Duration(template.FailureTTL).Milliseconds() && - req.InactivityTTLMillis == time.Duration(template.InactivityTTL).Milliseconds() { + req.InactivityTTLMillis == time.Duration(template.InactivityTTL).Milliseconds() && + req.FailureTTLMillis == time.Duration(template.LockedTTL).Milliseconds() { return nil } @@ -546,11 +553,13 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond failureTTL := time.Duration(req.FailureTTLMillis) * time.Millisecond inactivityTTL := time.Duration(req.InactivityTTLMillis) * time.Millisecond + lockedTTL := time.Duration(req.LockedTTLMillis) * time.Millisecond if defaultTTL != time.Duration(template.DefaultTTL) || maxTTL != time.Duration(template.MaxTTL) || failureTTL != time.Duration(template.FailureTTL) || inactivityTTL != time.Duration(template.InactivityTTL) || + lockedTTL != time.Duration(template.LockedTTL) || req.AllowUserAutostart != template.AllowUserAutostart || req.AllowUserAutostop != template.AllowUserAutostop { updated, err = (*api.TemplateScheduleStore.Load()).SetTemplateScheduleOptions(ctx, tx, updated, schedule.TemplateScheduleOptions{ @@ -563,6 +572,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { MaxTTL: maxTTL, FailureTTL: failureTTL, InactivityTTL: inactivityTTL, + LockedTTL: lockedTTL, }) if err != nil { return xerrors.Errorf("set template schedule options: %w", err) @@ -716,5 +726,6 @@ func (api *API) convertTemplate( AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, FailureTTLMillis: time.Duration(template.FailureTTL).Milliseconds(), InactivityTTLMillis: time.Duration(template.InactivityTTL).Milliseconds(), + LockedTTLMillis: time.Duration(template.LockedTTL).Milliseconds(), } } diff --git a/coderd/templates_test.go b/coderd/templates_test.go index fbfdfd6786c3b..e476519f55d2e 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -635,6 +635,7 @@ func TestPatchTemplateMeta(t *testing.T) { const ( failureTTL = 7 * 24 * time.Hour inactivityTTL = 180 * 24 * time.Hour + lockedTTL = 360 * 24 * time.Hour ) t.Run("OK", func(t *testing.T) { @@ -647,9 +648,11 @@ func TestPatchTemplateMeta(t *testing.T) { if atomic.AddInt64(&setCalled, 1) == 2 { require.Equal(t, failureTTL, options.FailureTTL) require.Equal(t, inactivityTTL, options.InactivityTTL) + require.Equal(t, lockedTTL, options.LockedTTL) } template.FailureTTL = int64(options.FailureTTL) template.InactivityTTL = int64(options.InactivityTTL) + template.LockedTTL = int64(options.LockedTTL) return template, nil }, }, @@ -659,6 +662,7 @@ func TestPatchTemplateMeta(t *testing.T) { 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()) + ctr.LockedTTL = ptr.Ref(0 * time.Hour.Milliseconds()) }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) @@ -674,12 +678,14 @@ func TestPatchTemplateMeta(t *testing.T) { AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, FailureTTLMillis: failureTTL.Milliseconds(), InactivityTTLMillis: inactivityTTL.Milliseconds(), + LockedTTLMillis: lockedTTL.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) + require.Equal(t, lockedTTL.Milliseconds(), got.LockedTTLMillis) }) t.Run("IgnoredUnlicensed", func(t *testing.T) { @@ -691,6 +697,7 @@ func TestPatchTemplateMeta(t *testing.T) { 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()) + ctr.LockedTTL = ptr.Ref(0 * time.Hour.Milliseconds()) }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) @@ -706,10 +713,12 @@ func TestPatchTemplateMeta(t *testing.T) { AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, FailureTTLMillis: failureTTL.Milliseconds(), InactivityTTLMillis: inactivityTTL.Milliseconds(), + LockedTTLMillis: lockedTTL.Milliseconds(), }) require.NoError(t, err) require.Zero(t, got.FailureTTLMillis) require.Zero(t, got.InactivityTTLMillis) + require.Zero(t, got.LockedTTLMillis) }) }) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 19af059277ea0..bcb360132b8b9 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -106,8 +106,11 @@ type CreateTemplateRequest struct { // 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. + // locks inactive workspaces created from this template. InactivityTTLMillis *int64 `json:"inactivity_ttl_ms,omitempty"` + // LockedTTL allows optionally specifying the max lifetime before Coder + // permanently deletes locked workspaces created from this template. + LockedTTL *int64 `json:"locked_ttl_ms,omitempty"` // DisableEveryoneGroupAccess allows optionally disabling the default // behavior of granting the 'everyone' group access to use the template. diff --git a/codersdk/templates.go b/codersdk/templates.go index 1fc6196832e3e..ee81f99f3106b 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -41,11 +41,12 @@ type Template struct { AllowUserAutostop bool `json:"allow_user_autostop"` AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs"` - // FailureTTLMillis and InactivityTTLMillis are enterprise-only. Their + // FailureTTLMillis, InactivityTTLMillis, and LockedTTLMillis 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"` + LockedTTLMillis int64 `json:"locked_ttl_ms"` } type TransitionStats struct { @@ -103,6 +104,7 @@ type UpdateTemplateMeta struct { AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"` FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"` InactivityTTLMillis int64 `json:"inactivity_ttl_ms,omitempty"` + LockedTTLMillis int64 `json:"locked_ttl_ms,omitempty"` } type TemplateExample struct { diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 85b1e8aa7dbb6..437354b240eff 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -9,18 +9,18 @@ We track the following resources: -| Resource | | -| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| -| Group
create, write, delete |
FieldTracked
avatar_urltrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
| -| GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| -| License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| -| Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
created_atfalse
created_bytrue
default_ttltrue
deletedfalse
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
inactivity_ttltrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
updated_atfalse
user_acltrue
| -| TemplateVersion
create, write |
FieldTracked
created_atfalse
created_bytrue
git_auth_providersfalse
idtrue
job_idfalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| -| User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typefalse
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| -| Workspace
create, write, delete |
FieldTracked
autostart_scheduletrue
created_atfalse
deletedfalse
idtrue
last_used_atfalse
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| -| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| -| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
display_nametrue
icontrue
idtrue
nametrue
token_hashed_secrettrue
updated_atfalse
urltrue
wildcard_hostnametrue
| +| Resource | | +| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| +| Group
create, write, delete |
FieldTracked
avatar_urltrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
| +| GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| +| License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| +| Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
created_atfalse
created_bytrue
default_ttltrue
deletedfalse
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
inactivity_ttltrue
locked_ttltrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
updated_atfalse
user_acltrue
| +| TemplateVersion
create, write |
FieldTracked
created_atfalse
created_bytrue
git_auth_providersfalse
idtrue
job_idfalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| +| User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typefalse
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| +| Workspace
create, write, delete |
FieldTracked
autostart_scheduletrue
created_atfalse
deletedfalse
idtrue
last_used_atfalse
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| +| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
display_nametrue
icontrue
idtrue
nametrue
token_hashed_secrettrue
updated_atfalse
urltrue
wildcard_hostnametrue
| diff --git a/docs/api/schemas.md b/docs/api/schemas.md index b05444c94696a..c14d2415aebbb 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1351,6 +1351,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "failure_ttl_ms": 0, "icon": "string", "inactivity_ttl_ms": 0, + "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1" @@ -1370,7 +1371,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `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. | +| `inactivity_ttl_ms` | integer | false | | Inactivity ttl ms allows optionally specifying the max lifetime before Coder locks inactive workspaces created from this template. | +| `locked_ttl_ms` | integer | false | | Locked ttl ms allows optionally specifying the max lifetime before Coder permanently deletes locked 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. | | `template_version_id` | string | true | | Template version ID is an in-progress or completed job to use as an initial version of the template. | @@ -3733,6 +3735,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "inactivity_ttl_ms": 0, + "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -3743,29 +3746,30 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ### Properties -| Name | Type | Required | Restrictions | Description | -| ---------------------------------- | ------------------------------------------------------------------ | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `active_user_count` | integer | false | | Active user count is set to -1 when loading. | -| `active_version_id` | string | false | | | -| `allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. | -| `allow_user_autostop` | boolean | false | | | -| `allow_user_cancel_workspace_jobs` | boolean | false | | | -| `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | | -| `created_at` | string | false | | | -| `created_by_id` | string | false | | | -| `created_by_name` | string | false | | | -| `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 | | | -| `provisioner` | string | false | | | -| `updated_at` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------------------- | ------------------------------------------------------------------ | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `active_user_count` | integer | false | | Active user count is set to -1 when loading. | +| `active_version_id` | string | false | | | +| `allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. | +| `allow_user_autostop` | boolean | false | | | +| `allow_user_cancel_workspace_jobs` | boolean | false | | | +| `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | | +| `created_at` | string | false | | | +| `created_by_id` | string | false | | | +| `created_by_name` | string | false | | | +| `default_ttl_ms` | integer | false | | | +| `description` | string | false | | | +| `display_name` | string | false | | | +| `failure_ttl_ms` | integer | false | | Failure ttl ms InactivityTTLMillis, and LockedTTLMillis 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 | | | +| `locked_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 | | | +| `provisioner` | string | false | | | +| `updated_at` | string | false | | | #### Enumerated Values diff --git a/docs/api/templates.md b/docs/api/templates.md index b9e3033a83e73..6b3298275eba6 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -51,6 +51,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "inactivity_ttl_ms": 0, + "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -70,33 +71,34 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat Status Code **200** -| Name | Type | Required | Restrictions | Description | -| ------------------------------------ | ---------------------------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `[array item]` | array | false | | | -| `» active_user_count` | integer | false | | Active user count is set to -1 when loading. | -| `» active_version_id` | string(uuid) | false | | | -| `» allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. | -| `» allow_user_autostop` | boolean | false | | | -| `» allow_user_cancel_workspace_jobs` | boolean | false | | | -| `» build_time_stats` | [codersdk.TemplateBuildTimeStats](schemas.md#codersdktemplatebuildtimestats) | false | | | -| `»» [any property]` | [codersdk.TransitionStats](schemas.md#codersdktransitionstats) | false | | | -| `»»» p50` | integer | false | | | -| `»»» p95` | integer | false | | | -| `» created_at` | string(date-time) | false | | | -| `» created_by_id` | string(uuid) | false | | | -| `» created_by_name` | string | false | | | -| `» 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 | | | -| `» provisioner` | string | false | | | -| `» updated_at` | string(date-time) | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------------------ | ---------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `[array item]` | array | false | | | +| `» active_user_count` | integer | false | | Active user count is set to -1 when loading. | +| `» active_version_id` | string(uuid) | false | | | +| `» allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. | +| `» allow_user_autostop` | boolean | false | | | +| `» allow_user_cancel_workspace_jobs` | boolean | false | | | +| `» build_time_stats` | [codersdk.TemplateBuildTimeStats](schemas.md#codersdktemplatebuildtimestats) | false | | | +| `»» [any property]` | [codersdk.TransitionStats](schemas.md#codersdktransitionstats) | false | | | +| `»»» p50` | integer | false | | | +| `»»» p95` | integer | false | | | +| `» created_at` | string(date-time) | false | | | +| `» created_by_id` | string(uuid) | false | | | +| `» created_by_name` | string | false | | | +| `» default_ttl_ms` | integer | false | | | +| `» description` | string | false | | | +| `» display_name` | string | false | | | +| `» failure_ttl_ms` | integer | false | | Failure ttl ms InactivityTTLMillis, and LockedTTLMillis 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 | | | +| `» locked_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 | | | +| `» provisioner` | string | false | | | +| `» updated_at` | string(date-time) | false | | | #### Enumerated Values @@ -134,6 +136,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "failure_ttl_ms": 0, "icon": "string", "inactivity_ttl_ms": 0, + "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1" @@ -178,6 +181,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "inactivity_ttl_ms": 0, + "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -305,6 +309,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "inactivity_ttl_ms": 0, + "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -628,6 +633,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \ "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "inactivity_ttl_ms": 0, + "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -738,6 +744,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \ "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "inactivity_ttl_ms": 0, + "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index c8b90b8b23567..ed1eabf115247 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -77,6 +77,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "max_ttl": ActionTrack, "failure_ttl": ActionTrack, "inactivity_ttl": ActionTrack, + "locked_ttl": ActionTrack, }, &database.TemplateVersion{}: { "id": ActionTrack, diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index 0e6f2bfa5bdab..36b98cde73004 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -327,6 +327,7 @@ func (*enterpriseTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.C MaxTTL: time.Duration(tpl.MaxTTL), FailureTTL: time.Duration(tpl.FailureTTL), InactivityTTL: time.Duration(tpl.InactivityTTL), + LockedTTL: time.Duration(tpl.LockedTTL), }, nil } @@ -335,6 +336,7 @@ func (*enterpriseTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.C int64(opts.MaxTTL) == tpl.MaxTTL && int64(opts.FailureTTL) == tpl.FailureTTL && int64(opts.InactivityTTL) == tpl.InactivityTTL && + int64(opts.LockedTTL) == tpl.LockedTTL && opts.UserAutostartEnabled == tpl.AllowUserAutostart && opts.UserAutostopEnabled == tpl.AllowUserAutostop { // Avoid updating the UpdatedAt timestamp if nothing will be changed. @@ -350,6 +352,7 @@ func (*enterpriseTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.C MaxTTL: int64(opts.MaxTTL), FailureTTL: int64(opts.FailureTTL), InactivityTTL: int64(opts.InactivityTTL), + LockedTTL: int64(opts.LockedTTL), }) if err != nil { return database.Template{}, xerrors.Errorf("update template schedule: %w", err) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 961cc8f16e455..6d1a67e0a6a8e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -179,6 +179,7 @@ export interface CreateTemplateRequest { readonly allow_user_autostop?: boolean readonly failure_ttl_ms?: number readonly inactivity_ttl_ms?: number + readonly locked_ttl_ms?: number readonly disable_everyone_group_access: boolean } @@ -805,6 +806,7 @@ export interface Template { readonly allow_user_cancel_workspace_jobs: boolean readonly failure_ttl_ms: number readonly inactivity_ttl_ms: number + readonly locked_ttl_ms: number } // From codersdk/templates.go @@ -972,6 +974,7 @@ export interface UpdateTemplateMeta { readonly allow_user_cancel_workspace_jobs?: boolean readonly failure_ttl_ms?: number readonly inactivity_ttl_ms?: number + readonly locked_ttl_ms?: number } // From codersdk/users.go diff --git a/site/src/i18n/en/templateSettingsPage.json b/site/src/i18n/en/templateSettingsPage.json index aa8cf7bca3604..669afa7b60ab8 100644 --- a/site/src/i18n/en/templateSettingsPage.json +++ b/site/src/i18n/en/templateSettingsPage.json @@ -25,6 +25,9 @@ "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.", + "lockedTTLHelperText_zero": "Coder will not automatically delete locked workspaces", + "lockedTTLHelperText_one": "Coder will automatically delete locked workspaces after {{count}} day.", + "lockedTTLHelperText_other": "Coder will automatically delete locked 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.", diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index 64ebacc2d45ad..922fef150a469 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -28,6 +28,7 @@ const validFormValues: FormValues = { allow_user_autostop: false, failure_ttl_ms: 0, inactivity_ttl_ms: 0, + locked_ttl_ms: 0, } const renderTemplateSettingsPage = async () => { diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index 5f2b330197132..e21d8776bc252 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -25,6 +25,7 @@ const MS_HOUR_CONVERSION = 3600000 const MS_DAY_CONVERSION = 86400000 const FAILURE_CLEANUP_DEFAULT = 7 const INACTIVITY_CLEANUP_DEFAULT = 180 +const LOCKED_CLEANUP_DEFAULT = 30 export interface TemplateScheduleForm { template: Template @@ -65,6 +66,9 @@ export const TemplateScheduleForm: FC = ({ inactivity_ttl_ms: allowAdvancedScheduling ? template.inactivity_ttl_ms / MS_DAY_CONVERSION : 0, + locked_ttl_ms: allowAdvancedScheduling + ? template.locked_ttl_ms / MS_DAY_CONVERSION + : 0, allow_user_autostart: template.allow_user_autostart, allow_user_autostop: template.allow_user_autostop, @@ -72,6 +76,8 @@ export const TemplateScheduleForm: FC = ({ allowAdvancedScheduling && Boolean(template.failure_ttl_ms), inactivity_cleanup_enabled: allowAdvancedScheduling && Boolean(template.inactivity_ttl_ms), + locked_cleanup_enabled: + allowAdvancedScheduling && Boolean(template.locked_ttl_ms), }, validationSchema, onSubmit: () => { @@ -114,6 +120,9 @@ export const TemplateScheduleForm: FC = ({ inactivity_ttl_ms: form.values.inactivity_ttl_ms ? form.values.inactivity_ttl_ms * MS_DAY_CONVERSION : undefined, + locked_ttl_ms: form.values.locked_ttl_ms + ? form.values.locked_ttl_ms * MS_DAY_CONVERSION + : undefined, allow_user_autostart: form.values.allow_user_autostart, allow_user_autostop: form.values.allow_user_autostop, @@ -158,6 +167,25 @@ export const TemplateScheduleForm: FC = ({ } } + const handleToggleLockedCleanup = async (e: ChangeEvent) => { + form.handleChange(e) + if (!form.values.locked_cleanup_enabled) { + // fill failure_ttl_ms with defaults + await form.setValues({ + ...form.values, + locked_cleanup_enabled: true, + locked_ttl_ms: LOCKED_CLEANUP_DEFAULT, + }) + } else { + // clear failure_ttl_ms + await form.setValues({ + ...form.values, + locked_cleanup_enabled: false, + locked_ttl_ms: 0, + }) + } + } + return ( = ({ = ({ /> + + + + } + label="Enable Locked Cleanup" + /> + , + )} + disabled={isSubmitting || !form.values.locked_cleanup_enabled} + fullWidth + inputProps={{ min: 0, step: "any" }} + label="Time until cleanup (days)" + type="number" + aria-label="Locked Cleanup" + /> + + )} } }, ), + locked_ttl_ms: Yup.number() + .min(0, "Locked cleanup days must not be less than 0.") + .test( + "positive-if-enabled", + "Locked cleanup days must be greater than zero when enabled.", + function (value) { + const parent = this.parent as TemplateScheduleFormValues + if (parent.locked_cleanup_enabled) { + return Boolean(value) + } else { + return true + } + }, + ), allow_user_autostart: Yup.boolean(), allow_user_autostop: Yup.boolean(), }) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index d377279896000..a633a3e7a318a 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -22,6 +22,7 @@ const validFormValues = { max_ttl_ms: 2, failure_ttl_ms: 7, inactivity_ttl_ms: 180, + locked_ttl_ms: 30, } const renderTemplateSchedulePage = async () => { @@ -37,11 +38,13 @@ const fillAndSubmitForm = async ({ max_ttl_ms, failure_ttl_ms, inactivity_ttl_ms, + locked_ttl_ms, }: { default_ttl_ms: number max_ttl_ms: number failure_ttl_ms: number inactivity_ttl_ms: number + locked_ttl_ms: number }) => { const user = userEvent.setup() const defaultTtlLabel = t("defaultTtlLabel", { ns: "templateSettingsPage" }) @@ -64,6 +67,11 @@ const fillAndSubmitForm = async ({ }) await user.type(inactivityTtlField, inactivity_ttl_ms.toString()) + const lockedTtlField = screen.getByRole("checkbox", { + name: /Locked Cleanup/i, + }) + await user.type(lockedTtlField, locked_ttl_ms.toString()) + const submitButton = await screen.findByText( FooterFormLanguage.defaultSubmitLabel, ) @@ -111,7 +119,7 @@ describe("TemplateSchedulePage", () => { ) }) - test("failure and inactivity ttl converted to and from days", async () => { + test("failure, inactivity, and locked ttl converted to and from days", async () => { await renderTemplateSchedulePage() jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ @@ -127,6 +135,7 @@ describe("TemplateSchedulePage", () => { expect.objectContaining({ failure_ttl_ms: validFormValues.failure_ttl_ms * 86400000, inactivity_ttl_ms: validFormValues.inactivity_ttl_ms * 86400000, + locked_ttl_ms: validFormValues.locked_ttl_ms * 86400000, }), ), ) @@ -218,4 +227,33 @@ describe("TemplateSchedulePage", () => { "Inactivity cleanup days must not be less than 0.", ) }) + + it("allows a locked ttl of 7 days", () => { + const values: UpdateTemplateMeta = { + ...validFormValues, + locked_ttl_ms: 86400000 * 7, + } + const validate = () => getValidationSchema().validateSync(values) + expect(validate).not.toThrowError() + }) + + it("allows a locked ttl of 0", () => { + const values: UpdateTemplateMeta = { + ...validFormValues, + locked_ttl_ms: 0, + } + const validate = () => getValidationSchema().validateSync(values) + expect(validate).not.toThrowError() + }) + + it("disallows a negative inactivity ttl", () => { + const values: UpdateTemplateMeta = { + ...validFormValues, + locked_ttl_ms: -1, + } + const validate = () => getValidationSchema().validateSync(values) + expect(validate).toThrowError( + "Locked cleanup days must not be less than 0.", + ) + }) }) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index dd4d8565e2278..1fe126fa4a450 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -388,6 +388,7 @@ export const MockTemplate: TypesGen.Template = { allow_user_cancel_workspace_jobs: true, failure_ttl_ms: 0, inactivity_ttl_ms: 0, + locked_ttl_ms: 0, allow_user_autostart: false, allow_user_autostop: false, }