From 0d09d4c9a4de1a7dbc95612b30da00e82ccfa080 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 Oct 2023 14:58:19 -0500 Subject: [PATCH 01/14] feat: template controls which days can autostart --- coderd/autobuild/lifecycle_executor.go | 12 +- coderd/database/dbfake/dbfake.go | 1 + coderd/database/dump.sql | 6 +- .../000165_prevent_autostart_days.down.sql | 25 ++ .../000165_prevent_autostart_days.up.sql | 27 ++ coderd/database/modelmethods.go | 9 + coderd/database/modelqueries.go | 1 + coderd/database/models.go | 3 + coderd/database/queries.sql.go | 247 +++++++++--------- coderd/database/queries/templates.sql | 7 +- coderd/schedule/template.go | 40 ++- coderd/templates.go | 56 +++- codersdk/organizations.go | 3 + codersdk/templates.go | 39 +-- enterprise/coderd/schedule/template.go | 20 +- provisionersdk/proto/provisioner.pb.go | 3 - site/src/api/typesGenerated.ts | 8 + 17 files changed, 349 insertions(+), 158 deletions(-) create mode 100644 coderd/database/migrations/000165_prevent_autostart_days.down.sql create mode 100644 coderd/database/migrations/000165_prevent_autostart_days.up.sql diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index 225c5057127d4..f752cdf948d5a 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -354,7 +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) - return !currentTick.Before(nextTransition) + // 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 + } + + return currentTick.After(nextTransition) } // isEligibleForAutostart returns true if the workspace should be autostopped. diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 5bf09f90721f7..384a68bbd3dff 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -5700,6 +5700,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 diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index be44222cc03e0..ab6eb95252f2d 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -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 diff --git a/coderd/database/migrations/000165_prevent_autostart_days.down.sql b/coderd/database/migrations/000165_prevent_autostart_days.down.sql new file mode 100644 index 0000000000000..c4f3351eeafa2 --- /dev/null +++ b/coderd/database/migrations/000165_prevent_autostart_days.down.sql @@ -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; diff --git a/coderd/database/migrations/000165_prevent_autostart_days.up.sql b/coderd/database/migrations/000165_prevent_autostart_days.up.sql new file mode 100644 index 0000000000000..79c2c2a5e5c22 --- /dev/null +++ b/coderd/database/migrations/000165_prevent_autostart_days.up.sql @@ -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; diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 3ce58ba38eefc..66e1618bf570f 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -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() diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 17b4852efcf56..bb380a8293bdc 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -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 { diff --git a/coderd/database/models.go b/coderd/database/models.go index 267bd1a7ce7c4..1684e4fe5ebeb 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -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. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 418a46eae4447..2f5ed4f8daf6c 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4716,7 +4716,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 @@ -4753,6 +4753,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat &i.TimeTilDormantAutoDelete, &i.AutostopRequirementDaysOfWeek, &i.AutostopRequirementWeeks, + &i.AutostartBlockDaysOfWeek, &i.CreatedByAvatarURL, &i.CreatedByUsername, ) @@ -4761,7 +4762,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 @@ -4806,6 +4807,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G &i.TimeTilDormantAutoDelete, &i.AutostopRequirementDaysOfWeek, &i.AutostopRequirementWeeks, + &i.AutostartBlockDaysOfWeek, &i.CreatedByAvatarURL, &i.CreatedByUsername, ) @@ -4813,7 +4815,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 ` @@ -4851,6 +4853,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { &i.TimeTilDormantAutoDelete, &i.AutostopRequirementDaysOfWeek, &i.AutostopRequirementWeeks, + &i.AutostartBlockDaysOfWeek, &i.CreatedByAvatarURL, &i.CreatedByUsername, ); err != nil { @@ -4869,7 +4872,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 @@ -4944,6 +4947,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate &i.TimeTilDormantAutoDelete, &i.AutostopRequirementDaysOfWeek, &i.AutostopRequirementWeeks, + &i.AutostartBlockDaysOfWeek, &i.CreatedByAvatarURL, &i.CreatedByUsername, ); err != nil { @@ -5130,9 +5134,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 ` @@ -5146,6 +5151,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"` @@ -5161,6 +5167,7 @@ func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateT arg.MaxTTL, arg.AutostopRequirementDaysOfWeek, arg.AutostopRequirementWeeks, + arg.AutostartBlockDaysOfWeek, arg.FailureTTL, arg.TimeTilDormant, arg.TimeTilDormantAutoDelete, @@ -9723,6 +9730,119 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In return items, nil } +const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many +SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many +INSERT INTO + workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) +SELECT + $1 :: uuid AS workspace_agent_id, + $2 :: timestamptz AS created_at, + unnest($3 :: uuid [ ]) AS log_source_id, + unnest($4 :: text [ ]) AS log_path, + unnest($5 :: text [ ]) AS script, + unnest($6 :: text [ ]) AS cron, + unnest($7 :: boolean [ ]) AS start_blocks_login, + unnest($8 :: boolean [ ]) AS run_on_start, + unnest($9 :: boolean [ ]) AS run_on_stop, + unnest($10 :: integer [ ]) AS timeout_seconds +RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds +` + +type InsertWorkspaceAgentScriptsParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` + LogPath []string `db:"log_path" json:"log_path"` + Script []string `db:"script" json:"script"` + Cron []string `db:"cron" json:"cron"` + StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` + RunOnStart []bool `db:"run_on_start" json:"run_on_start"` + RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` + TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, + arg.WorkspaceAgentID, + arg.CreatedAt, + pq.Array(arg.LogSourceID), + pq.Array(arg.LogPath), + pq.Array(arg.Script), + pq.Array(arg.Cron), + pq.Array(arg.StartBlocksLogin), + pq.Array(arg.RunOnStart), + pq.Array(arg.RunOnStop), + pq.Array(arg.TimeoutSeconds), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one WITH workspaces_with_jobs AS ( SELECT @@ -10683,116 +10803,3 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C _, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID) return err } - -const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many -SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) -` - -func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many -INSERT INTO - workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) -SELECT - $1 :: uuid AS workspace_agent_id, - $2 :: timestamptz AS created_at, - unnest($3 :: uuid [ ]) AS log_source_id, - unnest($4 :: text [ ]) AS log_path, - unnest($5 :: text [ ]) AS script, - unnest($6 :: text [ ]) AS cron, - unnest($7 :: boolean [ ]) AS start_blocks_login, - unnest($8 :: boolean [ ]) AS run_on_start, - unnest($9 :: boolean [ ]) AS run_on_stop, - unnest($10 :: integer [ ]) AS timeout_seconds -RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds -` - -type InsertWorkspaceAgentScriptsParams struct { - WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` - LogPath []string `db:"log_path" json:"log_path"` - Script []string `db:"script" json:"script"` - Cron []string `db:"cron" json:"cron"` - StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` - RunOnStart []bool `db:"run_on_start" json:"run_on_start"` - RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` - TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` -} - -func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, - arg.WorkspaceAgentID, - arg.CreatedAt, - pq.Array(arg.LogSourceID), - pq.Array(arg.LogPath), - pq.Array(arg.Script), - pq.Array(arg.Cron), - pq.Array(arg.StartBlocksLogin), - pq.Array(arg.RunOnStart), - pq.Array(arg.RunOnStop), - pq.Array(arg.TimeoutSeconds), - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index 81706500f6484..e89c245b0391d 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -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 ; diff --git a/coderd/schedule/template.go b/coderd/schedule/template.go index 2c916183e1f00..94da8566c87c9 100644 --- a/coderd/schedule/template.go +++ b/coderd/schedule/template.go @@ -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 restarted. If fully zero, the workspace is not allowed to be restarted. + // + // 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< 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, diff --git a/coderd/templates.go b/coderd/templates.go index a05017298d740..69580f1ccd11f 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -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.AutostartRequirment == nil { + req.AutostartRequirment = &codersdk.TemplateAutostartRequirement{ + DaysOfWeek: codersdk.BitmapToWeekdays(scheduleOpts.AutostartRequirement.DaysOfWeek), + } + } + if len(req.AutostartRequirment.DaysOfWeek) > 0 { + autostartRequirementDaysOfWeekParsed, err = codersdk.WeekdaysToBitmap(req.AutostartRequirment.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()), + }, } } diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 195681d019ec4..6c10bc2c91abe 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -89,6 +89,9 @@ type CreateTemplateRequest struct { // AutostopRequirement allows optionally specifying the autostop requirement // for workspaces created from this template. This is an enterprise feature. AutostopRequirement *TemplateAutostopRequirement `json:"autostop_requirement,omitempty"` + // AutostartRequirement allows optionally specifying the autostart allowed days + // for workspaces created from this template. This is an enterprise feature. + AutostartRequirement *TemplateAutostartRequirement `json:"autostart_requirement,omitempty"` // Allow users to cancel in-progress workspace jobs. // *bool as the default value is "true". diff --git a/codersdk/templates.go b/codersdk/templates.go index 339c624bb2f9e..bb6cb10a81550 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -31,11 +31,13 @@ type Template struct { DefaultTTLMillis int64 `json:"default_ttl_ms"` // TODO(@dean): remove max_ttl once autostop_requirement is matured MaxTTLMillis int64 `json:"max_ttl_ms"` - // AutostopRequirement is an enterprise feature. Its value is only used if - // your license is entitled to use the advanced template scheduling feature. - AutostopRequirement TemplateAutostopRequirement `json:"autostop_requirement"` - CreatedByID uuid.UUID `json:"created_by_id" format:"uuid"` - CreatedByName string `json:"created_by_name"` + // AutostopRequirement and AutostartRequirement are enterprise features. Its + // value is only used if your license is entitled to use the advanced template + // scheduling feature. + AutostopRequirement TemplateAutostopRequirement `json:"autostop_requirement"` + AutostartRequirement TemplateAutostartRequirement `json:"autostart_requirement"` + CreatedByID uuid.UUID `json:"created_by_id" format:"uuid"` + CreatedByName string `json:"created_by_name"` // AllowUserAutostart and AllowUserAutostop are enterprise-only. Their // values are only used if your license is entitled to use the advanced @@ -107,6 +109,12 @@ func BitmapToWeekdays(bitmap uint8) []string { return days } +type TemplateAutostartRequirement struct { + // DaysOfWeek is a list of days of the week in which autostart is allowed + // to happen. If no days are specified, autostart is not allowed. + DaysOfWeek []string `json:"days_of_week" enums:"monday,tuesday,wednesday,thursday,friday,saturday,sunday"` +} + type TemplateAutostopRequirement struct { // DaysOfWeek is a list of days of the week on which restarts are required. // Restarts happen within the user's quiet hours (in their configured @@ -193,16 +201,17 @@ type UpdateTemplateMeta struct { DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"` // TODO(@dean): remove max_ttl once autostop_requirement is matured MaxTTLMillis int64 `json:"max_ttl_ms,omitempty"` - // AutostopRequirement can only be set if your license includes the advanced - // template scheduling feature. If you attempt to set this value while - // unlicensed, it will be ignored. - AutostopRequirement *TemplateAutostopRequirement `json:"autostop_requirement,omitempty"` - 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"` - TimeTilDormantMillis int64 `json:"time_til_dormant_ms,omitempty"` - TimeTilDormantAutoDeleteMillis int64 `json:"time_til_dormant_autodelete_ms,omitempty"` + // AutostopRequirement and AutostartRequirment can only be set if your license + // includes the advanced template scheduling feature. If you attempt to set this + // value while unlicensed, it will be ignored. + AutostopRequirement *TemplateAutostopRequirement `json:"autostop_requirement,omitempty"` + AutostartRequirment *TemplateAutostartRequirement `json:"autostart_requirement,omitempty"` + 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"` + TimeTilDormantMillis int64 `json:"time_til_dormant_ms,omitempty"` + TimeTilDormantAutoDeleteMillis int64 `json:"time_til_dormant_autodelete_ms,omitempty"` // UpdateWorkspaceLastUsedAt updates the last_used_at field of workspaces // spawned from the template. This is useful for preventing workspaces being // immediately locked when updating the inactivity_ttl field to a new, shorter diff --git a/enterprise/coderd/schedule/template.go b/enterprise/coderd/schedule/template.go index c78d9718762b6..26b0c78c6bfc9 100644 --- a/enterprise/coderd/schedule/template.go +++ b/enterprise/coderd/schedule/template.go @@ -86,6 +86,9 @@ func (s *EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.S DaysOfWeek: uint8(tpl.AutostopRequirementDaysOfWeek), Weeks: tpl.AutostopRequirementWeeks, }, + AutostartRequirement: agpl.TemplateAutostartRequirement{ + DaysOfWeek: tpl.AutostartAllowedDays(), + }, FailureTTL: time.Duration(tpl.FailureTTL), TimeTilDormant: time.Duration(tpl.TimeTilDormant), TimeTilDormantAutoDelete: time.Duration(tpl.TimeTilDormantAutoDelete), @@ -107,6 +110,7 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S if int64(opts.DefaultTTL) == tpl.DefaultTTL && int64(opts.MaxTTL) == tpl.MaxTTL && int16(opts.AutostopRequirement.DaysOfWeek) == tpl.AutostopRequirementDaysOfWeek && + opts.AutostartRequirement.DaysOfWeek == tpl.AutostartAllowedDays() && opts.AutostopRequirement.Weeks == tpl.AutostopRequirementWeeks && int64(opts.FailureTTL) == tpl.FailureTTL && int64(opts.TimeTilDormant) == tpl.TimeTilDormant && @@ -119,7 +123,12 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S err := agpl.VerifyTemplateAutostopRequirement(opts.AutostopRequirement.DaysOfWeek, opts.AutostopRequirement.Weeks) if err != nil { - return database.Template{}, err + return database.Template{}, xerrors.Errorf("verify autostop requirement: %w", err) + } + + err = agpl.VerifyTemplateAutostartRequirement(opts.AutostartRequirement.DaysOfWeek) + if err != nil { + return database.Template{}, xerrors.Errorf("verify autostart requirement: %w", err) } var template database.Template @@ -136,9 +145,12 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S MaxTTL: int64(opts.MaxTTL), AutostopRequirementDaysOfWeek: int16(opts.AutostopRequirement.DaysOfWeek), AutostopRequirementWeeks: opts.AutostopRequirement.Weeks, - FailureTTL: int64(opts.FailureTTL), - TimeTilDormant: int64(opts.TimeTilDormant), - TimeTilDormantAutoDelete: int64(opts.TimeTilDormantAutoDelete), + // Database stores the inverse of the allowed days of the week. + // Make sure the 8th bit is always zeroed out, as there is no 8th day of the week. + AutostartBlockDaysOfWeek: int16(^opts.AutostartRequirement.DaysOfWeek & 0b01111111), + FailureTTL: int64(opts.FailureTTL), + TimeTilDormant: int64(opts.TimeTilDormant), + TimeTilDormantAutoDelete: int64(opts.TimeTilDormantAutoDelete), }) if err != nil { return xerrors.Errorf("update template schedule: %w", err) diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 271801463d426..844b619ce1a1e 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -836,7 +836,6 @@ type Agent struct { Directory string `protobuf:"bytes,7,opt,name=directory,proto3" json:"directory,omitempty"` Apps []*App `protobuf:"bytes,8,rep,name=apps,proto3" json:"apps,omitempty"` // Types that are assignable to Auth: - // // *Agent_Token // *Agent_InstanceId Auth isAgent_Auth `protobuf_oneof:"auth"` @@ -2107,7 +2106,6 @@ type Request struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Type: - // // *Request_Config // *Request_Parse // *Request_Plan @@ -2230,7 +2228,6 @@ type Response struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Type: - // // *Response_Log // *Response_Parse // *Response_Plan diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 996eb300275bc..483e034775b79 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -208,6 +208,7 @@ export interface CreateTemplateRequest { readonly default_ttl_ms?: number; readonly max_ttl_ms?: number; readonly autostop_requirement?: TemplateAutostopRequirement; + readonly autostart_requirement?: TemplateAutostartRequirement; readonly allow_user_cancel_workspace_jobs?: boolean; readonly allow_user_autostart?: boolean; readonly allow_user_autostop?: boolean; @@ -901,6 +902,7 @@ export interface Template { readonly default_ttl_ms: number; readonly max_ttl_ms: number; readonly autostop_requirement: TemplateAutostopRequirement; + readonly autostart_requirement: TemplateAutostartRequirement; readonly created_by_id: string; readonly created_by_name: string; readonly allow_user_autostart: boolean; @@ -927,6 +929,11 @@ export interface TemplateAppUsage { readonly seconds: number; } +// From codersdk/templates.go +export interface TemplateAutostartRequirement { + readonly days_of_week: string[]; +} + // From codersdk/templates.go export interface TemplateAutostopRequirement { readonly days_of_week: string[]; @@ -1145,6 +1152,7 @@ export interface UpdateTemplateMeta { readonly default_ttl_ms?: number; readonly max_ttl_ms?: number; readonly autostop_requirement?: TemplateAutostopRequirement; + readonly autostart_requirement?: TemplateAutostartRequirement; readonly allow_user_autostart?: boolean; readonly allow_user_autostop?: boolean; readonly allow_user_cancel_workspace_jobs?: boolean; From 1975a4518090456caadbf0fb83448bcaa22fd320 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 Oct 2023 15:08:15 -0500 Subject: [PATCH 02/14] Equal times should be valid for autostart --- coderd/autobuild/lifecycle_executor.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index f752cdf948d5a..07c586b2331bb 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -364,7 +364,8 @@ func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild return false } - return currentTick.After(nextTransition) + // Must used '.Before' vs '.After' so equal times are considered "valid for autostart". + return !currentTick.Before(nextTransition) } // isEligibleForAutostart returns true if the workspace should be autostopped. From 2511a18b84d3b7c7bfe9a59206b1ae8f5d45c0d8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 Oct 2023 15:40:35 -0500 Subject: [PATCH 03/14] typo on 'Requirement' --- coderd/templates.go | 8 ++++---- codersdk/templates.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/coderd/templates.go b/coderd/templates.go index 69580f1ccd11f..38bc7b1f2c1fa 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -552,13 +552,13 @@ 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.AutostartRequirment == nil { - req.AutostartRequirment = &codersdk.TemplateAutostartRequirement{ + if req.AutostartRequirement == nil { + req.AutostartRequirement = &codersdk.TemplateAutostartRequirement{ DaysOfWeek: codersdk.BitmapToWeekdays(scheduleOpts.AutostartRequirement.DaysOfWeek), } } - if len(req.AutostartRequirment.DaysOfWeek) > 0 { - autostartRequirementDaysOfWeekParsed, err = codersdk.WeekdaysToBitmap(req.AutostartRequirment.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()}) } diff --git a/codersdk/templates.go b/codersdk/templates.go index bb6cb10a81550..d0ee400a29b20 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -201,11 +201,11 @@ type UpdateTemplateMeta struct { DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"` // TODO(@dean): remove max_ttl once autostop_requirement is matured MaxTTLMillis int64 `json:"max_ttl_ms,omitempty"` - // AutostopRequirement and AutostartRequirment can only be set if your license + // AutostopRequirement and AutostartRequirement can only be set if your license // includes the advanced template scheduling feature. If you attempt to set this // value while unlicensed, it will be ignored. AutostopRequirement *TemplateAutostopRequirement `json:"autostop_requirement,omitempty"` - AutostartRequirment *TemplateAutostartRequirement `json:"autostart_requirement,omitempty"` + AutostartRequirement *TemplateAutostartRequirement `json:"autostart_requirement,omitempty"` AllowUserAutostart bool `json:"allow_user_autostart,omitempty"` AllowUserAutostop bool `json:"allow_user_autostop,omitempty"` AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"` From 9af54754d859338371ce2d189ec8eef2d24759bc Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 Oct 2023 15:44:48 -0500 Subject: [PATCH 04/14] add audit action track to new field --- enterprise/audit/table.go | 1 + 1 file changed, 1 insertion(+) diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 1261e4f6319c4..49f10d117057c 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -71,6 +71,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "icon": ActionTrack, "default_ttl": ActionTrack, "max_ttl": ActionTrack, + "autostart_block_days_of_week": ActionTrack, "autostop_requirement_days_of_week": ActionTrack, "autostop_requirement_weeks": ActionTrack, "created_by": ActionTrack, From 3bac18035d0ead24d16f6205f49c55341ae45314 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 Oct 2023 16:03:30 -0500 Subject: [PATCH 05/14] fix js test entities --- .../TemplateSettingsPage.test.tsx | 11 +++++++++++ site/src/testHelpers/entities.ts | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index adf3fa3e531de..8df20b8c3d399 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -27,6 +27,17 @@ const validFormValues: FormValues = { days_of_week: [], weeks: 1, }, + autostart_requirement: { + days_of_week: [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ], + }, failure_ttl_ms: 0, time_til_dormant_ms: 0, time_til_dormant_autodelete_ms: 0, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 606332a6e4b0f..fbc2ae889f03d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -446,6 +446,17 @@ export const MockTemplate: TypesGen.Template = { days_of_week: [], weeks: 1, }, + autostart_requirement: { + days_of_week: [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ], + }, created_by_id: "test-creator-id", created_by_name: "test_creator", icon: "/icon/code.svg", From 6cafda3900e41c144b98bdaaa852eb546c47b422 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 12 Oct 2023 14:14:32 +0000 Subject: [PATCH 06/14] make gen --- coderd/apidoc/docs.go | 34 ++++- coderd/apidoc/swagger.json | 34 ++++- coderd/database/queries.sql.go | 226 ++++++++++++++++----------------- docs/admin/audit-logs.md | 26 ++-- docs/api/schemas.md | 106 ++++++++++------ docs/api/templates.md | 82 +++++++----- 6 files changed, 307 insertions(+), 201 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 71065f4ad1371..4897cdc23cc01 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7660,6 +7660,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": [ @@ -9824,8 +9832,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" @@ -9941,6 +9952,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": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 12b388130c289..a0486682bf871 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -6821,6 +6821,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": [ @@ -8868,8 +8876,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" @@ -8977,6 +8988,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": { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2f5ed4f8daf6c..968ddd2fb3f42 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -9730,119 +9730,6 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In return items, nil } -const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many -SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) -` - -func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many -INSERT INTO - workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) -SELECT - $1 :: uuid AS workspace_agent_id, - $2 :: timestamptz AS created_at, - unnest($3 :: uuid [ ]) AS log_source_id, - unnest($4 :: text [ ]) AS log_path, - unnest($5 :: text [ ]) AS script, - unnest($6 :: text [ ]) AS cron, - unnest($7 :: boolean [ ]) AS start_blocks_login, - unnest($8 :: boolean [ ]) AS run_on_start, - unnest($9 :: boolean [ ]) AS run_on_stop, - unnest($10 :: integer [ ]) AS timeout_seconds -RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds -` - -type InsertWorkspaceAgentScriptsParams struct { - WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` - LogPath []string `db:"log_path" json:"log_path"` - Script []string `db:"script" json:"script"` - Cron []string `db:"cron" json:"cron"` - StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` - RunOnStart []bool `db:"run_on_start" json:"run_on_start"` - RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` - TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` -} - -func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, - arg.WorkspaceAgentID, - arg.CreatedAt, - pq.Array(arg.LogSourceID), - pq.Array(arg.LogPath), - pq.Array(arg.Script), - pq.Array(arg.Cron), - pq.Array(arg.StartBlocksLogin), - pq.Array(arg.RunOnStart), - pq.Array(arg.RunOnStop), - pq.Array(arg.TimeoutSeconds), - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one WITH workspaces_with_jobs AS ( SELECT @@ -10803,3 +10690,116 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C _, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID) return err } + +const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many +SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many +INSERT INTO + workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) +SELECT + $1 :: uuid AS workspace_agent_id, + $2 :: timestamptz AS created_at, + unnest($3 :: uuid [ ]) AS log_source_id, + unnest($4 :: text [ ]) AS log_path, + unnest($5 :: text [ ]) AS script, + unnest($6 :: text [ ]) AS cron, + unnest($7 :: boolean [ ]) AS start_blocks_login, + unnest($8 :: boolean [ ]) AS run_on_start, + unnest($9 :: boolean [ ]) AS run_on_stop, + unnest($10 :: integer [ ]) AS timeout_seconds +RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds +` + +type InsertWorkspaceAgentScriptsParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` + LogPath []string `db:"log_path" json:"log_path"` + Script []string `db:"script" json:"script"` + Cron []string `db:"cron" json:"cron"` + StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` + RunOnStart []bool `db:"run_on_start" json:"run_on_start"` + RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` + TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, + arg.WorkspaceAgentID, + arg.CreatedAt, + pq.Array(arg.LogSourceID), + pq.Array(arg.LogPath), + pq.Array(arg.Script), + pq.Array(arg.Cron), + pq.Array(arg.StartBlocksLogin), + pq.Array(arg.RunOnStart), + pq.Array(arg.RunOnStop), + pq.Array(arg.TimeoutSeconds), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index c1878b1daa9d0..c7da38bec58d1 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -8,19 +8,19 @@ 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
| -| AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| -| Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| -| 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
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| -| TemplateVersion
create, write |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
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_typetrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| -| Workspace
create, write, delete |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
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_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| -| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
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
| +| AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| +| Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| +| 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
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| +| TemplateVersion
create, write |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
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_typetrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| +| Workspace
create, write, delete |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
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_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
wildcard_hostnametrue
| diff --git a/docs/api/schemas.md b/docs/api/schemas.md index f90c37603eb10..7cfad1ec079e6 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1523,6 +1523,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "allow_user_autostart": true, "allow_user_autostop": true, "allow_user_cancel_workspace_jobs": true, + "autostart_requirement": { + "days_of_week": ["monday"] + }, "autostop_requirement": { "days_of_week": ["monday"], "weeks": 0 @@ -1543,23 +1546,24 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ### Properties -| Name | Type | Required | Restrictions | Description | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `allow_user_autostart` | boolean | false | | Allow user autostart allows users to set a schedule for autostarting their workspace. By default this is true. This can only be disabled when using an enterprise license. | -| `allow_user_autostop` | boolean | false | | Allow user autostop allows users to set a custom workspace TTL to use in place of the template's DefaultTTL field. By default this is true. If false, the DefaultTTL will always be used. This can only be disabled when using an enterprise license. | -| `allow_user_cancel_workspace_jobs` | boolean | false | | Allow users to cancel in-progress workspace jobs. \*bool as the default value is "true". | -| `autostop_requirement` | [codersdk.TemplateAutostopRequirement](#codersdktemplateautostoprequirement) | false | | Autostop requirement allows optionally specifying the autostop requirement for workspaces created from this template. This is an enterprise feature. | -| `default_ttl_ms` | integer | false | | Default ttl ms allows optionally specifying the default TTL for all workspaces created from this template. | -| `delete_ttl_ms` | integer | false | | Delete ttl ms allows optionally specifying the max lifetime before Coder permanently deletes dormant workspaces created from this template. | -| `description` | string | false | | Description is a description of what the template contains. It must be less than 128 bytes. | -| `disable_everyone_group_access` | boolean | false | | Disable everyone group access allows optionally disabling the default behavior of granting the 'everyone' group access to use the template. If this is set to true, the template will not be available to all users, and must be explicitly granted to users or groups in the permissions settings of the template. | -| `display_name` | string | false | | Display name is the displayed name of the template. | -| `dormant_ttl_ms` | integer | false | | Dormant ttl ms allows optionally specifying the max lifetime before Coder locks inactive workspaces created from this template. | -| `failure_ttl_ms` | integer | false | | Failure ttl ms allows optionally specifying the max lifetime before Coder stops all resources for failed workspaces created from this template. | -| `icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | -| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured | -| `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. | +| Name | Type | Required | Restrictions | Description | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `allow_user_autostart` | boolean | false | | Allow user autostart allows users to set a schedule for autostarting their workspace. By default this is true. This can only be disabled when using an enterprise license. | +| `allow_user_autostop` | boolean | false | | Allow user autostop allows users to set a custom workspace TTL to use in place of the template's DefaultTTL field. By default this is true. If false, the DefaultTTL will always be used. This can only be disabled when using an enterprise license. | +| `allow_user_cancel_workspace_jobs` | boolean | false | | Allow users to cancel in-progress workspace jobs. \*bool as the default value is "true". | +| `autostart_requirement` | [codersdk.TemplateAutostartRequirement](#codersdktemplateautostartrequirement) | false | | Autostart requirement allows optionally specifying the autostart allowed days for workspaces created from this template. This is an enterprise feature. | +| `autostop_requirement` | [codersdk.TemplateAutostopRequirement](#codersdktemplateautostoprequirement) | false | | Autostop requirement allows optionally specifying the autostop requirement for workspaces created from this template. This is an enterprise feature. | +| `default_ttl_ms` | integer | false | | Default ttl ms allows optionally specifying the default TTL for all workspaces created from this template. | +| `delete_ttl_ms` | integer | false | | Delete ttl ms allows optionally specifying the max lifetime before Coder permanently deletes dormant workspaces created from this template. | +| `description` | string | false | | Description is a description of what the template contains. It must be less than 128 bytes. | +| `disable_everyone_group_access` | boolean | false | | Disable everyone group access allows optionally disabling the default behavior of granting the 'everyone' group access to use the template. If this is set to true, the template will not be available to all users, and must be explicitly granted to users or groups in the permissions settings of the template. | +| `display_name` | string | false | | Display name is the displayed name of the template. | +| `dormant_ttl_ms` | integer | false | | Dormant ttl ms allows optionally specifying the max lifetime before Coder locks inactive workspaces created from this template. | +| `failure_ttl_ms` | integer | false | | Failure ttl ms allows optionally specifying the max lifetime before Coder stops all resources for failed workspaces created from this template. | +| `icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | +| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured | +| `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. | | This is required on creation to enable a user-flow of validating a template works. There is no reason the data-model cannot support empty templates, but it doesn't make sense for users. | ## codersdk.CreateTemplateVersionDryRunRequest @@ -4292,6 +4296,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "allow_user_autostart": true, "allow_user_autostop": true, "allow_user_cancel_workspace_jobs": true, + "autostart_requirement": { + "days_of_week": ["monday"] + }, "autostop_requirement": { "days_of_week": ["monday"], "weeks": 0 @@ -4327,31 +4334,32 @@ 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 | | | -| `autostop_requirement` | [codersdk.TemplateAutostopRequirement](#codersdktemplateautostoprequirement) | false | | Autostop requirement is an enterprise feature. Its value is only used if your license is entitled to use the advanced template scheduling feature. | -| `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 TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis 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 | | | -| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `provisioner` | string | false | | | -| `time_til_dormant_autodelete_ms` | integer | false | | | -| `time_til_dormant_ms` | integer | 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 | | | +| `autostart_requirement` | [codersdk.TemplateAutostartRequirement](#codersdktemplateautostartrequirement) | false | | | +| `autostop_requirement` | [codersdk.TemplateAutostopRequirement](#codersdktemplateautostoprequirement) | false | | Autostop requirement and AutostartRequirement are enterprise features. Its value is only used if your license is entitled to use the advanced template scheduling feature. | +| `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 TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis 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 | | | +| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `provisioner` | string | false | | | +| `time_til_dormant_autodelete_ms` | integer | false | | | +| `time_til_dormant_ms` | integer | false | | | +| `updated_at` | string | false | | | #### Enumerated Values @@ -4398,6 +4406,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `builtin` | | `app` | +## codersdk.TemplateAutostartRequirement + +```json +{ + "days_of_week": ["monday"] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------------- | --------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| `days_of_week` | array of string | false | | Days of week is a list of days of the week in which autostart is allowed to happen. If no days are specified, autostart is not allowed. | + ## codersdk.TemplateAutostopRequirement ```json diff --git a/docs/api/templates.md b/docs/api/templates.md index 6fa7270e37936..a6cc2d4b5a367 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -31,6 +31,9 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "allow_user_autostart": true, "allow_user_autostop": true, "allow_user_cancel_workspace_jobs": true, + "autostart_requirement": { + "days_of_week": ["monday"] + }, "autostop_requirement": { "days_of_week": ["monday"], "weeks": 0 @@ -75,38 +78,40 @@ 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 | | | -| `» autostop_requirement` | [codersdk.TemplateAutostopRequirement](schemas.md#codersdktemplateautostoprequirement) | false | | Autostop requirement is an enterprise feature. Its value is only used if your license is entitled to use the advanced template scheduling feature. | -| `»» days_of_week` | array | false | | Days of week is a list of days of the week on which restarts are required. Restarts happen within the user's quiet hours (in their configured timezone). If no days are specified, restarts are not required. Weekdays cannot be specified twice. | +| 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 | | | +| `» autostart_requirement` | [codersdk.TemplateAutostartRequirement](schemas.md#codersdktemplateautostartrequirement) | false | | | +| `»» days_of_week` | array | false | | Days of week is a list of days of the week in which autostart is allowed to happen. If no days are specified, autostart is not allowed. | +| `» autostop_requirement` | [codersdk.TemplateAutostopRequirement](schemas.md#codersdktemplateautostoprequirement) | false | | Autostop requirement and AutostartRequirement are enterprise features. Its value is only used if your license is entitled to use the advanced template scheduling feature. | +| `»» days_of_week` | array | false | | Days of week is a list of days of the week on which restarts are required. Restarts happen within the user's quiet hours (in their configured timezone). If no days are specified, restarts are not required. Weekdays cannot be specified twice. | | Restarts will only happen on weekdays in this list on weeks which line up with Weeks. | -| `»» weeks` | integer | false | | Weeks is the number of weeks between required restarts. Weeks are synced across all workspaces (and Coder deployments) using modulo math on a hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023). Values of 0 or 1 indicate weekly restarts. Values of 2 indicate fortnightly restarts, etc. | -| `» 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 TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis 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 | | | -| `» max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» provisioner` | string | false | | | -| `» time_til_dormant_autodelete_ms` | integer | false | | | -| `» time_til_dormant_ms` | integer | false | | | -| `» updated_at` | string(date-time) | false | | | +| `»» weeks` | integer | false | | Weeks is the number of weeks between required restarts. Weeks are synced across all workspaces (and Coder deployments) using modulo math on a hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023). Values of 0 or 1 indicate weekly restarts. Values of 2 indicate fortnightly restarts, etc. | +| `» 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 TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis 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 | | | +| `» max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» provisioner` | string | false | | | +| `» time_til_dormant_autodelete_ms` | integer | false | | | +| `» time_til_dormant_ms` | integer | false | | | +| `» updated_at` | string(date-time) | false | | | #### Enumerated Values @@ -137,6 +142,9 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "allow_user_autostart": true, "allow_user_autostop": true, "allow_user_cancel_workspace_jobs": true, + "autostart_requirement": { + "days_of_week": ["monday"] + }, "autostop_requirement": { "days_of_week": ["monday"], "weeks": 0 @@ -173,6 +181,9 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "allow_user_autostart": true, "allow_user_autostop": true, "allow_user_cancel_workspace_jobs": true, + "autostart_requirement": { + "days_of_week": ["monday"] + }, "autostop_requirement": { "days_of_week": ["monday"], "weeks": 0 @@ -305,6 +316,9 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "allow_user_autostart": true, "allow_user_autostop": true, "allow_user_cancel_workspace_jobs": true, + "autostart_requirement": { + "days_of_week": ["monday"] + }, "autostop_requirement": { "days_of_week": ["monday"], "weeks": 0 @@ -613,6 +627,9 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \ "allow_user_autostart": true, "allow_user_autostop": true, "allow_user_cancel_workspace_jobs": true, + "autostart_requirement": { + "days_of_week": ["monday"] + }, "autostop_requirement": { "days_of_week": ["monday"], "weeks": 0 @@ -728,6 +745,9 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \ "allow_user_autostart": true, "allow_user_autostop": true, "allow_user_cancel_workspace_jobs": true, + "autostart_requirement": { + "days_of_week": ["monday"] + }, "autostop_requirement": { "days_of_week": ["monday"], "weeks": 0 From 7812a9dab8a5d6eb9f7de9bc6f490f344ff3de19 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 12 Oct 2023 13:39:27 -0500 Subject: [PATCH 07/14] Add unit test for autostart enabled days --- .../lifecycle_executor_internal_test.go | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 coderd/autobuild/lifecycle_executor_internal_test.go diff --git a/coderd/autobuild/lifecycle_executor_internal_test.go b/coderd/autobuild/lifecycle_executor_internal_test.go new file mode 100644 index 0000000000000..a8d452c6477d4 --- /dev/null +++ b/coderd/autobuild/lifecycle_executor_internal_test.go @@ -0,0 +1,133 @@ +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, + }, + } + + 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") + }) + } +} From 18c709301b8e5749134adcf274ac8d0fa9e3922b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 12 Oct 2023 14:36:24 -0500 Subject: [PATCH 08/14] Linting + test --- coderd/autobuild/lifecycle_executor_test.go | 7 +++++++ provisionersdk/proto/provisioner.pb.go | 3 +++ 2 files changed, 10 insertions(+) diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go index 6d7c61bf59cf2..2ab9bc9662a8d 100644 --- a/coderd/autobuild/lifecycle_executor_test.go +++ b/coderd/autobuild/lifecycle_executor_test.go @@ -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) { diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 844b619ce1a1e..271801463d426 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -836,6 +836,7 @@ type Agent struct { Directory string `protobuf:"bytes,7,opt,name=directory,proto3" json:"directory,omitempty"` Apps []*App `protobuf:"bytes,8,rep,name=apps,proto3" json:"apps,omitempty"` // Types that are assignable to Auth: + // // *Agent_Token // *Agent_InstanceId Auth isAgent_Auth `protobuf_oneof:"auth"` @@ -2106,6 +2107,7 @@ type Request struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Type: + // // *Request_Config // *Request_Parse // *Request_Plan @@ -2228,6 +2230,7 @@ type Response struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Type: + // // *Response_Log // *Response_Parse // *Response_Plan From 352058bf533a2b0dfe1c13a930ffc15ae8d13fe8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 12 Oct 2023 14:39:24 -0500 Subject: [PATCH 09/14] Add auto start to template edit --- cli/templateedit.go | 56 +++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/cli/templateedit.go b/cli/templateedit.go index 1a62ec531d3af..ba079f99f7dd0 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -17,19 +17,20 @@ import ( func (r *RootCmd) templateEdit() *clibase.Cmd { var ( - name string - displayName string - description string - icon string - defaultTTL time.Duration - maxTTL time.Duration - autostopRequirementDaysOfWeek []string - autostopRequirementWeeks int64 - failureTTL time.Duration - inactivityTTL time.Duration - allowUserCancelWorkspaceJobs bool - allowUserAutostart bool - allowUserAutostop bool + name string + displayName string + description string + icon string + defaultTTL time.Duration + maxTTL time.Duration + autostopRequirementDaysOfWeek []string + autostopRequirementWeeks int64 + autostartRequirementDaysOfWeek []string + failureTTL time.Duration + inactivityTTL time.Duration + allowUserCancelWorkspaceJobs bool + allowUserAutostart bool + allowUserAutostop bool ) client := new(codersdk.Client) @@ -48,7 +49,9 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { !allowUserAutostop || maxTTL != 0 || failureTTL != 0 || - inactivityTTL != 0 + inactivityTTL != 0 || + len(autostartRequirementDaysOfWeek) > 0 + if requiresEntitlement { entitlements, err := client.Entitlements(inv.Context()) var sdkErr *codersdk.Error @@ -77,6 +80,12 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { if len(autostopRequirementDaysOfWeek) == 0 { autostopRequirementDaysOfWeek = template.AutostopRequirement.DaysOfWeek } + if len(autostartRequirementDaysOfWeek) == 1 && autostartRequirementDaysOfWeek[0] == "all" { + // Set it to every day of the week + autostartRequirementDaysOfWeek = []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"} + } else if len(autostartRequirementDaysOfWeek) == 0 { + autostartRequirementDaysOfWeek = template.AutostartRequirement.DaysOfWeek + } if unsetAutostopRequirementDaysOfWeek { autostopRequirementDaysOfWeek = []string{} } @@ -93,6 +102,9 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { DaysOfWeek: autostopRequirementDaysOfWeek, Weeks: autostopRequirementWeeks, }, + AutostartRequirement: &codersdk.TemplateAutostartRequirement{ + DaysOfWeek: autostartRequirementDaysOfWeek, + }, FailureTTLMillis: failureTTL.Milliseconds(), TimeTilDormantMillis: inactivityTTL.Milliseconds(), AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs, @@ -140,6 +152,22 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting, regardless of user activity. This is an enterprise-only feature. Maps to \"Max lifetime\" in the UI.", Value: clibase.DurationOf(&maxTTL), }, + { + Flag: "autostart-requirement-weekdays", + // workspaces created from this template must be restarted on the given weekdays. To unset this value for the template (and disable the autostop requirement for the template), pass 'none'. + Description: "Edit the template autostart requirement weekdays - workspaces created from this template can only autostart on the given weekdays. To unset this value for the template (and allow autostart on all days), pass 'all'.", + Value: clibase.Validate(clibase.StringArrayOf(&autostartRequirementDaysOfWeek), func(value *clibase.StringArray) error { + v := value.GetSlice() + if len(v) == 1 && v[0] == "all" { + return nil + } + _, err := codersdk.WeekdaysToBitmap(v) + if err != nil { + return xerrors.Errorf("invalid autostart requirement days of week %q: %w", strings.Join(v, ","), err) + } + return nil + }), + }, { Flag: "autostop-requirement-weekdays", Description: "Edit the template autostop requirement weekdays - workspaces created from this template must be restarted on the given weekdays. To unset this value for the template (and disable the autostop requirement for the template), pass 'none'.", From 4fdc57c86f4d495c056a496b43b79723199d19ad Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 12 Oct 2023 14:55:16 -0500 Subject: [PATCH 10/14] Add unit tests --- cli/templateedit_test.go | 9 ++++- enterprise/coderd/templates_test.go | 52 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go index 57aaf94ef45b8..358b7c72c6f70 100644 --- a/cli/templateedit_test.go +++ b/cli/templateedit_test.go @@ -248,7 +248,7 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, "", updated.Icon) assert.Equal(t, "", updated.DisplayName) }) - t.Run("AutostopRequirement", func(t *testing.T) { + t.Run("Autostop/startRequirement", func(t *testing.T) { t.Parallel() t.Run("BlockedAGPL", func(t *testing.T) { t.Parallel() @@ -286,6 +286,12 @@ func TestTemplateEdit(t *testing.T) { "--autostop-requirement-weeks", "1", }, }, + { + name: "AutostartDays", + flags: []string{ + "--autostart-requirement-weekdays", "monday", + }, + }, } for _, c := range cases { @@ -321,6 +327,7 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, template.AutostopRequirement.DaysOfWeek, updated.AutostopRequirement.DaysOfWeek) assert.Equal(t, template.AutostopRequirement.Weeks, updated.AutostopRequirement.Weeks) + assert.Equal(t, template.AutostartRequirement.DaysOfWeek, updated.AutostartRequirement.DaysOfWeek) }) } }) diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index dae642349af70..938dfe92a9ea0 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -140,6 +140,58 @@ func TestTemplates(t *testing.T) { require.EqualValues(t, exp, *ws.TTLMillis) }) + t.Run("SetAutostartRequirement", func(t *testing.T) { + t.Parallel() + + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAdvancedTemplateScheduling: 1, + }, + }, + }) + + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + require.Equal(t, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, template.AutostartRequirement.DaysOfWeek) + + // ctx := testutil.Context(t, testutil.WaitLong) + ctx := context.Background() + updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + Name: template.Name, + DisplayName: template.DisplayName, + Description: template.Description, + Icon: template.Icon, + AutostartRequirement: &codersdk.TemplateAutostartRequirement{ + DaysOfWeek: []string{"monday", "saturday"}, + }, + }) + require.NoError(t, err) + require.Equal(t, []string{"monday", "saturday"}, updated.AutostartRequirement.DaysOfWeek) + + template, err = client.Template(ctx, template.ID) + require.NoError(t, err) + require.Equal(t, []string{"monday", "saturday"}, template.AutostartRequirement.DaysOfWeek) + + // Ensure a missing field is a noop + updated, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + Name: template.Name, + DisplayName: template.DisplayName, + Description: template.Description, + Icon: template.Icon + "something", + }) + require.NoError(t, err) + require.Equal(t, []string{"monday", "saturday"}, updated.AutostartRequirement.DaysOfWeek) + + template, err = client.Template(ctx, template.ID) + require.NoError(t, err) + require.Equal(t, []string{"monday", "saturday"}, template.AutostartRequirement.DaysOfWeek) + }) + t.Run("SetAutostopRequirement", func(t *testing.T) { t.Parallel() From 78257b8d3c28be47058052df3720071800283b11 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 12 Oct 2023 20:15:32 +0000 Subject: [PATCH 11/14] Make gen --- cli/testdata/coder_templates_edit_--help.golden | 6 ++++++ docs/cli/templates_edit.md | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/cli/testdata/coder_templates_edit_--help.golden b/cli/testdata/coder_templates_edit_--help.golden index b184757965b77..d86be791db616 100644 --- a/cli/testdata/coder_templates_edit_--help.golden +++ b/cli/testdata/coder_templates_edit_--help.golden @@ -17,6 +17,12 @@ OPTIONS: --allow-user-cancel-workspace-jobs bool (default: true) Allow users to cancel in-progress workspace jobs. + --autostart-requirement-weekdays string-array + Edit the template autostart requirement weekdays - workspaces created + from this template can only autostart on the given weekdays. To unset + this value for the template (and allow autostart on all days), pass + 'all'. + --default-ttl duration Edit the template default time before shutdown - workspaces created from this template default to this value. Maps to "Default autostop" diff --git a/docs/cli/templates_edit.md b/docs/cli/templates_edit.md index 79f4ec0ba29f6..b58d1f61fc806 100644 --- a/docs/cli/templates_edit.md +++ b/docs/cli/templates_edit.md @@ -39,6 +39,14 @@ Allow users to customize the autostop TTL for workspaces on this template. This Allow users to cancel in-progress workspace jobs. +### --autostart-requirement-weekdays + +| | | +| ---- | ------------------------- | +| Type | string-array | + +Edit the template autostart requirement weekdays - workspaces created from this template can only autostart on the given weekdays. To unset this value for the template (and allow autostart on all days), pass 'all'. + ### --default-ttl | | | From db25e8456f577903bd4ed01846880a114a96960d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 13 Oct 2023 10:52:02 -0500 Subject: [PATCH 12/14] Add unit test to test blocking autostart with DaysOfWeek --- coderd/schedule/template.go | 2 +- enterprise/coderd/templates_test.go | 3 +- enterprise/coderd/workspaces_test.go | 66 ++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/coderd/schedule/template.go b/coderd/schedule/template.go index 94da8566c87c9..fc274d9a7d8ba 100644 --- a/coderd/schedule/template.go +++ b/coderd/schedule/template.go @@ -37,7 +37,7 @@ var DaysOfWeek = []time.Weekday{ type TemplateAutostartRequirement struct { // DaysOfWeek is a bitmap of which days of the week the workspace is allowed - // to be restarted. If fully zero, the workspace is not allowed to be restarted. + // 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 diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 938dfe92a9ea0..05bc26d66dace 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -159,8 +159,7 @@ func TestTemplates(t *testing.T) { template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.Equal(t, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, template.AutostartRequirement.DaysOfWeek) - // ctx := testutil.Context(t, testutil.WaitLong) - ctx := context.Background() + ctx := testutil.Context(t, testutil.WaitLong) updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index cd7bec21f281b..e38d1874e764d 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -736,6 +736,65 @@ func TestWorkspaceAutobuild(t *testing.T) { }) } +// Blocked by autostart requirements +func TestExecutorAutostartBlocked(t *testing.T) { + t.Parallel() + + now := time.Now() + var allowed []string + for _, day := range agplschedule.DaysOfWeek { + // Skip the day the workspace was created on and if the next day is within 2 + // hours, skip that too. The cron scheduler will start the workspace every hour, + // so it can span into the next day. + if day != now.UTC().Weekday() && + day != now.UTC().Add(time.Hour*2).Weekday() { + allowed = append(allowed, day.String()) + } + } + + var ( + sched = must(cron.Weekly("CRON_TZ=UTC 0 * * * *")) + tickCh = make(chan time.Time) + statsCh = make(chan autobuild.Stats) + client, owner = coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()), + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1}, + }, + }) + version = coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + template = coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) { + request.AutostartRequirement = &codersdk.TemplateAutostartRequirement{ + DaysOfWeek: allowed, + } + }) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + workspace = coderdtest.CreateWorkspace(t, client, owner.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) { + cwr.AutostartSchedule = ptr.Ref(sched.String()) + }) + _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + ) + + // Given: workspace is stopped + workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) + + // When: the autobuild executor ticks way into the future + go func() { + tickCh <- workspace.LatestBuild.CreatedAt.Add(24 * time.Hour) + close(tickCh) + }() + + // Then: the workspace should not be started. + stats := <-statsCh + require.NoError(t, stats.Error) + require.Len(t, stats.Transitions, 0) +} + func TestWorkspacesFiltering(t *testing.T) { t.Parallel() @@ -911,3 +970,10 @@ func TestWorkspaceLock(t *testing.T) { require.True(t, workspace.LastUsedAt.After(lastUsedAt)) }) } + +func must[T any](value T, err error) T { + if err != nil { + panic(err) + } + return value +} From 25c3b1ba1c28408190b2773781c8f87a04accff4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 13 Oct 2023 10:55:13 -0500 Subject: [PATCH 13/14] Add assertions to existing tests --- cli/templateedit_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go index 358b7c72c6f70..cf286adacf427 100644 --- a/cli/templateedit_test.go +++ b/cli/templateedit_test.go @@ -328,6 +328,7 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, template.AutostopRequirement.DaysOfWeek, updated.AutostopRequirement.DaysOfWeek) assert.Equal(t, template.AutostopRequirement.Weeks, updated.AutostopRequirement.Weeks) assert.Equal(t, template.AutostartRequirement.DaysOfWeek, updated.AutostartRequirement.DaysOfWeek) + assert.Equal(t, template.AutostartRequirement.DaysOfWeek, updated.AutostartRequirement.DaysOfWeek) }) } }) @@ -443,6 +444,7 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, template.AutostopRequirement.DaysOfWeek, updated.AutostopRequirement.DaysOfWeek) assert.Equal(t, template.AutostopRequirement.Weeks, updated.AutostopRequirement.Weeks) + assert.Equal(t, template.AutostartRequirement.DaysOfWeek, updated.AutostartRequirement.DaysOfWeek) }) } }) @@ -543,6 +545,7 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, template.AutostopRequirement.DaysOfWeek, updated.AutostopRequirement.DaysOfWeek) assert.Equal(t, template.AutostopRequirement.Weeks, updated.AutostopRequirement.Weeks) + assert.Equal(t, template.AutostartRequirement.DaysOfWeek, updated.AutostartRequirement.DaysOfWeek) }) }) // TODO(@dean): remove this test when we remove max_ttl @@ -815,6 +818,7 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, template.AutostopRequirement.DaysOfWeek, updated.AutostopRequirement.DaysOfWeek) assert.Equal(t, template.AutostopRequirement.Weeks, updated.AutostopRequirement.Weeks) + assert.Equal(t, template.AutostartRequirement.DaysOfWeek, updated.AutostartRequirement.DaysOfWeek) assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart) assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop) assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis) @@ -910,6 +914,7 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, template.AutostopRequirement.DaysOfWeek, updated.AutostopRequirement.DaysOfWeek) assert.Equal(t, template.AutostopRequirement.Weeks, updated.AutostopRequirement.Weeks) + assert.Equal(t, template.AutostartRequirement.DaysOfWeek, updated.AutostartRequirement.DaysOfWeek) assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart) assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop) assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis) @@ -1009,6 +1014,7 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, template.AutostopRequirement.DaysOfWeek, updated.AutostopRequirement.DaysOfWeek) assert.Equal(t, template.AutostopRequirement.Weeks, updated.AutostopRequirement.Weeks) + assert.Equal(t, template.AutostartRequirement.DaysOfWeek, updated.AutostartRequirement.DaysOfWeek) assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart) assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop) assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis) From 51b803135e090b10c44ac78c72e68d5fa021d24f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 13 Oct 2023 11:14:31 -0500 Subject: [PATCH 14/14] Add a test for invalid input --- .../lifecycle_executor_internal_test.go | 13 ++++++++ enterprise/coderd/templates_test.go | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/coderd/autobuild/lifecycle_executor_internal_test.go b/coderd/autobuild/lifecycle_executor_internal_test.go index a8d452c6477d4..33d3143fac19d 100644 --- a/coderd/autobuild/lifecycle_executor_internal_test.go +++ b/coderd/autobuild/lifecycle_executor_internal_test.go @@ -119,6 +119,19 @@ func Test_isEligibleForAutostart(t *testing.T) { 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 { diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 05bc26d66dace..50bd6ecf0798b 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -191,6 +191,38 @@ func TestTemplates(t *testing.T) { require.Equal(t, []string{"monday", "saturday"}, template.AutostartRequirement.DaysOfWeek) }) + t.Run("SetInvalidAutostartRequirement", func(t *testing.T) { + t.Parallel() + + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAdvancedTemplateScheduling: 1, + }, + }, + }) + + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + require.Equal(t, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, template.AutostartRequirement.DaysOfWeek) + + ctx := testutil.Context(t, testutil.WaitLong) + _, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + Name: template.Name, + DisplayName: template.DisplayName, + Description: template.Description, + Icon: template.Icon, + AutostartRequirement: &codersdk.TemplateAutostartRequirement{ + DaysOfWeek: []string{"foobar", "saturday"}, + }, + }) + require.Error(t, err) + }) + t.Run("SetAutostopRequirement", func(t *testing.T) { t.Parallel()