mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
feat: add controls to template for determining startup days (#10226)
* feat: template controls which days can autostart * Add unit test to test blocking autostart with DaysOfWeek
This commit is contained in:
34
coderd/apidoc/docs.go
generated
34
coderd/apidoc/docs.go
generated
@ -7732,6 +7732,14 @@ const docTemplate = `{
|
||||
"description": "Allow users to cancel in-progress workspace jobs.\n*bool as the default value is \"true\".",
|
||||
"type": "boolean"
|
||||
},
|
||||
"autostart_requirement": {
|
||||
"description": "AutostartRequirement allows optionally specifying the autostart allowed days\nfor workspaces created from this template. This is an enterprise feature.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.TemplateAutostartRequirement"
|
||||
}
|
||||
]
|
||||
},
|
||||
"autostop_requirement": {
|
||||
"description": "AutostopRequirement allows optionally specifying the autostop requirement\nfor workspaces created from this template. This is an enterprise feature.",
|
||||
"allOf": [
|
||||
@ -9896,8 +9904,11 @@ const docTemplate = `{
|
||||
"allow_user_cancel_workspace_jobs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"autostart_requirement": {
|
||||
"$ref": "#/definitions/codersdk.TemplateAutostartRequirement"
|
||||
},
|
||||
"autostop_requirement": {
|
||||
"description": "AutostopRequirement is an enterprise feature. Its value is only used if\nyour license is entitled to use the advanced template scheduling feature.",
|
||||
"description": "AutostopRequirement and AutostartRequirement are enterprise features. Its\nvalue is only used if your license is entitled to use the advanced template\nscheduling feature.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.TemplateAutostopRequirement"
|
||||
@ -10013,6 +10024,27 @@ const docTemplate = `{
|
||||
"TemplateAppsTypeApp"
|
||||
]
|
||||
},
|
||||
"codersdk.TemplateAutostartRequirement": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days_of_week": {
|
||||
"description": "DaysOfWeek is a list of days of the week in which autostart is allowed\nto happen. If no days are specified, autostart is not allowed.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.TemplateAutostopRequirement": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
34
coderd/apidoc/swagger.json
generated
34
coderd/apidoc/swagger.json
generated
@ -6889,6 +6889,14 @@
|
||||
"description": "Allow users to cancel in-progress workspace jobs.\n*bool as the default value is \"true\".",
|
||||
"type": "boolean"
|
||||
},
|
||||
"autostart_requirement": {
|
||||
"description": "AutostartRequirement allows optionally specifying the autostart allowed days\nfor workspaces created from this template. This is an enterprise feature.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.TemplateAutostartRequirement"
|
||||
}
|
||||
]
|
||||
},
|
||||
"autostop_requirement": {
|
||||
"description": "AutostopRequirement allows optionally specifying the autostop requirement\nfor workspaces created from this template. This is an enterprise feature.",
|
||||
"allOf": [
|
||||
@ -8936,8 +8944,11 @@
|
||||
"allow_user_cancel_workspace_jobs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"autostart_requirement": {
|
||||
"$ref": "#/definitions/codersdk.TemplateAutostartRequirement"
|
||||
},
|
||||
"autostop_requirement": {
|
||||
"description": "AutostopRequirement is an enterprise feature. Its value is only used if\nyour license is entitled to use the advanced template scheduling feature.",
|
||||
"description": "AutostopRequirement and AutostartRequirement are enterprise features. Its\nvalue is only used if your license is entitled to use the advanced template\nscheduling feature.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.TemplateAutostopRequirement"
|
||||
@ -9045,6 +9056,27 @@
|
||||
"enum": ["builtin", "app"],
|
||||
"x-enum-varnames": ["TemplateAppsTypeBuiltin", "TemplateAppsTypeApp"]
|
||||
},
|
||||
"codersdk.TemplateAutostartRequirement": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days_of_week": {
|
||||
"description": "DaysOfWeek is a list of days of the week in which autostart is allowed\nto happen. If no days are specified, autostart is not allowed.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.TemplateAutostopRequirement": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -354,6 +354,17 @@ func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild
|
||||
// Truncate is probably not necessary here, but doing it anyway to be sure.
|
||||
nextTransition := sched.Next(build.CreatedAt).Truncate(time.Minute)
|
||||
|
||||
// The nextTransition is when the auto start should kick off. If it lands on a
|
||||
// forbidden day, do not allow the auto start. We use the time location of the
|
||||
// schedule to determine the weekday. So if "Saturday" is disallowed, the
|
||||
// definition of "Saturday" depends on the location of the schedule.
|
||||
zonedTransition := nextTransition.In(sched.Location())
|
||||
allowed := templateSchedule.AutostartRequirement.DaysMap()[zonedTransition.Weekday()]
|
||||
if !allowed {
|
||||
return false
|
||||
}
|
||||
|
||||
// Must used '.Before' vs '.After' so equal times are considered "valid for autostart".
|
||||
return !currentTick.Before(nextTransition)
|
||||
}
|
||||
|
||||
|
146
coderd/autobuild/lifecycle_executor_internal_test.go
Normal file
146
coderd/autobuild/lifecycle_executor_internal_test.go
Normal file
@ -0,0 +1,146 @@
|
||||
package autobuild
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
)
|
||||
|
||||
func Test_isEligibleForAutostart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// okXXX should be set to values that make 'isEligibleForAutostart' return true.
|
||||
|
||||
// Intentionally chosen to be a non UTC time that changes the day of the week
|
||||
// when converted to UTC.
|
||||
localLocation, err := time.LoadLocation("America/Chicago")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 5s after the autostart in UTC.
|
||||
okTick := time.Date(2021, 1, 1, 20, 0, 5, 0, localLocation).UTC()
|
||||
okWorkspace := database.Workspace{
|
||||
DormantAt: sql.NullTime{Valid: false},
|
||||
AutostartSchedule: sql.NullString{
|
||||
Valid: true,
|
||||
// Every day at 8pm America/Chicago, which is 2am UTC the next day.
|
||||
String: "CRON_TZ=America/Chicago 0 20 * * *",
|
||||
},
|
||||
}
|
||||
okBuild := database.WorkspaceBuild{
|
||||
Transition: database.WorkspaceTransitionStop,
|
||||
// Put 24hr before the tick so it's eligible for autostart.
|
||||
CreatedAt: okTick.Add(time.Hour * -24),
|
||||
}
|
||||
okJob := database.ProvisionerJob{
|
||||
JobStatus: database.ProvisionerJobStatusSucceeded,
|
||||
}
|
||||
okTemplateSchedule := schedule.TemplateScheduleOptions{
|
||||
UserAutostartEnabled: true,
|
||||
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
||||
DaysOfWeek: 0b01111111,
|
||||
},
|
||||
}
|
||||
var okWeekdayBit uint8
|
||||
for i, weekday := range schedule.DaysOfWeek {
|
||||
// Find the local weekday
|
||||
if okTick.In(localLocation).Weekday() == weekday {
|
||||
okWeekdayBit = 1 << uint(i)
|
||||
}
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Workspace database.Workspace
|
||||
Build database.WorkspaceBuild
|
||||
Job database.ProvisionerJob
|
||||
TemplateSchedule schedule.TemplateScheduleOptions
|
||||
Tick time.Time
|
||||
|
||||
ExpectedResponse bool
|
||||
}{
|
||||
{
|
||||
Name: "Ok",
|
||||
Workspace: okWorkspace,
|
||||
Build: okBuild,
|
||||
Job: okJob,
|
||||
TemplateSchedule: okTemplateSchedule,
|
||||
Tick: okTick,
|
||||
ExpectedResponse: true,
|
||||
},
|
||||
{
|
||||
Name: "AutostartOnlyDayEnabled",
|
||||
Workspace: okWorkspace,
|
||||
Build: okBuild,
|
||||
Job: okJob,
|
||||
TemplateSchedule: schedule.TemplateScheduleOptions{
|
||||
UserAutostartEnabled: true,
|
||||
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
||||
// Specific day of week is allowed
|
||||
DaysOfWeek: okWeekdayBit,
|
||||
},
|
||||
},
|
||||
Tick: okTick,
|
||||
ExpectedResponse: true,
|
||||
},
|
||||
{
|
||||
Name: "AutostartOnlyDayDisabled",
|
||||
Workspace: okWorkspace,
|
||||
Build: okBuild,
|
||||
Job: okJob,
|
||||
TemplateSchedule: schedule.TemplateScheduleOptions{
|
||||
UserAutostartEnabled: true,
|
||||
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
||||
// Specific day of week is disallowed
|
||||
DaysOfWeek: 0b01111111 & (^okWeekdayBit),
|
||||
},
|
||||
},
|
||||
Tick: okTick,
|
||||
ExpectedResponse: false,
|
||||
},
|
||||
{
|
||||
Name: "AutostartAllDaysDisabled",
|
||||
Workspace: okWorkspace,
|
||||
Build: okBuild,
|
||||
Job: okJob,
|
||||
TemplateSchedule: schedule.TemplateScheduleOptions{
|
||||
UserAutostartEnabled: true,
|
||||
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
||||
// All days disabled
|
||||
DaysOfWeek: 0,
|
||||
},
|
||||
},
|
||||
Tick: okTick,
|
||||
ExpectedResponse: false,
|
||||
},
|
||||
{
|
||||
Name: "BuildTransitionNotStop",
|
||||
Workspace: okWorkspace,
|
||||
Build: func(b database.WorkspaceBuild) database.WorkspaceBuild {
|
||||
cpy := b
|
||||
cpy.Transition = database.WorkspaceTransitionStart
|
||||
return cpy
|
||||
}(okBuild),
|
||||
Job: okJob,
|
||||
TemplateSchedule: okTemplateSchedule,
|
||||
Tick: okTick,
|
||||
ExpectedResponse: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
c := c
|
||||
t.Run(c.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
autostart := isEligibleForAutostart(c.Workspace, c.Build, c.Job, c.TemplateSchedule, c.Tick)
|
||||
require.Equal(t, c.ExpectedResponse, autostart, "autostart not expected")
|
||||
})
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisioner/echo"
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestExecutorAutostartOK(t *testing.T) {
|
||||
@ -60,6 +61,12 @@ func TestExecutorAutostartOK(t *testing.T) {
|
||||
|
||||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||||
assert.Equal(t, codersdk.BuildReasonAutostart, workspace.LatestBuild.Reason)
|
||||
// Assert some template props. If this is not set correctly, the test
|
||||
// will fail.
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
template, err := client.Template(ctx, workspace.TemplateID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, template.AutostartRequirement.DaysOfWeek, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"})
|
||||
}
|
||||
|
||||
func TestExecutorAutostartTemplateUpdated(t *testing.T) {
|
||||
|
@ -5725,6 +5725,7 @@ func (q *FakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database
|
||||
tpl.MaxTTL = arg.MaxTTL
|
||||
tpl.AutostopRequirementDaysOfWeek = arg.AutostopRequirementDaysOfWeek
|
||||
tpl.AutostopRequirementWeeks = arg.AutostopRequirementWeeks
|
||||
tpl.AutostartBlockDaysOfWeek = arg.AutostartBlockDaysOfWeek
|
||||
tpl.FailureTTL = arg.FailureTTL
|
||||
tpl.TimeTilDormant = arg.TimeTilDormant
|
||||
tpl.TimeTilDormantAutoDelete = arg.TimeTilDormantAutoDelete
|
||||
|
6
coderd/database/dump.sql
generated
6
coderd/database/dump.sql
generated
@ -754,7 +754,8 @@ CREATE TABLE templates (
|
||||
time_til_dormant bigint DEFAULT 0 NOT NULL,
|
||||
time_til_dormant_autodelete bigint DEFAULT 0 NOT NULL,
|
||||
autostop_requirement_days_of_week smallint DEFAULT 0 NOT NULL,
|
||||
autostop_requirement_weeks bigint DEFAULT 0 NOT NULL
|
||||
autostop_requirement_weeks bigint DEFAULT 0 NOT NULL,
|
||||
autostart_block_days_of_week smallint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
|
||||
@ -771,6 +772,8 @@ COMMENT ON COLUMN templates.autostop_requirement_days_of_week IS 'A bitmap of da
|
||||
|
||||
COMMENT ON COLUMN templates.autostop_requirement_weeks IS 'The number of weeks between restarts. 0 or 1 weeks means "every week", 2 week means "every second week", etc. Weeks are counted from January 2, 2023, which is the first Monday of 2023. This is to ensure workspaces are started consistently for all customers on the same n-week cycles.';
|
||||
|
||||
COMMENT ON COLUMN templates.autostart_block_days_of_week IS 'A bitmap of days of week that autostart of a workspace is not allowed. Default allows all days. This is intended as a cost savings measure to prevent auto start on weekends (for example).';
|
||||
|
||||
CREATE VIEW template_with_users AS
|
||||
SELECT templates.id,
|
||||
templates.created_at,
|
||||
@ -796,6 +799,7 @@ CREATE VIEW template_with_users AS
|
||||
templates.time_til_dormant_autodelete,
|
||||
templates.autostop_requirement_days_of_week,
|
||||
templates.autostop_requirement_weeks,
|
||||
templates.autostart_block_days_of_week,
|
||||
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
||||
COALESCE(visible_users.username, ''::text) AS created_by_username
|
||||
FROM (public.templates
|
||||
|
@ -0,0 +1,25 @@
|
||||
BEGIN;
|
||||
|
||||
DROP VIEW template_with_users;
|
||||
|
||||
ALTER TABLE templates
|
||||
DROP COLUMN autostart_block_days_of_week;
|
||||
|
||||
-- Recreate view
|
||||
CREATE VIEW
|
||||
template_with_users
|
||||
AS
|
||||
SELECT
|
||||
templates.*,
|
||||
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
|
||||
coalesce(visible_users.username, '') AS created_by_username
|
||||
FROM
|
||||
templates
|
||||
LEFT JOIN
|
||||
visible_users
|
||||
ON
|
||||
templates.created_by = visible_users.id;
|
||||
|
||||
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
|
||||
|
||||
COMMIT;
|
@ -0,0 +1,27 @@
|
||||
BEGIN;
|
||||
|
||||
DROP VIEW template_with_users;
|
||||
|
||||
ALTER TABLE templates
|
||||
ADD COLUMN autostart_block_days_of_week smallint NOT NULL DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN templates.autostart_block_days_of_week IS 'A bitmap of days of week that autostart of a workspace is not allowed. Default allows all days. This is intended as a cost savings measure to prevent auto start on weekends (for example).';
|
||||
|
||||
-- Recreate view
|
||||
CREATE VIEW
|
||||
template_with_users
|
||||
AS
|
||||
SELECT
|
||||
templates.*,
|
||||
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
|
||||
coalesce(visible_users.username, '') AS created_by_username
|
||||
FROM
|
||||
templates
|
||||
LEFT JOIN
|
||||
visible_users
|
||||
ON
|
||||
templates.created_by = visible_users.id;
|
||||
|
||||
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
|
||||
|
||||
COMMIT;
|
@ -124,6 +124,15 @@ func (t Template) DeepCopy() Template {
|
||||
return cpy
|
||||
}
|
||||
|
||||
// AutostartAllowedDays returns the inverse of 'AutostartBlockDaysOfWeek'.
|
||||
// It is more useful to have the days that are allowed to autostart from a UX
|
||||
// POV. The database prefers the 0 value being 'all days allowed'.
|
||||
func (t Template) AutostartAllowedDays() uint8 {
|
||||
// Just flip the binary 0s to 1s and vice versa.
|
||||
// There is an extra day with the 8th bit that needs to be zeroed.
|
||||
return ^uint8(t.AutostartBlockDaysOfWeek) & 0b01111111
|
||||
}
|
||||
|
||||
func (TemplateVersion) RBACObject(template Template) rbac.Object {
|
||||
// Just use the parent template resource for controlling versions
|
||||
return template.RBACObject()
|
||||
|
@ -85,6 +85,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
|
||||
&i.TimeTilDormantAutoDelete,
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
|
@ -1891,6 +1891,7 @@ type Template struct {
|
||||
TimeTilDormantAutoDelete int64 `db:"time_til_dormant_autodelete" json:"time_til_dormant_autodelete"`
|
||||
AutostopRequirementDaysOfWeek int16 `db:"autostop_requirement_days_of_week" json:"autostop_requirement_days_of_week"`
|
||||
AutostopRequirementWeeks int64 `db:"autostop_requirement_weeks" json:"autostop_requirement_weeks"`
|
||||
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
|
||||
CreatedByAvatarURL sql.NullString `db:"created_by_avatar_url" json:"created_by_avatar_url"`
|
||||
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
|
||||
}
|
||||
@ -1927,6 +1928,8 @@ type TemplateTable struct {
|
||||
AutostopRequirementDaysOfWeek int16 `db:"autostop_requirement_days_of_week" json:"autostop_requirement_days_of_week"`
|
||||
// The number of weeks between restarts. 0 or 1 weeks means "every week", 2 week means "every second week", etc. Weeks are counted from January 2, 2023, which is the first Monday of 2023. This is to ensure workspaces are started consistently for all customers on the same n-week cycles.
|
||||
AutostopRequirementWeeks int64 `db:"autostop_requirement_weeks" json:"autostop_requirement_weeks"`
|
||||
// A bitmap of days of week that autostart of a workspace is not allowed. Default allows all days. This is intended as a cost savings measure to prevent auto start on weekends (for example).
|
||||
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
|
||||
}
|
||||
|
||||
// Joins in the username + avatar url of the created by user.
|
||||
|
@ -4726,7 +4726,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem
|
||||
|
||||
const getTemplateByID = `-- name: GetTemplateByID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, created_by_avatar_url, created_by_username
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_with_users
|
||||
WHERE
|
||||
@ -4763,6 +4763,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
&i.TimeTilDormantAutoDelete,
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
@ -4771,7 +4772,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
|
||||
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, created_by_avatar_url, created_by_username
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_with_users AS templates
|
||||
WHERE
|
||||
@ -4816,6 +4817,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
&i.TimeTilDormantAutoDelete,
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
@ -4823,7 +4825,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
}
|
||||
|
||||
const getTemplates = `-- name: GetTemplates :many
|
||||
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, created_by_avatar_url, created_by_username FROM template_with_users AS templates
|
||||
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username FROM template_with_users AS templates
|
||||
ORDER BY (name, id) ASC
|
||||
`
|
||||
|
||||
@ -4861,6 +4863,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
&i.TimeTilDormantAutoDelete,
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
@ -4879,7 +4882,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
|
||||
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, created_by_avatar_url, created_by_username
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_with_users AS templates
|
||||
WHERE
|
||||
@ -4954,6 +4957,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
|
||||
&i.TimeTilDormantAutoDelete,
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
@ -5140,9 +5144,10 @@ SET
|
||||
max_ttl = $6,
|
||||
autostop_requirement_days_of_week = $7,
|
||||
autostop_requirement_weeks = $8,
|
||||
failure_ttl = $9,
|
||||
time_til_dormant = $10,
|
||||
time_til_dormant_autodelete = $11
|
||||
autostart_block_days_of_week = $9,
|
||||
failure_ttl = $10,
|
||||
time_til_dormant = $11,
|
||||
time_til_dormant_autodelete = $12
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
@ -5156,6 +5161,7 @@ type UpdateTemplateScheduleByIDParams struct {
|
||||
MaxTTL int64 `db:"max_ttl" json:"max_ttl"`
|
||||
AutostopRequirementDaysOfWeek int16 `db:"autostop_requirement_days_of_week" json:"autostop_requirement_days_of_week"`
|
||||
AutostopRequirementWeeks int64 `db:"autostop_requirement_weeks" json:"autostop_requirement_weeks"`
|
||||
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
|
||||
FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"`
|
||||
TimeTilDormant int64 `db:"time_til_dormant" json:"time_til_dormant"`
|
||||
TimeTilDormantAutoDelete int64 `db:"time_til_dormant_autodelete" json:"time_til_dormant_autodelete"`
|
||||
@ -5171,6 +5177,7 @@ func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateT
|
||||
arg.MaxTTL,
|
||||
arg.AutostopRequirementDaysOfWeek,
|
||||
arg.AutostopRequirementWeeks,
|
||||
arg.AutostartBlockDaysOfWeek,
|
||||
arg.FailureTTL,
|
||||
arg.TimeTilDormant,
|
||||
arg.TimeTilDormantAutoDelete,
|
||||
|
@ -120,9 +120,10 @@ SET
|
||||
max_ttl = $6,
|
||||
autostop_requirement_days_of_week = $7,
|
||||
autostop_requirement_weeks = $8,
|
||||
failure_ttl = $9,
|
||||
time_til_dormant = $10,
|
||||
time_til_dormant_autodelete = $11
|
||||
autostart_block_days_of_week = $9,
|
||||
failure_ttl = $10,
|
||||
time_til_dormant = $11,
|
||||
time_til_dormant_autodelete = $12
|
||||
WHERE
|
||||
id = $1
|
||||
;
|
||||
|
@ -35,6 +35,18 @@ var DaysOfWeek = []time.Weekday{
|
||||
time.Sunday,
|
||||
}
|
||||
|
||||
type TemplateAutostartRequirement struct {
|
||||
// DaysOfWeek is a bitmap of which days of the week the workspace is allowed
|
||||
// to be auto started. If fully zero, the workspace is not allowed to be auto started.
|
||||
//
|
||||
// First bit is Monday, ..., seventh bit is Sunday, eighth bit is unused.
|
||||
DaysOfWeek uint8
|
||||
}
|
||||
|
||||
func (r TemplateAutostartRequirement) DaysMap() map[time.Weekday]bool {
|
||||
return daysMap(r.DaysOfWeek)
|
||||
}
|
||||
|
||||
type TemplateAutostopRequirement struct {
|
||||
// DaysOfWeek is a bitmap of which days of the week the workspace must be
|
||||
// restarted. If fully zero, the workspace is not required to be restarted
|
||||
@ -57,9 +69,15 @@ type TemplateAutostopRequirement struct {
|
||||
// DaysMap returns a map of the days of the week that the workspace must be
|
||||
// restarted.
|
||||
func (r TemplateAutostopRequirement) DaysMap() map[time.Weekday]bool {
|
||||
return daysMap(r.DaysOfWeek)
|
||||
}
|
||||
|
||||
// daysMap returns a map of the days of the week that are specified in the
|
||||
// bitmap.
|
||||
func daysMap(daysOfWeek uint8) map[time.Weekday]bool {
|
||||
days := make(map[time.Weekday]bool)
|
||||
for i, day := range DaysOfWeek {
|
||||
days[day] = r.DaysOfWeek&(1<<uint(i)) != 0
|
||||
days[day] = daysOfWeek&(1<<uint(i)) != 0
|
||||
}
|
||||
return days
|
||||
}
|
||||
@ -82,6 +100,19 @@ func VerifyTemplateAutostopRequirement(days uint8, weeks int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyTemplateAutostartRequirement returns an error if the autostart
|
||||
// requirement is invalid.
|
||||
func VerifyTemplateAutostartRequirement(days uint8) error {
|
||||
if days&0b10000000 != 0 {
|
||||
return xerrors.New("invalid autostart requirement days, last bit is set")
|
||||
}
|
||||
if days > 0b11111111 {
|
||||
return xerrors.New("invalid autostart requirement days, too large")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type TemplateScheduleOptions struct {
|
||||
UserAutostartEnabled bool `json:"user_autostart_enabled"`
|
||||
UserAutostopEnabled bool `json:"user_autostop_enabled"`
|
||||
@ -97,6 +128,8 @@ type TemplateScheduleOptions struct {
|
||||
// AutostopRequirement dictates when the workspace must be restarted. This
|
||||
// used to be handled by MaxTTL.
|
||||
AutostopRequirement TemplateAutostopRequirement `json:"autostop_requirement"`
|
||||
// AutostartRequirement dictates when the workspace can be auto started.
|
||||
AutostartRequirement TemplateAutostartRequirement `json:"autostart_requirement"`
|
||||
// FailureTTL dictates the duration after which failed workspaces will be
|
||||
// stopped automatically.
|
||||
FailureTTL time.Duration `json:"failure_ttl"`
|
||||
@ -154,6 +187,10 @@ func (*agplTemplateScheduleStore) Get(ctx context.Context, db database.Store, te
|
||||
// FailureTTL, TimeTilDormant, and TimeTilDormantAutoDelete are enterprise features.
|
||||
UseAutostopRequirement: false,
|
||||
MaxTTL: 0,
|
||||
AutostartRequirement: TemplateAutostartRequirement{
|
||||
// Default to allowing all days for AGPL
|
||||
DaysOfWeek: 0b01111111,
|
||||
},
|
||||
AutostopRequirement: TemplateAutostopRequirement{
|
||||
// No days means never. The weeks value should always be greater
|
||||
// than zero though.
|
||||
@ -186,6 +223,7 @@ func (*agplTemplateScheduleStore) Set(ctx context.Context, db database.Store, tp
|
||||
MaxTTL: tpl.MaxTTL,
|
||||
AutostopRequirementDaysOfWeek: tpl.AutostopRequirementDaysOfWeek,
|
||||
AutostopRequirementWeeks: tpl.AutostopRequirementWeeks,
|
||||
AutostartBlockDaysOfWeek: tpl.AutostartBlockDaysOfWeek,
|
||||
AllowUserAutostart: tpl.AllowUserAutostart,
|
||||
AllowUserAutostop: tpl.AllowUserAutostop,
|
||||
FailureTTL: tpl.FailureTTL,
|
||||
|
@ -225,12 +225,13 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
var (
|
||||
defaultTTL time.Duration
|
||||
// TODO(@dean): remove max_ttl once autostop_requirement is ready
|
||||
maxTTL time.Duration
|
||||
autostopRequirementDaysOfWeek []string
|
||||
autostopRequirementWeeks int64
|
||||
failureTTL time.Duration
|
||||
dormantTTL time.Duration
|
||||
dormantAutoDeletionTTL time.Duration
|
||||
maxTTL time.Duration
|
||||
autostopRequirementDaysOfWeek []string
|
||||
autostartRequirementDaysOfWeek []string
|
||||
autostopRequirementWeeks int64
|
||||
failureTTL time.Duration
|
||||
dormantTTL time.Duration
|
||||
dormantAutoDeletionTTL time.Duration
|
||||
)
|
||||
if createTemplate.DefaultTTLMillis != nil {
|
||||
defaultTTL = time.Duration(*createTemplate.DefaultTTLMillis) * time.Millisecond
|
||||
@ -239,6 +240,12 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
autostopRequirementDaysOfWeek = createTemplate.AutostopRequirement.DaysOfWeek
|
||||
autostopRequirementWeeks = createTemplate.AutostopRequirement.Weeks
|
||||
}
|
||||
if createTemplate.AutostartRequirement != nil {
|
||||
autostartRequirementDaysOfWeek = createTemplate.AutostartRequirement.DaysOfWeek
|
||||
} else {
|
||||
// By default, we want to allow all days of the week to be autostarted.
|
||||
autostartRequirementDaysOfWeek = codersdk.BitmapToWeekdays(0b01111111)
|
||||
}
|
||||
if createTemplate.FailureTTLMillis != nil {
|
||||
failureTTL = time.Duration(*createTemplate.FailureTTLMillis) * time.Millisecond
|
||||
}
|
||||
@ -250,8 +257,9 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
var (
|
||||
validErrs []codersdk.ValidationError
|
||||
autostopRequirementDaysOfWeekParsed uint8
|
||||
validErrs []codersdk.ValidationError
|
||||
autostopRequirementDaysOfWeekParsed uint8
|
||||
autostartRequirementDaysOfWeekParsed uint8
|
||||
)
|
||||
if defaultTTL < 0 {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."})
|
||||
@ -268,6 +276,12 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.days_of_week", Detail: err.Error()})
|
||||
}
|
||||
}
|
||||
if len(autostartRequirementDaysOfWeek) > 0 {
|
||||
autostartRequirementDaysOfWeekParsed, err = codersdk.WeekdaysToBitmap(autostartRequirementDaysOfWeek)
|
||||
if err != nil {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostart_requirement.days_of_week", Detail: err.Error()})
|
||||
}
|
||||
}
|
||||
if createTemplate.MaxTTLMillis != nil {
|
||||
maxTTL = time.Duration(*createTemplate.MaxTTLMillis) * time.Millisecond
|
||||
}
|
||||
@ -350,6 +364,9 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
DaysOfWeek: autostopRequirementDaysOfWeekParsed,
|
||||
Weeks: autostopRequirementWeeks,
|
||||
},
|
||||
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
||||
DaysOfWeek: autostartRequirementDaysOfWeekParsed,
|
||||
},
|
||||
FailureTTL: failureTTL,
|
||||
TimeTilDormant: dormantTTL,
|
||||
TimeTilDormantAutoDelete: dormantAutoDeletionTTL,
|
||||
@ -510,8 +527,9 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var (
|
||||
validErrs []codersdk.ValidationError
|
||||
autostopRequirementDaysOfWeekParsed uint8
|
||||
validErrs []codersdk.ValidationError
|
||||
autostopRequirementDaysOfWeekParsed uint8
|
||||
autostartRequirementDaysOfWeekParsed uint8
|
||||
)
|
||||
if req.DefaultTTLMillis < 0 {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."})
|
||||
@ -534,6 +552,17 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.days_of_week", Detail: err.Error()})
|
||||
}
|
||||
}
|
||||
if req.AutostartRequirement == nil {
|
||||
req.AutostartRequirement = &codersdk.TemplateAutostartRequirement{
|
||||
DaysOfWeek: codersdk.BitmapToWeekdays(scheduleOpts.AutostartRequirement.DaysOfWeek),
|
||||
}
|
||||
}
|
||||
if len(req.AutostartRequirement.DaysOfWeek) > 0 {
|
||||
autostartRequirementDaysOfWeekParsed, err = codersdk.WeekdaysToBitmap(req.AutostartRequirement.DaysOfWeek)
|
||||
if err != nil {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostart_requirement.days_of_week", Detail: err.Error()})
|
||||
}
|
||||
}
|
||||
if req.AutostopRequirement.Weeks < 0 {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: "Must be a positive integer."})
|
||||
}
|
||||
@ -622,6 +651,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
if defaultTTL != time.Duration(template.DefaultTTL) ||
|
||||
maxTTL != time.Duration(template.MaxTTL) ||
|
||||
autostopRequirementDaysOfWeekParsed != scheduleOpts.AutostopRequirement.DaysOfWeek ||
|
||||
autostartRequirementDaysOfWeekParsed != scheduleOpts.AutostartRequirement.DaysOfWeek ||
|
||||
req.AutostopRequirement.Weeks != scheduleOpts.AutostopRequirement.Weeks ||
|
||||
failureTTL != time.Duration(template.FailureTTL) ||
|
||||
inactivityTTL != time.Duration(template.TimeTilDormant) ||
|
||||
@ -640,6 +670,9 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
DaysOfWeek: autostopRequirementDaysOfWeekParsed,
|
||||
Weeks: req.AutostopRequirement.Weeks,
|
||||
},
|
||||
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
||||
DaysOfWeek: autostartRequirementDaysOfWeekParsed,
|
||||
},
|
||||
FailureTTL: failureTTL,
|
||||
TimeTilDormant: inactivityTTL,
|
||||
TimeTilDormantAutoDelete: timeTilDormantAutoDelete,
|
||||
@ -787,5 +820,8 @@ func (api *API) convertTemplate(
|
||||
DaysOfWeek: codersdk.BitmapToWeekdays(uint8(template.AutostopRequirementDaysOfWeek)),
|
||||
Weeks: autostopRequirementWeeks,
|
||||
},
|
||||
AutostartRequirement: codersdk.TemplateAutostartRequirement{
|
||||
DaysOfWeek: codersdk.BitmapToWeekdays(template.AutostartAllowedDays()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user